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
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.