Here is the first pwn challenge of the Boston Key Party CTF. Stay tuned for the
writeup of the Complex Calc challenge.
Basic information
From the organizers:
This is an ELF 64-bit binary not stripped without much security
mechanism:
With NX activated – well is this 2016 after all – we might have to create a ROP chain.
Operation
Obviously the binary is a calculator. It starts by asking the number of
expected operations:
This is done by the following code. The user provided value for
the number of calculation is stored by scanf at [rbp - local_14h]:
Afterwards there is a check on that value. If the value is not between 4 and
255, the program exits while printing Invalid number.:
With a correct value, malloc is called to create a buffer to store the result
of the calculations. At 0x00401432 we can see that the value is multiplied by
4 before being given to malloc as the size argument. The buffer will be used
to store 4 bytes values.
Then the program presents the menu of possible operations which is done by the
function print_menu:
For the options 1 to 4, the program asks two numbers and prints the result of
the corresponding operation. The numbers must be strictly greater than 39,
otherwise the program refuses to perform the operation:
Searching for the vulnerability
The option number 5 Save and Exit. doesn’t really save the results to disk
but it copies the content of the buffer allocated by malloc to the stack and
then exits:
The arguments of the call to memcpy are:
dest: the address corresponding to rbp - local_40h, this is on the stack
src: the address of the buffer created by malloc stored at rbp - local_10h
n: the number of calculations (multiplied by 4 because the buffer contains dwords)
Here is the problem, n should be less or equal to the size of the destination
buffer, otherwise there is a buffer overflow and memcpy writes after the end of
the buffer.
Let’s inspect the stack with GDB just before the call to memcpy:
18 dwords or 72 bytes are seperating the beginning of the buffer and the saved
rip address. As the maximum number of calculations is 255, we have enough
room to inject whatever we want!
After the call to memcpy, there is a call to free on the malloc‘ed buffer
before returning from the current function. We have to overwrite the address of
the buffer because it is placed on the stack between the buffer and saved rip.
This is not a problem because one of the first things that free does, is to
check if the pointer given to him is NULL. If this is the case, free
returns directly without performing any actions:
The layout of what we want to create looks something like this:
The goal of the ROP chain is to call the execve syscall to run /bin/sh so
that we can get a shell. As shown in the man page, execve needs three
arguments:
const char *filename: the command that must be executed, in our case a
pointer to /bin/sh
char *const argv[]: an array containing the arguments that is passed to the
commend, useless here
char *const envp[]: an array containing environment variables for the
context in which the command has to be executed, useless here
In the binary there is no /bin/shstring (there is one in the C library but we
won’t used that one here). We have to pass that string with the ROP chain. As
it will be on the stack, eventually the stack pointer rsp will point to it.
execveis the syscall number 59 and the calling
convention tells us that the first argument must be passed in
rdi, the second in rsi and the third in rdx. Therefore before launching
the syscall, the registers must hold the following values:
The gadget to mov rdi, rsp doesn’t end with a return but with a call:
We need to store the address of the next gadget in r12. Fortunalely there is
a gadget to pop r12:
Here is the overall ROP chain:
The operations done by the calculator are done on 32-bit numbers, therefore each 64-bit value in the ROP chain is constructed with two operations. For example to write the first gadget: