はじめに
こんにちは。KDIXサイバーセキュリティコースB3のGotoです。
皆さんは普段、ファイルの種類を何で判断していますか?おそらくファイル名の末尾にある「拡張子(.jpg, .pdfなど)」を見ていると思います。しかし、ご存知の通り拡張子はただのラベルに過ぎず、簡単に書き換えることができてしまいます。
では、拡張子が偽装されたとき、コンピュータはどうやって本当のファイル形式を見抜いているのでしょうか?
今回は、ファイルの「中身」であるバイナリデータの先頭にある 「マジックナンバー(ファイルシグネチャ)」 に着目し、Pythonを使って簡易的なファイル形式判定ツールを作ってみます。
1. バイナリエディタで正体を覗く
論より証拠、まずは実際のファイルの中身を見てみましょう。
手元の適当なPNG画像を、バイナリエディタ(今回はVSCode)で開いてみます。
せっかくなので今回はこの可愛い猫ちゃんのイラスト(著作権フリー)を使ってみます。
これをcat.pngとします。可愛い。
VSCodeで開いて、ファイルを開くアプリケーションの選択...を選択
16進数エディターで開きます。
Hex Editor という拡張機能を入れる必要があります。
あら不思議、可愛い猫ちゃんが訳の分からない数字の羅列になってしまいました。
まあそれはさておき、ここで注目すべきは先頭の数バイトです。
89 50 4E 47 ... という並びが見えますね。実はこれ、デコードされたテキストを見てもらうとわかりますが PNG と書いてあるんですよ。
これがマジックナンバーというやつです。OSやアプリケーションは、拡張子ではなくこの固有のバイト列を見て「これはPNG画像ですね」と判断しています。
他にも以下のようなマジックナンバーがあります。
- JPEG:
FF D8 FF ... - PDF:
25 50 44 46 ... - ZIP:
50 4B 03 04 ...
2. Pythonで判定ツールを作ってみる
仕組みが分かったので、拡張子に頼らずファイル形式を当てるスクリプトを超簡単に書いてみます。
ポイントは、ファイルをテキストとしてではなくバイナリモード(rb) で読み込むことです。
import sys
def detect_file_type(filepath):
try:
# バイナリ読み込みモード('rb')で開く
with open(filepath, 'rb') as f:
# 先頭の8バイトだけ読み取る
header = f.read(8)
# マジックナンバーと比較(16進数)
if header.startswith(b'\x89\x50\x4E\x47\x0D\x0A\x1A\x0A'):
return "これは【PNG画像】です"
elif header.startswith(b'\xFF\xD8\xFF'):
return "これは【JPEG画像】です"
elif header.startswith(b'\x25\x50\x44\x46'):
return "これは【PDFファイル】です"
elif header.startswith(b'\x50\x4B\x03\x04'):
return "これは【ZIPアーカイブ】です"
else:
return "不明なファイル形式です"
except FileNotFoundError:
return "ファイルが見つかりません"
if __name__ == '__main__':
# コマンドライン引数からファイルパスを取得(sys.argv[1])
target_file = sys.argv[1]
# 判定実行
result = detect_file_type(target_file)
print(f"判定結果: {result}")
extension.py↑
3. 拡張子を偽装して実験
では、このスクリプトの実力とやらを試していきます。
テストとして、「中身はPNG画像(可愛い猫ちゃん)だけど、ファイル名を fake_cat.txt に書き換えたファイル」 を用意しました。Macのデスクトップ上ではテキストファイルとして表示されています。
ですがこれを先ほどのスクリプトに読み込ませてみると...?
見事に 「PNG画像です」 と見抜くことができました!さすがです。
拡張子が .txt であっても、プログラムはバイナリの先頭(89 50...)を正しく評価していることが分かります。
せっかくなので他にもやってみます。
皆さんMicrosoftのWordやExcel、Powerpointなどはご存知ですよね。実はそれらのファイル形式がzipファイルと同じなのって知ってますか?百聞は一見に如かずということでこちらも早速見ていきましょう。
今回はこちらのWordファイル(実験あああ)を使ってみます。拡張子はもちろん.docxですね。
ではこちらも先ほどと同じようにスクリプトに読み込ませてみると...?
見事に 「ZIPアーカイブ」 と出ました。実はこれ、「Office Open XML」 という規格で、実体はXMLや画像をZIPで一つにまとめたものだからです。 実際にバイナリエディタで見ても、WordファイルのマジックナンバーはZIPと同じ 50 4B 03 04(PK...)で始まっています。「拡張子はただの飾り」どころか、「拡張子が違っても中身は同じ技術」ということもあるのですね。
4. セキュリティの観点から
最後に、サイバーセキュリティコースの学生として、セキュリティの観点からもこの話に触れてみたいと思います。
Webアプリケーションの「ファイルアップロード機能」において、サーバー側が拡張子だけでチェックを行っていると危険です。攻撃者が 「中身は悪意あるプログラム(ウイルスやWebシェル)」 なのに 「拡張子は".jpg"」 に偽装したファイルをアップロードした場合、サーバーがそれを画像として保存してしまうと、任意のコードを実行される脆弱性(CWE-434)に繋がります。
安全なシステムを作るためには、ユーザーがつけた拡張子を信用せず、必ずサーバー側でマジックナンバー等を確認して、中身の整合性をチェックする必要があるということですね。
おわりに
普段何気なく扱っているファイルも、バイナリレベルで見ると面白い仕組みが見えてきます。
皆さんもぜひ、身の回りのファイルがどんなマジックナンバーを持っているか覗いてみてください。最後まで見ていただきありがとうございました!






