diff --git a/.agents/skills/reverse-engineer-anything/SKILL.md b/.agents/skills/reverse-engineer-anything/SKILL.md index f27a38db40..c9203a2525 100644 --- a/.agents/skills/reverse-engineer-anything/SKILL.md +++ b/.agents/skills/reverse-engineer-anything/SKILL.md @@ -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 diff --git a/.agents/skills/reverse-engineer-anything/references/native-and-artifacts.md b/.agents/skills/reverse-engineer-anything/references/native-and-artifacts.md index 9566ad11e7..b31021e6b1 100644 --- a/.agents/skills/reverse-engineer-anything/references/native-and-artifacts.md +++ b/.agents/skills/reverse-engineer-anything/references/native-and-artifacts.md @@ -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 diff --git a/README.md b/README.md index b0d470e45e..0a8cd49929 100644 --- a/README.md +++ b/README.md @@ -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 | diff --git a/docs/.vitepress/config.ts b/docs/.vitepress/config.ts index 86d825c13d..e043d56db7 100644 --- a/docs/.vitepress/config.ts +++ b/docs/.vitepress/config.ts @@ -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", diff --git a/docs/cli.md b/docs/cli.md index b263b323d3..60496ebe42 100644 --- a/docs/cli.md +++ b/docs/cli.md @@ -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 diff --git a/docs/elf-notes.md b/docs/elf-notes.md new file mode 100644 index 0000000000..2627e4149a --- /dev/null +++ b/docs/elf-notes.md @@ -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. diff --git a/docs/index.md b/docs/index.md index 070b8aad51..0980ad312d 100644 --- a/docs/index.md +++ b/docs/index.md @@ -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) | diff --git a/docs/readme/README_ar.md b/docs/readme/README_ar.md index 435ed3e7b5..bc489aff62 100644 --- a/docs/readme/README_ar.md +++ b/docs/readme/README_ar.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) | diff --git a/docs/readme/README_de.md b/docs/readme/README_de.md index 5fddd80ff3..cb5c7aaf1c 100644 --- a/docs/readme/README_de.md +++ b/docs/readme/README_de.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) | diff --git a/docs/readme/README_el.md b/docs/readme/README_el.md index 93ef1517db..330504ace1 100644 --- a/docs/readme/README_el.md +++ b/docs/readme/README_el.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 | diff --git a/docs/readme/README_es.md b/docs/readme/README_es.md index 02dc7cda3e..f21d11224a 100644 --- a/docs/readme/README_es.md +++ b/docs/readme/README_es.md @@ -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) | diff --git a/docs/readme/README_fa.md b/docs/readme/README_fa.md index 37a3355d96..a0b4ea0c3f 100644 --- a/docs/readme/README_fa.md +++ b/docs/readme/README_fa.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) | diff --git a/docs/readme/README_fr.md b/docs/readme/README_fr.md index 40083bd489..b5b0011798 100644 --- a/docs/readme/README_fr.md +++ b/docs/readme/README_fr.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) | diff --git a/docs/readme/README_id.md b/docs/readme/README_id.md index b8906ba2b3..d06eb7d251 100644 --- a/docs/readme/README_id.md +++ b/docs/readme/README_id.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) | diff --git a/docs/readme/README_it.md b/docs/readme/README_it.md index 6831919be8..7d746d434f 100644 --- a/docs/readme/README_it.md +++ b/docs/readme/README_it.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) | diff --git a/docs/readme/README_ja.md b/docs/readme/README_ja.md index 325287fa4c..8764cec73a 100644 --- a/docs/readme/README_ja.md +++ b/docs/readme/README_ja.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) | diff --git a/docs/readme/README_ko.md b/docs/readme/README_ko.md index 462fd08e21..2a75886fc5 100644 --- a/docs/readme/README_ko.md +++ b/docs/readme/README_ko.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 한도 적용) | diff --git a/docs/readme/README_ml.md b/docs/readme/README_ml.md index 6dd281e900..c0b756dcc9 100644 --- a/docs/readme/README_ml.md +++ b/docs/readme/README_ml.md @@ -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+ | diff --git a/docs/readme/README_pl.md b/docs/readme/README_pl.md index 1ae828eb10..d7a10010bd 100644 --- a/docs/readme/README_pl.md +++ b/docs/readme/README_pl.md @@ -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) | diff --git a/docs/readme/README_pt-BR.md b/docs/readme/README_pt-BR.md index 1390281366..3a1558406a 100644 --- a/docs/readme/README_pt-BR.md +++ b/docs/readme/README_pt-BR.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) | diff --git a/docs/readme/README_ru.md b/docs/readme/README_ru.md index 42234a8c06..65935f2d58 100644 --- a/docs/readme/README_ru.md +++ b/docs/readme/README_ru.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) | diff --git a/docs/readme/README_th.md b/docs/readme/README_th.md index f8203910e9..4e3838a7d1 100644 --- a/docs/readme/README_th.md +++ b/docs/readme/README_th.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) | diff --git a/docs/readme/README_tr.md b/docs/readme/README_tr.md index 6758279259..f55ccf71fe 100644 --- a/docs/readme/README_tr.md +++ b/docs/readme/README_tr.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+ | diff --git a/docs/readme/README_uk.md b/docs/readme/README_uk.md index 59c29dbe16..56a3fd884f 100644 --- a/docs/readme/README_uk.md +++ b/docs/readme/README_uk.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) | diff --git a/docs/readme/README_vi.md b/docs/readme/README_vi.md index 72f66b8bdc..a528d4f0e3 100644 --- a/docs/readme/README_vi.md +++ b/docs/readme/README_vi.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) | diff --git a/docs/readme/README_zh-TW.md b/docs/readme/README_zh-TW.md index 777dc0d58d..cd2048bb90 100644 --- a/docs/readme/README_zh-TW.md +++ b/docs/readme/README_zh-TW.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) | diff --git a/docs/readme/README_zh.md b/docs/readme/README_zh.md index e7fe365fba..b5f7a09bf3 100644 --- a/docs/readme/README_zh.md +++ b/docs/readme/README_zh.md @@ -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 检查读取提供的文件,不运行应用。运行时捕获会以你的用户权限运行目标或与之交互;各运行时指南说明了具体影响。 diff --git a/scripts/lib/catalog-builders.mjs b/scripts/lib/catalog-builders.mjs index 2ac0bb9894..31557cea7c 100644 --- a/scripts/lib/catalog-builders.mjs +++ b/scripts/lib/catalog-builders.mjs @@ -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: diff --git a/src/application/CapabilityInventory.ts b/src/application/CapabilityInventory.ts index cd3394ddd8..bbcd1a953f 100644 --- a/src/application/CapabilityInventory.ts +++ b/src/application/CapabilityInventory.ts @@ -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 { diff --git a/src/application/InvestigationProviders.ts b/src/application/InvestigationProviders.ts index 00fcd5642d..3ad47a9621 100644 --- a/src/application/InvestigationProviders.ts +++ b/src/application/InvestigationProviders.ts @@ -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", diff --git a/src/application/binaryDiagnostics/ElfNotesPort.ts b/src/application/binaryDiagnostics/ElfNotesPort.ts new file mode 100644 index 0000000000..ad3c5a3734 --- /dev/null +++ b/src/application/binaryDiagnostics/ElfNotesPort.ts @@ -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; + inspect( + input: InspectElfNotesInput, + options?: ExecutionOptions, + ): Promise>; +} diff --git a/src/application/binaryDiagnostics/ElfNotesService.test.ts b/src/application/binaryDiagnostics/ElfNotesService.test.ts new file mode 100644 index 0000000000..7f2fa63f9a --- /dev/null +++ b/src/application/binaryDiagnostics/ElfNotesService.test.ts @@ -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); +}); diff --git a/src/application/binaryDiagnostics/ElfNotesService.ts b/src/application/binaryDiagnostics/ElfNotesService.ts new file mode 100644 index 0000000000..e34f4effb3 --- /dev/null +++ b/src/application/binaryDiagnostics/ElfNotesService.ts @@ -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 { + return this.provider.close?.() ?? Promise.resolve(); + } + + async inspect( + rawInput: unknown, + options?: ExecutionOptions, + ): Promise> { + 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), + ) + ); +}; diff --git a/src/cli/binaryDiagnosticsCommands.ts b/src/cli/binaryDiagnosticsCommands.ts index 7b1eb9173f..9e125c22d9 100644 --- a/src/cli/binaryDiagnosticsCommands.ts +++ b/src/cli/binaryDiagnosticsCommands.ts @@ -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, ): 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: diff --git a/src/cliCommandNames.ts b/src/cliCommandNames.ts index 27eeca5bdd..45b847f37a 100644 --- a/src/cliCommandNames.ts +++ b/src/cliCommandNames.ts @@ -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", diff --git a/src/composition/binaryDiagnostics.ts b/src/composition/binaryDiagnostics.ts index 4d9cfc69cb..e17ed4e22c 100644 --- a/src/composition/binaryDiagnostics.ts +++ b/src/composition/binaryDiagnostics.ts @@ -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 = process.env, diff --git a/src/contracts/native/binaryDiagnosticsToolContracts.ts b/src/contracts/native/binaryDiagnosticsToolContracts.ts index 0e31d8ef35..9c63f3aa5a 100644 --- a/src/contracts/native/binaryDiagnosticsToolContracts.ts +++ b/src/contracts/native/binaryDiagnosticsToolContracts.ts @@ -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"), diff --git a/src/contracts/toolEffects.test.ts b/src/contracts/toolEffects.test.ts index 0403a6d963..8c448d9085 100644 --- a/src/contracts/toolEffects.test.ts +++ b/src/contracts/toolEffects.test.ts @@ -27,6 +27,7 @@ const EXECUTION_WORDING_EXCEPTIONS: Readonly> = { 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", diff --git a/src/contracts/toolEffects.ts b/src/contracts/toolEffects.ts index 24d8e2989a..8a855a2853 100644 --- a/src/contracts/toolEffects.ts +++ b/src/contracts/toolEffects.ts @@ -253,6 +253,7 @@ export const TOOL_EFFECTS: Readonly> = { }), inspect_byte_entropy: evidence, inspect_pe_resources: evidence, + inspect_elf_notes: evidence, inspect_analysis_view: evidence, inspect_firmware_regions: effects({ mutatesSession: true, diff --git a/src/domain/artifactOperationError.ts b/src/domain/artifactOperationError.ts index e16e767707..a4dce9a6fa 100644 --- a/src/domain/artifactOperationError.ts +++ b/src/domain/artifactOperationError.ts @@ -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" diff --git a/src/domain/elfFixedHeader.ts b/src/domain/elfFixedHeader.ts index 38ae7c01da..030d7952c8 100644 --- a/src/domain/elfFixedHeader.ts +++ b/src/domain/elfFixedHeader.ts @@ -70,7 +70,7 @@ export const readElfFixedHeader = ( }); }; -const elfArchitecture = ( +export const elfArchitecture = ( machine: number, ): Exclude | undefined => { switch (machine) { diff --git a/src/domain/native/elfNotes.test.ts b/src/domain/native/elfNotes.test.ts new file mode 100644 index 0000000000..9aa98f45d2 --- /dev/null +++ b/src/domain/native/elfNotes.test.ts @@ -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([]); +}); diff --git a/src/domain/native/elfNotes.ts b/src/domain/native/elfNotes.ts new file mode 100644 index 0000000000..b6d60974de --- /dev/null +++ b/src/domain/native/elfNotes.ts @@ -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; +export type InspectElfNotesInput = z.infer; diff --git a/src/native/elf/ElfNoteParser.test.ts b/src/native/elf/ElfNoteParser.test.ts new file mode 100644 index 0000000000..730fae6234 --- /dev/null +++ b/src/native/elf/ElfNoteParser.test.ts @@ -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((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); +}); diff --git a/src/native/elf/ElfNoteParser.ts b/src/native/elf/ElfNoteParser.ts new file mode 100644 index 0000000000..80474c552f --- /dev/null +++ b/src/native/elf/ElfNoteParser.ts @@ -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; + readonly scans: Map; +} + +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> = { + 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[] = []; + 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 => { + 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), + }; +}; diff --git a/src/native/elf/ElfNotes.fixture.ts b/src/native/elf/ElfNotes.fixture.ts new file mode 100644 index 0000000000..a7ac803800 --- /dev/null +++ b/src/native/elf/ElfNotes.fixture.ts @@ -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); +}; diff --git a/src/native/elf/ElfNotesProvider.test.ts b/src/native/elf/ElfNotesProvider.test.ts new file mode 100644 index 0000000000..22a733bc88 --- /dev/null +++ b/src/native/elf/ElfNotesProvider.test.ts @@ -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); + }); +}); diff --git a/src/native/elf/ElfNotesProvider.ts b/src/native/elf/ElfNotesProvider.ts new file mode 100644 index 0000000000..06c25408fe --- /dev/null +++ b/src/native/elf/ElfNotesProvider.ts @@ -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 { + 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; +}; diff --git a/src/server/createServer.ts b/src/server/createServer.ts index 67f7a4e0d9..4d81d82340 100644 --- a/src/server/createServer.ts +++ b/src/server/createServer.ts @@ -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 { readonly rustBinary: RustBinaryService; readonly il2cppMetadata: Il2cppMetadataService; readonly peResources: PeResourcesService; + readonly elfNotes: ElfNotesService; readonly electronFuses: ElectronFuseService; readonly webNetworkCapture: WebNetworkCaptureService; readonly artifactResources: ArtifactResourceScope; diff --git a/src/server/registerElfNotesTool.ts b/src/server/registerElfNotesTool.ts new file mode 100644 index 0000000000..c383ad2959 --- /dev/null +++ b/src/server/registerElfNotesTool.ts @@ -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), + ); +}; diff --git a/tests/acceptance/elfNotesCli.test.ts b/tests/acceptance/elfNotesCli.test.ts new file mode 100644 index 0000000000..cc797ec2c1 --- /dev/null +++ b/tests/acceptance/elfNotesCli.test.ts @@ -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" }, + }); +}); diff --git a/tests/acceptance/tools/localToolContracts.test.ts b/tests/acceptance/tools/localToolContracts.test.ts index 980638aa5b..f0bb73c6c9 100644 --- a/tests/acceptance/tools/localToolContracts.test.ts +++ b/tests/acceptance/tools/localToolContracts.test.ts @@ -218,6 +218,7 @@ const pathRequests: readonly [string, Record][] = [ { 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 }], diff --git a/tests/boundary/filesystem/elfNotes.test.ts b/tests/boundary/filesystem/elfNotes.test.ts new file mode 100644 index 0000000000..a3c431da45 --- /dev/null +++ b/tests/boundary/filesystem/elfNotes.test.ts @@ -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"); + }); +}); diff --git a/tests/boundary/mcp/elfNotesMcp.test.ts b/tests/boundary/mcp/elfNotesMcp.test.ts new file mode 100644 index 0000000000..7bea4702cc --- /dev/null +++ b/tests/boundary/mcp/elfNotesMcp.test.ts @@ -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(); + } +});