『CTF形式で学ぶ Cプログラムの脆弱性』事前課題編
『CTF形式で学ぶ Cプログラムの脆弱性』の事前課題です。
www.security-camp.or.jp
※事前課題に取り組む前に、以下の記事に従って環境構築を行ってください。
haibara-works.hatenablog.com
- 【事前課題1】VMにSSHで接続してみよう
- 【事前課題2】VM上でプログラミングしてみよう
- 【事前課題3】Pwntoolsを使ってみよう
- 【事前課題4】gdb-pedaでプログラムを解析してみよう
- 【事前課題5】Pwntools・gdb-pedaを利用した攻撃
【事前課題1】VMにSSHで接続してみよう
- 確認すること:VMのネットワークの設定
環境構築編の「演習用VMへのSSH接続」のときと同じですが、改めてVMにSSH接続する方法を確認します。
VMのIPアドレスの確認
まずはVirtualBoxでVMを起動して、ログインします。ユーザー名は「alice」、パスワードは「password」です。
ログインができたら、SSH接続に必要なVMのIPアドレスを確認します。ターミナルにip a
と打ち込んで実行します。すると以下のような出力が表示されます(一部数値が異なる場合があります)。
alice@debian:~$ ip a 1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN group default qlen 1000 link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00 inet 127.0.0.1/8 scope host lo valid_lft forever preferred_lft forever inet6 ::1/128 scope host valid_lft forever preferred_lft forever 2: enp0s3: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc pfifo_fast state UP group default qlen 1000 link/ether 08:00:27:d0:e6:4f brd ff:ff:ff:ff:ff:ff inet 192.168.1.9/24 brd 192.168.1.255 scope global dynamic enp0s3 valid_lft 14222sec preferred_lft 14222sec inet6 2404:7a83:a520:3400:a00:27ff:fed0:e64f/64 scope global dynamic mngtmpaddr valid_lft 2591992sec preferred_lft 604792sec inet6 fe80::a00:27ff:fed0:e64f/64 scope link valid_lft forever preferred_lft forever
ここでは詳しい説明は省きますが、2: enp0s3
という項目のinet 192.168.1.9/24
がSSH接続に必要なIPアドレスです。SSHクライアントで下表のように指定することで、演習用VMにSSH接続することができます。
IPアドレス | 192.168.1.9 |
ユーザー名 | alice |
パスワード | password |
VMへのSSH接続
SSHクライアントとして、Windowsのコマンドプロンプトを使う例を紹介します。LinuxやMacをお使いの方は、それぞれのターミナルとして読み替えてください。
コマンドプロンプトを開いたら、次のコマンドを打ち込んで実行します。
ssh alice@<VMのIPアドレス>
「VMのIPアドレス」は先ほど調べたIPアドレスです。ここでのIPアドレスは192.168.1.9でしたので、ssh alice@192.168.1.9
と入力します。出力は以下のようになります。【】で囲んだ文は補足コメントです。
【コマンドプロンプトでsshコマンドを打ち込む】 C:\Users\haibara>ssh alice@192.168.1.9 The authenticity of host '192.168.1.9 (192.168.1.9)' can't be established. ECDSA key fingerprint is SHA256:iJ2rhaLbeTdl73AZqKI53LOvM1lPeCATex2mxSOzdAE. 【「yes」と入力】 Are you sure you want to continue connecting (yes/no)? yes Warning: Permanently added '192.168.1.9' (ECDSA) to the list of known hosts. 【パスワードを入力 (password)】 alice@192.168.1.9's password: Linux debian 4.19.0-10-amd64 #1 SMP Debian 4.19.132-1 (2020-07-24) x86_64 The programs included with the Debian GNU/Linux system are free software; the exact distribution terms for each program are described in the individual files in /usr/share/doc/*/copyright. Debian GNU/Linux comes with ABSOLUTELY NO WARRANTY, to the extent permitted by applicable law. Last login: Wed Oct 21 10:58:50 2020 from 192.168.1.4 【VMにログインできた!】 alice@debian:~$
【】で囲んだ補足コメントの通り、Are you sure you want to continue connecting (yes/no)?
と聞かれたら「yes」を、alice@192.168.1.9's password:
と表示されたら「password」(aliceのパスワード)をそれぞれ入力します。
alice@debian:~$
と出力されれば、VMにログインできています。ログインに成功すれば【事前課題1】は終了です!
【事前課題2】VM上でプログラミングしてみよう
- 確認すること:ターミナルとエディタの使い方
【事前課題2】では、VM上のファイルを編集してプログラミングをしてみます。
ひな形ファイルの確認
演習用VMには、ひな形となるファイルが用意されています。~/sample/
というディレクトリを開いてみましょう。
【事前課題1】に従って、VMにSSH接続した後、次のようにコマンドを実行するとディレクトリ内のファイルを確認することができます。
alice@debian:~$ cd sample/ 【~/sampleディレクトリに移動】 alice@debian:~/sample$ ls 【このディレクトリ内にあるファイル・ディレクトリを表示】 Makefile a.out exploit.py main.c setup.sh
ディレクトリ内にいくつかのファイルがあることがわかります。以下にそれぞれのファイルの説明を示します。
ファイル名 | 説明 |
---|---|
main.c | C言語のソースファイル |
Makefile | main.cをコンパイルするためのmakefile |
a.out | Makefileによってmain.cから生成された実行ファイル |
setup.sh | この後の演習で使うシェルスクリプト |
exploit.py | この後の演習で使うPythonスクリプト |
cat
コマンドを使って、main.c
・Makefile
の内容を表示させてみましょう。
- main.cの内容を表示
alice@debian:~/sample$ cat main.c #include <stdio.h> int main() { printf("Hello, mc-do!\n"); }
main.c
は、"Hello, mc-do!"という文字列を改行付きで出力するプログラムであることがわかります。
- Makefileの内容を表示
alice@debian:~/sample$ cat Makefile .PHONY: all a.out: main.c gcc -m32 -fno-stack-protector main.c -o a.out
makefileは、make
コマンドへの指示を記述するファイルです。makefileのあるディレクトリでmake
コマンドを実行することで、makefileに記述された処理を実行することができます。また、makefileをmakefile
またはMakefile
というファイル名で保存することで、自動的にmake
コマンドがmakefileの内容を取得します。
makefileの文法については触れませんが、基本的には「あるファイルを、何かの処理によって、別のファイルに変換する」という処理を記述します。~/sample/
ディレクトリにあるMakefile
は、「main.c
を、gcc -m32 -fno-stack-protector main.c -o a.out
というコマンドによって、a.out
に変換する」という意味を持ちます。
- a.outの確認
a.out
は、Makefile
によってmain.c
から生成されたファイルです。つまり、main.c
をコンパイルして出来た実行ファイルです。これはMakefile
やmain.c
と違い、テキストファイルではなくバイナリファイルです。まずはa.out
を実行してみましょう。以下のようにターミナルに打ち込みます。
alice@debian:~/sample$ ./a.out Hello, mc-do!
main.c
の記述通り、"Hello, mc-do!"という文字列が出力されています。
main.cの編集
main.c
の内容を書き換えてみましょう。VSCodeからSSH接続してファイルを開く方法もありますが、ここではVM上でCUIエディタを使う方法を紹介します。
CUIエディタにはnano・vim・emacsなどがあります。別に使い慣れたエディタがあれば、それを使っても構いません。
ここではnanoを使う方法を紹介します。nanoはvim・emacsに比べて、機能が少ない代わりに操作が簡単です。以下のコマンドによってmain.cをnanoで開きます。
alice@debian:~/sample$ nano main.c
コマンドを実行すると、次のような画面が表示されるはずです。
nanoでmain.c
を開けていますね。
ここで、printfで出力する文字列を"FLAG{hello_ctf!}\n"
に書き換えてみましょう。
まずは、printfの所までカーソルを移動させて、バックスペースで"Hello, mc-do!\n"
を消します。
その後に、"FLAG{hello_ctf!}\n"
をprintfのかっこの中にコピペします。
ブラウザでこの文字列をコピーして、(コマンドプロンプトの場合は) ターミナルのウィンドウを右クリックすることで、その時のカーソルの位置にペーストをすることができます。
文字列をコピペすることができたら、この変更をファイルに上書き保存して、nanoを閉じます。キーボードのコントロールキーとxキーを同時に押すと、保存するか聞かれます。
ここでyキーを押すと、今度は保存する際のファイル名を聞かれます。今回は上書き保存をするため、main.c
のままにします。エンターキーを押すと、nanoが終了し、main.c
には先ほどの変更が上書きされています。
cat
コマンドで確認してみましょう。
alice@debian:~/sample$ cat main.c #include <stdio.h> int main() { printf("FLAG{hello_ctf!}\n"); }
確かに先ほどの変更が上書きされていることが確認できました。
makefileによるコンパイル
main.c
を書き換えることができたら、コンパイルを行い、a.out
を更新してみましょう。~/sample/
ディレクトリで、make
コマンドを実行します。
alice@debian:~/sample$ make gcc -m32 -fno-stack-protector main.c -o a.out
Makefile
の内容に従って、main.c
がコンパイルされます。更新されたa.out
を実行してみましょう。
alice@debian:~/sample$ ./a.out FLAG{hello_ctf!}
a.out
が更新されていることが分かります。このように、FLAG{hello_ctf!}
と出力するa.out
を作ることができれば【事前課題2】は完了です!
【事前課題3】Pwntoolsを使ってみよう
- 確認すること:Pwntoolsの基本的な使い方
【事前課題3】では、PythonのライブラリであるPwntoolsを使ってみましょう。Pwntoolsは、この講義のテーマであるPwnを解く作業を自動化することができる便利なライブラリです。講義内の演習では、Pwntoolsを使ったエクスプロイトコード(脆弱性を攻撃するスクリプト)の作成にチャレンジします。その前に、PythonとPwntoolsの基本的な使い方を確認しましょう。
ひな形ファイルの確認
~/sample/
ディレクトリにあるexploit.py
は、Pwntoolsを使った簡単なスクリプトです。cat
コマンドで内容を表示してみます。
alice@debian:~/sample$ cat exploit.py from pwn import * io = process("./a.out") msg = io.recvline().decode() print("msg:", msg)
次のコマンドで、このスクリプトを実行します。
alice@debian:~/sample$ python3 exploit.py [+] Starting local process './a.out': pid 5756 msg: FLAG{hello_ctf!}
exploit.py
は、a.out
の出力を取得するスクリプトです。exploit.py
の各行は以下のような意味を持ちます。
from pwn import * # Pwntoolsのインポート io = process("./a.out") # a.out を開く msg = io.recvline().decode() # a.outの出力を取得して、変数msgに代入 print("msg:", msg) # msgの値を出力
補足:Pythonのbytesとstring
recvline()
メソッドはプログラムからの出力を返しますが、上のスクリプト内ではさらにdecode()
メソッドを使っています。これはrecvline()
の戻り値の型がbytes
であるため、それをstring
に変換するために必要です。
試しにdecode()
を使わない以下のスクリプトを実行してみましょう。
from pwn import * io = process("./a.out") msg = io.recvline() print("msg:", msg)
alice@debian:~/sample$ python3 exploit.py [+] Starting local process './a.out': pid 7137 msg: b'FLAG{hello_ctf!}\n'
実際の改行ではなく、改行を表す\n
そのものが出力されているのがわかります。
プログラムとのやり取りを自動化する
Pwnでは、対話型のプログラム(ユーザーから入力を受け取り、それに応じて出力を行うプログラム)の脆弱性を攻撃します。つまり、エクスプロイトコードは、脆弱なプログラムへの入出力を自動化することになります。
exploit.py
では単純にプログラムの出力を取得するだけでした。他にもPwntoolsによる入出力の扱いを見ていきましょう。
Pwntoolsの主な入出力を扱うメソッドを以下に示します。
メソッド名 | 説明 | 戻り値 |
---|---|---|
recvline() | 1行分(改行が来るまで)プログラムから読み取る。 | 読み取ったデータ |
recvuntil(delims) | delimsで指定したデータが来るまでプログラムから読み取る。 | 読み取ったデータ |
sendline(data) | dataを改行付きでプログラムへ出力する。 | 無し |
sendlineafter(delim, data) | recvuntil(delim) とsendline(data) を共に実行する。つまり、delimで指定したデータが来るまでプログラムから読み取った後に、dataを改行付きでプログラムへ出力する。 |
読み取ったデータ |
Pwntoolsには他にも多くの機能があります。詳細な情報は公式のドキュメント(pwntools — pwntools 4.3.1 documentation)を確認してみてください。
また、こちらの8ayacさんの記事はとても分かりやすく、Pwntoolsで何ができるのかを大まかに知ることができます。
qiita.com
さて、上表のメソッドを利用して、対話プログラムとのやり取りを自動化してみましょう。まずはmain.c
を次のように書き換えてください。
#include <stdio.h> #include <string.h> void vuln() { char buf[8]; printf(">> "); scanf("%s", buf); if (!strcmp(buf, "flag")) { printf("FLAG{hello_ctf!}\n"); } } int main() { vuln(); return 0; }
【事前課題2】と同じように、ターミナルを右クリックしてエディタに張り付けましょう。
main.c
を書き換えたら、make
コマンドでコンパイルします。
alice@debian:~/sample$ make gcc -m32 -fno-stack-protector main.c -o a.out
コンパイルできたら更新されたa.out
を実行して見ましょう。
alice@debian:~/sample$ ./a.out Hello everyone! >>
>>
が表示された後に、入力待ちの状態になります。ここでflag
と打ち込むと、FLAG{hello_ctf!}
と出力されます。flag
以外の文字列が入力された場合には、そのままプログラムが終了します。
alice@debian:~/sample$ ./a.out Hello everyone! >> flag FLAG{hello_ctf!}
それでは、このa.out
と自動でやり取りしてFLAG{hello_ctf!}
を取得するように、~/sample/exploit.py
を書き換えてみましょう。このとき、以下のようにprocess
メソッドに引数を追加してください。
io = process("./a.out", stdin=process.PTY)
process
の引数に、stdin=process.PTY
が追加されています。これは対象のプログラム(ここではa.out
)と対話的な入出力をするためのオプションです。
(参考:https://docs.pwntools.com/en/stable/tubes/processes.html#module-pwnlib.tubes.process)
- ヒント:FLAGを取得する手順
a.out
から>>
が出力された後に、a.out
にflag
と読ませる。- 出力されたFLAGを読み込む。
https://docs.python.org/ja/3/library/stdtypes.html#bytes
exploit.py
でFLAGを取得できたら、【事前課題3】は終了です!
【事前課題4】gdb-pedaでプログラムを解析してみよう
- 確認すること:gdb-pedaの基本的な使い方
gdb-pedaを使って実行ファイルの解析をしてみましょう。gdb-pedaは、GDBというデバッガにPEDAという拡張機能を追加したものです。
github.com
重要:解析の前に必ず~/sample/setup.sh
を実行してください。
alice@debian:~/sample$ ./setup.sh [sudo] password for alice: 【password と入力】 kernel.randomize_va_space = 0
~/sample/
ディレクトリで以下のコマンドを実行すると、gdb-pedaが起動します。
alice@debian:~/sample$ gdb ./a.out
gdb-pedaが起動すると、ターミナルの表示がgdb-peda$
という赤い文字に変わります。q
を入力すれば、gdb-pedaを終了できます。それではgdb-pedaのいくつかの機能を見てみます。
r:プログラムの実行
r
コマンドは「run」の略で、プログラムを実行することができます。
gdb-peda$ r Starting program: /home/alice/sample/tmp/a.out >> flag FLAG{hello_ctf!} [Inferior 1 (process 7717) exited normally] Warning: not running
checksec:セキュリティ機能の確認
checksec
コマンドを使うと、実行ファイルのセキュリティ機能について確認できます。gdb-pedaで実行してみます。
gdb-peda$ checksec CANARY : disabled FORTIFY : disabled NX : ENABLED PIE : ENABLED RELRO : Partial
「CANARY」や「NX」などはセキュリティ機能の名称です。ここでは触れませんが、実行ファイルがどのようなセキュリティ機能を持っていることが確認でき、攻撃の方針を立てる際に役立ちます。
disas:ディスアセンブル
disas
コマンドは、指定した関数をディスアセンブルするコマンドです。ディスアセンブルとは、機械語(バイナリ)をアセンブリ(テキスト)に変換することです。Pwnではソースコードは公開されずに、実行ファイルだけが渡される問題もあります。このようにソースコードが無い場合には、ディスアセンブル結果が実行ファイルを読み解くヒントになります。
gdb-peda$ disas main Dump of assembler code for function main: 0x56556235 <+0>: push ebp 0x56556236 <+1>: mov ebp,esp 0x56556238 <+3>: and esp,0xfffffff0 0x5655623b <+6>: call 0x56556251 <__x86.get_pc_thunk.ax> 0x56556240 <+11>: add eax,0x2dc0 0x56556245 <+16>: call 0x565561c9 <vuln> 0x5655624a <+21>: mov eax,0x0 0x5655624f <+26>: leave 0x56556250 <+27>: ret End of assembler dump.
※ 現時点でアセンブリを理解している必要はありません。
p:関数のアドレスなどを確認
p
コマンドはprintの略で、レジスタの状態などあらゆるものを出力できます。ここでは関数のアドレスを確認する例を紹介します。
重要:関数のアドレスを確認する前に、一度r
コマンドでプログラムを実行してください。
gdb-peda$ p main $1 = {<text variable, no debug info>} 0x56556260 <main>
main
のアドレスは0x1235であることが分かります。
pattc・patto:バッファのオフセットを調査
pattc
コマンドは、指定した長さのパターン文字列を生成します。
gdb-peda$ pattc 30 'AAA%AAsAABAA$AAnAACAA-AA(AADAA'
30文字分のパターンが生成されました。このパターン文字列をプログラムに読ませてみます。
gdb-peda$ run Starting program: /home/alice/sample/a.out >> AAA%AAsAABAA$AAnAACAA-AA(AADAA Program received signal SIGSEGV, Segmentation fault. [----------------------------------registers-----------------------------------] EAX: 0xffffffff EBX: 0x6e414124 ('$AAn') ECX: 0x66 ('f') EDX: 0xffffd5e8 ("AAA%AAsAABAA$AAnAACAA-AA(AADAA") ESI: 0xf7fbf000 --> 0x1d9d6c EDI: 0xf7fbf000 --> 0x1d9d6c EBP: 0x41434141 ('AACA') ESP: 0xffffd600 ("(AADAA") EIP: 0x41412d41 ('A-AA') EFLAGS: 0x10286 (carry PARITY adjust zero SIGN trap INTERRUPT direction overflow) [-------------------------------------code-------------------------------------] Invalid $PC address: 0x41412d41 [------------------------------------stack-------------------------------------] 0000| 0xffffd600 ("(AADAA") 0004| 0xffffd604 --> 0xf7004141 0008| 0xffffd608 --> 0x0 0012| 0xffffd60c --> 0xf7dffb41 (<__libc_start_main+241>: add esp,0x10) 0016| 0xffffd610 --> 0x1 0020| 0xffffd614 --> 0xffffd6a4 --> 0xffffd7d2 ("/home/alice/sample/a.out") 0024| 0xffffd618 --> 0xffffd6ac --> 0xffffd7eb ("SHELL=/bin/bash") 0028| 0xffffd61c --> 0xffffd634 --> 0x0 [------------------------------------------------------------------------------] Legend: code, data, rodata, value Stopped reason: SIGSEGV 0x41412d41 in ?? ()
何やらたくさん出力されました。焦らずに上から見ていくと、プログラムにパターン文字列を読ませた直後に「Program received signal SIGSEGV, Segmentation fault.」とあります。【事前課題3】で書き換えたmain.c
を見れば分かるように、vuln関数内の配列bufのサイズは8です。それに対して30文字分の入力を与えたために、セグメンテーションフォルトが発生しています。
gdb-pedaでは、解析するプログラムがセグメンテーションフォルトを発生させると、その時のレジスタやスタックの状態を表示してくれます。ここで注目すべきは、EIPというレジスタの値です。上のgdb-pedaの出力のregistersの中からEIPの項目を見てみてください。EIP: 0x41412d41 ('A-AA')
となっており、EIPの値が入力したパターン文字列の一部(A-AA
)で上書きされていることがわかります。このままだと、ただプログラムの実行が失敗しただけのように見えますが、EIPが上書きできたということは一大事なのです。
なぜなら、EIPというレジスタは「次に実行する命令のアドレス」という意味を持つレジスタだからです。つまり、「EIPの上書きができる」ということは、「次に実行する命令を決めることができる」ことに等しいのです。
EIPの上書きが可能なことがわかったら、攻撃者が次にやりたいのは「EIPに任意の値を書き込む」ことです。この例では、プログラム内の配列のサイズより十分に大きいサイズ(ここでは30)の文字列を読ませることで、文字列のどこかはEIPに書き込まれています。ぴったりEIPのアドレスに値を書き込むためには何文字分の文字列を読ませればいいのかがわかれば、「EIPに任意の値を書き込む」ことができそうです。
そして、pattc
コマンドとpatto
コマンドを組み合わせることで、それが明らかになります。EIPに上書きされた文字列であるA-AA
をpatto
コマンドの引数として実行してみます。
gdb-peda$ patto A-AA A-AA found at offset: 20
「A-AA
はオフセット 20で見つかりました」と表示されています。実はpattc
コマンドで生成された文字列は、「どの連続した4文字を取ってきても重複せず、パターン文字列の何文字目かがわかる」という性質を持っています。そのため、
pattc
コマンドで十分に長いパターン文字列を生成する- EIPを上書きした文字列を確かめる
patto
コマンドでEIPを上書きした文字列が、パターン文字列の何文字目から始まるのかを確かめる
という手順で、EIPに特定の値を書き込むための入力文字数がわかるのです。
ここまでの文章に目を通したら、【事前課題4】は終了です!
【事前課題5】Pwntools・gdb-pedaを利用した攻撃
これが最後の事前課題です。これまでの課題に登場したテクニックを使って、実際に脆弱なプログラムを攻撃してみましょう。
まずは問題になる脆弱なプログラムを作成します。以下のCプログラムをmain.c
に上書きしてください。
#include <stdio.h> void printFlag() { printf("FLAG{great_hacking!}\n"); } void vuln() { char buf[8]; printf(">> "); scanf("%s", buf); } int main() { vuln(); printf("There is no FLAG...\n"); return 0; }
main.c
を上書きできたら、make
コマンドでコンパイルします。
alice@debian:~/sample$ make gcc -m32 -fno-stack-protector main.c -o a.out
更新されたa.out
を実行してみます。
alice@debian:~/sample$ ./a.out >> foo 【適当にfooと入力】 There is no FLAG...
ソースコードを見るとわかる通り、(入力文字数が配列のサイズ以内であれば) FLAGを出力するprintFlag()
関数は実行されません。
【演習問題5】は、a.out
にFLAGを出力させるPythonスクリプトを作成できたら終了です!!!
- ヒント
お疲れ様でした!!講義当日を楽しみにしています!!!