一个绝妙的内核 exploit

最近 Linux 内核爆出了一个严重的安全漏洞,非root用户可以通过该漏洞的 exploit 获取root权限。这并不罕见,值得一提的是这个补丁看起来如此平常以至于我们绝大多数人都不会以为这是安全问题。

先看这个问题的补丁,就是下面这个:

  static int perf_swevent_init(struct perf_event *event)
 {
-    int event_id = event->attr.config;
+    u64 event_id = event->attr.config;

     if (event->attr.type != PERF_TYPE_SOFTWARE)
         return -ENOENT;
我们第一眼的感觉就是这大概只是修复了编译器报的一个小警告吧,怎么会引起如此严重的安全问题呢? 在没打补丁的代码中 event_id 是个**带符号**的整型,而且就在下面不远处的两行代码中只检查了其上界: [c] if (event_id >= PERF_COUNT_SW_MAX) return -ENOENT; [/c] 而如果传递进来的 event->attr.config 值正好设置了符号位,那么 event_id 就会变成负值,而且能躲过上面的检查。 负值意味着什么呢?再继续看后面的代码: [c] if (!event->parent) { int err; err = swevent_hlist_get(event); if (err) return err; atomic_inc(&perf_swevent_enabled[event_id]); event->destroy = sw_perf_event_destroy; } [/c] 意味着数组越界!这时你应该身上开始冒冷汗了。继续,数组 perf_swevent_enabled[] 在 RHEL6 上的定义是: [c] atomic_t perf_swevent_enabled[PERF_COUNT_SW_MAX]; [/c] 而 atomic_t 基本上就是int,也就是说 perf_swevent_enabled[] 是整型数组,那么用 event_id 访问该数组时会把 event_id 的值乘以4再加上数组的起始地址。很简单哈! 好,通过 System.map 文件我们可以得到 perf_swevent_enabled 的地址:
ffffffff81f360c0 B perf_swevent_enabled

那么当 event->attr.config == 0xffffffff (即有符号的-1)时,在 x86_64 上面我们最终会得到:

0xffffffffffffffff * 4 + 0xffffffff81f360c0 == 0xFFFFFFFF81F360BC

同理,当 event->attr.config == 0xfffffffe 时我们得到:

0xfffffffffffffffe * 4 + 0xffffffff81f360c0 == 0xFFFFFFFF81F360B8

所以上述的 atomic_inc() 其实增加的是前面两个地址中存放的值,而这俩地址都指向内核空间(参见 Documentation/x86/x86_64/mm.txt)!这时你应该感到紧张了。。。

后面更有趣的事情发生在 sw_perf_event_destroy() 函数中,它是在 perf_event_open() 返回的 fd 被关闭时被调用,RHEL6 上其定义如下:

[c]
static void sw_perf_event_destroy(struct perf_event *event)
{
u64 event_id = event->attr.config;

    WARN_ON(event->parent);

    atomic_dec(&perf_swevent_enabled[event_id]);
    swevent_hlist_put(event);

}
[/c]

很明显的不同是,event_id 这次是无符号的类型。那么,同上,当 event->attr.config == 0xffffffff 时我们得到:

0xffffffff * 4 + 0xffffffff81f360c0 == 0x0000000381F360BC

当 event->attr.config == 0xfffffffe 时我们得到:

0xfffffffe * 4 + 0xffffffff81f360c0 == 0x0000000381F360B8

所以这里的 atomic_dec() 实际上减小的是用户空间地址内的值。

上面是“基础知识”,带着这些知识我们看 exploit 代码究竟做了什么,代码片段如下:

[c]

define BASE 0x380000000

define SIZE 0x010000000

assert((map = mmap((void)BASE, SIZE, 3, 0x32, 0,0)) == (void)BASE);
memset(map, 0, SIZE);
sheep(-1); sheep(-2); // sheep will just invoke perf_event_open
// syscall with attr.config set to the param
for (i = 0; i < SIZE/4; i++) if (map[i]) {
assert(map[i+1]);
break;
}
[/c]

它首先会 mmap() 起始地址是 0x380000000 的一块内存区域。然后分别以 attr.config 为 -1 和 -2 调用两次 perf_event_open()。根据前面的计算,它实际上分别增加了 0xFFFFFFFF81F360BC 和 0xFFFFFFFF81F360B8 两处内存的值,减少了 0x0000000381F360BC 和 0x0000000381F360B8 的值。后面的 for 循环则是找出被减少的内存地址,这样一来也就可以算出 perf_swevent_enabled[] 数组的地址(System.map 并不总是存在,如果存在而且可读我们当然可以直接去读这个值)。

知道这个地址我们就可以操纵内核中某处的32bit的值,把其值加一。正因为如此,作者巧妙地选择了中断描述符表——一个16字节描述符的数组,它的地址可以通过 sidt 指令获取。它其中的描述符结构定义如下:

Offset     Size     Description
0     2     Offset low bits (0..15)
2     2     Selector (Code segment selector)
4     1     Zero
5     1     Type and Attributes (same as before)
6     2     Offset middle bits (16..31)
8     4     Offset high bits (32..63)
12     4     Zero

这里最有趣的是 offset 为8 的地方,在 x86_64 上面其值为 0xffffffff。作者选择的中断描述符是 0x4,所以相对于中断描述符表它的偏移实际上是 0x48。现在的任务就成了通过 perf_swevent_enabled[] 来计算出该中断描述符中偏移为8的内存地址,并对其加一!下面的代码就是做的这个工作:

[c]
sheep(-i + (((idt.addr&0xffffffff)-0x80000000)/4) + 16);
[/c]

i 是我们前面在 for 循环中搜到的 perf_swevent_enabled[] 的一个偏移,idt.addr 是中断描述符表的绝对内核地址,取其低32位并减去 0x80000000 是为了得到低28位作为偏移,除以4是因为数组是int,最后加的16就是 0x4 中断描述符中的偏移(4已经除去了),所以最终sheep()里面的参数就是我们想要的偏移,这样以来内核就替我们把 0x4 中断描述符中的偏移为 8 的 0xffffffff 加上了1,也就成了0,也就成了用户空间的地址!所以后面的 int 0x4 其实就会跳转到用户空间早已经设置好的代码!!!

而这段代码比较生涩,但其意思就是更改当前进程的 uid/gid 为0来提升权限,所以最终取得一个有 root 权限的 shell!整个攻击大功告成!

注:上面的链接可能不能用,exploit 代码也可以在这里看到:https://gist.github.com/onemouth/5625174