-->
Right after my 1st week of prelims was Project Sekai CTF - how could I miss this event?
I played with slight_smile and got 11th place, way higher than I expected given the overall difficulty of the challenges. We were just slightly below 10th place (in fact, near the end we kept swapping positions with Black Bauhinia because of dynamic scoring)
All the pwns were really cool and I had a lot of fun solving them. These are my
writeups for pwn/nolibc
, pwn/speedpwn
, and pwn/life-sim-2
(unfortunately
windows kernel and v8 0-day are just slightly out of my skill level)
No libc means no vulnerability right?!
Author: Marc
Solves: 56
The challenge binary was statically linked with a custom malloc/free implementation. No source was provided, so we gotta do a bit of reversing :(
We first need to register
and login
to the challenge.
register
We can create a user with a username and password. Upon creation, a large chunk
in the heap is allocated for the user’s data, and a pointer to this is stored in
the global array. state[4]
stores the number of created strings.
Then after logging in, the user can interact with the following functions:
create_string
We can create a string with a certain length between 0x0 and 0x100. The string
is created with malloc(size + 1)
, and we can write size + 1
bytes to it.
Note that the malloc
implementation is custom which we’ll look at later.
After the string is created, a pointer to it is appended to the user’s data, and the string count is incremented.
delete_string
Similarly, we can delete a string at a given index. After the string is freed, any pointers after the deleted string are shifted back by 1, and the string count is decremented.
view_string
This will print all strings in the user’s data. Nothing much of interest here.
save_strings_to_file
Each string is saved as a line in a user-specified file.
load_strings_from_file
Lines are read from a user-specified file and appended to the user’s existing list of strings.
My first thought was to use save_strings_to_file
to
overflow the heap (since there’s no actual check on the total length of the
chunk data being copied into the 0x7fff
size chunk). Let’s first analyze the
malloc
and free
implementations.
malloc
This malloc
is quite a simple implementation that only has 1 freed list,
sorted by increasing size. This is the shape of a chunk allocated with malloc
:
(size
doesn’t include the header size btw)
The program stores a pointer to the smallest freed chunk as the list head, and
cycles through the list until a chunk with size greater than or equal to
requested size is found, eventually reaching the forest chunk. If even the
forest chunk is too small to service the request, malloc
will return 0.
Then, if the victim chunk is large enough to be split, it will be split into the last remainder.
free
free
is slightly more complicated, and since the source is kinda long I won’t
include the decomp here. Basically, what it does is it again cycles through the
freed list, looking for a place to insert the freed chunk into. Once inserted,
it also updates the smallest_freed_chunk
global pointer if needed. Finally, it
calls another function which merges adjacent free chunks, and updates the size
of forest_chunk
to make sure &forest_chunk + forest_chunk->size
always
points to the end of the heap.
A very important thing that I missed at first, is that forest_chunk
is
initialized as the start of the writable segment in memory, and its size is set
to +0x10000 from the start of this segment, pointing to the end of heap. Right
after it are all our other global values:
So if forest chunk were to overflow, we can start overwriting all these values!
Earlier, we saw that size
of a chunk doesn’t include its header size. But if
the size of forest_chunk
was initialized to be end_of_heap - start_of_heap
(0x10000), and this doesn’t include the header size, then… won’t this lead to
a 0x10 overflow?
Looking at the load_strings_from_file
function, it allocates a chunk of size
0x20
for the filename, and another of size (0x7fff + 1) & 0xfffffff0 = 0x8000
for the file to be read into. Then, it starts to malloc
more chunks
for the strings read from the file. After registering a user, our forest_chunk
is of size 0xbf80
.
So, the amount we need to allocate to just fill up the heap is 0xbf80 - 0x8000 0x10 - 0x20 - 0x10 = 0x3f40
. Since we can allocate up to 0x7ff
chunks of
maximum size 0x100 + 0x10
, we could definitely do this manually using
create_string
. Or we can save ourselves a lot of time by just setting the file
we read from to be /dev/stdin
, and feed in the values we want!
Before:
After:
As we can see, we successfully overflowed into the global values, specifically the section:
What do these numbers correspond to? Taking a look in IDA:
If we look at the xrefs for each of the numbers, they are passed into rax
before certain syscalls are made. For example, looking at +0x15008
, it’s
referred to in the open_and_read_file
function:
This is perfect for getting a shell - we can just overwrite the value with the
execve
syscall number, and call load_strings_from_file
with /bin/sh
.
Unfortunately, this solution only works on local. On remote, the binary can’t
access /dev/stdin
or any of the other alternatives (I tried /proc/self/fd/0
,
/dev/tty
etc), so we can’t read in the overflow directly like that :(
I probably overthought this solution anyway…
The other alternative is to simply use create_string
to overflow the heap
directly. After registering, the size of forest_chunk
is still 0xbf80
. From
create_string
, we can fill up the heap with 0xbf * 0x100
chunks. The last
chunk left, 0x80
, will be used to overflow.
Note that we need to make a bit of space for the malloc(0x20)
call when
reading in the filename, if not the program will crash.
When running my exploit on remote, I realized how slow it was:
Nevertheless, after about 2 minutes we get our flag! Our final exploit:
Beat the bot and get the flag! First solver gets a $77.77 bounty :)
Author: Zafirr
Solves: 27
This challenge was released on the second day at 12:00am SGT, and there was a
bounty for first blood! I woke up at 12:01am to take a look, found the overflow
at 12:04am, and realized that I had no idea how to turn it into arb write. At
12:06am, I decided there was no way I would solve it in time LOL. After that I
went back to life-sim-2
. The Flat Network Society blooded the challenge at
1:45am - no chance I could have gotten it.
After solving life-sim-2
I took another look at this challenge.
Source was given for this challenge.
We can play some kind of number comparison game with the bot in the program. This is the function that does the comparison:
__builtin_popcountll
counts the number of set bits in a number.
Basically, it first compares the number of set bits in both numbers, then cycles through each bit of both numbers, starting from the least significant bit, and compares them, returning the value of the first bit (of the first number) that is different.
The challenge allows us to do the following: