Preamble
This post is part of a series on common software vulnerabilities. The exploits target binaries from Rainfall, a CTF project by 42. Prior knowledge of x86 assembly and C is required !
Initial analysis
According to file
:
|
|
So we know we want to spawn a shell while having the privilege of level3
because of the setuid bit.
Let’s analyse it further with objdump
:
|
|
This setup does not include any existing code in the .text section to spawn a shell. In our exploit, we must somehow inject additional code to achieve this. This is known as shellcode — a small piece of code used as the payload when exploiting a software vulnerability.
In function p
, the highly unsafe gets
function is called once again. This is a serious vulnerability because it allows a buffer overflow, enabling us to overwrite the return address of function p
on the stack. But where should we redirect execution?
Since we need to introduce additional code, we can inject it into the buffer used by gets
and then modify the return address to point to our shellcode, which resides in the stack.
However, this approach have an issue in this particular setup. Consider the following snippet of instructions:
|
|
The program performs a check on the return address located at ebp + 0x4
. Specifically, it verifies whether the most significant byte of the return address is 0xb
. If it is, this indicates that we have manipulated the return address to point to the stack, which would prevent us from successfully redirecting execution to our shellcode.
If the check fails, execution is redirected to 0x8048527
. This presents a challenge, as it blocks a straightforward return-to-shellcode approach. But we might have a solution :
|
|
The program first echoes back what we wrote into the buffer and then proceeds to duplicate the stack-allocated buffer into a heap-allocated region. This is an interesting behavior!
Instead of modifying the return address to point to our buffer in the stack — where execution is blocked by the “security check”. We can redirect it to the heap-allocated region instead. Since the data is duplicated exactly, our shellcode will still be present in memory and ready for execution.
The main challenge here is that our shellcode must not contain any NUL bytes (0x00), as strdup
treats NUL as the end of the string. If our shellcode includes a NUL byte, it will be truncated prematurely, making it ineffective.
Crafting the shellcode
Let’s do some assembly to craft our shellcode, shall we ?
|
|
This is a basic program that execve
a shell. Note the trick to get the address of the string /bin/sh
: at the beginning we immediately jump to a call .ret
, which pushes the address of /bin/sh
into the stack and jump to pop ebx
, which pop the address of /bin/sh
into the register ebx
. This trick is used to dynamically get the address, since somtimes we do not know the exact address our shellcode will be injected.
Do note that we use the relative version of jmp
and call
, so we can indicate offsets instead of absolute address.
We are using x86 calling conventions and int 0x80
to trap into kernel mode for the syscall.
Now, let’s compile the source into an object file, and dump the .text
section :
|
|
Offset is 0x2a0
into the file and the size of our shellcode is 0x1a
. We can use xxd
to extract the bytes and sed
with some regex to format into an hex litteral :
|
|
This shellcode doesn’t lead to any 0x00
expect for the last byte. strdup
will stop there. Perfect !
Putting it together
These are the remaining steps :
- Find how big the buffer is to know how many bytes we have to fill until overwriting the return address.
- Set the return address to the return address of
strdup
. - Craft a python command to inject our shellcode, filling characters, and the new return address.
Using GDB and the disassembly, we find out that the buffer starting address is located at ebp - 0x4c
:
|
|
We need to fill 76 bytes (0x4c
is 76 in decimal) + 4 bytes (because ebp
has been pushed to the stack, we need to add an extra 4 bytes), to get to the return address location in the stack (that is, ebp + 4
).
Let’s find the return address of strdup
by setting a breakpoint just after the call :
|
|
Our shellcode is 26 bytes long. This is the layout of the bytes we will inject :
|
|
This leads to the following python command :
|
|
We can then proceed to pwn this program. Notice the use cat
again to prevent premature closing of the standard input.
|
|
Success !