analyzing-rust-binaries について
マルウェア、CLI、コンパイル済みサービスなどの Rust バイナリを調べるリバースエンジニアリング向けスキルです。panic 文字列、シンボル、依存関係のパスを使って独自コードを特定し、逆アセンブル時の Rust 特有の構造を解説します。
主な機能
- 01panic 文字列からソースパスを特定
- 02Cargo 依存関係とビルド情報の確認
- 03旧形式と v0 シンボルのデマングル
- 04プロジェクトコードと標準ライブラリの識別
- 05Result、Option、単相化のパターン認識
- 06unsafe、FFI、デシリアライズのリスク確認
活用シーン
- 01シンボルを削除した Rust 実行ファイルの分析
- 02Rust マルウェアやインプラントの調査
- 03コンパイル済みバイナリのセキュリティレビュー
- 04サービスのネットワーク処理やエラーパスの追跡
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ trilwu/ secskills --skill analyzing-rust-binariesインストール後、Claude Code で/analyzing-rust-binariesを入力して読み込まれたか確認します。
詳細
Rust バイナリの分析
Rust バイナリはサイズが大きく、静的リンクされ、インライン化されたジェネリックコードで満ちていますが、多くの人が想像する以上に多くの情報が残されています。パニック箇所にはソースファイルのパスと行番号が埋め込まれ、シンボルにはクレートやモジュールの構造が含まれ、標準ライブラリのフォーマット処理機構はすぐに見分けられます。重要なのは、どの部分が独自のもので、どの部分が core、alloc、ベンダー提供のクレートからなる90%なのかを把握することです。
使用する場面
stringsにrustc version、core::panicking、/rustc/<hash>/library/が表示される- シンボルが
_ZN...17h<hex>E(旧方式)または_R(v0マングリング)で始まる - ストリップ済みバイナリのサイズが3~30 MBで、動的インポートが最小限である
- Rust マルウェア、Rust CLI、またはコンパイル済み Rust サービスを分析する
使用しない場面
- Go バイナリ —
analyzing-go-binariesを使用してください。シンボルの復元方法がまったく異なります - .NET または Unity — 該当するスキルを使用してください
- マルウェアの疑いがあり、隔離前の場合 — 先に
analyzing-malwareを使用してください - ソースが入手可能な場合 — Rust の
unsafeチェックリストがあるauditing-code-for-vulnerabilitiesを使用してください
確認とフィンガープリントの取得
strings -n 8 target | rg -m8 'rustc version|/rustc/[0-9a-f]{40}|core::panicking|cargo/registry'
逆アセンブルする前に、次の2つの手がかりから大部分を把握できます。
パニック文字列からソースツリーがわかります。 Rust は、すべての panic!、unwrap()、境界チェックについて、ファイルパスと行番号を埋め込みます。これにより、クレートの配置、開発者のディレクトリ構造、そして多くの場合はプロジェクト名もわかります。
strings -n 10 target | rg 'src/[a-z_/]+\.rs' | sort -u | head -40
# → src/main.rs, src/crypto/aes.rs, /home/dev/projects/implant/src/c2.rs
レジストリパスから依存関係がわかります。 コンパイルに含まれたベンダー提供のクレートは、パニック箇所に ~/.cargo/registry/src/.../<crate>-<version>/ のパスを残します。これは実質的に依存関係の一覧です。
strings target | rg -o 'cargo/registry/src/[^/]+/([a-z0-9_-]+-[0-9.]+)' -r '$1' | sort -u
モジュールの配置と依存関係の一覧を組み合わせれば、通常は命令をひとつも読む前にバイナリの動作を把握できます。
マングリングの解除
Rust には2種類のマングリング方式があり、ツールはその両方に対応する必要があります。
# Legacy: _ZN4core3fmt5Write9write_fmt17h<16 hex>E
nm -C target | head -30
rustfilt < symbols.txt
# v0 (RFC 2603): starts with _R, encodes generics and paths properly
rustfilt # handles both
# Ghidra 11+ and IDA 8+ have v0 demanglers; enable Rust demangling in the
# analyzer options rather than reading raw symbols
# Filter to your target's code. Everything under core::, alloc::, std::, and
# a registry crate name is stock.
nm -C target 2>/dev/null | rg -v '^.* (core|alloc|std|hashbrown|serde|tokio)::' | head -40
シンボルが完全にストリップされている場合は、パニック文字列を手がかりにします。各文字列は、それを含む関数の近くにあるため、パニックメッセージと既知のソースパスを照合すれば、周辺の関数名がわかります。
##逆アセンブラーで Rust を読む
混乱の大半は、次の5つのパターンで説明できます。
単相化。 ジェネリック関数は具体的な型ごとにコンパイルされるため、Vec<u8>::push と Vec<String>::push は、似た本体を持つ別々の関数になります。重複する関数があることを想定し、ほとんど同じ関数が2つあっても、コピー&ペーストされたものだと思い込まないでください。
Result と Option の戻り値。 これらは値渡しで返される列挙型で、多くの場合、判別値とペイロードの2つのレジスターに格納されます。Result<T, E> を返す関数は構造体を返しているように見えます。呼び出し直後に最初のレジスターを検査する分岐が、エラーチェックです。これを認識できるとエラーパスが読みやすくなります。そして、エラーパスにはバグが潜んでいることがよくあります。
あらゆる場所にある境界チェック。 スライスのインデックス参照ごとに、比較と、パニック箇所への条件分岐が生成されます。これらは逆アセンブル結果を見づらくするため、読み飛ばせるようになってください。境界チェックがない場合は重要な手がかりであり、unsafe または get_unchecked が使われていることを意味します。
トレイトオブジェクトのディスパッチ。 dyn Trait の呼び出しは vtable を経由します。つまり、データと vtable のポインターのペアを使い、固定された vtable オフセットへの間接呼び出しを行います。C++ の場合と同じように、vtable を復元すれば具体的な型を特定できます。
文字列の処理。 Rust の String と &str は、Go とまったく同じく、終端文字のないポインターと長さの組み合わせです。strings の出力では隣接するリテラルがつながって表示されます。ポインターの読み込みの隣にある長さ定数を探してください。
重要なコードを見つける
# Panic paths point at your code; use them as anchors
strings -t x target | rg 'src/' | head -30 # offsets included
# Cross-reference an offset in the disassembler to land in the owning function
# Crypto and network crates are recognizable by their panic paths
strings target | rg -i 'ring-|rustls|openssl|aes-gcm|chacha20|reqwest|hyper|tokio'
サービスやインプラントを分析する場合は、次の手順が最短です。まず非同期ランタイムを特定します(ほとんどの場合 tokio です)。次に、パニックパスからリクエストハンドラーまたは C2 ループを見つけ、その周辺を読み進めます。
Rust固有のセキュリティレビュー
挙動ではなくバグを探している場合:
unsafeブロックにはメモリ安全性のバグが潜んでいます。バイナリでは、インデックス参照時に境界チェックがない箇所や、シンボルが残っている場合はfrom_raw_parts/transmuteの呼び出し箇所を探してください。- 整数オーバーフローはデバッグビルドではチェックされますが、リリースでは黙ってラップアラウンドします。リリースバイナリの算術演算にはオーバーフロー時のトラップがありません。
- **攻撃者が制御できる入力に対する
unwrap()/expect()**はリモートパニックにつながります。サービス拒否であり、ネットワークサービスでは実際の脆弱性として報告すべきものです。 - FFI 境界。 ポインターを受け取る
extern "C"関数では、Rust の保証が及ばなくなります。 - デシリアライズ。
serdeを使って、コンストラクターでは不変条件が適用されるものの、Deserializeでは適用されない型にデシリアライズする場合です。
ソースコードがある場合は、auditing-code-for-vulnerabilities と cargo geiger を使用してください。
Rustマルウェアに関する注意点
Rustは、主にクロスコンパイルと解析者の負担を増やせることから、ランサムウェアやローダーでの使用が増えています。それでも、次の点は役立ちます。
- パニック経路から開発者のビルド環境が漏れることがあります。ユーザー名、プロジェクト名、ディレクトリー構造など、帰属分析に役立つ情報が含まれます。
- レジストリーパスから依存関係の一覧を特定できます。ネットワーク関連や暗号関連のクレートが分かれば、コードを読む前にC2プロトコルや暗号化方式を絞り込めます。
- バイナリ内の
rustcのバージョンからビルド時期が分かります。 - 徹底的にシンボルを削除したサンプルでも、
panic=abortと強力なstripを使ってコンパイルしない限り、境界チェックによるパニック箇所が残ります。それらもなくなっている場合は、意図的な解析妨害策として記録してください。
封じ込めとIOCの作業は analyzing-malware に任せてください。
退けるべき合理化
- 「シンボルが削除されているから、何も復元できない。」 パニック文字列はシンボル削除後も残り、ソースファイル名を示します。
- 「関数が何千個もある。」 そのほとんどは
core/alloc/ベンダー提供クレートです。逆マングル化したパスで絞り込んでください。 - 「この2つの関数は同じだから、コピー&ペーストだ。」 1つのジェネリック関数がモノモーフィゼーションされたものです。
- 「逆コンパイラーに表示された構造体の戻り値の意味が分からない。」
Result/Optionです。呼び出し後の判別値の検査がエラー分岐です。 - 「Rustはメモリー安全だから、メモリーバグはない。」
unsafeと FFI が存在し、ロジックのバグは借用チェッカーの影響を受けません。 - 「文字列が壊れている。」 Rustの文字列はポインターと長さで構成され、ヌル終端ではありません。
参考資料
analyzing-binaries— 一般的なトリアージ、動的解析、解析妨害analyzing-go-binaries— 静的リンクされていてサイズが大きい、もう1つのケースanalyzing-malware— Rustサンプルの封じ込めとIOCauditing-code-for-vulnerabilities— ソースコードがある場合のRustunsafeチェックリスト- rustfilt、Ghidra/IDA用Rust逆マングラー、
cargo-geiger、Binary Ninja