HackTM CTF 2023
This weekend, I spent my time competing at HackTM CTF 2023 held by WreckTheLine with my local team SKSD
. We got 28th place. I managed to solve one pwn challenge called CS2100
, and this is my writeup for that challenge.
CS2100
|
|
Attachment: https://drive.google.com/file/d/1fvZ0rfXOPmH_HqpG0tDVaPl45_bKmpGC/view?usp=sharing
Initial Analysis
We were given a zip file contains:
- Binary called
main
- File called
server.py
- Libc binary
libc-2.31.so
Let’s check the server.py
file first
|
|
Ah okay, so it just read hex-encoded bytecode, stored it in a temporary file, and then pass it to the binary main
.
The problem description also gives us a link to a github repo. Turn out, the given binary is the compiled version of that repo, which is a riscv emulator. Instead of decompiling the binary, we can just clone the repo and try to analyze the source code.
I noticed that this challenge is pretty similar to RealWorld CTF 2023 challenge called tinyvm
, where the given repo is a vm of x86 assembly implemented in C, while for this challenge, the repo tries to implement RISC-V emulator. So, based on that experience, I decided to try to look whether there might be Out-of-Bound Read and Write on the given repo, because it is the common mistake of developer during building a simulator. Let’s first inspect the main.c
file:
|
|
Okay, so basically, the file required us to input a filename, which will be loaded to the CPU
struct, and then it will iterate the instructions we supplied in the file. Checking the cpu_execute
implementation, the emulator implements a lot of RISC-V
instructions.
|
|
Our target is trying to look at an OOB read or write bug. So, I decided to skim for instructions that related to reading/storing value in memory. And then, I noticed this sequence of actions.
|
|
Notice that there isn’t any check on the addr
whether the result of addr-DRAM_BASE
is larger than the array mem
size or not. This means, if we put the correct addr
value, we can simply do OOB read and write. The exec_SD
above is used to do OOB write by passing the correct addr
, and then I notice another function called exec_LD
, which can be used to do OOB read.
|
|
With those two functions, we have found the OOB bug in the emulator. Now, let’s move to the exploitation step.
Exploitation
Okay, now that we have found our targeted functions during our initial analysis, to help craft the payload easier, I try to build a helper first in python to generate the file that will be passed to the emulator.
|
|
The helper above is built based on inspecting how the cpu_execute
function processes the instructions bytecode from the input file. Now that we have set the appropriate helper, let’s try to execute it for the first time to build our first payload to test it. Below is the example code that I used during analyzing the code.
|
|
After running the above code, it will generate a temp file, and then we can use the temp file to run the main
in a separate terminal. Trying to use the generated temp file, below is the example output of the emulator
|
|
We have successfully changed the a0
value to 0x41
. Now, notice that the sp
register value is 0x80100000
. Remember that the max size of array mem
is 0x100000
, and if we try to access the memory of the address 0x80100000
, that means we will try to get the value of mem[addr-DRAM_BASE]
, which is mem[0x100000]
. For example, if we try to increase the sp
value by 0x10
, that means we have achieved OOB access.
Let’s try to check it by using this payload.
|
|
The above payload trying to store 0x41414141
to mem[sp+0x8]
. exec_ADDI
only support 12-bit signed number based on my observation in the source code, so to set a0
value, I use the help of << 8
. Let’s try to inspect it in gdb to check where our 0x41414141
value is stored.
|
|
Based on that inspection, mem
array is reside in stack, and mem[0x100000+0x10]
is pointing to the stored RBP. Notice that mem[0x100000+0x8]
is canary value, and mem[0x100000+0x18]
is stored RIP. That means, if we try to overwrite it, we will get RIP Control. Let’s modify our payload to overwrite the stored RIP address instead.
|
|
And below is the gdb result
|
|
That confirms our hypothesis. We’re now able to control RIP. Now it’s time for us to leak some value. We can use the help of exec_LD
to do this. But first, let’s try to inspect for values around the stack.
|
|
We found a good address to get a stack address leak and libc address leak. To get the leak is very simple, we just need to:
- Set one register value to
0x80100000+0x58
, and then callexec_LD
to get that address value. We will get stack address leak - Set one register value to
0x80100000-0x2a8
, and then callexec_LD
to get that address value. We will get libc base
Now that we can get some leak, I notice that the SUB function in the code is not working. But we can use exec_ADDI
with negative 12-bit number to do subtraction. For example, look at this payload:
|
|
I try to set t2 to 0x800ffd58
(which is 0x80100000-0x2a8
), and then use exec_LD
to get the leak and store it in a3
. And then, with the help of exec_ADDI
, I try to subtract it by 0x100
step by step until the a3
value equals to libc base address.
Now that we know how to do OOB read and write, the exploitation is trivial. Notice that in server.py
file, the stdin
is redirected to DEVNULL
, so we can’t spawn a shell.
I choose to create a ROP payload to call execve('/bin/cat', ['/bin.cat', 'flag'])
. With the OOB read, we know the stack and libc address, and with the OOB write, we can carefully build the argv
array in the stack, and then pass the stack address during the call to execve
. Below is the ROP payload to execute that.
|
|
And we got the flag!
Flag:
HackTM{Now_get_an_A_for_the_class!}
Social Media
Follow me on twitter