📄 linux可加载内核模块完全版.htm
字号:
size=3>LKM</FONT>的开发人员可用如下的常规代码限制他们模块的输出符号:<FONT size=3></P>
<P align=justify>static struct symbol_table module_syms= {
/*</FONT>定义自己的符号表<FONT size=3>*/</P>
<P align=justify>#include <linux/symtab_begin.h>
/*</FONT>我们想要输出的符号,我们真想么?<FONT size=3>*/</P>
<P align=justify>... </P>
<P align=justify>};</P>
<P align=justify> </P>
<P align=justify>register_symtab(&module_syms); /*</FONT>做实际的注册工作<FONT
size=3>*/</FONT></P>
<P align=justify> </P>
<P align=justify>正如我所说,我们不想输出任何符号为公共符号,所以我们用如下构造函数:<FONT size=3></P>
<P align=justify>register_symtab(NULL);</FONT></P>
<P align=justify>这一行必须插入到<FONT size=3>init_module()</FONT>函数中,记住这一点!</P>
<P align=justify><FONT size=4> </P>
<P align=justify>4.</FONT><FONT face=宋体
size=4>如何进行内核与用户空间内存数据的交换</FONT></P>
<P
align=justify>到目前为止本文非常基本非常容易。现在我们来点难的(但提高不多)。在内核空间编程有很多好处,但也有很多不足。系统调用从用户空间获得参数(系统调用在一些封装程序如<FONT
size=3>libc</FONT>中实现),但我们的<FONT size=3>LKM</FONT>运行在内核空间。在节<FONT
size=3>II</FONT>中你会看到检查某个系统调用的参数非常重要,因为要根据参数决定对策。但我们怎么才能在工作于内核空间的模块中访问用户空间中的参数呢?</P>
<P align=justify>解决办法:我们必须进行传送。</P>
<P align=justify>对非利用内核入侵的黑客来说有点奇怪,但也非常容易。看下面的系统调用:<FONT size=3></P>
<P align=justify>int sys_chdir (const char *path)</FONT></P>
<P align=justify>想象一下系统调用它,我们截获了调用(将在节<FONT
size=3>II</FONT>中讲到)。我们想检查一下用户想设置的路径,所以我们必须访问<FONT size=3>char
*path</FONT>。如果你试着象下面那样直接访问<FONT size=3>path</FONT>变量<FONT size=3></P>
<P align=justify>printk("<1>%s\n", path);</FONT></P>
<P align=justify>就一定会出问题。</P>
<P align=justify>记住你是在内核空间,你不能轻易的读用户空间内存。在<FONT
size=3>Phrack52</FONT>你可得到<FONT
size=3>plaguez</FONT>的解决方法,专用于传送字符串。他用内核模式函数(宏)取回用户空间内存中的字节。<FONT
size=3></P>
<P align=justify>#include <asm/segment.h></P>
<P align=justify> </P>
<P align=justify>get_user(pointer);</FONT></P>
<P align=justify>给这个函数一个指针指向<FONT
size=3>*path</FONT>就可帮助我们从用户空间取到想要的东西到内核空间。看一下<FONT
size=3>plaguez</FONT>写的在用户空间到内核空间移动字符串的的程序:</P>
<P align=justify><FONT size=3> </P>
<P align=justify>char *strncpy_fromfs(char *dest, const char *src, int
n)</P>
<P align=justify>{</P>
<P align=justify>char *tmp = src;</P>
<P align=justify>int compt = 0;</P>
<P align=justify> </P>
<P align=justify>do {</P>
<P align=justify>dest[compt++] = __get_user(tmp++, 1);</P>
<P align=justify>}</P>
<P align=justify>while ((dest[compt - 1] != '\0') && (compt !=
n));</P>
<P align=justify> </P>
<P align=justify>return dest;</P>
<P align=justify>}</FONT></P>
<P align=justify>如果我们想转换<FONT size=3>*path</FONT>变量,我们可用如下内核代码:<FONT
size=3></P>
<P align=justify>char *kernel_space_path;</P>
<P align=justify> </P>
<P align=justify>kernel_space_path = (char *) kmalloc(100, GFP_KERNEL); /*
</FONT>在内核空间中分配内存<FONT size=3>*/</P>
<P align=justify>(void) strncpy_fromfs(test, path, 20); /*</FONT>调用<FONT
size=3>plaguez</FONT>写的函数<FONT size=3>*/</P>
<P align=justify>printk("<1>%s\n", kernel_space_path);
/*</FONT>现在我们可以使用任何想要的数据了<FONT size=3>*/</P>
<P align=justify>kfree(test); /*</FONT>想着释放内存<FONT size=3>*/</P>
<P align=justify></FONT> </P>
<P align=justify>上面的代码工作的非常好。一般性的传送太复杂;<FONT
size=3>plaguez</FONT>只用它来传送字符串(函数只用于字符串拷贝)。一般数据的传送可用如下函数简单实现:<FONT
size=3></P>
<P align=justify>#include <asm/segment.h></P>
<P align=justify>void memcpy_fromfs(void *to, const void *from, unsigned
long count);</FONT></P>
<P align=justify>两个函数显而易见基于同类命令,但第二个函数同<FONT
size=3>plaguez</FONT>新定义的函数几乎一样。我推荐用<FONT
size=3>memcpy_fromfs(...)</FONT>做一般数据传送,<FONT
size=3>plaguez</FONT>的前一个用于字符串拷贝。</P>
<P
align=justify>现在我们知道了如何把用户空间的内存转换到内核空间。但反向怎么办?这有点难,因为我们不容易在内核空间的位置定位用户空间。也许我们可以用如下方式处理转换:<FONT
size=3></P>
<P align=justify>#include <asm/segment.h></P>
<P align=justify>void memcpy_tofs(void *to, const void *from, unsigned
long count);</FONT></P>
<P align=justify>但如何在用户空间中定位<FONT size=3>*to</FONT>指针呢?<FONT
size=3>plaguez</FONT>在<FONT size=3>Phrack</FONT>一文中给出了最好的解决方法:<FONT
size=3></P>
<P align=justify>/*</FONT>我们需要<FONT size=3>brk</FONT>系统调用<FONT
size=3>*/</P>
<P align=justify>static inline _syscall1(int, brk, void *,
end_data_segment);</P>
<P align=justify> </P>
<P align=justify>...</P>
<P align=justify> </P>
<P align=justify>int ret, tmp;</P>
<P align=justify>char *truc = OLDEXEC;</P>
<P align=justify>char *nouveau = NEWEXEC;</P>
<P align=justify>unsigned long mmm;</P>
<P align=justify>mmm = current->mm->brk; /*</FONT>定位当前进程数据段大小<FONT
size=3>*/</P>
<P align=justify>ret = brk((void ) (mmm + 256));<B>
/</B>*</FONT>利用系统调用<FONT size=3>brk</FONT>为当前进程增加内存<FONT
size=3>256</FONT>个字节<FONT size=3>*/</P>
<P align=justify>if (ret < 0)</P>
<P align=justify>return ret; /*</FONT>分配不成功<FONT size=3>*/</P>
<P align=justify>memcpy_tofs((void *) (mmm + 2), nouveau, strlen(nouveau)
+ 1);</FONT></P>
<P align=justify>这里使用了一个非常高明的技巧。<FONT
size=3>Current</FONT>是指向当前进程任务结构的指针;<FONT
size=3>mm</FONT>是指向对应进程内存管理的数据结构<FONT size=3>mm_struct</FONT>的指针。通过用<FONT
size=3>brk</FONT>系统调用作用于<FONT
size=3>current->mm->brk</FONT>,我们可以增加未用数据段空间大小,同时我们知道分配内存就是处理数据段,所以通过增加未用空间大小,我们就为当前进程分配了一些内存。这块内存可用于将内核空间内存拷贝到用户空间(当前进程)。</P>
<P
align=justify>你可能想知道上面代码中第一行是做什么用的。这一行帮助我们使用在内核空间象调用函数一样使用用户空间。所有的用户空间函数对应一个<FONT
size=3>a_syscall(...)</FONT>形式的宏,<FONT size=3>
</FONT>所以我们可以构造一个系统调用宏对应用户空间的某个函数(通过系统调用对应);这里是针对<FONT
size=3>brk(..)</FONT>的。</P>
<P align=justify><FONT size=4> </P>
<P align=justify>5.</FONT><FONT face=宋体 size=4>使用用户空间的各种函数方法</FONT></P>
<P align=justify>你看到的在<FONT size=3>I.4</FONT>中我们用一系统调用宏来构造我们自己的<FONT
size=3>brk</FONT>调用,它很象我们所知的用户空间的<FONT
size=3>brk</FONT>。事实是用户空间的库函数(并非所有的)是通过这样的系统调用宏来实现的。下面的代码展示了用来构造我们在<FONT
size=3>I.4</FONT>中用的<FONT size=3>brk(...)</FONT>函数的<FONT
size=3>_syscall(...)</FONT>宏(取自<FONT size=3>/asm/unistd.h</FONT>)。</P>
<P align=justify><FONT size=3> </P>
<P align=justify>#define _syscall1(type,name,type1,arg1) \</P>
<P align=justify>type name(type1 arg1) \</P>
<P align=justify>{ \</P>
<P align=justify>long __res; \</P>
<P align=justify>__asm__ volatile ("int $0x80" \</P>
<P align=justify>: "=a" (__res) \</P>
<P align=justify>: "0" (__NR_##name),"b" ((long)(arg1))); \</P>
<P align=justify>if (__res >= 0) \</P>
<P align=justify>return (type) __res; \</P>
<P align=justify>errno = -__res; \</P>
<P align=justify>return -1; \</P>
<P align=justify>}</FONT></P>
<P align=justify>你无须了解这段代码的全部功能,它只是用<FONT
size=3>_syscall</FONT>的参数作为参数调用中断<FONT size=3>0x80</FONT>(见<FONT
size=3>I.2</FONT>)。<FONT size=3>name</FONT>是我们所需的系统调用(<FONT
size=3>name</FONT>被扩展为<FONT size=3>__NR_name</FONT>,在<FONT
size=3>/asm/unistd.h</FONT>中定义)。用这种办法我们实现了<FONT
size=3>brk</FONT>函数。其它带有不同个数参数的函数由其它宏实现<FONT
size=3>(_syscallX</FONT>,其中<FONT size=3>X</FONT>代表参数个数<FONT
size=3>)</FONT>。</P>
<P align=justify>我个人用其它方法实现函数;见下例:</P>
<P align=justify><FONT size=3> </P>
<P align=justify>int (*open)(char *, int, int); /*</FONT>声明原型<FONT
size=3>*/</P>
<P align=justify>open = sys_call_table[SYS_open]; /*</FONT>你也可以用<FONT
size=3>__NR_open*/</FONT></P>
<P align=justify> </P>
<P align=justify>用这种方法你无须用任何系统调用宏,你只用来自<FONT
size=3>sys_call_table</FONT>的函数指针就可以了。我曾在网上发现<FONT
size=3>SVAT</FONT>的著名<FONT
size=3>LKM</FONT>感染程序就是用的这种象函数一样构造用户空间的方法。我认为这是较好的解决办法,但你要自己判断和测试。</P>
<P align=justify>要注意为这些系统调用提供参数的时候,是来自用户空间而非你的内核空间。读<FONT
size=3>I.4</FONT>找把内核空间的数据传递到用户空间内存中的方法。</P>
<P align=justify>一个非常简单的做这些的方法是处理寄存器。你必须知道<FONT
size=3>Linux</FONT>用段选择器去区分内核空间、用户空间等等。从用户空间传给系统调用的参数位于数据段选择器限定的某个位置。<FONT
size=3>[</FONT>我在<FONT size=3>I.4</FONT>中没提到这些,因为它更适合本节。<FONT
size=3>]</FONT></P>
<P align=justify>从<FONT size=3>asm/segment.h</FONT>知<FONT
size=3>DS</FONT>可用<FONT
size=3>get_ds()</FONT>取回。所以系统调用中使用的参数数据可在内核空间中访问,只要我们把内核空间所用的段选择器的<FONT
size=3>DS</FONT>值设为用户段的值就可以了。这可用<FONT
size=3>set_fs(...)</FONT>实现。但要小心,你必须访问完系统调用的参数之后恢复<FONT
size=3>FS</FONT>。下面我们看一段有用的代码:</P>
<P align=justify>例如<FONT size=3>filename</FONT>在内核空间的我们刚建立的一个字符串,<FONT
size=3> </P>
<P align=justify>unsigned long old_fs_value=get_fs();</P>
<P align=justify> </P>
<P align=justify>set_fs(get_ds); /*</FONT>此后我们可以访问用户空间中数据<FONT
size=3>*/</P>
<P align=justify>open(filename, O_CREAT|O_RDWR|O_EXCL, 0640);</P>
<P align=justify>set_fs(old_fs_value); /*</FONT>恢复<FONT
size=3>fs...*/</FONT></P>
<P align=justify>我认为这是最简单<FONT
size=3>/</FONT>最快的解决问题的方法,但还需你自己测试。记住我在这里举的函数例子(<FONT
size=3>brk</FONT>,<FONT
size=3>open</FONT>)都是通过一个系统调用实现的。但也有很多用户空间函数是集成在一个系统调用里面的。看一下重要系统调用列表(<FONT
size=3>I.2</FONT>);例如,<FONT size=3>sys_socket</FONT>调用实现了所有关于<FONT
size=3>socket</FONT>的功能(创建、关闭、发送、接收<FONT
size=3>...</FONT>)。所以构造自己的函数是要小心,最好看一下内核源码。</P>
<P align=justify><FONT size=4> </P>
<P align=justify>6.</FONT><FONT face=宋体 size=4>常用内核空间函数列表</FONT></P>
<P align=justify>本文的开始我介绍了<FONT
size=3>printk(...)</FONT>函数,它是所有人都可在内核空间使用的,所以叫内核函数。内核开发人员需要很多通常只有通过库函数才能完成的复杂函数,这些函数被编制成内核函数。下面列出经常使用的最重要的内核函数:</P>
<P align=justify> </P>
<P align=center> </P>
<DIV align=center>
<CENTER>
<TABLE cellSpacing=1 cellPadding=7 width=568 border=1>
<TBODY>
<TR>
<TD vAlign=top width="60%">
<P align=center>函数<FONT size=3>/</FONT>宏</P></TD>
<TD vAlign=top width="40%">
<P align=center>描述</P></TD></TR>
<TR>
<TD vAlign=top width="60%"><FONT size=3>
<P align=justify>int sprintf (char *buf, const char *fmt, ...);</P>
<P align=justify>int vsprintf (char *buf, const char *fmt, va_list
args);</FONT></P></TD>
<TD vAlign=top width="40%">
<P align=justify>接收数据到字符串中的函数<FONT size=3> </FONT></P></TD></TR>
⌨️ 快捷键说明
复制代码
Ctrl + C
搜索代码
Ctrl + F
全屏模式
F11
切换主题
Ctrl + Shift + D
显示快捷键
?
增大字号
Ctrl + =
减小字号
Ctrl + -