持ち込み媒体を展開する前に検疫する
目標
持ち込み媒体の圧縮ファイルを展開する前に、一覧だけを読んで、規模・危険な項目・圧縮比・形式の不一致を判定する検疫器を作り、抽出フィルターで項目ごとに選別して隔離ディレクトリに展開したあと、根拠のハッシュが付いた持ち込み判定書を書きます。
なぜ重要なのか
エアギャップ環境に入ってくるものの大半は、人が持ってきた媒体で、その中には圧縮ファイルが1つ入っています。中を見ようとして展開する、その1行が、すでにファイルシステムに触れる行為です。絶対パス、上位パスへの脱出、外を指すリンク、特殊ファイル、圧縮爆弾は、すべて展開した瞬間に効果が出ます。 tarヘッダーには、名前・サイズ・権限・項目の種類・リンク先が書かれていて、展開しなくても、この5つをすべて見られます。そのため、順序を変えます。先に読み、判定し、条件をかけて展開します。 判定書には根拠が付いていなければなりません。どのアーカイブのどのハッシュを、どのレポートで読み、そのレポートからどの理由が出たかがつながっていてはじめて、協力会社が何を直せばよいかがわかり、審査者は同じ判定をもう一度出せます。 採点ツールは、文言を信用しません。アーカイブを直接開いて、出てくるべき値を自分で計算し、実行契約どおりに検疫器を再実行して、その出力まで突き合わせます。
ステップ
- 生成スクリプトで媒体2つを作り、受付台帳/root/media/intake.tsvに、連番・受領時刻・提出者の役割・外側のハッシュ・サイズを書きます。
- /root/media/inspect.pyを作成し、MEDIA-2609-017を展開せずに一覧だけを読んで、項目数・展開後の合計サイズ・最大の項目を/root/media/report/MEDIA-2609-017.jsonに書きます。
- inspect.pyにdanger分類を入れます。absolute・traversal・escaping_link・special・exec_bitの5つの欄を置き、該当のない欄も空のリストで残します。
- inspect.pyにratioとratio_verdictを入れます。展開後の合計サイズを圧縮ファイルのサイズで割った値が100を超えれば、bombです。
- inspect.pyにmismatchを入れます。拡張子が示す形式と、先頭バイトが示す形式が違う項目を、name・declared・actualで書きます。
- inspect.pyに
--extract-toを入れて、dataフィルターで項目ごとに判定し、/root/media/quarantine/MEDIA-2609-017に展開して、extractedとrejectedをレポートに書きます。 - /root/media/decide.pyで、レポートを持ち込み判定書/root/media/report/verdict-MEDIA-2609-017.jsonに移します。media・archive_sha256・report_sha256・decision・reasons・accepted_entries・rejected_entriesを書きます。
- 同じ検疫器と同じ判定ルールをMEDIA-2609-018に回して、/root/media/report/MEDIA-2609-018.jsonと/root/media/report/verdict-MEDIA-2609-018.jsonを作り、2つの媒体を並べた/root/media/report/compare.tsvを書きます。
参考
- 実行契約:
python3 /root/media/inspect.py --archive <매체> --out <보고서> [--extract-to <디렉터리>](プレースホルダーは順に、媒体、レポート、ディレクトリです)。判定ルールはpython3 /root/media/decide.py --report <보고서> --out <판정서>(プレースホルダーはレポートと判定書です)です。採点ツールが、この契約どおりに直接再実行します。 - 媒体は
/root/media/in/MEDIA-2609-017.tar.gzと/root/media/in/MEDIA-2609-018.tar.gz、隔離ディレクトリは/root/media/quarantine/<일련번호>(プレースホルダーは連番です)です。 - 判定ルール(このラボの仮定): absolute・traversal・escaping_link・special・bombのどれか1つでもあればreject、理由がexec_bitかmismatchだけならquarantine、何もなければacceptです。reasonsは、理由名をソートした一覧です。
- 一覧は
tar -tvfで、Pythonはtarfile.open(...).getmembers()で読みます。項目ごとの判定は、tarfile.data_filter(member, dest)が出します。 - 先頭バイトは、
od -An -tx1 -N8かPythonで見ます。NULが混ざった項目があるので、grepやheadでは正しく見られません。 - よくある間違いは、
extractall()にフィルターだけをかけて最初の拒否で止まること、レポートを作り直して判定書のハッシュをそのままにすること、dangerの空の欄をまるごと外してしまうことです。 - このラボはエアギャップ環境なので、
zip・unzip・xz・gpgがありません。tarとgzipだけを使います。
媒体を開く前にまず受付台帳
生成スクリプトで媒体2つを作り、/root/media/intake.tsvにヘッダー1行と媒体2行を書いてください。欄はserial・received_at・submitter_role・sha256・bytesで、タブで区切ります。
外から取ってくるサンプルがないので、python3で自分で作ります。シードとmtimeが固定されているので、何回回しても同じ媒体ができます。ハッシュは、アーカイブを展開せず、ファイルそのままで計算します。この値が、あとで「そのとき受け取ったそのファイルか」を問い直す唯一の根拠です。提出者は、人の名前ではなく役割で書きます。
展開せずに一覧だけを読んで規模を測る
/root/media/inspect.pyが、MEDIA-2609-017を展開せずにヘッダーだけを読んで、archive_sha256・compressed_bytes・entries・total_uncompressed_bytes・largest_entry_bytes・largest_entry_nameを/root/media/report/MEDIA-2609-017.jsonに書くようにしてください。
tarfile.open(パス, 'r:gz').getmembers()が、ヘッダーの一覧をそのまま返してくれます。各項目のsizeはヘッダーに書かれた値なので、内容を読まなくて済みます。archive_sha256はアーカイブファイル自体のハッシュで、compressed_bytesはそのファイルのサイズです。
危険な項目を5つに分ける
inspect.pyがレポートにdangerを入れるようにしてください。absolute・traversal・escaping_link・special・exec_bitの5つの欄に項目名の一覧を書き、該当する項目がない欄も空のリストにします。
名前がスラッシュで始まれば、絶対パスです。上位パスへの脱出は、文字列に'..'が入っているかではなく、階層単位で深さを数えて判定します。リンクは、issymとislnkに分けて見てください。シンボリックリンクは、自分が置かれた場所が基準で、ハードリンクは、アーカイブのルートが基準です。特殊ファイルは、ischr・isblk・isfifoです。
圧縮比で爆弾を見分ける
inspect.pyがレポートにratioとratio_verdictを入れるようにしてください。ratioは、展開後の合計サイズを圧縮ファイルのサイズで割って、小数第2位で丸めた値で、100を超えればratio_verdictはbomb、そうでなければokです。
2つの値はどちらもすでにレポートにあります。展開して測らないでください。爆弾を判定するために爆弾を展開することになります。しきい値は定数として外に出しておけば、次の人が組織の基準に合わせて変えられます。
拡張子と実際の形式を照らし合わせる
inspect.pyがレポートにmismatchを入れるようにしてください。通常ファイルのうち、拡張子が示す形式と先頭バイトが示す形式が違う項目を、name・declared・actualで書きます。
extractfileは、ディスクに展開せずにストリームだけを開きます。先頭の512バイトで十分です。gzipは1f 8b、ELFは7f 45 4c 46で始まります。先頭にNULがあれば、テキストではありません。拡張子がわからない項目は、判定せずに飛ばしてください。わからないものを間違いと書くと、一覧が使い物にならなくなります。
抽出フィルターで項目ごとに選別して展開する
inspect.pyに--extract-toを入れて、dataフィルターで項目ごとに判定し、/root/media/quarantine/MEDIA-2609-017に展開するようにしてください。レポートに、extractedの数とrejectedの一覧(name・reason)を書きます。
extractallにfilter='data'だけをかけると、最初の拒否で例外が上がって止まります。項目ごとにtarfile.data_filter(member, dest)を直接呼んで、FilterErrorを受け取れば、拒否した項目と理由が一度に集まります。reasonは、例外クラスの名前をそのまま使ってください。拒否されなかった項目だけを集めて、最後に一度に展開します。
根拠のハッシュが付いた持ち込み判定書を書く
/root/media/decide.pyでレポートを読んで、/root/media/report/verdict-MEDIA-2609-017.jsonを作ってください。media・archive_sha256・report_sha256・decision・reasons・accepted_entries・rejected_entriesを書きます。
判定ルールは、ラボの案内の「## 参考」にあります。reasonsは、dangerで空でない欄の名前に、bombとmismatchを加えて、ソートした一覧です。report_sha256は、sha256sum /root/media/report/MEDIA-2609-017.jsonの値です。レポートを作り直したら、この値も書き直さなければなりません。止める項目を直して通すステップではありません。
2つ目の媒体で判定が分かれるかを見る
同じ検疫器と同じ判定ルールをMEDIA-2609-018に回してレポートと判定書を作り、2つの媒体を並べた/root/media/report/compare.tsvを書いてください。ヘッダーはmedia・entries・ratio・ratio_verdict・danger・mismatch・rejected・decisionです。
新しく書かず、前に作った2つのスクリプトをそのまま使います。dangerの欄は、5つの区分の項目数をすべて足した値です。手順が値に反応せず、いつも同じ答えを出すなら、それは検疫ではなく判子です。2行のdecisionが分かれるかを見てください。