文章目录
漏洞简介
漏洞编号: CVE-2022-2588
漏洞产品: linux kernel - cls route4
影响范围: ~ linux kernel 5.19
利用条件: kernel 普通用户具有CAP_NET_ADMIN 权限
利用效果: 本地提权
该漏是Zhenpeng Lin 博士在blackhat 的议题:"Cautious! A New Exploitation Method! No Pipe but as Nasty as Dirty Pipe"中的演示漏洞之一 ,并在8月25号在github公开exp,该漏洞是netlink协议中的CLS_ROUTE4模块中的route4_change函数中的UAF/double free漏洞。使用漏洞作者的dirty cred方法可以完成不依赖内核版本(特定地址)的本地提权。和之前的dirty pipe 系列利用方法一样,实现无地址依赖的本地提权攻击。
环境搭建
编译内核需要满足如下编译选项:
//下面三个可以触发poc
CONFIG_NET_CLS_ROUTE4=y
CONFIG_DUMMY=y //或使用的其他设备
CONFIG_NET_SCH_QFQ=y //或使用的其他设备
//跑exp还需要,并且qemu内存得大
CONFIG_NET_CLS_BASIC=y
漏洞触发poc:https://github.com/sang-chu/CVE-2022-2588
漏洞利用exp:https://github.com/Markakd/CVE-2022-2588
详见[编译能复现指定poc的内核排错过程]
漏洞原理
漏洞发生点
漏洞发生在route4_change中,大概逻辑就是生成并初始化一个route4_filter的结构,如果存在相同句柄的route4_filter,则会申请新的释放旧的,用新的代替旧的。
net\sched\cls_route.c : route4_change
staticintroute4_change(structnet*net,structsk_buff*in_skb,structtcf_proto*tp,unsignedlong base, u32 handle,structnlattr**tca,void**arg, bool ovr,
bool rtnl_held,structnetlink_ext_ack*extack){structroute4_head*head =rtnl_dereference(tp->root);structroute4_filter __rcu **fp;structroute4_filter*fold,*f1,*pfp,*f =NULL;structroute4_bucket*b;structnlattr*opt = tca[TCA_OPTIONS];structnlattr*tb[TCA_ROUTE4_MAX +1];unsignedint h, th;int err;
bool new = true;
fold =*arg;//[1]来自上层函数,通过句柄找到的已经存在的route4_filter
··· ···route4_filter
f =kzalloc(sizeof(structroute4_filter), GFP_KERNEL);//[2]申请新的route4_filter,标志位GFP_KERNELif(!f)goto errout;
err =tcf_exts_init(&f->exts, net, TCA_ROUTE4_ACT, TCA_ROUTE4_POLICE);//[2]这里申请exts的内存空间if(err <0)goto errout;if(fold){//[3]如果当前已经存在route4_filter
f->id = fold->id;
f->iif = fold->iif;
f->res = fold->res;
f->handle = fold->handle;
f->tp = fold->tp;
f->bkt = fold->bkt;
new = false;//不是新的}
err =route4_set_parms(net, tp, base, f, handle, head, tb,
tca[TCA_RATE], new, ovr, extack);//会更新信息包括handle
··· ···
//[4]如果存在route4_filter,并且handle不为0,新老handle不同,则会删除并释放老的if(fold && fold->handle && f->handle != fold->handle){
th =to_hash(fold->handle);
h =from_hash(fold->handle >>16);
b =rtnl_dereference(head->table[th]);if(b){
fp =&b->ht[h];//ht存放的是route4_filter列表for(pfp =rtnl_dereference(*fp); pfp;
fp =&pfp->next, pfp =rtnl_dereference(*fp)){if(pfp == fold){rcu_assign_pointer(*fp, fold->next);//找到存在的那个,然后从链表中删除break;}}}}route4_reset_fastmap(head);*arg = f;if(fold){//[5]存在route4_filter的话,则释放之前的tcf_unbind_filter(tp,&fold->res);tcf_exts_get_net(&fold->exts);tcf_queue_work(&fold->rwork, route4_delete_filter_work);//[5]启动内核进程,完成route4_delete_filter_work任务}return0;
··· ···
}
[1] 上层函数通过route4_get 函数找到用户传入句柄对应的route4_filter结构,就是arg参数
[2] 无论如何都要申请一个新的route4_filter结构,并初始化,其中f->exts->actions也需要额外申请空间,都用的是GFP_KERNEL标志位:
staticinlineinttcf_exts_init(structtcf_exts*exts,structnet*net,int action,int police){
··· ···
exts->actions =kcalloc(TCA_ACT_MAX_PRIO,sizeof(structtc_action*),//这里申请32个指针类型共256字节
GFP_KERNEL);
··· ···
}
[3] 存在旧的则把信息拷贝到新的上
[4] 这里如果存在旧的并且handle不为0,则会将旧的从列表中删除。
[5] 如果存在旧的,则新建内核进程任务route4_delete_filter_work 来完成释放工作。
这里逻辑没问题,先将旧的route4_filter从列表中删除,然后再将旧的route4_filter释放掉,但问题在于这两个操作的判断条件不一样。将旧的route4_filter从列表中删除操作要求handle不为0,而释放旧的route4_filter 则只要旧route4_filter存在即可。这样如果我们构造handle 为0 的route4_filter,那么就会释放后还存在在链表中,后续还可以继续释放,造成double free。
调用链
相关操作以及结构体
释放route4_filter结构使用的是内核任务route4_delete_filter_work:
net\sched\cls_route.c : route4_delete_filter_work
voidtcf_exts_destroy(structtcf_exts*exts){#ifdefCONFIG_NET_CLS_ACTif(exts->actions){tcf_action_destroy(exts->actions, TCA_ACT_UNBIND);kfree(exts->actions);//释放exts->actions}
exts->nr_actions =0;#endif}staticvoid__route4_delete_filter(structroute4_filter*f){tcf_exts_destroy(&f->exts);tcf_exts_put_net(&f->exts);kfree(f);//释放route4_filter}staticvoidroute4_delete_filter_work(structwork_struct*work){structroute4_filter*f =container_of(to_rcu_work(work),structroute4_filter,
rwork);rtnl_lock();__route4_delete_filter(f);rtnl_unlock();}
在route4_delete函数中同样会调用route4_delete_filter_work 任务,所以这里doublefree可以使用route4_delete功能进行第二次free:
net\sched\cls_route.c : route4_delete
staticintroute4_delete(structtcf_proto*tp,void*arg, bool *last,
bool rtnl_held,structnetlink_ext_ack*extack){
··· ···
for(nf =rtnl_dereference(*fp); nf;
fp =&nf->next, nf =rtnl_dereference(*fp)){if(nf == f){
··· ···
tcf_queue_work(&f->rwork, route4_delete_filter_work);
··· ···
}}
··· ···
}
参与释放的有两个结构体:
structroute4_filter{//大小 144structroute4_filter __rcu *next;
u32 id;int iif;structtcf_result res;structtcf_exts exts;
u32 handle;structroute4_bucket*bkt;structtcf_proto*tp;structrcu_work rwork;};structtc_action{//大小192conststructtc_action_ops*ops;
__u32 type;/* for backward compat(TCA_OLD_COMPAT) */structtcf_idrinfo*idrinfo;
u32 tcfa_index;refcount_t tcfa_refcnt;atomic_t tcfa_bindcnt;int tcfa_action;structtcf_t tcfa_tm;structgnet_stats_basic_packed tcfa_bstats;structgnet_stats_basic_packed tcfa_bstats_hw;structgnet_stats_queue tcfa_qstats;structnet_rate_estimator __rcu *tcfa_rate_est;spinlock_t tcfa_lock;structgnet_stats_basic_cpu __percpu *cpu_bstats;structgnet_stats_basic_cpu __percpu *cpu_bstats_hw;structgnet_stats_queue __percpu *cpu_qstats;structtc_cookie __rcu *act_cookie;structtcf_chain __rcu *goto_chain;
u32 tcfa_flags;
u8 hw_stats;
u8 used_hw_stats;
bool used_hw_stats_valid;};
但参与释放的tc_action 不是结构体本身,而是32个结构体指针的空间,共256字节,之前分析过。
漏洞利用
dirty cred 利用方法
double free
之前提到过,通过route4_change 和route4_delete 都可以启动一个内核进程route4_delete_filter_work。在该任务中,一共会释放两个堆内存,其中之一是
struct route4_filter
,大小144属于kmalloc-192,另一个是一个指针数组空间,大小0x100属于kmalloc-256。
而
struct file
大小232属于kmalloc-256,正好可以利用256 的chunk来double free。这里完成的目标是让两个文件描述符指向同一个
struct file
。
常规操作直接释放,打开file,释放即可。但问题出在释放0x100的同时还会释放一个192,而再次释放的时候192是没有被重新分配的,就会造成double free,内核检测double free部分如下:
staticinlinevoidset_freepointer(structkmem_cache*s,void*object,void*fp){unsignedlong freeptr_addr =(unsignedlong)object + s->offset;#ifdefCONFIG_SLAB_FREELIST_HARDENEDBUG_ON(object == fp);/* naive detection of double free or corruption */#endif
freeptr_addr =(unsignedlong)kasan_reset_tag((void*)freeptr_addr);*(void**)freeptr_addr =freelist_ptr(s, fp, freeptr_addr);}
如果freelist指针已经指向刚释放的slab,则说明该slab释放了两次,则会崩溃。但这里只能检测出连续释放两次的slab,如果释放两次之间间隔释放了同一页的其他slab,则检测不出。
cross cache attack
struct file 结构体申请内存空间所用的slab 是filp_cache,而不是常规的kmalloc 申请空间所用的slab:
fs\file_table.c:
void __init files_init(void){
filp_cachep =kmem_cache_create("filp",sizeof(structfile),0,
SLAB_HWCACHE_ALIGN | SLAB_PANIC | SLAB_ACCOUNT,NULL);//初始化新slab:filp,大小是file结构体大小percpu_counter_init(&nr_files,0, GFP_KERNEL);}staticstructfile*__alloc_file(int flags,conststructcred*cred){structfile*f;int error;
f =kmem_cache_zalloc(filp_cachep, GFP_KERNEL);//调用kmem_cache_zalloc 从filp_cachep 中申请内存
··· ···
}
sizeof(struct file) 大小属于0x100,所以filp slab也是一个管理0x100大小的slab,只不过它和常规kmalloc-256不在一起而已。但这并不是问题,我们可以使用cross cache attack原理攻击,该利用方法的整体思路是,当一个slab 页面被全部释放的时候会被回收,这时被回收的页面是可以被其他种类的slab使用的这样就可以跨slab种类来进行利用,如Zhenpeng Lin 的ppt中演示的:
假定我们有一个非法释放漏洞(或double free),但只能释放普通slab 中的堆块:
- 首先喷射一堆该大小的普通堆块,这样会消耗一大堆slab 页面。我们的double free目标指针指向其中一个堆块,先将其释放
- 然后将喷射的一大堆普通堆块都释放掉,这样double free目标堆块所在slab 页面中的所有堆块(绝大概率)会被都释放掉,该slab 页面为空,会被系统回收
- 这时喷射一大堆filp / 其他slab 类型的堆块,这样目标指针所在页面大概率会被filp 类型slab或其他目标类型slab重新申请到吗,并且目标指针(double free漏洞指针)指向其中一个struct file结构体
- 使用漏洞的第二次释放能力,该struct file结构体被非法释放
具体操作
所以这里采用了两个进程:
- 在添加route4_filter之前喷射若干对应大小的slab是为了让他们和route4_filter都能在一个slab page中,这样整个slab page都被释放之后对应上面cross cache attack让该kmalloc-256 page被filp slab复用。
- 释放route4_filter 之后再释放若干,就会有同slab page 中的其他堆内存释放,避免连续释放double free
- 最后喷射若干file结构,filp slab复用上面的slab page之后总会申请到目标double free结构
- 最后达到两个文件描述符指向同一个文件结构体的状态。
另外,在攻击开始前,会先喷神若干文件结构消耗掉现有struct file 申请slab 中的内存,等到之后会分配新的内存。
dirty cred部分
具体的dirty cred分析请见[kernel exploit] Dirty Cred: 一种新的无地址依赖漏洞利用方案
dirty cred 的主要利用思路是利用向文件中写入的内容的具体内核实现过程为
- 进行权限验证,具备写权限则通过
- 获取写入内容
- 进行写入操作
如果我们能在1 和2 之间对struct file进行替换,则可以利用有写权限的文件进行权限判断,具体写入操作写到无写权限的问价之中。该操作需要漏洞来非法释放struct file结构体来替换,并且需要其他操作来延长替换的时间窗。dirty cred具体利用思路参见dirty cred 论文分析。这里操作分为三个进程:
- 进程A主要负责向被uaf的文件写入大量数据,同时进程C会尝试向该文件写入恶意内容。鉴权操作会同时进行,一瞬间都鉴权通过,但由于进程A写入数据量过大,inode锁会将其锁住大量时间,进程C只能等待,这期间我们要替换文件结构体
- 进程B中对之前构造的"指向同一个struct file 的两个文件描述符"分别进行close,其实是这里对该文件的引用计数是2(进程C还打开准备写入了),需要关闭两次才可以真正释放struct file。
- 进程B中释放完毕,会立刻喷射若干passwd 的struct file结构覆盖刚刚释放的位置,这样进程C中打算写入的文件就会变成passwd。
- 进程A写入完毕,进程C继续写入,这时struct file已经被替换为passwd 的,进程C直接将任意内容写入了passwd 之中。
利用效果:
另一种思路
这里也可以直接套用之前的伪dirty pipe方法,由于时间问题没写具体exp,主要思路就是:
- 目标同样选为kmalloc-256的堆块,利用漏洞释放第一次
- 喷射若干msg_msg
- 利用漏洞释放第二次
- 喷射若干sk_buff
这样sk_buff 就跟msg_msg 共用同一块内存,然后直接套用胜利方程式即可。
参考
Markakd/CVE-2022-2588
Cautious: A New Exploitation Method! No Pipe but as Nasty as Dirty Pipe - Black Hat USA 2022 | Briefings Schedule
版权归原作者 breezeO_o 所有, 如有侵权,请联系我们删除。