|
|
Message-ID: <1b176761-5462-4f25-db12-1b988c81c34a@gmail.com>
Date: Mon, 28 Mar 2022 20:28:21 +0200
From: David Bouman <davidbouman35@...il.com>
To: oss-security@...ts.openwall.com
Subject: Linux kernel: CVE-2022-1015,CVE-2022-1016 in nf_tables cause
privilege escalation, information leak
Hello list,
I'm reporting two linux kernel vulnerabilities in the nf_tables
component of the netfilter subsystem that I found.
CVE-2022-1015 pertains to an out of bounds access in nf_tables
expression evaluation due to validation of user register indices. It
leads to local privilege escalation, for example by overwriting a stack
return address OOB with a crafted nft_expr_payload.
CVE-2022-1015 is exploitable starting from commit 345023b0db3
("netfilter: nftables: add nft_parse_register_store() and use it"),
v5.12 and has been fixed in commit 6e1acfa387b9 ("netfilter: nf_tables:
validate registers coming from userspace.").
The bug has been present since commit 49499c3e6e18 ("netfilter:
nf_tables: switch registers to 32 bit addressing"), but to my knowledge
has not been exploitable until v5.12.
CVE-2022-1016 pertains to uninitialized stack data in the nft_do_chain
routine. CVE-2022-1016 is exploitable starting from commit 96518518cc41
(original merge of nf_tables), v3.13-rc1, and has been fixed in commit
4c905f6740a3 ("netfilter: nf_tables: initialize registers in
nft_do_chain()").
I will be releasing a detailed blog post and exploit code for both
vulnerabilities in a few days.
Root cause CVE-2022-1016: (it is the shortest, so I will begin with it)
The nft_do_chain routine in net/netfilter/nf_tables_core.c does not
initialize the register data that nf_tables expressions can read from-
and write to. These expressions inherently exhibit side effects that can
be used to determine the register data, which can contain kernel image
pointers, module pointers, and allocation pointers depending on the code
path taken to end up at nft_do_chain.
```
unsigned int
nft_do_chain(struct nft_pktinfo *pkt, void *priv)
{
const struct nft_chain *chain = priv, *basechain = chain;
const struct net *net = nft_net(pkt);
struct nft_rule *const *rules;
const struct nft_rule *rule;
const struct nft_expr *expr, *last;
struct nft_regs regs; // <-------- VULNERABLE! NOT INITIALIZED.
unsigned int stackptr = 0;
struct nft_jumpstack jumpstack[NFT_JUMP_STACK_SIZE];
bool genbit = READ_ONCE(net->nft.gencursor);
struct nft_traceinfo info;
info.trace = false;
if (static_branch_unlikely(&nft_trace_enabled))
nft_trace_init(&info, pkt, ®s.verdict, basechain);
do_chain:
if (genbit)
rules = rcu_dereference(chain->rules_gen_1);
else
rules = rcu_dereference(chain->rules_gen_0);
next_rule:
rule = *rules;
regs.verdict.code = NFT_CONTINUE;
for (; *rules ; rules++) {
rule = *rules;
nft_rule_for_each_expr(expr, last, rule) {
if (expr->ops == &nft_cmp_fast_ops)
nft_cmp_fast_eval(expr, ®s);
else if (expr->ops == &nft_bitwise_fast_ops)
nft_bitwise_fast_eval(expr, ®s);
else if (expr->ops != &nft_payload_fast_ops ||
!nft_payload_fast_eval(expr, ®s, pkt))
expr_call_ops_eval(expr, ®s, pkt);
...
```
Root cause CVE-2022-1015:
(below is pasted from my original security@...nel.org report)
Hello, I'm mailing to report a vulnerability I found in nf_tables
component of the netfilter subsystem. The vulnerability gives an
attacker a powerful primitive that can be used to both read from and
write to relative stack data. This can lead to arbitrary code execution
by an attacker.
In order for an unprivileged attacker to exploit this issue,
unprivileged user- and network namespaces access is required
(CLONE_NEWUSER | CLONE_NEWNET). The bug relies on a compiler
optimization that introduces behavior that the maintainer did not
account for, and most likely only occurs on kernels with
`CONFIG_CC_OPTIMIZE_FOR_PERFORMANCE=y`. I successfully exploited the bug
on x86_64 kernel version 5.16-rc3, but I believe this vulnerability
exists across different kernel versions and architectures (more on this
later).
Without further ado:
The bug resides in `linux/net/netfilter/nf_tables_api.c`, in the
`nft_validate_register_store` and `nft_validate_register_load` routines.
These routines are used to check if nft expression parameters supplied
by the user are sound and won't cause OOB stack accesses when evaluating
the expression.
From my 5.16-rc3 kernel source
(d58071a8a76d779eedab38033ae4c821c30295a5: Linux 5.16-rc3):
nft_validate_register_store:
```
static 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) {
...
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;
}
}
```
nft_validate_register_load:
```
static int nft_validate_register_load(enum nft_registers reg, unsigned
int len)
{
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;
return 0;
}
```
The problem lies in the fact that `enum nft_registers reg` is not
guaranteed only be a single byte. As per the C89 specification, 3.1.3.3
Enumeration constants: `An identifier declared as an enumeration
constant has type int.`.
Effectively this implies that the compiler is free to emit code that
operates on `reg` as if it were a 32-bit value. If this is the case (and
it is on the kernel I tested), a user can forge an expression register
value that will overflow upon multiplication with `NFT_REG32_SIZE` (4)
and upon addition with `len`, will be a value smaller than
`sizeof_field(struct nft_regs, data)` (0x50). Once this check passes,
the least significant byte of `reg` can still contain a value that will
index outside of the bounds of the `struct nft_regs regs` that it will
later be used with.
Take for example a `reg` value of `0xfffffff8` and a `len` value of
`0x40`. The expression `reg * 4 + len` will then result in `0xffffffe0 +
0x40 = 0x20`, which is lower than `0x50`. This makes that a value of
`0xf8` is recognized as a valid index, and is subsequently assigned to a
register value in the expression info structs.
Here is a snippet of the x86_64 assembly code that these functions might
generate:
```
Disassembly of section .text:
0000000000002ed0 <nft_validate_register_store>:
2ed0: e8 00 00 00 00 callq 2ed5
<nft_validate_register_store+0x5>
2ed5: 55 push %rbp
2ed6: 48 89 e5 mov %rsp,%rbp
2ed9: 41 54 push %r12
2edb: 85 f6 test %esi,%esi
2edd: 75 2b jne 2f0a
<nft_validate_register_store+0x3a>
2edf: 81 f9 00 ff ff ff cmp $0xffffff00,%ecx
2ee5: 75 49 jne 2f30
<nft_validate_register_store+0x60>
2ee7: 45 31 e4 xor %r12d,%r12d
2eea: 48 85 d2 test %rdx,%rdx
2eed: 74 3a je 2f29
<nft_validate_register_store+0x59>
2eef: 8b 02 mov (%rdx),%eax
2ef1: 83 c0 04 add $0x4,%eax
2ef4: 83 f8 01 cmp $0x1,%eax
2ef7: 77 30 ja 2f29
<nft_validate_register_store+0x59>
2ef9: 48 8b 72 08 mov 0x8(%rdx),%rsi
2efd: e8 7e da ff ff callq 980 <nf_tables_check_loops>
2f02: 85 c0 test %eax,%eax
2f04: 44 0f 4e e0 cmovle %eax,%r12d
2f08: eb 1f jmp 2f29
<nft_validate_register_store+0x59>
2f0a: 83 fe 03 cmp $0x3,%esi
2f0d: 76 21 jbe 2f30
<nft_validate_register_store+0x60>
2f0f: 45 85 c0 test %r8d,%r8d
2f12: 74 1c je 2f30
<nft_validate_register_store+0x60>
2f14: 41 8d 04 b0 lea (%r8,%rsi,4),%eax
2f18: 83 f8 50 cmp $0x50,%eax
2f1b: 77 1b ja 2f38
<nft_validate_register_store+0x68>
2f1d: 48 85 d2 test %rdx,%rdx
2f20: 74 04 je 2f26
<nft_validate_register_store+0x56>
2f22: 85 c9 test %ecx,%ecx
2f24: 75 0a jne 2f30
<nft_validate_register_store+0x60>
2f26: 45 31 e4 xor %r12d,%r12d
2f29: 44 89 e0 mov %r12d,%eax
2f2c: 41 5c pop %r12
2f2e: 5d pop %rbp
2f2f: c3 retq
2f30: 41 bc ea ff ff ff mov $0xffffffea,%r12d
2f36: eb f1 jmp 2f29
<nft_validate_register_store+0x59>
2f38: 41 bc de ff ff ff mov $0xffffffde,%r12d
2f3e: eb e9 jmp 2f29
<nft_validate_register_store+0x59>
```
the `lea` instruction at `2f14` will multiply `%rsi` (reg) by 4 and add
`%r8` len to it.
I created a working local privilege escalation exploit by using such an
out of bounds index to copy stack data to the actual register area
(declared in nf_tables_core.c:nft_do_chain). Then, I wrote a a few nft
rules that drop or accept packets depending on whether the targeted byte
is greater than the constant comparand in the rule or not. This way I
could create a binary search procedure that could determine the value of
the leaked byte by registering whether the packet was dropped or not.
This results in a kernel address leak.
Finally, I used a nft payload expression to write my arbitrary data
supplied in a packet to the stack in order to overwrite a return address
and execute a ROP chain.
An alternative exploitation strategy would be to overwrite to verdict
register (including its chain pointer) to arbitrary values, as you can
now get an register index of 0 in the same manner.
------------------------------
David Bouman
Powered by blists - more mailing lists
Please check out the Open Source Software Security Wiki, which is counterpart to this mailing list.
Confused about mailing lists and their use? Read about mailing lists on Wikipedia and check out these guidelines on proper formatting of your messages.