How The Tables Have Turned: An analysis of two new Linux vulnerabilities in nf_tables
tl;dr Two CVEs, one an integer overflow due to incorrectly assuming the compiler would optimize an enum
into a single byte, and the other some uninitialized kernel stack variables that could be exposed to userspace.
CVE-2022-1015 - An out-of-bounds access due to insufficient validation of input registers in nft_validate_register_store
and nft_validate_register_load
.
We’ll start off with nft_parse_register_load
though, which does two things, it parses the register (nft_parse_register) for a load expression, and validates that it won’t go out of bounds with a call to nft_validate_register_load
. Assuming all the checks passes, it writes the register index to a *u8
.
The problem is that the register index being written is just one byte (u8
) however it as an enum
that is being passed into the validation function and used for calculations. While the compiler may optimize an enum
to be just one byte in size, this is not a necessary optimization and the base type for an enum
is int
which is four bytes long.
And so when we enter nft_validate_register_load
the bounds check is written without concern for integer overflows as a one byte value couldn’t overflow the conditional:
if (reg * NFT_REG32_SIZE + len > sizeof_field(struct nft_regs, data))
When reg
is a full, four-byte value it becomes possible for an attacker to craft a register value such that the full calculation of reg * 4 * len
to be less than the field being compared against passing the check. And then this calculated index is truncated back in nft_parse_register_load
and only the least significant byte is used as an index later.
The same issue exists in the code for validating a store expression also.
CVE-2022-1016 - This one is simpler in concept. In nft_do_chain
the register structure is not initialized, so expressions could read the uninitialized register content. While the direct values could not be returned to userspace it is possible for an expression to have different behaviours based on the values read, and so leak the value to userspace.
Exploitation Primitives First off the starting primitives. The core vulnerability existed in the validate functions for loads and stores, but you still need expressions that actually do those actions with user-controllable data. Two options were found for this
nft_payload
provided a relative write to to the stack of up to 0xFF bytes of arbitrary data.nft_bitwise
provided a relative read and write primitive, of up to 0x40 bytes of arbitrary data.
Exploitation
The author started by exploring what data was visible to these primitives under different calling conditions, while the stack is far flexible to groom than the heap, you do still have variant calling patterns that can leak different bits of information. Settling on calling the nft_bitwise
read primtive from an output
chain, which allowed them to load some kernel pointers from the udp_sendmsg
function call stack.
They could then the value these to user-space by crafting an expression that would accept/drop packets based on the read value. Creating a sort of side-channel to expose the value it read despite not having direct, readable access to it.
Unfortunately, they could not use the same output
chain context for the control flow hijack, as how alignments worked out, they would end up corrupting a byte of the stack canary. So, they switched to the input
context which provided better access to snipe a return address from the stack.