PcapでマスクされたIPを復元する(DEF CON CTF 31参加記)

サイバーディフェンス研究所 アドベントカレンダー 2023の18日目の記事です。

こんにちは。技術統括部の前田です。

開催から少し時間が経過してしまいましたが、今年のDEF CON CTFに参加した概要とその中で運営のミス(?)を突いたIP復元の話をしたいと思います。

チームフラグ

DEF CON CTFとは?

CTFはCapture The Flagの略で、コンピュータセキュリティに関する問題を解いてフラグを獲得することにより点数を競う競技です。

中でもDEF CON CTFは世界最大のハッカーイベントであるDEF CONの一部として開催されることから、最も影響力のあるCTFの1つです。 競技は様々なルール形式を合わせた複合ルールで、2日半にかけて開催されました。 ルールは各チームに全く同じサービスが割り当てられ、互いを攻撃しあうAttack & Defense(A&D)形式を主軸に、解法の質を競い合うKing of the Hillや1対1で問題を解く速さを競うLiveCTFが同時進行で行われました。

CDIからは3名が日本人合同チームのundef1nedの一員として決勝に出場しました。 結果は振るわず最下位となってしまいましたが、振り返るととにかく準備不足を感じるので仕方ない気もします。 DEF CON CTFでは事前準備の量が競技中にじわじわ効いてくるので、次回出ることがあればしっかり準備したいですね。

A&D CTFの特徴

A&D形式のCTFでは、脆弱性を探して攻撃を成功させる以外にも様々なアプローチで問題を解析することがとても重要です。 統一されたルールがあるわけではないので個々のCTFにもよりますが、パケットキャプチャの解析、相手チームのパッチ解析、得点状況の分析など、とにかくやることがたくさんあるのがA&D形式です。

パケットキャプチャ

前述の通り、A&D形式のCTFでは何らかの手段でパケットが入手できることが多いです。今回のDEF CON CTFでも運営チームが15分ごとにpcapファイルを配布していました。 このパケットを解析することで、自分たちがまだ気づいていない脆弱性を探すのに役立てたり、場合によってはそのまま内容をリプレイして攻撃転換したりすることができます。

しかし、競技進行上の都合でキャプチャデータ全てをそのまま貰えるわけではありません。多くの場合は必要以上に情報を渡してしまわないように加工がされています。 今回の場合は攻撃元のIPアドレスが 0.0.0.0 になるようにマスクされていました。 マスクされている理由は公表されていませんが、一般的には攻撃元がわからないようにするという目的で送信元IPは匿名化されます。

IPの復元

1日目終了後、会場からホテルに戻った後の出来事です。

自分がWiresharkを眺めながら「チェックサムエラーで読みにくいタイプのやつだ1」とつぶやくと、チームメイトから「IP戻せるんじゃない?」という一言。 ちなみにこの時のWiresharkの画面は下図のように真っ黒でした。

before

詳しく確認すると、まずTCPチェックサムの部分でエラーが起きていることがわかりました。 しかしTCPレイヤーにはIPアドレスが含まれないため、復元には使えません。

次に、念のためIPヘッダも確認すると、こちらもチェックサムエラーの表示。 チェックサムの部分もゼロ埋めされている可能性も考慮しましたが、意味のありそうな値が入っていたのでIPアドレスの復元を試すことにしました。

これは運営から提供されたパケットの一例です。 チェックサムには0x3718が入っていて、SrcIPはゼロ埋め、DestIPには10.10.11.1 という自分のチームのサーバーを表すIPが指定されています。 Wiresharkにはチェックサムを計算してくれる機能があり、それによると0x427cが正しいチェックサムでした。 こうなる理由はいくつか考えられますが、チェックサムがSrcIPをゼロ埋めする前に計算されたと仮定して実験してみます。

IPチェックサムは16ビットなのでIPの検索範囲が広いと衝突してしまいます。 通常であれば衝突しまくって使い物になりませんが、競技環境の仕様を使って探索範囲を絞れば特定できます。 CTF競技環境ではIP10.0.[teamid].[client]がプレイヤー用ネットワークに割り当てられていました。 これならほぼ一意に定まります。

チェックサムが一致するIPを検索するPythonスクリプトを書いて実行すると10.0.1.100が表示されます。競技環境では100以降がDHCPで割り当てられていたので、正しくIPが復元できていると考えていいでしょう。

header = '4500003ce63540003d060000000000000a0a0b01'
correct = 0x3718

zeroip = 0
for i in range(0, len(header), 4):
    zeroip += int(header[i:i+4], 16)

for teamid in range(1, 13):
    for client in range(256):
        checksum = zeroip + 0x0a00 + (teamid << 8) + client
        checksum = (checksum & 0xffff) + (checksum >> 16)
        checksum = checksum ^ 0xffff
        if checksum == correct:
            print(f'10.0.{teamid}.{client}')

pcap全体の復元

パケット1つで実験した後は、複数パケットで検証して本当にすべてのパケットで復元できるかどうかを確認します。

pcapファイル全体を読み込むためにPythonのdpktパッケージを使います。 個々のパケットからIPヘッダーを抽出して前述のアルゴリズムでIPを特定します。 あとはsrc/dstのうちゼロ埋めされている方にそのIPを上書きしてあげればOKです。

import dpkt
import sys


def find_ip(header):
    target = int.from_bytes(header[12:14], 'big')
    emptyip = 0
    for i in range(0, len(header), 2):
        if i == 12:
            # skip checksum
            continue
        emptyip += int.from_bytes(header[i:i+2], 'big')

    ips = []
    for teamid in range(1, 13):
        for client in range(256):
            checksum = emptyip + 0x0a00 + (teamid << 8) + client
            checksum = (checksum & 0xffff) + (checksum >> 16)
            checksum = checksum ^ 0xffff
            if checksum == target:
                ips.append(bytes([10, 0, teamid, client]))

    if len(ips) == 1:
        return ips[0]
    else:
        return None


def recovery(filename: str):
    reader = dpkt.pcap.Reader(open(filename, 'rb'))
    writer = dpkt.pcap.Writer(open('output.pcap', 'wb'))

    for ts, buf in reader:
        eth = dpkt.ethernet.Ethernet(buf)

        if not isinstance(eth.data, dpkt.ip.IP):
            writer.writepkt(eth, ts)
            continue

        ipheader = eth.data.pack_hdr()
        ip = find_ip(ipheader)
        if ip:
            if eth.data.dst == b'\x00\x00\x00\x00':
                eth.data.dst = ip
            elif eth.data.src == b'\x00\x00\x00\x00':
                eth.data.src = ip

        writer.writepkt(eth, ts)

    writer.close()


if __name__ == '__main__':
    if len(sys.argv) < 2:
        sys.exit(1)

    recovery(sys.argv[1])

WiresharkでIPチェックサムの検証を有効にして開いてみると、真っ黒だったパケット一覧がきれいな色に戻りました2

after

ちなみに競技中は本当に全部復元できるか不安だったので、より精度を高めるためにTCPストリームの先頭3パケットから逆算されたIPを使うアルゴリズムを採用していました。

CTFプレイヤー同士の交流

競技終了後には、チームBlue Water主催でアフターパーティーが行われました。 個人的には騒がしい場所が苦手なので最初の1~2時間で撤退してしまいましたが、500平米以上あるスイート3がパンパンになるほどの混雑ぶりでした。 Black HatやDEF CONの期間は他にも各所でパーティが開かれていますが、積極的に立ち回るのが苦手な自分にとって海外のパーティ文化はなかなか恐ろしいものです。

DEF CON CTFではCTFに取り組むのはもちろんですが、世界中のCTFプレイヤーが一堂に会するため、このようなパーティは貴重な交流の機会です。 ちなみに日本人同士でも例外ではなく、10人以上のプレイヤーが同じチームで現地参加するCTFは他には滅多にありません。 DEF CONに限らずこういう機会が増えるといいなと密かに思っています。

おわりに

実はプレイヤーの間では恒例になりつつあるのですが、DEF CON CTFの運営はかなり不安定で、ルールが急に変わったりトラブルが起きたりが頻繁に起こります。 今回も例に漏れず色々起こり4、何度も集中が途切れてイライラする場面がありました。 しかし人間というのは不思議な生き物で、しばらく経つと記憶が薄れてきて、いい思い出の部分を元にまた来年も参戦したいなと思い始めています。

最後に、一緒に参戦してくれたチームメンバーの皆様、ありがとうございました。 来年も機会があれば是非よろしくお願いします。


  1. Wiresharkではデフォルトでチェックサムを検証しない設定になっているので気づきにくいです ↩︎

  2. srcipが10.10.11.1のパケットはTCPチェックサムの計算でエラーになるためTCPの検証はオフにしています ↩︎

  3. 寝泊りしている感があったのでBlue Waterはこの部屋からCTFに参加していたと思います ↩︎

  4. 気になる方はお近くの参加者にお尋ねください ↩︎

© 2016 - 2024 DARK MATTER / Built with Hugo / テーマ StackJimmy によって設計されています。