2018年9月5日 星期三

[Write-up] TokyoWesterns CTF 2018 - pwn240+300+300 EscapeMe

The best KVM (Kernel-based Virtual Machine) challenge I've ever seen! Thanks @shift_crops for giving such great challenge. He released the source code of EscapeMe after the contest as well.



The released files can be found in the author's repo, include 4 binaries, two text files, and one python script.
And you can find all my three exploit scripts in my ctf-writeups repo.

Introduction
There're three binary files, kvm.elf, kernel.bin, and memo-static.elf.
There're three flags in this challenge as well, which need arbitrary shellcode in user-space, in kernel-space, and in host emulator (kvm.elf), respectively.

To run the challenge, type ./kvm.elf kernel.bin memo-static.elf in shell, and a normal pwn challenge interface will be shown:

kvm.elf is an emulator (same role as qemu-system), which utilize KVM, the VM implemented inside Linux kernel, for emulating.
kernel.bin implements an extremely tiny kernel, with ability to load a static ELF binary and some syscalls.
memo-static.elf is a normal ELF that implements a simple memo system.

Since the source code has been released in the author's repository, I'll only introduce the vulnerabilities I used instead of the whole challenge.

EscapeMe1: user-space
The memo-static.elf is a static-linked binary, and the checksec is


Well, for this challenge the checksec is meaningless because the "kernel" that executes this binary is implemented in kernel.bin, which actually disable all modern protections on executable files. As a result, there's no ASLR, no NX (all segment is executable), life would be very easy once we can control rip.

Vulnerability
The bug is a cliché. In Alloc we can add a memo (on heap) with data at most 0x28 bytes, after created we have one chance of each memo to Edit, and the edit is implemented as:
read(0, memo[id].data, strlen(memo[id].data));
If the memo has exactly 0x28 non-null bytes of data, then this read will overflow the size of next chunk.

Exploitation
Though it's an easy heap overflow challenge, the memory allocator is not ptmalloc in glibc that we familiar with. The mechanism of malloc/free here is very similar with ptmalloc, just with tcache and fastbin removed.

We chose to use the unlink attack (don't know if this trick has a name) on forged chunk, see the figure below:
                   |-----------------------------|
                   |              |     0x31     |
         (*ptr) -> |              |     0x51     | <- fake chunk size
                   |  ptr - 0x18  |  ptr - 0x10  |
                   |-----------------------------|
                   |              |     0x31     |
                   |   BBBBBBBB   |   BBBBBBBB   |
                   |   BBBBBBBB   |   BBBBBBBB   |
                   |-----------------------------|
forge prev size -> |     0x50     |     0x30     | <- overflow, clear prev_in_use bit
                   |   CCCCCCCC   |   CCCCCCCC   |
                   |   CCCCCCCC   |   CCCCCCCC   |
                   |-----------------------------|
Heap overflow occurs when editing the B chunk, sets the size of next chunk from 0x31 to 0x30, also prepares a properly prev_size (0x50).

Then we Delete(free) the C chunk, it wants to merge with previous (fake) chunk so unlink will be invoked. As a result, *ptr which originally points to heap now points to ptr - 0x18.

After this, we almost have arbitrary write, but it's very limited because we can only write data with the same length on it (recall the implementation of Edit). So we can't directly modify the data on stack (with address 0x7fffffffxx, "longer" than heap address 0x606000). I got stuck for a while and came with this solution:

  1. modify the pointer of top_chunk (at 0x604098), let it points to 0x604038
    • 0x604038 is chosen because there's value on 0x604040, so we can bypass the size check during malloc
  2. Alloc memo three times and the third memo will malloc on top_chunk itself, then we forge top_chunk again and let it points to stack address
  3. Alloc memo again then we can malloc on stack, then forge the return address.
Remain is just control rip to heap with prepared shellcode, which can read more shellcode and execute it.


Then I was stuck here 😂


Yes I have arbitrary shellcode execution, but where's the flag? Well, because I'm sure we must need code execution to pwn further interfaces (kernel and emulator), so I started to exploit the binary before knowing how to get flag1.

After some reverse engineering, I find there's a special syscall with number 0x10c8 implemented in kernel.bin. The syscall will copy the flag onto a write-only page:
uint64_t sys_getflag(void){
  uint64_t addr;
  char flag[] = "Here is first flag : "FLAG1;

  addr = mmap_user(0, 0x1000, PROT_WRITE);
  copy_to_user(addr, flag, sizeof(flag));
  mprotect_user(addr, 0x1000, PROT_NONE);

  return addr;
}

All we need is invoke the syscall, do mprotect to mark the page readable, and print out the content.
shellcode = asm('''
        mov rax, 0x10c8
        syscall
        mov rbp, rax
''' + shellcraft.mprotect('rbp', 0x1000, 6) + shellcraft.write(1, 'rbp', 60))

The script I used during the contest can be found in my github repo.
Actually I didn't notice the NX is disabled in the meanwhile, so I did ROP to mmap a new page for putting shellcode. That's why the script in the link is much more complex than I described.

Flag1:
TWCTF{fr33ly_3x3cu73_4ny_5y573m_c4ll}

EscapeMe2: kernel-space

kernel.bin contains of three parts:

  1. implements a simple execve to parse and load the user binary
  2. implements a MMU table, which is the mapping from virtual memory to physical memory
  3. implements syscalls include: read, write, mmap, munmap, mprotect, brk, exit, and get_flag (for EscapeMe1)
My teammates and I spent lots time on finding bugs in the memory-related operations include mmap, munmap and the implementation of MMU, which is totally a wrong strategy 😭

Our target is, of course, arbitrary kernel-level shellcode. And because the self-implemented MMU table marks a bit if the virtual-address can be accessed by user-space, we can't overwrite kernel's code with user-space shellcode.

Vulnerability
As the hint described, there's a bug in memory management.
The bug is caused by inconsistent ABI between the emulator and the kernel. In the emulator there's a self-implemented memory allocator, palloc and pfree, and the kernel misuses the pfree method.

When user invoking mmap(vaddr, len, perm) syscall, kernel will:
  1. hyper-call palloc(0, len) to get a physical address paddr with length len
  2. setup MMU table that maps vaddr to paddr and mark permission bits on it.  More palloc(0, 0x1000)(s) might be invoked during the setup (depends on if vaddr has corresponding entries created)
  3. return vaddr
When user invoking munmap(vaddr, len) syscall, kernel will:
  1. map vaddr to paddr
  2. hyper-call for(i=0 ~ len >> 12) pfree(paddr + (i << 12), 0x1000);
There's no bug here - only if the pfree call works as kernel imagines.
In emulator, pfree(addr, len) doesn't care the argument len at all (its function prototype is pfree(void*)).
Therefore, if a memory addr has length 0x2000, then invoke munmap(addr, 0x1000), in kernel only the first page is un-mapped, while in emulator the whole memory will be freed!

To be clearer, see the code before:
shellcode = asm(
        mmap(0x7fff1ffc000, 0x2000) +
        munmap(0x7fff1ffc000, 0x1000) +
        mmap(0x217000, 0x1000)
)
After this shellcode being executed, 0x7fff1ffc000 + 0x1000 still can be accessed by user, but it will points to the MMU table entry that paclloced during mapping 0x217000!

Exploitation
Life is extremely easy if we can forge the MMU table. After some properly setting, my 0x217000 maps to physical address 0x0, i.e. the kernel code located at.
Now we only need to invoke read(0, 0x217000+off, len) to overwrite the kernel.

There's an useful hyper-call in emulator that will read a file onto a buffer, use it we can easily read flag2.txt.

kernel_sc = asm('''
        mov rdi, 0
        call sys_load_file
        movabs rdi, 0x8040000000
        add rdi, rax
        mov rsi, 100
        call sys_write
        ret
    sys_write:
        mov eax, 0x11
        mov rbx, rdi
        mov rcx, rsi
        mov rdx, 0
        vmmcall
        ret
    sys_load_file:
        mov eax, 0x30
        mov ebx, 2 /* index 2, the flag2.txt */
        mov rcx, rdi /* addr */
        mov esi, 100 /* len */
        movabs rdx, 0x0
        vmmcall
        ret
        ''')
The full script for this stage is here.
Flag2:
TWCTF{ABI_1nc0n51573ncy_l34d5_70_5y573m_d357ruc710n}

EscapeMe3: control the world
Now is the last stage - pwn the emulator.
To pwn the emulator we must care about there're seccomp rules installed.

Vulnerability
In EscapeMe2 we already have the ability to forge the MMU table, which is also very useful for this stage. The physical memory records on the MMU table actually are offsets of a mmap-ed (in emulator) page, which is right before the libc-2.27.so's pages. So with forging the MMU table carefully we can access the memory in glibc.

And there's a bug in the seccomp rules, which I found it in 5 minutes after the challenge released. Thanks to my great tool seccomp-tools :D

Seccomp-tools' emulator shows clearly that we can have arbitrary syscall if args[0]&0xff < 7.

Remain has no new stuff, just pwn it.

Exploitation
With forging MMU table we have arbitrary memory access, but we need to defeat ASLR first. So I read pointers in libc to leak both libc's base and argv address. Then we can write ROP chain on stack.

I used ROP chain that invokes mprotect(stack, 0x3000, 7) and returns to shellcode on stack.

Since the limitation by seccomp, we cannot launch a shell because the further syscall after execve such as open will be forbidden. So I chose to write the ls shellcode to get the filename of flag3:
asm('''
        /* open('.') */
        mov rdi, 0x605000
        mov rax, 0x2e /* . */
        mov [rdi], rax
        mov rax, 2
        xor rsi, rsi
        cdq
        syscall

        /* getdents */
        mov rdi, rax
        mov rax, 0x4e
        mov rsi, 0x605000
        cdq
        mov dh, 0x10
        syscall

        /* write */
        mov rdi, 1
        mov rsi, 0x605000
        mov rdx, rax
        mov rax, 1
        syscall
    '''))

And got output

Then read the file flag3-415254a0b8be92e0a976f329ad3331aa6bbea816.txt to get the final flag.

Full script

Flag3:
TWCTF{Or1g1n4l_Hyp3rc4ll_15_4_h07b3d_0f_bug5}

Conclusion
This challenge makes me learn a lot about KVM (though its not important in this challenge), and the escaping level by level design is really great and fun.

I will also write a post for noting how KVM works, not only as a note for myself but also (wish to be) a nice article for beginners to know about KVM. Update: comes the KVM introduction post! [Note] Learning KVM - implement your own Linux kernel.

Thanks @shift_crops again for letting me have a great weekend :D

5 則留言:

  1. I want to debug memo-static.elf through VM. I execute run.sh to run program. And use gdb(with gef) attach it. Then search some asm pattern in heap and set breakpoint to it. However once process hit breakpoint, gdb will show `[Inferior 1 (process 16928) exited normally]` and program will show `exit_reason : 8`.
    I found `exit_reason : 8` means KVM_EXIT_SHUTDOWN.
    Did I do some wrong operations? How to debug ELF through VM properly?

    回覆刪除
  2. No you can't set breakpoints at heap. GDB attaches the hypervisor (./kvm.elf), so you can only set breakpoints on the hypervisor.
    Since the hypervisor enables SINGLE_STEP mode (https://github.com/shift-crops/EscapeMe/blob/a71768985939c7402b7303bac07e7e7ef6adb71e/kvm/vm/vm.c#L133), the ioctl(KVM_RUN) will execute exactly one instruction every time.
    To debug memo-static.elf, a simple way is you can set the breakpoint after the ioctl (https://github.com/shift-crops/EscapeMe/blob/a71768985939c7402b7303bac07e7e7ef6adb71e/kvm/vm/vm.c#L163), and use GDB to check if the heap of hypervisor (includes memo-static.elf's stack, heap) has expected data.

    回覆刪除
    回覆
    1. Oh...cool! Get it!
      I should look more careful in hypervisor.
      Thank you so much!

      刪除
  3. I'm new in Security Field. I just came here to see how experts compete in CTF.

    回覆刪除
  4. Hi, I'm trying to run the the example as mentioned (without debugging or anything else):
    ./kvm.elf kernel.bin memo-static.elf

    And I get the following output:
    exit_reason : 8

    Which means: KVM_EXIT_SHUTDOWN

    I tried it on several machines and I keep getting the same result...
    Do you have any idea what could be the problem?

    Your help would be most appreciated...
    Thanks,
    Mark.

    回覆刪除