mirror of
https://github.com/morluto/rea
synced 2026-10-25 22:08:11 -04:00
feat(elf): inspect PT_NOTE and SHT_NOTE tables (#4837)
Inspect selected ELF32/ELF64 PT_NOTE and SHT_NOTE declarations without executing the artifact, retaining independent table identities, source ranges, descriptor digests, GNU build-id/ABI fields and explicit coverage limitations. Integrate mvanhorn's implementation and hardening with current main while preserving byte-entropy registration and the Windows curated regression suite. The maintainer follow-up uses ordinary merges/pushes, not history rewriting. Executed validation: 96 focused/shared/docs tests, complete static/docs checks, eleven real CLI/MCP pairs including GCC ELF32 and Clang big-endian PowerPC64, and two actual Codex task traces checked against their Evidence. Full costs, grader correction and verification limits are recorded in PR #4837. Refs #6262.
This commit is contained in:
@@ -219,6 +219,12 @@ explicit path or endpoint and do not need it.
|
||||
delivery requires `--evidence_output`.
|
||||
See the
|
||||
[Rust binary guide](https://github.com/morluto/rea/blob/main/docs/rust-binaries.md).
|
||||
- ELF note tables: `inspect_elf_notes` with the explicit local ELF32/ELF64
|
||||
file. This built-in target-free inspection requires no disassembler. Preserve
|
||||
PT_NOTE and SHT_NOTE ranges, owners, types, descriptor SHA-256, and GNU
|
||||
build-id or ABI-tag fields. A missing table is observed absence of those
|
||||
declarations, not proof the image never carried notes. See the
|
||||
[ELF notes guide](https://github.com/morluto/rea/blob/main/docs/elf-notes.md).
|
||||
|
||||
- Explicit local SQLite database snapshot: `inspect_sqlite_database`. Inspect
|
||||
with Node.js 24.x >=24.15 or 26+ providing native SQLite limits. Read schema
|
||||
|
||||
@@ -96,6 +96,17 @@ dependencies. An omitted package kind stays null. A rustc commit hash is
|
||||
inferred from a read-only `/rustc/<40-hex>/` path and does not by itself
|
||||
identify the compiler. See [Rust binaries](https://github.com/morluto/rea/blob/main/docs/rust-binaries.md).
|
||||
|
||||
## ELF note tables
|
||||
|
||||
Use `inspect_elf_notes` for an explicit local ELF32/ELF64 image when the
|
||||
question concerns PT_NOTE/SHT_NOTE owners, GNU build-id, or GNU ABI-tag
|
||||
fields. No `open_binary` or target execution is required. The CLI equivalent
|
||||
is `inspect-elf-notes PATH`. Preserve artifact SHA-256, table file ranges,
|
||||
descriptor hashes, and original owner spellings. A missing table is observed
|
||||
absence of those declarations, not proof the image never carried notes in
|
||||
another container. See
|
||||
[ELF note tables](https://github.com/morluto/rea/blob/main/docs/elf-notes.md).
|
||||
|
||||
## SQLite snapshots
|
||||
|
||||
Use `inspect_sqlite_database` with an explicit local database snapshot for its
|
||||
|
||||
@@ -191,6 +191,7 @@ Additional tools and host support depend on the target:
|
||||
| IL2CPP metadata | Unity global-metadata.dat assembly images, type and method names and index ranges | Built-in static decoding of versions 24, 27, 29, 31 and 39; no Unity required |
|
||||
| Godot PCK | Archive inventory and extraction, original header/directory facts and selected content identities | Built-in static formats 2, 3 and 4; no Godot required |
|
||||
| Rust binaries | Embedded cargo-auditable crate names, versions, sources and dependency edges | Built-in static ELF/PE/thin Mach-O inspection; [Rust binaries](docs/rust-binaries.md) |
|
||||
| ELF notes | PT_NOTE/SHT_NOTE owners, types, GNU build-id and ABI-tag fields | Built-in static ELF inspection; [ELF notes](docs/elf-notes.md) |
|
||||
| WASM artifact | Validation, sections, exact import/export WAT forms and decoded WAT | Caller-supplied WABT 1.0.42; `inspect_wasm_artifact` |
|
||||
| EVM bytecode | Dispatch selectors, byte offsets, inferred arguments and mutability | Local raw/hex carrier on Linux x64; [offline EVM guide](docs/evm-bytecode.md) |
|
||||
| SQLite databases | Tables, columns, indexes, views, triggers and selected ordinary-table rows | Local database/WAL snapshot; Node.js 24.x >=24.15 or 26+ with native SQLite limits |
|
||||
|
||||
@@ -32,6 +32,7 @@ export default defineConfig({
|
||||
{ text: "IL2CPP metadata", link: "/il2cpp-metadata" },
|
||||
{ text: "Model artifacts", link: "/model-artifacts" },
|
||||
{ text: "Rust dependencies", link: "/rust-binaries" },
|
||||
{ text: "ELF notes", link: "/elf-notes" },
|
||||
{
|
||||
text: "JavaScript and Electron",
|
||||
link: "/javascript-artifact-reconstruction",
|
||||
|
||||
+10
@@ -155,6 +155,16 @@ dependency edges, and the section's file range. A missing `.dep-v0` section is
|
||||
null metadata, not proof that the binary has no dependencies. See
|
||||
[Rust binaries](rust-binaries.md).
|
||||
|
||||
Inspect ELF PT_NOTE and SHT_NOTE tables without executing the image:
|
||||
|
||||
```bash
|
||||
rea inspect-elf-notes /absolute/path/to/program --json
|
||||
```
|
||||
|
||||
The workflow reports note owners, types, descriptor hashes, and GNU build-id
|
||||
or ABI-tag fields when those descriptors match. A missing note table is
|
||||
observed absence of those declarations. See [ELF note tables](elf-notes.md).
|
||||
|
||||
Configure [Hopper or Ghidra](installation.md#hopper), the
|
||||
[IDA adapter](ida-provider.md), or [Kuna](kuna-provider.md) before analyzing a
|
||||
native target. Substitute
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
# ELF note tables
|
||||
|
||||
`inspect_elf_notes` / `inspect-elf-notes` answers whether one selected local
|
||||
ELF32/ELF64 image declares `PT_NOTE` program headers or `SHT_NOTE` sections,
|
||||
which note owners and types those tables store, and which GNU build-id or
|
||||
ABI-tag fields those notes carry. It reads the file without executing it and
|
||||
does not require a disassembler, debug package, or package catalog.
|
||||
|
||||
```bash
|
||||
rea inspect-elf-notes /absolute/path/to/program --json
|
||||
```
|
||||
|
||||
The CLI resolves relative paths against the caller's working directory. MCP
|
||||
requires an absolute filesystem path and preserves that exact selection:
|
||||
|
||||
```json
|
||||
{
|
||||
"name": "inspect_elf_notes",
|
||||
"arguments": { "path": "/artifacts/program" }
|
||||
}
|
||||
```
|
||||
|
||||
## Observations
|
||||
|
||||
The inline Evidence result contains the artifact path, SHA-256, and byte length.
|
||||
`tables` lists each declared note program header or section, including the
|
||||
section name when a string table is present. Each `entries[]` item reports owner,
|
||||
type, descriptor SHA-256, and a hex preview for descriptors of 64 bytes or less.
|
||||
|
||||
For GNU notes, `gnu.build_id_hex`, `gnu.abi_tag`, or `gnu.gold_version` are
|
||||
filled when the descriptor matches that type. Other owners keep `gnu` null.
|
||||
|
||||
`PT_NOTE` and `SHT_NOTE` are examined independently. The same file range may
|
||||
appear twice when both tables declare it.
|
||||
|
||||
A missing table list is observed absence of those declarations. It is not proof
|
||||
the image never carried notes in another container or overlay.
|
||||
|
||||
## Limits and failures
|
||||
|
||||
The selected file is limited to 64 MiB by default (512 MiB maximum). At most
|
||||
32 note entries are examined by default across every table. Retained note
|
||||
names, section names, and GNU descriptor text share that file's byte budget,
|
||||
and repeated table ranges count again. Each descriptor range is hashed once,
|
||||
and each GNU version's trailing NUL run is scanned once; neither walk may
|
||||
exceed that byte ceiling. Malformed ELF headers, truncated header
|
||||
tables, declared note tables that extend past the file, and truncated
|
||||
note entries fail without partial success. ELF extended
|
||||
numbering (`PN_XNUM`, including a section count stored when `e_shnum` is 0) is
|
||||
unsupported.
|
||||
|
||||
This workflow does not execute the image or verify GNU build-id uniqueness
|
||||
against package catalogs.
|
||||
+1
-1
@@ -50,7 +50,7 @@ For runtime instrumentation, see the [Frida guide](./frida-instrumentation.md).
|
||||
| Task | Guides |
|
||||
| --------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| Identify a desktop installation | [Runtime candidates, evidence and follow-up files](desktop-applications.md) |
|
||||
| Inspect native functions, references, or types | [Native binaries](native-investigation.md), [Go metadata](go-binaries.md), [Rust dependencies](rust-binaries.md), [offline ELF layout](binary-diagnostics.md), [recorded crashes](recorded-crashes.md) |
|
||||
| Inspect native functions, references, or types | [Native binaries](native-investigation.md), [Go metadata](go-binaries.md), [Rust dependencies](rust-binaries.md), [ELF notes](elf-notes.md), [offline ELF layout](binary-diagnostics.md), [recorded crashes](recorded-crashes.md) |
|
||||
| Select a native provider or import format | [Ghidra semantics](ghidra-provider.md), [Windows](windows-ghidra-p0.md), [DOS](ghidra-dos.md), [raw bytes](raw-binary.md), [MIPS](ghidra-mips.md), [PSP](ghidra-psp.md), [NativeAOT](ghidra-nativeaot.md), [IDA](ida-provider.md), [Kuna](kuna-provider.md), [JEB](jeb-analysis.md) |
|
||||
| Analyze JavaScript or Electron artifacts | [Static analysis](javascript-artifact-reconstruction.md), [tracing and comparisons](javascript-application-workflows.md), [application graph](javascript-application-graph.md), [source recovery](javascript-recovery.md) |
|
||||
| Compare static JavaScript with runtime observations | [Runtime attribution](javascript-runtime-observation.md), [reconciliation](javascript-runtime-reconciliation.md), [Electron observation](electron-observation.md), [Electron fuses](electron-fuses.md) |
|
||||
|
||||
@@ -145,6 +145,7 @@ npx rea-agents@latest setup
|
||||
| بنية ELF دون تشغيل | الأقسام والمقاطع والرموز ومعلومات إعادة التموضع الأصلية، وآليات الحماية المحتملة المستدلّ عليها بالفحص الثابت | pwntools يوفّره المستدعي على Linux x64؛ [تشخيص الملفات الثنائية](../binary-diagnostics.md) |
|
||||
| شيفرة EVM البايتية | محددات التوجيه، وإزاحات البايتات، والمعاملات المستنتجة، وقابلية تغيير الحالة | مدخل محلي يحمل بايتات خامًا أو تمثيلًا سداسيًا عشريًا على Linux x64؛ [دليل EVM دون تشغيل](../evm-bytecode.md) |
|
||||
| Rust binaries | Embedded cargo-auditable dependency list | [Rust binaries](../rust-binaries.md) |
|
||||
| ELF notes | PT_NOTE/SHT_NOTE owners, types, GNU build-id and ABI-tag fields | Built-in static ELF inspection; [ELF notes](../elf-notes.md) |
|
||||
| أعطال Linux المسجّلة | سجلات note الخام، ومسجّلات المعالج/الإشارات لكل خيط مسجّل، وترشيحات اختيارية لخرائط الذاكرة | pwntools يوفّره المستدعي على Linux x64؛ GDB/pwndbg اختياريان؛ [الأعطال المسجّلة](../recorded-crashes.md) |
|
||||
| تثبيتات سطح المكتب | عائلات وقت التشغيل، والملفات الداعمة، وعلامات المكونات، والخطوات التالية | فحص محدود للملفات والترويسات;[تطبيقات سطح المكتب](../desktop-applications.md) |
|
||||
| JavaScript / Electron | الوحدات، وعمليات الاستيراد، وخرائط المصدر، والمسارات، وIPC، وعلاقات إضافات الشيفرة الأصلية | Node.js وnpm؛ [تحليل التطبيقات](../javascript-application-workflows.md) |
|
||||
|
||||
@@ -145,6 +145,7 @@ REA benötigt Node.js 22.x (>=22.19), 24.x (>=24.11) oder 26+ sowie npm. Zusätz
|
||||
| Offline-ELF-Struktur | Sektionen, Segmente, ursprüngliche Symbole/Relokationen und mögliche Schutzmaßnahmen aus statischer Analyse | Vom Aufrufer bereitgestelltes pwntools unter Linux x64; [Binärdiagnostik](../binary-diagnostics.md) |
|
||||
| EVM-Bytecode | Dispatch-Selektoren, Byte-Offsets, abgeleitete Argumente und Veränderbarkeit des Zustands | Lokale Eingabe mit Rohbytes oder Hexdarstellung unter Linux x64; [Offline-EVM-Leitfaden](../evm-bytecode.md) |
|
||||
| Rust binaries | Embedded cargo-auditable dependency list | [Rust binaries](../rust-binaries.md) |
|
||||
| ELF notes | PT_NOTE/SHT_NOTE owners, types, GNU build-id and ABI-tag fields | Built-in static ELF inspection; [ELF notes](../elf-notes.md) |
|
||||
| Aufgezeichnete Linux-Abstürze | Unverarbeitete Note-Einträge, Register/Signale aller aufgezeichneten Threads und optionale Zuordnungskandidaten | Vom Aufrufer bereitgestelltes pwntools unter Linux x64; optional GDB/pwndbg; [aufgezeichnete Abstürze](../recorded-crashes.md) |
|
||||
| Desktop-Installationen | Laufzeitkandidaten, Quelldateien, Komponenten und nächste Schritte | Begrenzte Verzeichnis- und Headerprüfung;[Desktop-Anwendungen](../desktop-applications.md) |
|
||||
| JavaScript / Electron | Module, Imports, Source Maps, Routen, IPC und Beziehungen zu nativen Add-ons | Node.js und npm; [Anwendungsanalyse](../javascript-application-workflows.md) |
|
||||
|
||||
@@ -149,6 +149,7 @@ npx rea-agents@latest setup
|
||||
| Δομή ELF χωρίς εκτέλεση | Ενότητες, τμήματα, αρχικά σύμβολα/εγγραφές μετεγκατάστασης και ενδείξεις πιθανών μέτρων προστασίας από στατική ανάλυση | pwntools που παρέχει ο χρήστης σε Linux x64· [διαγνωστικά δυαδικών αρχείων](../binary-diagnostics.md) |
|
||||
| Δυαδικά αρχεία Go | Ενσωματωμένη έκδοση μεταγλωττιστή, modules, αντικαταστάσεις και ρυθμίσεις μεταγλώττισης | Ενσωματωμένη στατική εξέταση ELF/PE/thin Mach-O· δεν απαιτείται εγκατάσταση του Go |
|
||||
| Δυαδικά αρχεία Rust | Ενσωματωμένα ονόματα, εκδόσεις, πηγές και σχέσεις εξάρτησης των crates από το cargo-auditable | Ενσωματωμένη στατική εξέταση ELF/PE/thin Mach-O· [δυαδικά αρχεία Rust](../rust-binaries.md) |
|
||||
| ELF notes | PT_NOTE/SHT_NOTE owners, types, GNU build-id and ABI-tag fields | Built-in static ELF inspection; [ELF notes](../elf-notes.md) |
|
||||
| Αρχείο WASM | Έλεγχο εγκυρότητας, ενότητες, ακριβείς μορφές εισαγωγών/εξαγωγών σε WAT και αποκωδικοποιημένο WAT | WABT 1.0.42 που παρέχει ο χρήστης· `inspect_wasm_artifact` |
|
||||
| Bytecode EVM | Selectors επιλογής συνάρτησης, μετατοπίσεις σε bytes, εκτιμώμενα ορίσματα και δυνατότητα μεταβολής της κατάστασης | Τοπική είσοδος σε ακατέργαστα bytes ή δεκαεξαδική μορφή σε Linux x64· [οδηγός EVM χωρίς εκτέλεση](../evm-bytecode.md) |
|
||||
| Βάσεις δεδομένων SQLite | Πίνακες, στήλες, ευρετήρια, όψεις, triggers και επιλεγμένες γραμμές κανονικών πινάκων | Τοπικό στιγμιότυπο βάσης δεδομένων/WAL· Node.js 24.x >=24.15 ή 26+ με εγγενή όρια SQLite |
|
||||
|
||||
@@ -145,6 +145,7 @@ REA requiere Node.js 22.x (>=22.19), 24.x (>=24.11) o 26+, además de npm. Las h
|
||||
| Estructura ELF sin ejecutar | Secciones, segmentos, símbolos/reubicaciones originales y posibles medidas de mitigación detectadas mediante análisis estático | pwntools proporcionado por quien llama, en Linux x64; [diagnóstico de binarios](../binary-diagnostics.md) |
|
||||
| Bytecode EVM | Selectores de despacho, desplazamientos en bytes, argumentos inferidos y mutabilidad | Entrada local con bytes sin procesar o representación hexadecimal, en Linux x64; [guía de EVM sin ejecutar](../evm-bytecode.md) |
|
||||
| Rust binaries | Embedded cargo-auditable dependency list | [Rust binaries](../rust-binaries.md) |
|
||||
| ELF notes | PT_NOTE/SHT_NOTE owners, types, GNU build-id and ABI-tag fields | Built-in static ELF inspection; [ELF notes](../elf-notes.md) |
|
||||
| Fallos de Linux registrados | Registros note sin procesar, registros/señales de cada hilo registrado y posibles correspondencias de memoria opcionales | pwntools proporcionado por quien llama, en Linux x64; GDB/pwndbg opcionales; [fallos registrados](../recorded-crashes.md) |
|
||||
| Instalaciones de escritorio | Posibles entornos de ejecución, archivos de evidencia, componentes y siguientes pasos | Inspección acotada de directorios y cabeceras;[Aplicaciones de escritorio](../desktop-applications.md) |
|
||||
| JavaScript / Electron | Módulos, importaciones, mapas de código fuente, rutas, IPC y relaciones con complementos nativos | Node.js y npm; [análisis de aplicaciones](../javascript-application-workflows.md) |
|
||||
|
||||
@@ -137,6 +137,7 @@ REA به Node.js 22.x (>=22.19)، 24.x (>=24.11) یا 26+، بههمراه np
|
||||
| ساختار ELF آفلاین | بخشها، سگمنتها، نمادها و relocationهای اصلی و گزینههای احتمالی کاهش آسیبپذیری ایستا | pwntools تأمینشده توسط فراخواننده روی Linux x64؛ [تشخیص باینری](../binary-diagnostics.md) |
|
||||
| بایتکد EVM | selectorهای dispatch، offsetهای بایتی، آرگومانهای استنباطشده و قابلیت تغییر وضعیت | دادهٔ خام/hex محلی روی Linux x64؛ [راهنمای EVM آفلاین](../evm-bytecode.md) |
|
||||
| Rust binaries | Embedded cargo-auditable dependency list | [Rust binaries](../rust-binaries.md) |
|
||||
| ELF notes | PT_NOTE/SHT_NOTE owners, types, GNU build-id and ABI-tag fields | Built-in static ELF inspection; [ELF notes](../elf-notes.md) |
|
||||
| کرشهای ثبتشدهٔ لینوکس | یادداشتهای خام، رجیسترها/سیگنالهای تمام threadهای ثبتشده و گزینههای احتمالی نگاشت | pwntools تأمینشده توسط فراخواننده روی Linux x64؛ GDB/pwndbg اختیاری؛ [کرشهای ثبتشده](../recorded-crashes.md) |
|
||||
| نصبهای دسکتاپ | نامزدهای زمان اجرا، فایلهای شاهد، نشانههای مؤلفهها و مراحل بعدی | بررسی محدود پوشهها و سرآیندها;[برنامههای دسکتاپ](../desktop-applications.md) |
|
||||
| JavaScript / Electron | ماژولها، importها، source mapها، مسیرها، IPC و ارتباط افزونههای بومی | Node.js و npm؛ [تحلیل برنامه](../javascript-application-workflows.md) |
|
||||
|
||||
@@ -149,6 +149,7 @@ REA nécessite Node.js 22.x (>=22.19), 24.x (>=24.11) ou 26+, ainsi que npm. Les
|
||||
| Artefact WASM | Validation, sections, formes WAT exactes d'import/export et WAT décodé | WABT 1.0.42 fourni par l'utilisateur ; `inspect_wasm_artifact` |
|
||||
| Bytecode EVM | Sélecteurs de dispatch, offsets en octets, arguments inférés et mutabilité | Fichier local brut ou hexadécimal sous Linux x64 ; [guide EVM hors ligne](../evm-bytecode.md) |
|
||||
| Rust binaries | Embedded cargo-auditable dependency list | [Rust binaries](../rust-binaries.md) |
|
||||
| ELF notes | PT_NOTE/SHT_NOTE owners, types, GNU build-id and ABI-tag fields | Built-in static ELF inspection; [ELF notes](../elf-notes.md) |
|
||||
| Crashs Linux enregistrés | Notes brutes, registres et signaux de chaque thread enregistré, et correspondances de mappings facultatives | pwntools fourni par l'utilisateur sous Linux x64 ; GDB/pwndbg en option ; [crashs enregistrés](../recorded-crashes.md) |
|
||||
| Installations de bureau | Environnements candidats, fichiers justificatifs, composants et étapes suivantes | Inspection bornée des répertoires et en-têtes;[Applications de bureau](../desktop-applications.md) |
|
||||
| JavaScript / Electron | Modules, imports, source maps, routes, IPC et relations avec les modules natifs | Node.js et npm ; [analyse d'applications](../javascript-application-workflows.md) |
|
||||
|
||||
@@ -152,6 +152,7 @@ Alat tambahan dan dukungan sistem host bergantung pada target yang dianalisis:
|
||||
| Artefak WASM | Validasi, bagian, bentuk WAT import/ekspor yang tepat, dan WAT yang didekode | WABT 1.0.42 yang disediakan pemanggil; `inspect_wasm_artifact` |
|
||||
| Bytecode EVM | Selector dispatch, offset byte, argumen yang disimpulkan, dan sifat mutabilitas | Pembawa data raw/hex lokal; [panduan EVM offline](../evm-bytecode.md) |
|
||||
| Rust binaries | Embedded cargo-auditable dependency list | [Rust binaries](../rust-binaries.md) |
|
||||
| ELF notes | PT_NOTE/SHT_NOTE owners, types, GNU build-id and ABI-tag fields | Built-in static ELF inspection; [ELF notes](../elf-notes.md) |
|
||||
| Crash Linux yang direkam | Catatan mentah, register/sinyal setiap thread yang direkam, dan kandidat pemetaan opsional | pwntools yang disediakan pemanggil; GDB/pwndbg opsional; [crash yang direkam](../recorded-crashes.md) |
|
||||
| Instalasi desktop | Kandidat runtime, berkas pendukung, penanda komponen, dan langkah berikutnya | Pemeriksaan direktori dan header terbatas;[Aplikasi desktop](../desktop-applications.md) |
|
||||
| JavaScript / Electron | Modul, impor, source map, rute, IPC, dan hubungan add-on native | Node.js dan npm; [analisis aplikasi](../javascript-application-workflows.md) |
|
||||
|
||||
@@ -149,6 +149,7 @@ REA richiede Node.js 22.x (>=22.19), 24.x (>=24.11) o 26+, oltre a npm. Strument
|
||||
| Artefatto WASM | Convalida, sezioni, forme WAT di import/export esatte e WAT decodificato | WABT 1.0.42 fornito dal chiamante; `inspect_wasm_artifact` |
|
||||
| Bytecode EVM | Selettori di dispatch, offset dei byte, argomenti inferiti e mutabilità | Carrier raw/hex locale; [guida EVM offline](../evm-bytecode.md) |
|
||||
| Rust binaries | Embedded cargo-auditable dependency list | [Rust binaries](../rust-binaries.md) |
|
||||
| ELF notes | PT_NOTE/SHT_NOTE owners, types, GNU build-id and ABI-tag fields | Built-in static ELF inspection; [ELF notes](../elf-notes.md) |
|
||||
| Crash Linux registrati | Note grezze, registri/segnai di ogni thread registrato e candidati mapping opzionali | pwntools fornito dal chiamante; GDB/pwndbg opzionale; [crash registrati](../recorded-crashes.md) |
|
||||
| Installazioni desktop | Runtime candidati, file di supporto, componenti e passi successivi | Ispezione limitata di directory e intestazioni;[Applicazioni desktop](../desktop-applications.md) |
|
||||
| JavaScript / Electron | Moduli, import, source map, route, IPC e relazioni con add-on nativi | Node.js e npm; [analisi delle applicazioni](../javascript-application-workflows.md) |
|
||||
|
||||
@@ -145,6 +145,7 @@ REA には Node.js 22.x(>=22.19)、24.x(>=24.11)、または 26+ と npm
|
||||
| オフライン ELF レイアウト | セクション、セグメント、元のシンボル/再配置情報、静的解析によるセキュリティ対策の候補 | Linux x64 で利用者が用意する pwntools;[バイナリ診断](../binary-diagnostics.md) |
|
||||
| EVM バイトコード | ディスパッチセレクター、バイトオフセット、推定された引数と状態変更の可否 | Linux x64 上のローカルの生バイト/16 進数の入力ファイル;[オフライン EVM ガイド](../evm-bytecode.md) |
|
||||
| Rust binaries | Embedded cargo-auditable dependency list | [Rust binaries](../rust-binaries.md) |
|
||||
| ELF notes | PT_NOTE/SHT_NOTE owners, types, GNU build-id and ABI-tag fields | Built-in static ELF inspection; [ELF notes](../elf-notes.md) |
|
||||
| 記録済みの Linux クラッシュ | 生の note レコード、記録された全スレッドのレジスター/シグナル、任意のマッピング候補 | Linux x64 で利用者が用意する pwntools;GDB/pwndbg は任意;[記録済みクラッシュ](../recorded-crashes.md) |
|
||||
| デスクトップのインストール先 | ランタイム候補、根拠ファイル、コンポーネントの目印、次の調査手順 | 上限付きのディレクトリ・ヘッダー検査;[デスクトップアプリ](../desktop-applications.md) |
|
||||
| JavaScript / Electron | モジュール、インポート、ソースマップ、ルート、IPC、ネイティブアドオンの関係 | Node.js と npm;[アプリケーション分析](../javascript-application-workflows.md) |
|
||||
|
||||
@@ -153,6 +153,7 @@ REA에는 Node.js 22.x(>=22.19), 24.x(>=24.11) 또는 26+와 npm이 필요합니
|
||||
| 오프라인 ELF 레이아웃 | 섹션, 세그먼트, 원본 심볼/재배치 정보, 정적 분석으로 얻은 보안 완화 기법 후보 | Linux x64에서 호출자가 제공하는 pwntools; [바이너리 진단](../binary-diagnostics.md) |
|
||||
| Go 바이너리 | 내장된 컴파일러 버전, 모듈, `replace` 지시어, 빌드 설정 | REA 내장 ELF/PE/thin Mach-O 정적 검사; Go 설치 불필요 |
|
||||
| Rust 바이너리 | cargo-auditable이 내장한 크레이트 이름, 버전, 소스, 의존성 관계 | REA 내장 ELF/PE/thin Mach-O 정적 검사; [Rust 바이너리](../rust-binaries.md) |
|
||||
| ELF notes | PT_NOTE/SHT_NOTE owners, types, GNU build-id and ABI-tag fields | Built-in static ELF inspection; [ELF notes](../elf-notes.md) |
|
||||
| WASM 아티팩트 | 유효성 검사, 섹션, import/export의 정확한 WAT 형식, 디코딩된 WAT | 호출자 제공 WABT 1.0.42; `inspect_wasm_artifact` |
|
||||
| EVM 바이트코드 | 디스패치 선택자, 바이트 오프셋, 추론한 인자와 상태 변경 가능 여부 | Linux x64의 로컬 원시 바이트/16진수 입력 파일; [오프라인 EVM 가이드](../evm-bytecode.md) |
|
||||
| SQLite 데이터베이스 | 테이블, 열, 인덱스, 뷰, 트리거, 선택한 일반 테이블의 행 | 로컬 데이터베이스/WAL 스냅샷; Node.js 24.15 이상의 24.x 또는 26 이상(네이티브 SQLite 한도 적용) |
|
||||
|
||||
@@ -151,6 +151,7 @@ REA-യ്ക്ക് Node.js 22.x (>=22.19), 24.x (>=24.11), അല്ലെ
|
||||
| Offline ELF layout | Sections, segments, യഥാർത്ഥ symbols/relocations, static mitigation candidates | Linux x64-ൽ caller നൽകുന്ന pwntools; [binary diagnostics](../binary-diagnostics.md) |
|
||||
| Go binaries | ഉൾച്ചേർത്ത compiler version, modules, replacements, build settings | Built-in static ELF/PE/thin Mach-O പരിശോധന; Go installation ആവശ്യമില്ല |
|
||||
| Rust binaries | ഉൾച്ചേർത്ത cargo-auditable crate പേരുകൾ, versions, sources, dependency edges | Built-in static ELF/PE/thin Mach-O പരിശോധന; [Rust binaries](../rust-binaries.md) |
|
||||
| ELF notes | PT_NOTE/SHT_NOTE owners, types, GNU build-id and ABI-tag fields | Built-in static ELF inspection; [ELF notes](../elf-notes.md) |
|
||||
| WASM artifact | Validation, sections, കൃത്യമായ import/export WAT രൂപങ്ങൾ, decoded WAT | Caller നൽകുന്ന WABT 1.0.42; `inspect_wasm_artifact` |
|
||||
| EVM bytecode | Dispatch selectors, byte offsets, അനുമാനിച്ച arguments, mutability | Linux x64-ൽ local raw/hex carrier; [offline EVM guide](../evm-bytecode.md) |
|
||||
| SQLite databases | Tables, columns, indexes, views, triggers, തിരഞ്ഞെടുത്ത സാധാരണ table rows | Local database/WAL snapshot; native SQLite പരിമിതികളോടെ Node.js 24.x >=24.15 അല്ലെങ്കിൽ 26+ |
|
||||
|
||||
@@ -145,6 +145,7 @@ REA wymaga Node.js 22.x (>=22.19), 24.x (>=24.11) lub 26+ oraz npm. Dodatkowe na
|
||||
| Układ pliku ELF w trybie offline | Sekcje, segmenty, oryginalne symbole/relokacje i potencjalne mechanizmy zabezpieczeń wskazane przez analizę statyczną | pwntools dostarczone przez użytkownika w systemie Linux x64; [diagnostyka plików binarnych](../binary-diagnostics.md) |
|
||||
| Kod bajtowy EVM | Selektory wywołań, przesunięcia bajtowe, wywnioskowane argumenty i mutowalność | Lokalny plik z surowymi bajtami lub zapisem szesnastkowym w systemie Linux x64; [przewodnik EVM offline](../evm-bytecode.md) |
|
||||
| Rust binaries | Embedded cargo-auditable dependency list | [Rust binaries](../rust-binaries.md) |
|
||||
| ELF notes | PT_NOTE/SHT_NOTE owners, types, GNU build-id and ABI-tag fields | Built-in static ELF inspection; [ELF notes](../elf-notes.md) |
|
||||
| Zarejestrowane awarie systemu Linux | Surowe rekordy note, rejestry/sygnały każdego zarejestrowanego wątku i opcjonalni kandydaci na mapowania | pwntools dostarczone przez użytkownika w systemie Linux x64; opcjonalnie GDB/pwndbg; [zarejestrowane awarie](../recorded-crashes.md) |
|
||||
| Instalacje desktopowe | Kandydaci środowisk uruchomieniowych, pliki źródłowe dowodów, komponenty i dalsze kroki | Ograniczona inspekcja katalogów i nagłówków;[Aplikacje desktopowe](../desktop-applications.md) |
|
||||
| JavaScript / Electron | Moduły, importy, mapy źródeł, trasy, IPC i powiązania z natywnymi dodatkami | Node.js i npm; [analiza aplikacji](../javascript-application-workflows.md) |
|
||||
|
||||
@@ -145,6 +145,7 @@ O REA requer Node.js 22.x (>=22.19), 24.x (>=24.11) ou 26+, além do npm. As fer
|
||||
| Estrutura ELF sem execução | Seções, segmentos, símbolos/relocações originais e possíveis mecanismos de mitigação identificados por análise estática | pwntools fornecido pelo chamador no Linux x64; [diagnóstico de binários](../binary-diagnostics.md) |
|
||||
| Bytecode EVM | Seletores de despacho, offsets de bytes, argumentos inferidos e mutabilidade | Entrada local com bytes brutos ou representação hexadecimal no Linux x64; [guia de EVM sem execução](../evm-bytecode.md) |
|
||||
| Rust binaries | Embedded cargo-auditable dependency list | [Rust binaries](../rust-binaries.md) |
|
||||
| ELF notes | PT_NOTE/SHT_NOTE owners, types, GNU build-id and ABI-tag fields | Built-in static ELF inspection; [ELF notes](../elf-notes.md) |
|
||||
| Falhas do Linux registradas | Registros note brutos, registradores/sinais de cada thread registrada e candidatos opcionais a mapeamentos | pwntools fornecido pelo chamador no Linux x64; GDB/pwndbg opcionais; [falhas registradas](../recorded-crashes.md) |
|
||||
| Instalações desktop | Ambientes candidatos, arquivos de evidência, componentes e próximos passos | Inspeção limitada de diretórios e cabeçalhos;[Aplicações desktop](../desktop-applications.md) |
|
||||
| JavaScript / Electron | Módulos, importações, source maps, rotas, IPC e relações com complementos nativos | Node.js e npm; [análise de aplicativos](../javascript-application-workflows.md) |
|
||||
|
||||
@@ -145,6 +145,7 @@ REA требует Node.js 22.x (>=22.19), 24.x (>=24.11) или 26+, а так
|
||||
| Структура ELF без запуска | Секции, сегменты, исходные символы/релокации и возможные механизмы защиты по данным статического анализа | pwntools, предоставленный вызывающей стороной, в Linux x64; [диагностика бинарных файлов](../binary-diagnostics.md) |
|
||||
| Байт-код EVM | Селекторы диспетчеризации, смещения байтов, предполагаемые аргументы и изменяемость состояния | Локальные необработанные байты или шестнадцатеричное представление в Linux x64; [руководство по EVM без запуска](../evm-bytecode.md) |
|
||||
| Rust binaries | Embedded cargo-auditable dependency list | [Rust binaries](../rust-binaries.md) |
|
||||
| ELF notes | PT_NOTE/SHT_NOTE owners, types, GNU build-id and ABI-tag fields | Built-in static ELF inspection; [ELF notes](../elf-notes.md) |
|
||||
| Записанные сбои Linux | Необработанные записи note, регистры/сигналы каждого записанного потока и необязательные кандидаты сопоставления с памятью | pwntools, предоставленный вызывающей стороной, в Linux x64; необязательные GDB/pwndbg; [записанные сбои](../recorded-crashes.md) |
|
||||
| Установленные настольные приложения | Возможные среды выполнения, файлы с доказательствами, компоненты и дальнейшие шаги | Ограниченная проверка каталогов и заголовков;[Настольные приложения](../desktop-applications.md) |
|
||||
| JavaScript / Electron | Модули, импорты, карты исходного кода, маршруты, IPC и связи с нативными дополнениями | Node.js и npm; [анализ приложений](../javascript-application-workflows.md) |
|
||||
|
||||
@@ -145,6 +145,7 @@ REA ต้องใช้ Node.js 22.x (>=22.19), 24.x (>=24.11) หรือ 26
|
||||
| โครงสร้าง ELF แบบออฟไลน์ | เซกชัน เซกเมนต์ สัญลักษณ์/รีโลเคชันต้นฉบับ และตัวเลือกกลไกป้องกันที่พบจากการวิเคราะห์แบบสถิต | pwntools ที่ผู้ใช้จัดเตรียมบน Linux x64; [การวินิจฉัยไบนารี](../binary-diagnostics.md) |
|
||||
| ไบต์โค้ด EVM | ตัวเลือกฟังก์ชันสำหรับการเรียก ออฟเซ็ตไบต์ อาร์กิวเมนต์และความสามารถในการเปลี่ยนสถานะที่อนุมานได้ | ไฟล์บนเครื่องที่มีไบต์ดิบหรือข้อมูลเลขฐานสิบหกบน Linux x64; [คู่มือ EVM แบบออฟไลน์](../evm-bytecode.md) |
|
||||
| Rust binaries | Embedded cargo-auditable dependency list | [Rust binaries](../rust-binaries.md) |
|
||||
| ELF notes | PT_NOTE/SHT_NOTE owners, types, GNU build-id and ABI-tag fields | Built-in static ELF inspection; [ELF notes](../elf-notes.md) |
|
||||
| ข้อมูลการแครชของ Linux ที่บันทึกไว้ | ระเบียน note ดิบ รีจิสเตอร์/สัญญาณของทุกเธรดที่บันทึกไว้ และตัวเลือกการแมปเพิ่มเติม | pwntools ที่ผู้ใช้จัดเตรียมบน Linux x64; เลือกใช้ GDB/pwndbg เพิ่มเติมได้; [การแครชที่บันทึกไว้](../recorded-crashes.md) |
|
||||
| ไดเรกทอรีติดตั้งแอปเดสก์ท็อป | รันไทม์ที่เป็นไปได้ ไฟล์หลักฐาน ตัวบ่งชี้ส่วนประกอบ และขั้นตอนถัดไป | ตรวจสอบไดเรกทอรีและส่วนหัวโดยจำกัดขอบเขต;[แอปเดสก์ท็อป](../desktop-applications.md) |
|
||||
| JavaScript / Electron | โมดูล การนำเข้า ซอร์สแมป เส้นทาง IPC และความสัมพันธ์กับส่วนเสริมเนทีฟ | Node.js และ npm; [การวิเคราะห์แอปพลิเคชัน](../javascript-application-workflows.md) |
|
||||
|
||||
@@ -147,6 +147,7 @@ REA; Node.js 22.x (>=22.19), 24.x (>=24.11) veya 26+ ve npm gerektirir. Ek araç
|
||||
| Çevrimdışı ELF yapısı | Bölümler, segmentler, özgün semboller/yeniden konumlandırmalar ve statik analizden olası güvenlik önlemleri | Linux x64 üzerinde çağıranın sağladığı pwntools; [ikili dosya tanılama](../binary-diagnostics.md) |
|
||||
| Go ikili dosyaları | Gömülü compiler sürümü, modüller, replacement'lar ve build ayarları | Yerleşik statik ELF/PE/thin Mach-O incelemesi; Go kurulumu gerekmez |
|
||||
| Rust ikili dosyaları | Gömülü cargo-auditable crate adları, sürümleri, kaynakları ve bağımlılık ilişkileri | Yerleşik statik ELF/PE/thin Mach-O incelemesi; [Rust ikili dosyaları](../rust-binaries.md) |
|
||||
| ELF notes | PT_NOTE/SHT_NOTE owners, types, GNU build-id and ABI-tag fields | Built-in static ELF inspection; [ELF notes](../elf-notes.md) |
|
||||
| WASM artefaktı | Doğrulama, bölümler, tam import/export WAT biçimleri ve decode edilmiş WAT | Çağıranın sağladığı WABT 1.0.42; `inspect_wasm_artifact` |
|
||||
| EVM bayt kodu | Yönlendirme seçicileri, bayt ofsetleri, çıkarımla belirlenen argümanlar ve durum değiştirilebilirliği | Linux x64 üzerinde yerel ham bayt/onaltılık girdi; [çevrimdışı EVM rehberi](../evm-bytecode.md) |
|
||||
| SQLite veritabanları | Tablolar, sütunlar, index'ler, view'lar, trigger'lar ve seçilen sıradan tablo satırları | Yerel database/WAL snapshot'ı; native SQLite sınırlarıyla Node.js 24.x >=24.15 veya 26+ |
|
||||
|
||||
@@ -145,6 +145,7 @@ REA потребує Node.js 22.x (>=22.19), 24.x (>=24.11) або 26+, а та
|
||||
| Структура ELF без запуску | Секції, сегменти, початкові символи/релокації та можливі захисні механізми за даними статичного аналізу | pwntools, наданий стороною, що викликає інструмент, у Linux x64; [діагностика бінарних файлів](../binary-diagnostics.md) |
|
||||
| Байт-код EVM | Селектори диспетчеризації, зміщення байтів, виведені припущення про аргументи та змінюваність стану | Локальні необроблені байти або шістнадцяткове представлення у Linux x64; [посібник EVM без запуску](../evm-bytecode.md) |
|
||||
| Rust binaries | Embedded cargo-auditable dependency list | [Rust binaries](../rust-binaries.md) |
|
||||
| ELF notes | PT_NOTE/SHT_NOTE owners, types, GNU build-id and ABI-tag fields | Built-in static ELF inspection; [ELF notes](../elf-notes.md) |
|
||||
| Записані аварійні завершення в Linux | Необроблені записи note, регістри/сигнали кожного записаного потоку й необов'язкові кандидати зіставлення пам'яті | pwntools, наданий стороною, що викликає інструмент, у Linux x64; необов'язкові GDB/pwndbg; [записані аварійні завершення](../recorded-crashes.md) |
|
||||
| Встановлені настільні програми | Можливі середовища виконання, файли доказів, компоненти та наступні кроки | Обмежена перевірка каталогів і заголовків;[Настільні програми](../desktop-applications.md) |
|
||||
| JavaScript / Electron | Модулі, імпорти, карти вихідного коду, маршрути, IPC і зв'язки з нативними доповненнями | Node.js і npm; [аналіз застосунків](../javascript-application-workflows.md) |
|
||||
|
||||
@@ -145,6 +145,7 @@ REA yêu cầu Node.js 22.x (>=22.19), 24.x (>=24.11) hoặc 26+, cùng với np
|
||||
| Bố cục ELF ngoại tuyến | Các section, segment, ký hiệu/thông tin tái định vị gốc và các biện pháp giảm thiểu có thể có từ phân tích tĩnh | pwntools do bên gọi cung cấp trên Linux x64; [chẩn đoán tệp nhị phân](../binary-diagnostics.md) |
|
||||
| Bytecode EVM | Bộ chọn điều phối, độ lệch byte, tham số suy luận và khả năng thay đổi trạng thái | Đầu vào cục bộ chứa byte thô hoặc dạng thập lục phân trên Linux x64; [hướng dẫn EVM ngoại tuyến](../evm-bytecode.md) |
|
||||
| Rust binaries | Embedded cargo-auditable dependency list | [Rust binaries](../rust-binaries.md) |
|
||||
| ELF notes | PT_NOTE/SHT_NOTE owners, types, GNU build-id and ABI-tag fields | Built-in static ELF inspection; [ELF notes](../elf-notes.md) |
|
||||
| Sự cố Linux đã ghi lại | Bản ghi note thô, thanh ghi/tín hiệu của mọi luồng được ghi lại và các ứng viên ánh xạ tùy chọn | pwntools do bên gọi cung cấp trên Linux x64; GDB/pwndbg tùy chọn; [sự cố đã ghi lại](../recorded-crashes.md) |
|
||||
| Thư mục cài đặt ứng dụng máy tính | Các runtime có khả năng được dùng, tệp bằng chứng, dấu hiệu thành phần và bước tiếp theo | Kiểm tra thư mục và phần đầu tệp có giới hạn;[Ứng dụng máy tính](../desktop-applications.md) |
|
||||
| JavaScript / Electron | Mô-đun, các khai báo import, source map, tuyến, IPC và quan hệ với tiện ích mở rộng mã máy | Node.js và npm; [phân tích ứng dụng](../javascript-application-workflows.md) |
|
||||
|
||||
@@ -149,6 +149,7 @@ REA 需要 Node.js 22.x(>=22.19)、24.x(>=24.11)或 26+,以及 npm。
|
||||
| WASM 工件 | 驗證、節、精確的匯入/匯出 WAT 形式及解碼後的 WAT | 呼叫方提供的 WABT 1.0.42;`inspect_wasm_artifact` |
|
||||
| EVM 位元組碼 | 分派選擇器、位元組偏移、推斷的參數和狀態可變性 | Linux x64 上的本地原始位元組/十六進位輸入載體;[離線 EVM 指南](../evm-bytecode.md) |
|
||||
| Rust binaries | Embedded cargo-auditable dependency list | [Rust binaries](../rust-binaries.md) |
|
||||
| ELF notes | PT_NOTE/SHT_NOTE owners, types, GNU build-id and ABI-tag fields | Built-in static ELF inspection; [ELF notes](../elf-notes.md) |
|
||||
| 已記錄的 Linux 崩潰 | 原始 note 記錄、每個已記錄執行緒的暫存器/訊號及可選的對映候選項 | Linux x64 上由呼叫方提供的 pwntools;可選的 GDB/pwndbg;[已記錄崩潰](../recorded-crashes.md) |
|
||||
| 桌面安裝目錄 | 執行階段候選、支持檔案、元件標記與後續操作 | 有界的目錄與檔頭檢查;[桌面應用](../desktop-applications.md) |
|
||||
| JavaScript / Electron | 模組、匯入、source map、路由、IPC 和原生附加元件關係 | Node.js 和 npm;[應用分析](../javascript-application-workflows.md) |
|
||||
|
||||
+27
-26
@@ -141,32 +141,33 @@ npx rea-agents@latest setup
|
||||
|
||||
REA 需要 Node.js 22.x(>=22.19)、24.x(>=24.11)或 26+,以及 npm。额外工具和主机支持取决于分析目标:
|
||||
|
||||
| 目标 | REA 返回的内容 | 要求与指南 |
|
||||
| --------------------- | ------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------ |
|
||||
| 原生二进制文件 | 伪代码、汇编、字符串、符号、调用和引用 | Hopper、Ghidra 或 IDA;[原生分析](../native-investigation.md) |
|
||||
| 离线 ELF 布局 | 节、段、原始符号/重定位信息及静态防护机制候选项 | Linux x64 上由调用方提供的 pwntools;[二进制诊断](../binary-diagnostics.md) |
|
||||
| Go 二进制文件 | 内嵌编译器版本、模块、替换项和构建设置 | 内置静态 ELF/PE/thin Mach-O 检查;无需安装 Go |
|
||||
| WASM 工件 | 验证、节、精确的导入/导出 WAT 形式及解码后的 WAT | 调用方提供的 WABT 1.0.42;`inspect_wasm_artifact` |
|
||||
| EVM 字节码 | 分派选择器、字节偏移、推断的参数和状态可变性 | Linux x64 上的本地原始字节/十六进制输入载体;[离线 EVM 指南](../evm-bytecode.md) |
|
||||
| Rust binaries | Embedded cargo-auditable dependency list | [Rust binaries](../rust-binaries.md) |
|
||||
| 已记录的 Linux 崩溃 | 原始 note 记录、每个已记录线程的寄存器/信号及可选的映射候选项 | Linux x64 上由调用方提供的 pwntools;可选的 GDB/pwndbg;[已记录崩溃](../recorded-crashes.md) |
|
||||
| 桌面安装目录 | 运行时候选、支持文件、组件标记和后续操作 | 有界的目录与文件头检查;[桌面应用](../desktop-applications.md) |
|
||||
| JavaScript / Electron | 模块、导入、source map、路由、IPC 和原生扩展关系 | Node.js 和 npm;[应用分析](../javascript-application-workflows.md) |
|
||||
| 网站 | 页面结构、脚本、网络观察结果和按请求获取的截图 | Chrome 系浏览器;[浏览器分析](../browser-observation.md) |
|
||||
| 已保存的网络捕获 | 请求、响应、可访问的载荷和来源位置 | HAR;原生 mitmproxy 捕获需要 Linux 上的 mitmdump;[捕获指南](../web-network-captures.md) |
|
||||
| 数据包捕获 | 端点、DNS、TLS SNI、协议分布和 HTTP 请求 | 调用方提供的 tshark 和 capinfos;[pcap 指南](../pcap-analysis.md) |
|
||||
| .NET 程序集 | 元数据、CIL 指令、声明的原生依赖和构建对比 | 静态检查;[托管代码指南](../managed-code-analysis.md) |
|
||||
| PyInstaller | CArchive, PYZ, Python bytecode | Static inspection; [Python packaged applications](../python-packaged-applications.md) |
|
||||
| Android APK | 清单声明、类、反编译的方法和引用 | Linux/macOS/Windows x64 上的无界面 JADX 和完整 JDK;[Android 指南](../android-analysis.md) |
|
||||
| Android 设备 | 设备、软件包、进程、日志、转储、屏幕截图以及 APK/文件传输 | 调用方提供的 adb(模拟器或实体设备);[ADB 指南](../adb-device-analysis.md) |
|
||||
| JEB 项目 | 引擎支持的项目单元、工件摘要和类型/方法的伪代码 | 由调用方启动、提供 MCP 的 JEB 客户端,通过 REA_JEB_MCP_URL 选择;[JEB 指南](../jeb-analysis.md) |
|
||||
| Android 资源 | 解码后的清单、字符串表、语言区域和版本信息 | 调用方提供的 Apktool 启动器;[资源指南](../apktool-resource-analysis.md) |
|
||||
| Flutter 应用 | Dart 快照哈希、按 ABI 的库摘要及引擎构建溯源 | 无(静态 APK 解析);[Flutter 指南](../flutter-analysis.md) |
|
||||
| 固件 | 区域、提取结果和转交原生分析的内容 | Linux 上的 Binwalk / Unblob;[固件指南](../firmware-analysis.md) |
|
||||
| 抓包文件 | 指定字段、重复值、过滤条件与解码规则 | Linux: TShark 4.x + prlimit; [抓包分析指南](../packet-captures.md) |
|
||||
| 录制的逻辑信号 | 解码注释、样本范围和诊断信息 | Linux: sigrok-cli 0.7.2 + prlimit; [信号记录指南](../signal-recordings.md) |
|
||||
| 软件包与资源 | 文件清单、摘要、plist、Apple bundle 结构和提取的资源 | [制品与 JavaScript 指南](../javascript-artifact-reconstruction.md)、[Apple 应用](../apple-application-analysis.md) |
|
||||
| 进程行为 | 终端输出、交互、退出和文件系统观察结果,以及运行对比 | 支持原生 PTY 的 Linux/macOS;[进程捕获](../process-capture.md) |
|
||||
| 目标 | REA 返回的内容 | 要求与指南 |
|
||||
| --------------------- | --------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------ |
|
||||
| 原生二进制文件 | 伪代码、汇编、字符串、符号、调用和引用 | Hopper、Ghidra 或 IDA;[原生分析](../native-investigation.md) |
|
||||
| 离线 ELF 布局 | 节、段、原始符号/重定位信息及静态防护机制候选项 | Linux x64 上由调用方提供的 pwntools;[二进制诊断](../binary-diagnostics.md) |
|
||||
| Go 二进制文件 | 内嵌编译器版本、模块、替换项和构建设置 | 内置静态 ELF/PE/thin Mach-O 检查;无需安装 Go |
|
||||
| WASM 工件 | 验证、节、精确的导入/导出 WAT 形式及解码后的 WAT | 调用方提供的 WABT 1.0.42;`inspect_wasm_artifact` |
|
||||
| EVM 字节码 | 分派选择器、字节偏移、推断的参数和状态可变性 | Linux x64 上的本地原始字节/十六进制输入载体;[离线 EVM 指南](../evm-bytecode.md) |
|
||||
| Rust binaries | Embedded cargo-auditable dependency list | [Rust binaries](../rust-binaries.md) |
|
||||
| ELF notes | PT_NOTE/SHT_NOTE owners, types, GNU build-id and ABI-tag fields | Built-in static ELF inspection; [ELF notes](../elf-notes.md) |
|
||||
| 已记录的 Linux 崩溃 | 原始 note 记录、每个已记录线程的寄存器/信号及可选的映射候选项 | Linux x64 上由调用方提供的 pwntools;可选的 GDB/pwndbg;[已记录崩溃](../recorded-crashes.md) |
|
||||
| 桌面安装目录 | 运行时候选、支持文件、组件标记和后续操作 | 有界的目录与文件头检查;[桌面应用](../desktop-applications.md) |
|
||||
| JavaScript / Electron | 模块、导入、source map、路由、IPC 和原生扩展关系 | Node.js 和 npm;[应用分析](../javascript-application-workflows.md) |
|
||||
| 网站 | 页面结构、脚本、网络观察结果和按请求获取的截图 | Chrome 系浏览器;[浏览器分析](../browser-observation.md) |
|
||||
| 已保存的网络捕获 | 请求、响应、可访问的载荷和来源位置 | HAR;原生 mitmproxy 捕获需要 Linux 上的 mitmdump;[捕获指南](../web-network-captures.md) |
|
||||
| 数据包捕获 | 端点、DNS、TLS SNI、协议分布和 HTTP 请求 | 调用方提供的 tshark 和 capinfos;[pcap 指南](../pcap-analysis.md) |
|
||||
| .NET 程序集 | 元数据、CIL 指令、声明的原生依赖和构建对比 | 静态检查;[托管代码指南](../managed-code-analysis.md) |
|
||||
| PyInstaller | CArchive, PYZ, Python bytecode | Static inspection; [Python packaged applications](../python-packaged-applications.md) |
|
||||
| Android APK | 清单声明、类、反编译的方法和引用 | Linux/macOS/Windows x64 上的无界面 JADX 和完整 JDK;[Android 指南](../android-analysis.md) |
|
||||
| Android 设备 | 设备、软件包、进程、日志、转储、屏幕截图以及 APK/文件传输 | 调用方提供的 adb(模拟器或实体设备);[ADB 指南](../adb-device-analysis.md) |
|
||||
| JEB 项目 | 引擎支持的项目单元、工件摘要和类型/方法的伪代码 | 由调用方启动、提供 MCP 的 JEB 客户端,通过 REA_JEB_MCP_URL 选择;[JEB 指南](../jeb-analysis.md) |
|
||||
| Android 资源 | 解码后的清单、字符串表、语言区域和版本信息 | 调用方提供的 Apktool 启动器;[资源指南](../apktool-resource-analysis.md) |
|
||||
| Flutter 应用 | Dart 快照哈希、按 ABI 的库摘要及引擎构建溯源 | 无(静态 APK 解析);[Flutter 指南](../flutter-analysis.md) |
|
||||
| 固件 | 区域、提取结果和转交原生分析的内容 | Linux 上的 Binwalk / Unblob;[固件指南](../firmware-analysis.md) |
|
||||
| 抓包文件 | 指定字段、重复值、过滤条件与解码规则 | Linux: TShark 4.x + prlimit; [抓包分析指南](../packet-captures.md) |
|
||||
| 录制的逻辑信号 | 解码注释、样本范围和诊断信息 | Linux: sigrok-cli 0.7.2 + prlimit; [信号记录指南](../signal-recordings.md) |
|
||||
| 软件包与资源 | 文件清单、摘要、plist、Apple bundle 结构和提取的资源 | [制品与 JavaScript 指南](../javascript-artifact-reconstruction.md)、[Apple 应用](../apple-application-analysis.md) |
|
||||
| 进程行为 | 终端输出、交互、退出和文件系统观察结果,以及运行对比 | 支持原生 PTY 的 Linux/macOS;[进程捕获](../process-capture.md) |
|
||||
|
||||
静态 JavaScript 和 .NET 检查读取提供的文件,不运行应用。运行时捕获会以你的用户权限运行目标或与之交互;各运行时指南说明了具体影响。
|
||||
|
||||
|
||||
@@ -250,6 +250,13 @@ export const providerCatalog = (sources) => {
|
||||
({ name }) => name === "inspect_pe_resources",
|
||||
),
|
||||
},
|
||||
{
|
||||
identity: sources.artifactProviders.ELF_NOTES_PROVIDER,
|
||||
contracts:
|
||||
sources.binaryDiagnosticsContracts.BINARY_DIAGNOSTICS_TOOL_CONTRACTS.filter(
|
||||
({ name }) => name === "inspect_elf_notes",
|
||||
),
|
||||
},
|
||||
{
|
||||
identity: sources.artifactProviders.BYTE_ENTROPY_PROVIDER,
|
||||
contracts:
|
||||
|
||||
@@ -500,6 +500,8 @@ const workflowAvailabilityFor = ({
|
||||
};
|
||||
if (name === "inspect_pe_resources")
|
||||
return { reason: "available", remediation: null };
|
||||
if (name === "inspect_elf_notes")
|
||||
return { reason: "available", remediation: null };
|
||||
if (name === "inspect_evm_interface") {
|
||||
if (policy.evmInterfaceEnabled !== true)
|
||||
return {
|
||||
|
||||
@@ -48,6 +48,13 @@ export const PE_RESOURCES_PROVIDER = Object.freeze({
|
||||
version: "1",
|
||||
} as const);
|
||||
|
||||
/** Provider identity for portable, execution-free ELF note-table decoding. */
|
||||
export const ELF_NOTES_PROVIDER = Object.freeze({
|
||||
id: "rea-elf-notes",
|
||||
name: "REA ELF note parser",
|
||||
version: "1",
|
||||
} as const);
|
||||
|
||||
/** Provider identity for execution-free Shannon byte-entropy profiling. */
|
||||
export const BYTE_ENTROPY_PROVIDER = Object.freeze({
|
||||
id: "byte-entropy",
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
import type {
|
||||
ExecutionOptions,
|
||||
ProviderIdentity,
|
||||
} from "../AnalysisProvider.js";
|
||||
import type { AnalysisError } from "../../domain/analysisErrorBase.js";
|
||||
import type {
|
||||
InspectElfNotesInput,
|
||||
ElfNotes,
|
||||
} from "../../domain/native/elfNotes.js";
|
||||
import type { Result } from "../../domain/result.js";
|
||||
|
||||
/** Stable read and ELF note-table decoding, independent of a disassembler session. */
|
||||
export interface ElfNotesPort {
|
||||
readonly identity: ProviderIdentity;
|
||||
close?(): Promise<void>;
|
||||
inspect(
|
||||
input: InspectElfNotesInput,
|
||||
options?: ExecutionOptions,
|
||||
): Promise<Result<ElfNotes, AnalysisError>>;
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
import { expect, it } from "vitest";
|
||||
import { ElfNotesService } from "./ElfNotesService.js";
|
||||
import { ELF_NOTES_PROVIDER } from "../InvestigationProviders.js";
|
||||
import type { ElfNotes } from "../../domain/native/elfNotes.js";
|
||||
import { ok } from "../../domain/result.js";
|
||||
import {
|
||||
AnalysisCancelledError,
|
||||
AnalysisInputError,
|
||||
} from "../../domain/analysisErrorCore.js";
|
||||
|
||||
const identity = ELF_NOTES_PROVIDER;
|
||||
const report: ElfNotes = {
|
||||
artifact: {
|
||||
path: "/artifacts/app",
|
||||
sha256: "a".repeat(64),
|
||||
bytes: 512,
|
||||
},
|
||||
format: "elf64",
|
||||
byte_order: "little",
|
||||
machine: 62,
|
||||
type: 3,
|
||||
type_name: "shared",
|
||||
tables: [],
|
||||
entries: [],
|
||||
coverage: { status: "complete", examined_tables: 0, examined_entries: 0 },
|
||||
limitations: ["No PT_NOTE program header or SHT_NOTE section was declared."],
|
||||
};
|
||||
|
||||
it("rejects a relative path and extra arguments before reading", async () => {
|
||||
let calls = 0;
|
||||
const service = new ElfNotesService({
|
||||
identity,
|
||||
inspect: () => {
|
||||
calls += 1;
|
||||
return Promise.resolve(ok(report));
|
||||
},
|
||||
});
|
||||
const relative = await service.inspect({ path: "app" });
|
||||
const extra = await service.inspect({
|
||||
path: report.artifact.path,
|
||||
extra: true,
|
||||
});
|
||||
expect(relative.ok).toBe(false);
|
||||
expect(extra.ok).toBe(false);
|
||||
if (!relative.ok) expect(relative.error).toBeInstanceOf(AnalysisInputError);
|
||||
if (!extra.ok) expect(extra.error).toBeInstanceOf(AnalysisInputError);
|
||||
expect(calls).toBe(0);
|
||||
});
|
||||
|
||||
it("records the selected ELF and an absent note table", async () => {
|
||||
const result = await new ElfNotesService({
|
||||
identity,
|
||||
inspect: () => Promise.resolve(ok(report)),
|
||||
}).inspect({ path: report.artifact.path });
|
||||
expect(result.ok).toBe(true);
|
||||
if (!result.ok) throw result.error;
|
||||
expect(result.value).toMatchObject({
|
||||
confidence: "observed",
|
||||
subject: { format: "elf", architecture: "x86_64" },
|
||||
normalized_result: report,
|
||||
});
|
||||
});
|
||||
|
||||
it("discards an observation if the caller cancels while the provider completes", async () => {
|
||||
const controller = new AbortController();
|
||||
const service = new ElfNotesService({
|
||||
identity,
|
||||
inspect: () => {
|
||||
controller.abort();
|
||||
return Promise.resolve(ok(report));
|
||||
},
|
||||
});
|
||||
const result = await service.inspect(
|
||||
{ path: report.artifact.path },
|
||||
{ signal: controller.signal },
|
||||
);
|
||||
expect(result.ok).toBe(false);
|
||||
if (!result.ok) expect(result.error).toBeInstanceOf(AnalysisCancelledError);
|
||||
});
|
||||
@@ -0,0 +1,127 @@
|
||||
import { isAbsolute } from "node:path";
|
||||
import type { ExecutionOptions } from "../AnalysisProvider.js";
|
||||
import type { ElfNotesPort } from "./ElfNotesPort.js";
|
||||
import type { AnalysisError } from "../../domain/analysisErrorBase.js";
|
||||
import {
|
||||
AnalysisCancelledError,
|
||||
AnalysisInputError,
|
||||
AnalysisOutputError,
|
||||
} from "../../domain/analysisErrorCore.js";
|
||||
import { createEvidence, type Evidence } from "../../domain/evidence.js";
|
||||
import {
|
||||
inspectElfNotesInputSchema,
|
||||
elfNotesSchema,
|
||||
type ElfNotes,
|
||||
} from "../../domain/native/elfNotes.js";
|
||||
import { analysisInputErrorFromIssues } from "../../domain/inputIssueProjection.js";
|
||||
import { BINARY_ARCHITECTURES } from "../../domain/binaryTargetTypes.js";
|
||||
import { elfArchitecture } from "../../domain/elfFixedHeader.js";
|
||||
import { err, type Result } from "../../domain/result.js";
|
||||
import { completeProviderEvidence } from "../ProviderOperationExecution.js";
|
||||
|
||||
const OPERATION = "inspect_elf_notes";
|
||||
|
||||
/** Shared caller validation, output parsing and Evidence provenance for CLI/MCP. */
|
||||
export class ElfNotesService {
|
||||
constructor(readonly provider: ElfNotesPort) {}
|
||||
|
||||
close(): Promise<void> {
|
||||
return this.provider.close?.() ?? Promise.resolve();
|
||||
}
|
||||
|
||||
async inspect(
|
||||
rawInput: unknown,
|
||||
options?: ExecutionOptions,
|
||||
): Promise<Result<Evidence, AnalysisError>> {
|
||||
if (options?.signal?.aborted)
|
||||
return err(new AnalysisCancelledError(OPERATION));
|
||||
const input = inspectElfNotesInputSchema.safeParse(rawInput);
|
||||
if (!input.success)
|
||||
return err(
|
||||
analysisInputErrorFromIssues(OPERATION, input.error.issues, rawInput, {
|
||||
cause: input.error,
|
||||
}),
|
||||
);
|
||||
if (!isAbsolute(input.data.path))
|
||||
return err(
|
||||
new AnalysisInputError(OPERATION, undefined, [
|
||||
{
|
||||
path: ["path"],
|
||||
reason: "invalid_format",
|
||||
message: "Expected an absolute filesystem path on this host.",
|
||||
},
|
||||
]),
|
||||
);
|
||||
const result = await this.provider.inspect(input.data, options);
|
||||
if (!result.ok) return result;
|
||||
const report = elfNotesSchema.safeParse(result.value);
|
||||
if (
|
||||
!report.success ||
|
||||
report.data.artifact.path !== input.data.path ||
|
||||
!validReport(
|
||||
report.data,
|
||||
input.data.max_entries,
|
||||
input.data.max_file_bytes,
|
||||
)
|
||||
)
|
||||
return err(
|
||||
new AnalysisOutputError(
|
||||
OPERATION,
|
||||
"ELF note provider returned malformed output or changed artifact identity.",
|
||||
),
|
||||
);
|
||||
const value = report.data;
|
||||
const architecture = BINARY_ARCHITECTURES.find(
|
||||
(supported) => supported === elfArchitecture(value.machine),
|
||||
);
|
||||
const locations = [
|
||||
{ kind: "artifact-path" as const, path: value.artifact.path },
|
||||
...value.tables.map((table) => ({
|
||||
kind: "file-offset-range" as const,
|
||||
start: table.location.offset,
|
||||
end: table.location.offset + table.location.bytes,
|
||||
})),
|
||||
];
|
||||
return completeProviderEvidence(
|
||||
createEvidence(
|
||||
{
|
||||
path: value.artifact.path,
|
||||
format: "elf",
|
||||
sha256: value.artifact.sha256,
|
||||
...(architecture === undefined ? {} : { architecture }),
|
||||
},
|
||||
this.provider.identity,
|
||||
{
|
||||
operation: OPERATION,
|
||||
parameters: input.data,
|
||||
result: value,
|
||||
confidence: "observed",
|
||||
limitations: value.limitations,
|
||||
locations,
|
||||
},
|
||||
),
|
||||
options?.signal,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const validReport = (
|
||||
report: ElfNotes,
|
||||
maxEntries: number,
|
||||
maxBytes: number,
|
||||
): boolean => {
|
||||
const inside = ({ offset, bytes }: { offset: number; bytes: number }) =>
|
||||
offset <= report.artifact.bytes - bytes;
|
||||
if (
|
||||
report.artifact.bytes > maxBytes ||
|
||||
report.coverage.examined_entries > maxEntries ||
|
||||
report.entries.length > maxEntries
|
||||
)
|
||||
return false;
|
||||
return (
|
||||
report.tables.every((table) => inside(table.location)) &&
|
||||
report.entries.every(
|
||||
(entry) => inside(entry.location) && inside(entry.descriptor.location),
|
||||
)
|
||||
);
|
||||
};
|
||||
@@ -4,6 +4,7 @@ import {
|
||||
createByteEntropyService,
|
||||
createRecordedCrashService,
|
||||
createBinaryLayoutService,
|
||||
createElfNotesService,
|
||||
createPeResourcesService,
|
||||
} from "../composition/binaryDiagnostics.js";
|
||||
import { CLI_COMMANDS } from "../cliCommandNames.js";
|
||||
@@ -14,6 +15,7 @@ import { withCommandCancellation } from "./commandCancellation.js";
|
||||
import { inspectBinaryLayoutInputSchema } from "../domain/native/binaryLayout.js";
|
||||
import { inspectRecordedCrashInputSchema } from "../domain/native/recordedCrash.js";
|
||||
import { inspectPeResourcesInputSchema } from "../domain/native/peResources.js";
|
||||
import { inspectElfNotesInputSchema } from "../domain/native/elfNotes.js";
|
||||
import { projectAnalysisError } from "../domain/analysisErrorProjection.js";
|
||||
import { finishAnalysisCommand } from "./finishAnalysisCommand.js";
|
||||
import { absoluteCallerPath } from "../filesystem/callerPath.js";
|
||||
@@ -24,6 +26,39 @@ export const registerBinaryDiagnosticsCommands = (
|
||||
logger: Logger,
|
||||
environment: Readonly<NodeJS.ProcessEnv>,
|
||||
): void => {
|
||||
cli.command(CLI_COMMANDS.inspectElfNotes, {
|
||||
description:
|
||||
"Inspect ELF PT_NOTE and SHT_NOTE identities without executing the image",
|
||||
args: z.object({ path: z.string().describe("Local ELF32/ELF64 image") }),
|
||||
options: z.object({
|
||||
maxFileBytes: inspectElfNotesInputSchema.shape.max_file_bytes,
|
||||
maxEntries: inspectElfNotesInputSchema.shape.max_entries,
|
||||
}),
|
||||
run: ({ args, options }) =>
|
||||
withCommandCancellation(async (signal) => {
|
||||
const elf = createElfNotesService();
|
||||
return await logCliCommand(
|
||||
logger,
|
||||
CLI_COMMANDS.inspectElfNotes,
|
||||
async () =>
|
||||
finishAnalysisCommand(
|
||||
() =>
|
||||
elf.inspect(
|
||||
{
|
||||
path: absoluteCallerPath(args.path),
|
||||
max_file_bytes: options.maxFileBytes,
|
||||
max_entries: options.maxEntries,
|
||||
},
|
||||
{ signal },
|
||||
),
|
||||
elf,
|
||||
"rea-elf-notes",
|
||||
"inspect_elf_notes",
|
||||
signal,
|
||||
),
|
||||
);
|
||||
}),
|
||||
});
|
||||
const entropy = createByteEntropyService();
|
||||
cli.command(CLI_COMMANDS.inspectByteEntropy, {
|
||||
description:
|
||||
|
||||
@@ -66,6 +66,7 @@ export const CLI_COMMANDS = Object.freeze({
|
||||
inspectGpuCodeObjects: "inspect-gpu-code-objects",
|
||||
inspectRustBinary: "inspect-rust-binary",
|
||||
inspectPeResources: "inspect-pe-resources",
|
||||
inspectElfNotes: "inspect-elf-notes",
|
||||
inspectAnalysisView: "inspect-analysis-view",
|
||||
listFridaDevices: "list-frida-devices",
|
||||
listFridaProcesses: "list-frida-processes",
|
||||
|
||||
@@ -5,12 +5,18 @@ import { BinaryLayoutService } from "../application/binaryDiagnostics/BinaryLayo
|
||||
import { PwntoolsLayoutProvider } from "../native/pwntools/PwntoolsLayoutProvider.js";
|
||||
import { PeResourcesService } from "../application/binaryDiagnostics/PeResourcesService.js";
|
||||
import { PeResourcesProvider } from "../native/pe/PeResourcesProvider.js";
|
||||
import { ElfNotesService } from "../application/binaryDiagnostics/ElfNotesService.js";
|
||||
import { ElfNotesProvider } from "../native/elf/ElfNotesProvider.js";
|
||||
import type { ArtifactResourceScope } from "../artifacts/ArtifactResourceScope.js";
|
||||
|
||||
/** Portable PE inspection with no engine, installation or target acquisition. */
|
||||
export const createPeResourcesService = (): PeResourcesService =>
|
||||
new PeResourcesService(new PeResourcesProvider());
|
||||
|
||||
/** Portable ELF note-table inspection with no engine or package catalog. */
|
||||
export const createElfNotesService = (): ElfNotesService =>
|
||||
new ElfNotesService(new ElfNotesProvider());
|
||||
|
||||
/** Compose the offline adapter without import-time I/O or dependency acquisition. */
|
||||
export const createBinaryLayoutService = (
|
||||
environment: Readonly<NodeJS.ProcessEnv> = process.env,
|
||||
|
||||
@@ -21,6 +21,10 @@ import {
|
||||
inspectPeResourcesInputSchema,
|
||||
peResourcesSchema,
|
||||
} from "../../domain/native/peResources.js";
|
||||
import {
|
||||
inspectElfNotesInputSchema,
|
||||
elfNotesSchema,
|
||||
} from "../../domain/native/elfNotes.js";
|
||||
|
||||
/** Caller-selected delivery of completed binary layout Evidence. */
|
||||
export const inspectBinaryLayoutRequestSchema =
|
||||
@@ -36,6 +40,21 @@ export const inspectByteEntropyRequestSchema = publicInputStrictObject(
|
||||
|
||||
/** Offline binary diagnostics are independent of macOS process/UI providers. */
|
||||
export const BINARY_DIAGNOSTICS_TOOL_CONTRACTS = [
|
||||
{
|
||||
name: "inspect_elf_notes",
|
||||
...toolContractMetadata("inspect_elf_notes"),
|
||||
kind: "native-provider",
|
||||
description:
|
||||
"Inspect one explicit local ELF32/ELF64 image's PT_NOTE program headers and SHT_NOTE sections without executing it or opening a disassembler. Returns note-table file ranges, owners, types, descriptor SHA-256 and hex previews, plus GNU build-id, ABI-tag and gold-version fields when those descriptors match. A missing table is observed absence of those declarations, not proof the image never carried notes in another container. Reads a stable regular file; malformed ELF headers, truncated notes and exhausted budgets return no partial success. Portable on supported Node hosts; no debug package or catalog is required.",
|
||||
inputSchema: inspectElfNotesInputSchema,
|
||||
outputSchema: evidenceResultOf(elfNotesSchema),
|
||||
examples: [
|
||||
{
|
||||
title: "Inspect ELF GNU build-id and ABI-tag notes",
|
||||
input: { path: "/artifacts/program" },
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: "inspect_byte_entropy",
|
||||
...toolContractMetadata("inspect_byte_entropy"),
|
||||
|
||||
@@ -27,6 +27,7 @@ const EXECUTION_WORDING_EXCEPTIONS: Readonly<Record<string, string>> = {
|
||||
resolve_native_call_targets: "states that it does not execute code",
|
||||
inspect_byte_entropy: "states that it profiles the file without execution",
|
||||
inspect_pe_resources: "states that it does not execute the image",
|
||||
inspect_elf_notes: "states that it does not execute the image",
|
||||
inspect_desktop_application:
|
||||
"states that directory triage performs no target execution",
|
||||
inspect_go_binary: "reads embedded metadata without executing the target",
|
||||
|
||||
@@ -253,6 +253,7 @@ export const TOOL_EFFECTS: Readonly<Record<string, ToolEffects>> = {
|
||||
}),
|
||||
inspect_byte_entropy: evidence,
|
||||
inspect_pe_resources: evidence,
|
||||
inspect_elf_notes: evidence,
|
||||
inspect_analysis_view: evidence,
|
||||
inspect_firmware_regions: effects({
|
||||
mutatesSession: true,
|
||||
|
||||
@@ -26,6 +26,7 @@ export class ArtifactOperationError extends AnalysisError {
|
||||
| "inspect_artifact"
|
||||
| "inspect_desktop_application"
|
||||
| "inspect_pe_resources"
|
||||
| "inspect_elf_notes"
|
||||
| "extract_artifact"
|
||||
| "decode_interface_builder"
|
||||
| "inspect_asset_catalog"
|
||||
|
||||
@@ -70,7 +70,7 @@ export const readElfFixedHeader = (
|
||||
});
|
||||
};
|
||||
|
||||
const elfArchitecture = (
|
||||
export const elfArchitecture = (
|
||||
machine: number,
|
||||
): Exclude<BinaryArchitecture, RawOnlyArchitecture> | undefined => {
|
||||
switch (machine) {
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
import { expect, it } from "vitest";
|
||||
import {
|
||||
elfObjectTypeName,
|
||||
gnuNoteTypeName,
|
||||
inspectElfNotesInputSchema,
|
||||
elfNotesSchema,
|
||||
} from "./elfNotes.js";
|
||||
|
||||
it("maps ELF object and GNU note types without inventing unknown names", () => {
|
||||
expect(elfObjectTypeName(1)).toBe("relocatable");
|
||||
expect(elfObjectTypeName(2)).toBe("executable");
|
||||
expect(elfObjectTypeName(3)).toBe("shared");
|
||||
expect(elfObjectTypeName(4)).toBe("core");
|
||||
expect(elfObjectTypeName(99)).toBe("unknown");
|
||||
expect(gnuNoteTypeName(1)).toBe("gnu_abi_tag");
|
||||
expect(gnuNoteTypeName(3)).toBe("gnu_build_id");
|
||||
expect(gnuNoteTypeName(99)).toBe("unknown");
|
||||
});
|
||||
|
||||
it("rejects extra caller arguments and admits an empty note inventory", () => {
|
||||
expect(
|
||||
inspectElfNotesInputSchema.safeParse({
|
||||
path: "/artifacts/app",
|
||||
extra: true,
|
||||
}).success,
|
||||
).toBe(false);
|
||||
const report = elfNotesSchema.parse({
|
||||
artifact: {
|
||||
path: "/artifacts/app",
|
||||
sha256: "a".repeat(64),
|
||||
bytes: 512,
|
||||
},
|
||||
format: "elf64",
|
||||
byte_order: "little",
|
||||
machine: 62,
|
||||
type: 3,
|
||||
type_name: "shared",
|
||||
tables: [],
|
||||
entries: [],
|
||||
coverage: { status: "complete", examined_tables: 0, examined_entries: 0 },
|
||||
limitations: [
|
||||
"No PT_NOTE program header or SHT_NOTE section was declared.",
|
||||
],
|
||||
});
|
||||
expect(report.tables).toEqual([]);
|
||||
expect(report.entries).toEqual([]);
|
||||
});
|
||||
@@ -0,0 +1,161 @@
|
||||
import { z } from "zod";
|
||||
|
||||
import { digestSchema } from "../digests.js";
|
||||
import { localPathStringSchema } from "../localPath.js";
|
||||
|
||||
const uint = z.number().int().min(0).max(0xffffffff);
|
||||
const range = z.strictObject({ offset: uint, bytes: uint });
|
||||
|
||||
/** Hard ceiling on the caller-selected input read budget. */
|
||||
export const MAX_INSPECT_ELF_NOTE_FILE_BYTES = 536870912;
|
||||
/** Hard ceiling on decoded note entries across every declared table. */
|
||||
export const MAX_INSPECT_ELF_NOTE_ENTRIES = 256;
|
||||
|
||||
export const ELF_NOTE_TYPE_NAMES = [
|
||||
"gnu_abi_tag",
|
||||
"gnu_hwcap",
|
||||
"gnu_build_id",
|
||||
"gnu_gold_version",
|
||||
"gnu_property",
|
||||
"unknown",
|
||||
] as const;
|
||||
export type ElfNoteTypeName = (typeof ELF_NOTE_TYPE_NAMES)[number];
|
||||
|
||||
export const ELF_OBJECT_TYPE_NAMES = [
|
||||
"relocatable",
|
||||
"executable",
|
||||
"shared",
|
||||
"core",
|
||||
"unknown",
|
||||
] as const;
|
||||
export type ElfObjectTypeName = (typeof ELF_OBJECT_TYPE_NAMES)[number];
|
||||
|
||||
/** Map e_type without inventing names for unknown object kinds. */
|
||||
export const elfObjectTypeName = (type: number): ElfObjectTypeName => {
|
||||
switch (type) {
|
||||
case 1:
|
||||
return "relocatable";
|
||||
case 2:
|
||||
return "executable";
|
||||
case 3:
|
||||
return "shared";
|
||||
case 4:
|
||||
return "core";
|
||||
default:
|
||||
return "unknown";
|
||||
}
|
||||
};
|
||||
|
||||
/** Map GNU n_type when the owner is GNU; other owners stay unknown. */
|
||||
export const gnuNoteTypeName = (type: number): ElfNoteTypeName => {
|
||||
switch (type) {
|
||||
case 1:
|
||||
return "gnu_abi_tag";
|
||||
case 2:
|
||||
return "gnu_hwcap";
|
||||
case 3:
|
||||
return "gnu_build_id";
|
||||
case 4:
|
||||
return "gnu_gold_version";
|
||||
case 5:
|
||||
return "gnu_property";
|
||||
default:
|
||||
return "unknown";
|
||||
}
|
||||
};
|
||||
|
||||
/** Complete bounded inspection of ELF PT_NOTE and SHT_NOTE tables. */
|
||||
export const inspectElfNotesInputSchema = z.strictObject({
|
||||
path: localPathStringSchema.describe(
|
||||
"Absolute filesystem path to an ELF image",
|
||||
),
|
||||
max_file_bytes: z
|
||||
.number()
|
||||
.int()
|
||||
.min(64)
|
||||
.max(MAX_INSPECT_ELF_NOTE_FILE_BYTES)
|
||||
.default(67108864)
|
||||
.describe("Maximum stable artifact bytes to read (default 64 MiB)"),
|
||||
max_entries: z
|
||||
.number()
|
||||
.int()
|
||||
.min(1)
|
||||
.max(MAX_INSPECT_ELF_NOTE_ENTRIES)
|
||||
.default(32)
|
||||
.describe(
|
||||
"Maximum note entries to examine across every table; exceeding the budget fails without partial success",
|
||||
),
|
||||
});
|
||||
|
||||
const gnuAbiTagSchema = z.strictObject({
|
||||
os: uint,
|
||||
os_name: z.string().nullable(),
|
||||
major: uint,
|
||||
minor: uint,
|
||||
subminor: uint,
|
||||
});
|
||||
|
||||
const noteEntrySchema = z.strictObject({
|
||||
index: uint,
|
||||
table_kind: z.enum(["program", "section"]),
|
||||
table_index: uint,
|
||||
location: range,
|
||||
namesz: uint,
|
||||
descsz: uint,
|
||||
type: uint,
|
||||
type_name: z.enum(ELF_NOTE_TYPE_NAMES),
|
||||
owner: z.string(),
|
||||
owner_bytes_hex: z.string().regex(/^[0-9a-f]*$/),
|
||||
descriptor: z.strictObject({
|
||||
location: range,
|
||||
bytes: uint,
|
||||
sha256: digestSchema,
|
||||
hex: z
|
||||
.string()
|
||||
.regex(/^[0-9a-f]*$/)
|
||||
.nullable(),
|
||||
}),
|
||||
gnu: z
|
||||
.strictObject({
|
||||
build_id_hex: z
|
||||
.string()
|
||||
.regex(/^[0-9a-f]*$/)
|
||||
.nullable(),
|
||||
abi_tag: gnuAbiTagSchema.nullable(),
|
||||
gold_version: z.string().nullable(),
|
||||
})
|
||||
.nullable(),
|
||||
});
|
||||
|
||||
/** Observed ELF note tables; absence is an empty list, not proof notes never existed. */
|
||||
export const elfNotesSchema = z.strictObject({
|
||||
artifact: z.strictObject({
|
||||
path: z.string().min(1),
|
||||
sha256: digestSchema,
|
||||
bytes: uint,
|
||||
}),
|
||||
format: z.enum(["elf32", "elf64"]),
|
||||
byte_order: z.enum(["little", "big"]),
|
||||
machine: uint,
|
||||
type: uint,
|
||||
type_name: z.enum(ELF_OBJECT_TYPE_NAMES),
|
||||
tables: z.array(
|
||||
z.strictObject({
|
||||
kind: z.enum(["program", "section"]),
|
||||
index: uint,
|
||||
name: z.string().nullable(),
|
||||
location: range,
|
||||
alignment: uint,
|
||||
}),
|
||||
),
|
||||
entries: z.array(noteEntrySchema),
|
||||
coverage: z.strictObject({
|
||||
status: z.literal("complete"),
|
||||
examined_tables: uint,
|
||||
examined_entries: uint,
|
||||
}),
|
||||
limitations: z.array(z.string()),
|
||||
});
|
||||
|
||||
export type ElfNotes = z.infer<typeof elfNotesSchema>;
|
||||
export type InspectElfNotesInput = z.infer<typeof inspectElfNotesInputSchema>;
|
||||
@@ -0,0 +1,406 @@
|
||||
import { createHash } from "node:crypto";
|
||||
import { readFileSync } from "node:fs";
|
||||
import { expect, it } from "vitest";
|
||||
import { inspectElfNotesInputSchema } from "../../domain/native/elfNotes.js";
|
||||
import { ArtifactReaderFailure } from "../../artifacts/ArtifactReader.js";
|
||||
import {
|
||||
elfNote,
|
||||
elfNoteFixture,
|
||||
elfNoteTablePastEnd,
|
||||
gnuAbiTagNote,
|
||||
gnuBuildIdNote,
|
||||
gnuPropertyNote,
|
||||
} from "./ElfNotes.fixture.js";
|
||||
import { parseElfNotes } from "./ElfNoteParser.js";
|
||||
|
||||
const defaults = inspectElfNotesInputSchema.parse({
|
||||
path: "/artifacts/app",
|
||||
});
|
||||
|
||||
const parse = (bytes: Buffer, input: typeof defaults = defaults) =>
|
||||
parseElfNotes(
|
||||
bytes,
|
||||
{
|
||||
path: input.path,
|
||||
sha256: createHash("sha256").update(bytes).digest("hex"),
|
||||
bytes: bytes.length,
|
||||
},
|
||||
input,
|
||||
);
|
||||
|
||||
const elf64 = (
|
||||
fields: {
|
||||
readonly phoff?: number;
|
||||
readonly shoff?: number;
|
||||
readonly phnum?: number;
|
||||
readonly shnum?: number;
|
||||
readonly phentsize?: number;
|
||||
readonly shentsize?: number;
|
||||
},
|
||||
tail?: Buffer,
|
||||
): Buffer => {
|
||||
const header = Buffer.alloc(64);
|
||||
header.write("\x7fELF", 0, "latin1");
|
||||
header[4] = 2;
|
||||
header[5] = 1;
|
||||
header[6] = 1;
|
||||
header.writeUInt16LE(2, 16);
|
||||
header.writeUInt16LE(62, 18);
|
||||
header.writeUInt32LE(1, 20);
|
||||
header.writeBigUInt64LE(BigInt(fields.phoff ?? 0), 32);
|
||||
header.writeBigUInt64LE(BigInt(fields.shoff ?? 0), 40);
|
||||
header.writeUInt16LE(64, 52);
|
||||
header.writeUInt16LE(fields.phentsize ?? 56, 54);
|
||||
header.writeUInt16LE(fields.phnum ?? 0, 56);
|
||||
header.writeUInt16LE(fields.shentsize ?? 64, 58);
|
||||
header.writeUInt16LE(fields.shnum ?? 0, 60);
|
||||
return tail === undefined ? header : Buffer.concat([header, tail]);
|
||||
};
|
||||
|
||||
it("reads GNU build-id and ABI-tag notes from ELF32 and ELF64", () => {
|
||||
for (const bits of [32, 64] as const) {
|
||||
const bytes = elfNoteFixture({ bits, ownership: "program" });
|
||||
const report = parse(bytes);
|
||||
expect(report.format).toBe(bits === 64 ? "elf64" : "elf32");
|
||||
expect(report.byte_order).toBe("little");
|
||||
expect(report.tables).toHaveLength(1);
|
||||
expect(report.tables[0]?.kind).toBe("program");
|
||||
const buildId = report.entries.find(
|
||||
(entry) => entry.type_name === "gnu_build_id",
|
||||
);
|
||||
const abi = report.entries.find(
|
||||
(entry) => entry.type_name === "gnu_abi_tag",
|
||||
);
|
||||
expect(buildId?.gnu?.build_id_hex).toBe(
|
||||
"0123456789abcdef0123456789abcdef01234567",
|
||||
);
|
||||
expect(abi?.gnu?.abi_tag).toMatchObject({
|
||||
os: 0,
|
||||
os_name: "linux",
|
||||
major: 3,
|
||||
minor: 2,
|
||||
subminor: 0,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
it("reads the same notes from an SHT_NOTE section including its name", () => {
|
||||
const report = parse(elfNoteFixture({ ownership: "section" }));
|
||||
expect(report.tables).toEqual([
|
||||
expect.objectContaining({
|
||||
kind: "section",
|
||||
name: ".note.gnu.build-id",
|
||||
}),
|
||||
]);
|
||||
expect(
|
||||
report.entries.some((entry) => entry.type_name === "gnu_build_id"),
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
it("reports independent PT_NOTE and SHT_NOTE declarations of the same bytes", () => {
|
||||
const report = parse(elfNoteFixture({ ownership: "both" }));
|
||||
expect(report.tables.map((table) => table.kind).sort()).toEqual([
|
||||
"program",
|
||||
"section",
|
||||
]);
|
||||
expect(
|
||||
report.entries.filter((entry) => entry.type_name === "gnu_build_id"),
|
||||
).toHaveLength(2);
|
||||
});
|
||||
|
||||
it("reports missing note tables without claiming notes never existed", () => {
|
||||
const report = parse(elfNoteFixture({ ownership: "none" }));
|
||||
expect(report.tables).toEqual([]);
|
||||
expect(report.entries).toEqual([]);
|
||||
expect(report.limitations[0]).toMatch(/No PT_NOTE/u);
|
||||
});
|
||||
|
||||
it("fails a truncated note payload without partial success", () => {
|
||||
const notes = gnuBuildIdNote();
|
||||
notes.writeUInt32LE(64, 0);
|
||||
const bytes = elfNoteFixture({ ownership: "program", notes });
|
||||
expect(() => parse(bytes)).toThrow(ArtifactReaderFailure);
|
||||
});
|
||||
|
||||
it("keeps non-GNU owners as unknown types with descriptor hashes", () => {
|
||||
const payload = Buffer.from("hello");
|
||||
const notes = elfNote("CORE", 1, payload);
|
||||
const report = parse(elfNoteFixture({ ownership: "program", notes }));
|
||||
expect(report.entries[0]).toMatchObject({
|
||||
owner: "CORE",
|
||||
type_name: "unknown",
|
||||
gnu: null,
|
||||
});
|
||||
expect(report.entries[0]?.descriptor.sha256).toBe(
|
||||
createHash("sha256").update(payload).digest("hex"),
|
||||
);
|
||||
});
|
||||
|
||||
it("reads a big-endian ELF32 GNU build-id", () => {
|
||||
const notes = gnuBuildIdNote(undefined, false);
|
||||
const report = parse(
|
||||
elfNoteFixture({
|
||||
bits: 32,
|
||||
little: false,
|
||||
machine: 20,
|
||||
ownership: "program",
|
||||
notes,
|
||||
}),
|
||||
);
|
||||
expect(report.byte_order).toBe("big");
|
||||
expect(report.machine).toBe(20);
|
||||
expect(report.entries[0]?.gnu?.build_id_hex).toBe(
|
||||
"0123456789abcdef0123456789abcdef01234567",
|
||||
);
|
||||
});
|
||||
|
||||
it("reads an 8-aligned GNU property note from a PT_NOTE program header", () => {
|
||||
const notes = gnuPropertyNote();
|
||||
expect(notes.length).toBe(48);
|
||||
const report = parse(
|
||||
elfNoteFixture({ ownership: "program", notes, alignment: 8 }),
|
||||
);
|
||||
expect(report.tables).toEqual([
|
||||
expect.objectContaining({ kind: "program", alignment: 8 }),
|
||||
]);
|
||||
expect(report.entries).toHaveLength(1);
|
||||
expect(report.entries[0]).toMatchObject({
|
||||
owner: "GNU",
|
||||
type: 5,
|
||||
type_name: "gnu_property",
|
||||
namesz: 4,
|
||||
descsz: 32,
|
||||
});
|
||||
expect(report.entries[0]?.descriptor.location.offset).toBe(
|
||||
(report.entries[0]?.location.offset ?? 0) + 16,
|
||||
);
|
||||
});
|
||||
|
||||
it("reads an 8-aligned GNU property note from an SHT_NOTE section", () => {
|
||||
const report = parse(
|
||||
elfNoteFixture({
|
||||
ownership: "section",
|
||||
notes: gnuPropertyNote(),
|
||||
alignment: 8,
|
||||
sectionName: ".note.gnu.property",
|
||||
}),
|
||||
);
|
||||
expect(report.tables).toEqual([
|
||||
expect.objectContaining({
|
||||
kind: "section",
|
||||
name: ".note.gnu.property",
|
||||
alignment: 8,
|
||||
}),
|
||||
]);
|
||||
expect(report.entries[0]?.type_name).toBe("gnu_property");
|
||||
expect(report.entries[0]?.descriptor.bytes).toBe(32);
|
||||
});
|
||||
|
||||
it.each([0, 1] as const)(
|
||||
"treats declared note alignment %i as 4-byte padding",
|
||||
(alignment) => {
|
||||
const report = parse(elfNoteFixture({ ownership: "program", alignment }));
|
||||
expect(report.tables[0]?.alignment).toBe(4);
|
||||
expect(
|
||||
report.entries.some((entry) => entry.type_name === "gnu_build_id"),
|
||||
).toBe(true);
|
||||
expect(
|
||||
report.entries.some((entry) => entry.type_name === "gnu_abi_tag"),
|
||||
).toBe(true);
|
||||
},
|
||||
);
|
||||
|
||||
const hostElf = [
|
||||
"/bin/ls",
|
||||
"/usr/bin/ls",
|
||||
"/usr/bin/env",
|
||||
].reduce<Buffer | null>((found, path) => {
|
||||
if (found) return found;
|
||||
try {
|
||||
const bytes = readFileSync(path);
|
||||
if (
|
||||
bytes.length > 64 &&
|
||||
bytes[0] === 0x7f &&
|
||||
bytes[1] === 0x45 &&
|
||||
bytes[2] === 0x4c &&
|
||||
bytes[3] === 0x46
|
||||
)
|
||||
return bytes;
|
||||
} catch {
|
||||
return found;
|
||||
}
|
||||
return found;
|
||||
}, null);
|
||||
|
||||
it.skipIf(hostElf === null)(
|
||||
"parses a host ELF without requiring it to contain notes",
|
||||
() => {
|
||||
const report = parse(hostElf!);
|
||||
expect(report.coverage.status).toBe("complete");
|
||||
const buildId = report.entries.find(
|
||||
(entry) => entry.type_name === "gnu_build_id",
|
||||
);
|
||||
const abi = report.entries.find(
|
||||
(entry) => entry.type_name === "gnu_abi_tag",
|
||||
);
|
||||
if (buildId !== undefined)
|
||||
expect(buildId.gnu?.build_id_hex).toMatch(/^[0-9a-f]+$/u);
|
||||
if (abi !== undefined)
|
||||
expect(abi.gnu?.abi_tag).toMatchObject({
|
||||
os: expect.any(Number),
|
||||
major: expect.any(Number),
|
||||
minor: expect.any(Number),
|
||||
subminor: expect.any(Number),
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
const unterminatedNoteSections = (
|
||||
sections: number,
|
||||
nameBytes: number,
|
||||
): Buffer => {
|
||||
const shnum = sections + 2;
|
||||
const tableOffset = 64 + shnum * 64;
|
||||
const shdr = (type: number, offset = 0, size = 0): Buffer => {
|
||||
const header = Buffer.alloc(64);
|
||||
header.writeUInt32LE(type, 4);
|
||||
header.writeBigUInt64LE(BigInt(offset), 24);
|
||||
header.writeBigUInt64LE(BigInt(size), 32);
|
||||
return header;
|
||||
};
|
||||
const image = Buffer.concat([
|
||||
elf64({ shoff: 64, shnum }),
|
||||
shdr(0),
|
||||
shdr(3, tableOffset, nameBytes),
|
||||
...Array.from({ length: sections }, () => shdr(7, tableOffset)),
|
||||
Buffer.alloc(nameBytes, 0x41),
|
||||
]);
|
||||
image.writeUInt16LE(1, 62);
|
||||
return image;
|
||||
};
|
||||
|
||||
it("bounds repeated scans of an unterminated section name", () => {
|
||||
expect(parse(unterminatedNoteSections(1, 64)).tables[0]?.name).toBeNull();
|
||||
expect(() => parse(unterminatedNoteSections(3, 400))).toThrow(/byte budget/u);
|
||||
});
|
||||
|
||||
it("rejects note text that repeats beyond the shared byte budget", () => {
|
||||
const notes = elfNote("A".repeat(40), 1, Buffer.alloc(0));
|
||||
const once = elfNoteFixture({ ownership: "program", notes });
|
||||
const twice = elfNoteFixture({ ownership: "both", notes });
|
||||
const input = inspectElfNotesInputSchema.parse({
|
||||
path: defaults.path,
|
||||
max_file_bytes: 200,
|
||||
});
|
||||
expect(parse(once, input).entries).toHaveLength(1);
|
||||
expect(() => parse(twice, input)).toThrow(ArtifactReaderFailure);
|
||||
expect(() => parse(twice, input)).toThrow(/byte budget/u);
|
||||
});
|
||||
|
||||
it("rejects program and section header tables that do not fit", () => {
|
||||
expect(() =>
|
||||
parse(elf64({ phoff: 64, phnum: 1 }, Buffer.from([1, 0, 0, 0]))),
|
||||
).toThrow(/leaves the artifact/u);
|
||||
expect(() =>
|
||||
parse(
|
||||
elf64({ shoff: 64, shnum: 1, shentsize: 64 }, Buffer.from([0, 0, 0, 0])),
|
||||
),
|
||||
).toThrow(/leaves the artifact/u);
|
||||
});
|
||||
|
||||
it("rejects an extended section count stored in section zero", () => {
|
||||
expect(() => parse(elf64({ shoff: 64, shnum: 0 }))).toThrow(
|
||||
/extended numbering/u,
|
||||
);
|
||||
});
|
||||
|
||||
const repeatedNotes = (count: number, notes: Buffer): Buffer => {
|
||||
const headers = Buffer.alloc(56 * count);
|
||||
const offset = 64 + headers.length;
|
||||
for (let index = 0; index < count; index += 1) {
|
||||
const at = index * 56;
|
||||
headers.writeUInt32LE(4, at);
|
||||
headers.writeBigUInt64LE(BigInt(offset), at + 8);
|
||||
headers.writeBigUInt64LE(BigInt(notes.length), at + 32);
|
||||
headers.writeBigUInt64LE(4n, at + 48);
|
||||
}
|
||||
return Buffer.concat([elf64({ phoff: 64, phnum: count }), headers, notes]);
|
||||
};
|
||||
|
||||
it("reuses one descriptor hash and GNU NUL scan across note headers", () => {
|
||||
const payload = Buffer.alloc(400);
|
||||
const hashed = parse(repeatedNotes(6, elfNote("X", 1, payload)));
|
||||
expect(hashed.entries).toHaveLength(6);
|
||||
expect(
|
||||
new Set(hashed.entries.map((entry) => entry.descriptor.sha256)).size,
|
||||
).toBe(1);
|
||||
const gold = parse(repeatedNotes(6, elfNote("GNU", 4, payload)));
|
||||
expect(gold.entries).toHaveLength(6);
|
||||
expect(gold.entries.every((entry) => entry.gnu?.gold_version === "")).toBe(
|
||||
true,
|
||||
);
|
||||
const distinct = Buffer.concat([
|
||||
elfNote("X", 1, payload),
|
||||
elfNote("X", 1, Buffer.alloc(400, 1)),
|
||||
]);
|
||||
expect(() =>
|
||||
parse(
|
||||
elfNoteFixture({ ownership: "program", notes: distinct }),
|
||||
inspectElfNotesInputSchema.parse({
|
||||
path: defaults.path,
|
||||
max_file_bytes: payload.length + 160,
|
||||
}),
|
||||
),
|
||||
).toThrow(/byte budget/u);
|
||||
});
|
||||
|
||||
it("decodes a GNU gold version by scanning trailing NULs", () => {
|
||||
const trailing = elfNote("GNU", 4, Buffer.from("gold\0\0x\0", "latin1"));
|
||||
expect(
|
||||
parse(elfNoteFixture({ ownership: "program", notes: trailing })).entries[0]
|
||||
?.gnu?.gold_version,
|
||||
).toBe("gold\0\0x");
|
||||
const descriptor = Buffer.concat([Buffer.alloc(131_072), Buffer.from("x")]);
|
||||
const started = Date.now();
|
||||
const report = parse(
|
||||
elfNoteFixture({
|
||||
ownership: "program",
|
||||
notes: elfNote("GNU", 4, descriptor),
|
||||
}),
|
||||
);
|
||||
expect(Date.now() - started).toBeLessThan(1_000);
|
||||
expect(report.entries[0]?.gnu?.gold_version).toBe(`${"\0".repeat(131_072)}x`);
|
||||
});
|
||||
|
||||
it("rejects a declared note table that extends past the file", () => {
|
||||
const truncated = elfNoteTablePastEnd();
|
||||
expect(truncated.length).toBe(137);
|
||||
expect(() => parse(truncated)).toThrow(ArtifactReaderFailure);
|
||||
expect(() => parse(truncated)).toThrow(/leaves the artifact/u);
|
||||
const present = Buffer.concat([truncated, Buffer.alloc(3)]);
|
||||
expect(parse(present).entries).toHaveLength(1);
|
||||
expect(parse(present).tables[0]?.location).toEqual({
|
||||
offset: 120,
|
||||
bytes: 20,
|
||||
});
|
||||
});
|
||||
|
||||
it("fails when the entry budget is exhausted", () => {
|
||||
const notes = Buffer.concat([
|
||||
gnuBuildIdNote(),
|
||||
gnuAbiTagNote(),
|
||||
elfNote("GNU", 4, Buffer.from("gold")),
|
||||
]);
|
||||
const bytes = elfNoteFixture({ ownership: "program", notes });
|
||||
expect(() =>
|
||||
parseElfNotes(
|
||||
bytes,
|
||||
{
|
||||
path: defaults.path,
|
||||
sha256: createHash("sha256").update(bytes).digest("hex"),
|
||||
bytes: bytes.length,
|
||||
},
|
||||
inspectElfNotesInputSchema.parse({ path: defaults.path, max_entries: 1 }),
|
||||
),
|
||||
).toThrow(ArtifactReaderFailure);
|
||||
});
|
||||
@@ -0,0 +1,455 @@
|
||||
import { createHash } from "node:crypto";
|
||||
import { ArtifactReaderFailure } from "../../artifacts/ArtifactReader.js";
|
||||
import { readElfFixedHeader } from "../../domain/elfFixedHeader.js";
|
||||
import type {
|
||||
ElfNotes,
|
||||
InspectElfNotesInput,
|
||||
} from "../../domain/native/elfNotes.js";
|
||||
import {
|
||||
elfObjectTypeName,
|
||||
gnuNoteTypeName,
|
||||
} from "../../domain/native/elfNotes.js";
|
||||
|
||||
const PT_NOTE = 4;
|
||||
const SHT_NOTE = 7;
|
||||
const SHT_STRTAB = 3;
|
||||
const HEX_DESCRIPTOR_LIMIT = 64;
|
||||
|
||||
interface TextBudget {
|
||||
readonly limit: number;
|
||||
used: number;
|
||||
hashed: number;
|
||||
scanned: number;
|
||||
readonly hashes: Map<string, string>;
|
||||
readonly scans: Map<string, number>;
|
||||
}
|
||||
|
||||
const fail = (message: string): never => {
|
||||
throw new ArtifactReaderFailure("format", message);
|
||||
};
|
||||
|
||||
const chargeText = (
|
||||
budget: TextBudget,
|
||||
bytes: number,
|
||||
field: "used" | "hashed" | "scanned" = "used",
|
||||
): void => {
|
||||
if (
|
||||
!Number.isSafeInteger(bytes) ||
|
||||
bytes < 0 ||
|
||||
bytes > budget.limit - budget[field]
|
||||
)
|
||||
throw new ArtifactReaderFailure(
|
||||
"limit",
|
||||
`ELF note text exceeds the ${String(budget.limit)}-byte budget.`,
|
||||
{ boundary: "memory" },
|
||||
);
|
||||
budget[field] += bytes;
|
||||
};
|
||||
|
||||
const sha256 = (bytes: Buffer): string =>
|
||||
createHash("sha256").update(bytes).digest("hex");
|
||||
|
||||
const alignTo = (value: number, alignment: number): number => {
|
||||
if (alignment <= 1) return value;
|
||||
return Math.ceil(value / alignment) * alignment;
|
||||
};
|
||||
|
||||
const GNU_OS_NAMES: Readonly<Record<number, string>> = {
|
||||
0: "linux",
|
||||
1: "hurd",
|
||||
2: "solaris",
|
||||
3: "freebsd",
|
||||
4: "netbsd",
|
||||
5: "syllable",
|
||||
6: "nacl",
|
||||
};
|
||||
|
||||
const LIMITATIONS = [
|
||||
"This workflow does not execute the image, resolve runtime notes, or verify GNU build-id uniqueness against package catalogs.",
|
||||
"PT_NOTE and SHT_NOTE are examined independently; the same file range may appear twice when both tables declare it.",
|
||||
] as const;
|
||||
|
||||
const ABSENT_LIMITATIONS = [
|
||||
"No PT_NOTE program header or SHT_NOTE section was declared. That is not proof the image never carried notes in another container or overlay.",
|
||||
"This workflow does not execute the image, resolve runtime notes, or verify GNU build-id uniqueness against package catalogs.",
|
||||
] as const;
|
||||
|
||||
class ElfReader {
|
||||
constructor(
|
||||
readonly bytes: Buffer,
|
||||
readonly little: boolean,
|
||||
readonly bits: 32 | 64,
|
||||
) {}
|
||||
|
||||
u16(offset: number): number {
|
||||
this.require(offset, 2);
|
||||
return this.little
|
||||
? this.bytes.readUInt16LE(offset)
|
||||
: this.bytes.readUInt16BE(offset);
|
||||
}
|
||||
|
||||
u32(offset: number): number {
|
||||
this.require(offset, 4);
|
||||
return this.little
|
||||
? this.bytes.readUInt32LE(offset)
|
||||
: this.bytes.readUInt32BE(offset);
|
||||
}
|
||||
|
||||
word(offset: number): number {
|
||||
const width = this.bits === 64 ? 8 : 4;
|
||||
this.require(offset, width);
|
||||
if (this.bits === 32) return this.u32(offset);
|
||||
const value = this.little
|
||||
? this.bytes.readBigUInt64LE(offset)
|
||||
: this.bytes.readBigUInt64BE(offset);
|
||||
if (value > BigInt(Number.MAX_SAFE_INTEGER))
|
||||
fail("ELF offset or size exceeds a safe integer.");
|
||||
return Number(value);
|
||||
}
|
||||
|
||||
require(offset: number, length: number): void {
|
||||
if (
|
||||
!Number.isSafeInteger(offset) ||
|
||||
!Number.isSafeInteger(length) ||
|
||||
offset < 0 ||
|
||||
length < 0 ||
|
||||
offset > this.bytes.length - length
|
||||
)
|
||||
fail(
|
||||
`ELF range ${String(offset)} + ${String(length)} leaves the artifact.`,
|
||||
);
|
||||
}
|
||||
|
||||
slice(offset: number, length: number): Buffer {
|
||||
this.require(offset, length);
|
||||
return this.bytes.subarray(offset, offset + length);
|
||||
}
|
||||
|
||||
cString(table: Buffer, offset: number, text: TextBudget): string | null {
|
||||
if (offset < 0 || offset >= table.length) return null;
|
||||
const affordable = text.limit - text.used;
|
||||
const available = table.length - offset;
|
||||
const scanned = Math.min(available, affordable + 1);
|
||||
const end = table.subarray(offset, offset + scanned).indexOf(0);
|
||||
if (end < 0) {
|
||||
chargeText(text, scanned);
|
||||
return null;
|
||||
}
|
||||
chargeText(text, end);
|
||||
return table.subarray(offset, offset + end).toString("latin1");
|
||||
}
|
||||
}
|
||||
|
||||
/** Note name/descriptor padding follows the containing table's p_align or
|
||||
* sh_addralign: 8 stays 8; 0, 1, 4, and other values pad as 4, matching
|
||||
* readelf/binutils. */
|
||||
const noteAlignment = (declared: number): number => (declared === 8 ? 8 : 4);
|
||||
|
||||
interface ParsedNote {
|
||||
readonly location: { offset: number; bytes: number };
|
||||
readonly namesz: number;
|
||||
readonly descsz: number;
|
||||
readonly type: number;
|
||||
readonly owner: string;
|
||||
readonly owner_bytes_hex: string;
|
||||
readonly descriptor: Buffer;
|
||||
readonly descriptor_offset: number;
|
||||
}
|
||||
|
||||
const parseNoteStream = (
|
||||
reader: ElfReader,
|
||||
offset: number,
|
||||
size: number,
|
||||
alignment: number,
|
||||
maxEntries: number,
|
||||
examined: { count: number },
|
||||
text: TextBudget,
|
||||
): ParsedNote[] => {
|
||||
// Alignment padding is not read when a note's aligned size equals the
|
||||
// declared table. Require the whole table first so a short file is invalid
|
||||
// input instead of a provider result the service rejects.
|
||||
reader.require(offset, size);
|
||||
const notes: ParsedNote[] = [];
|
||||
let at = 0;
|
||||
while (at + 12 <= size) {
|
||||
examined.count += 1;
|
||||
if (examined.count > maxEntries)
|
||||
throw new ArtifactReaderFailure(
|
||||
"limit",
|
||||
`ELF note tables exceed the ${String(maxEntries)}-entry budget.`,
|
||||
{ boundary: "capacity" },
|
||||
);
|
||||
const header = offset + at;
|
||||
const namesz = reader.u32(header);
|
||||
const descsz = reader.u32(header + 4);
|
||||
const type = reader.u32(header + 8);
|
||||
// Align offsets from the start of this note. Independently rounding
|
||||
// namesz/descsz disagrees with binutils when alignment is 8 because the
|
||||
// 12-byte note header is not 8-aligned (GNU property namesz=4 then starts
|
||||
// its descriptor at 16, not 20).
|
||||
const descRel = alignTo(12 + namesz, alignment);
|
||||
const nextRel = alignTo(descRel + descsz, alignment);
|
||||
const next = at + nextRel;
|
||||
if (next > size) fail("ELF note entry exceeds its table.");
|
||||
if (next <= at) fail("ELF note alignment did not advance.");
|
||||
const nameBytes = reader.slice(header + 12, namesz);
|
||||
const descriptor = reader.slice(header + descRel, descsz);
|
||||
const ownerEnd = nameBytes.indexOf(0);
|
||||
const ownerBytes = ownerEnd < 0 ? namesz : ownerEnd;
|
||||
chargeText(text, ownerBytes + namesz * 2);
|
||||
notes.push({
|
||||
location: { offset: header, bytes: next - at },
|
||||
namesz,
|
||||
descsz,
|
||||
type,
|
||||
owner:
|
||||
ownerEnd < 0
|
||||
? nameBytes.toString("latin1")
|
||||
: nameBytes.subarray(0, ownerEnd).toString("latin1"),
|
||||
owner_bytes_hex: nameBytes.toString("hex"),
|
||||
descriptor,
|
||||
descriptor_offset: header + descRel,
|
||||
});
|
||||
at = next;
|
||||
}
|
||||
if (
|
||||
at < size &&
|
||||
reader.slice(offset + at, size - at).some((byte) => byte !== 0)
|
||||
)
|
||||
fail("ELF note table has trailing non-zero bytes.");
|
||||
return notes;
|
||||
};
|
||||
|
||||
const decodeGnu = (
|
||||
owner: string,
|
||||
type: number,
|
||||
descriptor: Buffer,
|
||||
little: boolean,
|
||||
text: TextBudget,
|
||||
): ElfNotes["entries"][number]["gnu"] => {
|
||||
if (owner !== "GNU") return null;
|
||||
if (type === 3 && descriptor.length > 0) {
|
||||
chargeText(text, descriptor.length * 2);
|
||||
return {
|
||||
build_id_hex: descriptor.toString("hex"),
|
||||
abi_tag: null,
|
||||
gold_version: null,
|
||||
};
|
||||
}
|
||||
if (type === 1 && descriptor.length === 16) {
|
||||
const read = (offset: number) =>
|
||||
little
|
||||
? descriptor.readUInt32LE(offset)
|
||||
: descriptor.readUInt32BE(offset);
|
||||
const os = read(0);
|
||||
return {
|
||||
build_id_hex: null,
|
||||
abi_tag: {
|
||||
os,
|
||||
os_name: GNU_OS_NAMES[os] ?? null,
|
||||
major: read(4),
|
||||
minor: read(8),
|
||||
subminor: read(12),
|
||||
},
|
||||
gold_version: null,
|
||||
};
|
||||
}
|
||||
if (type === 4) {
|
||||
const range = `${String(descriptor.byteOffset)}:${String(descriptor.length)}`;
|
||||
let end = text.scans.get(range);
|
||||
if (end === undefined) {
|
||||
chargeText(text, descriptor.length, "scanned");
|
||||
end = descriptor.length;
|
||||
while (end > 0 && descriptor[end - 1] === 0) end -= 1;
|
||||
text.scans.set(range, end);
|
||||
}
|
||||
chargeText(text, end);
|
||||
return {
|
||||
build_id_hex: null,
|
||||
abi_tag: null,
|
||||
gold_version: descriptor.toString("latin1", 0, end),
|
||||
};
|
||||
}
|
||||
return {
|
||||
build_id_hex: null,
|
||||
abi_tag: null,
|
||||
gold_version: null,
|
||||
};
|
||||
};
|
||||
|
||||
/** Decode PT_NOTE and SHT_NOTE tables from one in-memory ELF image. */
|
||||
export const parseElfNotes = (
|
||||
bytes: Buffer,
|
||||
artifact: ElfNotes["artifact"],
|
||||
input: InspectElfNotesInput,
|
||||
): ElfNotes => {
|
||||
const fixed = readElfFixedHeader(bytes);
|
||||
if (!fixed.ok) return fail(fixed.error);
|
||||
const header = fixed.value;
|
||||
const reader = new ElfReader(
|
||||
bytes,
|
||||
header.byteOrder === "little",
|
||||
header.elfClass,
|
||||
);
|
||||
const wide = header.elfClass === 64;
|
||||
const type = reader.u16(16);
|
||||
const phoff = reader.word(wide ? 32 : 28);
|
||||
const shoff = reader.word(wide ? 40 : 32);
|
||||
const phentsize = reader.u16(wide ? 54 : 42);
|
||||
const phnum = reader.u16(wide ? 56 : 44);
|
||||
const shentsize = reader.u16(wide ? 58 : 46);
|
||||
const shnum = reader.u16(wide ? 60 : 48);
|
||||
const shstrndx = reader.u16(wide ? 62 : 50);
|
||||
if (
|
||||
phnum === 0xffff ||
|
||||
shnum === 0xffff ||
|
||||
shstrndx === 0xffff ||
|
||||
(shoff !== 0 && shnum === 0)
|
||||
)
|
||||
return fail("ELF extended numbering is unsupported.");
|
||||
if (phnum !== 0) {
|
||||
if (phentsize < (wide ? 56 : 32))
|
||||
return fail("ELF program header width is invalid.");
|
||||
reader.require(phoff, phnum * phentsize);
|
||||
}
|
||||
if (shnum !== 0) {
|
||||
if (shentsize < (wide ? 64 : 40))
|
||||
return fail("ELF section header width is invalid.");
|
||||
reader.require(shoff, shnum * shentsize);
|
||||
}
|
||||
const text: TextBudget = {
|
||||
limit: Math.min(bytes.length, input.max_file_bytes),
|
||||
used: 0,
|
||||
hashed: 0,
|
||||
scanned: 0,
|
||||
hashes: new Map(),
|
||||
scans: new Map(),
|
||||
};
|
||||
let shstrtab: Buffer | null = null;
|
||||
if (shnum !== 0 && shstrndx !== 0) {
|
||||
if (shstrndx >= shnum)
|
||||
return fail("ELF section name index is out of range.");
|
||||
const namesHeader = shoff + shstrndx * shentsize;
|
||||
if (reader.u32(namesHeader + 4) !== SHT_STRTAB)
|
||||
return fail("ELF section name table is not a string table.");
|
||||
const strOff = reader.word(namesHeader + (wide ? 24 : 16));
|
||||
const strSize = reader.word(namesHeader + (wide ? 32 : 20));
|
||||
shstrtab = reader.slice(strOff, strSize);
|
||||
}
|
||||
const tables: ElfNotes["tables"] = [];
|
||||
const parsed: ReturnType<typeof toEntry>[] = [];
|
||||
const examined = { count: 0 };
|
||||
if (phnum !== 0) {
|
||||
if (phoff < header.headerSize)
|
||||
return fail("ELF program header table overlaps the native header.");
|
||||
for (let index = 0; index < phnum; index += 1) {
|
||||
const at = phoff + index * phentsize;
|
||||
if (reader.u32(at) !== PT_NOTE) continue;
|
||||
const fileOffset = reader.word(at + (wide ? 8 : 4));
|
||||
const fileSize = reader.word(at + (wide ? 32 : 16));
|
||||
const alignment = noteAlignment(reader.word(at + (wide ? 48 : 28)));
|
||||
tables.push({
|
||||
kind: "program",
|
||||
index,
|
||||
name: null,
|
||||
location: { offset: fileOffset, bytes: fileSize },
|
||||
alignment,
|
||||
});
|
||||
for (const note of parseNoteStream(
|
||||
reader,
|
||||
fileOffset,
|
||||
fileSize,
|
||||
alignment,
|
||||
input.max_entries,
|
||||
examined,
|
||||
text,
|
||||
))
|
||||
parsed.push(toEntry(note, "program", index, reader.little, text));
|
||||
}
|
||||
}
|
||||
if (shnum !== 0) {
|
||||
if (shoff < header.headerSize)
|
||||
return fail("ELF section header table overlaps the native header.");
|
||||
for (let index = 0; index < shnum; index += 1) {
|
||||
const at = shoff + index * shentsize;
|
||||
if (reader.u32(at + 4) !== SHT_NOTE) continue;
|
||||
const nameOffset = reader.u32(at);
|
||||
const fileOffset = reader.word(at + (wide ? 24 : 16));
|
||||
const fileSize = reader.word(at + (wide ? 32 : 20));
|
||||
const alignment = noteAlignment(reader.word(at + (wide ? 48 : 32)));
|
||||
tables.push({
|
||||
kind: "section",
|
||||
index,
|
||||
name:
|
||||
shstrtab === null ? null : reader.cString(shstrtab, nameOffset, text),
|
||||
location: { offset: fileOffset, bytes: fileSize },
|
||||
alignment,
|
||||
});
|
||||
for (const note of parseNoteStream(
|
||||
reader,
|
||||
fileOffset,
|
||||
fileSize,
|
||||
alignment,
|
||||
input.max_entries,
|
||||
examined,
|
||||
text,
|
||||
))
|
||||
parsed.push(toEntry(note, "section", index, reader.little, text));
|
||||
}
|
||||
}
|
||||
const absent = tables.length === 0;
|
||||
return {
|
||||
artifact,
|
||||
format: wide ? "elf64" : "elf32",
|
||||
byte_order: reader.little ? "little" : "big",
|
||||
machine: header.machine,
|
||||
type,
|
||||
type_name: elfObjectTypeName(type),
|
||||
tables,
|
||||
entries: parsed.map((entry, index) => ({ ...entry, index })),
|
||||
coverage: {
|
||||
status: "complete",
|
||||
examined_tables: tables.length,
|
||||
examined_entries: examined.count,
|
||||
},
|
||||
limitations: [...(absent ? ABSENT_LIMITATIONS : LIMITATIONS)],
|
||||
};
|
||||
};
|
||||
|
||||
const toEntry = (
|
||||
note: ParsedNote,
|
||||
tableKind: "program" | "section",
|
||||
tableIndex: number,
|
||||
little: boolean,
|
||||
text: TextBudget,
|
||||
): Omit<ElfNotes["entries"][number], "index"> => {
|
||||
const gnuOwner = note.owner === "GNU";
|
||||
const range = `${String(note.descriptor_offset)}:${String(note.descsz)}`;
|
||||
let digest = text.hashes.get(range);
|
||||
if (digest === undefined) {
|
||||
chargeText(text, note.descriptor.length, "hashed");
|
||||
digest = sha256(note.descriptor);
|
||||
text.hashes.set(range, digest);
|
||||
}
|
||||
return {
|
||||
table_kind: tableKind,
|
||||
table_index: tableIndex,
|
||||
location: note.location,
|
||||
namesz: note.namesz,
|
||||
descsz: note.descsz,
|
||||
type: note.type,
|
||||
type_name: gnuOwner ? gnuNoteTypeName(note.type) : "unknown",
|
||||
owner: note.owner,
|
||||
owner_bytes_hex: note.owner_bytes_hex,
|
||||
descriptor: {
|
||||
location: { offset: note.descriptor_offset, bytes: note.descsz },
|
||||
bytes: note.descsz,
|
||||
sha256: digest,
|
||||
hex:
|
||||
note.descsz <= HEX_DESCRIPTOR_LIMIT
|
||||
? note.descriptor.toString("hex")
|
||||
: null,
|
||||
},
|
||||
gnu: decodeGnu(note.owner, note.type, note.descriptor, little, text),
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,239 @@
|
||||
const concat = (...parts: Buffer[]): Buffer => Buffer.concat(parts);
|
||||
|
||||
const alignTo = (value: number, alignment: number): number =>
|
||||
Math.ceil(value / alignment) * alignment;
|
||||
|
||||
/** Effective namesz/descsz padding: 8 stays 8; 0, 1, 4, and other values use 4. */
|
||||
const notePadding = (declared: number): number => (declared === 8 ? 8 : 4);
|
||||
|
||||
const writeWord = (
|
||||
bytes: Buffer,
|
||||
offset: number,
|
||||
value: number,
|
||||
bits: 32 | 64,
|
||||
little: boolean,
|
||||
): void => {
|
||||
if (bits === 64) {
|
||||
if (little) bytes.writeBigUInt64LE(BigInt(value), offset);
|
||||
else bytes.writeBigUInt64BE(BigInt(value), offset);
|
||||
return;
|
||||
}
|
||||
if (little) bytes.writeUInt32LE(value, offset);
|
||||
else bytes.writeUInt32BE(value, offset);
|
||||
};
|
||||
|
||||
const write16 = (
|
||||
bytes: Buffer,
|
||||
offset: number,
|
||||
value: number,
|
||||
little: boolean,
|
||||
): void => {
|
||||
if (little) bytes.writeUInt16LE(value, offset);
|
||||
else bytes.writeUInt16BE(value, offset);
|
||||
};
|
||||
|
||||
const write32 = (
|
||||
bytes: Buffer,
|
||||
offset: number,
|
||||
value: number,
|
||||
little: boolean,
|
||||
): void => {
|
||||
if (little) bytes.writeUInt32LE(value, offset);
|
||||
else bytes.writeUInt32BE(value, offset);
|
||||
};
|
||||
|
||||
/** Encode one ELF note, padding name/descriptor to the table alignment. */
|
||||
export const elfNote = (
|
||||
owner: string,
|
||||
type: number,
|
||||
descriptor: Buffer,
|
||||
little = true,
|
||||
alignment = 4,
|
||||
): Buffer => {
|
||||
const name = Buffer.from(`${owner}\0`, "latin1");
|
||||
const pad = notePadding(alignment);
|
||||
const descOff = alignTo(12 + name.length, pad);
|
||||
const bytes = Buffer.alloc(alignTo(descOff + descriptor.length, pad));
|
||||
write32(bytes, 0, name.length, little);
|
||||
write32(bytes, 4, descriptor.length, little);
|
||||
write32(bytes, 8, type, little);
|
||||
name.copy(bytes, 12);
|
||||
descriptor.copy(bytes, descOff);
|
||||
return bytes;
|
||||
};
|
||||
|
||||
/** GNU NT_GNU_BUILD_ID note with a 20-byte descriptor. */
|
||||
export const gnuBuildIdNote = (
|
||||
hex = "0123456789abcdef0123456789abcdef01234567",
|
||||
little = true,
|
||||
): Buffer => elfNote("GNU", 3, Buffer.from(hex, "hex"), little);
|
||||
|
||||
/** GNU NT_GNU_ABI_TAG note: Linux 3.2.0. */
|
||||
export const gnuAbiTagNote = (little = true): Buffer => {
|
||||
const descriptor = Buffer.alloc(16);
|
||||
write32(descriptor, 0, 0, little);
|
||||
write32(descriptor, 4, 3, little);
|
||||
write32(descriptor, 8, 2, little);
|
||||
write32(descriptor, 12, 0, little);
|
||||
return elfNote("GNU", 1, descriptor, little);
|
||||
};
|
||||
|
||||
/** GNU NT_GNU_PROPERTY_TYPE_0 note with 8-byte table padding. */
|
||||
export const gnuPropertyNote = (little = true): Buffer => {
|
||||
const descriptor = Buffer.alloc(32);
|
||||
write32(descriptor, 0, 0xc0000002, little);
|
||||
write32(descriptor, 4, 4, little);
|
||||
write32(descriptor, 8, 3, little);
|
||||
write32(descriptor, 16, 0xc0008002, little);
|
||||
write32(descriptor, 20, 4, little);
|
||||
write32(descriptor, 24, 1, little);
|
||||
return elfNote("GNU", 5, descriptor, little, 8);
|
||||
};
|
||||
|
||||
export interface ElfNoteImageOptions {
|
||||
readonly bits?: 32 | 64;
|
||||
readonly little?: boolean;
|
||||
readonly notes?: Buffer;
|
||||
readonly ownership?: "program" | "section" | "both" | "none";
|
||||
readonly machine?: number;
|
||||
readonly type?: number;
|
||||
readonly alignment?: number;
|
||||
readonly sectionName?: string;
|
||||
}
|
||||
|
||||
/** Minimal ELF32/ELF64 image declaring PT_NOTE and/or SHT_NOTE. */
|
||||
export const elfNoteFixture = (options: ElfNoteImageOptions = {}): Buffer => {
|
||||
const bits = options.bits ?? 64;
|
||||
const little = options.little ?? true;
|
||||
const ownership = options.ownership ?? "both";
|
||||
const notes =
|
||||
options.notes ??
|
||||
concat(gnuBuildIdNote(undefined, little), gnuAbiTagNote(little));
|
||||
const declaredAlign = options.alignment ?? 4;
|
||||
const pad = notePadding(declaredAlign);
|
||||
const header = bits === 64 ? 64 : 52;
|
||||
const phent = bits === 64 ? 56 : 32;
|
||||
const shent = bits === 64 ? 64 : 40;
|
||||
const program = ownership === "program" || ownership === "both";
|
||||
const section = ownership === "section" || ownership === "both";
|
||||
const phnum = program ? 1 : 0;
|
||||
const sectionName = options.sectionName ?? ".note.gnu.build-id";
|
||||
const shstrtab = Buffer.from(`\0${sectionName}\0.shstrtab\0`, "latin1");
|
||||
const shnum = section ? 3 : 0;
|
||||
const phoff = header;
|
||||
const shoff = phoff + phnum * phent;
|
||||
const noteOff = alignTo(shoff + shnum * shent, pad);
|
||||
const strOff = noteOff + notes.length;
|
||||
const bytes = Buffer.alloc(strOff + (section ? shstrtab.length : 0));
|
||||
bytes.writeUInt32LE(0x464c457f, 0);
|
||||
bytes[4] = bits === 64 ? 2 : 1;
|
||||
bytes[5] = little ? 1 : 2;
|
||||
bytes[6] = 1;
|
||||
write16(bytes, 0x10, options.type ?? 3, little);
|
||||
write16(bytes, 0x12, options.machine ?? 62, little);
|
||||
write32(bytes, 0x14, 1, little);
|
||||
write16(bytes, bits === 64 ? 0x34 : 0x28, header, little);
|
||||
writeWord(
|
||||
bytes,
|
||||
bits === 64 ? 0x20 : 0x1c,
|
||||
phnum === 0 ? 0 : phoff,
|
||||
bits,
|
||||
little,
|
||||
);
|
||||
write16(bytes, bits === 64 ? 0x36 : 0x2a, phent, little);
|
||||
write16(bytes, bits === 64 ? 0x38 : 0x2c, phnum, little);
|
||||
writeWord(
|
||||
bytes,
|
||||
bits === 64 ? 0x28 : 0x20,
|
||||
shnum === 0 ? 0 : shoff,
|
||||
bits,
|
||||
little,
|
||||
);
|
||||
write16(bytes, bits === 64 ? 0x3a : 0x2e, shent, little);
|
||||
write16(bytes, bits === 64 ? 0x3c : 0x30, shnum, little);
|
||||
write16(bytes, bits === 64 ? 0x3e : 0x32, section ? 2 : 0, little);
|
||||
if (program) {
|
||||
const ph = phoff;
|
||||
write32(bytes, ph, 4, little);
|
||||
if (bits === 64) {
|
||||
write32(bytes, ph + 4, 4, little);
|
||||
writeWord(bytes, ph + 8, noteOff, bits, little);
|
||||
writeWord(bytes, ph + 32, notes.length, bits, little);
|
||||
writeWord(bytes, ph + 40, notes.length, bits, little);
|
||||
writeWord(bytes, ph + 48, declaredAlign, bits, little);
|
||||
} else {
|
||||
writeWord(bytes, ph + 4, noteOff, bits, little);
|
||||
writeWord(bytes, ph + 16, notes.length, bits, little);
|
||||
writeWord(bytes, ph + 20, notes.length, bits, little);
|
||||
write32(bytes, ph + 24, 4, little);
|
||||
writeWord(bytes, ph + 28, declaredAlign, bits, little);
|
||||
}
|
||||
}
|
||||
if (section) {
|
||||
const noteName = 1;
|
||||
const strName = `${sectionName}\0`.length + 1;
|
||||
const noteSh = shoff + shent;
|
||||
const strSh = shoff + 2 * shent;
|
||||
write32(bytes, noteSh, noteName, little);
|
||||
write32(bytes, noteSh + 4, 7, little);
|
||||
writeWord(bytes, noteSh + (bits === 64 ? 24 : 16), noteOff, bits, little);
|
||||
writeWord(
|
||||
bytes,
|
||||
noteSh + (bits === 64 ? 32 : 20),
|
||||
notes.length,
|
||||
bits,
|
||||
little,
|
||||
);
|
||||
writeWord(
|
||||
bytes,
|
||||
noteSh + (bits === 64 ? 48 : 32),
|
||||
declaredAlign,
|
||||
bits,
|
||||
little,
|
||||
);
|
||||
write32(bytes, strSh, strName, little);
|
||||
write32(bytes, strSh + 4, 3, little);
|
||||
writeWord(bytes, strSh + (bits === 64 ? 24 : 16), strOff, bits, little);
|
||||
writeWord(
|
||||
bytes,
|
||||
strSh + (bits === 64 ? 32 : 20),
|
||||
shstrtab.length,
|
||||
bits,
|
||||
little,
|
||||
);
|
||||
shstrtab.copy(bytes, strOff);
|
||||
}
|
||||
notes.copy(bytes, noteOff);
|
||||
return bytes;
|
||||
};
|
||||
|
||||
/**
|
||||
* ELF64 whose only PT_NOTE declares 20 bytes. The file holds the 12-byte
|
||||
* header, 4-byte name, and 1-byte descriptor; the 3 alignment bytes are absent.
|
||||
*/
|
||||
export const elfNoteTablePastEnd = (): Buffer => {
|
||||
const note = Buffer.alloc(17);
|
||||
write32(note, 0, 4, true);
|
||||
write32(note, 4, 1, true);
|
||||
write32(note, 8, 1, true);
|
||||
note.write("GNU\0", 12, "latin1");
|
||||
note[16] = 0x2a;
|
||||
const header = Buffer.alloc(64);
|
||||
header.writeUInt32LE(0x464c457f, 0);
|
||||
header[4] = 2;
|
||||
header[5] = 1;
|
||||
header[6] = 1;
|
||||
write16(header, 0x10, 3, true);
|
||||
write16(header, 0x12, 62, true);
|
||||
write32(header, 0x14, 1, true);
|
||||
write16(header, 0x34, 64, true);
|
||||
writeWord(header, 0x20, 64, 64, true);
|
||||
write16(header, 0x36, 56, true);
|
||||
write16(header, 0x38, 1, true);
|
||||
const program = Buffer.alloc(56);
|
||||
write32(program, 0, 4, true);
|
||||
writeWord(program, 8, 120, 64, true);
|
||||
writeWord(program, 32, 20, 64, true);
|
||||
writeWord(program, 48, 4, 64, true);
|
||||
return concat(header, program, note);
|
||||
};
|
||||
@@ -0,0 +1,91 @@
|
||||
import { writeFile } from "node:fs/promises";
|
||||
import { join } from "node:path";
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { ElfNotesService } from "../../application/binaryDiagnostics/ElfNotesService.js";
|
||||
import {
|
||||
AnalysisInputError,
|
||||
AnalysisResourceConstraintError,
|
||||
} from "../../domain/analysisErrorCore.js";
|
||||
import { projectAnalysisError } from "../../domain/analysisErrorProjection.js";
|
||||
import { createTestTempDirectory } from "../../../tests/fixtures/temporaryDirectory.js";
|
||||
import {
|
||||
elfNote,
|
||||
elfNoteFixture,
|
||||
elfNoteTablePastEnd,
|
||||
} from "./ElfNotes.fixture.js";
|
||||
import { ElfNotesProvider } from "./ElfNotesProvider.js";
|
||||
|
||||
const inspect = async (
|
||||
path: string,
|
||||
input: { readonly max_file_bytes?: number } = {},
|
||||
) => {
|
||||
const service = new ElfNotesService(new ElfNotesProvider());
|
||||
try {
|
||||
return await service.inspect({ path, ...input });
|
||||
} finally {
|
||||
await service.close();
|
||||
}
|
||||
};
|
||||
|
||||
describe("ELF note provider failures", () => {
|
||||
it("reports a note table past the end of the file as invalid input", async () => {
|
||||
const root = await createTestTempDirectory("rea-elf-note-past-end-");
|
||||
const path = join(root, "program");
|
||||
await writeFile(path, elfNoteTablePastEnd());
|
||||
const result = await inspect(path);
|
||||
expect(result.ok).toBe(false);
|
||||
if (result.ok) throw new Error("Expected invalid input");
|
||||
expect(result.error).toBeInstanceOf(AnalysisInputError);
|
||||
expect(result.error).toMatchObject({
|
||||
_tag: "AnalysisInputError",
|
||||
issues: [
|
||||
{
|
||||
path: ["path"],
|
||||
reason: "invalid_format",
|
||||
message: expect.stringContaining("leaves the artifact"),
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
it("names the note text budget instead of host memory", async () => {
|
||||
const root = await createTestTempDirectory("rea-elf-note-text-budget-");
|
||||
const path = join(root, "program");
|
||||
await writeFile(
|
||||
path,
|
||||
elfNoteFixture({
|
||||
ownership: "program",
|
||||
notes: elfNote("GNU", 3, Buffer.alloc(4096, 1)),
|
||||
}),
|
||||
);
|
||||
const result = await inspect(path);
|
||||
expect(result.ok).toBe(false);
|
||||
if (result.ok) throw new Error("Expected a text budget refusal");
|
||||
const error = result.error as AnalysisResourceConstraintError;
|
||||
expect(error.resource).toBe("memory");
|
||||
expect(error.message).toMatch(/byte budget/u);
|
||||
const projected = projectAnalysisError(error);
|
||||
expect(projected.retryable).toBe(false);
|
||||
expect(projected.remediation.action).toContain(
|
||||
"ELF note text, hash, and scan budget",
|
||||
);
|
||||
expect(projected.remediation.action).toMatch(/admitted image length/u);
|
||||
expect(projected.remediation.action).toMatch(/do not raise it/u);
|
||||
expect(projected.remediation.action).not.toMatch(/available host memory/u);
|
||||
});
|
||||
|
||||
it("keeps a caller file-size budget distinct from host memory", async () => {
|
||||
const root = await createTestTempDirectory("rea-elf-note-file-budget-");
|
||||
const path = join(root, "program");
|
||||
await writeFile(path, elfNoteFixture({ ownership: "program" }));
|
||||
const result = await inspect(path, { max_file_bytes: 64 });
|
||||
expect(result.ok).toBe(false);
|
||||
if (result.ok) throw new Error("Expected a file-size refusal");
|
||||
const error = result.error as AnalysisResourceConstraintError;
|
||||
expect(error.resource).toBe("file-size");
|
||||
const projected = projectAnalysisError(error);
|
||||
expect(projected.retryable).toBe(false);
|
||||
expect(projected.remediation.action).toContain("max_file_bytes");
|
||||
expect(projected.remediation.action).not.toMatch(/available host memory/u);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,207 @@
|
||||
import { ArtifactReaderFailure } from "../../artifacts/ArtifactReader.js";
|
||||
import { readStableArtifact } from "../../artifacts/readStableArtifact.js";
|
||||
import {
|
||||
ArtifactFileCleanupFailure,
|
||||
bindArtifactFileCleanup,
|
||||
artifactFileCancellationCause,
|
||||
} from "../../artifacts/ArtifactFileOperation.js";
|
||||
import { ArtifactResourceScope } from "../../artifacts/ArtifactResourceScope.js";
|
||||
import { analysisErrorWithCleanupFailure } from "../../domain/analysisErrorCleanup.js";
|
||||
import type { ElfNotesPort } from "../../application/binaryDiagnostics/ElfNotesPort.js";
|
||||
import type { ExecutionOptions } from "../../application/AnalysisProvider.js";
|
||||
import { ArtifactOperationError } from "../../domain/artifactOperationError.js";
|
||||
import {
|
||||
AnalysisAccessDeniedError,
|
||||
AnalysisArtifactChangedError,
|
||||
AnalysisCancelledError,
|
||||
AnalysisInputError,
|
||||
AnalysisResourceConstraintError,
|
||||
} from "../../domain/analysisErrorCore.js";
|
||||
import { fixedAllowanceOptions } from "../../domain/fixedAllowanceRemediation.js";
|
||||
import {
|
||||
MAX_INSPECT_ELF_NOTE_ENTRIES,
|
||||
MAX_INSPECT_ELF_NOTE_FILE_BYTES,
|
||||
type InspectElfNotesInput,
|
||||
} from "../../domain/native/elfNotes.js";
|
||||
import { err, ok } from "../../domain/result.js";
|
||||
import { ProviderCleanupError } from "../../domain/providerCleanupError.js";
|
||||
import { ELF_NOTES_PROVIDER } from "../../application/InvestigationProviders.js";
|
||||
import { parseElfNotes } from "./ElfNoteParser.js";
|
||||
|
||||
const OPERATION = "inspect_elf_notes";
|
||||
|
||||
/** Portable parser over one bounded, identity-checked regular file. */
|
||||
export class ElfNotesProvider implements ElfNotesPort {
|
||||
readonly identity = ELF_NOTES_PROVIDER;
|
||||
readonly #resources = new ArtifactResourceScope();
|
||||
|
||||
async close(): Promise<void> {
|
||||
try {
|
||||
await this.#resources.close();
|
||||
} catch (cause: unknown) {
|
||||
if (
|
||||
!(cause instanceof ArtifactReaderFailure) ||
|
||||
cause.cleanup === undefined
|
||||
)
|
||||
throw cause;
|
||||
const error = new ProviderCleanupError(
|
||||
ELF_NOTES_PROVIDER.id,
|
||||
cause.cleanup.resources,
|
||||
{ reason: cause.cleanup.reason },
|
||||
{ cause, operation: OPERATION },
|
||||
);
|
||||
bindArtifactFileCleanup(cause, error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async inspect(input: InspectElfNotesInput, options?: ExecutionOptions) {
|
||||
try {
|
||||
return await this.#resources.run(async () => {
|
||||
const snapshot = await readStableArtifact(
|
||||
input.path,
|
||||
input.max_file_bytes,
|
||||
options?.signal,
|
||||
undefined,
|
||||
this.#resources,
|
||||
);
|
||||
return ok(
|
||||
parseElfNotes(
|
||||
snapshot.bytes,
|
||||
{
|
||||
path: input.path,
|
||||
sha256: snapshot.sha256,
|
||||
bytes: snapshot.bytes.length,
|
||||
},
|
||||
input,
|
||||
),
|
||||
);
|
||||
});
|
||||
} catch (cause) {
|
||||
return err(elfNotesFailure(cause, input, options?.signal));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const NOTE_TEXT_BUDGET =
|
||||
"The ELF note text, hash, and scan budget tied to the admitted image length";
|
||||
|
||||
const inputBudgetRemediation = (
|
||||
boundary: "file-size" | "capacity",
|
||||
input: InspectElfNotesInput,
|
||||
): string =>
|
||||
boundary === "file-size"
|
||||
? input.max_file_bytes >= MAX_INSPECT_ELF_NOTE_FILE_BYTES
|
||||
? `The image exceeds the ${String(MAX_INSPECT_ELF_NOTE_FILE_BYTES)}-byte input ceiling this operation accepts; select a smaller artifact.`
|
||||
: `Raise max_file_bytes above ${String(input.max_file_bytes)} bytes or select a smaller artifact; the limit is the caller-selected input read budget.`
|
||||
: input.max_entries >= MAX_INSPECT_ELF_NOTE_ENTRIES
|
||||
? `The image exceeds the ${String(MAX_INSPECT_ELF_NOTE_ENTRIES)}-entry note-table ceiling this operation accepts; select a smaller artifact.`
|
||||
: `Raise max_entries above ${String(input.max_entries)} or select a smaller artifact; the limit is the caller-selected note entry budget.`;
|
||||
|
||||
const elfNotesFailurePrimary = (
|
||||
cause: unknown,
|
||||
input: InspectElfNotesInput,
|
||||
) => {
|
||||
if (cause instanceof ArtifactReaderFailure && cause.reason === "integrity")
|
||||
return new AnalysisArtifactChangedError(
|
||||
OPERATION,
|
||||
input.path,
|
||||
cause.message,
|
||||
{
|
||||
cause,
|
||||
},
|
||||
);
|
||||
if (cause instanceof ArtifactReaderFailure && cause.reason === "cancelled")
|
||||
return new AnalysisCancelledError(OPERATION, {
|
||||
cause: artifactFileCancellationCause(cause),
|
||||
});
|
||||
if (cause instanceof ArtifactReaderFailure) {
|
||||
if (cause.reason === "limit") {
|
||||
// Caller-selected read and entry budgets name the bound to raise.
|
||||
// The note text, hash, and scan ceiling equals the admitted image
|
||||
// length, so host memory cannot raise it.
|
||||
const noteTextBudget = cause.boundary === "memory";
|
||||
return new AnalysisResourceConstraintError(
|
||||
OPERATION,
|
||||
cause.boundary ?? "memory",
|
||||
cause.message,
|
||||
{
|
||||
max_file_bytes: input.max_file_bytes,
|
||||
max_entries: input.max_entries,
|
||||
},
|
||||
{
|
||||
cause,
|
||||
...(cause.boundary === undefined
|
||||
? {}
|
||||
: noteTextBudget
|
||||
? fixedAllowanceOptions(NOTE_TEXT_BUDGET, "parser")
|
||||
: {
|
||||
retryable: false as const,
|
||||
remediationAction: inputBudgetRemediation(
|
||||
cause.boundary,
|
||||
input,
|
||||
),
|
||||
}),
|
||||
},
|
||||
);
|
||||
}
|
||||
if (cause.reason === "path" || cause.reason === "format")
|
||||
return new AnalysisInputError(OPERATION, { cause }, [
|
||||
{
|
||||
path: ["path"],
|
||||
reason: "invalid_format",
|
||||
message: cause.message,
|
||||
},
|
||||
]);
|
||||
}
|
||||
if (cause instanceof Error && "code" in cause) {
|
||||
if (cause.code === "EACCES" || cause.code === "EPERM")
|
||||
return new AnalysisAccessDeniedError(OPERATION, input.path, cause.code, {
|
||||
cause,
|
||||
});
|
||||
if (
|
||||
["ENOENT", "ENOTDIR", "ELOOP", "ENAMETOOLONG"].includes(
|
||||
String(cause.code),
|
||||
)
|
||||
)
|
||||
return new AnalysisInputError(OPERATION, { cause }, [
|
||||
{
|
||||
path: ["path"],
|
||||
reason: "invalid_value",
|
||||
message: `Selected ELF image could not be read (${String(cause.code)}): ${input.path}.`,
|
||||
},
|
||||
]);
|
||||
}
|
||||
return new ArtifactOperationError(
|
||||
OPERATION,
|
||||
cause instanceof ArtifactReaderFailure ? cause.reason : "io",
|
||||
undefined,
|
||||
cause instanceof Error ? cause.message : "ELF note inspection failed.",
|
||||
{ cause },
|
||||
);
|
||||
};
|
||||
|
||||
const elfNotesFailure = (
|
||||
cause: unknown,
|
||||
input: InspectElfNotesInput,
|
||||
signal: AbortSignal | undefined,
|
||||
) => {
|
||||
const primary =
|
||||
cause instanceof ArtifactFileCleanupFailure &&
|
||||
cause.fileCleanupFailure.outcome.kind === "failed"
|
||||
? elfNotesFailurePrimary(cause.fileCleanupFailure.outcome.cause, input)
|
||||
: cause instanceof ArtifactFileCleanupFailure && signal?.aborted
|
||||
? new AnalysisCancelledError(OPERATION, { cause: signal.reason })
|
||||
: elfNotesFailurePrimary(cause, input);
|
||||
if (!(cause instanceof ArtifactReaderFailure) || cause.cleanup === undefined)
|
||||
return primary;
|
||||
const cleanup = new ProviderCleanupError(
|
||||
ELF_NOTES_PROVIDER.id,
|
||||
cause.cleanup.resources,
|
||||
{ reason: cause.cleanup.reason },
|
||||
{ operation: OPERATION, cause },
|
||||
);
|
||||
const error = analysisErrorWithCleanupFailure(primary, cleanup, OPERATION);
|
||||
bindArtifactFileCleanup(cause, error);
|
||||
return error;
|
||||
};
|
||||
@@ -12,8 +12,13 @@ import { wasmArtifactModule } from "../composition/wasm.js";
|
||||
import { registerWasmTools } from "./registerWasmTools.js";
|
||||
import { snapshotEnvironment } from "../process/snapshotEnvironment.js";
|
||||
import { captureProviderEnvironment } from "./providerEnvironment.js";
|
||||
import { createPeResourcesService } from "../composition/binaryDiagnostics.js";
|
||||
import {
|
||||
createElfNotesService,
|
||||
createPeResourcesService,
|
||||
} from "../composition/binaryDiagnostics.js";
|
||||
import type { ElfNotesService } from "../application/binaryDiagnostics/ElfNotesService.js";
|
||||
import type { PeResourcesService } from "../application/binaryDiagnostics/PeResourcesService.js";
|
||||
import { registerElfNotesTool } from "./registerElfNotesTool.js";
|
||||
import { registerPeResourcesTool } from "./registerPeResourcesTool.js";
|
||||
import type { EvmInterfaceService } from "../application/evm/EvmInterfaceService.js";
|
||||
import {
|
||||
@@ -350,6 +355,7 @@ export const createServer = (
|
||||
const il2cppMetadata =
|
||||
options.il2cppMetadata ?? createIl2cppMetadataService();
|
||||
const peResources = createPeResourcesService();
|
||||
const elfNotes = createElfNotesService();
|
||||
const electronFuses = options.electronFuses ?? createElectronFuseService();
|
||||
const webNetworkCapture =
|
||||
options.webNetworkCapture ?? createWebNetworkCaptureService(environment);
|
||||
@@ -451,6 +457,7 @@ export const createServer = (
|
||||
modelArtifact.close(),
|
||||
gpuCodeObjects.close(),
|
||||
peResources.close(),
|
||||
elfNotes.close(),
|
||||
electronFuses.close(),
|
||||
webNetworkCapture.close(),
|
||||
javascriptRecovery.close?.() ?? Promise.resolve(),
|
||||
@@ -514,6 +521,7 @@ export const createServer = (
|
||||
rustBinary,
|
||||
il2cppMetadata,
|
||||
peResources,
|
||||
elfNotes,
|
||||
electronFuses,
|
||||
webNetworkCapture,
|
||||
};
|
||||
@@ -745,6 +753,7 @@ const registerConfiguredAnalysisTools = (
|
||||
rustBinary,
|
||||
il2cppMetadata,
|
||||
peResources,
|
||||
elfNotes,
|
||||
webNetworkCapture,
|
||||
artifactResources,
|
||||
}: ServerToolContext,
|
||||
@@ -794,6 +803,13 @@ const registerConfiguredAnalysisTools = (
|
||||
recordEvidence,
|
||||
withAdmittedAnalysis,
|
||||
);
|
||||
registerElfNotesTool(
|
||||
server,
|
||||
elfNotes,
|
||||
toolLogger,
|
||||
recordEvidence,
|
||||
withAdmittedAnalysis,
|
||||
);
|
||||
registerGoTools(
|
||||
server,
|
||||
goBinary,
|
||||
@@ -927,6 +943,7 @@ interface ServerToolContext extends ReturnType<typeof createSessionRecorders> {
|
||||
readonly rustBinary: RustBinaryService;
|
||||
readonly il2cppMetadata: Il2cppMetadataService;
|
||||
readonly peResources: PeResourcesService;
|
||||
readonly elfNotes: ElfNotesService;
|
||||
readonly electronFuses: ElectronFuseService;
|
||||
readonly webNetworkCapture: WebNetworkCaptureService;
|
||||
readonly artifactResources: ArtifactResourceScope;
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
import type { EvidenceMcpServer } from "./EvidenceMcpServer.js";
|
||||
import type { ElfNotesService } from "../application/binaryDiagnostics/ElfNotesService.js";
|
||||
import type { EvidenceWriter } from "../application/investigation/InvestigationRecordPort.js";
|
||||
import { toolContract } from "../contracts/toolContracts.js";
|
||||
import type { Logger } from "pino";
|
||||
import { createEvidenceToolHandler } from "./contractToolHandler.js";
|
||||
import type { WithAdmittedAnalysis } from "./analysisAdmission.js";
|
||||
import { toolRegistrationOptions } from "./toolRegistrationOptions.js";
|
||||
|
||||
/** Register the explicit-file ELF note-table workflow and retain its complete Evidence. */
|
||||
export const registerElfNotesTool = (
|
||||
server: EvidenceMcpServer,
|
||||
service: ElfNotesService,
|
||||
logger: Logger,
|
||||
recordEvidence?: EvidenceWriter["recordEvidence"],
|
||||
withAdmittedAnalysis?: WithAdmittedAnalysis,
|
||||
): void => {
|
||||
const contract = toolContract("inspect_elf_notes");
|
||||
const handler = createEvidenceToolHandler(
|
||||
server,
|
||||
(input, options) => service.inspect(input, options),
|
||||
logger,
|
||||
recordEvidence,
|
||||
withAdmittedAnalysis,
|
||||
);
|
||||
server.registerTool(
|
||||
contract.name,
|
||||
toolRegistrationOptions(contract),
|
||||
handler(contract),
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,60 @@
|
||||
import { writeFile } from "node:fs/promises";
|
||||
import { join } from "node:path";
|
||||
import { expect } from "vitest";
|
||||
import { toolContract } from "../../src/contracts/toolContracts.js";
|
||||
import { elfNoteFixture } from "../../src/native/elf/ElfNotes.fixture.js";
|
||||
import { createTestTempDirectory } from "../fixtures/temporaryDirectory.js";
|
||||
import { cliTest } from "../support/cli/cliFixture.js";
|
||||
|
||||
cliTest("reports a GNU build-id through the public CLI", async ({ cli }) => {
|
||||
const root = await createTestTempDirectory("rea-elf-notes-cli-");
|
||||
const path = join(root, "program");
|
||||
await writeFile(path, elfNoteFixture({ ownership: "program" }));
|
||||
const result = await cli.run({
|
||||
arguments: ["inspect-elf-notes", path, "--json"],
|
||||
});
|
||||
expect(result.exitCode, result.stderr).toBe(0);
|
||||
const evidence = toolContract("inspect_elf_notes").outputSchema.parse(
|
||||
result.json,
|
||||
);
|
||||
expect(
|
||||
evidence.normalized_result.entries.find(
|
||||
(entry) => entry.type_name === "gnu_build_id",
|
||||
)?.gnu?.build_id_hex,
|
||||
).toBe("0123456789abcdef0123456789abcdef01234567");
|
||||
});
|
||||
|
||||
cliTest(
|
||||
"resolves a relative ELF path against the working directory",
|
||||
async ({ cli }) => {
|
||||
const root = await createTestTempDirectory("rea-elf-notes-cli-rel-");
|
||||
await writeFile(
|
||||
join(root, "unsigned"),
|
||||
elfNoteFixture({ ownership: "none" }),
|
||||
);
|
||||
const result = await cli.run({
|
||||
arguments: ["inspect-elf-notes", "unsigned", "--json"],
|
||||
cwd: root,
|
||||
});
|
||||
expect(result.exitCode, result.stderr).toBe(0);
|
||||
const evidence = toolContract("inspect_elf_notes").outputSchema.parse(
|
||||
result.json,
|
||||
);
|
||||
expect(evidence.normalized_result.tables).toEqual([]);
|
||||
expect(evidence.subject?.local_path).toBe(join(root, "unsigned"));
|
||||
},
|
||||
);
|
||||
|
||||
cliTest("reports a truncated ELF as invalid input", async ({ cli }) => {
|
||||
const root = await createTestTempDirectory("rea-elf-notes-cli-bad-");
|
||||
const path = join(root, "broken");
|
||||
await writeFile(path, Buffer.from("\x7fELF"));
|
||||
const result = await cli.run({
|
||||
arguments: ["inspect-elf-notes", path, "--json"],
|
||||
});
|
||||
expect(result.exitCode).toBe(1);
|
||||
expect(result.json).toMatchObject({
|
||||
code: "invalid_request",
|
||||
details: { operation: "inspect_elf_notes" },
|
||||
});
|
||||
});
|
||||
@@ -218,6 +218,7 @@ const pathRequests: readonly [string, Record<string, unknown>][] = [
|
||||
{ capture_path: nul, output_directory: "/tmp/exported-scripts" },
|
||||
],
|
||||
["inspect_pe_resources", { path: nul }],
|
||||
["inspect_elf_notes", { path: nul }],
|
||||
["inspect_binary_layout", { path: nul }],
|
||||
["inspect_recorded_crash", { path: nul }],
|
||||
["inspect_wasm_artifact", { path: nul }],
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
import { symlink, writeFile } from "node:fs/promises";
|
||||
import { join } from "node:path";
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { createElfNotesService } from "../../../src/composition/binaryDiagnostics.js";
|
||||
import { elfNoteFixture } from "../../../src/native/elf/ElfNotes.fixture.js";
|
||||
import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js";
|
||||
|
||||
describe("ELF note stable filesystem boundary", () => {
|
||||
it("reports non-file and malformed ELF selections as caller input errors", async () => {
|
||||
const root = await createTestTempDirectory("rea-elf-notes-files-");
|
||||
const path = join(root, "program");
|
||||
const alias = join(root, "alias");
|
||||
const malformed = join(root, "malformed");
|
||||
await writeFile(path, elfNoteFixture());
|
||||
await writeFile(malformed, Buffer.from("not an elf"));
|
||||
await symlink(path, alias);
|
||||
const service = createElfNotesService();
|
||||
for (const [candidate, reason] of [
|
||||
[root, "invalid_format"],
|
||||
[alias, "invalid_format"],
|
||||
[malformed, "invalid_format"],
|
||||
[join(root, "missing"), "invalid_value"],
|
||||
[join(path, "child"), "invalid_value"],
|
||||
] as const) {
|
||||
const result = await service.inspect({ path: candidate });
|
||||
if (result.ok) throw new Error("Expected file acquisition failure");
|
||||
expect(result.error).toMatchObject({
|
||||
_tag: "AnalysisInputError",
|
||||
operation: "inspect_elf_notes",
|
||||
issues: [
|
||||
{
|
||||
path: ["path"],
|
||||
reason,
|
||||
message: expect.any(String),
|
||||
},
|
||||
],
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
it("returns typed resource and cancellation failures", async () => {
|
||||
const root = await createTestTempDirectory("rea-elf-notes-budget-");
|
||||
const path = join(root, "program");
|
||||
await writeFile(path, elfNoteFixture());
|
||||
const service = createElfNotesService();
|
||||
const tooSmall = await service.inspect({ path, max_file_bytes: 64 });
|
||||
if (tooSmall.ok) throw new Error("Expected resource budget failure");
|
||||
expect(tooSmall.error._tag).toBe("AnalysisResourceConstraintError");
|
||||
const cancelled = await service.inspect(
|
||||
{ path },
|
||||
{ signal: AbortSignal.abort() },
|
||||
);
|
||||
if (cancelled.ok) throw new Error("Expected cancellation");
|
||||
expect(cancelled.error._tag).toBe("AnalysisCancelledError");
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,70 @@
|
||||
import { writeFile } from "node:fs/promises";
|
||||
import { join } from "node:path";
|
||||
import { expect } from "vitest";
|
||||
import { toolContract } from "../../../src/contracts/toolContracts.js";
|
||||
import { createServer } from "../../../src/server/createServer.js";
|
||||
import { createTestBinarySession } from "../../fixtures/binarySession.js";
|
||||
import { parseMcpToolError } from "../../fixtures/mcpToolError.js";
|
||||
import { elfNoteFixture } from "../../../src/native/elf/ElfNotes.fixture.js";
|
||||
import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js";
|
||||
import { mcpTest, type TestMcp } from "../../support/mcp/mcpFixture.js";
|
||||
|
||||
const connect = async (mcp: TestMcp) => {
|
||||
const session = createTestBinarySession(() => {
|
||||
throw new Error("ELF note inspection must not start a deep provider");
|
||||
});
|
||||
const client = await mcp.connect(createServer({ kind: "session", session }));
|
||||
return { client, session };
|
||||
};
|
||||
|
||||
mcpTest(
|
||||
"advertises inspect_elf_notes and reads a GNU build-id",
|
||||
async ({ mcp }) => {
|
||||
const { client, session } = await connect(mcp);
|
||||
try {
|
||||
const advertised = (await client.listTools()).tools.find(
|
||||
(tool) => tool.name === "inspect_elf_notes",
|
||||
);
|
||||
expect(advertised?.annotations).toMatchObject({
|
||||
readOnlyHint: false,
|
||||
idempotentHint: true,
|
||||
});
|
||||
const root = await createTestTempDirectory("rea-elf-notes-mcp-");
|
||||
const path = join(root, "program");
|
||||
await writeFile(path, elfNoteFixture({ bits: 32, ownership: "section" }));
|
||||
const response = await client.callTool({
|
||||
name: "inspect_elf_notes",
|
||||
arguments: { path },
|
||||
});
|
||||
expect(response.isError, JSON.stringify(response.content)).not.toBe(true);
|
||||
const evidence = toolContract("inspect_elf_notes").outputSchema.parse(
|
||||
response.structuredContent,
|
||||
);
|
||||
expect(evidence.normalized_result.format).toBe("elf32");
|
||||
expect(
|
||||
evidence.normalized_result.entries.find(
|
||||
(entry) => entry.type_name === "gnu_build_id",
|
||||
)?.gnu?.build_id_hex,
|
||||
).toBe("0123456789abcdef0123456789abcdef01234567");
|
||||
} finally {
|
||||
await session.close();
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
mcpTest("requires an absolute path for inspect_elf_notes", async ({ mcp }) => {
|
||||
const { client, session } = await connect(mcp);
|
||||
try {
|
||||
const response = await client.callTool({
|
||||
name: "inspect_elf_notes",
|
||||
arguments: { path: "program" },
|
||||
});
|
||||
expect(response.isError).toBe(true);
|
||||
expect(parseMcpToolError(response).error).toMatchObject({
|
||||
code: "invalid_request",
|
||||
details: { operation: "inspect_elf_notes" },
|
||||
});
|
||||
} finally {
|
||||
await session.close();
|
||||
}
|
||||
});
|
||||
Reference in New Issue
Block a user