analyzing-shellcode について
PEやELFなどのヘッダーを持たない生のシェルコードを調べる、マルウェア・リバースエンジニアリング分析向けの技能です。デコード、逆アセンブル、エミュレーション、IOC抽出を扱い、隔離環境での実行を重視します。
主な機能
- 01シェルコードのバイト列を整形
- 02アーキテクチャとデコーダーの特徴を特定
- 03生コードの逆アセンブル
- 04実行をエミュレートしてAPIを解決
- 05ペイロード段階と系統の特定
- 06C2などのIOCを抽出
活用シーン
- 01エクスプロイトから抽出したコードの分析
- 02難読化・自己復号ペイロードのデコード
- 03MetasploitやCobalt Strikeの段階の調査
- 04シェルコードからネットワーク指標を抽出
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ trilwu/ secskills --skill analyzing-shellcodeインストール後、Claude Code で/analyzing-shellcodeを入力して読み込まれたか確認します。
詳細
シェルコードの分析
シェルコードは、PE/ELF ヘッダーもインポートテーブルも固定ロードアドレスもない、コンテナを持たないコードです。そのため、必要なものをすべて実行時に見つけなければなりません。この自立性こそが、シェルコードを見分ける手掛かりになります。PEB の走査、API ハッシュ化、位置独立型の GetPC トリックは、ペイロードの機能がわかる前でも読み取れる構造上の指紋です。
使用する場面
- PE/ELF/Mach-O ヘッダーのない、位置独立型コードの生データ
- エクスプロイト、マクロ、ローダー、JS/HTA ドロッパーから切り出されたペイロード
\xでエスケープされた、0xが先頭に付いた、base64 化された、または XOR で難読化されたバイトバッファー- Metasploit のステージャー(reverse_tcp/https)または Cobalt Strike のビーコンステージと疑われるもの
- より大きなバイナリのトリアージ中に見つかったエッグハンター、または GetPC/デコーダースタブ
- ネットワークキャプチャ、RWX メモリ、またはドキュメントストリームから復元されたバイト列
使用しない場面
- サンプルがヘッダー付きの通常の PE/ELF/Mach-O である場合 — ローダーを利用した逆アセンブルのワークフローには
analyzing-binariesを使用してください - より広範なサンプルのトリアージやファミリーの特定が必要な場合 —
analyzing-malwareを使用してください。 サンプル内の生のペイロードステージだけを対象とする場合に、こちらを使用してください - 生のシェルコードではなく、パックされた実行ファイルの場合 — まず
unpacking-protected-binariesを使用して展開してください - メモリダンプからシェルコードを切り出す必要がある場合 —
analyzing-memory-imagesを使用して 領域を特定して抽出してから、こちらに戻ってください - デコード後の詳細な静的解析を行う場合 —
reverse-engineeringはスキルではありません。 継続的な逆アセンブル作業はanalyzing-binariesに戻してください
まずバイト列を取得する
シェルコードはラップされた状態で見つかります。ほかの作業に取りかかる前に、フラットなバイナリに正規化してください。
# \x-escaped or 0x C-array pasted from an exploit → raw bytes
python3 -c "import sys,re; d=open('sc.txt').read(); \
b=bytes(int(x,16) for x in re.findall(r'(?:0x|\\\\x)([0-9a-fA-F]{2})',d)); \
open('sc.bin','wb').write(b); sys.stderr.write(f'{len(b)} bytes\n')"
# base64 buffer
base64 -d payload.b64 > sc.bin
# single-byte XOR brute (find the key that yields printable / low entropy)
python3 -c "import sys; d=open('sc.bin','rb').read(); \
[print(k, bytes(c^k for c in d[:16]).hex()) for k in range(256)]"
次に、逆アセンブルする前に内容を判別します。
file sc.bin && wc -c < sc.bin # size: stagers ~200-800B, stages tens of KB
ent sc.bin 2>/dev/null || binwalk -E sc.bin # high entropy → encoded/self-decoder
ドキュメントやキャプチャに含まれるシェルコードの場合は、まず抽出してください。Office マクロには olevba --deobf を使用します(この手順は analyzing-malware で説明しています)。PCAP からは tshark -r c.pcap --export-objects http,out/ または tcp.stream の追跡で切り出し、以降は上記の手順でバイト列を扱います。
初動トリアージ
xxd sc.bin | head -40
先頭部分は、意味ではなく構造を読み取ります。
- アーキテクチャの推定。 x86 では
55 8B ECのプロローグ、64 A1 30 00/64 8B(fs:[0x30])による PEB アクセス、E8 00000000 5xの call/pop が見られます。x64 では65 48 8B(gs:[0x60])、REX プレフィックス付きの48 ...、RSP のアラインメントを示す48 83 ECが見られます。 ARM/AArch64 は一様な 4 バイトワードで、x86 のプロローグを示す特徴はありません。 - GetPC トリック。
E8 00 00 00 00 58/59/5A/5B/5E(call $+5 の後に pop)や、fnstenv/D9 EE D9 74 24 F4 5Bという FPU トリックで、位置独立性のために EIP を取得します。 これがあれば、コードが自身を再配置しており、その後にデコーダーが続くこともよくあります。 - エッグハンターのタグ。
w00tw00t(77 30 30 74×2)のような 8 バイトのマーカーの繰り返しは、エッグハンターがメモリ内の実際のペイロードを探していることを示します。 - NULL バイトなし/英数字エンコード。
00バイトがない、またはすべて印字可能なバイトである場合は、 エンコーダースタブ(Metasploit のalpha_mixed、shikata_ga_nai)が実際のコードをラップしています。 ここで見ているのはペイロードではなくデコーダーです。
適切なベースアドレスでの静的逆アセンブル
最もよくある間違いは、アーキテクチャを誤るか、命令の途中から逆アセンブルすることです。両方のビット幅を試し、妥当なプロローグが生成されるほうを確認してください。
# objdump on a raw blob — set the arch explicitly, Intel syntax
objdump -D -b binary -m i386 -M intel sc.bin # 32-bit
objdump -D -b binary -m i386:x86-64 -M intel sc.bin # 64-bit
# ndisasm — quick, and -e skips a leading offset if the entry is not at 0
ndisasm -b 32 sc.bin
ndisasm -b 64 sc.bin
# rizin / radare2: assemble/disassemble a blob directly
rasm2 -a x86 -b 32 -d "$(xxd -p sc.bin | tr -d '\n')"
r2 -a x86 -b 32 -m 0x400000 sc.bin # load raw at a base
# e asm.arch=x86 ; e asm.bits=32 ; s 0 ; pd 60 disassemble entry
# pdf @ 0 treat entry as a function
Ghidra/IDA/Cutter で生のシェルコードを読み込むには、raw binary としてインポートし、プロセッサー(x86:LE:32 または x86:LE:64)を設定し、ベースアドレスを選択して、エントリーポイントを定義し、実際の先頭命令から逆アセンブルします。長さプレフィックスや無意味なスレッドが先行する場合は、オフセット 0 から始めないでください。エントリーポイントを定義しないと、自動解析ツールがデータをコードとして逆アセンブルするため、リストがノイズだらけになります。
自己復号型のデコード
エンコードされたシェルコードは、小さなデコーダースタブと、それに続く暗号化された本体で構成されています。本体を静的に読み取ろうとせず、スタブだけを逆アセンブルしてから、実行させてください。
- スタブを見分けます。 実行中のポインター、長さカウンター、キーまたはキースケジュールに対して、
xor/add/sub/rolを行う短いループです。末尾でカウンターがゼロになるまで先頭へ戻り、その後、復号された本体へ処理が進みます。 - shikata_ga_nai。 Metasploit のポリモーフィックなフィードバック XOR デコーダーです。GetPC ブロックの後に、前の平文によって各キーのワードが変更される(加算フィードバック)XOR ループが続きます。ビルドごとに異なる形で再エンコードされるため、バイトシグネチャでは一致しません。パターン照合ではなく、実行してデコードしてください。
- 自己復号させます。 スタブをエミュレートするか、シングルステップ実行し、ループ終了後にメモリをダンプします。そこに書き込まれた領域が実際のステージです。その領域を逆アセンブルしてください。
# scdbg dumps the decoded buffer and unpacks the second stage for x86 Windows
scdbg -f sc.bin -s -1 # -s -1 lifts the step limit (default 2M)
scdbg -f sc.bin -d # dump self-modified/decoded memory
ハッシュによる Windows API の解決
シェルコードにはインポートがないため、ロード済みモジュールのリストをたどり、名前のハッシュを使って API を解決します。これを見分けることが、分析の大部分を占めることもよくあります。
- PEB の走査。
fs:[0x30](x86)またはgs:[0x60](x64)から PEB を読み込み、PEB->Ldr->InMemoryOrderModuleListをたどって、ハードコードされたアドレスを使わずにkernel32.dll(またはntdll.dll)を見つけます。 - エクスポートのハッシュ化。 スタブは各エクスポート名のハッシュを計算します。一般的な方式は、
ror13加算方式(Metasploit の古典的なblock_api)またはdjb2です。そして、4 バイト定数と比較します。この定数は、逆算すれば API 名そのものです。 - ハッシュを名前に対応付けます。 システム DLL のすべてのエクスポートについてハッシュを事前計算し、見つかった定数を照合します。
# Reproduce Metasploit block_api ror13 — the constant is hash(module)+hash(name)
python3 - <<'PY'
def ror13(b):
h=0
for c in b:
h=((h>>13)|(h<<19))&0xffffffff; h=(h+c)&0xffffffff
return h
mod=ror13("KERNEL32.DLL\0".encode('utf-16le')) # module: UTF-16LE, uppercased
for api in ("LoadLibraryA","GetProcAddress","WinExec","VirtualAlloc"):
print(hex((ror13((api+"\0").encode('ascii'))+mod)&0xffffffff), api)
PY
ツール:scdbgはエミュレート中に、引数付きで解決済みの API 呼び出しを表示します。デバッガーでjmp2it/blob_runnerを使うと、リゾルバーでブレークできます。エクスポートテーブルはdllexpまたはrabin2 -E kernel32.dllから取得できます。ハッシュアルゴリズムが独自の場合は、Python ハーネスに移植してエクスポート一覧を総当たりします。
エミュレーションが最短ルート
Windows x86 のシェルコードでは、エミュレーションによって数秒で API を解決し、各ステージをデコードして動作を表示できます。手動でトレースする前に、まずエミュレーションを試してください。
# scdbg — emulates x86 Windows shellcode, reports API calls, C2, and stages
scdbg -f sc.bin # default trace of resolved API calls
scdbg -f sc.bin /findsc # brute-force the correct start offset
scdbg -f sc.bin -foff 0x1a # force entry at a known offset
# speakeasy — emulate x86/x64 shellcode or a raw module
speakeasy -t sc.bin --raw --arch x86 # --raw shellcode, --arch x86|amd64
speakeasy -t sc.bin --raw --arch amd64 -o report.json
独自のロジックや Windows 以外のターゲットには、ハーネスを構築します。
- Unicorn — メモリをマップしてレジスターを設定し、
code/memイベントをフックして実行し、デコードされた領域を読み戻します。デコードをスクリプト化する必要がある場合に最適です。 - Qiling — より高水準のツールです。OS レイヤーをエミュレートするため、ハッシュ化された API の解決や
VirtualAlloc/WinExecが、rootfs とシステムコールのインターセプトを使って動作します。 - デバッガーで実行します。
blob_runner、shellcode2exe、jmp2itでペイロードをロード可能なスタブにラップします。それをx32dbg/x64dbgで開き、EIP/RIP をエントリーポイントに設定して、デコーダーから実行中のステージまでシングルステップで進めます。
ステージとファミリーの特定
- Metasploit のステージャー。
reverse_tcpはソケットを作成し、recvで第 2 ステージを RWX メモリに受信して、そこへジャンプします。reverse_httpsはWinInet(InternetOpen/HttpOpenRequest)を使います。scdbg/speakeasy では接続先アドレスとポートを直接確認できます。 - Cobalt Strike。 ビーコンのステージにはエンコードされた設定が含まれています。デコード後、C2 ホスト、ポート、URI、ユーザーエージェント、スリープ/ジッター、マレアブルプロファイルを解析します。コミュニティ製パーサー(ビーコン設定の抽出ツールなど)で設定ブロックを読み取れます。ビーコンであることを確認したら、
analyzing-malwareに渡してください。 - エンドポイントを抽出します。 エミュレーターのログまたはデコード済み設定から、C2 ホスト/ポートとリクエスト URI を取得します。これらは、すぐにブロックすべき IOC です。
- 既知のデコーダーと比較します。 shikata、
call4_dword_xor、alpha_mixed、block_apiリゾルバーには、それぞれ特徴的な形があります。一致すればツールキットを特定でき、その後に続くバイト列を判断する手掛かりになります。
安全な実行の徹底
エミュレーションは安全ですが、シェルコードを実際に実行することはデトネーションです。analyzing-malwareで求められるのと同じ封じ込めを適用してください。
- 隔離されたVMを使用し、実行前にスナップショットを取得して、実行後に復元します。
- ネットワークを無効にするか、偽のC2(INetSim/FakeNet-NG)を用意して、ステージャーのコールバックが実際のオペレーターではなく、自分のリスナーに解決されるようにします。
- 進行中のインシデントでは、ステージャーを実際のC2に絶対に接続させないでください。コールバックによって、オペレーターに調査中だと知られてしまい、第2ステージのダウンロードは分析内容の漏えいにつながります。
IOCを抽出して引き継ぐ
デコードしながら、永続性のある指標を収集します。C2のホスト/IP、ポート、リクエストURIとユーザーエージェント、ミューテックス名、デコーダーキーまたはハッシュ定数、そしてエッグタグです。analyzing-malwareと同じように優先順位を付けてください。動作や設定情報は、ローテーションするホストより長く有効です。
次に、作業を振り分けます。
- デコーダーまたはリゾルバーのバイト列/構造シグネチャ →
writing-yara-rules(ルールを作成)、その後engineering-detections(デプロイ) - ドロップされた、またはダウンロードされた第2ステージ →
analyzing-malware - 大規模なデコード済みステージの継続的な逆アセンブル →
analyzing-binaries - 調査結果の報告書作成 →
reporting-security-findings
退けるべき合理化
- 「objdumpが意味不明な結果を表示したから、暗号化されている。」 アーキテクチャやオフセットを間違えているか、デコーダースタブをそのまま逆アセンブルしている可能性のほうが高いです。両方のビット幅を試し、まず妥当なプロローグがあるオフセットを見つけてください。
- 「インポートがないから、どのAPIを呼び出しているのかわからない。」 シェルコードはハッシュを使ってAPIを解決します。PEBの走査を見つけ、ハッシュ定数を逆算してください。
- 「実行して動作を確認すればいい。」 隔離され、ネットワークが制御されたハーネスがなければ実行してはいけません。代わりにscdbg/speakeasyでエミュレートしてください。
- 「バイト列シグネチャでこのステージャーを検出できる。」 ポリモーフィックな場合はできません。shikata_ga_naiはビルドのたびに再エンコードします。エンコーダーではなく、デコード済みステージまたは構造的なリゾルバーにシグネチャを付けてください。
- 「文字列に明らかなC2がない。」 アドレスは実行時に組み立てられ、設定情報はエンコードされています。スタブをデコードしてからエミュレートし、connect呼び出しを確認してください。
- 「オフセット0から始まる。」 そうでないこともよくあります。長さのプレフィックス、アラインメント用のスレッド、GetPCブロックがエントリの前にあることがあります。
/findscを使うか、真の最初の命令を読み取ってください。 - 「逆アセンブラーが自動解析したのだから、リストは正しい。」 ヘッダーのないバイナリでは、解析器がエントリを推測し、データをコードとして扱います。実際のエントリとプロセッサーを指定してください。そうしないと、リスト全体がずれてしまいます。
ATT&CKのカバレッジ
secskills-core/ttp-index.jsonから生成されます。このファイルを編集してから、
python3 scripts/sync_attack.py --writeを実行してください。レポートで引用する前に、
IDを現在のATT&CKリリースと照合し、再確認してください。
防御回避 (TA0005)
- T1027 難読化されたファイルまたは情報 —
analyzing-malware、analyzing-binariesも参照 - T1140 ファイルまたは情報の難読化解除/デコード —
analyzing-malware、analyzing-binariesも参照 - T1620 リフレクティブコードローディング —
analyzing-malwareも参照
これらのいずれかに関する検知コンテンツ:engineering-detections。プロアクティブな検索:hunting-threats。侵害後の対応:responding-to-incidents。
参考資料
analyzing-binaries— デコード済みステージの逆アセンブラーワークフローanalyzing-malware— サンプルのトリアージ、封じ込め、ファミリーの特定、ドロップされたステージの分析analyzing-memory-images— まずメモリダンプからシェルコードを切り出すwriting-yara-rules— デコーダーまたはデコード済みステージ向けの永続性のあるシグネチャを作成engineering-detections— デコーダー/リゾルバーのシグネチャをデプロイ済みルールに変換reporting-security-findings— 分析結果を報告書にまとめる- エミュレーション用のscdbg、speakeasy、Unicorn、Qiling
- 生のバイナリの静的逆アセンブル用のradare2/rizin(
rasm2/pd)、Cutter - デバッガーで実行するためのblob_runner / shellcode2exe / jmp2itとx64dbg/x32dbg