How The Tables Have Turned: An analysis of two new Linux vulnerabilities in nf_tables

We discussed this vulnerability during Episode 134 on 05 April 2022

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.