..

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)。

block_arr

block_struct

用户创建此缓冲区请求的结构体定义如下:

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);
}

接收包执行流程,存放数据至缓冲区

  1. 检查包大小,不能超过 block 能容纳的最大大小 max_frame_len: tpacket_rcv()
  2. 确定存放地址:tpacket_rcv() -> packet_current_rx_frame() -> __packet_lookup_frame_in_block() 
    1. 检查当前活跃的内存块是否有充足的空间存放数据包;
    2. 如果空间足够,保存数据包到当前的内存块,然后返回;
    3. 如果空间不够,就调度下一个内存块,将数据包保存到下一个内存块。
  3. 填充数据: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 利用两个地方:

  1. 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)
    
  2. packet_sock->xmit:该函数指针会在发送数据包的时候被触发。

Heap FengShui

注意 SLAB 分配器是以 slab 为单位申请的页,packet_sock 对应的是 kmalloc-2048,其对应的 slab 大小为 8K。为了尽可能让 slab 和 block 尽可能相邻步骤如下:

  1. 清掉缓存
    1. 申请许多 packet_sock 来消耗 kmalloc-2048 的 freelist
    2. 申请许多 8k 页来消耗 pree page 的 freelist
  2. 申请 2 个 block 包含待溢出的 block
  3. 申请几个 packet_sock 作为 victim (slab 内部也是随机的)

heap

bypass kaslr and LPE

利用 timer,执行 native_write_cr4(X) 来 disable smep,smap;然后修改 packet_sock->xmit 指向用户空间的函数,执行 commit_creds(init_cred) 即可提权。


忽略了一些对齐方面的细节和一些关于 net namespace 的东西,调试跟了一下 exp 的流程,没有自己写 exp。

final

参考