Show Notes

146 - Python 3 UAF and PS4/PS5 PPPoE Kernel Bug

Couple of bugs originating in Solana’s JIT: one an optimization issue, the other a bad instruction choice, both found through fuzzing.

Denial of Service - Resource Exhaustion ($100,000 bounty) the first bug was a memory leak from the following code:

entrypoint:
  r0 = r0 + 255
  if r0 <= 8355838 goto -2
  r9 = r3 >> 3
  call -1

What happens here is that the first two lines just iterate and add 255 to r0 to run for exactly 65534 instructions. Each Solana program is limited to only 65535 instructions. After that it does a calculation for the 65535th instruction, and then a call -1 which gives an unresolved symbol error as -1 isn’t a defined function. This error will allocate a buffer to store what the unresolved symbol was.

The problem is that the instruction count is only checked at the end of each basic block, so it checked and updated after the if and added the call. So after the call happens, the instruction count is updated. It sees that it has gone over 65535 instructions and issues a new error for exceeding the maximum instruction count. This new error overwrites the previously error, overwritting the pointer to the allocated buffer and never freeing it.

Persistent read-only data corruption This one has a much simpler root cause, the cmp_immediate instruction would use the x86 opcode 0x81 for all compares including 8bit wide compares, but 0x80 should be used for 8bit immediate. This would happen while looking up an offset to a memory address, and if some non-zero values were in the upper bits, the comparison could end up approving a write to a read-only memory location thinking it is intended for a different location.

Taking an unexpected reference to a memoryview object resulting in a use-after-free when the parent of said object is destroyed. Though this is a rather low impact bug because it requires control over the code being executed, so one could just write an os.system(...) call or something similar. The post is a great writeup on the process of exploiting a use-after-free and has some notes about exploit stability as the author targeted having their exploit run across all versions of Python 3.

By providing a custom File class that extends io.RawIOBase a user could provide a malicious imeplement of the readinto method. This is the method that a BufferedReader will call into to actually read the underlying resource. It passes in a memoryviewwhich is the buffer into which the the contents should be read. If the readinto method takes a reference to this buffer, placing it into a global variable then after the BufferedReader is destroyed, any use of the global variable would result in a use-after-free. The root issue just seems to be not doing a proper reference count, however this bug is not slated to be fixed too soon. It requires someone able to inject arbitrary python, so you could simply use os.system(...) in most cases to gain code execution.

Exploitation - Primitives

First step of the exploitation was just to reclaim the recently freed memory with a controlled object type. They did this by allocating a List of the same size as the buffer, and could consisently reclaim the improperly freed memoryview. With this they now have two ways of accessing the memory, using the originally grabbed reference to the memoryview, this gives them read/write access to all the bytes. And now as part of a list, where Python thinks it is a list of PyObject buffers.

This List is what is used to craft the arbitrary read/write primitive.

Firstly, to know where any python object is in memory the id() function call, will return the address, so ASLR for those objects is a non-issue. Then to get content they control directly in memory they used a bytes object (like a b"string".) The payload of the string would be 32 bytes away from the address of the object itself.

Using this they could craft any fake Python object, place the pointer to the bytes payload into the memoryview and then access the object through the List object that reclaimed the memoryview’s memory. Accessing it through the list will parse the crafted object from the bytes payload. The bytes payload for an arbitrary memory read/write primitive was a bytesarray object. this object would contain a pointer to a backing buffer which could be accessed like any other array. So by crafting a fake bytesarray pointing to the desired memory location, one could read/write any memory.

Exploitation - Chain

Now with the arbitrary read/write the rest was fairly straight forward, but they did take some extra steps for stability. To start they walked memory looking for the ELF header to calculate the CPython base address. Parsed the headers to find the PLT entries and resolve the address for system from libc. Then faked a PyObject with a fake type object that gives them control over various object method/function pointers. Replacing one with the system address and call it for a shell.

Great writeup even if the impact isn’t huge it was a good read.

Heap overflow in the mbuf zone in the PPPoE driver, which the PS4/PS5 borrow from NetBSD. The issue is the fact that pppoe_send_padr() can calculate a packet length that exceeds MCLBYTES (2048 bytes). When an mbuf cluster is allocated to hold this packet data via pppoe_get_mbuf(), if the length exceeds MHLEN (256), it’ll allocate a cluster of MCLBYTES length. While it’s not possible to send a packet larger than MCLBYTES in one go, you can send two packets that will get combined. In this case, they used the ACCOOKIE tag and the RELAYSID tag of 1400 bytes each, which combined to writing 2800 bytes into a 2048-byte mbuf cluster.