96 - Type Confusion in Android NFC, PHP-FPM Local Privilege Escalation, and CallbackHell
Android’s NFC stack uses TCB
or which is assumed to stand for “task control blocks”, which are used to track tasks that come from the NFC controller. The NFC specification supports a variety of formats for different types of NFC tags, and this tag type has to be tracked in the control block. Furthermore, each control block has an auxiliary buffer for “timer data”. When a tag is deactivated and another tag is activated by the controller, the tag type on the control block is reset and the auxiliary data is cleared to be overwritten. The problem is, previously setup timers are not cancelled when the tag is reset, meaning timer events that fire after a tag reset might operate on corrupted data due to the type confusion caused by the tag change.
A privilege escalation to root in PHP FPM from a worker process where the attacker has arbitrary memory read/write and has escaped the PHP sandbox.
The core issue is in the parent (root) process keeping some of its data in a shared memory region that is shared with the less privileged user/worker processes. The shared memory itself makes sense, workers track their status and FPM can look in on it and aggregate it. Specifically, there is an array of pointers to the status structures that can be tampered with by the workers also.
Side-Effects of Killing a Worker
There are two important side effects that come in the process of killing a worker process. FPM will see the process died, free its structure, and then allocate a new structure and worker to replace the dead one. Remember that the attacker controls the pointer FPM uses to look up these structures.
- Clear 1168 bytes. This happens as a normal part of the freeing process. FPM checks the dead process structure, see it was in use, and will
memset
the structure resulting in a 1168 byte write of 0s. This happens if the->used
field is not zero. - Set-0-to-1 after the free, FPM will try to alloc a new structure for the new proc. In this process is checks if the
->used
field is 0, if it is it sets it to1
and uses that structure.
Injecting data into the root process
Now this primtive isn’t terribly powerful but it is enough to start making some changes within the root process that can be useful. The author starts by turning on the catch_workers_output
setting, which is off (0) by default. Creating a way to inject content into the root process heap (stdout/stderr from worker processes will get buffered by the root process before being written to a log file).
Heap Overflow
Being able to control data on the heap, along with these two primtives is enough to start considering a heap-exploit. (There is a section here about exploit stability and preventing any further workers from spawning during exploitation that I’m not summarizing)
Ultimately the author turns the 0 to 1 primtives into a heap overflow by targeting the zlog_stream
structure which is created for every worker (though not initalized until data is written to stderr). By corrupting the buf.size
and len
fields which track the size of the backing buffer, and number of bytes already written respectively an overflow situation could be created by causing FPM to think it has more room to write.
** Arbitrary write**
Using the overflow they can target another worker process’s zlog_stream
structure, this time replacing its buf.data
(the backing buffer for stderr data) address with the desired memory address. Then writting on that processes stderr, will result in write relative to that address.
Code Execution
From this point, the write can target function pointers to get code execution. There is little concern regarding ASLR since worker processes are forked from the root process, so the children know the memory mapping of the parent.
Always a fun issue to see, the root of it being that a user-mode callback during a ResetDC
(Reset Device Context) can unexpectedly tamper with the device context data that the kernel thinks will be stable.
ResetDC as a function updates a specified printer or plotter device context (DC). Internally it seems that it does this by freeing the old one and allocating a new device context. A user-mode application calls ResetDC
, eventually reaching GreResetDCInternal
in the kernel. This function get a pointer to a device context object and calls hdcOpenDCW
which will execute a user-mode callback. During this callback the attacker can unexpectedly trigger another call to ResetDC
on the same handle again kicking off the above process a second time.
Ignoring the second run’s callback, allowing the second call of ResetDC
to proceed normally will result in the current device context (the one the first call has a pointer to) being freed. Once the second call finished, the first callback can return, and will have a use-after-free due to the dangling pointer to the device context.
Always a fun issue to see, the root of it being that a user-mode callback during a ResetDC
(Reset Device Context) can unexpectedly tamper with the device context data that the kernel thinks will be stable.
ResetDC as a function updates a specified printer or plotter device context (DC). Internally it seems that it does this by freeing the old one and allocating a new device context. A user-mode application calls ResetDC
, eventually reaching GreResetDCInternal
in the kernel. This function get a pointer to a device context object and calls hdcOpenDCW
which will execute a user-mode callback. During this callback the attacker can unexpectedly trigger another call to ResetDC
on the same handle again kicking off the above process a second time.
Ignoring the second run’s callback, allowing the second call of ResetDC
to proceed normally will result in the current device context (the one the first call has a pointer to) being freed. Once the second call finished, the first callback can return, and will have a use-after-free due to the dangling pointer to the device context.