CVE-2017-7308 - packet_sock 结构体利用
在学习 BlindSide 的时候,需要具有对 CVE-2017-7308 利用的前置知识,所以简单学习一下这个比较老也比较经典的漏洞原理及利用。
TOC
AF_PACKET sockets 简介
用户可以使用 AF_PACKET 在设备驱动层(物理传输层)发送或者接受数据包。这样用户就能在物理层实现自己的协议,也可以嗅探包含以太网和更高层协议头部的数据包。
关键在于其维护的 buffer 结构,大致如下图所示,用户初始化这个结构可以指定 block 的大小和数量,frame 的大小,以及 block 内私有区域的大小等。kernel 内部由数组 pg_vec 指向每个 block,每个 block = block 头部 + 私有数据区域 + n 个 frame(好像 V3 就只有一个 frame)。
用户创建此缓冲区请求的结构体定义如下:
struct tpacket_req3 {
unsigned int tp_block_size; /* Minimal size of contiguous block */
unsigned int tp_block_nr; /* Number of blocks */
unsigned int tp_frame_size; /* Size of frame */
unsigned int tp_frame_nr; /* Total number of frames */
unsigned int tp_retire_blk_tov; /* timeout in msecs */
unsigned int tp_sizeof_priv; /* offset to private data area */
unsigned int tp_feature_req_word;
};
内核中各个相关结构体的指向关系:packet_sock (rx_ring) -> packet_ring_buffer (pg_vec) -> pgv (*buffer 指向存内存块的数组) -> tpacket_block_desc (内存块的头部) -> tpacket3_hdr (帧的头部)
创建缓冲区执行流程
packet_set_ring() -> init_prb_bdqc() -> prb_open_block()
漏洞最终会影响到的有效字段包括:
- max_frame_len: frame 最大值
- nxt_offset: frame 起始地址
static int packet_set_ring(struct sock *sk, union tpacket_req_u *req_u,
int closing, int tx_ring)
{
if (req->tp_block_nr) {
if (po->tp_version >= TPACKET_V3 && // <=== vulnerable
(int)(req->tp_block_size -
BLK_PLUS_PRIV(req_u->req3.tp_sizeof_priv)) <= 0)
goto out;
order = get_order(req->tp_block_size);
pg_vec = alloc_pg_vec(req, order); // <===[0] use __get_free_pages to alloc memory for each block
switch (po->tp_version) {
case TPACKET_V3:
/* Transmit path is not supported. We checked
* it above but just being paranoid
*/
if (!tx_ring)
init_prb_bdqc(po, rb, pg_vec, req_u); // <===[1] active block
break;
...
}
static void init_prb_bdqc(struct packet_sock *po,
struct packet_ring_buffer *rb,
struct pgv *pg_vec,
union tpacket_req_u *req_u)
{
pbd = (struct tpacket_block_desc *)pg_vec[0].buffer;
p1->kblk_size = req_u->req3.tp_block_size;
p1->blk_sizeof_priv = req_u->req3.tp_sizeof_priv;
p1->max_frame_len = p1->kblk_size - BLK_PLUS_PRIV(p1->blk_sizeof_priv);
prb_init_ft_ops(p1, req_u);
prb_setup_retire_blk_timer(po);
prb_open_block(p1, pbd); // <===[2] active block stage2
}
static void prb_open_block(struct tpacket_kbdq_core *pkc1,
struct tpacket_block_desc *pbd1)
{
pkc1->pkblk_start = (char *)pbd1;
pkc1->nxt_offset = pkc1->pkblk_start + BLK_PLUS_PRIV(pkc1->blk_sizeof_priv);
}
接收包执行流程,存放数据至缓冲区
- 检查包大小,不能超过 block 能容纳的最大大小 max_frame_len: tpacket_rcv()
- 确定存放地址:tpacket_rcv() -> packet_current_rx_frame() -> __packet_lookup_frame_in_block()
- 检查当前活跃的内存块是否有充足的空间存放数据包;
- 如果空间足够,保存数据包到当前的内存块,然后返回;
- 如果空间不够,就调度下一个内存块,将数据包保存到下一个内存块。
- 填充数据:tpacket_rcv() -> skb_copy_bits()
static int tpacket_rcv(struct sk_buff *skb, struct net_device *dev,
struct packet_type *pt, struct net_device *orig_dev)
{
if (po->tp_version <= TPACKET_V2) {
} else if (unlikely(macoff + snaplen >
GET_PBDQC_FROM_RB(&po->rx_ring)->max_frame_len)) {
pr_err_once("tpacket_rcv: packet too big, clamped from %u to %u. macoff=%u\n", snaplen, nval, macoff);
}
}
h.raw = packet_current_rx_frame(po, skb, TP_STATUS_KERNEL, (macoff+snaplen));
...
skb_copy_bits(skb, 0, h.raw + macoff, snaplen); // <===[overflow]
...
}
static void *__packet_lookup_frame_in_block(struct packet_sock *po,
struct sk_buff *skb,
int status,
unsigned int len
)
{
curr = pkc->nxt_offset;
pkc->skb = skb;
end = (char *)pbd + pkc->kblk_size;
/* first try the current block */
if (curr+TOTAL_PKT_LEN_INCL_ALIGN(len) < end) {
prb_fill_curr_block(curr, pkc, pbd, len);
return (void *)curr;
}
/* Ok, close the current block */
prb_retire_current_block(pkc, po, 0);
/* Now, try to dispatch the next block */
curr = (char *)prb_dispatch_next_block(pkc, po);
if (curr) {
pbd = GET_CURR_PBLOCK_DESC_FROM_CORE(pkc);
prb_fill_curr_block(curr, pkc, pbd, len);
return (void *)curr;
}
}
漏洞点
无符号数比较方法有错,当 req_u->req3.tp_sizeof_priv 为大数(最高 bit 为 1)时被视为负数,即使 tp_block_size <(u) tp_sizeof_priv 也能通过检查。
if (po->tp_version >= TPACKET_V3 &&
(int)(req->tp_block_size -
BLK_PLUS_PRIV(req_u->req3.tp_sizeof_priv)) <= 0)
goto out;
漏洞挖掘复现
syzkaller
漏洞利用复现
攻击模型
ubuntu 16.01: smap, smep, kaslr, SLAB(why not SLUB?)
OOB write
回顾错误的 tp_sizeof_priv 会产生哪些影响时,首先需要注意 [1] blk_sizeof_priv 是 unsigned short 类型的,对初始的 tp_sizeof_priv 有一个截断的操作。而触发 bug 的要求是 tp_sizeof_priv 最高位为 1,这个截断意味着可以忽略为了触发 BUG 带来的影响,减轻了利用的难度。注意 nxt_offset 是数据区的起始地址,控制了 blk_sizeof_priv 意味着可以控制任意的数据区起始地址。赋值 blk_sizeof_priv > kblk_size 就可以使得写入的时候越界,并且此时 max_frame_len 会为负数*,由于其是无符号数,会变成很大的正整数,导致之后 recv packet 时对 packet len 的检查也会通过。
p1->blk_sizeof_priv = req_u->req3.tp_sizeof_priv; // <===[1]
p1->max_frame_len = p1->kblk_size - BLK_PLUS_PRIV(p1->blk_sizeof_priv);
pkc1->nxt_offset = pkc1->pkblk_start + BLK_PLUS_PRIV(pkc1->blk_sizeof_priv);
同时需要注意在 recv packet 的逻辑上,会在 [1] 处检查当前接收到的包能不能在当前 block 放下,如果不能就直接放到下一个 block 里(之前的代码对 packet len 做了检查 [0],确保小于 max_frame_len ,正常流程下保证包能全部放到一个 block 里)。由于 [1] 处的 curr 是修改后的 nxt_offset ,会大于 end (start + size),所以这个检查又恰好使得之前 corrupted nxt_offset 在 [2] 处失效。
不过 prb_dispatch_next_block 在计算 next block 的 nxt_offset 时同样会因为构造的 blk_sizeof_priv 而越界,所以我们需要申请两个 block ,并且溢出第二个 block。
/* check packet length */
if (unlikely(macoff + snaplen > // <===[0] check
GET_PBDQC_FROM_RB(&po->rx_ring)->max_frame_len)) {
...
pr_err_once("tpacket_rcv: packet too big, clamped from %u to %u. macoff=%u\n", snaplen, nval, macoff);
}
/* first try the current block */
if (curr+TOTAL_PKT_LEN_INCL_ALIGN(len) < end) { // <===[1] check
prb_fill_curr_block(curr, pkc, pbd, len);
return (void *)curr;
}
/* Ok, close the current block */
prb_retire_current_block(pkc, po, 0);
/* Now, try to dispatch the next block */
curr = (char *)prb_dispatch_next_block(pkc, po); // <===[2] corrupt too
if (curr) {
pbd = GET_CURR_PBLOCK_DESC_FROM_CORE(pkc);
prb_fill_curr_block(curr, pkc, pbd, len);
return (void *)curr;
}
overwrite object 选取
由于 block 采用 free_page 进行分配,所以选择大块的 UAF 结构体比较合适,这里作者选用了 packet_sock 利用两个地方:
- timer 回调函数 packet_sock->rx_ring->prb_bdqc->retire_blk_timer:packet_sock 内部有一个 timer 结构体,覆盖回调函数和参数指针。(用户设置缓冲区的时候请求结构体包含一个 tp_retire_blk_tov 字段,可以用来设置 block 超时 retire 的时间,会调用到这个函数指针)。
retire_blk_timer.function(retire_blk_timer.data)
- packet_sock->xmit:该函数指针会在发送数据包的时候被触发。
Heap FengShui
注意 SLAB 分配器是以 slab 为单位申请的页,packet_sock 对应的是 kmalloc-2048,其对应的 slab 大小为 8K。为了尽可能让 slab 和 block 尽可能相邻步骤如下:
- 清掉缓存
- 申请许多
packet_sock
来消耗 kmalloc-2048 的 freelist - 申请许多 8k 页来消耗 pree page 的 freelist
- 申请许多
- 申请 2 个 block 包含待溢出的 block
- 申请几个
packet_sock
作为 victim (slab 内部也是随机的)
bypass kaslr and LPE
利用 timer,执行 native_write_cr4(X) 来 disable smep,smap;然后修改 packet_sock->xmit 指向用户空间的函数,执行 commit_creds(init_cred) 即可提权。
忽略了一些对齐方面的细节和一些关于 net namespace 的东西,调试跟了一下 exp 的流程,没有自己写 exp。