はじめに
第1章 準備
第2章 login1(スタックバッファオーバーフロー1)
第3章 login2(スタックバッファオーバーフロー2)
第4章 login3(スタックバッファオーバーフロー3)
第5章 rot13(書式文字列攻撃)
第6章 birdcage(関数テーブルの書き換えによる攻撃)
第7章 strstr(double freeに対する攻撃)
第8章 strstrstr(チャンクの統合を利用した攻撃)
第9章 freefree(House of Orange)
第10章 freefree++(file stream oriented programming)
第11章 writefree(House of Corrosion)
第12章 shellsort(シェルコード)
あとがき
情報セキュリティーの分野でCTF(Capture the Flag)とは、セキュリティー技術を競うコンテストのことを指します。予選を勝ち抜いたチームが競うオンサイトのコンテストでは、互いのサーバーを攻撃する攻防戦形式もあります。ですが、多数の参加チームがいるオンラインの予選では、数十個程度の問題を解いて合計点数を競うJeopardy形式(クイズ形式)がほとんどです。出題される問題にはweb、network、binary、forensicsなどのジャンルがあり、その中のひとつにpwnableがあります。本書はpwnableの解説書です。
一般的に、pwnableでは実行ファイルとその実行ファイルが動いているサーバーのアドレスやポート番号が、問題文とともに与えられます。参加者は与えられた実行ファイルを解析して脆弱性を探し、攻撃するスクリプトを作成して、出題者のサーバーを攻撃します。サーバーには「フラグ」と呼ばれる、キーワードが書かれたファイルが置かれています。フラグを入手してスコアサーバーに入力することで、問題を解いたと見なされて点数が入ります。「セキュリティーのコンテスト」と聞いて想像する形に近いジャンルではないでしょうか。
筆者は各ジャンルの中で、pwnableが最も取っ付きにくいと考えています。実際のコンテストでも、pwnableだけは解いているチーム数が少ないという光景をよく目にします。一方で、pwnableは、一度ある程度の技術を身に付ければ、安定して点数が取れるジャンルだとも考えています。目まぐるしく変化するwebなどに比べて、枯れた分野だからです。また、他のジャンルの問題では、得られた情報をどのようにフラグに変換するかの方法が絞れなかったり、とっかかりとなるURLを得る手段がなかったりする問題が「エスパー問題」(エスパーでなければ解けない問題)と非難されることがあります。ですが、pwnableはそのような推測が必要となる事柄が少ないです。
本書のためにpwnableの問題を作り、Dockerを用いてスコアサーバーとともに動かせるようにしました。それぞれの問題の解説とともに、pwnableに用いられる技術を解説していきます。ぜひ実際に手を動かして、攻撃用のスクリプトを書いてみてください。Pwnableの技術を身に付けて、CTFでの高順位を目指しましょう。
本書に記載された内容は、情報の提供のみを目的としています。したがって、本書を用いた開発、製作、運用は、必ずご自身の責任と判断によって行ってください。これらの情報による開発、製作、運用の結果について、著者はいかなる責任も負いません。
また、本書にはサーバーで動作しているプログラムを攻撃する内容が記載されています。これは、コンテストで攻撃対象として提供されるサーバーなど、アクセス管理者が攻撃されることを承諾しているサーバーに対して行われることを意図しています。
本書に記載されている会社名、製品名などは、一般に各社の登録商標または商標、商品名です。会社名、製品名については、本文中では©、®、™マークなどは表示していません。
本書籍は、技術系同人誌即売会「技術書典9」で頒布された「Malleus CTF Pwn Second Edition」を底本としています。
問題サーバーとスコアサーバーをDocker Hubに公開し、Docker1を用いて動かせるようにしています。使用しているOSに応じたDockerをインストールして、下記のコマンドを実行してください。Windows 10にインストールした、Docker for WindowsとDocker Tooolboxで動作することを確認しています。Docker Tooolboxを用いる場合、docker -pでのポート開放の指定に加えて、VirtualBoxでもポートフォワーディングの設定が必要です。本書では、localhostで問題サーバーにアクセスできることを前提としています。仮想マシンや別のマシンで問題サーバーを動かす場合は、解説や攻撃スクリプトのlocalhostを適宜読み替えてください。
>docker run --rm -it -p 10080:80 -p 10001-10013:10001-10013 kusanok/ctfpwn:3
できる限りDocker Hubで公開し続けようと思っていますが、もし何らかの理由でDocker Hubでの公開を停止した場合は、GitHubリポジトリー2からダウンロードして、次のコマンドでビルドしてください。
>docker build -t ctfpwn .
>docker run --rm -it -p 10080:80 -p 10001-10013:10001-10013 ctfpwn
ブラウザーでhttp://localhost:10080/を開き、問題の一覧が表示されれば、正常に動作しています。
docker runを実行している間だけ、問題サーバーとスコアサーバーが動作します。
問題サーバーに対して、攻撃用のスクリプトを実行する環境を準備します。
後述のライブラリーpwntoolsがWindowsでは動かないので、Linuxをおすすめします。筆者はWindows上でWSL(Windows Subsystem for Linux)を使用しています。仮想マシンや、Dockerで問題サーバーを動かしているマシンとは別のマシンを利用する場合、ncコマンドなどで問題サーバーへの疎通を確認してください。仮想マシンのネットワーク設定の変更や、ファイアウォールでのDockerの許可などが必要かもしれません。
$ nc 【問題サーバーのアドレス】 10001
ID: aaa
Password: bbb
Invalid ID or password
ちなみに、ここで50文字程度のIDを入力すると、login1への攻撃が成功し、フラグが表示されます。なぜ攻撃が成立するのかは、login1の章で解説します。
攻撃スクリプトを作成するプログラミング言語は、TCPでの送受信ができればどの言語でもいいのですが、write-up(ブログなどに書かれた解法)を見ていると、Pythonを使っている人が多いようです。本書でもPythonを使用します。本書の攻撃スクリプトは、Python 3.6で動作を確認しています。
pwntoolsは、CTFの問題を解くときに役立つPython用のライブラリーです。同じ処理を自前で書くこともできますが、様々な関数やクラスが用意されていて、やはり便利です。後半で利用します。
$ sudo pip3 install pwntools
python3でPythonの対話環境を立ち上げ、import pwnでエラーが出なければ、正常にインストールができています。
問題の実行ファイルを解析するためのLinuxサーバーを準備します。あわせてインストールしたツールの使用法も、簡単に解説します。
難しい問題でmallocなどの挙動に依存する問題を解くときには、libcのバージョンが問題と一致しているといいでしょう。問題サーバーのOSはUbuntu 20.04で、glibcのバージョンは2.31です。glibc 2.27を対象とした問題は、patchelfでパッチを当てて、Ubuntu 18.04のlibcで動かしています。WSLでも問題ありません。
問題の実行ファイルの逆アセンブルに使用します。本書の問題ではソースコードもダウンロードできますが、コンテストではソースコードが提供されないことが多いです。また、変数のメモリー上の配置などはソースコードからはわからないので、いずれにせよ逆アセンブルが必要となります。より高機能なIDA3やGhidra4も活用できると思います。筆者はobjdumpの出力をテキストファイルに書き出し、メモを書き込みながら解析しています。objdumpはBinutilsに含まれています。
$ sudo apt install binutils
たとえば、rot13の問題の実行ファイルをrot13.txtに逆アセンブルするコマンドは、次のようになります。
$ objdump -d -M intel rot13 > rot13.txt
リスト1.1はrot13.txtの抜粋です。左端の列が実行時のメモリーのアドレス、中央が逆アセンブル元のバイナリ、右が逆アセンブル結果です。
401451: 88 94 05 f0 fe ff ff mov BYTE PTR [rbp+rax*1-0x110],dl
401458: 83 85 e8 fe ff ff 01 add DWORD PTR [rbp-0x118],0x1
40145f: 81 bd e8 fe ff ff ff cmp DWORD PTR [rbp-0x118],0xff
401466: 00 00 00
401469: 0f 8e 00 ff ff ff jle 40136f <main+0x18e>
40146f: 48 8d 85 f0 fe ff ff lea rax,[rbp-0x110]
401476: 48 89 c7 mov rdi,rax
401479: b8 00 00 00 00 mov eax,0x0
40147e: e8 cd fb ff ff call 401050 <printf@plt>
401483: 48 8d 3d 7e 0b 00 00 lea rdi,[rip+0xb7e] # 402008 <_...
40148a: e8 a1 fb ff ff call 401030 <puts@plt>
アセンブリ言語の文法はシンプルで、1行ごとに命令の種類を表すニーモニックと、操作対象を示すディスティネーションとソースが書かれています。たとえば、アドレス401476の命令はmovがニーモニック、rdiがディスティネーション、raxがソースです。各命令は、ソース(とディスティネーション)を読んで演算を行い、結果をディスティネーションに書き込みます。ソースとディスティネーションをあわせて、オペランドといいます。命令の一次資料はIntelのドキュメント5ですが、命令名と「ニーモニック」や「アセンブリ」などのキーワードで検索して解説しているページを見れば、たいていはこと足りるでしょう。
アセンブリ言語を理解する際につまずくのは、lea命令でしょうか。lea命令は、メモリーのアドレス自体をディスティネーションに書き込みます。例えば、rbpの値が0x7ffffffedca0のとき、lea rax,[rbp-0x110]はraxに0x7ffffffedb90を書き込みます。これがmov rax,[rbp-0x110]となると、raxにはメモリーの0x7ffffffedb90に格納されている値が書き込まれます。メモリーアドレスの指定では、制限はあるものの加算や乗算ができるので、これをアドレス指定だけではなく、一般の演算に活用できるようになっています。
raxやrdiなどはCPU内部のレジスタです。1個のレジスタは複数のサイズで読み書きできます。64ビットのレジスタraxの下位32ビットはeaxであり、下位16ビットはaxです。axの上位8ビットはah、下位8ビットはalとなります。[rbp+rax*1-0x110]のような表記は、メモリーのアドレスを表します。
Linuxのx64の関数呼び出し規約6では、関数の引数は第1引数から順に、rdi、rsi、rdx、rcx、r8、r9で渡されます。