これは、私のタームペーパーのHTML版であり、PDFとしてダウンロードできます ここ。
ファジングは、ソフトウェアのバグを見つける「最も効果的な方法の1つ」となっています。このような主張や 類似した主張で、現在のファジング関連の論文の多くが始まります [google-scholar]。 「Internet of Vulnerable Things」というトピックに関する前回のタームペーパーの主な目標は、メモリ関連のバグを見つけて、 この脆弱性のエクスプロイトを書くことでした。ファームウェアをリバースエンジニアリングして脆弱性を 見つけることはできましたが、メモリ関連のバグは見つかりませんでした。バッファオーバーフローを 手動でバイナリをリバースして見つけるのは、時間がかかるだけでなく、多くの経験も必要です。 同時に、ファジングはそのようなメモリ関連の脆弱性を見つける「最も効果的な方法」を目指しています。 例えば、Googleはオープンソースソフトウェアを継続的にファジングするOSS-Fuzzを導入し、 すでに1,000プロジェクトで10,000以上の脆弱性を発見しています [oss-fuzz]。
このタームペーパーの目標は、再びメモリ関連の脆弱性を見つけることですが、今回はファジングを 使用します。対象となる脆弱性は、管理者の資格情報を知らなくてもネットワーク経由で悪用可能で ある必要があります。この論文はこの目標を達成する方法を説明します。そのために、論文は 2つの部分に分かれています。最初の部分は、有力なターゲットを見つける方法、使用できるツール、 優れたファジングターゲットが何で構成されるべきかに焦点を当てています。2番目の部分では、 バイナリ内の特定の関数をファジングできるハーネスを開発およびデバッグする方法を説明します。 そして、開発されたハーネスはAFL++によってターゲット関数をファジングするために使用されます。 以下では、IoTデバイスのファジングに関する現在の技術水準と背景を簡単に説明します。
このタームペーパーの文脈で作成されたすべてのファイルは、GitHub上で完全に公開されており、 次のURLからアクセスできます。 otsmr/blackbox-fuzzing。
IoTデバイスのファジングは、オープンソースプロジェクトのファジングほど簡単ではありません。多くの場合、ソースコードは プロプライエタリであり、最高のファジングパフォーマンスのためにソースコードを計測するグレーボックスファジングは 不可能です [afl-persistent]。 また、CPUアーキテクチャがファザーによってネイティブにサポートされていないことが多く、 QEMU [qemu] のようなエミュレータが必要になり、ファジング速度も遅くなります。 [afl-persistent]。 もう1つの問題は、ハードウェアペリフェラルであり、一般的なアプローチの開発を複雑にします。 論文「Embedded Fuzzing: A Review of Challenges, Tools, and Solutions」 [embedded-fuzzing] は、ハードウェアベースの組み込みファジングなど、さまざまなファジング戦略の概要を提供します。これらの戦略のほとんどは、 ターゲットプログラムのソースコードを必要とします。例えば、ファザーのソースコード(AFLなど)をARMベースのIoTデバイスに 移植してIoTハードウェア上でファザーを実行する場合です。デバイスのハードウェア上でファザーを実行すると、 パフォーマンスの問題もあります。多くの場合、通常のデスクトップCPUよりも遅い低レベルのCPUを搭載しているためです。 この論文で紹介されているもう1つのアプローチは、エミュレーションベースの組み込みファジングです。 これは、単一のターゲットプログラムをエミュレータ内で実行してカバレッジガイド付きファジングを実行するか、 システム全体を実行します。
前述のアプローチはすべて、エミュレータを使用するかソースコードを計測することにより、バイナリを直接ターゲットにします。
これらのアプローチでは、多くの場合、単一のIoTデバイス向けに特別に作成する必要があるファジング設定が必要であり、
一般化が困難です。そのために、研究者はプログラム
IoTFuzzer を作成しました。これは、「ファームウェアイメージにアクセスせずにメモリ破壊の脆弱性を
見つける」ことを目的とした自動ファジングフレームワークです。
[iotfuzzer]。 IoTFuzzers
は、ほとんどのIoTデバイスにそれらを制御するモバイルアプリがあり、そのようなアプリにはデバイスとの
通信に使用されるプロトコルに関する情報が含まれているという観察に基づいています。プログラムは次に
プログラム固有のロジックを特定して再利用し、テストケースを変異させてIoTターゲットを効果的にテストします
[iotfuzzer]。
ハーネスは、ファザーが提供する入力を処理するAPI呼び出しのシーケンスを記述します。通常はハーネスを必要としない 通常のアプリケーションとは対照的に、再利用可能な関数を実装するライブラリは、正しいパラメータで、かつ 正しい順序で呼び出す必要があります。これにより、複数の共有関数呼び出しの間の状態を 呼び出すことができます。状態機械を構築せずにライブラリをランダムにファジングしても成功する可能性は低く、 逆に、ライブラリの依存関係が強制されない場合、多くの誤検知のクラッシュが発生します。 これは、例えば、ファザーがバッファサイズチェックをスキップし、結果として誤ったバッファオーバーフローが発生する 場合に起こり得ます。
この論文では、通常のアプリケーションをファジングしますが、ソケットとマルチスレッドの使用に関する ハードウェア依存性のため、それらのためにもハーネスを作成する必要があります。ハーネスは バイナリのコンテキストでロードされ、対象プログラムの内部関数を呼び出すことができます。 これは Code 10 に示されています。
「コーパス」という用語は、有効な入力サンプルまたはテストケースを表し、ファジングプロセス中に新しい入力データを 生成するための基礎となる参照として機能します。Code 10では、これは例えば HTTPリクエストになります。ファザーはこのコーパスを利用して、変異した、または多様化されたテスト ケースを作成し、さまざまな入力シナリオの探索を通じてソフトウェアの脆弱性の検出を 支援します。
ブラックボックスファジングの最も時間のかかる部分は、潜在的な
脆弱な関数をファームウェア内で見つけることです。最初のステップは、
興味深いバイナリを見つけることです。例えば、ネットワーク経由でアクセス可能で、
安全でない関数を使用する、またはスタックカナリアのようなセキュリティ機能が
有効になっていないバイナリです。スタックカナリアはバッファオーバーフロー保護です。前回の論文
([iovt]) では、対象ルーターからファームウェアを抽出する方法と、
潜在的に危険なバイナリを見つける方法についてすでに説明しました。
このために、EMBAツール [emba] が使用されました。EMBAは、
ファームウェア内で見つかったすべてのバイナリを、安全でない関数の数、例えば
strcpy、ネットワークアクセス、そしてスタックカナリアやNXビットのような
セキュリティ保護によってランク付けします。これらはバッファオーバーフローを悪用する際に重要になり、
Code 1 にあります。
コード1: strcpy関数の安全でない使用に関するEMBAの結果。
本論文の目的は、管理者の認証情報を知らなくてもネットワーク経由で悪用可能なメモリ脆弱性を 見つけることであるため、脆弱な関数はネットワーク経由で呼び出し可能であり、提供されたユーザー入力と直接 やり取りする必要がある。しかし、ネットワークとのやり取りがあるからといって、そのバイナリが ネットワーク経由で直接アクセス可能であるとは限らない。どのバイナリが待ち受けているかを調べるには、 [iovt]で 既に確立されたUARTルートシェルを使用することができる。
```txt ~ # netstat -tulpn Active Internet connections (only servers) Proto Recv-Q Send-Q Local Address Foreign Address State PID/Program name tcp 0 0 127.0.0.1:20002 0.0.0.0:* LISTEN 1045/tmpd tcp 0 0 0.0.0.0:1900 0.0.0.0:* LISTEN 1034/upnpd tcp 0 0 0.0.0.0:80 0.0.0.0:* LISTEN 1027/httpd tcp 0 0 0.0.0.0:22 0.0.0.0:* LISTEN 1224/dropbear udp 0 0 0.0.0.0:20002 0.0.0.0:* 1048/tdpd [...] ```コード2: UARTルートシェルを使用してnetstatを実行する
最初に有望と思われるバイナリは wscd です。このバイナリは、(libcmm.so ライブラリを除けば) 最も安全でない strcpy 呼び出しとネットワーク通信を持ち、wscd の場合は UPnP デバイスに接続し、特定のポートで待ち受けることはありません。後述するように、ファジングしやすい関数を持っているため、本書では一般的な手順を説明するための例としてこのバイナリが選ばれました。リバースエンジニアリングの前に、UARTルートシェルを使用して、このバイナリが実行されているかどうか、またどのように起動されたかを確認できます。
コード3: psコマンドを使用して実行中のすべてのプログラムを表示する。
psを使うと、バイナリが実行されていることだけでなく、引数も確認できます。引数は、
潜在的な関数が実際に呼び出されているかどうかを検証するために重要です。これらの引数の意味は、
バイナリを引数なしで呼び出したときに表示されるCLIヘルプから得ることができます。
コード4: バイナリ wscd のオプション。
コード4 に示すように、wscd は "Enabled UPnP Device service" で起動され、有望に見えます。バイナリが実際にルーター上で実行されていることを確認した後、Ghidra を使用してバイナリを分析し、疑わしい関数を検索することができます。ファジングでは、パース関数が特に興味深いです。パース関数は通常複雑であり、解析される入力には、含まれるデータの長さフィールドが頻繁に存在するからです。たとえば、TCPパケットにはペイロードの長さが含まれます。

図1: Ghidra を使用したパース関数の検索。
パース関数のもう1つの利点は、それらがコードの他の部分とやり取りしたり、ネットワーク上でユーザーと対話したりすることがほとんどないことです。そのため、パース関数はバイナリを変更したり他の関数を上書きしたりすることなく、入力を直接渡して呼び出すことができ、ファジングが可能になります。
関数のファジングを開始する前に、その関数が実際にトリガーされるかどうかを確認する必要があります。関数は、ユーザー制御の入力を伴って呼び出された場合にのみ興味深いからです。このため、Ghidra を使用して、対象関数への参照を検索できます。parser_parse 関数の場合、複数の方法があります。プログラムの起動方法がわかっているため、呼び出しを コード5 に示す単一の関数呼び出しツリーに絞り込むことができます。
コード5: 関数 parser_parse の呼び出しツリー
ターゲット関数が見つかったら、次にその関数をファジングするセットアップを作成できるようになります。これについては、 次のパートで説明します。しかしその前に、他の有力な関数を紹介します。
本論文では、疑わしい関数を探すために、候補となる複数のバイナリを手動で分析しました。 以下は、見つかった他の候補ターゲットの簡単な要約です。
バイナリ httpd は管理者用Webインターフェースのバックエンドです。このバイナリはポート80でネットワーク経由で
アクセスできます。httpd 内の興味深い関数の1つが httpd_parser_main 関数です。Ghidraを使用して
パーサー実装をざっと確認すると、いくつかの疑わしいコード部分が特定できました。疑わしい部分の1つは、Content-Type の解析です。以下に、基本的な
HTTPリクエストがあります。```txt
POST / HTTP/1.1\r\n
Content-Type: multipart/form-data; boundary=X;\r\n
Host: example.com\r\n
\r\n
\r\n
DATA\r\n
以下は、ユーザーが提供したhttpリクエストから`Content-Type`を解析する
`httpd_parser_main`関数のスニペットです。
<div id="c6"></div>```c
// user_input_ptr points to
// "Content-Type: multipart/form-data; boundary=X;\r\nHost: example.com\r\n..."
cursor = strstr(user_input_ptr,"multipart/form-data");
if (user_input_ptr == cursor) {
cursor = strstr(user_input_ptr,"boundary=");
user_input_ptr = cursor + 9;
// user_input_ptr points now to "X;\r\nHost: example.com\r\n..."
if (cursor != (char *)0x0) {
do {
while (cursor = user_input_ptr, *cursor == " ") {
user_input_ptr = cursor + 1;
}
user_input_ptr = cursor + 1;
} while (*cursor == "\t");
// cursor points now to "X;\r\nHost: example.com\r\n..."
// strchr returns a pointer to the first occurrence of ";" in the user request.
// If ";" is not found, the function returns a null pointer.
user_input_ptr = strchr(cursor, ";");
if (user_input_ptr != (char *)0x0) {
// The character ";" is replaced by an null byte to terminate the string
*user_input_ptr = "\0";
// cursor points now to "X\0\r\nHost: example.com\r\n..."
}
// DAT_00444050 global array from 0x00444050 to 0x0044414f (255 Bytes)
strcpy(&DAT_00444050, cursor);
// DAT_00444050 contains now "X"
}
}
コード6: 関数 parser_parse のコールツリー
このコードの脆弱性は、関数呼び出し strcpy と、
Content-Type がセミコロンで終わるという仮定にあります。strcpy は次のヌルバイトまでバッファをコピーするため、
コード6 に示すように、ヌルバイトはセミコロンが見つかった場合にのみ追加されます。セミコロンを
削除すると、次のヌルバイトは入力バッファの末尾、例えば HTTP リクエストの末尾になります。
したがって、グローバル変数 DAT_00444050 はオーバーフローし、アドレス
0x0044414f を超えたデータを上書きします。難しいのは、このアドレスの先にある、
上書きされ得る興味深いグローバル変数を見つけることだけでなく、strcpy のためにヌルバイトが使用できないことです。しかし
このようなミスが1つあるなら、他にも見つかる可能性が高いでしょう。
バイナリ tdpd はモバイルアプリで使用され、ローカルネットワーク上で UDP 経由でアクセス可能です。
tdpd は tmpd とほぼ同じ機能を持ちますが、それらの機能はほとんど呼び出されることはありません。メイン
関数は UDP ポート上のメッセージをリッスンするだけで、常に基本情報で応答します。
その基本情報はルーターの名前やモデルなどです。ユーザーが提供する入力とのやり取りはほとんどないため、
その入力はファジングの対象としては興味深くありません。
もう1つの興味深いバイナリのペアは upnpd と ushare です。両方のバイナリは UPnP
メッセージを処理するため、XML をパースする必要があります。 バイナリ内に著作権文字列が見つかることから、
これらのプログラムは TP-Link によって開発されたものではないと推測できます。```sh
$ strings usr/bin/ushare | grep "(C)"
Benjamin Zores (C) 2005-2007, for GeeXboX Team.
両方のバイナリは、オープンソースプロジェクト `pupnp` [\[pupnp\]](https://github.com/pupnp/pupnp/) と同じ機能を持つ共有ライブラリ `libupnp.so` と `libixml.so` を読み込んでいます。本稿の焦点はブラックボックスファジングであるため、これらのバイナリは無視されます。しかし、グレーボックスファジングには可能性があるかもしれません。2021年に `libixml.so` でメモリリークが発見されたためです [\[pupnp-mem-leak\]](https://github.com/pupnp/pupnp/issues/249)。
バイナリ **tmpd** はモバイルアプリのバックエンドです。興味深いのは、ルーターとモバイルアプリが独自のバイナリプロトコルで通信していることです。以下に、クライアントからサーバーへのメッセージを示します。
<div id="c7"></div>```txt
00000000 01 00 05 00 00 08 00 00 00 00 00 17 50 7b 6e fe |............P{n.|
00000010 01 01 02 00 00 00 00 00 |........ |
コード7: モバイルアプリからルーターへのメッセージ。
バイナリプロトコルを理解するため、バイナリtmpdはGhidraを用いてリバースエンジニアリングされた。この情報
により、コード7のメッセージは次のように分解できる:```txt
01 00 05 00 : Version
00 08 00 00 : Size (8 Bytes)
00 00 00 17 : Datatype
50 7b 6e fe : Checksum (CRC32)
01 01 : Options
02 00 : Function id
00 00 00 00 : Function parameters
<p class="text-align: center">コード8: カスタムバイナリプロトコルの分解</p>
これは有望に見えます。なぜなら、そのようなバイナリプロトコルはパースされなければならないからです。しかし、バイナリプロトコルの最も疑わしい部分は、
長さフィールドではなく、関数IDと関数パラメータの使用です。
<figure id="f2">
<p><img src="https://assets.kitploit.com/production/public/readmes/48851/43cb089d7f85d30ba036234ff0fdb29b6a54dd4a3382b2bd68e065f9f6c75369.png" style="width:90.0%" /></p>
<figcaption>
<p style="text-align: center">図2: tmpdからリバースエンジニアリングされた関数。関数IDとそのパラメータを解析する。</p>
</figcaption>
</figure>
[図2](#f2) は、カスタムプロトコルのデコンパイルされたパーサー関数の一部を示しています。16行目で、
関数IDが抽出され、対応する関数が29行目で呼び出されます。疑わしい
動作は、ユーザー制御の入力バッファから何のチェックもなしに抽出されたパラメータで関数が呼び出されることです。ここで、
ジャンプテーブル内の関数を探して、危険な箇所を見つけることができます。危険性は、
[図3](#f3) に示されているように、パラメータがバッファのインデックスとして使用されたり、文字列として解釈されたりする場合に発生します。
手動で100以上の関数をリバースエンジニアリングして検索するのは時間がかかるため、
これを自動的に行うファザーを使用できます。
<figure id="f3">
<p><img src="https://assets.kitploit.com/production/public/readmes/48851/708cf59aee459b840ca6dbcac32948d2b80fb426c3d4cc745157eac4f9b7c28e.png"
style="width:90.0%" /></p>
<figcaption><p style="text-align: center">図3: tmpdのリバースエンジニアリングされた関数。関数ID
とそのパラメータを解析する。</p></figcaption>
</figure>
残念ながら、`tmpd` バイナリはネットワーク経由でローカルにのみ到達可能です。[コード
2](#c2) に示すように。このバイナリに接続するには、アプリはまずSSHでルーターに接続します。モードは
`direct-tcpip` で、パケットをローカルプロセスに転送するだけです。また、SSH接続は
管理者の認証情報によって保護されています。しかし、
[\[iovt\]](https://raw.githubusercontent.com/otsmr/internet-of-vulnerable-things/main/Internet_of_Vulnerable_Things.pdf)
で説明されているように、SSH接続は簡単に侵害される可能性があります。サーバーのホスト鍵がアプリによって
チェックされることは決してないからです。インターネットにルーティングされるすべてのパケットをドロップすることで、管理者を騙してルーターにログインさせ、
その間に中間者攻撃を実行して資格情報を盗むことができます。
## AFL++ と QEMU によるファジング
このセクションでは、以前に特定した関数の1つを対象としたハーネスを開発します。ハーネスの
開発後、最先端のファザーであるAFL++
[\[aflpp\]](https://github.com/AFLplusplus/AFLplusplus) が、対象の関数をファジングするために使用されます。なぜなら、バイナリは
`mipsel` アーキテクチャ用にコンパイルされているため、エミュレータQEMUを使用して
バイナリを実行します。この論文で使用する基本的なファジング設定は、主にAdam Van Prooyenによるブログ記事 "Firmware
Fuzzing 101" [\[b101\]](https://www.mayhem.security/blog/firmware-fuzzing-101) に触発されています。
### ファジング環境
再現可能なファジング環境を簡単に作成するには、Dockerが最適です。私たちは
Dockerfileを作成しました。これは、`mipsel` CPUアーキテクチャ用のクロスコンパイラや、
`gdb-multiarch` など、必要なすべてのツールをインストールします。
さらに、AFLplusplusはQEMUと一緒にダウンロードおよびコンパイルされます。QEMUは、
非計装バイナリをafl-fuzzで実行できるようにするための小幅な調整を加えたバージョンで構築されています。```docker
FROM debian:latest
RUN apt update && apt install -y \
curl \
vim \
gcc-mipsel-linux-gnu \
openssh-server \
qemu-user-static \
gdb-multiarch
# Qemu statics are installed at /usr/bin/qemu-mipsel-static
# Compiling AFL++
RUN apt install -y git make build-essential clang ninja-build pkg-config libglib2.0-dev libpixman-1-dev
RUN git clone https://github.com/AFLplusplus/AFLplusplus /AFLplusplus
WORKDIR /AFLplusplus
RUN make all
WORKDIR /AFLplusplus/qemu_mode
RUN CPU_TARGET=mipsel ./build_qemu_support.sh
RUN echo "#!/bin/bash\n\nsleep infinity" >> /entry.sh
RUN chmod +x /entry.sh
WORKDIR /share
ENTRYPOINT [ "/entry.sh" ]
必要なツールをインストールするDockerfile。
その後、イメージは docker build を使用してビルドできます。```sh
docker build -t fuzz .
イメージがビルドされると、`docker run` で簡単に使用でき、
その後コンテナが起動します。```sh
docker run -d --rm -v $PWD/:/share --name fuzz fuzz
-d オプションを使用すると、コンテナがバックグラウンドで起動します。docker exec を使用すると、複数のシェルを
コンテナ内で起動できます。これは、一方のセッションで QEMU を使用して実行ファイルを起動し、
もう一方のセッションで gdb-multiarch を使用するのに役立ちます。```sh
docker exec -it fuzz /bin/bash
### main関数をオーバーライドする
前のセクションで、強力なファズターゲットが特定されました。問題は、バイナリを実行したときに、
関数呼び出しに到達できないことです。なぜなら `parser_parse` 関数はソケット経由でTCPパケットを受信した場合にのみ
呼び出されるからです。これはパフォーマンスに悪いだけでなく、セットアップも
困難です。このため、ファザーのエントリポイントは通常の main
関数とは異なる場所にあるべきです。このために、内部関数にアクセスできるハーネスを注入できる環境変数 `LD_PRELOAD` を使用できます。
実行時に実行可能ファイルが必要とする共有ライブラリのリンクを担う `ld.so` のマニュアルページが説明するように、`LD_PRELOAD` は
「他の共有オブジェクト内の関数を選択的にオーバーライドするために
[\[man-pages\]](https://www.man7.org/linux/man-pages/man8/ld.so.8.html)」
使用できます。
`__uClibc_main` 関数はこの目的に最適です。この関数をオーバーライドするには、Cファイルを
作成し、その中に同じ名前の関数を含める必要があります。```c
void __uClibc_main(void *main, int argc, char** argv) {
// Harness code, e.g. call the function parser_append
printf("My custom __uClibc_main was called!");
}
Cファイルはその後、mipsel-linux-gnu-gcc を使用してmipselアーキテクチャの共有オブジェクトにクロスコンパイルできます。-fPIC オプションは「位置独立コード」(Position Independent Code)を有効にします。これは、機械コードが絶対アドレスの代わりに相対アドレスを使用することで、特定のアドレスに配置されることに依存しないことを意味します。```txt
$ mipsel-linux-gnu-gcc parser_parse_hook.c -o parser_parse_hook.o -shared -fPIC
新しく作成された共有ライブラリは、その後環境変数`LD_PRELOAD`
をQEMUコマンドに追加することでロードできます。```txt
$ chroot root /qemu-mipsel-static -E LD_PRELOAD=/parser_parse_hook.o /usr/bin/wscd
My custom __uClibc_main was called!
chroot コマンドを使用すると、指定されたコマンドのカレントディレクトリとルートディレクトリを変更できます。
これは、実行可能ファイル wscd がファームウェアの共有ライブラリなどの他のファイルを開くため、役立ちます。
この動作は、QEMU に引数 -strace を追加することで確認できます。```txt
chroot root /qemu-mipsel-static -E LD_PRELOAD=/parser_parse_hook.o -strace /usr/bin/wscd /corpus/notify.txt
38180 mmap(NULL,4096,PROT_READ|PROT_WRITE,MAP_PRIVATE|MAP_ANONYMOUS|0x4000000,-1,0) = 0x7f7e7000
38180 stat("/etc/ld.so.cache",0x7ffffa48) = -1 errno=2 (No such file or directory)
38180 open("/parser_parse_hook.o",O_RDONLY) = 3
38180 fstat(3,0x7ffff920) = 0
38180 close(3) = 0
38180 munmap(0x7f7e6000,4096) = 0
38180 open("/lib/libpthread.so.0",O_RDONLY) = 3
38180 open("/lib/libc.so.0",O_RDONLY) = 3
[...]
見てわかるように、実行可能ファイルはホスト上ではなくファームウェアの `/lib/` フォルダ内にある複数のライブラリを開いています。
### ハーネスの開発とデバッグ
セットアップが完了したら、ハーネスの開発を開始できます。背景セクションで説明したように、ハーネスはファザーと対象関数の間のドライバです。ハーネスは、AFL++ がファイルに保存するファズ入力を読み込みます。ハーネスはファイルパスをパラメータとして、ファジング対象を呼び出します。この場合、それは `parser_append` になります。関数はアドレスを使用して呼び出すことができます。
<div id="c10"></div>```c
void __uClibc_main(void *main, int argc, char** argv)
{
// Verify that a filename is provided
if (argc != 2) exit(1);
// Create function pointer to the fuzz target
int (*parser_request_init)(void *, int) = (void *) 0x00412564;
int (*parser_append)(void *, void *, int) = (void *) 0x00412e98;
// Open the fuzz input file
int fd = open(argv[1], O_RDONLY);
char fuzz_buf[2048 + 1];
int fuzz_buf_len = read(fd, fuzz_buf, sizeof(fuzz_buf) - 1);
if (fuzz_buf_len < 0) exit(1);
fuzz_buf[fuzz_buf_len] = 0;
// Call the target functions
uint8_t parsed_data[220];
parser_request_init(parsed_data, 8);
int status = parser_append(parsed_data, fuzz_buf, fuzz_buf_len);
printf("Response is %d\n", status);
exit(0);
}
コード10: バイナリwscd内のファズターゲット`parser_append`を用いたハーネスコード。
コード10に示すように、関数parser_parseは直接呼び出されるのではなく、関数parser_appendを使用して呼び出されます。この関数が呼び出される前に、初期化関数parser_request_initを呼び出す必要があります。この関数は、parser_parse関数の出力構造体を初期化します。
parser_parseの場合、ハーネスのセットアップは非常に簡単ですが、他のターゲットではhttpd_parser_main関数のような、より高度なハーネスが必要です。たとえば、ターゲットを呼び出す前に、http_init_main関数を呼び出す必要がありますが、これはSIGSEGVで終了します。このセグメンテーションフォールトがどこで発生するかを特定するには、gdbのようなデバッガでコードをデバッグすると便利です。これを行うには、QEMUをオプション-g付きで起動し、指定したポートでgdb-serverを起動できます。```sh
chroot root /qemu-mipsel-static -strace -g 1234 -E LD_PRELOAD="/httpd_parser_main.o" /usr/bin/httpd
corpus/httpd/simple.txt
バイナリは `mipsel` アーキテクチャであるため、`gdb-multiarch` を使用しなければなりません。gdb を
起動した後、以下の init スクリプトは、gdb で `sources <path to script>` を使用してロードできます。```sh
set solib-absolute-prefix /share/root/
file /share/root/usr/bin/httpd
target remote :1234
# break bevor fuzz target is called
# break __uClibc_main
break http_parser_main
display/4i $pc
chrootのため、スクリプトはまず絶対プレフィックスパスを変更し、バイナリが
共有オブジェクトをロードするときにgdbがファイルを見つけられるようにします。次に、対象ファイルを設定します。QEMUの
gdb-serverはファイル転送をサポートしていないため、gdbは代わりにディスクからファイルをロードしようとします。
gdbの設定後、スクリプトはtarget remoteでgdb-serverに接続し、
対象関数の先頭にブレークポイントを作成します。displayを使用すると出力が改善され、ステップ実行時に
次の4行のアセンブリが表示されます。siを使用すると1命令ずつステップできます。
これは、ハーネスがデフォルトのコーパスを使用してセグメンテーション違反を起こした場合に役立ちます。
デフォルトのコーパスは常に機能するはずです。 Code 11に示すように、バイナリは
fprintf関数でセグメンテーション違反を起こします。
Program received signal SIGSEGV, Segmentation fault.
<p style="text-align: center">コード 11: printf でのセグメンテーション違反。</p>
エラーを調査するには、Ghidra を使用して、関数がどのパラメータで呼び出されたかを
確認できます。```c
fprintf(
*(FILE **)(iVar1 + 0x101c),
"HTTP/1.1 %d %s\r\n",
*(undefined4 *)(&DAT_0042ee68 + (uint)(byte)(&DAT_00414570)[statuscode & 0x3f] * 8),
(&PTR_DAT_0042ee6c)[(uint)(byte)(&DAT_00414570)[statuscode & 0x3f] * 2]
);
SIGSEGV はおそらく、最初のパラメータがファイルディスクリプタではなくヌルポインタであることが原因です。ここで iVar1 は単に httpd_parser_main 関数の入力への参照です。これは、ファジング入力が 0x101c の位置にファイルディスクリプタを持たなければならないことを意味します。したがって、入力は次の struct に合わせて調整する必要があります。```c
typedef struct {
int _a; // 4 Bytes
int _b; // 4 Bytes
int socket; // 4 Bytes
int ip; // 4 Bytes
int mac; // 4 Bytes
unsigned char body[0x1008]; // 0x101c - 4*5 = 0x1008 Bytes
FILE * fd_out; // expected to be a valid file descriptor
} HttpMainT;
`fd_out` は有効なファイルディスクリプタポインタであるだけでよいため、簡単に `stdout` に設定できます。
`httpd_parser_main` を再度実行すると、有効な HTTP 出力が生成されるようになります。```c
$ chroot root /qemu-mipsel-static -E LD_PRELOAD=/httpd_parser_main.o \
/usr/bin/httpd /httpd_corpus.txt
bind: No such file or directory
[ dm_shmInit ] 086: shmget to exitst shared memory failed. Could not create shared memory.
rdp_getObj is called with: 4274932gdpr_getSystemGDPREntry Error
gdpr_getNewSystemGDPREntry OK
#Msg: getsockname error
HTTP/1.1 200 OK
Content-Type: text/html; charset=utf-8
Content-Length: 24257
Set-Cookie: JSESSIONID=deleted; Expires=Thu, 01 Jan 1970 00:00:01 GMT; Path=/; HttpOnly
Connection: close
<!DOCTYPE html>
[...]
ハーネスは現在動作しており、AFL++を使用して関数をファジングできます。AFL++の使用方法は 次のセクションで説明します。
背景で述べたように、シードコーパスは有効な入力サンプルを記述し、 ファジングプロセス中に新しい入力データを生成するための基礎的な参照として機能します。
これらの入力は通常、対象プログラムのさまざまな側面を表すように選択されます。シード コーパスはファザーによって使用され、変異または進化したテストケースを生成します。これらはその後、 対象ソフトウェアに対して実行され、バグ、クラッシュ、その他の問題を発見します。このコーパスは、 ファザーをプログラムの関連領域に導き、 脆弱性や予期しない動作を検出する確率を高める上で重要な役割を果たします。初期入力の多様で代表的なセットを提供することで、 シードコーパスはファザーが対象内の異なるパスをより速く探索するのを助け、それによって カバレッジを向上させます。
ネットワークデータを解析する関数に関しては、これらの入力はWiresharkを使用して さまざまなパケットを記録することで作成できます。
関数httpd_parse_mainについては、4つの異なるコーパスが作成されました。それぞれがバイナリ内の
異なるパスを対象としています。一例として、ユーザー名とパスワードを含むログインリクエストがあります。この
コーパスでは、TP-Linkが(弱い)暗号化を使用してパスワードを「保護」しているため、
ハーネスを変更する必要がありました。このため、パスワードはブラウザでAESを使用して暗号化され、
その後バックエンドで復号化されます。 ここでパスワードはブラウザで生成され、RSAを使用して暗号化されます。次に、
暗号化されたデータは署名されます。ファザーは署名を作成したりデータを暗号化したりできないため、いくつかの
関数が上書きされ、現在はデータをbase64からデコードするだけになっています。このため、データはまず
図4に示すデバッガを使用して、ブラウザから平文で抽出されました。

図4: 暗号化前のデータの抽出。
対象では、関数rsa_tmp_decrypt_bypartが上書きされ、
データの復号化から、単にbase64からデコードするだけのロジックに置き換えられました。```c
// Replacing the logic with b64_decode
int rsa_tmp_decrypt_bypart(uint8_t *input, int input_len, uint8_t *output) { // other params just key data
int (*b64_decode)(uint8_t *, int, uint8_t *, int) = (void *) 0x0040bf00;
b64_decode(output, 0x1000, input, input_len);
int * seqnumber = (int *) 0x00444db0;
*seqnumber = 0x3ac28e29-input_len+12;
return 0; // says it was okay
}
<p style="text-align: center">コード12: 関数 rsa_tmp_decrypt_bypart は、データを復号する代わりに、
base64 をデコードするだけになりました。</p>
コーパスを実行中、対象の関数は常に「408
Request Timeout」エラーを含むHTMLドキュメントを返す。GhidraとGDBを使用することで問題を特定できた。エラーは常に
`http_stream_fgets` の関数呼び出し後に発生する。問題のある行は、
改行文字 `\n` のチェックだった。```c
if (((cVar1 == '\n') && (param_3 < pcVar4)) && (pcVar4[-1] == '\r')) {
この条件は、すべての改行の後にキャリッジリターンが続くことを強制します。キャリッジリターンを追加した後、 作成されたすべてのコーパスが機能しました。
前のセクションでは、複数のハーネスを開発し、QEMUを使用して実行しました。このセクションでは、
QEMUはAFL++に置き換えられます。AFL++は、生成されたコーパスをシード入力として受け取り、ターゲット関数をファズします。
「ファジング環境」のセクションでは、GitHubからAFL++をすでに取得し、
その後AFL++が提供するスクリプトを使用してパッチ適用済みのQEMUをビルドするDockerイメージが作成されました。
したがって、AFL++は、-Qのような異なるパラメータを受け取る次のコマンドで起動できるようになりました。
-QはAFL++にパッチ適用済みのQEMUを使用するように指示します。```sh
QEMU_LD_PREFIX=/share/root AFL_PRELOAD=/share/root/httpd_parser_main.o
/AFLplusplus/afl-fuzz -Q
-i /share/root/corpus/httpd/ -o /share/afl-out/httpd/
-- /share/root/usr/bin/httpd @@
<p style="text-align: center">コード13: ハーネスと<code>afl-fuzz</code>を使用してバイナリ<code>httpd</code>をファジングする。</p>
以前とは異なり、`chroot`コマンドは不要になり、`QEMU_LD_PREFIX`変数に置き換えられます。これはQEMUに共有オブジェクトを検索する場所を指示します。また、`LD_PRELOAD`変数はAFL固有のバージョンである`AFL_PRELOAD`に置き換えられます。コマンドの最後の引数は2つの`@`文字です。これらはAFL++によってファジング入力を保持するファイルパスに置き換えられます。起動すると、AFL++は[図5](#f5)に示すターミナルUIを使用して進行状況を表示します。
<div id="f5"></div>
<figure>
<p><img src="https://assets.kitploit.com/production/public/readmes/48851/ac4b12fcf84c2041c9edc2a0871c5bb89b576d976b2eba0a2d0f843e7e708795.png" style="width:95.0%" /></p>
<figcaption><p style="text-align: center">図5: AFL++のステータス画面。</p></figcaption>
</figure>
`AFL++`のステータス画面は、現在のファジングプロセスに関する重要な洞察を提供します。`AFL++`のドキュメントには、ステータス画面で使用される用語の概要がよくまとめられています[\[afl-screen\]](https://aflplus.plus/docs/status_screen/)。以下の環境変数を使用してコーパスをデバッグする場合、UIを無効にでき、`AFL_DEBUG`を使用すると詳細なロギングが有効になり、現在のファザー入力とターゲットプログラムからの`stdout`が表示されます。```sh
export AFL_DEBUG=1 && export AFL_NO_UI=1
unset AFL_DEBUG && unset AFL_NO_UI
図5に示すように、バイナリのファジングにはかなりの時間がかかることがあります。ドキュメントによると、「数日から数週間かかることが予想される」とされ、「一部のジョブは数か月間実行されることもある」とのことです。所要時間を短縮するには、実行速度を毎秒100回以上(100 execs/sec)にする必要があります。例えば、ターゲットのhttpd_main_parserをファジングしたとき、実行速度は当初毎秒約30回でした。速度を改善するため、ターゲットのバイナリ内で、速度低下の原因と思われる疑わしい関数を検索しました。疑わしい関数の1つはrsa_gdpr_generate_keyでした。RSA鍵の生成は遅いことで知られているからです。その関数を上書きしたところ、速度は毎秒600回に向上しました。
ファジングをいつ止めるかを判断するのに役立つ指標の1つがサイクルカウンタです。AFL++は、「ファザーがしばらくの間何もアクションを確認していない」場合にその数値を緑色で強調表示します。これにより、ファザーを停止する判断に役立ちます。
しかし、最も興味深い数値はおそらく「total crashes」でしょう。これは、現在のファジング入力によってプログラムがクラッシュしたことを示しており、おそらくメモリ関連のバグです。これが実際のバグであることを確認するには、gdbを再度使用してバグの位置を特定できます。
ファジングは、セキュリティ脆弱性を見つける最も効果的な方法かもしれません。本稿では、3つの異なる関数をファジングしましたが、いずれも脆弱性は見つかりませんでした。ブラックボックスファジングのセットアップ自体はそれほど複雑で時間がかかるものではありませんが、強力なターゲットを見つけ、動作するハーネスを開発することは、複雑で時間がかかります。ほとんどの場合、ハーネスをデバッグする必要があり、その後、バイナリ内の根底にあるロジックをリバースエンジニアリングする必要がありますが、これもまた長い時間を消費します。