Tokyo Westerns CTF 2018 - Neighbor C
Table of Contents
0x00. Introduction
Environment
The provided libc is a quite old version, so it cannot be loaded with the latest loader. However, using the local libc would make the solution more difficult, so I decided to set up the environment by configuring a server with Docker.
When the server is configured this way, it listens on port 9999 and executes the neighbor process through EXEC:/home/user/neighbor when connected.
Therefore, connection is not possible with the existing exploit.py format, and it needs to be modified as follows for proper debugging.
=
=
=
=
=
I provided the sysroot argument so the debugger can properly read libc and load symbols, which makes it recognize the current location as the root directory and search for the libc file. Therefore, you can check the libc path through vmmap, create a directory accordingly, and copy the libc file.
0x01. Vulnerability
void __fastcall __noreturn
void __noreturn
void __fastcall __noreturn
Looking at the binary, FSB occurs in sub_8D0() because format can be directly input to fprintf.
As I experienced in the previous problem, when the vulnerability is simple, the exploit becomes complex, and this problem seems to be the same.
0x02. Exploit
Stack Control
Before exploiting the vulnerability, the problem is that format is a global variable, so we cannot input values onto the stack. Then we cannot create pointers on the stack, making it impossible to write values to where the pointer points using %n, which is the core of FSB. Therefore, we need to acquire a primitive that can write values to desired locations appropriately using values on the stack.
So I printed the stack when fprintf() is called, and there were two stack addresses created during push rbp. This is where the question of why the functions are called as main() -> sub_937() -> sub_8D0() was answered - it was to enable Double Staged FSB.
Since 0x7fffffffebd0($rsp+0x20) points to 0x7fffffffebe0($rsp+0x30), we can construct the desired address at 0x7fffffffebe0 using FSB.
Since 0x7fffffffebd0 is the 9th format string, writing the payload as follows allows us to control the value stored in 0x7fffffffebe0.
%1c%9$hhn: 0x00007fffffffebf0 -> 0x00007fffffffeb01%258c%9$hn: 0x00007fffffffebf0 -> 0x00007fffffff0102%16909060c%9$n: 0x00007fffffffebf0 -> 0x00007fff01020304
In the debugging environment, ASLR is turned off for convenience, so the first byte of $rsp is fixed as 0xb0, but since ASLR will be enabled in the actual server environment, the exploit success rate drops to 1/16.
Anyway, in the local environment, I could see error messages, so I could view the result of fprintf(stderr, format), but in the server environment, error messages cannot be seen. Therefore, I determined that the first thing to do through the above stack control is to change stderr to stdout to proceed to the next stage.
Libc Leak
# fprintf(stderr, format);
The stderr in fprintf is not the stderr in the libc Data section, but the stderr on the stack passed as an argument when calling sub_8D0(). And this stderr can be accessed as $rbp-0x8, which is the address 0x7fffffffebc8($rsp+0x18).
Fortunately, the controllable 0x7fffffffebe0 already contains a stack address, so if we overwrite only the first byte with 0xc8, 0x7fffffffebe0 will point to 0x7fffffffebc8. Then, since 0x7fffffffebe0 is the 11th format string, we can change stderr to stdout.
However, since the second byte of stderr and stdout is different here, if we overwrite the first two bytes of 0x7fffffffebc8 with 0x2620, the exploit success rate becomes 1/16 again due to ASLR.
Ultimately, exploitation is possible with a 1/256 success rate.
Comparing the format string output using the obtained stdout with the actual stack contents shows the above.
Looking carefully, you can see that starting from the 5th format string, it matches the stack contents. The format strings in front of the stack output register values according to the calling convention:
rsi,rdx,rcx,r8,r9
However, in the above case, you can see that it starts outputting from rdx, which I think is because the second argument goes into fprintf.
Anyway, coming back, since the 10th value of the stack contains the libc address, we can obtain the libc base address by calculating and subtracting the offset.
Triggering malloc
Now for the most important part - where to write what - since it’s an old libc, we can use malloc_hook. Therefore, where is decided, and I checked the one-shot gadgets.
)
|| }
)
|| }
)
|| }
Fortunately, the conditions aren’t too strict, so I checked and the gadget at 0xf1247 looked usable. Therefore, what was naturally resolved, but when I thought about it, malloc needs to be called somewhere for malloc_hook to be invoked…
In the while loop flow, the only functions called are fgets and fprintf.
I tried to check if malloc is called internally within the functions, and I could confirm that fgets is simple and has no malloc. On the other hand, fprintf calls vfprintf, which has too much code inside to check easily.
So I googled and found some information.
- https://stackoverflow.com/questions/6743034/does-fprintf-use-malloc-under-the-hood
- https://github.com/Naetw/CTF-pwn-tips?tab=readme-ov-file
After checking, it seems that if the output string created through the format string has a size of 0x10001 or more, it can trigger malloc.
So I xref’ed backwards from functions that call malloc, functions that call j_malloc, and confirmed that vfprintf is there.
Arbitrary Write
Now, looking at the stack again, 0x7fffffffebc0($rsp+0x10) is empty with NULL.
First, we’ll use Double Staged FSB to create the desired address (addr) in this empty space (free_space). Then, using the created address as a pointer, we’ll use Double Staged FSB again to write the desired value (value).
Naturally, addr will be malloc_hook, and value will be the loaded one-shot gadget. This process can be created in Python as follows:
= + 0x10
= f
+= b
= f
+= b
= f
+= b
Actually, many unnecessary payloads are sent, so it’s not efficient, but I got greedy while working on it and made it possible to do arbitrary write in one line like this.
0x03. Payload
= True
=
=
= 0x0000555555400000
= 0xb0
= 0x3c4b10
= 0xf1247
=
= f
=
= + 0x10
= f
+= b
return
= f
+= b
= f
+= b
=
=
=
=
=
# overwrite stderr in stack to stdout
# leak libc base
= b
= - 0x20840
= +
= +
# write one_gadget address to malloc_hook
# trigger malloc -> malloc_hook