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
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.
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 ๐
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)
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:
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.
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:
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 :)
Cheminventory
Storing explosive chemicals in the kernel is a wonderful idea! Surely nothing can go wrongโฆright?
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
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:
The code here is pretty secure, so letโs move on.
DO_READ and DO_DELETE
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.
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:
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:
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:
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.
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 on
msg_msg.bk - also important later on
msg_msg.text - can leak the msg_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.
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:
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:
(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.
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:
(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.
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.
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.
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.
Stack pivot 2
Letโs take a step back and look at our current sk_buff/pipe_buf object:
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:
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:
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:
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:
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.