This weekend my team HATS SG played in the nullcon HackIM CTF. I think this was our best performance yet in a CTF, finishing 7th! This is probably the first time we had a single-digit rank :D. I’ve solved a bunch of the pwn challenges so I’ll talk about my solutions to them. As a team, we’ve solved the following challenges.
- easy-shell - Solves: ?, 451pts
- HackIM Shop - Solves: ?, 458pts
- peasy-shell - Solves: ?, 493pts
- babypwn - Solves: ?, 495pts
- tudutudututu - Solves: ?, 495pts
- Captcha Forest - Solves: ?, 150pts*
- Captcha Forest Harder - Solves: ?, 431pts*
- mlAuth - Solves: ?, 475pts*
Go get yourself a shell while it’s possible
nc pwn.ctf.nullcon.net 4010
Disclaimer: I was not the one who solved this during the CTF, my teammate Engimatrix solved this instead. However, I was working on a separate solution from him in parallel to have a better idea for when we want to solve the next part of the challenge
In short, we are given a binary that mmaps an RWX region, reads shellcode from the user into this region, then jumps to the shellcode. Pretty straightforward. However, there are 2 restrictions we have to bypass. Firstly, the shellcode we pass must be alphanumeric, in regex form:
[a-zA-Z0-9]. Additionally, there is a seccomp rule implemented which prevents the
line CODE JT JF K ================================= 0000: 0x20 0x00 0x00 0x00000004 A = arch 0001: 0x15 0x00 0x03 0xc000003e if (A != ARCH_X86_64) goto 0005 0002: 0x20 0x00 0x00 0x00000000 A = sys_number 0003: 0x15 0x01 0x00 0x0000003b if (A == execve) goto 0005 0004: 0x06 0x00 0x00 0x7fff0000 return ALLOW 0005: 0x06 0x00 0x00 0x00051234 return ERRNO(4660)
Just with any other shellcoding challenge, the first thing to do it search exploit-db to check if the relevant shellcode already exists. So simply google “x64 alphanumeric shellcode”. There are a few samples but they all use
execve to pop a shell, in our case, we would either need to use
execveat or use a
open-read-write(orw) shellcode instead. Since I could not find any such existing shellcodes, it is time to start writing our own shellcode.
With the strict restriction of only alphanumeric shellcode, my approach is always to figure out how to perform a
read shellcode from user input into the RWX region. This is commonly referred to as a multi-stage attack, and I think it is the most effective approach to restrictive shellcoding challenges. Once we are able to perform a
read from user input into a RWX region, we can then write a second stage payload without any restrictions! It is far easier to setup the read shellcode as opposed to the entire open-read-write chain in alphanumeric.
An important factor to consider when doing shellcoding is the context in which our shellcode runs. By this, I’m referring to the current state of the registers and memory before the first instruction from our shellcode is even executed. This is very important as it enables us to do a lot more, and reduce a lot of unnecessary instructions. To find out the state of the registers and memory, we can simply set a breakpoint using
pie break * 0xb94 in gef, and checkout the memory.
$rax : 0x0 $rbx : 0x4000 $rcx : 0x00007f26a1c82260 → <__read_nocancel+7> cmp rax, 0xfffffffffffff001 $rdx : 0x56 $rsp : 0x00007fffa9d69ce8 → 0x00005652ce7e9b97 → jmp 0x5652ce7e9b4c $rbp : 0x0 $rsi : 0x00007f26a2176000 → "Our shellcode!" $rdi : 0x0 $rip : 0x00007f26a2176000 → "Our shellcode!" $r8 : 0x00007f26a1f51780 → 0x0000000000000000 $r9 : 0x00007f26a2155700 → 0x00007f26a2155700 → [loop detected] $r10 : 0x22 $r11 : 0x246 $r12 : 0x00007f26a2176000 → "Our shellcode!" $r13 : 0x00007fffa9d69de0 → 0x0000000000000001 $r14 : 0x0 $r15 : 0x0
The most important registers we should look at are generally
rax, rdi, rsi, rdx when in x64. These registers are used for the syscalls and the arguments to function calls, thus if they are already set to a good value, a lot of our work is already done for us. And indeeed, the registers are actually setup in a perfect manner for us to do a
read syscall into the RWX region as we’d planned to do. Now all we need to do is provide a
syscall instruction, and we can read in our second stage payload! Unfortunately, the
syscall instruction uses the bytes
\x0f\x05, which are not in our alphanumeric charset, how can we bypass this?
As the header suggests, we can create our
syscall instruction in the shellcode, by making our shellcode modify itself! This is a useful technique whenever you are not able to write certain bytes. Additionally, since all our instructions are present in RWX regions, there is nothing stopping us from overwriting the instructions. After doing some research, I find that the following instructions only require alphanumeric characters to represent.
xor DWORD PTR [rcx + imm8], eax push [any reg] push imm32 pop rax pop rcx
With these instructions, do you see how we can perform our self-modifiying shellcode? Since we know that we can push any registers, we can notice from above that
rsi, rip, r12 all contain a pointer to our shellcode. Thus we can simply push this value onto the stack, then pop it off into
rcx. Now using the
push imm32 followed by a
pop rax, we are able to get any 4 byte value (within alphanumeric range) into eax. Now we are in a perfect setup to use the
xor DWORD PTR [rcx + imm8], eax to xor some instructions in our own shellcode! Now all we have to do is to find pairs of alphanumeric characters that xor to form
\x05. Just open up your python interpreter and do a quick bruteforce.
All we have to do is make sure that imm8 and imm32 are only using alphanumeric bytes and we’re good to go with forming our
syscall instruction. Also, since we are forced to use an alphanumeric imm8, our
syscall instruction is a bit far from the start our shellcode, so we need to use some
nop instructions to reach that
syscall instruction. Since the actual
nop instruction is
\x90 (not alphanumeric), we can use a two byte nop, like
push rax; pop rax. This achieves nothing and helps us travel down the shellcode to our syscall.
After this, our shellcode will hit the
syscall, and read some input from the user. We simply provide an open-read-write shellcode that will open “flag” and print it. I will not go into detail on this and you can check my exploit source code for details.
one more easy shell for free!
nc pwn.ctf.nullcon.net 4011
Now, after Enigmatrix had solved easy-shell, we began working on the sequel
peasy-shell. This challenge is pretty much the same, with the big difference that the shellcode region is made RX before we jump to it. This is tragic as it kills our self-modifying strategy to achieve the
syscall instruction. … Or does it?
If you were to do a quick reversing of the
make_rx function that they use to do this, you will notice something very important.
len parameter on the mprotect call is only 1! In this case, what will happen is that mprotect will round it upwards to 0x1000, however, our mmaped region is larger than 0x1000 :D. This means that we can use the same self-modifying shellcode strategy as before, just that our
syscall instruction is 0x1000 bytes after the beginning.
Now that we know this, we can try to modify our inital shellcode to get this working. An additional challenge is that the condition of the registers is also different from before, and our registers are no longer setup nicely for a
read syscall. This requires a lot of weird popping and xor-ing in the stack to get our registers set nicely. I think the tricks are quite cool, so I suggest that you take a look through the shellcode and figure it out :P (feel a bit lazy to describe them thoroughly).
After the modifications to fix the registers, we place many two byte nops to traverse all the way to the
syscall instruction. Perfect! We read in our second stage payload and get the flag locally! Now lets run it on the server!
[+] Opening connection to pwn.ctf.nullcon.net on port 4011: Done 103 [*] Switching to interactive mode [*] Got EOF while reading in interactive
Ah, maybe it’s failing because the exploit only works 50% of the time, even locally (this is because I am using an xor trick to increment a register, this creates a situation where sometimes it decrements, and sometimes it increments).
[+] Opening connection to pwn.ctf.nullcon.net on port 4011: Done 103 [*] Switching to interactive mode [*] Got EOF while reading in interactive
oof *20 more tries
Now our shellcode should not be failing this many times, a 50% chance to work is very good already. So why does it fail 100% on remote but work locally? Then I remembered something I learnt from someone in OpenToAll in the past. When sending our data across the network, they are sometimes chunked into smaller packets before being sent across, this will happen for larger chunks of data. However, the
read call that is initially reading our stage 1 payload will not consider this chunking, and only read the first chunk. Since our shellcode is a giant size of 0x1032 bytes (with all the nops), only a part of our shellcode is read into memory, causing the exploit to fail. So now there’s only one option: We need to make it smaller
So what’s interesting about this challenge is that at the end of our shellcode, the binary will add a
What you may realise is, if you can push the address of our
syscall instruction onto the stack, and get to the
ret instruction, this is effectively a jump! Therefore, we can modify our shellcode to the xor magic to form the
syscall instruction inside eax. Then we get that
syscall instruction in memory using
xor DWORD PTR [rcx + imm8], eax as before. Now just push the correct address, and we
ret to the
syscall! Just as before, we read in our second stage open-read-write shellcode and get the flag!
I skipped a lot of details about the exploit in this writeup, if you are unclear about anything you can leave a comment or dm me or smth. The exploit used quite a lot of messing around with registers and memory so I didn’t want to go into detail on everything, but I hope you can understand the main approach to these challenges
peasy-shell. Also, I’ve never really been a fan of shellcoding challenges but these were really fun for me and sparked my interest shellcoding. Kudos to the author.
Welcome to our bookstore, check if you find anything interesting.
nc pwn.ctf.nullcon.net 4002
Menu pwnable! My fav <3
NullCon Shop (1) Add book to cart (2) Remove from cart (3) View cart (4) Check out >
After reversing the binary, I noticed that the
remove_book functionality did not clear up the pointer for a book in the list of books after freeing the memory. This is a use-after-free of (UAF) bug. Now we should check out the structure of a book to see what we could possible exploit if we can control the chunk.
copyright buffer is something interesting to us. If you look at the
print_books functionality, there is the following line.
Usually, this buffer will only contain “Copyright NullCon Shop”. However, should we be able to control this value, this is a classic format string vulnerability.
Now that we are aware of two big vulnerabilities, we can see the path we should take for exploitation. We should somehow make use of this UAF vulnerability to control the contents of the copyright buffer, then we can do quite a lot with the format string vulnerability. So how can we control this UAF? We can make use of the heap’s fitting logic, which allows us to have the data pointer of one chunk point to a book. Then we can write any arbitrary pointer we one in that fake book.
make two books, with data size 0x10 [ 0x38 bytes ] BOOK_1 [ 0x10 bytes ] DATA_1 [ 0x38 bytes ] BOOK_2 [ 0x10 bytes ] DATA_2 after we free both books 0x38 bytes BOOK_1 0x10 bytes DATA_1 0x38 bytes BOOK_2 0x10 bytes DATA_2 Now if we were to allocate a new book with data of size 0x38 instead... [ 0x38 bytes ] BOOK_1 0x10 bytes unused [ 0x38 bytes ] DATA_1 and BOOK_2 0x10 bytes unused
With this setup, we control all the members of BOOK_2, including the
copyright buffer! Since we also control the data pointer for BOOK_2 through the contents of DATA_1, we can leak the libc addresses using the
data pointer of BOOK_2. After leaking, we can figure out the libc version using tools like niklasb’s libc-db.
After this, we can try to exploit the format string vulnerability. The first thing we do is to check what the stack looks like during the printf call. If we can control any pointers within the stack when the printf is called, this allows to have an arbitrary write or read primitive. And indeed, we do! Luckily for us, when the printf call occurs, the index of the book is on the stack, since we control the book structure, we can write any arbitrary pointer as the index.
With this arbitrary read and write primitive. I began to exploit this challenge using the
house-of-spirit technique inside the Global Offset Table (GOT). I used the format string vuln to write the correct size bytes in the GOT. Then, by freeing the fake BOOK_2 that we created, I could also achieve an arbitrary free. With these two combined, I could arbitrarily free the fake chunk that I placed within the GOT. Now, when I allocated a new book, it’s data pointer would point inside the GOT, allowing me to write many
one_gadgets inside the GOT that would pop a shell for me!
Can you exploit the basic bugs?
nc pwn.ctf.nullcon.net 4001
This challenge was actually quite straightforward. And I’m a bit surprised it didn’t have more solves (I found HackIM harder), maybe it’s because the scanf trick was not so known. Regardless here is the solution I used. The challenge has two vulnerabilities. Firstly, there is the format string vulnerability in the name.
This allows us to use “%p” to leak pointers present in the stack. Thus we can leak a libc pointer in the stack and bypass ASLR to find the libc base in memory.
Create a tressure box? Y name: %p.%p.%p.%p.%p How many coins do you have? 0 Tressure Box: 0x1.0x7fc685178790.0x10.(nil).(nil) created!
The second vulnerability is the signed check when asking for the number of coins we want.
If we provided a negative
0xff (-1), this would pass the signed check, but the following for loop would treat
numcoins as an unsigned variable, allowing us to write
0xff (255) dwords in the stack. This means we can overwrite the saved
rip in stack (just like a buffer overflow).
However, there is also a stack canary! If we overwrite this with a wrong value, this will cause the program to exit prematurely before returning, which ruins our exploit. Now we can bypass this canary if we can leak it, like through the format string vulnerability. However, the format string is only printed after we’ve specified all the coins, thus we cannot know the canary when writing. This requires knowledge of a cool scanf trick. When scanf is called like so:
You can provide the characters
+, and the scanf will not change the value of the variable. Thus, we can use this to not destroy the canary while overwriting the saved rip. Afterwards, we just need to change saved
rip back to main, so we can overflow one more time to return to a
I found a ToDo service to maintain my daily tasks. My life is sorted now!
nc pwn.ctf.nullcon.net 4003
Menu: (1) Create a new todo (2) Set description for a todo (3) Delete an existing todo (4) Print todos (5) Exit >
Another heap menu pwnable!
This challenge is supposed to be a program to create todos to remember things. When creating a new todo, the program does a simple
malloc(0x10) followed by a
strdup(user_input), creating 2 chunks in the heap.
[ 0x10 bytes ] todo_1 [ 0x? bytes ] topic_1
The structure of the todo is as follows:
Now after we initialise the todo with a topic, we have the additional option of adding a description to the todo, this will allocate a new chunk and set the pointer to the chunk in the struct of the todo. The fatal error however, is that this description pointer is not initialised to zero when a todo is created. This means that if there is leftover data in the chunk when it was allocated, the program will think that it is the description pointer. When we reverse the
delete functionality, we can see these lines of code.
Thus, if we can control the value of this uninitialised description member, we can exploit it through the
delete functionality of the program. In essense, should we control this description member, we have an arbitrary free primitive.
Now that we understand the main bug, we can try to exploit this. The steps to exploit the uninitialised member are as follows.
create new todo, with 0x10 byte topic [ 0x10 bytes ] todo_1 [ 0x10 bytes ] topic_1 free this todo 0x10 bytes old_todo_1 0x10 bytes old_topic_1 allocate 2 new todos, with topics larger than 0x10 bytes [ 0x10 bytes ] todo_2 [ 0x10 bytes ] todo_3 !! ( this used to be old_topic_1 ) [ 0x? bytes ] topic_2 [ 0x? bytes ] topic_3
As we can see from this scenario,
todo_3 uses the same chunk that
topic_1 previously used. In this case, we can write an address at the 8 byte offset of
topic_1, this will then be used as the description of
todo_3 later on. With this primitive, we can arbitrarily read any address, and free any address. Since we can leak any address, I utilised this to leak the heap address and the libc base.
Now that we have all the leaks we probably will ever need, we need to figure out how to control
rip to change program execution. How I did this was to transition our arbitrary free to a
fastbin double free. Since we knew the address of the heap, we could create 2 todos that had descriptions or topics pointing to the same chunk. If we freed both these todos, we would thus create a circular fastbin list, allowing us to perform a
fastbin attack. Now to utilise this
fastbin attack to control
rip, I made it return an arbitrary chunk before the location of
__malloc_hook. This allows me to overwrite the value of
__malloc_hook with a
one_gadget. And that gets us our shell!