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:
The provided connection information must be used to connect with SSH to the machine:
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:
Operation
The binary need a file passed as argument:
The file is not really saved anywhere, its content is just copied on the stack of the binary:
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:
From another terminal:
Back in the debugger:
We control eip! 4128 bytes are needed to overwrite it:
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 mprotectsyscall 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 one
ecx: pointer to argv[] => useless, put 0
edx: pointer to envp[] => useless, put 0
Finding /bin/sh
The /bin/shstring 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 .datasegment. Or to provide it through a
environment variable. I chose the latter possibility. A pointer to the
environment variable is available here:
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:
ROP chain
Here is the python script that generate the payload which is then cast into the
fifo:
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 and cat payload > fifo
execute the launcher
It would have been easier to push /bin/sh into the .data segment…