链表游戏:CVE-2017-10661之完全利用
原文来自安全客,作者:huahuaisadog@360 Vulpecker Team
原文链接:https://www.anquanke.com/post/id/129468
最近在整理自己以前写的一些Android内核漏洞利用的代码,发现了一些新的思路。
CVE-2017-10661的利用是去年CORE TEAM在hitcon上分享过的:https://hitcon.org/2017/CMT/slide-files/d1_s3_r0.pdf。他们给出的利用是在有CAP_SYS_TIME这个capable权限下的利用方式,而普通用户没这个权限。最近整理到这里的时候,想了想如何利用这个漏洞从0权限到root呢?没想到竟然还能有一些收获,分享一哈:
- CVE-2017-10661简单分析
- CAP_SYS_TIME下的利用
- pipe的TOCTTOU
- 思考下链表操作与UAF
- 0权限下的利用
CVE-2017-10661简单分析
关于CVE-2017-10661的分析和SYS_TIME下的利用,CORE TEAM的ppt中已经有比较清晰的解释。我这里再简单的用文字描述一遍吧。
这个漏洞存在于Linux内核代码 fs/timerfd.c的timerfd_setup_cancel函数中:
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) { //[1][2]ctx->might_cancel = true; //[3][4]spin_lock(&cancel_lock);list_add_rcu(&ctx->clist, &cancel_list); //[5][6]spin_unlock(&cancel_lock);}} else if (ctx->might_cancel) {timerfd_remove_cancel(ctx);}
}
这里会有一个race condition:假设两个线程同时对同一个ctx执行timerfd_setup_cancel操作,可能会出现这样的情况(垂直方向为时间线):
Thread1 Thread2
[1]检查ctx->might_cancel,值为false
. [2]检查ctx->might_cancel,值为false
[3]将ctx->might_cancel赋值为true
. [4]将ctx->might_cancel赋值为true
[5]将ctx加入到cancel_list中
. [6]将ctx再次加入到cancel_list中
所以,这里其实是因为ctx->might_cancel是临界资源,而这个函数对它的读写并没有加锁,虽然在if(!ctx->might_cancel)
和ctx->might_cancel
的时间间隔很小,但是还是可以产生资源冲突的情况,也就导致了后面的问题:会对同一个节点执行两次list_add_rcu
操作,这是一个非常严重的问题。
首先cancel_list
是一个带头结点的循环双链表。list_add_rcu
是一个头插法加入节点的操作,所以第一次调用后,链表结构如图:
而对我们的victim ctx再次调用list_add_rcu会变成什么样子呢?
static inline void list_add_rcu(struct list_head *new, struct list_head *head) {__list_add_rcu(new, head, head->next);
}static inline void __list_add_rcu(struct list_head *new,struct list_head *prev, struct list_head *next)
{new->next = next;new->prev = prev;rcu_assign_pointer(list_next_rcu(prev), new); //可以看做 prev->next = new;next->prev = new;
}
要注意的是,第二次操作,我们的new == head->next,于是操作相当于:
victim->next = victim;victim->prev = victim;
那么链表这时候就变成了这样:
可以看到victim的next指针和prev指针都指向了自己。这时候就会发生一系列问题,第一我们再也没办法通过链表来访问到victim ctx后面的节点了(这点和漏洞利用关系不大),第二我们也没办法将victim这个节点从链表上删除,尽管我们可以在kfree ctx之前对其执行list_del_rcu
操作:
static inline void __list_del(struct list_head * prev, struct list_head * next)
{next->prev = prev;prev->next = next;
}static inline void __list_del_entry(struct list_head *entry)
{__list_del(entry->prev, entry->next);
}static inline void list_del_rcu(struct list_head *entry)
{__list_del_entry(entry); //上一句可描述为://entry->next->prev = entry->prev;//entry->prev->next = entry->next;entry->prev = LIST_POISON2;
}
于是list_del_rcu
执行之后,链表又变成了这样子:
所以尽管之后会执行kfree将victim ctx给free掉,但是我们的cancel_list
链表还保存着这段free掉的ctx的指针:head->next
以及ctx->prev
。所以如果后续有对cancel_list
链表的一些操作,就会产生USE-AFTER-FREE的问题。
这也就是这个漏洞的成因了。
CAP_SYS_TIME下的利用
CORE TEAM的ppt里给出了这种利用方式。他们从victim ctx释放后并没有真正从cancel_list拿下来,仍然可以通过遍历cancel_list访问到victim ctx这一点做文章。
对cancel_list的遍历在函数timerfd_clock_was_set
:
void timerfd_clock_was_set(void)
{ktime_t moffs = ktime_get_monotonic_offset();struct timerfd_ctx *ctx;unsigned long flags;rcu_read_lock();list_for_each_entry_rcu(ctx, &cancel_list, clist) {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); //会走到 __wake_up_common函数}spin_unlock_irqrestore(&ctx->wqh.lock, flags);}rcu_read_unlock();
}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) && //curr->func(flags & WQ_FLAG_EXCLUSIVE) && !--nr_exclusive)break;}
}
思路就是
等victim ctx被free之后,进行堆喷将victim ctx覆盖成自己精心构造的数据(这里可以用keyctl或者是sendmmsg实现)。
然后调用
timerfd_clock_was_set
函数,这时会遍历cancel_list,由于head->next就是我们的victim ctx,所以victim ctx会被这次操作引用到。数据构造得OK的话,会调用wake_up_locked(&ctx->wqh)
,而ctx就是我们的victim ctx这以后ctx->wqh是自己定义的数据,所以
\_\_wake\_up\_common
的curr,curr->func也是我们可以决定的。所以执行到curr->func的时候,我们就控制了PC寄存器,而X0等于我们的curr
劫持了pc,之后找rop/jop就能轻松实现提权操作,这里不再多说。
为什么说这是CAP_SYS_TIME权限下的利用方法呢?因为timerfd_clock_was_set
函数的调用链是这样:
timerfd_clock_was_set <-- clock_was_set <-- do_settimeofday <-- do_sys_settimeofday <--SYS_setttimeofday
用户态需要调用settimeofday这个系统调用来触发。而在do_sys_settimeofday
函数里有对CAP_SYS_TIME的检查:
int do_sys_settimeofday(const struct timespec *tv, const struct timezone *tz)
{...error = security_settime(tv, tz); //权限检查if (error)return error;...if (tv)return do_settimeofday(tv);return 0;
}static inline int security_settime(const struct timespec *ts,const struct timezone *tz)
{return cap_settime(ts, tz);
}int cap_settime(const struct timespec *ts, const struct timezone *tz)
{if (!capable(CAP_SYS_TIME)) //检查CAP_SYS_TIMEreturn -EPERM;return 0;
}
所以我们如果想以这种方式来利用这个漏洞,就需要进程本身有CAP_SYS_TIME的权限,这也就限制了这种方法的适用范围。于是我们想要从0权限来利用这个漏洞,就得另辟蹊径。
pipe的TOCTTOU
在介绍0权限的利用方法思路之前,我觉得得先介绍下pipe的TOCTTOU机制,因为这个是接下来利用思路的一个基础。关于这部分的内容,也可以参考shendi大牛的slide
TOCTTOU : time of check to time of use .写程序的时候通常都会在使用前,对要使用的数据进行一个检查。而这个检查的时间点,和使用的时间点之间,其实是有空隙的。如果能在这个时间空隙里,做到对已经check的数据的更改,那么就可能在use的时刻,使用到非法的数据。
pipe的readv / writev就是这样一个典型。以readv为例,readv会在do_readv_writev
的rw_copy_check_uvector
函数里对用户态传进来的所有iovector进行合法性检查:
struct iovec {void *iov_base;size_t iov_len;
};
ssize_t rw_copy_check_uvector(int type, const struct iovec __user * uvector,unsigned long nr_segs, unsigned long fast_segs,struct iovec *fast_pointer,struct iovec **ret_pointer)
{unsigned long seg;ssize_t ret;struct iovec *iov = fast_pointer;...if (nr_segs > fast_segs) {iov = kmalloc(nr_segs*sizeof(struct iovec), GFP_KERNEL); //[1]...}if (copy_from_user(iov, uvector, nr_segs*sizeof(*uvector))) {...}...for (seg = 0; seg < nr_segs; seg++) {void __user *buf = iov[seg].iov_base;ssize_t len = (ssize_t)iov[seg].iov_len;...if (type >= 0&& unlikely(!access_ok(vrfy_dir(type), buf, len))) { //[2]ret = -EFAULT;goto out;}...}
}
可以看到这个检查函数做了两件事:
[1]如果iovector的个数比较多(大于8),就会kmalloc一段内存,然后将用户态传来的iovector拷贝进去。当然如果比较小,就直接把用户态传来的iovector放到栈上。
[2]对iovector进行合法性检查,确保所有的iovecor的iov_base都是用户态地址。
这里也就是pipe的time of check。
在检查通过之后,会去执行pipe_read函数,相信分析过CVE-2015-1805的朋友们都知道,pipe_read函数里对iovector的iov_base只会做是不是可写地址的检查,而不会做是不是用户态地址的检查,然后有数据就写入。pipe_read函数往iovector的iov_base里写入数据的时刻(__copy_to_user),就是pipe的time of use。
那么这个check 和 use的间隙是多长呢?这取决于我们什么时候往pipe的buffer里写入数据。因为pipe_read默认是阻塞的,如果pipe的buffer里没有数据,pipe_read就会一直被阻塞,直到我们调用writev往pipe的buffer写数据。
所以,pipe的time of check to time of use这个间隔,可以由我们自己控制。
如果在这个时间间隔有办法对iovector进行更改,那么就可能往非法地址写入数据:
那么,怎么才能在这个时间间隔,对iovector进行更改呢?
这当然要通过漏洞来实现:
1,堆溢出漏洞。前面分析知道,如果有8个以上的的iovctor,就会调用kmalloc来存储这些iovector。如果能有一个内核堆溢出漏洞,那么只要把堆布局好,就能让溢出的数据,该卸掉iovector的iov_base.
2,UAF漏洞。要知道,我们kmalloc的iovector也是有占位功能的,如果使用iovector进行堆喷,将free过的victim进行占位。然后触发UAF,如果这个use的操作,能对占位的iovector进行更改,那么也就实现了目的。
知道了pipe的TOCTTOU的基础,我们可以来重新思考下CVE-2017-10661。
思考下链表操作与UAF
链表其实是个变化过程比较多的数据结构,对某节点的删除或者添加都会影响相邻的节点。那如果一个节点出现了问题,对它的相邻节点进行一系列操作会产生什么样的变化呢?在基于CVE-2017-10661将链表破坏之后,我在这里将给出两种情景。首先贴一张已经释放了victim ctx之后,cancel_list的状态图吧:
victim ctx已经被free,但是head->next和ctx_A->prev仍然保留着这段内存的指针。那么:
情景一:添加一个新的节点ctx_B
同样还是头插法,于是下面这几段代码会执行:
ctx_B->next = head->next;ctx_B->prev = head;head->next->prev = ctx_B; //这里等价于 victim_mem->data2 = ctx_Bhead->next = ctx_B;
可以看到,这个添加操作(list_add_rcu)会对已经free了的内存进行操作,会将victim_mem->data2赋值为ctx_B。语言总是没有图片来的直观,添加操作执行后链表的状态如图:
结合我们之前讨论的pipe TOCTTOU,如果victim_mem刚好是由我们的pipe的iovector所占位,那么这里对data2的更改,可能就会对某个iov_base进行更改:iov_base = ctx_B。那么这样就允许我们对ctx_B->list进行任意写入。
情景二:删除节点ctx_A
删除操作会影响前后两个节点,我们假设ctx_A的next节点是ctx_C,那么就有:
ctx_A->prev->next = ctx_A->next;//等价于 victim_mem->data1 = ctx_Cctx_A->next->prev = ctx_A->prev;//等价于 ctx_C->prev = victim_memctx_A->prev = LIST_POISION2;
与情景1类似,这个删除操作(list_del_rcu),也会已经free了的内存进行操作,将victim_mem->data1赋值为ctx_C:
同样的,如果victim_mem刚好是由我们的pipe的iovector占位,对data1的更改,也可能改掉iov_base:iov_base = ctx_C
。这样也就能对ctx_C->list进行任意写入。
为什么要给出两种情景呢?因为我们需要考虑一个究竟是data1对应iov_base,还是data2对应iov_base。iovector的结构是这样:
struct iovec {void *iov_base;size_t iov_len;};
64位下,struct iovec是16字节大小,跟上面list结构的大小一样。于是data1和data2中必有一个是iov_base,一个是iov_len。而我们需要改的是iov_base。所以上述两种情景,根据具体情况就能找到一种适用的。
问题又来了,比如说情景二,能够对ctx_C->list进行任意写入又能做什么呢?
能够对双链表某节点的next,prev指针进行完全控制,是一件很恐怖的事情。因为在删除这个节点的时候,会导致一个很严重的问题。具体怎么回事我们看代码:
static inline void list_del_rcu(struct list_head *entry)
{__list_del_entry(entry); //上一句可描述为://entry->next->prev = entry->prev;//entry->prev->next = entry->next;entry->prev = LIST_POISON2;
}
假设我们将prev指针改为target_address,next指针改为target_value。那么上述代码就等价于:
*(uint64_t)(target_value + 8) = target_address;*(uint64_t)(target_address) = target_value;
于是这导致了一个任意地址写入任意内容的问题。当然,写入的内容没那么任意,它的值必须也要是一个可写的地址。
0权限下的利用
有了上述的讨论之后,我们利用的思路逐渐明朗。
我们的ctx是0xF8的大小,处于0x100的slab块里面,所以地址总是0地址对其。那么如果要做iovector进行占位,得到的地址也总是0地址对其,所以里面元素的iov_base也会是0地址对其。在我测试的机器(nexus6p)上,next指针偏移是0xE0,prev指针是0xE8。所以我们需要选择情景二:删除victim的next节点。那么我们的步骤应该是:‘
在创造victim ctx之前,将ctx_C加入cancel_list,然后将ctx_A加入cancel_list
赢得竞争,导致victim ctx被list_add_rcu两次
对victim ctx执行list_del_rcu操作,并将victim_ctx释放,此时cacncel_list是这样:
用iovector进行堆喷,使得其将victim mem占位:
这时pipe_read被阻塞,执行删除ctx_A的操作,会导致iov_base的更改,改成指向我们的ctx_C:
然后我们执行pipe_write,这时会导致ctx_C的next指针和prev指针被我们改写。next指针改写为target_value,prev指针改写为target_addr:
最后我们对ctx_C执行删除节点的操作,就能实现任意地址写任意内容了,当然写的内容不能那么任意。 在这之后,再进行提权是一件很容易的事情。这里简单描述两种做法:
1,target_addr设置为&ptmx_cdev->ops,target_value设置为0x30000000。这样我们在用户态0x30000000布置好函数指针, 后续操作就很容易了。修改task_prctl相关的也是一样的道理。
2,增加/修改地址转换表中的内存描述符。这个虽然说原理比较复杂,介绍起来可能比本文之前说的所有的内容还要长,但是实现起来却是很方便。像nexus6p这样的机器,kernel的第一级地址转换表的地址固定为0xFFFFFFC00007d000,在中添加一条合适的内存描述符,就能实现在用户态读取/修改kernel的text段的内容,实现kernel patch。提权也就很轻松了,而且好处是不需要找各种各样的地址,自己读取kernel的内容,自己能计算出来,可以做成通用的root。不过这种方法在三星这种有RKP保护的机器上不适用,或者说得绕过才行。
然后,这个漏洞,其实还是可以转化为任意地址写任意内容,这次的写的内容可以任意,但是做法就不一样了。需要把iov_len做得长一点,把对ctx_C的写入转化为一个堆溢出的漏洞。然后达成目标。
江湖规矩放图:
最后,对于文中出现的问题,还请各路大牛加以斧正,欢迎技术交流:huahuaisadog@gmail.com
参考文档 1, https://hitcon.org/2017/CMT/slide-files/d1_s3_r0.pdf
2, https://android.googlesource.com/kernel/msm/+/0fecf48887cf173503612936bad2c85b436a5296%5E%21/#F0
3, https://android.googlesource.com/kernel/msm/+/e7a3029ebf4175889e8bdb278fd9cf02a211118c/fs/read_write.c
4, https://github.com/retme7/My-Slides/blob/master/The-Art-of-Exploiting-Unconventional-Use-after-free-Bugs-in-Android-Kernel.pdf
链表游戏:CVE-2017-10661之完全利用相关推荐
- AAAI 2017论文简析:利用可拍照移动设备感知空气质量---Crowdsensing Air Quality with Camera-enabled Mobile Devices
AAAI 2017论文简析:利用可拍照移动设备感知空气质量 论文思想 论文背景 论文工作 解决方法 总结 补充 论文思想 利用群智感知的思想通过可拍照的移动设备去监测环境空气质量 (即将智能移动设备转 ...
- linux内核安全数据,【漏洞分析】Linux内核XFRM权限提升漏洞分析预警(CVE–2017–16939)...
0x00 背景介绍 2017年11月24日, OSS社区披露了一个由独立安全研究员Mohamed Ghannam发现的一处存在于Linux 内核Netlink socket子系统(XFRM)的漏洞,漏 ...
- 小游戏市场被引爆,如何利用才能正确解锁?
作者:小杨 还记得在2018微信公开课Pro上,张小龙在演讲的过程中也多次涉及到了小游戏,可以预见微信将会在2018年开始大力推广小程序,一解2017年的疲态,而容易引爆的小游戏正好可以作为切入点. ...
- 如何用python开发一个贪吃蛇游戏_教你一步步利用python实现贪吃蛇游戏
教你一步步利用python实现贪吃蛇游戏 来源:中文源码网 浏览: 次 日期:2019年11月5日 [下载文档: 教你一步步利用python实现贪吃蛇游戏.txt ] (友情提示:右键点 ...
- [编程题]大富翁游戏 美团2017 JAVA
美团2017 JAVA [编程题]大富翁游戏 [编程题]拼凑钱币 [编程题]最大矩形面积 [编程题]最长公共连续子串 这道题限制了1≤n≤61\leq n\leq61≤n≤6,降低了问题难度,我首先想 ...
- QQ游戏怎么引流?如何利用QQ游戏引流让别人加你?
随着时间的推移,网赚者们所熟知各的大引流平台管理是越来越严格.造成了很多网赚者们不得不寻找新的引流处女地.今天写这篇文章就是想与大家分享一个自己所知的一个新的引流处女地,在这块处女地上操作得当,日引流 ...
- html5游戏联机教程,纯前端如何利用帧同步做一款联机游戏?
一.游戏帧同步 1.简介 ·现代多人游戏中,多个客户端之间的通讯大多以同步多方状态为主要目标,为了实现这一目标,主要有两个技术方向:状态同步.帧同步. ·状态同步的思想中不同玩家屏幕上的一致性的表现并 ...
- Q3广告业务稳健、游戏超预期,搜狐利用直播技术向上破圈
北京时间11月16日,搜狐对外公布了2020财年三季度财报. 从财报的基本面来看,报告期内,搜狐营收1.58亿美元:归属于搜狐的非美国通用会计准则持续经营业务净亏损为700万美元,同比减亏超76%. ...
- 【unity3d游戏开发之基础篇】利用射线实现鼠标控制角色转向和移动(角色移动一)...
由于最近搞2D游戏, 下面的代码配合NGUI来使用 ... 将代码拖到角色身上就OK, 实现了角色转向.移动 ,想看效果的可以将代码下下来~ 用到了向量来计算角度 以及方向, 得恶补下向量知识了 ...
最新文章
- phpMyAdmin 数据库添加int类型的值时默认设为唯一主键的问题解决
- android.support-v7版本依赖配置
- 程序员面试100题之二:跳台阶问题(变态跳台阶)
- STM32F103输出互补PWM波
- STM32封装库下载
- 蛋白组+代谢组联合分析
- Python问题解决6:使用jupyter notebook时安装第三方库提示升级pip,pip升级不成功一直报错
- 如何修改request的parameter的几种方式
- 基于SSM的医院管理系统
- html自动补位的功能,js中位数不足自动补位扩展padLeft、padRight实现代码
- 3D游戏——AR图片识别与建模
- Unity 的 Scroll View组件
- android app 运行时提示 应用专为旧版 Android 打造
- vue3学习(模板语法)
- 华为手机鸿蒙系统有什么优点和缺点,有多少人愿意亲身体验鸿蒙系统?华为自研系统,有哪些优势?...
- Vercel和Railway都是云端的平台即服务提供商
- WordPress使用SQL语句批量替换文章内容
- 苹果应用商店审核指南中文翻译
- php属于什么职类,老师属于什么职业类别
- Matlab矩阵乘法