Let_go
人生如棋,我愿为卒。行动虽慢,可谁曾见我后退一步。

CVE-2017-10661

2017/12/29 Android_Kernel

CVE-2018-10661


前言

测试内核

测试时使用的linux内核是MI 5C的内核(3.10.58-03548-gc670b5a)

漏洞描述

在4.10.15之前的linux内核中fs/timerfd.c中存在条件竞争漏洞,允许本地用户通过使用不合适的might_cancel列表的文件描述符操作获取特权或导致拒绝服务(list corruption or use-after-free)

Google修复链接

https://android.googlesource.com/kernel/goldfish/+/95cb006041c2f53060f4decffc7ef27f60aa1d39

存在漏洞的Nexus内核版本

Nexus 6P :Linux version 3.10.73-geac7d674 (android-build@wpef26.hot.corp.google.com) (gcc version 4.9.x-google 20140827 (prerelease) (GCC) ) #1 SMP PREEMPT Tue Dec 13 10:11:12 UTC 2016
Nexus 5X :Linux version 3.10.73-gf97f123 (android-build@wpiu4.hot.corp.google.com) (gcc version 4.9.x-google 20140827 (prerelease) (GCC) ) #1 SMP PREEMPT Mon Nov 2 20:10:58 UTC 2015

漏洞复现

红米4A_崩溃日志

[  259.810287] Unable to handle kernel paging request at virtual address dead000000000200
[  259.810331] pgd = ffffffc06b967000
[  259.810339] [dead000000000200] *pgd=00000000a1fb1003, *pud=00000000a1fb1003, *pmd=0000000000000000
[  259.810360] Internal error: Oops: 96000044 [#1] PREEMPT SMP
[  259.810367] Modules linked in: wlan(O)
[  259.810386] CPU: 2 PID: 6022 Comm: main Tainted: G        W  O   3.18.24-perf-gdcca0a6 #1
[  259.810394] Hardware name: Qualcomm Technologies, Inc. MSM8917-PMI8937 QRD SKU5 (DT)
[  259.810403] task: ffffffc053611900 ti: ffffffc035e48000 task.ti: ffffffc035e48000
[  259.810418] PC is at do_timerfd_settime+0x124/0x384
[  259.810427] LR is at do_timerfd_settime+0x118/0x384
[  259.810435] pc : [<ffffffc0001eb300>] lr : [<ffffffc0001eb2f4>] pstate: 80000145
[  259.810442] sp : ffffffc035e4bdd0
[  259.810448] x29: ffffffc035e4bdd0 x28: ffffffc035e48000
[  259.810461] x27: ffffffc035e4bea8 x26: 0000000000000056
[  259.810474] x25: ffffffc0111f3300 x24: ffffffc0111f3301
[  259.810486] x23: 0000000000000001 x22: ffffffc035e4be88
[  259.810498] x21: 0000000000000000 x20: ffffffc0015ebb90
[  259.810511] x19: ffffffc0111f3b00 x18: 0000000000000000
[  259.810523] x17: 0000000000000001 x16: ffffffc0001ebb54
[  259.810535] x15: 0000007f995ff838 x14: 000000002b63a2d4
[  259.810547] x13: ffffffffa5cce430 x12: 0000000000000000
[  259.810559] x11: 00000000041cdaeb x10: 00000000000f4240
[  259.810571] x9 : 00000000000003e8 x8 : 0000000000000056
[  259.810583] x7 : 0000000000000000 x6 : 0000010624dd2fb8
[  259.810594] x5 : 0000000000000000 x4 : ffffffc035e4bda0
[  259.810606] x3 : 0000000000000000 x2 : 0000000000000000
[  259.810617] x1 : ffffffc0111f3be0 x0 : dead000000000200
...
[  259.812135]
[  259.812143] Process main (pid: 6022, stack limit = 0xffffffc035e48058)
[  259.812149] Call trace:
[  259.812160] [<ffffffc0001eb300>] do_timerfd_settime+0x124/0x384
[  259.812170] [<ffffffc0001ebbe0>] SyS_timerfd_settime+0x8c/0x108
[  259.812181] Code: 9429cf30 f9407261 f9407660 f9000420 (f9000001)
[  259.812295] ---[ end trace 10b0993cfa7a40d0 ]---
[  259.874037] Kernel panic - not syncing: Fatal exception
[  259.874050] CPU3: stopping

MI 5C崩溃日志

[   72.679105] Unable to handle kernel paging request at virtual address 00200200
[   72.679130] pgd = ffffffc03603a000
[   72.679136] [00200200] *pgd=0000000054a8a003, *pmd=0000000000000000
[   72.679152] Internal error: Oops: 96000046 [#1] PREEMPT SMP
[   72.679159] Modules linked in:
[   72.679172] CPU: 5 PID: 3907 Comm: main Not tainted 3.10.58-03548-gc670b5a #1
[   72.679179] task: ffffffc006d18b00 ti: ffffffc030464000 task.ti: ffffffc030464000
[   72.679198] PC is at do_timerfd_settime+0x13c/0x384
[   72.679204] LR is at do_timerfd_settime+0x124/0x384
[   72.679210] pc : [<ffffffc0001fe99c>] lr : [<ffffffc0001fe984>] pstate: 80000145
[   72.679215] sp : ffffffc030467dc0
[   72.679220] x29: ffffffc030467dc0 x28: ffffffc030464000
[   72.679230] x27: ffffffc000e2e000 x26: ffffffc030467eb0
[   72.679239] x25: 0000000000000001 x24: ffffffc04ab4c200
[   72.679249] x23: 0000000000000000 x22: 0000000000000001
[   72.679258] x21: ffffffc030467e90 x20: ffffffc0010fa350
[   72.679268] x19: ffffffc04ab4d700 x18: 0000000000000000
[   72.679277] x17: 0000000000000001 x16: ffffffc0001ff204
[   72.679286] x15: 00000073aacff838 x14: 00000000322852a5
[   72.679297] x13: ffffffffa5cce228 x12: 0000000000000000
[   72.679306] x11: 000000002d2614c5 x10: 00000000000f4240
[   72.679315] x9 : 00000000000003e8 x8 : 0000000000000056
[   72.679325] x7 : 0000000000000000 x6 : 0000010624dd2fb8
[   72.679334] x5 : 0000000000000000 x4 : 0000000000000003
[   72.679344] x3 : ffffffc04ab4d7c0 x2 : 0000000000200200
[   72.679353] x1 : 0000000000200200 x0 : ffffffc0010fa350
...
[   72.679516] Call trace:
[   72.679524] [<ffffffc0001fe99c>] do_timerfd_settime+0x13c/0x384
[   72.679532] [<ffffffc0001ff284>] SyS_timerfd_settime+0x80/0x100
[   72.679539] Code: f9406662 f2a00401 aa1403e0 f9000462 (f9000043)
[   72.679588] ---[ end trace 4bf3b843607c1053 ]---
[   72.704005] Kernel panic - not syncing: Fatal exception
[   72.704028] CPU4: stopping

我们通过分析不同手机的内核崩溃日志可以看到是存在两种不同崩溃的,这取决于当前内核的LIST_POISON2宏的值(0xdead000000200200 或 0x00200200)当LIST_POISON2等于0xdead000000200200因为无法使用mmap对其进行映射,导致触发漏洞时会稳定崩溃在对该地址进行写入的位置,没想到如何避免该崩溃,而当LIST_POISON2等与0x00200200时我们可以通过mmap函数在exploit程序中对其进行映射,当触发漏洞的时候也就不会造成内核崩溃,也就可以进行我们下一步的利用操作,以下的操作都是基于LIST_POISON2等于0x00200200

漏洞成因

首先看下官方给的补丁
左边属于打补丁之前的代码,右边属于打补丁之后的代码
可以看到主要是新添加了一个__timerfd_remove_cancel函数,该函数是在对timerfd_remove_cancel函数做调用的时候进行锁操作,
在timerfd_setup_cancel函数中也加了锁操作,所以我们着重分析没打补丁之前的timerfd_setup_canceled函数和timerfd_remove_cancel函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
static void timerfd_setup_cancel(struct timerfd_ctx *ctx, int flags)
{
if ((ctx->clockid == CLOCK_REALTIME ||
ctx->clockid == CLOCK_REALTIME_ALARM) &&
(flags & TFD_TIMER_ABSTIME) && (flags & TFD_TIMER_CANCEL_ON_SET)) {
if (!ctx->might_cancel) {
ctx->might_cancel = true;
spin_lock(&cancel_lock);
list_add_rcu(&ctx->clist, &cancel_list); /* 对ctx->clist做添加操作 */
spin_unlock(&cancel_lock);
}
} else if (ctx->might_cancel) {
timerfd_remove_cancel(ctx); /* 对ctx->clist做删除操作 */
}
}

1
2
3
4
static inline void list_add_rcu(struct list_head *new, struct list_head *head)
{
__list_add_rcu(new, head, head->next);
}
1
2
3
4
5
6
7
8
9
static void timerfd_remove_cancel(struct timerfd_ctx *ctx)
{
if (ctx->might_cancel) {
ctx->might_cancel = false;
spin_lock(&cancel_lock);
list_del_rcu(&ctx->clist); /* 对ctx->clist做删除操作*/
spin_unlock(&cancel_lock);
}
}
1
2
3
4
5
static inline void list_del_rcu(struct list_head *entry)
{
__list_del_entry(entry);
entry->prev = LIST_POISON2;
}

首先来熟悉下timerfd_setup_cancel函数的逻辑,该函数存在两条分支,而这两条分支又都是通过判断ctx->might_cancel变量的值来控制应该执行哪条分支

1.ctx->might_cancel == false, call -> list_add_rcu函数
2.ctx->might_cancel == true, call -> timerfd_remove_cancel函数

第一次调用timerfd_setup_cancel函数时,当ctx->might_cancel等于false的时候,内核会走分支1,然后把ctx->might_cancel设置为true,然后调用list_add_rcu函数对cancel_list进行添加操作。
再次调用timerfd_setup_cancel函数的时候,因为第一次已经把ctx->might_cancel设置为true了,所以会进入分支2执行,然后调用timerfd_remove_cancel函数,把ctx->might_cancel设置为false,并且调用list_del_rcu函数对cancel_list进行删除操作。
弄清楚该函数逻辑后再回头看看补丁,也就明白为什么官方要增加两个锁操作了,猜测是因为这里的ctx->might_cancel在没打补丁之前是允许被别的进程修改的,一加一减本来是没什么问题,如果是单线程对timerfd_setup_cancel函数做调用的话,但是如果是两个线程或多个线程同时调用timerfd_setup_cancel函数那么会怎么样呢?可能就会出现线程同步问题,存在下面3种情况。

为什么说两个不同的线程修改的会是同一个ctx结构体呢?
那么我们可以先来看看ctx这个变量是怎么来的,在系统调用timerfd_create处可以看到ctx是在我们创建计时器对象时申请的,ctx在timerfd_create函数中被创建后,后续函数中的ctx都是以结构体指针(struct timerfd_ctx *)的方式对最开始创建的ctx进行访问,也就是说每个函数中操作的ctx->might_cancel其实也都是同一个变量,这也证明我们之前的猜测是没毛病的,这样的话我们完全可以创建多个线程与主线程做竞争改变ctx->might_cancel变量的值。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
SYSCALL_DEFINE2(timerfd_create, int, clockid, int, flags){
int ufd;
struct timerfd_ctx *ctx;
enum alarmtimer_type type;
/* Check the TFD_* constants for consistency. */
BUILD_BUG_ON(TFD_CLOEXEC != O_CLOEXEC);
BUILD_BUG_ON(TFD_NONBLOCK != O_NONBLOCK);
if ((flags & ~TFD_CREATE_FLAGS) ||
(clockid != CLOCK_MONOTONIC &&
clockid != CLOCK_REALTIME &&
clockid != CLOCK_REALTIME_ALARM &&
clockid != CLOCK_BOOTTIME &&
clockid != CLOCK_BOOTTIME_ALARM &&
clockid != CLOCK_POWEROFF_ALARM))
return -EINVAL;
ctx = kzalloc(sizeof(*ctx), GFP_KERNEL); /* ctx在此处被创建,并会被用于后续的操作 */
if (!ctx)
return -ENOMEM;
init_waitqueue_head(&ctx->wqh);
ctx->clockid = clockid;
if (isalarm(ctx)) {
type = clock2alarm(ctx->clockid);
alarm_init(&ctx->t.alarm, type, timerfd_alarmproc);
} else {
hrtimer_init(&ctx->t.tmr, clockid, HRTIMER_MODE_ABS);
}
ctx->moffs = ktime_mono_to_real((ktime_t){ .tv64 = 0 });
ufd = anon_inode_getfd("[timerfd]", &timerfd_fops, ctx,
O_RDWR | (flags & TFD_SHARED_FCNTL_FLAGS));
if (ufd < 0)
kfree(ctx);
return ufd;
}

该漏洞属于多线程并行造成的条件竞争问题,从崩溃信息来看,我们触发的应该是第3种情况(对cancel_list做两次删除操作)。

  • 第一次删除entry->prev被设置为了LIST_POISON2(0x00200200),因为属于正常删除,所以此时系统并没有崩溃,
  • 第二次删除在__list_del_entry中取entry_prev指向地址(0x00200200)的值,因为0x00200200该地址未映射,导致访问异常,系统崩溃
    因为两次删除的是同一个结构体,而第一次把结构体的entry_prev设置为了0x00200200,当第二次进行取值的时候就出现了访问异常。

漏洞利用

ctx->clist增删调用链:用户层的系统调用timerfd_settime()

timerfd_settime()                                       ---> 增加链
            \__do_timerfd_settime()
                        \__timerfd_setup_cancel()
                                    \__list_add_rcu()
timerfd_settime()                                       ---> 删除链
            \__do_timerfd_settime()
                        \__timerfd_setup_cancel()
                                    \__timerfd_remove_cancel()
                                                \__list_del_rcu()

利用思路

我们可以把条件竞争漏洞进阶为Use_After_Free漏洞,也就是前面提到的3种情况中的第2种情况,该情况会对cancel_list添加两次,那时的ctx->prev == ctx->next,然后释放掉ctx->prev指向的内存—也就是节点本身,但是ctx->next却对该内存还存在引用,这就给了我们后面的利用思路。
总体可分为6步:

  • 第一步:调用timerfd_create()函数创建一个timerfd和分配ctx结构体内存
  • 第二步:创建多个线程同时调用timerfd_settime函数,争取触发第2种竞争问题
  • 第三步:调用close函数触发释放结构体内存
    kfree_rcu(ctx) --> timerfd_release --> timerfd_remove_cancel(ctx)
    
  • 第四步:通过Heap spray技术对ctx结构体进行喷射,填充释放的ctx结构体
  • 第五步:通过调用settimeofday函数,触发释放后重引用漏洞,从而控制”PC”
  • 第六步:使用gadget对address_limit进行修改,并绕过PXN保护机制
1
2
3
4
5
6
7
8
let_go_timerfd_setup_cancel list_add_rcu >>>>>>> ctx = c5c9b240 prev=200200,next=c5c9b388
let_go_timerfd_setup_cancel list_add_rcu >>>>>>> ctx = c5c9b240 prev=c049c244,next=c5c9b388
let_go_timerfd_release >>>>>>> ctx = c5c9b240 prev=c5c9b2c8,next=c5c9b2c8
let_go_timerfd_remove_cancel list_del_rcu >>>>>>> ctx = c5c9b240 prev=c5c9b2c8,next=c5c9b2c8 /*第一次删除,此时前后指针已经指向同一地址 0xc5c9b2c8*/
/*-----------------------------------喷射时机----------------------------------------------- ----*/
结束时
let_go_timerfd_release >>>>>>> ctx = c5c9b300 prev=c5c9b2c8,next=c92891c8
let_go_timerfd_remove_cancel list_del_rcu >>>>>>> ctx = c5c9b300 prev=c5c9b2c8,next=c92891c8 /*第二次删除*/

首先通过竞争条件对cancel_list链表添加了两次同一地址的结构体,第一次释放结构体时会把ctx->prev指向的内存释放掉,却并没有操作ctx->next变量的值,因为ctx->next == ctx->prev,ctx->next还指向自身,所以该ctx内存虽然被释放,但还存在cancel_list链表中,紧接着我们通过喷射,填充之前释放的结构体内存。

然后在用户层触发重引用(settimerofday()),settimerofday底层实现函数timerfd_clock_was_set会对cancel_list链表进行遍历,并且取遍历出的ctx结构体成员wqh的地址作为wake_up_locked()函数的参数传入。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
void clock_was_set(void){
#ifdef CONFIG_HIGH_RES_TIMERS
/* Retrigger the CPU local events everywhere */
on_each_cpu(retrigger_next_event, NULL, 1);
#endif
timerfd_clock_was_set();
}
void timerfd_clock_was_set(void){
ktime_t moffs = ktime_mono_to_real((ktime_t){ .tv64 = 0 });
struct timerfd_ctx *ctx;
unsigned long flags;
rcu_read_lock();
list_for_each_entry_rcu(ctx, &cancel_list, clist) { /* 遍历喷射ctx结构体 */
if (!ctx->might_cancel)
continue;
spin_lock_irqsave(&ctx->wqh.lock, flags);
if (ctx->moffs.tv64 != moffs.tv64) {
ctx->moffs.tv64 = KTIME_MAX;
ctx->ticks++;
wake_up_locked(&ctx->wqh); /* 传入喷射结构体 */
}
spin_unlock_irqrestore(&ctx->wqh.lock, flags);
}
rcu_read_unlock();
}

再来看看wake_up_locked函数内部实现。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
#define wake_up_locked(x) __wake_up_locked((x), TASK_NORMAL, 1)
void __wake_up_locked(wait_queue_head_t *q, unsigned int mode, int nr){
__wake_up_common(q, mode, nr, 0, NULL);
}
static void __wake_up_common(wait_queue_head_t *q, unsigned int mode,
int nr_exclusive, int wake_flags, void *key){
wait_queue_t *curr, *next;
list_for_each_entry_safe(curr, next, &q->task_list, task_list) {
unsigned flags = curr->flags;
if (curr->func(curr, mode, wake_flags, key) && /* 控制内核执行流*/
(flags & WQ_FLAG_EXCLUSIVE) && !--nr_exclusive)
break;
}
}
struct __wait_queue_head {
spinlock_t lock;
struct list_head task_list;
};
typedef struct __wait_queue wait_queue_t;
typedef int (*wait_queue_func_t)(wait_queue_t *wait, unsigned mode, int flags, void *key); /* 函数指针原型 */
struct __wait_queue {
unsigned int flags;
void *private;
wait_queue_func_t func; /* 函数指针 */
struct list_head task_list;
};

可以看到在该函数内部存在一个函数指针(curr->func)的间接调用,而这个func属于curr的成员,curr又是通过使用list_for_each_entry_safe宏把q->task_list的next赋值过来的,q又是wake_up_locked函数的参数1,也就是遍历到的ctx成员wqh。

因为cancel_list链表中还存在之前被释放的ctx结构体,所以这里遍历到的ctx结构体也就是我们喷射的ctx结构体,这样看来所有的一切我们似乎都可以控制,只要通过内核源码,精心构造喷射的ctx结构体,绕过内核的一些检测控制内核的执行流还是很容易的。不过构造数据时需要注意一下list_for_each_entry_safe这个宏,不然可能会进入死循环,导致内核崩溃。
list_for_each_entry_safe宏

list_for_each_entry_safe(curr, next, &q->task_list, task_list)
    #define list_for_each_entry_safe(pos, n, head, member)          \
    for (pos = list_entry((head)->next, typeof(*pos), member),  \           /* curr赋值*/
        n = list_entry(pos->member.next, typeof(*pos), member); \           /* 初始化pos */
         &pos->member != (head);                    \                       /* 结束条件 */
         pos = n, n = list_entry(n->member.next, typeof(*n), member))       /* 改变条件 */

喷射结构体信息

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
struct timerfd_ctx {
union {
struct hrtimer tmr;
struct alarm alarm;
} t;
ktime_t tintv;
ktime_t moffs;
wait_queue_head_t wqh; /*>>>>>*/
u64 ticks;
int clockid;
short unsigned expired;
short unsigned settime_flags; /* to show in fdinfo */
struct rcu_head rcu;
struct list_head clist;
bool might_cancel;
};
32位
(gdb) p &((struct timerfd_ctx*)0)->might_cancel
$1 = (bool *) 0x90

64位
(gdb) p &((struct timerfd_ctx*)0)->might_cancel
$1 = (bool *) 0xf0
不同的内核,编译出来的大小也会不一样

触发链

在内核中找到了几条触发链:
比较无语的就是这些系统调用函数都会判断当前进程是否有CAP_SYS_TIME权限,如果有该权限才会执行后面的函数,不然就会中途退出

  • sys_stime() -> do_settimeofday() -> clock_was_set() -> timerfd_clock_was_set()
  • adjtimex() -> do_adjtimex() -> clock_was_set() -> timerfd_clock_was_set()
  • settimeofday() -> do_sys_settimeofday -> do_settimeofday() -> clock_was_set() -> timerfd_clock_was_set()

看了下android中的系统进程(SystemServer)是存在该权限的,所以我也可以结合别的AOSP漏洞,先拿到SystemServer进程的执行权限,然后再利用该内核漏洞进行提权,不过感觉好鸡肋啊 ̄□ ̄||

Author: Let_go

Link: http://github.com/2017/12/29/CVE-2017-10661/

Copyright: All articles in this blog are licensed under CC BY-NC-SA 3.0 unless stating additionally.

< PreviousPost
Linux Kernel x86-64 bypass SMEP-KASLR-kptr_restric
NextPost >
CVE-2017-8890
CATALOG
  1. 1. CVE-2018-10661
    1. 1.1. 前言
      1. 1.1.1. 测试内核
      2. 1.1.2. 漏洞描述
      3. 1.1.3. Google修复链接
      4. 1.1.4. 存在漏洞的Nexus内核版本
    2. 1.2. 漏洞复现
      1. 1.2.1. 红米4A_崩溃日志
      2. 1.2.2. MI 5C崩溃日志
    3. 1.3. 漏洞成因
    4. 1.4. 漏洞利用
      1. 1.4.1. 利用思路
      2. 1.4.2. 喷射结构体信息
      3. 1.4.3. 触发链