【上級プロフェッショナル向け】マジックバイトとGetExtensionNameの乖離を突く:拡張子偽装検知セキュリティ・アーキテクチャ
開発現場において、ファイル操作の自動化やアップロード機能の監視は避けて通れない領域だ。
特に、FSO(FileSystemObject)の `GetExtensionName` メソッドを過信し、「`.txt` だから安全」「`.jpg` だから画像だ」と判定しているシステムは、セキュリティの観点から見れば「穴だらけのザル」と言わざるを得ない。
攻撃者は平然と実行ファイル(`.exe`)やスクリプト(`.vbs`, `.bat`)の拡張子を `.jpg` や `.txt` に書き換えてシステムの網を潜り抜けようとする。
今回は、VBScriptの限界領域に踏み込み、ファイルの中身そのものである「マジックバイト(ファイルシグネチャ)」を読み解き、宣言された拡張子との乖離を完全自動で暴き出す、エンタープライズ向けの堅牢な監査スクリプトを授けよう。
—
なぜ `GetExtensionName` だけでは破綻するのか?
多くのプログラマは、ファイルの正当性を検証する際に以下のようなコードを書く。
‘ 【アンチパターン】これでは拡張子偽装を防げない
Dim fso, ext
Set fso = CreateObject(“Scripting.FileSystemObject”)
ext = LCase(fso.GetExtensionName(“malware.exe.jpg”))
If ext = “jpg” Then
‘ 画像として処理してしまう(危険!)
End If
このアプローチの致命的な欠陥は、「OSのファイル名文字列しか見ていない」点にある。FSOは文字列を操作しているに過ぎず、ファイルの実体がバイナリレベルで何であるかを気にも留めない。
真にセキュアなシステムを構築するには、ファイルストリームの先頭数バイト(マジックバイト)を直接バイナリとして読み込み、「このバイナリパターンを持つファイルは、この拡張子でなければならない」という厳格なホワイトリスト検証を行わなければならない。
—
バイナリを制する:ADODB.Streamによるマジックバイト抽出
VBScript単体では、直接ファイルポインタを精密に制御してバイナリを読む機能が弱い。そこで、COMオブジェクトである `ADODB.Stream` を駆使する。
`ADODB.Stream` をバイナリモード(`Type = 1`)で開くことにより、ファイルの先頭から任意の長さをバイト配列として安全かつ高速に切り出すことが可能になる。
オブジェクトのライフサイクルを考慮し、メモリリークを防ぐための適切な開放(`Set obj = Nothing`)も忘れてはならない。
—
プロダクションコード:拡張子偽装監査エンジン
以下のコードは、指定したフォルダ内のファイルを再帰的あるいは一括でスキャンし、マジックバイトと拡張子の不一致(偽装)を検知してログ出力、あるいは隔離・削除を行うエンタープライズクオリティのVBScriptである。
実務でそのままコピー&ペーストして、セキュリティ監査タスクに組み込むことが可能だ。
‘ ==============================================================================
‘ スクリプト名: AuditExtensionSpoofing.vbs
‘ 概要: マジックバイトと拡張子の乖離を検出し、不審なファイルを特定する
‘ ターゲット: 上級プロフェッショナル向けエンタープライズ防御スクリプト
‘ ==============================================================================
Option Explicit
‘ 監査対象のディレクトリパス
Const TARGET_DIRECTORY = “C:\Uploads”
‘ 監査結果のログ出力先
Const LOG_FILE_PATH = “C:\Logs\SecurityAudit.log”
Sub Main()
Dim fso, targetFolder
Set fso = CreateObject(“Scripting.FileSystemObject”)
If Not fso.FolderExists(TARGET_DIRECTORY) Then
WScript.Echo “エラー: 監査対象ディレクトリが存在しません: ” & TARGET_DIRECTORY
Exit Sub
End If
WriteLog “=== 拡張子偽装監査セッション開始 ===”
Set targetFolder = fso.GetFolder(TARGET_DIRECTORY)
AuditFilesRecursive targetFolder, fso
WriteLog “=== 拡張子偽装監査セッション終了 ===”
Set fso = Nothing
WScript.Echo “監査が完了しました。ログを確認してください。”
End Sub
‘ フォルダ内を再帰的に走査
Sub AuditFilesRecursive(folder, fso)
Dim file, subFolder
‘ 現在のフォルダ内のファイルを監査
For Each file in folder.Files
AuditSingleFile file, fso
Next
‘ サブフォルダを再帰
For Each subFolder in folder.SubFolders
AuditFilesRecursive subFolder, fso
Next
End Sub
‘ 個別ファイルの監査ロジック
Sub AuditSingleFile(file, fso)
Dim ext, magicBytesHex, expectedType, actualType
ext = LCase(fso.GetExtensionName(file.Path))
magicBytesHex = GetHeaderBytesHex(file.Path, 4) ‘ 先頭4バイトを取得
If magicBytesHex = “” Then
Exit Sub ‘ 読み込みエラーまたは空ファイルはスキップ
End If
‘ マジックバイトから実際のファイル形式を判定
actualType = IdentifyFileType(magicBytesHex)
‘ 拡張子と実体の整合性チェック
If Not IsExtensionValid(ext, actualType) Then
WriteLog “[警告] 拡張子偽装の疑い検知! パス: ” & file.Path & ” (宣言拡張子: .” & ext & ” / 実体推定: ” & actualType & ” [Magic: ” & magicBytesHex & “])”
‘ 【実務上のアクション】必要に応じてここでファイルの移動やリネームを実施
‘ 隔離フォルダへの移動例:
‘ fso.MoveFile file.Path, “C:\Quarantine\” & file.Name
End If
End Sub
‘ ADODB.Streamを使用してファイルの先頭バイトを16進数文字列で取得
Function GetHeaderBytesHex(filePath, length)
On Error Resume Next
Dim stream, bytes, i, hexStr, byteVal
Set stream = CreateObject(“ADODB.Stream”)
stream.Type = 1 ‘ 1 = adTypeBinary
stream.Open
stream.LoadFromFile filePath
If Err.Number <> 0 Then
GetHeaderBytesHex = “”
Exit Function
End If
bytes = stream.Read(length)
stream.Close
Set stream = Nothing
‘ バイト配列を16進数文字列に変換
hexStr = “”
For i = 0 To LenB(bytes) – 1
byteVal = AscB(MidB(bytes, i + 1, 1))
If byteVal < 16 Then hexStr = hexStr & "0"
hexStr = hexStr & Hex(byteVal)
Next
GetHeaderBytesHex = hexStr
On Error GoTo 0
End Function
' マジックバイトの定義(代表的な脅威・主要フォーマット)
Function IdentifyFileType(hexHeader)
' 先頭部分の一致で判定
If Left(hexHeader, 4) = "4D5A" Then
IdentifyFileType = "EXE/DLL" ' MZヘッダ (PEファイル)
ElseIf Left(hexHeader, 8) = "89504E47" Then
IdentifyFileType = "PNG"
ElseIf Left(hexHeader, 4) = "FFD8" Then
IdentifyFileType = "JPEG"
ElseIf Left(hexHeader, 6) = "47494638" Then
IdentifyFileType = "GIF"
ElseIf Left(hexHeader, 8) = "25504446" Then
IdentifyFileType = "PDF"
ElseIf Left(hexHeader, 8) = "504B0304" Then
IdentifyFileType = "ZIP/Office(OpenXML)"
Else
IdentifyFileType = "UNKNOWN"
End Function
End Function
' 拡張子と実体形式のホワイトリスト検証
Function IsExtensionValid(ext, actualType)
Select Case actualType
Case "EXE/DLL"
' .exe, .dll, .scr 以外は偽装とみなす
If ext = "exe" Or ext = "dll" Or ext = "scr" Then IsExtensionValid = True Else IsExtensionValid = False
Case "PNG"
If ext = "png" Then IsExtensionValid = True Else IsExtensionValid = False
Case "JPEG"
If ext = "jpg" Or ext = "jpeg" Then IsExtensionValid = True Else IsExtensionValid = False
Case "GIF"
If ext = "gif" Then IsExtensionValid = True Else IsExtensionValid = False
Case "PDF"
If ext = "pdf" Then IsExtensionValid = True Else IsExtensionValid = False
Case "ZIP/Office(OpenXML)"
' xlsx, docx, pptx, zip 等が該当
If ext = "zip" Or ext = "xlsx" Or ext = "docx" Or ext = "pptx" Or ext = "jar" Then IsExtensionValid = True Else IsExtensionValid = False
Case Else
' 未知の形式は今回は厳しく弾くか、あるいはスルーするかを業務要件で決定
' ここでは未知のバイナリは拡張子がtxt等の場合は偽装と判定させるためTrueを返す、等のチューニングが可能
IsExtensionValid = True
End Select
End Function
' ログ出力関数
Sub WriteLog(message)
Dim fso, logFile
Set fso = CreateObject("Scripting.FileSystemObject")
' ログフォルダの自動作成
Dim logDir: logDir = fso.GetParentFolderName(LOG_FILE_PATH)
If Not fso.FolderExists(logDir) Then
fso.CreateFolder logDir
End If
Set logFile = fso.OpenTextFile(LOG_FILE_PATH, 8, True) ' 8 = ForAppending
logFile.WriteLine "[" & Now & "] " & message
logFile.Close
Set fso = Nothing
End Sub
' 処理の実行
Main()
---
現場で活きるプロの知見:実装上の注意点とパフォーマンス
1. 巨大ファイルの読込コストに配慮せよ
今回のコードでは `stream.LoadFromFile` を用いているため、数GBある映像ファイルなどを対象にするとメモリとCPUに高負荷がかかる。監査対象は「アップロードされたドキュメントや画像等」に限定し、巨大なアーカイブや動画ファイルはマジックバイトの読込サイズ(バイト数)を最小限に絞るか、監査対象外とするスループット設計が不可欠だ。
2. 偽装検知後の「フェイルセーフ」アクション
ログを吐くだけで満足してはならない。プロダクション環境では、検知した瞬間にファイルを即座に `C:\Quarantine`(隔離領域)へ移動させ、Webサーバーや業務アプリから絶対にアクセスできない権限設定を施す自動化パイプラインと直結させるべきである。
3. テキストファイル(`.txt`, `.csv`)の扱い
テキストファイルには明確な「固定のマジックバイト」が存在しない場合が多い(UTF-8のBOM `EF BB BF` などはあるが、プレーンなANSIテキストにはない)。そのため、テキストを標的にした偽装(例:中身がVBScriptコードなのに `.txt` と称している場合)を検知するには、バイナリ判定だけでなく、ファイル内に特定のキーワード(`CreateObject`, `WScript`, `Execute` 等)が含まれていないかをテキスト解析するサニタイズ処理を組み合わせるのが、真のプロフェッショナルなアプローチとなる。
甘いチェックでシステムを危機に晒すのは今日で終わりだ。
この知見を実装に組み込み、鉄壁のファイルガバナンスを確立してほしい。
