Lag and Crash 2024
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!
Lucky Plaza
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
Analysis
bool menu(vector<long> &vec) |
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.
void guess_lucky_number(vector<long> &vec) |
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!
void view_lucky_number(vector<long> &vec) |
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.
std::vector
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 👍
FROM ubuntu:22.04@sha256:cb2af41f42b9c9bc9bcdc7cf1735e3c4b3d95b2137be86fd940373471a34c8b0 AS app |
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 /* '*' */ |
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 |
or in generic form (tail - head) / sizeof(long)
. We can confirm this in the
source for libstdc++
:
// Line 567 |
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.
// [23.2.4.3] modifiers |
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
.
Exploit
From here, we know our vector is stored on the heap. Are there any targets worth attacking using our OOB?
Attack other chunks/tcache struct
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 |
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.
Cross-page OOB
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 :)
#!/usr/bin/env python3 |
Cheminventory
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 :)
Analysis
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.
DO_CREATE
case DO_CREATE: { |
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.
About pages and slabs
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:
struct chemical { |
0xffff888100f8cd00: 0x0000000000000000 0x0000000000000000 -> name.. |
The code here is pretty secure, so let’s move on.
DO_READ and DO_DELETE
case DO_READ: { |
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.
DO_REPLACE
Now, for the most interesting and complicated function - DO_REPLACE
.
case 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…
Step 1: UAF
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 |
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:
msg_queue = msgget(IPC_PRIVATE, 0666 | IPC_CREAT); |
And it can be freed like this:
msgrcv(msg_queue, &buf, sizeof(buf) - sizeof(long), mtype, 0); |
msg_msg
is allocated in this function:
static struct msg_msg *alloc_msg(size_t len) |
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
/* one msg_msg structure for each message */ |
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:
struct itimerspec its; |
And it will be freed a short while after closing timer_fd
.
The struct is allocated as such:
SYSCALL_DEFINE2(timerfd_create, int, clockid, int, flags) |
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:
struct timerfd_ctx { |
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 |
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 |
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
:
- kbase leak
- kheap leak in
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.
Step 2: Cross-cache attack
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)
Step 3: Fake chemicals
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 |
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 themsg_msg.idx
we placed at the start of user input
Last 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.
if (likely(prev->next == entry && next->prev == entry)) |
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.
Leaking more 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:
int queue = msgget(IPC_PRIVATE, 0666 | IPC_CREAT); |
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!
Step 4: Arbitrary free
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:
struct tty_struct { |
(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.
struct tty_operations { |
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
struct pipe_buffer { |
This struct is a lot simpler and smaller.
And we can see it being allocated here:
struct pipe_inode_info *alloc_pipe_info(void) |
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.
Step 5: RIP control via overlap
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
again
Here are the structs of msg_msg
and pipe_buf
in memory:
msg_msg |
(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:
struct sk_buff { |
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:
char payload[1024 - 320]; |
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.
Step 6: ROP chain
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.
void (*release)(struct pipe_inode_info *, struct pipe_buffer *); |
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.
Stack pivot 1
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] |
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 |
Stack pivot 2
Let’s take a step back and look at our current sk_buff/pipe_buf
object:
gef> x/200gx 0xffff8881026ca400 |
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 ; |
Now our second stack will point to +0x50
. Here we have lots of space, so let’s start our final ROP chain!
Final ROP chain
We want to finally call this:
commit_creds(prepare_kernel_cred(&init_task)); |
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):
void save_state() { |
Lastly, our second stack:
+0x50: pop rdi ; ret ; |
After constructing this, we spray sk_buff
onto pipe_buf
, release all pipe_buf
and should get root!
Step 7: Running the exploit
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:
from pwn import * |
Note: I ran it with python3 solve.py NOTERM
because the VM would output annoying carriage returns that cleared the
output.
Conclusion
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.