who3411のブログ

情報系の勉強やイベントの参加に関するブログ(をしたい)です。

Peach FuzzerでTFTPサーバへのファジングを試してみた話

この記事はBoushitsu Advent Calendar 2020 25日目の記事になります。㍆㌋㌉㌏㌉㌸㌾㌋㌞㌹㌅なので、一人寂しく記事を書くことにしました。今回は個人的な備忘録も兼ねて、初心者でもある程度分かりやすいようにPeach Fuzzerによるファジングについて簡単に説明しようかなぁと思いましたが、何故か文字が大量に生えてきました。これ需要ないのでは…タイトルにあるような本題に関係する部分だけ見たい方は、TFTPの仕様を確認するから見ていただくとちょうど本題の部分だけ読み進められると思います。

はじめに

前々からたまに、Peach Fuzzerを改造してわちゃわちゃやっています。そのときに疑問に思ったこと、問題となったことなどがいくつかあったので、需要があるか全く分かりませんが、折角なので自分の知識の整理も兼ねてブログにします。なお、今回実装したコードなどは、全て以下のリンク先にあります。

github.com

github.com

注意点

今回使用しているのはPeach Fuzzer2系になります。Peach Fuzzer3系関係の記事・ブログなどを閲覧したい方は、本ブログの対象外となります。

また、はてなブログで表示される文字数で27,000文字を超えています。リンクやコードによる部分が大きいとは思いますが、かなりの分量になってしまったので、全部読む場合は少し長いかもしれません。

最後に、今回書いた内容の大半はPeach Fuzzer Community(コミュニティ版のサイト)に書いてありますので、そちらを読むとより良いファジングが行えると思います*1

ファザー

ファザーとは何かというと、ファジングを行うためのソフトウェアです。では、ファジングとは何かというと、プログラムに対して自動的に生成した大量のデータを生成・送信して、送られたプログラム側の応答や挙動を確認することで、脆弱性診断やテストを行う手法です。要するに、データを生成しまくって、生成したデータを入力しまくって、エラーなどが発生しないか確認するのを機械的に行うものになります。この時、生成されたデータをファズ、データを生成する際に一部の値を変化させることを変異と呼ぶことがあります。ただ、ファズはよく聞きますが、変異はMutation(突然変異)をほぼ直訳したものなので、私が変異だと思って勝手に呼んでいる可能性が高いです…

ファジングによるデータ生成・入力回数がどれくらい早いかというと、例えばintel Core i7–8750Hかつ8GB のメモリのUbuntu16.04上で、AFL(American Fuzzy Lop)*2をとあるプログラムに対してファジングを実行したときには、1秒間に約9,100回データを入力していました。恐るべしファジング…

ファザーの種類

ファザーは、手法や対象とするソフトウェアなどによって、様々な種類に分類されることがあります。自分のブログで申し訳ないですが、以下のリンクなどを参照するとある程度わかってもらえると思います。

who3411.hatenablog.com

詳しく分けていくと、リモートの機器で動作するソフトウェアを対象として動作するリモートファザーなど、もう少し細かく分けることが出来ますが、詳しい説明は割愛します。「色んな種類のファザーがあるんだなぁ…」と思ってもらえれば十分です。

Peach Fuzzer

今回は、既存のファザーとしてPeach Fuzzer*3 (以下、Peachと呼びます)を選択して、Peachに対して新しい変異手法と通信方式を用意した上でTFTPサーバをファジングしようと思います。Peachは、ローカル・リモート問わず、ソフトウェア全般に対してファジングを行うことができます。Peachでは、PITファイルと呼ばれる、様々な情報をXML形式で定義するファイルを用意して、そのファイルの内容に沿ってファジングを行います。

community.peachfuzzer.com

2系と3系

Peachには、Peach2系とPeach3系がありますが、注意点にも記載したように、今回は使用するコードの関係上Peach2系を使用します。ユーザから見たPeach2系とPeach3系と主な違いは、PITファイルの構造や定義可能な要素になりますが、Peach2系におけるPITファイルの定義については後述します。コードから違いを見ると、Peach2系までは主にPythonで書かれており、Peach3系からはC#で書かれています。私はC#が書けないので、今回はPython2で書かれているPeach2系を選択しました。改めて、今回選択したPeachリポジトリを、下にリンクとして用意しておきます。

github.com

PITファイルを作成してPeachによるファジングを行ってみる

Peach2系では、主に5つの要素(XMLヘッダの部分を含めると6つの要素)があります。それぞれの要素を、 下に示したようなcurl http://localhost:8000 を実行した場合のHTTPリクエストヘッダをファジングすることを考えながら、各要素を定義してみます。

GET / HTTP/1.1
Host: localhost:8000
User-Agent: curl/7.54.0
Accept: */*

なお、XMLヘッダは例として次のように定義し、全ての要素はXMLヘッダ内に記載します。

<?xml version="1.0" encoding="UTF-8" ?>
<Peach version="1.0"  author="著者名" description="説明">
    <!-- この中に各要素を記載する。 -->
</Peach>

Include

Include要素に指定された別ファイルを読み込みます。自作したプログラムをPeachに組み込まない場合は、ほぼ確実に Peach/Engine/defaults.xmlを読み込むことになります。defaults.xmlで定義されている要素は以下の通りです。

  • PythonPath: 新しくPythonのパスを追加する。
  • Import: 指定されたパッケージやライブラリを動的にインポートする。
  • Mutators(Mutator): インポートされているクラスから、使用する変異手法を定義する。

defaults.xmlをInclude要素で読み込むには、次のような記述をする必要があります。

<Include ns="default" src="file:defaults.xml"/>

DataModel

ファズの元となるデータ構造を定義します。例えば、以下のようなデータ構造の要素を定義できます。

  • String: 文字列
  • Number: 数値
  • Blob: 未定の型やバイナリデータなど
  • Relation: StringやBlobなどの子要素となることで、定義した他データ構造の大きさや数値などを取得できます。
  • Block: いくつかのデータを一まとめにブロックとして定義することが出来ます。(Block要素を対象とした変異があったり、データ構造の定義に便利だったりします)

また、これらのデータ構造に対しては、isStatic属性やmutable属性を定義することで変異させ内容にしたり、Hint要素を子要素に置いて追加してほしい変異手法を指定したりできます。

String要素を用いて、curl http://localhost:8000のHTTPリクエストヘッダーをDataModel要素で定義する場合、例として次のようなDataModel要素が考えられます。

<DataModel name="HttpRequestHeader">
    <!-- 'GET / HTTP/1.1' -->
    <String name="HttpMethod" value="GET"/>
    <String name="Space" value=" " type="char"/>
    <String name="HttpRequestUri" value="/"/>
    <String name="Space" value=" " type="char"/>
    <String name="HttpVersion" value="HTTP/1.1"/>
    <String name="Newline" value="\r\n"/>

    <!-- 'Host: localhost:8000' -->
    <String name="HostHeader" value="Host"/>
    <String name="Colon" value=":" type="char"/>
    <String name="Space" value=" " type="char"/>
    <String name="HostValue" value="localhost:8000"/>
    <String name="Newline" value="\r\n"/>

    <!-- 'User-Agent: curl/7.54.0' -->
    <String name="UserAgentHeader" value="User-Agent"/>
    <String name="Colon" value=":" type="char"/>
    <String name="Space" value=" " type="char"/>
    <String name="UserAgentValue" value="curl/7.54.0"/>
    <String name="Newline" value="\r\n"/>

    <!-- 'Accept: */*' -->
    <String name="AcceptHeader" value="Accept"/>
    <String name="Colon" value=":" type="char"/>
    <String name="Space" value=" " type="char"/>
    <String name="AcceptValue" value="*/*"/>
    <String name="Newline" value="\r\n"/>
    <String name="Newline" value="\r\n"/>
</DataModel>

上の定義は、Block要素などを用いることで、次のようにもう少し綺麗に書くことも出来ます。

<DataModel name="HeaderLine">
    <String name="HeaderName"/>
    <String name="Colon" value=":" type="char"/>
    <String name="Space" value=" " type="char"/>
    <String name="HeaderValue"/>
    <String name="Newline" value="\r\n"/>
</DataModel>

<DataModel name="HttpRequestHeader">
    <!-- 'GET / HTTP/1.1' -->
    <Block name="RequestLine">
        <String name="HttpMethod" value="GET"/>
        <String name="Space" value=" " type="char"/>
        <String name="HttpRequestUri" value="/"/>
        <String name="Space" value=" " type="char"/>
        <String name="HttpVersion" value="HTTP/1.1"/>
        <String name="newline" value="\r\n"/>
    </Block>

    <!-- 'Host: localhost:8000' -->
    <Block name="HostHeader" ref="HeaderLine">
        <String name="HeaderName" value="Host"/>
        <String name="HeaderValue" value="localhost:8000"/>
    </Block

    <!-- 'User-Agent: curl/7.54.0' -->
    <Block name="UserAgentHeader" ref="HeaderLine">
        <String name="HeaderName" value="User-Agent"/>
        <String name="HeaderValue" value="curl/7.54.0"/>
    </Block>

    <!-- 'Accept: */*' -->
    <Block name="AcceptHeader" ref="HeaderLine">
        <String name="HeaderName" value="Accept"/>
        <String name="HeaderValue" value="*/*"/>
    </Block>

    <String name="newline" value="\r\n"/>
</DataModel>

StateModel

どのような手順でファジングを実施するかを定義します。その際、State要素やAction要素といった要素を使用することで、詳細な動作を定義することが出来ます。

  • State: 状態を表します。StateModel要素には、最低でも1つ以上の状態が必要です。2つ以上のStateを使用すると、Action要素のchangeStateという動作を使用して状態遷移することもできます。
  • Action: State要素内での動作を表します。動作に関しては、例えば以下のようなものがあります。
    • input: ファジング対象からのデータを受信します。この時、DataModel要素で定義した受信時のデータ構造が必要となります。
    • output: ファジング対象に対してデータを送信します。この時、DataModel要素で定義した送信時のデータ構造が必要となります。
    • accept: ファジング対象からの接続を受け付けます。データの送受信方法によってはサポートされていない場合もあります。
    • connect: ファジング対象に接続します。ただ、input/outputなどの動作時に、接続されていないと勝手に接続してくれるので、正直あまり使うことはないです。
    • close: ファジング対象との接続を閉じます。

State要素やAction要素を用いて、StateModel要素を定義する場合、例として次のようなStateModel要素が考えられます。なお、初期状態を定義するにはStateModel要素のinitialState属性を使用し、Action要素で使用するDataModel要素を指定する場合にはDataModel要素のref属性を使用します。

<StateModel name="HttpRequest" initialState="SendRequest">
    <State name="SendRequest">
        <Action type="output">
            <DataModel ref="HttpRequestHeader"/>
        </Action>
    </State>
</StateModel>

Test

以下のような要素を使うことで、ファジングを行う場合のテスト環境について定義します。

  • Include/Exclude: 変異の対象から追加/除外する要素を定義します。(ここのInclude要素は、先ほど紹介したIncludeとは異なるので注意してください。)
  • Agent: その他の要素で紹介するAgent要素を、ref属性を用いてこちらに定義します。
  • StateModel: ファジングで使用するStateModel要素を、ref属性を用いてこちらに定義します。
  • Publisher: 通信方式について定義します。リモート機器を対象とするなど、相手に関する情報が必要な場合は、Param要素を子要素として以下のような情報を定義します。なお、使用可能な通信方式については、peach/Publishersなどをご覧ください。
    • host: 接続先のホスト名・IPアドレスを定義します。
    • port: 接続先のポート番号を定義します。
  • Strategy: ファジングの変異戦略(どのような変異を行うか、1回のファズ生成につき何箇所変異を行うか、など)を、Strategy要素の属性やParam要素を用いながら定義します。
  • Mutator: 使用する変異を改めて定義します。ただし、Mutator要素を定義したTest要素を用いてファジングを実行した場合、defaults.xmlなどで予め追加されている変異は使えなくなります。

各要素を用いて、Test要素を定義する場合、例として次のようなTest要素が考えられます。今回はシンプルにしたいので、Include/Exlucde/Agentは用いません。

<Test name="DefaultTest">
    <StateModel ref="HttpRequest"/>
    <Publisher class="tcp.Tcp">
        <Param name="host" value="localhost"/>
        <Param name="port" value="8000"/>
    </Publisher>
    <Strategy class="rand.RandomMutationStrategy" params="MaxFieldsToMutate=4"/>
</Test>

Run

実際にファジングを行うための環境を用意します*4。必要であれば、ログを取得する環境を定義することも出来ます*5。通常動作させるRun要素は、DefaultRunという名前をつける必要があります。Run要素の子要素として定義する要素は以下の通りです。

  • Test: 別で定義したTest要素を、ref属性を用いてこちらに定義します。
  • Logger: ログ環境を定義します。ログ取得のために必要な情報があれば、Param要素を用いて定義する必要があります。

各要素を用いて、Run要素を定義する場合、例として次のようなものが考えられます。今回はシンプルにしたいので、Include/Exlucde/Agentは用いません。

<Run name="DefaultRun" description="Run (HttpRequestHeader)">
    <Test ref="DefaultTest">
    <Logger class="logger.Filesystem">
        <Param name="path" value="Logs/HttpRequestHeader"/>
    </Logger>
</Run>

その他の要素

各要素内で紹介した要素以外にも、以下のような要素があります。全ての要素を知りたい場合は、コミュニティ版のサイトソースコードをご覧ください。

  • Configuration: PITファイル内で使用するマクロとしてMacro要素を定義できます。Configuration要素を上手く使用することで、後述するようなPITファイルの再利用が容易になります。
  • Agent: デバッガの接続、ネットワークトラフィックのキャプチャなどを実行することで、Peach実行時の状況を関し可能なMonitor要素を定義できます。
  • Data: 定義したDataModel要素内の子要素のvalue属性を定義することが出来ます。定義の方法に関しても、値をそのまま書くだけではなく、ファイルから読み込んだりすることも可能です。

最終的なPITファイル

PITファイルに必要となる要素をそれぞれ説明してきましたが、今回使用するPeach2系では、これらの要素を2つのPITファイルに分けて使用することが推奨されています。もちろん、1つのPITファイルのみで全ての記載を行うことも可能ですが、その場合PITファイルの再利用が難しくなります。本ブログでは、推奨されているPITファイルを2つに分ける方法を採用します。また、2つのPITファイルのことを、それぞれFile PITファイル、Target PITファイルと呼んでいきます。それぞれに記載する必要のある要素は以下の通りです。

  • File PITファイル: ファジングを行うための仕様(変異手法の種類、データ構造)を定義します。こちらには、XMLヘッダ、Include要素、DataModel要素の3つを定義します。
  • Target PITファイル: ファジングを行うための環境を定義します。こちらには、XMLヘッダ、StateModel要素、Test要素、Run要素の4つを定義します。

最終的に作成したFile PITファイルとTarget PITファイルは、以下の通りです。

<!-- File PITファイル -->
<?xml version="1.0" encoding="UTF-8" ?>
<Peach version="1.0"  author="who3411" description="curl HttpRequestHeader">
    <!-- Include要素 -->
    <Include ns="default" src="file:defaults.xml"/>

    <!-- DataModel要素 -->
    <DataModel name="HttpRequestHeader">
        <!-- 'GET / HTTP/1.1' -->
        <String name="HttpMethod" value="GET"/>
        <String name="Space" value=" " type="char"/>
        <String name="HttpRequestUri" value="/"/>
        <String name="Space" value=" " type="char"/>
        <String name="HttpVersion" value="HTTP/1.1"/>
        <String name="Newline" value="\r\n"/>

        <!-- 'Host: localhost:8000' -->
        <String name="HostHeader" value="Host"/>
        <String name="Colon" value=":" type="char"/>
        <String name="Space" value=" " type="char"/>
        <String name="HostValue" value="localhost:8000"/>
        <String name="Newline" value="\r\n"/>

        <!-- 'User-Agent: curl/7.54.0' -->
        <String name="UserAgentHeader" value="User-Agent"/>
        <String name="Colon" value=":" type="char"/>
        <String name="Space" value=" " type="char"/>
        <String name="UserAgentValue" value="curl/7.54.0"/>
        <String name="Newline" value="\r\n"/>

        <!-- 'Accept: */*' -->
        <String name="AcceptHeader" value="Accept"/>
        <String name="Colon" value=":" type="char"/>
        <String name="Space" value=" " type="char"/>
        <String name="AcceptValue" value="*/*"/>
        <String name="Newline" value="\r\n"/>
        <String name="Newline" value="\r\n"/>
    </DataModel>
</Peach>
<!-- Target PITファイル -->
<?xml version="1.0" encoding="UTF-8" ?>
<Peach version="1.0"  author="who3411" description="curl HttpRequestHeader">
    <!-- StateModel要素 -->
    <StateModel name="HttpRequest" initialState="SendRequest">
        <State name="SendRequest">
            <Action type="output">
                <DataModel ref="HttpRequestHeader"/>
            </Action>
        </State>
    </StateModel>

    <!-- Test要素 -->
    <Test name="DefaultTest">
        <StateModel ref="HttpRequest"/>
        <Publisher class="tcp.Tcp">
            <Param name="host" value="localhost"/>
            <Param name="port" value="8000"/>
        </Publisher>
        <Strategy class="rand.RandomMutationStrategy" params="MaxFieldsToMutate=4"/>
    </Test>

    <!-- Run要素 -->
    <Run name="DefaultRun" description="Run (HttpRequestHeader)">
        <Test ref="DefaultTest"/>
        <Logger class="logger.Filesystem">
            <Param name="path" value="Logs/HttpRequestHeader"/>
        </Logger>
    </Run>
</Peach>

なお、Pitsディレクトリ内を確認すると、FilesディレクトリとTargetsディレクトリに、それぞれFile PITファイルとTarget PITファイルの例があります。各ディレクトリの例では、Configuration要素やAgent要素に関する例もありますので、自分でPITファイルを書きたい方は、こちらもあわせて確認すると良いです。

実際にファジングしてみる

最終的なPITファイルで作成したFile PITファイルとTarget PITファイルを用いて、Python2のSimpleHTTPServerに対して、実際にファジングを行ってみます。なお、インストールなどに関しては各自README.mdのSetupをご確認ください。

作成したPITファイルはcurl http://localhost:8000を実行した場合のHTTPリクエストヘッダーを生成してファジングを行うので、これに合うようにSimpleHTTPServerを動作させるには、以下のコマンドを使用します。

# Python2の場合
$ python -m SimpleHTTPServer 8000

# Python3の場合
$ python -m http.server 8000

上のコマンドを用いて立ち上げたSimpleHTTPServerに対して、新しくターミナルを立ち上げて、実際にPeachによるファジングを行いながらSimpleHTTPServerとファジングの出力を確認します。Peachによるファジングでは、下のコマンドのようにFile PITファイルとTarget PITファイルを、それぞれpitオプションとtargetオプションで指定します。なお、それぞれのファイルは、冒頭で紹介したリポジトリにも含まれています。

$ python peach.py -pit Pits/Files/httprequestheader.xml -target Pits/Targets/simplehttpserver.xml

コマンドを入力すると、凄い勢いで文字列が流れていくと思います。また、SimpleHTTPServerを立ち上げていた方を見ると、そちらも凄い勢いで文字列が流れていると思います。実際に大量のリクエストを送信していることが実感できたでしょうか。

TFTPの仕様を確認する

前置きがはてなブログで表示される文字数で15,000文字弱とかなり長くなってしまいましたが、ようやく本題のTFTPサーバのファジングに移ります。ただし、TFTPの仕様がどのようなものであるか分からないと、上手くファジングを行うことが出来ません。そのため、まずは今回の対象とするTFTPについて確認していきます。なお、TFTPに限らず、Peachを用いて新しい対象のファジングを行う場合、大抵は以下のような流れになると思います。

  • 対象の仕様(データ構造、シーケンスなど)を理解し、ファジングを行うための方針を策定する
  • (必要であれば)追加で変異手法(Mutator)や通信形式(Publisher)を作成する
    • この場合、PITファイルには新しく作成したMutatorやPublisherも対応できるようにする
  • 対象に合ったPITファイルを作成する

TFTPとは

TFTPを雑に説明すると、認証機能の存在しないUDPで動作するFTPのようなものです。UDP上で通信が行われるので、TCPコネクションの確立などのオーバーヘッドを必要とせず、簡単な再送処理を行うことも出来ます。

TFTPでは、データのアップロードとダウンロードにおいて、それぞれ以下の図のような手順でデータを転送します。図のうち、until~の部分はデータ転送が終了するまでループします。

f:id:who3411:20201224113321p:plain
ファイルアップロード時の手順

f:id:who3411:20201224113316p:plain
ファイルダウンロード時の手順

TFTPのコマンド

今回ファジングを行う際のTFTPでは、RFC1350で定義されている5つのコマンドが存在します。

  1. RRQ: データのダウンロード要求
  2. WRQ: データのアップロード要求
  3. DATA: 実際に送信されるデータ
  4. ACK: 要求に対する承認
  5. ERROR: エラーの内容

5つのコマンドのうち、はじめにTFTPクライアントからTFTPサーバへ送信するコマンドは、RRQかWRQの2つになります。そのため、今回はRRQとWRQの2つをファジングできるようにPITファイルを定義しようと思います。

RRQとWRQのコマンドに関して、それぞれのフォーマットは以下の図のようになっています。下の図のうち、データの区切りのためにある文字を除くと、

  • Opcode: コマンドの番号(0x0001ならRRQ、0x0002ならWRQ)
  • Filename: データ転送を行うファイル名(RRQならTFTPサーバ上にあるファイル名、WRQならTFTPサーバ上にアップロードするファイル名)
  • MODE: 転送モード(octet、netascii、mailの計3つあるが、今回使用するソースコードの関係上、あまり気にしなくていい。多分octetが一番使われている)

の3つのパラメータがあることがあります。なので、RRQとWRQをファジングできるようにするには、opcodeだけを変えることで達成できます。また、変異を行うデータに関しては、Opcode、Filename、MODEの3つで大丈夫そうです。

f:id:who3411:20201225075342p:plain
RRQ/WRQのパケットフォーマット

なお、RRQとWRQ以外のデータ構造の詳細に関して知りたい方は、RFC1350のAppendixにあるTFTP Formatsを参照してください。

ファジングを行う上でのTFTPの問題点

今回のファジングを行う上で問題になりそうなのは、以下の3点になります。

  1. ポート番号の変更: TFTPでは、UDP69番ポートへのリクエストの後、別のポートからデータ転送を行います。この場合、今まで説明してきた方法ではポート番号を変更することができないので、対処する必要があります。
  2. データ転送時のファイル分割: TFTPでは、データ転送を行う際、512バイトごとに区切ったデータを転送し、都度データが転送されたことを伝えるレスポンスが返ってきます。この時、512バイトごとに区切ったデータを転送する、という変異手法はないので、こちらも対処する必要があります。
  3. ファイル名をランダムに返す変異がない: Peachには、ファイルに記載された文字列を、上から1列ずつ返す変異手法が存在します。これを利用することで、ファイル名を返す変異を作成することは可能です。ただし、ディレクトリ内にあるファイル名を返す変異や、ファイルに記載された文字列をランダムに返す変異などは存在しないので、そういった変異を行う際には自作する必要があります。

今回は、これらの問題点をMutatorとPublisherを追加することで解決しようと思います。

TFTPサーバに対応したファジングを作成する

TFTPの仕様を確認するで確認した情報などを元に、TFTPサーバのファジングの仕様と、実施に必要となる修正内容をそれぞれ下に記載します。

  • 仕様
  • 修正内容
    • 通信の途中で発生するポート番号の変更、データ転送時のファイル分割など、PITファイルで上手く対応できない箇所を、Publisherの追加によって解決する。
    • ファイル名をランダムに返す変異が存在しないため、そのような変異を作成する。

まずは、修正内容に従って、Publisherを変異手法をそれぞれ追加します。

Publisherの追加

ファジングを行う上でのTFTPの問題点の問題点1,2に対処するため、TFTP通信を行ってくれるPublisherを作成します。

Publisherを追加する場合、一から自作したり、既に存在するライブラリなどを使用したり、いくつか手法はあるかと思いますが、今回は既に存在するライブラリのtftpy*6を使おうと思います。

github.com

tftpyについて

tftpyは、2,000行程度と小規模なPython製のライブラリで、RFC1350を含めたTFTP通信をサポートするライブラリです。実装されているTFTPクライアントでは、download(RRQ)とupload(WRQ)のメソッドを使うことができます。

それぞれのメソッドは、次のように使うことが出来ます。

import tftpy

client = tftpy.TftpClient('tftp_server_addr', 69)
client.download('remote_filename', 'local_filename')
client.upload('local_filename', 'remote_filename')
tftpyをファジングに使えるようにする

downloadとuploadだけでは、ファズを上手く送信することが出来ないので、新しくrawdataというメソッドを追加しました。rawdataでは、RRQ/WRQコマンドで変更可能なパラメータであるopcode、filename、modeを全て受け取り、それ以降の通信を全て行ってくれます*7

また、tftpyでは、エラーが発生するとディスクリプタを閉じないままプログラムが終了してしまいます。リクエストを大量に送信し、リクエストごとにエラーの発生する確率の高いファジングでは、ディスクリプタを閉じないままリクエストを送信し続けることで、そのうちディスクリプタが枯渇してしまいます。このような問題を解決するため、エラー発生に関わらず必ずディスクリプタを閉じるようにしました。

更に、TFTP通信を行うと、どうしてもファイルが増えてしまうため、ファイルをダウンロードできないように、ダウンロード時の書き込み先ファイルを/dev/nullにしました*8

最終的に、次のような形式でrawdataメソッドを使えるようにしました。

import tftpy

client = tftpy.TftpClient('tftp_server_addr', 69)
# client.rawdata(opcode, filename, mode)
client.rawdata("\x00\x01", "filename", "octet") #download
client.rawdata("\x00\x02", "filename", "octet") #upload

実装の詳細は以下のリンクのtftpy/TftpClient.pyとtftpy/TftpContexts.pyをご覧ください。

github.com

新しいPublisherとしてTftpを追加する

修正したtftpyを使って、新しいPublisherのTftpを追加します。Publisherを追加する際の方法は、コミュニティ版サイトのCreate a Custom Publisherにも説明があります。今回は、少しでも楽をしたい上手くtftpyに追加したrawdataメソッドを使いたいので、PublisherのsendWithNodeメソッドを使います。

sendWithNodeメソッドでは、DataModel要素のデータ構造をそのまま受け取ることが出来ます。つまり、「DataModel要素内のname属性の値がfilenameのString要素の値(ファズ済み)を取得する」といったことができるようになります。標準で用意されているPublisherではほとんど使われていませんが、自分でカスタマイズしてPublisherを作成する際には非常に便利なメソッドになります*9

実際のsendWithNodeメソッドの実装は以下の通りです。param_listに格納した名前の要素の値(opcode, filename, mode)を取り出して、その値をtftpyのrawdataメソッドに突っ込むだけという簡単な実装になっています。ちなみに、実際にファジングを行う際には例外処理を細かく指定するのですが、今回は簡略化しています。

def sendWithNode(self, data, dataNode):
    '''
    Publish some data

    @type   data: string
    @param  data: Data to publish
    @type   dataNode: DataElement
    @param  dataNode: Root of data model that produced data
    '''
    params = dict()
    param_list = ["opcode", "filename", "mode"]
    for child in dataNode.getAllChildDataElements():
        if child.name in param_list:
            if child.get_Value() is None:
                params[child.name] = ""
            else:
                params[child.name] = child.get_Value()
    
    try:
        self.client.rawdata(params["opcode"], params["filename"], params["mode"])
    except KeyboardInterrupt:
        raise
    except:
        pass

後は初期設定などを簡単に指定して新しいPublisherを作成しました。詳細な実装は以下のリンク先をご覧ください。

peach/tftp.py at support_tftp · who3411/peach · GitHub

Mutatorの追加

ファジングを行う上でのTFTPの問題点の問題点3に対処するため、ファイル名をランダムに返す変異を作成します。

Peachには、予めファイルに記載された文字列を上から1列ずつ返すDictionaryというデータ生成手法が存在するので、今回はそれを使います。Dictionaryに使用するファイルには、予めTFTP通信で使用するファイル名を記載しておく必要はありますが、この方が楽々に仕上げられる汎用性の高いデータ生成手法になります。

Dictionaryの改良

Dictionaryでは、ファズを生成するたびに1列ずつファイルを読んで返しているだけなので、今回は次のような手順でランダムに返すように改良しました。

  1. 最初にファズを生成する際に、ファイル内のデータを全て読み、1列を1要素としたリストにしておく。
  2. ファズの生成時、リストにしたデータからランダムに1つだけ返すようにする。

上のような手順になるようにDictionaryを継承して、新しくRandomDictというデータ生成手法を追加しました。詳細な実装は以下のリンク先をご覧ください。

peach/randomdict.py at support_tftp · who3411/peach · GitHub

新しいMutatorとしてDownloadFileListMutatorとUploadFileListMutatorを追加する

Dictionaryを改良したRandomDictを使って、新しいMutatorを作成します。今回はダウンロード時のコマンドとアップロード時のコマンドで、それぞれ別のファイルパスを指定したいので、ダウンロード用の変異手法としてDownloadFileListMutator、アップロード用の変異手法としてUploadFileListMutatorをそれぞれ用意します。

幸い、Peachの変異手法には、Dictionaryを使用した変異手法である_SimpleGeneratorMutatorがあるので、これを利用してFileListMutatorを作成します。FileListMutatorは、_SimpleGenetorMutatoと比較して、データ生成手法をRandomDictにして、ランダムな変異を追加しただけの簡単な変異手法です。

次に、FileListMutatorを元に、DonwloadFileListMutatorとUploadFileListMutatorを作成します。それぞれの変異手法を使用する際に参照されるファイルは、Tftp/downloadFileList.txtとTftp/uploadFileList.txtに置くように設定しました。なお、今回のようなあまり汎用的ではないMutatorは、変異可能な要素を可能な限り抑えたいので、MutatorのsupportedDataElementメソッドを使って、変異可能な要素を限定します。

限定する条件は以下の3つです。

  1. String要素である。
  2. 変異可能な要素である。
  3. 子要素にHint要素があり、特定の属性を有している。

例えば、DownloadFileListMutatorのsupportedDataElementメソッドは、次のようになっています。

def supportedDataElement(node):
    if isinstance(node, String) and node.isMutable:
        for child in node.hints:
            if child.name == 'type' and child.value == 'downloadfilelist':
                return True
    return False

これでMutatorの追加も終わりです。詳細な実装は以下のリンク先をご覧ください。

peach/filelist.py at support_tftp · who3411/peach · GitHub

PITファイルの作成

作成したPublisherとMutatorを使って、新しくTFTPサーバをファジングするためのPITファイルを作成します。と言っても、全て解説すると長くなってしまうので、Include要素とDataModel要素について説明します。

Include要素における注意点

HTTPヘッダの方ではIncludeの対象をdefaults.xmlとしていましたが、今回は独自で作成したプログラムがあるので、それらを含める必要があります。そのため、defaults.xmlを今回にあわせて修正したdefaults-tftp.xmlを用意し、それをIncludeの対象としました。

注意点として、Include要素の後で新しくMutators要素を定義したり、Test要素内でMutator要素を定義したりすると、前に定義したMutators要素が上書きされてしまいます。そのため、Mutators要素を定義する際には、必ず自分が定義したいMutatorを全て定義しておくようにしてください。

DataModel要素とtftpyの連携

新しいPublisherとしてTftpを追加するで説明したように、今回はDataModel要素内で、name属性がopcode、filename、modeである要素をそれぞれ1つずつ用意する必要があります。今回はRRQコマンド、WRQコマンド、その他のコマンドの3つのDataModel要素を定義しましたが、どれもopcode、filename、modeを含んでいます。

最終的なTFTP用PITファイル

最終的に作成したPITファイルは、

  • Tftp/defaults-tftp.xml: defaults.xmlをTFTPサーバのファジング用に修正したもの
  • Pits/Files/tftp.xml: File PITファイル
  • Pits/Target/tftpy.xml: Target PITファイル

の3つになります。それぞれのリンク先を下に置いておきます。

実際にファジングしてみる

最終的なTFTP用PITファイルで作成したFile PITファイルとTarget PITファイルを用いて、tftpyのTftpServerに対して実際にファジングを行ってみます。なお、TFTPServerを立ち上げる際のPythonコードは、以下のとおりです。

import tftpy
server = tftpy.TftpServer("tftp server root directory") #TFTPサーバにとってのルートディレクトリを引数としてください。
server.listen("0.0.0.0", 23456) #well-known portは通常Permission Deniedが出るので、適当に23456としました。

上のコードを用いて立ち上げたTftpServerに対して、新しくターミナルを立ち上げて、実際にPeachによるファジングを行いながらファジングの出力を確認します。Peachによるファジングでは、下のコマンドのようにFile PITファイルとTarget PITファイルを、それぞれpitオプションとtargetオプションで指定します。なお、上のPythonコードと違う方法でサーバを立ち上げたりした場合は、Pits/Target/tftpy.xmlのPublisherのhostとportを自身の環境に合うように変更してください。

$ python peach.py -pit Pits/Files/tftp.xml -target Pits/Targets/tftpy.xml

ファジング側の出力を見てみると、DownloadFileListMutatorやUploadFileLitMutatorが動いていること、実際にデータのやり取りが行われいてることなどがわかります。使用している変異手法が見えるようにしたターミナルの出力を一部以下に貼り付けます。

[Peach.root] Performing mutation.
[Peach.root] DownloadFileListMutator => filename
[Peach.root] UnicodeBadUtf8Mutator => mode
[Peach.root] BitFlipperMutator => N/A
[Peach.root] Mutation finished.
[Peach.tftpy.TftpContext] Sending tftp rawdata request to localhost
[Peach.tftpy.TftpContext]     filename -> bluetooth.py
[Peach.tftpy.TftpContext]     options -> {}

...

[Peach.root] Performing mutation.
[Peach.root] UploadFileListMutator => filename
[Peach.root] Mutation finished.
[Peach.tftpy.TftpContext] Sending tftp rawdata request to localhost
[Peach.tftpy.TftpContext]     filename -> Peach/Publishers/wifi.py
[Peach.tftpy.TftpContext]     options -> {}

また、上ではTftpServerを対象としましたが、他にもRaspberry Pi上で動作するatftpdに対してもファジングを行ってみました。ファジング実施時のパケットをキャプチャしたところ、下の図のようになりました。実際にデータをやり取りできていることや、エラーが発生していることなどがわかると思います。

f:id:who3411:20201225074555p:plain
ファジングのキャプチャ結果(一部抜粋)

今後の課題

今回はtftpyを使ってTFTPサーバへのファジングを行いましたが、TFTPサーバへのファジングをもっと厳密に行うには、

  • はじめのリクエスト以外の部分もファズを生成できるようにする
  • TFTPサーバからのレスポンスも詳しくチェックする
  • はじめのリクエストにおいてRRQ/WRQ以外のフォーマットのファズも生成・送受信できるようにする

などの課題が残っています。今回はTFTPサーバへのファジングを簡単に行うために、色々と制限をつけてある程度簡単にファジングを行えるようにしましたが、この辺りの課題を詰めていくともっと面白いかもしれません。

おわりに

果たしてここまで上から飛ばさずに読んでくれた方はいるのだろうか…私なら絶対に飛ばす…

Peachについて、おそらくユーザが一番使用することになるPITファイルを簡単に紹介して、本題であるTFTPサーバへのファジングを試してみました。本当はもっとサーバを追い込んで問題を発生させたり、問題が発生したと思われる入力を絞り込んで検証していったりするのですが、そこまで書くと文字数が大変なことになってしまうし期日中に記事が出来なくなってしまうので、ここまでとさせていただきます。

私がPeachを本格的に触ろうと思った頃は、PITファイルが意味不明で分からず、カスタマイズしようにもコミュニティ版のサイトがなぜか403エラーで読めなかったので地獄でした。今はサイトも復活してますし、ブログや記事も充実してきているので、Peachもやりやすくなったのではないかなぁ、と感じています。ただ、今回のようなMutatorやPublisherをカスタマイズする際の注意点、実際にカスタマイズを行った上でファジング、sendWithNodeメソッドを使ったデータの送信など、個人的にやりたい部分はまだまだ手探りで進めないと厳しい部分があると感じます。もちろん初心者でも上から読めばある程度は理解できるように記事を作ったつもりですが、今回の記事の内容が上手く行かずに困っている人にも見ていただければ幸いです。

最後に、Advent Calendar最終日にこんな長文の怪文書を作成してしまい大変申し訳ありませんでした。もし来年以降参加する機会があれば、もう少し文字数を減らすように努力しようと思います。ありがとうございました。

おまけ

ここまでPeachを使ってTFTPサーバのファジングを行いましたが、もちろんこれらを行っていく上で、PITファイルの作成で問題が発生したり、痒いところに手が届かなかったり、そういった状況があると思います。個人的にそのような状況に陥った際のごり押し解決方法をいくつか紹介したいと思います。

なお、おまけで紹介する解決方法は大体がPeachのコードを直接いじるものになっているので、正直非推奨な方法が多いです。ご注意ください。

特定の要素だけ変異手法を制限したい

defaults.xmlを使って様々な変異手法を入れたはいいものの、例えば「name=filenameのString要素は、今回実装したDonwloadFileListMutatorのみを変異手法として追加したい」みたいな状況がたまにあります。こういうとき、PITファイルを使って上手く解決することが出来ないのですが、コードを書き換えて無理やり解決することが出来ます。

Peachでは、初めに要素ごとに実行可能な変異手法を調べ、その結果を元に要素ごとに変異手法を用意します。この処理は変異手法をどのような流れで行うかを制御する変異戦略によって行われます。変異戦略関係のコードはPeach/MutateStrategies以下にあります。今回は、変異戦略のうち、ランダム戦略であるrand.pyにおいて、最初に書いた状況に合致するようなコードを書いてみます。

rand.pyを読んでみると、270行目から279行目において、全ての要素で実行可能な変異を調査・用意していることが分かります。例えば、279行目を以下のようなコードに書き換えることで、特定の要素だけ変異手法を制限することが出来ます。

mut = m(Engine.context, node)
if node.name.find("filename") >= 0:
    if mut.name.find("DownloadFileListMutator") < 0:
        continue
mutators.append(mut)

コードの修正前後で、name=filenameのString要素に用意される変異手法は、次のように変化します。

# 修正前
DataTreeRemoveMutator
DataTreeDuplicateMutator
DataTreeSwapNearNodesMutator
StringMutator
FilenameMutator
StringCaseMutator
UnicodeBomMutator
UnicodeBadUtf8Mutator
UnicodeUtf8ThreeCharMutator
UnicodeUtf8ThreeCharMutator
UnicodeUtf8ThreeCharMutator
UnicodeUtf8ThreeCharMutator
UnicodeUtf8ThreeCharMutator
UnicodeUtf8ThreeCharMutator
UnicodeUtf8ThreeCharMutator
UnicodeUtf8ThreeCharMutator
UnicodeUtf8ThreeCharMutator
UnicodeUtf8ThreeCharMutator
UnicodeUtf8ThreeCharMutator
UnicodeUtf8ThreeCharMutator
UnicodeUtf8ThreeCharMutator
UnicodeUtf8ThreeCharMutator
UnicodeUtf8ThreeCharMutator
UnicodeUtf8ThreeCharMutator
DownloadFileListMutator

# 修正後
DownloadFileListMutator

これが何に役立つかというと、例えばDownloadFileListMutatorは実際に動いているのか確認する場合に結構役立ちます。

DataModel要素を消されないようにしたい

Peachでは、DataModel要素がデータ構造のトップレベルの要素として認識されている一方、DataModel要素自体も変異の対象となります。そのため、BlobやDataTree関係の変異手法が用意され、場合によっては空文字列になったりします。すると、Publisherのsendメソッドで空文字列が送られてくることになることがあります。

DataModel内の各要素はisStatic属性やmutable属性を使うことで、各属性がtrueの要素は変異を行わないようにできますのですが、DataModel要素自身はその属性を付与することが出来ません。そこで、コードの方からDataModel要素に対する変異手法を用意できないように修正することで解決することが出来ます。

こちらも変異戦略関係の問題なので、rand.pyのコードを変更します。rand.pyの268行目を見ると、nodes.append(dataModel)というコードがあると思います。これがDataModel要素を実行可能な変異手法の調査・用意を行うための準備なので、この行を削除します。このままでも解決しますが、一応変異しないノードを追加しておくnonMutableNodesにdataModelを追加すると、より丁寧になります。

*1:私がPeach Fuzzerをいじってた最初の頃、何故かコミュニティ版のサイトが403が出て閲覧できなくなっていたので、もしかしたら見れなくなっているかもしれません

*2:最近だとIPAからAFLの実践資料が公開されています。詳しくはこちら

*3:こちらもIPAの実践資料があります。詳しくはこちら

*4:脚注3の資料では、Peach2系のRun要素について、「詳細情報: なし」としていますが、実際はhttps://community.peachfuzzer.com/v2/Run.htmlにあります。

*5:脚注3の資料では、Peach2~3系において、Test要素にログの出力先などを定義できるとありますが、実際は、Peach2系ではRun要素でログの出力先を定義する必要があります。今回使用するコードを確認すると、Run要素の解析を行うHandleRunメソッド内にログ関係の定義を行うLogger要素の解析部分があります。

*6:ドキュメントはhttp://tftpy.sourceforge.net/sphinx/index.htmlにあります。

*7:tftpyは、TFTP通信に存在する3つの通信モードのうち、octetモードにしか対応していないので、実は別のモードを試せなかったりします。今後の課題です…

*8:ひどい時はディレクトリ内に1000ファイル溜まったりして苦しんでました…

*9:sendメソッドでTFTP通信を何とかしようとしたら、一切加工/注釈付けされていないファズを無理やりパースして使わないといけないので、非常に面倒です。というかデータ生成時点で、ほとんどのデータの変異を禁止しておくなど、何らかの工夫をしておかないと確実に上手くパースできません。