Show Notes

134 - FORCEDENTRY Sandbox Escape and NetFilter Bugs

Follow-up to the December post which covered an int overflow in the CoreGraphics PDF parser for the JBIG2 image format, which implemented a weird machine / mini architecture to execute code. This post covers the sandbox escape that was chained with it, which unlike the first bug, is a logic issue rather than a memory corruption.

Background A bit of background is necessary to understand the sandbox escape, because it’s a very intricate and complex bug. First, objects in obj-c have a metadata field called the isa field, which stores a pointer to the object’s class along with other stuff. Prior to iOS 14.5, this field was not protected by Pointer Authentication Codes (PAC). This allowed the exploit authors to fake different types of obj-c objects.

One critical type of object for this exploit is the NSPredicate object, which can be used to query and filter other objects such as NSArrays. These predicates can be created in the form of expressions through keywords, such as NSFunctionExpressions with the “FUNCTION” keyword to invoke an arbitrary method. With some tricks, this can be effectively be used to execute arbitrary obj-c code.

Sandbox escape Once arbitrary code can be ran, the sandbox escape comes into play via attacking a service over NSXPC (obj-c’s RPC mechanism). Basically with NSXPC, a service can export an object with a protocol attached which defines which methods and argument types can be used by the client. What’s noteworthy is, the type enforcement is pretty loose by design. Not only will that type be permitted and deserialized by the service, but any sub-class of that type will also be accepted and deserialized. In an extreme case, any protocol that accepts an NSObject somewhere can effectively receive almost any object, since most objects inherit from it.

The evaluateMobileSubscriberIdentity() method of the CoreTelephonyClient for the CommCenter service accepted NSObject types in it’s protocol. In theory, an attacker can pass an NSPredicate object here, which when deserialized will execute arbitrary obj-c in the CommCenter service context. In practice, there’s some defense in depth onNSPredicate objects where it won’t be evaluated until allowEvaluation() is called. Since evaluateMobileSubscriberIdentity() doesn’t expect predicates, this will never get called.

Fortunately for the attackers, there’s another object from the PrototypeTools framework called PTSection, which has a child NSArray of PTRow objects. These PTRow objects have conditions, which can be evaluated as predicates. By passing a PTSection with a PTRow that has a malicious NSPredicate, code execution in the CommCenter service can be achieved when the object is deserialized.

Fix The primary fix was introduced by Apple in iOS 15.1. They now forbid NSExpressions from performing operations with significant side-effects like creation and destruction of objects, neutering the ability to execute arbitrary obj-c.

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.

Starts off with the premise that a kernel read where user-space can leak the results of the read is a clear info disclosure, but what about getting the kernel to make a read, but where the attacker can’t see the result of the read? That can be used for DOS maybe, or perhaps leaked with a side-channel. Could it be possible to exploit it for something useful?

So they posit the idea of hardware MMIO. Some devices will use reads for two-way communication. They give two examples:

  • A imaginary device that performs and atomic read-and-reset of a state/last-error register
  • serial input device removing characters from an internal buffer when they are read

Would it be possible for an attacker to subvert those actions with perhaps unexpected reads or out of order events. Corrupting the device’s machine logic leading to an exploitable scenario. Its an interesting idea in my opinion.

Unfortunately they didn’t manage any such exploit, but the post does cover some of their initial recon finding potential regions and devices.Ultimately they try some fuzzing on the identified memory regions but didn’t have any success, though they were only looking for crashes. I imagine any such vulnerability would likely be much more subtle.