TBTL CTF
Last updated
Last updated
Participated under the banner of HCS, ranked 20 out of 792 teams.
Challenge | Category | Points | Solves |
---|---|---|---|
May the fastest code win! Just make sure you get a green light from the security team before racing.
nc 0.cloud.chals.io 10840
we're given the following files
server.py
is the what will handle our connection.
the server takes two input, a filename and it's content in base64.
notice how the program does a slow print, instead of the regular ones using slow_print()
which is a custom function as follows:
what's relevant next is what how's the file content is being processed. we can take a look at this in check_compile_and_run()
the function will check for the shasum of the content that we gave earlier. which mean it will only allows for anything that has the same content as fibonacci.c
and primes.c
both fibonacci.c
and primes.c
contains normal code and provide as a test case for what the intended functionality of the server is.
if everything passes the check, it will then compile and execute our code using gcc.
the problem here is the slow_print()
as it makes executing one line of code takes quite a bit of time.
such that say if we provide the server with a valid file content, and reaching this part
it will take its time printing the text bit by bit before actually compiling the and running the executable. within that timeframe we can make another connection rewriting the valid content with malicious one and eventually when that line of code is done, it will instead compile the malicious content and running it.
first we will have two connection to the server, giving the same filename.
then we will give the valid content and wait for the compile message
then when it reaches up to that point, it will already passes the check, which means our other connection can overwrite it and close it to avoid any further error
as for the malicious content, it will be a simple cat to the flag
run it againts the server
below is the full exploit:
Flag: TBTL{T1m3_0f_chEck_70_tIM3_0f_PWN}
Last year's solutions were unintended, let's try it again.
nc 0.cloud.chals.io 12348
my first CPP heap exploitation challenge
here's what we're given
it's a C++ pwn challenge, which is I'm very unfamiliar. thankfully they gave us the source code, cause reversing C++ is a pain.
first thing first, here's a win function:
interacting with the binary it asks for a string followed by pre defined commands.
here's the commands available as well some help to use it
the main function is not necessarily lengthy but coming from someone who does little to no interaction with C++, it can take some time to understand, so I'll explain it piece by piece.
first, it will prompt for an input and store it in string s
, the string must not less than 0x20
.
it then goes for a infinite loop and prompt for a command
we all know string, but what is istringstream
?
basically something like tokenization or strtok()
in C. an >>
operator will take the string up until a whitespace that the stream is initialized.
so for example if line contains the string "INPUT SAMPLE"
, then after iss >> command
, command will contain the string "INPUT"
next it will compare our input to the defined commands.
first command is poke x y
, poke will change our input string at any given index with any byte.
peek x
, peek reads a byte and print it out as integer.
then other than that we'll just quit the program
so in summary:
We gave the program a string of length bigger than 0x20
poke x y
to write byte x to offset y relative to the string address
peek x
to read a byte at offset x relative to the string address
recap:
arbitrary read only on heap region
arbitrary write only on heap region
So I know string can have dynamic length and intuitively can't be stored in the stack so it must use the heap. I verify this by giving a long string and a short string then comparing the heap for both of it.
here's where I gave a long string:
and here's when I gave a short one:
so we do have some sort of indirect control to the heap. However unlike the typical CRUD heap challenge, we can't decide when or what will be allocated and free'd. we can only interact with the heap through the std::string
API.
so I look up for resources and found these:
the readings doesn't provide me with a definitive answer, but it provide me with quite enough information so that I didn't go as blind in exploiting this.
okay so, first I gave the program enough string to initialize and locate its the s
heap chunk
and notice conveniently there's a free chunk right after, we can read at any offset and leak the heap's address. since we can only read one byte at a time, I wrote this poorly written function that allows us to read 8 byte from an offset
calculate its offset from GDB we then can leak it and calculate its offset again to get our read's base address ( i.e the address where our string is located, since every offset will be relative from it )
next thing that came into my mind is that since libc is 2.27, the hooks are still within the library so we can hijack it, but how we have no control over what chunks are allocated and free'd.
I did try to look at the decompiled version binary and notice a deconstructor
called on one or mote of the string variables
I did put breakpoints on this calls in GDB and observe the heap condition to find out that these do not have any behaviour on the chunks. so we all learn something at the end.
at this point I notice when giving a quite huge string as I shown above, I noticed that the there are bins that contain more than 1 chunk. and this happens as we provide the input, and so I think if our input goes through it and have some sort of control to the chunk's data?
turns out it does as shown below
so the subsequent chunks contain the string we gave, this behaviour is well explained in the links I provided above. In short, in order for the string length to be dynamic, it will create a larger memory for it to store in case it has reached the current memory capacity of the string.
from this we potentially can do tcache poisoning to allocate a chunk into the hooks.
the size of the bin that has a double chunk depends on the size of our input, so I banter and fuzz a little bit more to find the ideal size (this will be important later)
I find 0x20 to be good
and this is our heap state up to this point.
next let's get a libc leak by giving it a huge string
calculate the offset of our s
chunk to the unsorted bin chunk in GDB, and use read to gain libc address
next, we will overwrite the fd pointer to __free_hook
to do tcache poisoning. the offset to the chunks are obtained within GDB.
since we can only write one byte at a time, I've wrote this another wrapper function that handles an 8 byte write
and now what's important is that we can't directly overwrite it to __free_hook
, this is because the chunks are only allocated only if it needed to do so (i.e. if our input is large enough).
means if we gave the program small input (as what we will do it if we were to overwrite it with __free_hook
directly), it will not need to allocate memory and thus the poisoning will not be triggered.
overwriting it directly and giving a large input will also not work since the function address we wanna write to __free_hook
, will have null bytes thus terminating the string and end up in a small input.
so let's fuzz at how large of an input that it the start to affect chunk in the bin. let's try with a small one to verify our thoughts
as you can see, the chunk doesn't contain our new input, lets try a little bigger one at 0x18
and now those chunks contain our new input. so we will need to poison the fd with __free_hook
- 0x10. ( offset to the chunks are obtained within GDB )
and we can further verify the idea by giving a command and see if we are actually able to write to it
and we do, great !
this is earlier we gave the a relatively small amount of string to the program, this is to create and link two chunk inside of an 0x30 tcache.
if we were to put it into an bigger size tcache, this would prove to be complicated since we would also need to overwrite more values that came before __free_hook
. which can crash the program
anyway, so what function do we wanna call? there's a win function right? well recall that the program has PIE, and we have no leaks to gain the base address.
I found this post which explains clearly how we can pivot around memory to get leaks, but in the end its too much work and why don't we try system()
welp, that fails because free is immediately called when we gave the input. notice that the string bash try execute is our payload padding. we can't also just pad it with /bin/sh
because of null bytes. so let's try one_gadgets
.
and it works! but ... fails remotely
turns out the remote server has different offsets, since the organizer has no discord and they only way to contact is through email, I opted to just fuzz the server
remember to fuzz at the correct stage of the exploit as different stages will have different heap state thus resulting in a different offset as well.
with trials and error, I found the correct offset and got the flag!
here's the full exploit:
Flag: TBTL{uN1n73nDED_20Lu720nS_4R3_4wl4y2_W3LCOm3}
A Day at the Races
Binary Exploitation
100 pts
35
Heap Peek and Poke
Binary Exploitation
469 pts
8