tl;dr
- Sanitizing request causes null byte overflow which corrupts type
- Processing corrupted request doesn’t remove it from
incoming_queue
- Reaping corrupted request still leaves it in
incoming_queue
causing UAF - Setup crosscache to abuse UAF
- UAF provides free primitive through double reset
Challenge Points: 991
No. of solves: 5
Challenge Author: k1R4
Challenge Description
An unnecessarily complex palindrome checker, implemented as a kernel driver. What could possibly go wrong?
Handout has the files bzImage, rootfs.ext3, run.sh, the module (.ko) and its source code. A sample .config is also included to indicate what security features are compiled in.
Initial Analysis
Mitigations
SMEP, SMAP, KPTI and KASLR are all enabled as seen in run.sh
. Additionally typical slub hardening features are enabled and modprobe_path
overwrite isn’t possible due to CONFIG_STATIC_USERMODEHELPER
.
Although CONFIG_RANDOM_KMALLOC_CACHES
makes exploitation harder by reducing chances of similarly sized objects being in the same cache, it doesn’t prevent cross-cache attacks.
The Module
The module is a glorified and overcomplicated palindrome checker. Users can submit requests and operate on submitted requests through ioctl. There are two queues to keep track of requests, incoming_queue
and outgoing_queue
. The functionalites offered by the ioctl are:
- For incoming queue,
- QUEUE => add request to rear of incoming_queue
- SANITIZE => update request at front by translating [A-Z|a-z] to [A-Z] & discarding other chars
- RESET => pop request at front, if sanitized, reset to raw and send it to rear, otherwise discard
- PROCESS => pop request at front, check if palindrome and add to rear of outgoing_queue
- For outgoing queue,
- REAP => pop request at front, provide verdict if it was a palindrome or not
- QUERY => returns available capacity in both queues
More on requests
1 | typedef struct request_t |
ptype
indicates whether the request is unprocessed [RAW, SANITIZED] or processed [PALINDROME, NONPALINDROME]magic
is used to check for corrupted requests (not really :P)str
is the buffer where the string for a request is storedsanstr
is the buffer where the sanitized string for a request is stored
Requests are allocated in a separate cache due to SLAB_NO_MERGE
. Since there are no pointers or critical members in request, to leverage any bug in the driver, we will have to perform a cross-cache attack.
1 | pm_cache = kmem_cache_create("palindromatic", TARGET_SZ, __alignof__(request_t), |
Bugs
The main bug is null byte overflow, that occurs in pm_sanitize_request()
if the str
buffer is completely filled with characters in [A-Z].
1 | for(int i = 0; i < STRING_SZ; i++) |
If the null byte overflow occurs, the ptype
of request below (in memory) will be corrupted. This by itself is a fairly harmless bug. But now taking a look at pm_process_request()
1 | request_t *req = pm_queue_peek(&incoming_queue); |
Majority of the code in processing doesn’t concern the exploitation, so it can be ignored. Initially the request at front of incoming_queue
is added to rear of outgoing_queue
. The request is only removed from the incoming_queue
at the end of the if clauses. However when processing request with corrupted ptype
, it won’t enter either if clause, so it will never get removed from incoming_queue
. This can lead to UAF if the request is reaped from outgoing_queue
but it still remains in incoming_queue
.
Resetting a request in incoming_queue
will eventually free it, giving a potential free primitive on UAF request.
Exploit Strategy
Triggering UAF (Stage 1)
- Spray requests, filling the
incoming_queue
- Triggering
SANITIZE
now has a high chance of corrupting another request - Now
PROCESS
one request at a time andQUERY
capacity - If capacity of
incoming_queue
doesn’t change, it means a corrupted request was processed RESET
to send the corrupted request back to rear ofincoming_queue
PROCESS
remaining requests andREAP
them all- Now corrupted request is left in
incoming_queue
while it has already been freed by reaping.
Convert UAF request to pipe_buffer (Stage 2)
For the remainder of the exploit, I used pipe_buffer
and msg_msgseg
. The reason being, I came across this cool technique, which is essentially modifying flags
of a pipe_buffer
to PIPE_BUF_FLAG_CAN_MERGE
. If the pipe was spliced from a readonly file, writing to the pipe after this will actually write to the file. In short, another primitive can potentially be abused to revive the infamous DirtyPipe. msg_msgseg
is used to overlap on the pipe_buffer
to leak and modify it.
- Since all requests have been freed, the slab with UAF request is also returned to allocator
- Spray a lot of
pipe_buffer
usingpipe()
- This will eventually re-allocate the slab with UAF request (cross-cache)
- This is quite reliable since
pipe_buffer
and requests are similarly sized and have slabs of same order - Write some content to the pipes
- Trigger free of victim
pipe_buffer
, through resetting UAF request twice RESET
once to setptype
asRAW
and once more to actually free it- Spray some more
pipe_buffer
to occupy slot of victim - Write different content to the pipes in the second spray
- Check content of all pipes from first spray to find victim pipe
Overwrite pipe_buffer flags (Stage 3)
A quick look at pipe_buffer
:
1 | struct pipe_buffer { |
The actual pipe_buffer
object that is allocated is actually a ring of pipe_buffer
structs. Initially the object is empty, when its written to for the first time, a pipe_buffer
is added to the ring. This can also be triggered by F_SETPIPESZ
function of fcntl()
. Additionally, splicing also adds a new pipe_buffer
to the ring. Armed with this information, the exploit might make more sense.
- Now free all
pipe_buffer
byclose()
, while holding a reference to the victimpipe_buffer
- Spray
msg_msgseg
usingmsgsnd()
msg_msgseg
is used in instead ofmsg_msg
since it has a smaller header- Doesn’t cause issues when overlapped with
pipe_buffer
msg_msgseg
of size 0x400 is used since its the same size aspipe_buffer
objectmsg_msgseg
now completely overwrites thepipe_buffer
object- Splice from
/etc/passwd
to the victim pipe, this will add a newpipe_buffer
struct to the ring - Using
msgrcv()
thepipe_buffer
can be leaked - Use leak to craft fake
pipe_buffer
withflags = PIPE_BUF_FLAG_CAN_MERGE
- Spray
msg_msgseg
again, this time containing fakepipe_buffer
- Writing to victim
pipe_buffer
will now, write to/etc/passwd
This is because of the logic in pipe_write()
which writes into the backing page of the lastly added pipe_buffer
in the ring, if PIPE_BUF_FLAG_CAN_MERGE
is set. Finally use su
to login as root :D
Conclusion
I learnt a lot about crosscache when working on this challenge. Hope you learnt something too!
You can find the full exploit here
Flag: bi0sctf{p4l1ndr0me5_4r3_pr0bl3m4t1c_frfr_b851ea94}