feat(pe): inspect import and delay-load tables (#6509)

* feat(pe): inspect import and delay-load tables

Add inspect_pe_imports / inspect-pe-imports to read PE32/PE32+ import
and delay-load directories without executing the image. The report keeps
DLL names, named and ordinal functions, ILT/IAT RVAs, and a pefile
import hash of the standard import table. A missing directory is
observed absence, not proof the image never imported APIs.

* fix(pe): default PE import fixture options

Allow peImportImage() without an argument so CLI and filesystem tests
can use the KERNEL32 sample without repeating an empty options object.

* test(pe): keep import path cases on the dedicated MCP contract

inspect_pe_imports already rejects relative MCP paths in its own
boundary test. Leave the shared absolute-path matrix to tools whose
handler issue projection matches that suite's expected shape.

* fix(native): index virtual-to-file mapping lookups

mappedFileOffset scans every mapping twice per span, so a parser that
translates one span per table entry costs sections x entries. Add
fileMappingLookup, which builds an O(n log n) interval index once and
answers each span in O(log n) with the same accept, reject and
translation results. A differential test checks it against
mappedFileOffset over randomized overlapping mappings and zero fills.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ra7LM2NyQ7DumdssxbUq5g

* fix(pe): report partial import evidence and exact pefile hashes

- Withhold imphash with an explicit reason whenever the standard import
  table is partial (unreadable names or thunks, missing tables), or when
  pefile 2024.8.26 would hash a different row sequence: invalid or
  truncated names, ordinal 0, descriptors without functions, tables its
  bounds or repeat/spread heuristics discard, and its 8192-thunk import
  symbol cap. Delay-load issues do not withhold it.
- Hash ordinal imports with pefile's complete ordlookup tables and its
  ord<decimal> fallback instead of declining unmapped ordinals.
- Report a nonterminal descriptor whose FirstThunk is zero, and a
  delay-load descriptor whose IAT or INT RVA is zero, as malformed_entry
  partial coverage instead of a complete empty inventory.
- Directory evidence lists the file spans actually consumed for the
  descriptor array and terminator; discontiguous mappings produce
  separate spans and separate Evidence ranges.
- Carry exhausted limits as PeImportLimitFailure.limit and derive
  recovery from it; the fixed 16 MiB retained-text ceiling reports a
  fixed allowance instead of max_file_bytes advice.
- Translate RVAs through the indexed mapping lookup and yield with a
  cancellation check every 1024 table entries.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ra7LM2NyQ7DumdssxbUq5g

* fix(pe): read only own ordinal tables for DLL names

DLL names such as constructor and __proto__ pass pefile's name rule but
resolved inherited object members, throwing before the ord<n> fallback.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ra7LM2NyQ7DumdssxbUq5g

* fix(native): preserve PE import facts and prove hash compatibility

* test(native): verify PE import workflows and measured costs

* refactor(native): narrow PE facts across the container union

* docs(native): record integrated PE import validation

* fix(native): bound every PE import RVA span

* docs(native): measure RVA guard completed workflows

* fix(pe): reconcile import coverage with retained rows

* fix(pe): retain partial imports at descriptor mapping gaps

---------

Co-authored-by: Matt Van Horn <455140+mvanhorn@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: gensokyo-dev <342580529+gensokyo-dev@users.noreply.github.com>
This commit is contained in:
authored and GitHub committed 2026-10-24 11:44:12 +08:00
1 parent f508a1a47e
commit 4ef916da03
66 files changed
+4824 -31

No files matched your search

@@ -251,6 +251,14 @@ explicit path or endpoint and do not need it.
absence of that table, not proof the image is unsigned in every catalog sense.
Signatures are not verified. See the
[PE certificate guide](https://github.com/morluto/rea/blob/main/docs/pe-certificates.md).
- PE import and delay-load tables: `inspect_pe_imports` with the explicit
local PE32/PE32+ file. This built-in target-free inspection requires no
Windows SDK. Preserve DLL names, named and ordinal functions, ILT/IAT RVAs,
delay-load descriptors, and the pefile import hash of the standard import
table. A missing import directory is observed absence of that table, not
proof the image never imported APIs through another table. Bound snapshots
and loaded IAT contents remain unobserved. See the
[PE import guide](https://github.com/morluto/rea/blob/main/docs/pe-imports.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
@@ -112,6 +112,19 @@ not proof the image is unsigned in every catalog or overlay sense. Signatures,
chains, timestamps, and Authenticode digest coverage remain unobserved. See
[PE Authenticode certificate tables](https://github.com/morluto/rea/blob/main/docs/pe-certificates.md).
## PE import and delay-load tables
Use `inspect_pe_imports` for an explicit local PE32/PE32+ image when the
question concerns imported DLL and function names, ordinals, delay-load
descriptors, or the pefile import hash. No `open_binary`, Windows SDK, or
target execution is required. The CLI equivalent is `inspect-pe-imports PATH`.
Preserve artifact SHA-256, directory file ranges, original DLL spellings, and
ILT/IAT RVAs. A missing `IMAGE_DIRECTORY_ENTRY_IMPORT` directory is observed
absence of that table, not proof the image never imported APIs through another
table or overlay. Bound snapshots, loaded IAT contents, and export-forward
resolution remain unobserved. See
[PE import and delay-load tables](https://github.com/morluto/rea/blob/main/docs/pe-imports.md).
## ELF note tables
Use `inspect_elf_notes` for an explicit local ELF32/ELF64 image when the
+1
View File
@@ -192,6 +192,7 @@ Additional tools and host support depend on the target:
| 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) |
| PE certificates | Authenticode WIN_CERTIFICATE entries and PKCS#7 signer/X.509 identity fields | Built-in static PE inspection; [PE certificates](docs/pe-certificates.md) |
| PE imports | Import and delay-load DLL names, ordinals, IAT RVAs and pefile import hash | Built-in static PE inspection; [PE imports](docs/pe-imports.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) |
| Erlang BEAM | IFF chunks, atoms, exports, imports, lambdas and the Code header | Built-in static BEAM inspection; [Erlang BEAM](docs/erlang-beam.md) |
| WASM artifact | Validation, sections, exact import/export WAT forms and decoded WAT | Caller-supplied WABT 1.0.42; `inspect_wasm_artifact` |
+1
View File
@@ -16,6 +16,7 @@ notices supplied with the software you actually obtain or redistribute.
| Material | Use and licensing records |
| ------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| npm dependencies | [Complete inventory](licenses/npm-dependencies.txt) of every entry in `package-lock.json`, including transitive, optional platform, development, and website dependencies. [Runtime notices](licenses/npm-runtime-notices.txt) preserve the copyright, license, and NOTICE documents from integrity-verified release archives, plus the MIT notice for Rolldown's generated runtime helpers. Development dependencies are inventoried but are not all distributed in REA's npm package. |
| pefile ordinal tables | Import-hash ordinal names are derived from [pefile 2024.8.26](https://github.com/erocarrera/pefile/tree/4b3b1e2e568a88d4f1897d694d684f23d9e270c4), copyright 2004–2024 Ero Carrera. Preserve the [MIT license](licenses/pefile-2024.8.26-LICENSE.txt). The Python package is not a runtime dependency. |
| NativeAOT parser | REA's derived parser uses the MIT-licensed [ghidra-nativeaot](https://github.com/Washi1337/ghidra-nativeaot/tree/effeb734fc570c32650f88b159608979dc7b423e) work of Washi and the NativeAOT layout definitions in [dotnet/runtime](https://github.com/dotnet/runtime/tree/a3b2d40328be0be3f8dd43f0e5cc925b8827b044). Preserve [Washi's MIT notice](licenses/nativeaot-parser-MIT.txt) and the [.NET Foundation MIT notice](licenses/dotnet-runtime-MIT.txt). |
| EVM analysis | `evmole@0.9.3`, MIT, copyright 2023 Maxim Andreev. The published npm archive omits its license document; REA retains the [original upstream license](third_party/evmole/LICENSE) and [provenance](third_party/evmole/README.md). It is also included in the runtime notices. |
| Windows native addon | The release addon is compiled with MinGW-w64 and statically links GCC runtime libraries. Preserve the [MinGW-w64 runtime notices](licenses/MinGW-w64-runtime.txt), [GPLv3 text](licenses/GPL-3.0.txt), and [GCC Runtime Library Exception 3.1](licenses/GCC-Runtime-Library-Exception-3.1.txt). The exception governs eligible compiled combinations; it does not relicense the runtime itself under MIT. The Node-API build headers are from Node 22.19.0; its [original license and component notices](licenses/Node-22.19.0-LICENSE.txt) are retained. Node itself is supplied by the user. Snapshot provenance and release-toolchain review requirements are in [licenses/README.md](licenses/README.md). |
+1
View File
@@ -33,6 +33,7 @@ export default defineConfig({
{ text: "Model artifacts", link: "/model-artifacts" },
{ text: "Rust dependencies", link: "/rust-binaries" },
{ text: "PE certificates", link: "/pe-certificates" },
{ text: "PE imports", link: "/pe-imports" },
{ text: "ELF notes", link: "/elf-notes" },
{ text: "CPython pyc files", link: "/python-pyc" },
{ text: "Erlang BEAM", link: "/erlang-beam" },
+11
View File
@@ -290,6 +290,17 @@ embedded X.509 serial, issuer, subject, and validity fields. A missing security
directory is observed absence of that table. See
[PE Authenticode certificate tables](pe-certificates.md).
Inspect PE import and delay-load tables without executing the image:
```bash
rea inspect-pe-imports /absolute/path/to/application.exe --json
```
The workflow reports DLL names, named and ordinal functions, delay-load
descriptors, and a pefile-compatible import hash of the standard import table.
A missing import directory is observed absence of that table. See
[PE import and delay-load tables](pe-imports.md).
Inspect ELF PT_NOTE and SHT_NOTE tables without executing the image:
```bash
+1 -1
View File
@@ -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), [CPython pyc files](python-pyc.md), [PE certificates](pe-certificates.md), [ELF notes](elf-notes.md), [Erlang BEAM](erlang-beam.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), [CPython pyc files](python-pyc.md), [PE certificates](pe-certificates.md), [PE imports](pe-imports.md), [ELF notes](elf-notes.md), [Erlang BEAM](erlang-beam.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) |
+73
View File
@@ -0,0 +1,73 @@
# PE import and delay-load tables
`inspect_pe_imports` / `inspect-pe-imports` answers whether one selected local
PE32/PE32+ image declares an `IMAGE_DIRECTORY_ENTRY_IMPORT` table, which DLLs
and functions that table names, which delay-load descriptors are present, and
the pefile-compatible import hash of the standard import table. It reads the
file without executing it and does not require a Windows SDK or disassembler.
```bash
rea inspect-pe-imports /absolute/path/to/application.exe --json
```
The CLI resolves relative paths against the caller's working directory. MCP
and the advertised input schema require an absolute filesystem path and
preserve that exact selection:
```json
{
"name": "inspect_pe_imports",
"arguments": { "path": "/artifacts/application.exe" }
}
```
## Observations
The inline Evidence result contains the artifact path, SHA-256, and byte length.
`import_directory` and `delay_import_directory` report the data directory RVA
and size, or `null` when a directory is absent or zero. Their `locations` are
the file spans actually read for the descriptor array and its terminator. When
consecutive descriptors map to discontiguous file bytes (for example across a
section boundary), each span is reported separately, and each becomes its own
Evidence file-offset range.
If the first descriptor cannot be examined, the directory declaration remains
present with no consumed `locations`, and coverage is partial. Descriptors,
thunks and NUL-terminated names stay within PE's 32-bit RVA space, including
PE32+ images; earlier valid rows survive an overflowing descriptor walk.
Each `imports[]` item reports the DLL name, descriptor RVAs, and named or
ordinal functions with ILT/IAT RVAs. `delay_imports[]` reports RVA-based
delay-load descriptors separately. `imphash` is pefile's `get_imphash()` over
the standard import table only; delay-load rows do not participate. Ordinal
imports use pefile's ordinal name tables and otherwise its `ord<decimal>`
fallback.
`imphash.md5` is `null` with a `reason` whenever the value would not be
pefile's hash of this image: the standard import table is partial, or pefile
would read a different row sequence (names it treats as invalid or truncates,
ordinal 0, descriptors without functions, or tables its bounds and import
symbol cap truncate). Delay-load issues do not withhold the hash.
A missing import directory is observed absence of that table. It is not proof
the image never imported APIs through another table or overlay.
## Limits and failures
The selected file is limited to 64 MiB by default (512 MiB maximum). At most
256 import and delay-load descriptors are examined by default (4096 maximum),
and at most 8192 functions across every table (262144 maximum). Malformed PE
headers and exhausted DLL or function budgets fail without partial success.
Decoded DLL and function names are limited to a fixed 16 MiB of retained text
with no runtime override; `max_file_bytes`, `max_dlls` and `max_functions` do
not raise it. Resource failures report the exhausted allowance as `limit`
(`dlls`, `functions` or `retained_text`) with matching recovery.
Unreadable DLL names, hint/name entries, or thunks are reported as partial
coverage with `issues`. A descriptor whose required table RVA is zero (a
standard `FirstThunk`, or a delay-load import address or name table) is
reported as `malformed_entry` with no functions, never as a complete empty
inventory. Delay-load descriptors that store virtual addresses rather than RVAs
are recorded as `unsupported_form` and are not decoded.
This workflow does not resolve bound imports, loaded IAT contents, or
export-forward names in another DLL.
+1
View File
@@ -154,6 +154,7 @@ npx rea-agents@latest setup
| الملفات الثنائية للشيفرة الأصلية | شيفرة شبه برمجية، وتعليمات تجميع، وسلاسل نصية، ورموز، واستدعاءات، ومراجع | Hopper أو Ghidra أو IDA؛ [تحليل الشيفرة الأصلية](../native-investigation.md) |
| بنية ELF دون تشغيل | الأقسام والمقاطع والرموز ومعلومات إعادة التموضع الأصلية، وآليات الحماية المحتملة المستدلّ عليها بالفحص الثابت | pwntools يوفّره المستدعي على Linux x64؛ [تشخيص الملفات الثنائية](../binary-diagnostics.md) |
| PE certificates | Authenticode WIN_CERTIFICATE entries and PKCS#7 signer/X.509 identity fields | Built-in static PE inspection; [PE certificates](../pe-certificates.md) |
| PE imports | Import and delay-load DLL names, ordinals, IAT RVAs and pefile import hash | Built-in static PE inspection; [PE imports](../pe-imports.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) |
+1
View File
@@ -154,6 +154,7 @@ REA benötigt Node.js 22.x (>=22.23.4), 24.x (>=24.22.1) oder 26+ (>=26.12.1) so
| Native Binärdateien | Pseudocode, Assemblercode, Zeichenketten, Symbole, Aufrufe und Verweise | Hopper, Ghidra oder IDA; [native Analyse](../native-investigation.md) |
| 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) |
| PE certificates | Authenticode WIN_CERTIFICATE entries and PKCS#7 signer/X.509 identity fields | Built-in static PE inspection; [PE certificates](../pe-certificates.md) |
| PE imports | Import and delay-load DLL names, ordinals, IAT RVAs and pefile import hash | Built-in static PE inspection; [PE imports](../pe-imports.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) |
+1
View File
@@ -156,6 +156,7 @@ npx rea-agents@latest setup
| Δυαδικά αρχεία Go | Ενσωματωμένη έκδοση μεταγλωττιστή, modules, αντικαταστάσεις και ρυθμίσεις μεταγλώττισης | Ενσωματωμένη στατική εξέταση ELF/PE/thin Mach-O· δεν απαιτείται εγκατάσταση του Go |
| Δυαδικά αρχεία Rust | Ενσωματωμένα ονόματα, εκδόσεις, πηγές και σχέσεις εξάρτησης των crates από το cargo-auditable | Ενσωματωμένη στατική εξέταση ELF/PE/thin Mach-O· [δυαδικά αρχεία Rust](../rust-binaries.md) |
| PE certificates | Authenticode WIN_CERTIFICATE entries and PKCS#7 signer/X.509 identity fields | Built-in static PE inspection; [PE certificates](../pe-certificates.md) |
| PE imports | Import and delay-load DLL names, ordinals, IAT RVAs and pefile import hash | Built-in static PE inspection; [PE imports](../pe-imports.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) |
| Erlang BEAM | IFF chunks, atoms, exports, and tables | Built-in static BEAM inspection; [Erlang BEAM](../erlang-beam.md) |
| Αρχείο WASM | Έλεγχο εγκυρότητας, ενότητες, ακριβείς μορφές εισαγωγών/εξαγωγών σε WAT και αποκωδικοποιημένο WAT | WABT 1.0.42 που παρέχει ο χρήστης· `inspect_wasm_artifact` |
+1
View File
@@ -154,6 +154,7 @@ REA requiere Node.js 22.x (>=22.23.4), 24.x (>=24.22.1) o 26+ (>=26.12.1), adem
| Binarios nativos | Pseudocódigo, ensamblador, cadenas, símbolos, llamadas y referencias | Hopper, Ghidra o IDA; [análisis nativo](../native-investigation.md) |
| 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) |
| PE certificates | Authenticode WIN_CERTIFICATE entries and PKCS#7 signer/X.509 identity fields | Built-in static PE inspection; [PE certificates](../pe-certificates.md) |
| PE imports | Import and delay-load DLL names, ordinals, IAT RVAs and pefile import hash | Built-in static PE inspection; [PE imports](../pe-imports.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) |
+1
View File
@@ -146,6 +146,7 @@ REA به Node.js 22.x (>=22.23.4)، 24.x (>=24.22.1) یا 26+ (>=26.12.1)، به
| باینری‌های بومی | شبه‌کد، اسمبلی، رشته‌ها، نمادها، فراخوانی‌ها و ارجاعات | Hopper، Ghidra یا IDA؛ [تحلیل بومی](../native-investigation.md) |
| ساختار ELF آفلاین | بخش‌ها، سگمنت‌ها، نمادها و relocationهای اصلی و گزینه‌های احتمالی کاهش آسیب‌پذیری ایستا | pwntools تأمین‌شده توسط فراخواننده روی Linux x64؛ [تشخیص باینری](../binary-diagnostics.md) |
| PE certificates | Authenticode WIN_CERTIFICATE entries and PKCS#7 signer/X.509 identity fields | Built-in static PE inspection; [PE certificates](../pe-certificates.md) |
| PE imports | Import and delay-load DLL names, ordinals, IAT RVAs and pefile import hash | Built-in static PE inspection; [PE imports](../pe-imports.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) |
+1
View File
@@ -153,6 +153,7 @@ REA nécessite Node.js 22.x (>=22.23.4), 24.x (>=24.22.1) ou 26+ (>=26.12.1), ai
| Binaires natifs | Pseudo-code, assembleur, chaînes, symboles, appels et références | Hopper, Ghidra ou IDA ; [analyse native](../native-investigation.md) |
| Structure ELF hors ligne | Sections, segments, symboles et relocations d'origine, et protections statiques candidates | pwntools fourni par l'utilisateur sous Linux x64 ; [diagnostics binaires](../binary-diagnostics.md) |
| PE certificates | Authenticode WIN_CERTIFICATE entries and PKCS#7 signer/X.509 identity fields | Built-in static PE inspection; [PE certificates](../pe-certificates.md) |
| PE imports | Import and delay-load DLL names, ordinals, IAT RVAs and pefile import hash | Built-in static PE inspection; [PE imports](../pe-imports.md) |
| 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) |
+1
View File
@@ -156,6 +156,7 @@ Alat tambahan dan dukungan sistem host bergantung pada target yang dianalisis:
| Biner native | Pseudocode, assembly, string, simbol, pemanggilan, dan referensi | Hopper, Ghidra, atau IDA; [analisis native](../native-investigation.md) |
| Tata letak ELF offline | Bagian, segmen, simbol/relokasi asli, dan kandidat mitigasi statis | pwntools yang disediakan pemanggil pada Linux x64; [diagnostik biner](../binary-diagnostics.md) |
| PE certificates | Authenticode WIN_CERTIFICATE entries and PKCS#7 signer/X.509 identity fields | Built-in static PE inspection; [PE certificates](../pe-certificates.md) |
| PE imports | Import and delay-load DLL names, ordinals, IAT RVAs and pefile import hash | Built-in static PE inspection; [PE imports](../pe-imports.md) |
| 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) |
+1
View File
@@ -153,6 +153,7 @@ REA richiede Node.js 22.x (>=22.23.4), 24.x (>=24.22.1) o 26+ (>=26.12.1), oltre
| Binari nativi | Pseudocodice, assembly, stringhe, simboli, chiamate e riferimenti | Hopper, Ghidra o IDA; [analisi nativa](../native-investigation.md) |
| Layout ELF offline | Sezioni, segmenti, simboli/relocazioni originali e candidati mitigazioni statiche | pwntools fornito dal chiamante su Linux x64; [diagnostica binaria](../binary-diagnostics.md) |
| PE certificates | Authenticode WIN_CERTIFICATE entries and PKCS#7 signer/X.509 identity fields | Built-in static PE inspection; [PE certificates](../pe-certificates.md) |
| PE imports | Import and delay-load DLL names, ordinals, IAT RVAs and pefile import hash | Built-in static PE inspection; [PE imports](../pe-imports.md) |
| 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) |
+1
View File
@@ -154,6 +154,7 @@ REA には Node.js 22.x(>=22.23.4)、24.x(>=24.22.1)、または 26+ (>=
| ネイティブバイナリ | 疑似コード、アセンブリ、文字列、シンボル、呼び出し、参照 | Hopper、Ghidra、または IDA;[ネイティブ分析](../native-investigation.md) |
| オフライン ELF レイアウト | セクション、セグメント、元のシンボル/再配置情報、静的解析によるセキュリティ対策の候補 | Linux x64 で利用者が用意する pwntools;[バイナリ診断](../binary-diagnostics.md) |
| PE certificates | Authenticode WIN_CERTIFICATE entries and PKCS#7 signer/X.509 identity fields | Built-in static PE inspection; [PE certificates](../pe-certificates.md) |
| PE imports | Import and delay-load DLL names, ordinals, IAT RVAs and pefile import hash | Built-in static PE inspection; [PE imports](../pe-imports.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) |
+1
View File
@@ -156,6 +156,7 @@ REA에는 Node.js 22.x(>=22.23.4), 24.x(>=24.22.1) 또는 26+ (>=26.12.1)와 npm
| Go 바이너리 | 내장된 컴파일러 버전, 모듈, `replace` 지시어, 빌드 설정 | REA 내장 ELF/PE/thin Mach-O 정적 검사; Go 설치 불필요 |
| Rust 바이너리 | cargo-auditable이 내장한 크레이트 이름, 버전, 소스, 의존성 관계 | REA 내장 ELF/PE/thin Mach-O 정적 검사; [Rust 바이너리](../rust-binaries.md) |
| PE certificates | Authenticode WIN_CERTIFICATE entries and PKCS#7 signer/X.509 identity fields | Built-in static PE inspection; [PE certificates](../pe-certificates.md) |
| PE imports | Import and delay-load DLL names, ordinals, IAT RVAs and pefile import hash | Built-in static PE inspection; [PE imports](../pe-imports.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) |
| Erlang BEAM | IFF chunks, atoms, exports, and tables | Built-in static BEAM inspection; [Erlang BEAM](../erlang-beam.md) |
| WASM 아티팩트 | 유효성 검사, 섹션, import/export의 정확한 WAT 형식, 디코딩된 WAT | 호출자 제공 WABT 1.0.42; `inspect_wasm_artifact` |
+1
View File
@@ -156,6 +156,7 @@ REA-യ്ക്ക് Node.js 22.x (>=22.23.4), 24.x (>=24.22.1), അല്ല
| 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) |
| PE certificates | Authenticode WIN_CERTIFICATE entries and PKCS#7 signer/X.509 identity fields | Built-in static PE inspection; [PE certificates](../pe-certificates.md) |
| PE imports | Import and delay-load DLL names, ordinals, IAT RVAs and pefile import hash | Built-in static PE inspection; [PE imports](../pe-imports.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) |
| Erlang BEAM | IFF chunks, atoms, exports, and tables | Built-in static BEAM inspection; [Erlang BEAM](../erlang-beam.md) |
| WASM artifact | Validation, sections, കൃത്യമായ import/export WAT രൂപങ്ങൾ, decoded WAT | Caller നൽകുന്ന WABT 1.0.42; `inspect_wasm_artifact` |
+1
View File
@@ -154,6 +154,7 @@ REA wymaga Node.js 22.x (>=22.23.4), 24.x (>=24.22.1) lub 26+ (>=26.12.1) oraz n
| Natywne pliki binarne | Pseudokod, kod asemblera, ciągi znaków, symbole, wywołania i odwołania | Hopper, Ghidra lub IDA; [analiza kodu natywnego](../native-investigation.md) |
| 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) |
| PE certificates | Authenticode WIN_CERTIFICATE entries and PKCS#7 signer/X.509 identity fields | Built-in static PE inspection; [PE certificates](../pe-certificates.md) |
| PE imports | Import and delay-load DLL names, ordinals, IAT RVAs and pefile import hash | Built-in static PE inspection; [PE imports](../pe-imports.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) |
+1
View File
@@ -154,6 +154,7 @@ O REA requer Node.js 22.x (>=22.23.4), 24.x (>=24.22.1) ou 26+ (>=26.12.1), alé
| Binários nativos | Pseudocódigo, assembly, strings, símbolos, chamadas e referências | Hopper, Ghidra ou IDA; [análise nativa](../native-investigation.md) |
| 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) |
| PE certificates | Authenticode WIN_CERTIFICATE entries and PKCS#7 signer/X.509 identity fields | Built-in static PE inspection; [PE certificates](../pe-certificates.md) |
| PE imports | Import and delay-load DLL names, ordinals, IAT RVAs and pefile import hash | Built-in static PE inspection; [PE imports](../pe-imports.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) |
+1
View File
@@ -156,6 +156,7 @@ REA требует Node.js 22.x (>=22.23.4), 24.x (>=24.22.1) или 26+ (>=26.1
| Байт-код EVM | Селекторы диспетчеризации, смещения байтов, предполагаемые аргументы и изменяемость состояния | Локальные необработанные байты или шестнадцатеричное представление в Linux x64; [руководство по EVM без запуска](../evm-bytecode.md) |
| Rust binaries | Embedded cargo-auditable dependency list | [Rust binaries](../rust-binaries.md) |
| PE certificates | Authenticode WIN_CERTIFICATE entries and PKCS#7 signer/X.509 identity fields | Built-in static PE inspection; [PE certificates](../pe-certificates.md) |
| PE imports | Import and delay-load DLL names, ordinals, IAT RVAs and pefile import hash | Built-in static PE inspection; [PE imports](../pe-imports.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) |
| Erlang BEAM | IFF chunks, atoms, exports, and tables | Built-in static BEAM inspection; [Erlang BEAM](../erlang-beam.md) |
| Записанные сбои Linux | Необработанные записи note, регистры/сигналы каждого записанного потока и необязательные кандидаты сопоставления с памятью | pwntools, предоставленный вызывающей стороной, в Linux x64; необязательные GDB/pwndbg; [записанные сбои](../recorded-crashes.md) |
+1
View File
@@ -154,6 +154,7 @@ REA ต้องใช้ Node.js 22.x (>=22.23.4), 24.x (>=24.22.1) หรื
| ไบนารีเนทีฟ | โค้ดเทียม แอสเซมบลี สตริง สัญลักษณ์ การเรียก และการอ้างอิง | Hopper, Ghidra หรือ IDA; [การวิเคราะห์เนทีฟ](../native-investigation.md) |
| โครงสร้าง ELF แบบออฟไลน์ | เซกชัน เซกเมนต์ สัญลักษณ์/รีโลเคชันต้นฉบับ และตัวเลือกกลไกป้องกันที่พบจากการวิเคราะห์แบบสถิต | pwntools ที่ผู้ใช้จัดเตรียมบน Linux x64; [การวินิจฉัยไบนารี](../binary-diagnostics.md) |
| PE certificates | Authenticode WIN_CERTIFICATE entries and PKCS#7 signer/X.509 identity fields | Built-in static PE inspection; [PE certificates](../pe-certificates.md) |
| PE imports | Import and delay-load DLL names, ordinals, IAT RVAs and pefile import hash | Built-in static PE inspection; [PE imports](../pe-imports.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) |
+1
View File
@@ -154,6 +154,7 @@ REA; Node.js 22.x (>=22.23.4), 24.x (>=24.22.1) veya 26+ (>=26.12.1) ve npm gere
| 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) |
| PE certificates | Authenticode WIN_CERTIFICATE entries and PKCS#7 signer/X.509 identity fields | Built-in static PE inspection; [PE certificates](../pe-certificates.md) |
| PE imports | Import and delay-load DLL names, ordinals, IAT RVAs and pefile import hash | Built-in static PE inspection; [PE imports](../pe-imports.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) |
| Erlang BEAM | IFF chunks, atoms, exports, and tables | Built-in static BEAM inspection; [Erlang BEAM](../erlang-beam.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` |
+1
View File
@@ -154,6 +154,7 @@ REA потребує Node.js 22.x (>=22.23.4), 24.x (>=24.22.1) або 26+ (>=26
| Нативні бінарні файли | Псевдокод, асемблерний код, рядки, символи, виклики й посилання | Hopper, Ghidra або IDA; [нативний аналіз](../native-investigation.md) |
| Структура ELF без запуску | Секції, сегменти, початкові символи/релокації та можливі захисні механізми за даними статичного аналізу | pwntools, наданий стороною, що викликає інструмент, у Linux x64; [діагностика бінарних файлів](../binary-diagnostics.md) |
| PE certificates | Authenticode WIN_CERTIFICATE entries and PKCS#7 signer/X.509 identity fields | Built-in static PE inspection; [PE certificates](../pe-certificates.md) |
| PE imports | Import and delay-load DLL names, ordinals, IAT RVAs and pefile import hash | Built-in static PE inspection; [PE imports](../pe-imports.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) |
+1
View File
@@ -196,6 +196,7 @@ Mazeed tools aur host support target par depend karte hain:
| Godot PCK | Archive inventory aur extraction, original header/directory facts aur selected content identities | Built-in static formats 2, 3 aur 4; Godot zaroori nahi |
| Rust binaries | Embedded cargo-auditable crate names, versions, sources aur dependency edges | Built-in static ELF/PE/thin Mach-O inspection; [Rust binaries](../rust-binaries.md) |
| PE certificates | Authenticode WIN_CERTIFICATE entries aur PKCS#7 signer/X.509 identity fields | Built-in static PE inspection; [PE certificates](../pe-certificates.md) |
| PE imports | Import aur delay-load DLL names, ordinals, IAT RVAs aur pefile import hash | Built-in static PE inspection; [PE imports](../pe-imports.md) |
| ELF notes | PT_NOTE/SHT_NOTE owners, types, GNU build-id aur ABI-tag fields | Built-in static ELF inspection; [ELF notes](../elf-notes.md) |
| Erlang BEAM | IFF chunks, atoms, exports, imports, lambdas aur Code header | Built-in static BEAM inspection; [Erlang BEAM](../erlang-beam.md) |
| WASM artifact | Validation, sections, exact import/export WAT forms aur decoded WAT | Aapka diya hua WABT 1.0.42; `inspect_wasm_artifact` |
+1
View File
@@ -156,6 +156,7 @@ REA yêu cầu Node.js 22.x (>=22.23.4), 24.x (>=24.22.1) hoặc 26+ (>=26.12.1)
| 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) |
| PE certificates | Authenticode WIN_CERTIFICATE entries and PKCS#7 signer/X.509 identity fields | Built-in static PE inspection; [PE certificates](../pe-certificates.md) |
| PE imports | Import and delay-load DLL names, ordinals, IAT RVAs and pefile import hash | Built-in static PE inspection; [PE imports](../pe-imports.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) |
| Erlang BEAM | IFF chunks, atoms, exports, and tables | Built-in static BEAM inspection; [Erlang BEAM](../erlang-beam.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) |
+1
View File
@@ -157,6 +157,7 @@ REA 需要 Node.js 22.x(>=22.23.4)、24.x(>=24.22.1)或 26+ (>=26.12.1)
| Rust binaries | Embedded cargo-auditable dependency list | [Rust binaries](../rust-binaries.md) |
| Erlang BEAM | IFF chunks, atoms, exports, and tables | Built-in static BEAM inspection; [Erlang BEAM](../erlang-beam.md) |
| PE certificates | Authenticode WIN_CERTIFICATE entries and PKCS#7 signer/X.509 identity fields | Built-in static PE inspection; [PE certificates](../pe-certificates.md) |
| PE imports | Import and delay-load DLL names, ordinals, IAT RVAs and pefile import hash | Built-in static PE inspection; [PE imports](../pe-imports.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) |
+1
View File
@@ -157,6 +157,7 @@ REA 需要 Node.js 22.x(>=22.23.4)、24.x(>=24.22.1)或 26+ (>=26.12.1)
| Rust binaries | Embedded cargo-auditable dependency list | [Rust binaries](../rust-binaries.md) |
| Erlang BEAM | IFF chunks, atoms, exports, and tables | Built-in static BEAM inspection; [Erlang BEAM](../erlang-beam.md) |
| PE certificates | Authenticode WIN_CERTIFICATE entries and PKCS#7 signer/X.509 identity fields | Built-in static PE inspection; [PE certificates](../pe-certificates.md) |
| PE imports | Import and delay-load DLL names, ordinals, IAT RVAs and pefile import hash | Built-in static PE inspection; [PE imports](../pe-imports.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) |
+255
View File
@@ -0,0 +1,255 @@
# PE import inventory verification — 2026-10-24
The analyst task is to list a local PE's standard and delay imports, keep source
locations and partial facts, and obtain a pefile-compatible imphash only when
the retained standard import sequence supports it. CLI and MCP use the same
application service and static parser. Targets are read without execution.
## Revisions and ownership
- Original PR: `0b65cbcfe2bf78a808d41e12fa5aee9c0fc93da4`.
- Main integration and mapping index: `336f83cbb08dcc02a2d4c932f9c82f2c7b83554e`.
- Measured baseline: `073e2612e783dc38d3603cd7dcc9dc721c0d9412`, the preceding
integration with provider identity imports and artifact-failure classification
repaired so the runtime builds. The measured candidate uses the same provider.
- Measured candidate: `105f4ea66ebc82e559fe1000aabe75ced3024a41`.
`readPeImage` owns machine, image base, directories, alignments and declared raw
section pointers. The import parser consumes those facts rather than reading the
PE header again or substituting a missing base with zero. The shared mapping
index preserves canonical ambiguous/unmapped failures and adjacent mappings.
Its construction is O(n log n), with O(log n) nonempty range queries; both normal
and failed lookups avoid a linear fallback. Indexed and canonical CString reads
share one implementation and can reconstruct strings across physical sections.
These structures belong to one parse, with no cross-request cache or invalidation.
Derived ILT/IAT slots are checked against the 32-bit RVA space before a row is
returned. A malformed table preserves preceding valid rows and reports partial
coverage instead of failing the output schema. Named thunks are checked before
conversion from bigint, retaining the exact source hex when reserved high bits
are set. The format's named hint/name pointer uses 31 bits; see the
[Microsoft PE format specification](https://learn.microsoft.com/en-us/windows/win32/debug/pe-format#import-lookup-table).
Early cancellation preserves the caller's reason before input validation in
PE imports and the two adjacent certificate/ELF-note services. Artifact failure
classification consumes the shared owner's origin facts and regular-file error
predicate; cleanup ownership and retry remain with `ArtifactResourceScope`.
## Independent oracle and regressions
The oracle is pefile **2024.8.26**, installed without dependencies into the
temporary verification directory. Its `pefile.py` SHA-256 is
`0d0eb68f0f169182613dc64b3ab50b20855508c1c1d2faafe8fccf23edb6a345`.
The release's upstream commit is
[`4b3b1e2e568a88d4f1897d694d684f23d9e270c4`](https://github.com/erocarrera/pefile/tree/4b3b1e2e568a88d4f1897d694d684f23d9e270c4).
All **696** copied ordinal names match its three complete maps: 425 oleaut32,
196 ws2_32 and 75 wsock32. Own-key lookup and `ord<decimal>` fallback are retained.
The upstream MIT license is preserved in `licenses/` and the notices inventory.
Independent byte fixtures establish these differences:
- A cross-section function name is `Function128` in native mappings; pefile
reads `Functi`. A cross-section DLL name is `KERNEL32.dll` natively and
`KERNEL32.d` in pefile. Complete native facts remain useful, while hashing them
would produce a different valid-looking MD5.
- pefile stops after 2,048 section headers. With imports in section 2,049, native
parsing retains both functions while pefile finds no import table.
- pefile also stops at a null section header or a section with at least three
of its structural warnings. Native parsing can still see a later import table.
The candidate checks byte-mapping compatibility from the parsed owner facts,
including pointer/address normalization, section clipping and those stop rules.
Unproven geometry withholds only the hash, with an explicit reason. Delay-only
issues do not invalidate the standard table's hash. This is conservative
compatibility checking, not universal qualification of every unusual PE layout.
PE32/PE32+ IAT overflow and exact wide-thunk regressions fail on the old producer.
The three section-stop cases fail on the precursor before its stop guard was
added; the independent oracle sees no imports in each. All pass after correction.
Each of the three early-cancellation services fails its null-reason control
before the fix; afterward both null and caller-object identity are preserved.
The existing retained-text failure already propagates through its tagged error
and is not claimed as a newly fixed defect.
Executed checks at the candidate stages:
- **58** parser/mapping/image/cancellation checks pass. Randomized mapping
comparisons use the canonical mapper as an independent semantic oracle.
- **172** selected domain/service/filesystem/CLI/MCP/artifact-lifecycle checks
pass before the final section-stop guard; sets overlap and are not added.
- The final parser file passes after the stress fixture's header/section ranges
are made disjoint. **5** public CLI cases then pass, including both IAT widths,
a complete cross-section name, section-cap and null-section hash refusals.
- Full static gates pass after provider integration and after the compatibility
guard. The initial missing artifact-failure API compile error was corrected.
The public CLI checks parse actual responses with the named output contract.
MCP tests exercise the real SDK and advertised schema. The artifact-consumer
suite covers physical read errors, cancellation, cleanup failure and retry with
the PE provider included. No deep native engine or Windows runtime was newly
qualified by these static-format checks.
## Shipped PE artifacts
The two launchers come from the official
[pip 24.0 wheel](https://files.pythonhosted.org/packages/8a/6a/19e9fe04fca059ccf770861c7d5721ab4c2aebc539889e97c7977528a53b/pip-24.0-py3-none-any.whl),
SHA-256 `ba0d021a166865d2265246961bec0152ff124de910c5cc39f1156ce3fa7c69dc`.
They were extracted as analysis artifacts, without installation or execution.
| Artifact | Bytes | SHA-256 | Oracle result |
| -------- | ------: | ------------------------------------------------------------------ | --------------------------------------------------------- |
| t32.exe | 97,792 | `6b4195e640a85ac32eb6f9628822a622057df1e459df7c17a12f97aeabc9415b` | 2 DLLs / 85 functions; `5e24f42b46c247f13d78f0f21a4a2bf7` |
| t64.exe | 108,032 | `81a618f21cb87db9076134e70388b6e9cb7c2106739011b6a51772d22cae06b7` | 2 DLLs / 86 functions; `c51d659b4b1142d4af3795d09f1d63f7` |
For both launchers the ordered DLLs, names, ordinals, hints, IAT RVAs, machine,
bitness and imphash agree with pefile. The benchmark verifies the complete named
rows, not merely the function count or MD5.
## Completed-task costs
Three alternating pairs per fixture execute one fresh public CLI process:
`rea inspect-pe-imports PATH --json`. All **24** tasks complete. Complete Evidence
objects are equal across all six runs of each fixture, including provenance,
coverage, limitations, locations and identity; nothing is stripped for equality.
Artifact digests remain unchanged. The stress fixture contains 2,048 independent
sections and 2,048 functions, SHA-256
`0540676161eb7408805277a75e4d47e3d752732b6d5f294c59c8c670c7c1e905`.
The empty fixture is 3,072 bytes, SHA-256
`b32058d9121313554af54bc0aed762871d3d8eb5e2d9eeab6a365f0518c6f897`.
| Complete CLI task | Estimated stdout + invocation tokens, before → after | Elapsed median s | CPU median s | Sampled family RSS median MiB |
| -------------------------- | ---------------------------------------------------: | ---------------: | ------------: | ----------------------------: |
| No imports | 676 → 676 | 1.817 → 1.867 | 2.157 → 2.219 | 364.6 → 369.9 |
| Shipped PE32 | 3,678 → 3,678 | 1.868 → 1.867 | 2.201 → 2.221 | 365.2 → 366.4 |
| Shipped PE32+ | 3,715 → 3,715 | 1.868 → 1.868 | 2.238 → 2.220 | 367.1 → 370.3 |
| 2,048 sections / functions | 70,342 → 70,342 | 1.969 → 2.069 | 2.413 → 2.482 | 388.4 → 391.2 |
Tokens use tokenx 1.3.0 estimates after timing, not actual model usage. The task
uses known CLI commands without catalog/schema/help or continuation calls.
Prompts, model framing, rendering and client JSON parsing are unmeasured.
Node is 24.22.1 on Linux 6.1.0-54 x86-64, affinity CPUs 0–1, on a shared host.
Every process is fresh; OS caches were not flushed and had prior oracle reads.
There is no warm provider-cache measurement. Build/setup costs are separate.
Elapsed ranges overlap, and the 50 ms profiler poll quantizes short-task timing;
no speedup is claimed. The largest case's median increases about 0.10 s elapsed,
0.07 s CPU and 2.9 MiB RSS for the additional correctness checks. RSS is the
50 ms observed sum across descendants: shared pages may be counted twice and
short/unobserved processes missed. CPU accounts for waited children. No resource
stop occurred. The mapping index was already in the baseline, so these numbers
do not isolate its speedup or qualify a 65,535-section worst case.
## Main integration verification
Integration `a5a1f919e896488d0651e621f334efec84e9132f` includes main
`044c9705eadf62f7c47ddd42a0b2dc065b26b228`. The PE refinement is a type
intersection with main's new container union; runtime PE interpretation is
unchanged. The newly added Roman Urdu README receives the same PE imports link
as the canonical table. The full static gate passes; documentation/license
validation and test discovery pass after this link correction. The complete
selected parser/mapping/image/service/filesystem/CLI/MCP/lifecycle selection
passes **201** checks in 54.062 s (60.363 s waited-child CPU). Sets above overlap.
Later main-integration performance was not remeasured; the cost table remains
pinned to its recorded revisions. These tests use built-in static parsing, not
a simulated claim of deep-engine behavior.
## Additional RVA review and independent ordinal control
The pre-merge review found a real missing boundary: PE32+ virtual mappings could
carry descriptors, their terminator, or a DLL name beyond the 32-bit RVA end.
Five new controls fail before correction (four standard/delay directory cases
and one NUL beyond the RVA end). The producer now owns one RVA-span predicate
used by primitive reads and partial row admission. CString reads include only
bytes within that space. A name whose NUL is the final legal byte still passes.
An unexamined first descriptor retains the declared directory with zero consumed
locations and partial coverage. The schema represents that observation; the
service rejects an empty consumed-span list claimed as complete. Four public
CLI cases and the real MCP SDK/advertised AJV schema exercise both zero-span and
preceding-row outcomes. The expanded selected workflow passes **213** checks
in 71.408 s (71.595 s waited-child CPU). The full static gate passes. The earlier
cost table remains pinned to its revisions; these later RVA checks were not
included in that first measurement; the separate comparison below covers them.
The ordinal-mask suggestion was tested rather than applied: installed pefile
2024.8.26 itself uses `addr_of_data & 0x7FFFFFFF` in its ordinal corruption
heuristic, including PE32+. Independent images with both ILT/IAT words rewritten
produce these results:
| Raw PE32+ thunk | pefile result | Native result |
| ------------------ | ----------------------------------------------------------- | -------------------------------------- |
| `8000000100000001` | ordinal 1 + ExitProcess; `72d9cd67504bb3b9f29c37a226bf4331` | identical, complete raw thunk retained |
| `8000000000010001` | import table discarded; empty hash | hash withheld |
| `8000000000000001` | ordinal 1 + ExitProcess; `72d9cd67504bb3b9f29c37a226bf4331` | identical |
All three controls already pass before the RVA correction. Expanding the mask
to 63 payload bits would diverge from this pinned pefile behavior. The reason
text now specifies the low 31-bit payload; the raw thunk remains available, and
matching pefile does not claim loader validity for reserved bits.
## RVA-guard completed-task follow-up
To measure the final guard separately from other integration changes, repeat
three alternating pairs of the same four completed CLI tasks, now comparing
`a7406eca8137c9fae58a12cb8354f042047f5045` against
`feda57ae4ef420bc72e8e04393b75810da86cc3c` on their common main integration.
All **24** tasks complete. All complete Evidence objects match across each
fixture's six runs, and every ordered inventory field still matches the
independent pefile oracle. Artifact digests stay unchanged; stderr is empty.
The token estimator, invocation/discovery scope, process/cache conditions and
profiler limitations are the same as the earlier measurement.
| Complete CLI task | Estimated stdout + invocation tokens, before → after | Elapsed median s | CPU median s | Sampled family RSS median MiB |
| -------------------------- | ---------------------------------------------------: | ---------------: | ------------: | ----------------------------: |
| No imports | 676 → 676 | 2.220 → 2.019 | 2.383 → 2.382 | 367.6 → 364.0 |
| Shipped PE32 | 3,678 → 3,678 | 1.918 → 1.918 | 2.253 → 2.203 | 366.8 → 367.9 |
| Shipped PE32+ | 3,715 → 3,715 | 1.973 → 1.968 | 2.363 → 2.293 | 368.9 → 369.3 |
| 2,048 sections / functions | 70,342 → 70,342 | 2.825 → 2.221 | 2.423 → 2.428 | 382.9 → 389.0 |
No speedup is claimed. In the largest case, elapsed ranges are 2.019–3.085 s
before and 2.019–2.776 s after. CPU medians differ by about 0.004 s; sampled
RSS increases about 6.1 MiB (1.6%). Shared-host variance dominates the apparent
elapsed improvement. This validates the measured completed tasks, not a maximum
format-size or transport-buffer qualification. Later main changes remain
outside these pinned measurements.
## Coverage consistency
Three new service controls reject contradictory standard-import, delay-import,
and complete descriptor counts; all three fail before the correction. The
service now compares counts with the retained rows before issuing Evidence.
Partial results may still report more examined descriptors than retained DLLs,
as verified with an unsupported delay descriptor. Complete results must agree
and carry no issues. Existing function-count traversals are reused; the change
adds no further row scan. These validation checks postdate the pinned cost
measurements above.
The expanded selection passes **217** checks in 73.179 s (70.360 s
waited-child CPU), with a sampled process-family peak of 2,039.3 MiB. Full
`npm run check` passes. These counts overlap earlier selections. A subsequent
main integration at `7f70797b98c226009163358f91a9cfcedf03b390` includes the
upstream plist-fixture correction for the three unrelated native-provider
failures seen in CI run 40055148308.
## Descriptor mapping stops
A later review identified the same partial-result boundary when a descriptor's
uint32 RVA is legal but lacks a file mapping. Four controls (standard/delay,
first descriptor/after one row) all threw before correction. Both walks now
use one descriptor admission function: range failures and mapping failures
append explicit issues, then preserve the directory declaration, consumed
spans and preceding rows. Only mapping failures are caught; unrelated failures
continue to propagate. Standard-table failures withhold imphash; delay-table
failures preserve an independently complete standard hash.
The public CLI covers all four mapping-stop cases alongside the RVA cases.
The actual MCP SDK and advertised output schema cover an unmapped first
descriptor. These later validation changes are outside the pinned performance
comparisons above; no additional speed or maximum-size claim is made.
Final integration at main `7f70797b` passes **277** selected checks: the
expanded PE/shared lifecycle selection plus native-provider tests that cover
the upstream plist-fixture correction. This takes 126.978 s, 127.567 s
waited-child CPU and 2,061.9 MiB sampled process-family RSS. These are test
suite costs, not analyst-task costs. Full `npm run check`, `npm run docs:check`
and test discovery pass; discovery accounts for all 2,323 files exactly once.
+21
View File
@@ -0,0 +1,21 @@
The MIT License (MIT)
Copyright (c) 2004-2024 Ero Carrera
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
+5
View File
@@ -73,5 +73,10 @@
"file": "licenses/protobuf-es-v2.16.0-LICENSE.txt",
"source": "https://raw.githubusercontent.com/bufbuild/protobuf-es/b3b232f7f466eda27c66e8f63c82f930578b6cc6/LICENSE",
"sha256": "c04b4216f1cd4c5a4f7fb2f2a1b0ae70d847e9e0cac7c9dee9bf8cc03177c449"
},
{
"file": "licenses/pefile-2024.8.26-LICENSE.txt",
"source": "https://raw.githubusercontent.com/erocarrera/pefile/4b3b1e2e568a88d4f1897d694d684f23d9e270c4/LICENSE",
"sha256": "b44409c067c6da52bfb54bb6624fb11a7b52157c3a13ae1400232f8196e86ad3"
}
]
+7
View File
@@ -260,6 +260,13 @@ export const providerCatalog = (sources) => {
({ name }) => name === "inspect_pe_certificates",
),
},
{
identity: sources.artifactProviders.PE_IMPORTS_PROVIDER,
contracts:
sources.binaryDiagnosticsContracts.BINARY_DIAGNOSTICS_TOOL_CONTRACTS.filter(
({ name }) => name === "inspect_pe_imports",
),
},
{
identity: sources.artifactProviders.ELF_NOTES_PROVIDER,
contracts:
+1 -1
View File
@@ -529,7 +529,7 @@ const workflowAvailabilityFor = ({
};
if (name === "inspect_pe_resources" || name === "inspect_native_exports")
return { reason: "available", remediation: null };
if (name === "inspect_pe_certificates")
if (name === "inspect_pe_certificates" || name === "inspect_pe_imports")
return { reason: "available", remediation: null };
if (name === "inspect_elf_notes")
return { reason: "available", remediation: null };
@@ -0,0 +1,49 @@
import { expect, it } from "vitest";
import { ElfNotesService } from "./ElfNotesService.js";
import { PeCertificatesService } from "./PeCertificatesService.js";
import { PeImportsService } from "./PeImportsService.js";
import {
ELF_NOTES_PROVIDER,
PE_CERTIFICATES_PROVIDER,
PE_IMPORTS_PROVIDER,
} from "../../domain/investigationProviders.js";
const inspect = (): never => {
throw new Error("An already cancelled request must not reach its provider");
};
it.each([
[
"PE imports",
() => new PeImportsService({ identity: PE_IMPORTS_PROVIDER, inspect }),
],
[
"PE certificates",
() =>
new PeCertificatesService({
identity: PE_CERTIFICATES_PROVIDER,
inspect,
}),
],
[
"ELF notes",
() => new ElfNotesService({ identity: ELF_NOTES_PROVIDER, inspect }),
],
] as const)(
"preserves an early cancellation reason for %s before validation",
async (_label, create) => {
const service = create();
try {
for (const reason of [null, { caller: "selected task" }]) {
const result = await service.inspect(null, {
signal: AbortSignal.abort(reason),
});
if (result.ok) throw new Error("Expected early cancellation");
expect(result.error._tag).toBe("AnalysisCancelledError");
expect(result.error.cause).toBe(reason);
}
} finally {
await service.close();
}
},
);
@@ -34,7 +34,9 @@ export class ElfNotesService {
options?: ExecutionOptions,
): Promise<Result<Evidence, AnalysisError>> {
if (options?.signal?.aborted)
return err(new AnalysisCancelledError(OPERATION));
return err(
new AnalysisCancelledError(OPERATION, { cause: options.signal.reason }),
);
const input = inspectElfNotesInputSchema.safeParse(rawInput);
if (!input.success)
return err(
@@ -37,7 +37,9 @@ export class PeCertificatesService {
options?: ExecutionOptions,
): Promise<Result<Evidence, AnalysisError>> {
if (options?.signal?.aborted)
return err(new AnalysisCancelledError(OPERATION));
return err(
new AnalysisCancelledError(OPERATION, { cause: options.signal.reason }),
);
const input = inspectPeCertificatesInputSchema.safeParse(rawInput);
if (!input.success)
return err(
@@ -0,0 +1,20 @@
import type {
ExecutionOptions,
ProviderIdentity,
} from "../AnalysisProvider.js";
import type { AnalysisError } from "../../domain/analysisErrorBase.js";
import type {
InspectPeImportsInput,
PeImports,
} from "../../domain/native/peImports.js";
import type { Result } from "../../domain/result.js";
/** Stable read and PE import-table decoding, independent of a disassembler session. */
export interface PeImportsPort {
readonly identity: ProviderIdentity;
close?(): Promise<void>;
inspect(
input: InspectPeImportsInput,
options?: ExecutionOptions,
): Promise<Result<PeImports, AnalysisError>>;
}
@@ -0,0 +1,242 @@
import { expect, it } from "vitest";
import { PeImportsService } from "./PeImportsService.js";
import { PE_IMPORTS_PROVIDER } from "../../domain/investigationProviders.js";
import type { PeImports } from "../../domain/native/peImports.js";
import { peImportsSchema } from "../../domain/native/peImports.js";
import { ok } from "../../domain/result.js";
import {
AnalysisCancelledError,
AnalysisInputError,
AnalysisOutputError,
} from "../../domain/analysisErrorCore.js";
const identity = PE_IMPORTS_PROVIDER;
const report: PeImports = {
artifact: {
path: "/artifacts/app.exe",
sha256: "a".repeat(64),
bytes: 512,
},
format: "pe32-plus",
machine: 0x8664,
bits: 64,
architecture: "amd64",
import_directory: null,
delay_import_directory: null,
imports: [],
delay_imports: [],
imphash: {
algorithm: "pefile",
md5: null,
reason: "IMAGE_DIRECTORY_ENTRY_IMPORT is absent.",
},
coverage: {
status: "complete",
reason: null,
examined_dlls: 0,
imports: 0,
delay_imports: 0,
},
issues: [],
limitations: ["IMAGE_DIRECTORY_ENTRY_IMPORT is absent."],
};
it("rejects a relative path and extra arguments before reading", async () => {
let calls = 0;
const service = new PeImportsService({
identity,
inspect: () => {
calls += 1;
return Promise.resolve(ok(report));
},
});
const relative = await service.inspect({ path: "app.exe" });
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 PE and an absent import table", async () => {
const result = await new PeImportsService({
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: "pe", architecture: "x86_64" },
normalized_result: report,
});
});
it.each(["imports", "delay_imports", "examined_dlls"] as const)(
"rejects contradictory complete %s coverage",
async (field) => {
const result = await new PeImportsService({
identity,
inspect: () =>
Promise.resolve(
ok({
...report,
coverage: { ...report.coverage, [field]: 1 },
}),
),
}).inspect({ path: report.artifact.path });
expect(result.ok).toBe(false);
if (!result.ok) expect(result.error).toBeInstanceOf(AnalysisOutputError);
},
);
it.each(["import_directory", "delay_import_directory"] as const)(
"accepts unexamined %s declarations only with partial coverage",
async (field) => {
for (const status of ["complete", "partial"] as const) {
const value: PeImports = {
...report,
[field]: { rva: 0xfffffff0, size: 40, locations: [] },
coverage: {
...report.coverage,
status,
reason:
status === "partial"
? "First descriptor cannot be examined."
: null,
},
issues:
status === "partial"
? [
{
code: "malformed_entry",
location: "0xfffffff0",
message: "First descriptor cannot be examined.",
},
]
: [],
};
const result = await new PeImportsService({
identity,
inspect: () => Promise.resolve(ok(value)),
}).inspect({ path: report.artifact.path });
expect(result.ok).toBe(status === "partial");
if (!result.ok) expect(result.error).toBeInstanceOf(AnalysisOutputError);
}
},
);
it("discards an observation if the caller cancels while the provider completes", async () => {
const controller = new AbortController();
const service = new PeImportsService({
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);
});
it("publishes every consumed directory span as its own Evidence location", async () => {
const split: PeImports = {
...report,
import_directory: {
rva: 0x1000,
size: 40,
locations: [
{ offset: 0x100, bytes: 20 },
{ offset: 0x1e0, bytes: 20 },
],
},
imports: [
{
index: 0,
name: "KERNEL32.dll",
name_rva: 0x1100,
original_first_thunk_rva: 0x1120,
first_thunk_rva: 0x1128,
time_date_stamp: 0,
forwarder_chain: 0,
location: { offset: 0x100, bytes: 20 },
functions: [],
},
],
coverage: { ...report.coverage, examined_dlls: 1 },
imphash: { ...report.imphash, reason: "The descriptor has no functions." },
limitations: ["Loaded import state remains unobserved."],
};
const result = await new PeImportsService({
identity,
inspect: () => Promise.resolve(ok(split)),
}).inspect({ path: report.artifact.path });
if (!result.ok) throw result.error;
expect(result.value.locations).toEqual([
{ kind: "artifact-path", path: report.artifact.path },
{ kind: "file-offset-range", start: 0x100, end: 0x114 },
{ kind: "file-offset-range", start: 0x1e0, end: 0x1f4 },
]);
const outside = await new PeImportsService({
identity,
inspect: () =>
Promise.resolve(
ok({
...split,
import_directory: {
rva: 0x1000,
size: 40,
locations: [
{ offset: 0x100, bytes: 20 },
{ offset: 0x1f8, bytes: 20 },
],
},
}),
),
}).inspect({ path: report.artifact.path });
expect(outside.ok).toBe(false);
if (!outside.ok) expect(outside.error).toBeInstanceOf(AnalysisOutputError);
});
it("retains examined unsupported descriptors without inventing DLL rows", async () => {
const partial: PeImports = {
...report,
delay_import_directory: {
rva: 0x1000,
size: 64,
locations: [{ offset: 0x100, bytes: 64 }],
},
coverage: {
...report.coverage,
status: "partial",
reason: "An unsupported descriptor was examined.",
examined_dlls: 1,
},
issues: [
{
code: "unsupported_form",
location: "0x1000",
message: "Delay descriptor uses virtual addresses.",
},
],
};
const result = await new PeImportsService({
identity,
inspect: () => Promise.resolve(ok(partial)),
}).inspect({ path: report.artifact.path });
if (!result.ok) throw result.error;
const normalized = peImportsSchema.parse(result.value.normalized_result);
expect(normalized.coverage).toMatchObject({
status: "partial",
examined_dlls: 1,
});
expect(normalized.delay_imports).toEqual([]);
});
@@ -0,0 +1,177 @@
import { isAbsolute } from "node:path";
import type { ExecutionOptions } from "../AnalysisProvider.js";
import type { PeImportsPort } from "./PeImportsPort.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 {
inspectPeImportsInputSchema,
peImportsSchema,
type PeImports,
} from "../../domain/native/peImports.js";
import { analysisInputErrorFromIssues } from "../../domain/inputIssueProjection.js";
import {
decodePeMachine,
peOptionalHeaderMagic,
type PeArchitecture,
} from "../../domain/peMachine.js";
import { err, type Result } from "../../domain/result.js";
import { completeProviderEvidence } from "../ProviderOperationExecution.js";
const OPERATION = "inspect_pe_imports";
/** Shared caller validation, output parsing and Evidence provenance for CLI/MCP. */
export class PeImportsService {
constructor(readonly provider: PeImportsPort) {}
close(): Promise<void> {
return this.provider.close?.() ?? Promise.resolve();
}
async inspect(
rawInput: unknown,
options?: ExecutionOptions,
): Promise<Result<Evidence, AnalysisError>> {
if (options?.signal?.aborted)
return err(
new AnalysisCancelledError(OPERATION, { cause: options.signal.reason }),
);
const input = inspectPeImportsInputSchema.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 = peImportsSchema.safeParse(result.value);
if (
!report.success ||
report.data.artifact.path !== input.data.path ||
!validReport(report.data, input.data)
)
return err(
new AnalysisOutputError(
OPERATION,
"PE import provider returned malformed output or changed artifact identity.",
),
);
const value = report.data;
const architecture = subjectArchitecture(value);
const locations = [
{ kind: "artifact-path" as const, path: value.artifact.path },
...fileRange(value.import_directory),
...fileRange(value.delay_import_directory),
];
return completeProviderEvidence(
createEvidence(
{
path: value.artifact.path,
format: "pe",
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 subjectArchitecture = (report: PeImports): PeArchitecture | undefined => {
const machine = decodePeMachine(report.machine);
if (machine === undefined || machine.nativeOsOverride) return undefined;
const magic = report.format === "pe32-plus" ? 0x20b : 0x10b;
return magic === peOptionalHeaderMagic(machine.architecture)
? machine.architecture
: undefined;
};
const fileRange = (
directory: PeImports["import_directory"],
): readonly {
kind: "file-offset-range";
start: number;
end: number;
}[] =>
directory === null
? []
: directory.locations.map(({ offset, bytes }) => ({
kind: "file-offset-range" as const,
start: offset,
end: offset + bytes,
}));
const validReport = (
report: PeImports,
input: {
readonly max_dlls: number;
readonly max_functions: number;
readonly max_file_bytes: number;
},
): boolean => {
const inside = ({ offset, bytes }: { offset: number; bytes: number }) =>
offset <= report.artifact.bytes - bytes;
const dllCount = report.imports.length + report.delay_imports.length;
if (
report.artifact.bytes > input.max_file_bytes ||
report.coverage.examined_dlls > input.max_dlls ||
dllCount > report.coverage.examined_dlls ||
(report.coverage.status === "complete" &&
(dllCount !== report.coverage.examined_dlls || report.issues.length > 0))
)
return false;
const importCount = report.imports.reduce(
(count, dll) => count + dll.functions.length,
0,
);
const delayImportCount = report.delay_imports.reduce(
(count, dll) => count + dll.functions.length,
0,
);
if (
importCount !== report.coverage.imports ||
delayImportCount !== report.coverage.delay_imports ||
importCount + delayImportCount > input.max_functions
)
return false;
for (const directory of [
report.import_directory,
report.delay_import_directory,
]) {
if (
directory !== null &&
(!directory.locations.every(inside) ||
(directory.locations.length === 0 &&
report.coverage.status === "complete"))
)
return false;
}
const descriptorsFit = (
rows: readonly { location: { offset: number; bytes: number } }[],
) => rows.every((row) => inside(row.location));
return descriptorsFit(report.imports) && descriptorsFit(report.delay_imports);
};
+37
View File
@@ -7,6 +7,7 @@ import {
createElfNotesService,
createPeResourcesService,
createPeCertificatesService,
createPeImportsService,
createNativeExportsService,
} from "../composition/binaryDiagnostics.js";
import { CLI_COMMANDS } from "../cliCommandNames.js";
@@ -18,6 +19,7 @@ import { inspectBinaryLayoutInputSchema } from "../domain/native/binaryLayout.js
import { inspectRecordedCrashInputSchema } from "../domain/native/recordedCrash.js";
import { inspectPeResourcesInputSchema } from "../domain/native/peResources.js";
import { inspectPeCertificatesInputSchema } from "../domain/native/peCertificates.js";
import { inspectPeImportsInputSchema } from "../domain/native/peImports.js";
import { inspectNativeExportsInputSchema } from "../domain/native/nativeExports.js";
import { inspectElfNotesInputSchema } from "../domain/native/elfNotes.js";
import { projectAnalysisError } from "../domain/analysisErrorProjection.js";
@@ -143,6 +145,41 @@ export const registerBinaryDiagnosticsCommands = (
);
}),
});
cli.command(CLI_COMMANDS.inspectPeImports, {
description:
"Inspect PE import and delay-load table identities without executing the image",
args: z.object({ path: z.string().describe("Local PE32/PE32+ image") }),
options: z.object({
maxFileBytes: inspectPeImportsInputSchema.shape.max_file_bytes,
maxDlls: inspectPeImportsInputSchema.shape.max_dlls,
maxFunctions: inspectPeImportsInputSchema.shape.max_functions,
}),
run: ({ args, options }) =>
withCommandCancellation(async (signal) => {
const pe = createPeImportsService();
return await logCliCommand(
logger,
CLI_COMMANDS.inspectPeImports,
async () =>
finishAnalysisCommand(
() =>
pe.inspect(
{
path: absoluteCallerPath(args.path),
max_file_bytes: options.maxFileBytes,
max_dlls: options.maxDlls,
max_functions: options.maxFunctions,
},
{ signal },
),
pe,
"rea-pe-imports",
"inspect_pe_imports",
signal,
),
);
}),
});
cli.command(CLI_COMMANDS.inspectPeResources, {
description:
"Inspect PE resource identities, payload ranges, hashes and icon-group relationships",
+1
View File
@@ -71,6 +71,7 @@ export const CLI_COMMANDS = Object.freeze({
inspectErlangBeam: "inspect-erlang-beam",
inspectPeResources: "inspect-pe-resources",
inspectPeCertificates: "inspect-pe-certificates",
inspectPeImports: "inspect-pe-imports",
inspectNativeExports: "inspect-native-exports",
inspectElfNotes: "inspect-elf-notes",
inspectAnalysisView: "inspect-analysis-view",
+6
View File
@@ -8,6 +8,8 @@ import { PeResourcesService } from "../application/binaryDiagnostics/PeResources
import { PeResourcesProvider } from "../native/pe/PeResourcesProvider.js";
import { PeCertificatesService } from "../application/binaryDiagnostics/PeCertificatesService.js";
import { PeCertificatesProvider } from "../native/pe/PeCertificatesProvider.js";
import { PeImportsService } from "../application/binaryDiagnostics/PeImportsService.js";
import { PeImportsProvider } from "../native/pe/PeImportsProvider.js";
import { ElfNotesService } from "../application/binaryDiagnostics/ElfNotesService.js";
import { ElfNotesProvider } from "../native/elf/ElfNotesProvider.js";
import type { ArtifactResourceScope } from "../artifacts/ArtifactResourceScope.js";
@@ -24,6 +26,10 @@ export const createPeResourcesService = (): PeResourcesService =>
export const createPeCertificatesService = (): PeCertificatesService =>
new PeCertificatesService(new PeCertificatesProvider());
/** Portable PE import-table inspection with no engine or catalog verification. */
export const createPeImportsService = (): PeImportsService =>
new PeImportsService(new PeImportsProvider());
/** Portable PE/ELF export tables with no engine, toolchain or target acquisition. */
export const createNativeExportsService = (): NativeExportsService =>
new NativeExportsService(new NativeExportsProvider());
@@ -32,6 +32,10 @@ import {
inspectPeCertificatesInputSchema,
peCertificatesSchema,
} from "../../domain/native/peCertificates.js";
import {
inspectPeImportsInputSchema,
peImportsSchema,
} from "../../domain/native/peImports.js";
import {
inspectElfNotesInputSchema,
elfNotesSchema,
@@ -109,6 +113,21 @@ export const BINARY_DIAGNOSTICS_TOOL_CONTRACTS = [
},
],
},
{
name: "inspect_pe_imports",
...toolContractMetadata("inspect_pe_imports"),
kind: "native-provider",
description:
"Inspect one explicit local PE32/PE32+ image's IMAGE_DIRECTORY_ENTRY_IMPORT and delay-load tables without executing it or opening a disassembler. Returns DLL names, named and ordinal functions, ILT/IAT RVAs, delay-load descriptors, and the pefile import hash of the standard import table, withheld with a reason when that table is partial or pefile would hash a different row sequence. A missing import directory is observed absence of that table, not proof the image never imported APIs through another table or overlay. Bound snapshots, loaded IAT contents and export-forward resolution remain unobserved. Reads a stable regular file; malformed PE headers and exhausted DLL or function budgets return no partial success. Unreadable names or thunks and descriptors missing a required table are reported as partial coverage with issues. Portable on supported Node hosts; no Windows SDK or engine installation required.",
inputSchema: inspectPeImportsInputSchema,
outputSchema: evidenceResultOf(peImportsSchema),
examples: [
{
title: "Inspect PE import and delay-load identities",
input: { path: "/artifacts/application.exe" },
},
],
},
{
name: "inspect_pe_resources",
...toolContractMetadata("inspect_pe_resources"),
+1
View File
@@ -32,6 +32,7 @@ const EXECUTION_WORDING_EXCEPTIONS: Readonly<Record<string, string>> = {
inspect_desktop_application:
"states that directory triage performs no target execution",
inspect_pe_certificates: "states that it does not execute the image",
inspect_pe_imports: "states that it does not execute the image",
inspect_go_binary: "reads embedded metadata without executing the target",
inspect_rust_binary:
"reads an embedded dependency list without executing the target",
+1
View File
@@ -258,6 +258,7 @@ export const TOOL_EFFECTS: Readonly<Record<string, ToolEffects>> = {
inspect_byte_entropy: evidence,
inspect_pe_resources: evidence,
inspect_pe_certificates: evidence,
inspect_pe_imports: evidence,
inspect_native_exports: evidence,
inspect_elf_notes: evidence,
inspect_analysis_view: evidence,
+7
View File
@@ -33,6 +33,13 @@ export const PE_CERTIFICATES_PROVIDER = Object.freeze({
version: "1",
} as const);
/** Provider identity for portable, execution-free PE import-table decoding. */
export const PE_IMPORTS_PROVIDER = Object.freeze({
id: "rea-pe-imports",
name: "REA PE import parser",
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",
+44
View File
@@ -0,0 +1,44 @@
import { expect, it } from "vitest";
import { inspectPeImportsInputSchema, peImportsSchema } from "./peImports.js";
it("rejects extra caller arguments and admits an empty import inventory", () => {
expect(
inspectPeImportsInputSchema.safeParse({
path: "/artifacts/app.exe",
extra: true,
}).success,
).toBe(false);
const report = peImportsSchema.parse({
artifact: {
path: "/artifacts/app.exe",
sha256: "a".repeat(64),
bytes: 512,
},
format: "pe32-plus",
machine: 0x8664,
bits: 64,
architecture: "amd64",
import_directory: null,
delay_import_directory: null,
imports: [],
delay_imports: [],
imphash: {
algorithm: "pefile",
md5: null,
reason: "IMAGE_DIRECTORY_ENTRY_IMPORT is absent.",
},
coverage: {
status: "complete",
reason: null,
examined_dlls: 0,
imports: 0,
delay_imports: 0,
},
issues: [],
limitations: [
"IMAGE_DIRECTORY_ENTRY_IMPORT is absent. That is not proof the image never imported APIs through another table or overlay.",
],
});
expect(report.imports).toEqual([]);
expect(report.imphash.md5).toBeNull();
});
+153
View File
@@ -0,0 +1,153 @@
import { z } from "zod";
import { digestSchema } from "../digests.js";
import { absoluteLocalPathStringSchema } from "../localPath.js";
const uint = z.number().int().min(0).max(0xffffffff);
const range = z.strictObject({ offset: uint, bytes: uint });
const md5Schema = z.string().regex(/^[a-f0-9]{32}$/u);
/** Hard ceiling on the caller-selected input read budget. */
export const MAX_INSPECT_PE_IMPORT_FILE_BYTES = 536870912;
/** Hard ceiling on import and delay-load descriptors examined together. */
export const MAX_INSPECT_PE_IMPORT_DLLS = 4096;
/** Hard ceiling on named and ordinal functions across every table. */
export const MAX_INSPECT_PE_IMPORT_FUNCTIONS = 262144;
/** Fixed ceiling on decoded DLL and function name text retained in one report. */
export const MAX_INSPECT_PE_IMPORT_RETAINED_TEXT_BYTES = 16 * 1024 * 1024;
/** Complete bounded inspection of PE import and delay-load tables. */
export const inspectPeImportsInputSchema = z.strictObject({
path: absoluteLocalPathStringSchema.describe(
"Absolute filesystem path to a PE image",
),
max_file_bytes: z
.number()
.int()
.min(64)
.max(MAX_INSPECT_PE_IMPORT_FILE_BYTES)
.default(67108864)
.describe("Maximum stable artifact bytes to read (default 64 MiB)"),
max_dlls: z
.number()
.int()
.min(1)
.max(MAX_INSPECT_PE_IMPORT_DLLS)
.default(256)
.describe(
"Maximum import and delay-load descriptors to examine; exceeding the budget fails without partial success",
),
max_functions: z
.number()
.int()
.min(1)
.max(MAX_INSPECT_PE_IMPORT_FUNCTIONS)
.default(8192)
.describe(
"Maximum imported functions to examine across every table; exceeding the budget fails without partial success",
),
});
export type InspectPeImportsInput = z.infer<typeof inspectPeImportsInputSchema>;
const peImportFunctionSchema = z.strictObject({
hint: uint.nullable(),
name: z.string().min(1).nullable(),
ordinal: z.number().int().min(0).max(0xffff).nullable(),
ilt_rva: uint.nullable(),
iat_rva: uint,
thunk_hex: z.string().regex(/^[0-9a-f]+$/u),
});
const peImportDllSchema = z.strictObject({
index: uint,
name: z.string().min(1).nullable(),
name_rva: uint,
original_first_thunk_rva: uint,
first_thunk_rva: uint,
time_date_stamp: uint,
forwarder_chain: uint,
location: range,
functions: z.array(peImportFunctionSchema),
});
const peDelayImportDllSchema = z.strictObject({
index: uint,
name: z.string().min(1).nullable(),
name_rva: uint,
attributes: uint,
import_address_table_rva: uint,
import_name_table_rva: uint,
bound_import_rva: uint,
unload_information_table_rva: uint,
module_handle_rva: uint,
time_date_stamp: uint,
location: range,
functions: z.array(peImportFunctionSchema),
});
/**
* `locations` are the file spans the parser actually consumed for the
* descriptor array and its terminator, in RVA order. Consecutive descriptors
* that map to discontiguous file bytes produce separate spans.
* No bytes are consumed when the first slot cannot be examined; the declared
* directory remains present and coverage is partial.
*/
const directorySchema = z
.strictObject({
rva: uint,
size: uint,
locations: z.array(range),
})
.nullable();
const peImportIssueSchema = z.strictObject({
code: z.enum([
"malformed_entry",
"descriptor_unreadable",
"name_unreadable",
"thunk_unreadable",
"unsupported_form",
]),
location: z.string().min(1).nullable(),
message: z.string().min(1),
});
const coverageSchema = z.strictObject({
status: z.enum(["complete", "partial"]),
reason: z.string().min(1).nullable(),
examined_dlls: uint,
imports: uint,
delay_imports: uint,
});
const imphashSchema = z.strictObject({
algorithm: z.literal("pefile"),
md5: md5Schema.nullable(),
reason: z.string().min(1).nullable(),
});
/** Static import inventory of one PE32/PE32+ image, read without execution. */
export const peImportsSchema = z.strictObject({
artifact: z.strictObject({
path: z.string().min(1),
sha256: digestSchema,
bytes: uint,
}),
format: z.enum(["pe32", "pe32-plus"]),
machine: uint,
bits: z.union([z.literal(32), z.literal(64)]),
architecture: z.string().min(1),
import_directory: directorySchema,
delay_import_directory: directorySchema,
imports: z.array(peImportDllSchema),
delay_imports: z.array(peDelayImportDllSchema),
imphash: imphashSchema,
coverage: coverageSchema,
issues: z.array(peImportIssueSchema),
limitations: z.array(z.string().min(1)),
});
export type PeImports = z.infer<typeof peImportsSchema>;
export type PeImportDll = z.infer<typeof peImportDllSchema>;
export type PeDelayImportDll = z.infer<typeof peDelayImportDllSchema>;
export type PeImportFunction = z.infer<typeof peImportFunctionSchema>;
export type PeImportIssue = z.infer<typeof peImportIssueSchema>;
+140 -18
View File
@@ -1,10 +1,95 @@
import { expect, it } from "vitest";
import {
BinaryFormatFailure,
BinaryMappingFailure,
createFileMappingIndex,
mappedFileOffset,
readMappedCString,
type BinaryContainer,
type FileMapping,
type ZeroFillMapping,
} from "./BinaryContainer.js";
const outcome = (translate: () => number): number | string => {
try {
return translate();
} catch (cause) {
if (cause instanceof BinaryMappingFailure) return cause.reason;
if (cause instanceof BinaryFormatFailure) return cause.message;
throw cause;
}
};
/** Deterministic xorshift so failures replay exactly. */
const random = (seed: number) => {
let state = seed;
return (limit: number): number => {
state ^= state << 13;
state ^= state >>> 17;
state ^= state << 5;
return (state >>> 0) % limit;
};
};
it("indexes exactly the spans mappedFileOffset accepts, rejects and translates", () => {
const next = random(0x5eed);
const seen = new Set<string>();
for (let round = 0; round < 150; round++) {
const mappings: FileMapping[] = Array.from({ length: 1 + next(6) }, () => ({
address: 0x1000n + BigInt(next(96)),
offset: next(4) === 0 ? 0x400 : next(256),
size: next(48),
}));
// Some rounds alias one mapping at the same translation to cover consistent overlap.
if (next(3) === 0 && mappings[0] !== undefined)
mappings.push({
address: mappings[0].address + 4n,
offset: mappings[0].offset + 4,
size: Math.max(0, mappings[0].size - 4),
});
const zeroFills: ZeroFillMapping[] = Array.from(
{ length: next(3) },
() => ({
address: 0x1000n + BigInt(next(160)),
size: BigInt(next(16)),
}),
);
const lookup = createFileMappingIndex(mappings, zeroFills).offset;
const indexed: (number | string)[] = [];
const scanned: (number | string)[] = [];
for (let address = 0xff8n; address < 0x10a8n; address++) {
for (const size of [1, 2, 4, 9, 20]) {
indexed.push(outcome(() => lookup(address, size)));
scanned.push(
outcome(() => mappedFileOffset(mappings, address, size, zeroFills)),
);
}
}
expect(indexed, `round ${String(round)}`).toEqual(scanned);
for (const value of scanned)
seen.add(typeof value === "number" ? "translated" : value);
}
expect([...seen].sort()).toEqual(["ambiguous", "translated", "unmapped"]);
});
it("maps adjacent ranges sharing one file translation", () => {
const lookup = createFileMappingIndex([
{ address: 0x1000n, offset: 0x400, size: 0x10 },
{ address: 0x1010n, offset: 0x410, size: 0x10 },
]).offset;
expect(lookup(0x1008n, 8)).toBe(0x408);
expect(lookup(0x100cn, 8)).toBe(0x40c);
});
it("rejects partially overlapping mappings with different file translations", () => {
const lookup = createFileMappingIndex([
{ address: 0x1000n, offset: 0x400, size: 0x20 },
{ address: 0x1018n, offset: 0x900, size: 0x10 },
]).offset;
expect(lookup(0x1000n, 0x10)).toBe(0x400);
expect(() => lookup(0x1010n, 0x10)).toThrow(/ambiguous/u);
});
const mappings = [
{ address: 100n, offset: 0, size: 4 },
{ address: 104n, offset: 4, size: 4 },
@@ -32,21 +117,58 @@ it("refuses a zero-fill alias inside a span spanning multiple mappings", () => {
mappedFileOffset(mappings, 102n, 4, [{ address: 104n, size: 1n }]),
).toThrow(/zero-fill/);
});
it("reads strings across discontiguous source ranges without examining bytes after NUL", () => {
const image: BinaryContainer = {
format: "pe",
architecture: "amd64",
bits: 64,
byte_order: "little",
pe_header_mapping: mappings[0]!,
mappings: [{ address: 104n, offset: 8, size: 4 }],
zero_fills: [{ address: 106n, size: 2n }],
sections: [],
};
expect(
readMappedCString(Buffer.from("ABCDxxxxE\0xx"), image, 102n, 8).toString(),
).toBe("CDE");
expect(() =>
mappedFileOffset([...mappings.slice(0, 1), ...image.mappings], 102n, 4),
).toThrow(/ambiguous/);
});
it.each(["scan", "index"] as const)(
"reads strings across discontiguous source ranges without examining bytes after NUL (%s)",
(mode) => {
const image: BinaryContainer = {
format: "pe",
architecture: "amd64",
bits: 64,
byte_order: "little",
pe_header_mapping: mappings[0]!,
mappings: [{ address: 104n, offset: 8, size: 4 }],
zero_fills: [{ address: 106n, size: 2n }],
sections: [],
};
const bytes = Buffer.from("ABCDxxxxE\0xx");
const result =
mode === "scan"
? readMappedCString(bytes, image, 102n, 8)
: createFileMappingIndex(
[mappings[0]!, ...image.mappings],
image.zero_fills,
).readCString(bytes, 102n, 8);
expect(result.toString()).toBe("CDE");
expect(() =>
mappedFileOffset([...mappings.slice(0, 1), ...image.mappings], 102n, 4),
).toThrow(/ambiguous/);
},
);
it.each([0, -1, 0.5, Number.NaN, Number.POSITIVE_INFINITY])(
"preserves zero-span and invalid-size semantics for %s",
(size) => {
const index = createFileMappingIndex(mappings);
expect(outcome(() => index.offset(104n, size))).toEqual(
outcome(() => mappedFileOffset(mappings, 104n, size)),
);
},
);
it.each([false, true])(
"keeps conflict classification before gap and zero-fill checks (%s)",
(conflicting) => {
const ranges = [
{ address: 100n, offset: 0, size: 2 },
{ address: 104n, offset: conflicting ? 8 : 4, size: 2 },
];
const zeroFills = [{ address: 102n, size: 1n }];
const index = createFileMappingIndex(ranges, zeroFills);
expect(outcome(() => index.offset(101n, 4))).toBe(
conflicting ? "ambiguous" : "unmapped",
);
expect(outcome(() => index.offset(101n, 4))).toEqual(
outcome(() => mappedFileOffset(ranges, 101n, 4, zeroFills)),
);
},
);
+227 -6
View File
@@ -315,6 +315,207 @@ export const mappedFileOffset = (
return found;
};
/** Index of the last sorted value at or below `value`, or -1. */
const lastAtOrBelow = (sorted: readonly bigint[], value: bigint): number => {
let low = 0;
let high = sorted.length;
while (low < high) {
const middle = (low + high) >>> 1;
if ((sorted[middle] as bigint) <= value) low = middle + 1;
else high = middle;
}
return low - 1;
};
/** Sorted starts with the furthest end reached by any span starting at or before each. */
const prefixReach = (
spans: readonly { readonly start: bigint; readonly end: bigint }[],
): { readonly starts: bigint[]; readonly reach: bigint[] } => {
const sorted = [...spans].sort((left, right) =>
left.start < right.start ? -1 : left.start > right.start ? 1 : 0,
);
const starts: bigint[] = [];
const reach: bigint[] = [];
let furthest = -1n;
for (const span of sorted) {
if (span.end > furthest) furthest = span.end;
starts.push(span.start);
reach.push(furthest);
}
return { starts, reach };
};
/**
* Indexed `mappedFileOffset` for parsers that translate many spans in one
* image. Construction is O(n log n) over mappings; each lookup is O(log n), so
* hostile section counts cannot multiply the cost of every table entry. It
* accepts, rejects and translates exactly the spans `mappedFileOffset` does;
* a span that is both a conflicting file alias and a zero-fill overlap may be
* refused under either ambiguity message.
*/
export const createFileMappingIndex = (
mappings: readonly FileMapping[],
zeroFills: readonly ZeroFillMapping[] = [],
): FileMappingIndex => {
const backed = mappings
.filter((mapping) => mapping.size > 0)
.map((mapping) => ({
start: mapping.address,
end: mapping.address + BigInt(mapping.size),
translation: BigInt(mapping.offset) - mapping.address,
}));
const zeroFilled = prefixReach(
zeroFills
.filter((zeroFill) => zeroFill.size > 0n)
.map((zeroFill) => ({
start: zeroFill.address,
end: zeroFill.address + zeroFill.size,
})),
);
// Elementary intervals between every mapping boundary, each with the single
// file translation shared by all mappings covering it (or none/ambiguous).
const events = new Map<bigint, { add: bigint[]; remove: bigint[] }>();
const event = (at: bigint) => {
let entry = events.get(at);
if (entry === undefined) {
entry = { add: [], remove: [] };
events.set(at, entry);
}
return entry;
};
for (const span of backed) {
event(span.start).add.push(span.translation);
event(span.end).remove.push(span.translation);
}
const boundaries = [...events.keys()].sort((left, right) =>
left < right ? -1 : left > right ? 1 : 0,
);
const active = new Map<bigint, number>();
// Undefined is a gap; null is an overlap with conflicting translations.
const translations: (bigint | null | undefined)[] = [];
for (const boundary of boundaries) {
const entry = events.get(boundary);
for (const translation of entry?.remove ?? []) {
const count = (active.get(translation) ?? 0) - 1;
if (count === 0) active.delete(translation);
else active.set(translation, count);
}
for (const translation of entry?.add ?? [])
active.set(translation, (active.get(translation) ?? 0) + 1);
translations.push(
active.size === 0
? undefined
: active.size === 1
? (active.keys().next().value as bigint)
: null,
);
}
// runEnd[i]: end of the maximal run of intervals sharing interval i's translation.
const runEnd: bigint[] = boundaries.map(() => 0n);
for (let index = boundaries.length - 1; index >= 0; index--) {
const next = boundaries[index + 1];
const translation = translations[index];
runEnd[index] =
next === undefined
? (boundaries[index] as bigint)
: translation !== null &&
translation !== undefined &&
translations[index + 1] === translation
? (runEnd[index + 1] as bigint)
: next;
}
const ambiguousPrefix = [0];
const previousMapped: number[] = [];
const nextMapped: number[] = [];
const translationRuns: number[] = [];
let previous = -1;
let run = 0;
let previousTranslation: bigint | undefined;
for (const [index, translation] of translations.entries()) {
ambiguousPrefix.push(
(ambiguousPrefix[index] ?? 0) + (translation === null ? 1 : 0),
);
if (typeof translation === "bigint") {
if (translation !== previousTranslation) run += 1;
previousTranslation = translation;
previous = index;
}
previousMapped.push(previous);
translationRuns.push(run);
}
let next = translations.length;
for (let index = translations.length - 1; index >= 0; index--) {
if (typeof translations[index] === "bigint") next = index;
nextMapped[index] = next;
}
const offset = (address: bigint, size: number): number => {
if (!Number.isSafeInteger(size) || size <= 0)
return mappedFileOffset(mappings, address, size, zeroFills);
const end = address + BigInt(size);
const interval = lastAtOrBelow(boundaries, address);
const lower = Math.max(0, interval);
const upper = lastAtOrBelow(boundaries, end - 1n);
const first = nextMapped[lower] ?? translations.length;
const last = previousMapped[upper] ?? -1;
// Like the scan, conflicts anywhere in the requested span win over gaps.
if (
(ambiguousPrefix[upper + 1] ?? 0) - (ambiguousPrefix[lower] ?? 0) > 0 ||
(first <= upper &&
last >= lower &&
translationRuns[first] !== translationRuns[last])
)
throw new BinaryMappingFailure(
"ambiguous",
"Virtual span has ambiguous or noncontiguous file mappings",
);
const translation = translations[interval];
if (typeof translation !== "bigint" || (runEnd[interval] as bigint) < end)
throw new BinaryMappingFailure(
"unmapped",
"Virtual address or string range is not file mapped",
);
const zeroFill = lastAtOrBelow(zeroFilled.starts, end - 1n);
if (zeroFill >= 0 && (zeroFilled.reach[zeroFill] as bigint) > address)
throw new BinaryMappingFailure(
"ambiguous",
"Virtual address has ambiguous file and zero-fill mappings",
);
return Number(address + translation);
};
const access: MappedStringAccess = {
offset,
mappingAt: (address) => {
const start = offset(address, 1);
const interval = lastAtOrBelow(boundaries, address);
return {
address,
offset: start,
size: Number((runEnd[interval] as bigint) - address),
};
},
};
return {
offset,
readCString: (bytes, address, maximumBytes) =>
readMappedString(bytes, access, address, maximumBytes),
};
};
/** One immutable image's indexed mappings and bounded string access. */
export interface FileMappingIndex {
readonly offset: (address: bigint, size: number) => number;
readonly readCString: (
bytes: Buffer,
address: bigint,
maximumBytes: number,
) => Buffer;
}
interface MappedStringAccess {
readonly mappingAt: (address: bigint) => FileMapping | undefined;
readonly offset: (address: bigint, size: number) => number;
}
/** Read a bounded NUL-terminated string while retaining mapping/alias semantics. */
export const readMappedCString = (
bytes: Buffer,
@@ -323,16 +524,36 @@ export const readMappedCString = (
maximumBytes: number,
): Buffer => {
const mappings = imageFileMappings(image);
return readMappedString(
bytes,
{
mappingAt: (cursor) =>
mappings.find(
(range) =>
range.address <= cursor &&
cursor < range.address + BigInt(range.size),
),
offset: (cursor, size) =>
mappedFileOffset(mappings, cursor, size, image.zero_fills),
},
address,
maximumBytes,
);
};
const readMappedString = (
bytes: Buffer,
access: MappedStringAccess,
address: bigint,
maximumBytes: number,
): Buffer => {
const chunks: Buffer[] = [];
let length = 0;
while (length <= maximumBytes) {
const cursor = address + BigInt(length);
const mapping = mappings.find(
(range) =>
range.address <= cursor && cursor < range.address + BigInt(range.size),
);
const mapping = access.mappingAt(cursor);
if (mapping === undefined) {
mappedFileOffset(mappings, cursor, 1, image.zero_fills);
access.offset(cursor, 1);
throw new BinaryMappingFailure(
"unmapped",
"String range is not file mapped",
@@ -346,7 +567,7 @@ export const readMappedCString = (
const chunk = bytes.subarray(offset, offset + size);
const terminator = chunk.indexOf(0);
const consumed = terminator < 0 ? size : terminator + 1;
mappedFileOffset(mappings, cursor, consumed, image.zero_fills);
access.offset(cursor, consumed);
if (terminator >= 0) {
const tail = chunk.subarray(0, terminator);
if (chunks.length === 0) return tail;
+28 -3
View File
@@ -61,8 +61,29 @@ const optionalHeaderMagic = (
return reader.u16(offset);
};
/** Declared PE fields retained by their parsing owner for format consumers. */
export type PeImageSection = Extract<
ImageSection,
{ readonly format: "pe" }
> & {
readonly raw_offset: number;
};
export type PeImage = BinaryContainer & {
readonly format: "pe";
readonly image_base: bigint;
readonly pe_machine: number;
readonly pe_section_alignment: number;
readonly pe_file_alignment: number;
readonly pe_header_mapping: FileMapping;
readonly pe_data_directories: NonNullable<
BinaryContainer["pe_data_directories"]
>;
readonly sections: readonly PeImageSection[];
};
/** Read PE32/PE32+ native image mappings and section facts. */
export const readPeImage = (bytes: Buffer): BinaryContainer => {
export const readPeImage = (bytes: Buffer): PeImage => {
const reader = new BinaryReader(bytes, true);
const pe = peHeaderOffset(reader);
const optional = pe + 24;
@@ -107,7 +128,7 @@ export const readPeImage = (bytes: Buffer): BinaryContainer => {
const mappings: FileMapping[] = [];
const executableMappings: FileMapping[] = [];
const zeroFills: ZeroFillMapping[] = [];
const sections: ImageSection[] = [];
const sections: PeImageSection[] = [];
for (let index = 0; index < count; index++) {
const entry = start + index * 40;
const {
@@ -167,6 +188,9 @@ export const readPeImage = (bytes: Buffer): BinaryContainer => {
return {
format: "pe",
image_base: imageBase,
pe_machine: machine,
pe_section_alignment: reader.u32(optional + 32),
pe_file_alignment: fileAlignment,
pe_data_directories: dataDirectories,
pe_header_mapping: headerMapping,
architecture: names.get(machine) ?? `pe-machine-${machine}`,
@@ -199,7 +223,7 @@ const peImageSection = ({
readonly virtualSize: number;
readonly rawSize: number;
readonly fileAlignment: number;
}): ImageSection => {
}): PeImageSection => {
const range = declaredFileRange(
reader.bytes.length,
BigInt(offset),
@@ -217,6 +241,7 @@ const peImageSection = ({
virtual_address: virtualAddress,
virtual_size: virtualSize,
raw_size: rawSize,
raw_offset: offset,
file_alignment: fileAlignment,
characteristics,
...range,
+650
View File
@@ -0,0 +1,650 @@
import { createHash } from "node:crypto";
import { expect, it } from "vitest";
import {
inspectPeImportsInputSchema,
peImportsSchema,
} from "../../domain/native/peImports.js";
import {
peImportFixtureOffset,
peImportImage,
peImportLeadingSectionImage,
peImportManySectionsImage,
peImportDirectoryAtRva,
peImportDirectoryMappingStops,
peImportNameAtRva,
peWithoutImportTableFixture,
} from "../../../tests/fixtures/binaryDiagnostics/peImports.js";
import { parsePeImports, PeImportLimitFailure } from "./PeImportParser.js";
const input = inspectPeImportsInputSchema.parse({
path: "/artifacts/app.exe",
});
const artifact = (bytes: Buffer) => ({
path: "/artifacts/app.exe",
sha256: createHash("sha256").update(bytes).digest("hex"),
bytes: bytes.length,
});
it("reads named PE32+ imports and a pefile-compatible import hash", async () => {
const bytes = peImportImage({ bits: 64 });
const report = await parsePeImports(bytes, artifact(bytes), input);
expect(report.format).toBe("pe32-plus");
expect(report.bits).toBe(64);
expect(report.imports).toHaveLength(1);
expect(report.imports[0]?.name).toBe("KERNEL32.dll");
expect(report.imports[0]?.functions.map((row) => row.name)).toEqual([
"GetProcAddress",
"ExitProcess",
]);
expect(report.imphash.md5).toBe("92cef6cc91a85825f3c8a0516517c66f");
expect(report.coverage.status).toBe("complete");
expect(report.delay_imports).toEqual([]);
});
const parse = (bytes: Buffer) => parsePeImports(bytes, artifact(bytes), input);
it.each([
["standard", 0],
["standard", 1],
["delay", 0],
["delay", 1],
] as const)(
"retains %s rows before a descriptor mapping gap after %s rows",
async (kind, retained) => {
const report = await parse(peImportDirectoryMappingStops(kind, retained));
const directory =
kind === "standard"
? report.import_directory
: report.delay_import_directory;
const rows = kind === "standard" ? report.imports : report.delay_imports;
const width = kind === "standard" ? 20 : 32;
expect(rows).toHaveLength(retained);
expect(directory).toMatchObject({
rva: 0x40000000,
locations: retained === 0 ? [] : [{ offset: 0x1000, bytes: width }],
});
expect(report.coverage.status).toBe("partial");
expect(report.issues).toContainEqual(
expect.objectContaining({
code: "descriptor_unreadable",
location: `0x${(0x40000000 + retained * width).toString(16)}`,
}),
);
expect(report.imphash.md5).toBe(
kind === "delay" ? "92cef6cc91a85825f3c8a0516517c66f" : null,
);
expect(peImportsSchema.safeParse(report).success).toBe(true);
},
);
it.each([
["standard", 0xffffffec, 1, "0x100000000"],
["standard", 0xfffffff0, 0, "0xfffffff0"],
["delay", 0xffffffe0, 1, "0x100000000"],
["delay", 0xfffffff0, 0, "0xfffffff0"],
] as const)(
"stops the %s descriptor walk at RVA %s",
async (kind, rva, retained, invalid) => {
const report = await parse(peImportDirectoryAtRva(kind, rva));
const directory =
kind === "standard"
? report.import_directory
: report.delay_import_directory;
const rows = kind === "standard" ? report.imports : report.delay_imports;
expect(rows).toHaveLength(retained);
expect(directory?.rva).toBe(rva);
expect(directory?.locations).toHaveLength(retained);
expect(report.coverage.status).toBe("partial");
expect(report.issues).toEqual(
expect.arrayContaining([
expect.objectContaining({ code: "malformed_entry", location: invalid }),
]),
);
expect(peImportsSchema.safeParse(report).success).toBe(true);
expect(report.imphash.md5).toBe(
kind === "delay" ? "92cef6cc91a85825f3c8a0516517c66f" : null,
);
},
);
it("does not consume a DLL name past the uint32 RVA end", async () => {
const report = await parse(peImportNameAtRva(0xfffffff8));
expect(report.imports[0]?.name).toBeNull();
expect(report.imports[0]?.functions).toHaveLength(2);
expect(report.coverage.status).toBe("partial");
expect(report.issues).toEqual(
expect.arrayContaining([
expect.objectContaining({
code: "name_unreadable",
location: "0xfffffff8",
}),
]),
);
expect(report.imphash.md5).toBeNull();
});
it("accepts a DLL name whose NUL is the final uint32 RVA byte", async () => {
const report = await parse(peImportNameAtRva(0xfffffff3));
expect(report.imports[0]?.name).toBe("KERNEL32.dll");
expect(report.coverage.status).toBe("complete");
expect(report.imphash.md5).toBe("92cef6cc91a85825f3c8a0516517c66f");
});
it.each([
[0x8000000100000001n, "72d9cd67504bb3b9f29c37a226bf4331"],
[0x8000000000010001n, null],
[0x8000000000000001n, "72d9cd67504bb3b9f29c37a226bf4331"],
] as const)(
"matches pefile 2024.8.26 for ordinal thunk %s",
async (thunk, expected) => {
const bytes = peImportImage();
for (const rva of [0x1028, 0x1040])
bytes.writeBigUInt64LE(thunk, peImportFixtureOffset(rva));
const report = await parse(bytes);
expect(report.imports[0]?.functions[0]).toMatchObject({
ordinal: 1,
thunk_hex: thunk.toString(16),
});
expect(report.imphash.md5).toBe(expected);
},
);
it("withholds the hash beyond pefile's section parser capacity", async () => {
const report = await parse(peImportManySectionsImage(2049, 2));
expect(report.imports[0]?.functions.map((row) => row.name)).toEqual([
"Function0000",
"Function0001",
]);
expect(report.coverage.status).toBe("complete");
expect(report.imphash.md5).toBeNull();
expect(report.imphash.reason).toMatch(/mapping|compatibility/u);
});
it.each([false, true])(
"withholds the hash when pefile stops at an earlier section (suspicious: %s)",
async (suspicious) => {
const report = await parse(peImportLeadingSectionImage(suspicious));
expect(report.imports[0]?.functions.map((row) => row.name)).toEqual([
"GetProcAddress",
"ExitProcess",
]);
expect(report.coverage.status).toBe("complete");
expect(report.imphash.md5).toBeNull();
expect(report.imphash.reason).toMatch(/mapping|compatibility/u);
},
);
/** Default PE32+ KERNEL32 image with one descriptor field rewritten. */
const withDescriptorField = (
field: number,
value: number,
options: Parameters<typeof peImportImage>[0] = {},
) => {
const bytes = peImportImage(options);
bytes.writeUInt32LE(value, peImportFixtureOffset(0x1000) + field);
return bytes;
};
it("hashes unmapped ordinals as pefile's ord<decimal> fallback", async () => {
const bytes = peImportImage({
bits: 32,
dlls: [
{
name: "USER32.dll",
functions: [{ ordinal: 42 }],
},
],
});
const report = await parsePeImports(bytes, artifact(bytes), input);
expect(report.format).toBe("pe32");
expect(report.imports[0]?.functions[0]).toMatchObject({
name: null,
ordinal: 42,
hint: null,
});
// pefile 2024.8.26 get_imphash() over this image: MD5("user32.ord42").
expect(report.imphash).toEqual({
algorithm: "pefile",
md5: "f39f7ee67c122319a74d4270d3e5c36c",
reason: null,
});
});
it("falls back to ord<decimal> for DLL names that shadow object members", async () => {
const bytes = peImportImage({
bits: 32,
dlls: [
{ name: "constructor", functions: [{ ordinal: 1 }] },
{ name: "__proto__", functions: [{ ordinal: 2 }] },
],
});
// pefile: "constructor.ord1,__proto__.ord2".
expect((await parse(bytes)).imphash).toEqual({
algorithm: "pefile",
md5: "8a4fd3988e11f8e29b11a7edc60886e6",
reason: null,
});
});
it("uses pefile's complete ws2_32 and wsock32 ordinal tables", async () => {
const bytes = peImportImage({
bits: 32,
dlls: [
{
name: "WS2_32.dll",
functions: [{ ordinal: 23 }, { ordinal: 999 }, { name: "WSAStartup" }],
},
{ name: "wsock32.dll", functions: [{ ordinal: 1155 }, { ordinal: 1 }] },
],
});
// pefile: "ws2_32.socket,ws2_32.ord999,ws2_32.wsastartup,wsock32.ord1155,wsock32.accept".
expect((await parse(bytes)).imphash.md5).toBe(
"79b3bdee998e474e75390fac55093cb0",
);
});
it("withholds the import hash when a standard import name is unreadable", async () => {
const bytes = peImportImage();
const lookup = (await parse(bytes)).imports[0]?.original_first_thunk_rva ?? 0;
bytes.writeBigUInt64LE(0x7fff0000n, peImportFixtureOffset(lookup + 8));
const report = await parse(bytes);
expect(report.imports[0]?.functions.map((row) => row.name)).toEqual([
"GetProcAddress",
]);
expect(report.issues).toMatchObject([
{ code: "name_unreadable", location: "0x7fff0000" },
]);
expect(report.coverage.status).toBe("partial");
expect(report.imphash.md5).toBeNull();
expect(report.imphash.reason).toMatch(/standard import table is incomplete/u);
});
it("withholds the import hash when a standard thunk is unreadable", async () => {
const bytes = withDescriptorField(0, 0x7fff0000);
const report = await parse(bytes);
expect(report.imports[0]?.functions).toEqual([]);
expect(report.issues[0]?.code).toBe("thunk_unreadable");
expect(report.coverage.status).toBe("partial");
expect(report.imphash.md5).toBeNull();
});
it("reports a zero FirstThunk beside a populated lookup table as malformed", async () => {
const report = await parse(withDescriptorField(16, 0));
expect(report.imports[0]).toMatchObject({
name: "KERNEL32.dll",
first_thunk_rva: 0,
functions: [],
});
expect(report.issues).toMatchObject([
{ code: "malformed_entry", message: expect.stringMatching(/FirstThunk/u) },
]);
expect(report.coverage.status).toBe("partial");
expect(report.imphash.md5).toBeNull();
expect(report.imphash.reason).toMatch(/incomplete/u);
});
it.each([
["import address table", 12],
["import name table", 16],
])(
"reports a delay-load descriptor with a zero %s as malformed without withholding the standard hash",
async (table, field) => {
const bytes = peImportImage({
delayDlls: [
{ name: "ADVAPI32.dll", functions: [{ name: "RegOpenKeyExW" }] },
],
});
const location = (await parse(bytes)).delay_imports[0]?.location;
if (location === undefined) throw new Error("Expected a delay descriptor");
bytes.writeUInt32LE(0, location.offset + field);
const report = await parse(bytes);
expect(report.delay_imports[0]?.functions).toEqual([]);
expect(report.issues).toMatchObject([
{
code: "malformed_entry",
message: expect.stringContaining(`zero ${table} RVA`),
},
]);
expect(report.coverage.status).toBe("partial");
expect(report.imphash.md5).toBe("92cef6cc91a85825f3c8a0516517c66f");
},
);
it("reports each discontiguous file span a descriptor array consumes", async () => {
const options = {
delayDlls: [
{ name: "ADVAPI32.dll", functions: [{ name: "RegOpenKeyExW" }] },
],
};
const whole = await parse(peImportImage(options));
expect(whole.import_directory?.locations).toEqual([
{ offset: 0x400, bytes: 40 },
]);
expect(whole.delay_import_directory?.locations).toEqual([
{ offset: 0x428, bytes: 64 },
]);
// Second section bytes move to file offset 0x400 + 0x800 + 0x200.
const importSplit = await parse(peImportImage({ ...options, splitAt: 20 }));
expect(importSplit.import_directory?.locations).toEqual([
{ offset: 0x400, bytes: 20 },
{ offset: 0xe00, bytes: 20 },
]);
expect(importSplit.delay_import_directory?.locations).toEqual([
{ offset: 0xe14, bytes: 64 },
]);
const delaySplit = await parse(peImportImage({ ...options, splitAt: 72 }));
expect(delaySplit.import_directory?.locations).toEqual([
{ offset: 0x400, bytes: 40 },
]);
expect(delaySplit.delay_import_directory?.locations).toEqual([
{ offset: 0x428, bytes: 32 },
{ offset: 0xe00, bytes: 32 },
]);
for (const report of [importSplit, delaySplit]) {
expect(report.coverage.status).toBe("complete");
expect(report.imports[0]?.functions.map((row) => row.name)).toEqual([
"GetProcAddress",
"ExitProcess",
]);
expect(report.delay_imports[0]?.functions[0]?.name).toBe("RegOpenKeyExW");
expect(report.imphash.md5).toBe("92cef6cc91a85825f3c8a0516517c66f");
}
});
it.each([
[
"a function name pefile treats as invalid",
{ name: "Get-Proc" },
/invalid/u,
],
["ordinal 0", { ordinal: 0 }, /ordinal 0/u],
["a name pefile truncates", { name: "A".repeat(512) }, /invalid/u],
] as const)(
"withholds the import hash for %s rather than diverge from pefile",
async (_label, entry, reason) => {
const report = await parse(
peImportImage({
sectionBytes: 0x1000,
dlls: [{ name: "KERNEL32.dll", functions: [entry] }],
}),
);
expect(report.coverage.status).toBe("complete");
expect(report.imphash.md5).toBeNull();
expect(report.imphash.reason).toMatch(reason);
},
);
it("reads delay-load descriptors separately from the import hash", async () => {
const bytes = peImportImage({
delayDlls: [
{
name: "ADVAPI32.dll",
functions: [{ name: "RegOpenKeyExW", hint: 3 }],
},
],
});
const report = await parsePeImports(bytes, artifact(bytes), input);
expect(report.delay_imports).toHaveLength(1);
expect(report.delay_imports[0]?.name).toBe("ADVAPI32.dll");
expect(report.delay_imports[0]?.functions[0]?.name).toBe("RegOpenKeyExW");
expect(report.imphash.md5).toBe("92cef6cc91a85825f3c8a0516517c66f");
});
it("reports an image without an import directory as importing nothing", async () => {
const bytes = peWithoutImportTableFixture();
const report = await parsePeImports(bytes, artifact(bytes), input);
expect(report.import_directory).toBeNull();
expect(report.imports).toEqual([]);
expect(report.coverage.status).toBe("complete");
expect(report.limitations[0]).toContain(
"IMAGE_DIRECTORY_ENTRY_IMPORT is absent",
);
});
it("fails closed when the DLL budget is exhausted", async () => {
const bytes = peImportImage({
dlls: [
{ name: "A.dll", functions: [{ name: "a" }] },
{ name: "B.dll", functions: [{ name: "b" }] },
],
});
await expect(
parsePeImports(
bytes,
artifact(bytes),
inspectPeImportsInputSchema.parse({
path: "/artifacts/app.exe",
max_dlls: 1,
}),
),
).rejects.toThrow(
expect.objectContaining({ limit: "dlls", maximum_bytes: 1 }),
);
});
it("fails closed when the function budget is exhausted", async () => {
const bytes = peImportImage({
dlls: [
{
name: "KERNEL32.dll",
functions: [{ name: "GetProcAddress" }, { name: "ExitProcess" }],
},
],
});
await expect(
parsePeImports(
bytes,
artifact(bytes),
inspectPeImportsInputSchema.parse({
path: "/artifacts/app.exe",
max_functions: 1,
}),
),
).rejects.toThrow(
expect.objectContaining({ limit: "functions", maximum_bytes: 1 }),
);
});
it("fails closed with a structured tag when retained name text is exhausted", async () => {
const name = "A".repeat(4096);
const bytes = peImportImage({
sectionBytes: 0x1200000,
dlls: [
{
name: "KERNEL32.dll",
functions: Array.from({ length: 4097 }, () => ({ name })),
},
],
});
const failure = parse(bytes);
await expect(failure).rejects.toThrow(PeImportLimitFailure);
await expect(failure).rejects.toThrow(
expect.objectContaining({
limit: "retained_text",
maximum_bytes: 16 * 1024 * 1024,
}),
);
});
it("computes a pefile import hash for oleaut32 ordinal names", async () => {
const bytes = peImportImage({
dlls: [
{
name: "OLEAUT32.dll",
functions: [{ ordinal: 2 }],
},
],
});
const report = await parsePeImports(bytes, artifact(bytes), input);
expect(report.imports[0]?.functions[0]).toMatchObject({
name: null,
ordinal: 2,
});
expect(report.imphash.md5).toBe("3dc76082b322e4bf41120e4657c7f5c6");
});
it("records delay-load virtual-address descriptors as unsupported", async () => {
const bytes = peImportImage({
delayDlls: [
{
name: "ADVAPI32.dll",
functions: [{ name: "RegOpenKeyExW" }],
},
],
delayAttributes: 0,
});
const report = await parsePeImports(bytes, artifact(bytes), input);
expect(report.delay_imports).toEqual([]);
expect(report.coverage.status).toBe("partial");
expect(report.issues[0]?.code).toBe("unsupported_form");
expect(report.imphash.md5).toBe("92cef6cc91a85825f3c8a0516517c66f");
});
it("withholds the import hash exactly where pefile's import symbol cap truncates", async () => {
const image = (sizes: readonly number[]) =>
peImportImage({
sectionBytes: 0x100000,
dlls: sizes.map((count, dll) => ({
name: `D${String(dll)}.dll`,
functions: Array.from({ length: count }, (_, index) => ({
name: `F${String(dll)}_${String(index)}`,
})),
})),
});
// pefile 2024.8.26 hashes all 4096 rows here...
expect((await parse(image([4095, 1]))).imphash.md5).toBe(
"2217c72699a462a8ca9980afe6a852aa",
);
// ...but drops D1.dll here because its 8192-thunk cap is reached first.
const capped = await parse(image([4096, 1]));
expect(capped.coverage.status).toBe("complete");
expect(capped.imphash.md5).toBeNull();
expect(capped.imphash.reason).toMatch(/descriptor 1 .*symbol cap/u);
});
it("yields during long table walks and stops when the caller cancels", async () => {
const bytes = peImportImage({
sectionBytes: 0x20000,
dlls: [
{
name: "KERNEL32.dll",
functions: Array.from({ length: 4000 }, (_, index) => ({
name: `F${String(index)}`,
})),
},
],
});
const controller = new AbortController();
const pending = parsePeImports(
bytes,
artifact(bytes),
input,
controller.signal,
);
let ticks = 0;
const tick = setImmediate(() => {
ticks += 1;
controller.abort();
});
await expect(pending).rejects.toMatchObject({
name: "ArtifactReaderFailure",
reason: "cancelled",
});
clearImmediate(tick);
expect(ticks).toBe(1);
});
it("withholds the import hash once pefile would discard a repetitive lookup table", async () => {
const repeated = async (repeats: number) => {
const bytes = peImportImage({
dlls: [
{
name: "KERNEL32.dll",
functions: Array.from({ length: repeats + 1 }, (_, index) => ({
name: `F${String(index)}`,
})),
},
],
});
const lookup = (await parse(bytes)).imports[0]?.original_first_thunk_rva;
if (lookup === undefined) throw new Error("Expected a lookup table");
const first = bytes.readBigUInt64LE(peImportFixtureOffset(lookup));
for (let index = 1; index <= repeats; index++)
bytes.writeBigUInt64LE(first, peImportFixtureOffset(lookup + index * 8));
return parse(bytes);
};
// pefile 2024.8.26 keeps 14 repeats and hashes the lookup rows...
expect((await repeated(14)).imphash.md5).toBe(
"a849c88f8733c2f2171c4130e925bca6",
);
// ...but discards the table at 15 and hashes the import address table instead.
const discarded = await repeated(15);
expect(discarded.coverage.status).toBe("complete");
expect(discarded.imphash.md5).toBeNull();
expect(discarded.imphash.reason).toMatch(/repeats hint\/name addresses 15/u);
});
it.each([32, 64] as const)(
"retains valid rows before a PE%s IAT RVA overflows",
async (bits) => {
const width = bits / 8;
const bytes = withDescriptorField(16, 0x100000000 - width, { bits });
const report = await parse(bytes);
expect(peImportsSchema.safeParse(report).success).toBe(true);
expect(report.imports[0]?.functions).toHaveLength(1);
expect(report.imports[0]?.functions[0]?.iat_rva).toBe(0x100000000 - width);
expect(report.issues).toEqual(
expect.arrayContaining([
expect.objectContaining({
code: "malformed_entry",
location: "0x100000000",
}),
]),
);
expect(report.coverage.status).toBe("partial");
expect(report.imphash.md5).toBeNull();
},
);
it("preserves an invalid wide named thunk without rounding its source value", async () => {
const bytes = peImportImage();
bytes.writeBigUInt64LE(0x20000000000001n, peImportFixtureOffset(0x1028));
const report = await parse(bytes);
expect(report.issues).toEqual(
expect.arrayContaining([
expect.objectContaining({
code: "malformed_entry",
location: "0x20000000000001",
}),
]),
);
expect(report.imports[0]?.functions.map((row) => row.name)).toEqual([
"ExitProcess",
]);
expect(report.coverage.status).toBe("partial");
expect(report.imphash.md5).toBeNull();
});
it.each(["function", "DLL"])(
"retains a cross-section %s name but withholds an unproven pefile hash",
async (kind) => {
const count = kind === "function" ? 140 : 134;
const functions = Array.from({ length: count }, (_, i) => ({
name: `Function${String(i).padStart(3, "0")}${kind === "DLL" && i === 0 ? "A".repeat(10) : ""}`,
}));
const bytes = peImportImage({
sectionBytes: 0x4000,
splitAt: 0x1000,
dlls: [{ name: "KERNEL32.dll", functions }],
});
const report = await parse(bytes);
expect(report.imports[0]?.name).toBe("KERNEL32.dll");
expect(report.imports[0]?.functions.map((row) => row.name)).toEqual(
functions.map((row) => row.name),
);
expect(report.coverage.status).toBe("complete");
expect(report.imphash.md5).toBeNull();
expect(report.imphash.reason).toMatch(/mapping|compatibility/u);
},
);
+574
View File
@@ -0,0 +1,574 @@
import { createPefileReadCompatibility } from "./PefileReadCompatibility.js";
import { setImmediate } from "node:timers/promises";
import { assertArtifactReadActive } from "../../artifacts/ArtifactFileOperation.js";
import {
BinaryFormatFailure,
BinaryMappingFailure,
BinaryReader,
BinaryResourceFailure,
createFileMappingIndex,
imageFileMappings,
} from "../metadata/BinaryContainer.js";
import { readPeImage } from "../metadata/PeImage.js";
import {
MAX_INSPECT_PE_IMPORT_RETAINED_TEXT_BYTES,
type InspectPeImportsInput,
type PeDelayImportDll,
type PeImportDll,
type PeImportFunction,
type PeImportIssue,
type PeImports,
} from "../../domain/native/peImports.js";
import {
IMPORT_DESCRIPTOR_BYTES,
PEFILE_MAX_IMPORT_SYMBOLS,
pefileImphash,
} from "./pefileImphash.js";
type Artifact = PeImports["artifact"];
const PE_IMPORT_DIRECTORY = 1;
const PE_DELAY_IMPORT_DIRECTORY = 13;
const DELAY_DESCRIPTOR_BYTES = 32;
const MAXIMUM_NAME_BYTES = 4096;
/** Table entries decoded between event-loop yields and cancellation checks. */
const ENTRIES_PER_YIELD = 1024;
const ORDINAL_FLAG32 = 0x80000000n;
const ORDINAL_FLAG64 = 0x8000000000000000n;
const PE_RVA_END = 0x100000000;
/** PE32+ keeps 32-bit RVAs even though preferred virtual addresses are 64-bit. */
const fitsRva = (rva: number, size: number): boolean =>
Number.isSafeInteger(rva) &&
rva >= 0 &&
Number.isSafeInteger(size) &&
size > 0 &&
rva <= PE_RVA_END - size;
const requireRva = (rva: number, size: number): void => {
if (!fitsRva(rva, size))
throw new BinaryFormatFailure(
"malformed",
`PE RVA ${hex(rva)} span of ${String(size)} bytes extends beyond the 32-bit RVA space`,
);
};
const PE_LIMITATIONS = [
"Rows reproduce the PE import and delay-load directories; they do not establish that an import is bound, forwarded or reachable at runtime.",
"Bound import snapshots, the import address table as loaded, and export-forward resolution in another DLL remain unobserved.",
];
/** The parser-owned allowance an image exhausted. */
export type PeImportLimit = "dlls" | "functions" | "retained_text";
/** Structured parser allowance refusal; consumers branch on `limit`, never on prose. */
export class PeImportLimitFailure extends BinaryResourceFailure {
constructor(
readonly limit: PeImportLimit,
readonly allowance: number,
message: string,
) {
super("structure", allowance, message);
this.name = "PeImportLimitFailure";
}
}
const hex = (value: bigint | number): string => `0x${value.toString(16)}`;
const dllBudget = (count: number, max: number): void => {
if (count > max)
throw new PeImportLimitFailure(
"dlls",
max,
`Import inventory declares ${String(count)} descriptors, above the ${String(max)}-entry DLL allowance`,
);
};
const functionBudget = (count: number, max: number): void => {
if (count > max)
throw new PeImportLimitFailure(
"functions",
max,
`Import inventory declares ${String(count)} functions, above the ${String(max)}-entry function allowance`,
);
};
const createRetention = () => {
let text = 0;
return (value: string): void => {
text += value.length;
if (text > MAX_INSPECT_PE_IMPORT_RETAINED_TEXT_BYTES)
throw new PeImportLimitFailure(
"retained_text",
MAX_INSPECT_PE_IMPORT_RETAINED_TEXT_BYTES,
`Import names exceed the fixed ${String(MAX_INSPECT_PE_IMPORT_RETAINED_TEXT_BYTES)}-byte retained-text allowance`,
);
};
};
const failureMessage = (cause: unknown): string => {
if (cause instanceof BinaryFormatFailure) return cause.message;
throw cause;
};
/** Append one consumed file span, merging it into the previous span when contiguous. */
const consume = (
spans: { offset: number; bytes: number }[],
offset: number,
bytes: number,
): void => {
const last = spans.at(-1);
if (last !== undefined && last.offset + last.bytes === offset)
last.bytes += bytes;
else spans.push({ offset, bytes });
};
const coverage = (
issues: readonly PeImportIssue[],
examinedDlls: number,
imports: number,
delayImports: number,
): PeImports["coverage"] =>
issues.length === 0
? {
status: "complete",
reason: null,
examined_dlls: examinedDlls,
imports,
delay_imports: delayImports,
}
: {
status: "partial",
reason: `${String(issues.length)} import table issue(s) left entries unreported; see issues`,
examined_dlls: examinedDlls,
imports,
delay_imports: delayImports,
};
/**
* Read PE import and delay-load directories without executing the image.
* Yields to the event loop and honors cancellation every
* `ENTRIES_PER_YIELD` descriptors or thunks.
*/
export const parsePeImports = async (
bytes: Buffer,
artifact: Artifact,
input: InspectPeImportsInput,
signal?: AbortSignal,
): Promise<PeImports> => {
assertArtifactReadActive(signal);
const image = readPeImage(bytes);
const bits = image.bits;
const base = image.image_base;
const reader = new BinaryReader(bytes, true);
const lookup = createFileMappingIndex(
imageFileMappings(image),
image.zero_fills,
);
const compatibleRead = createPefileReadCompatibility(image, bytes.length);
let mappingCompatibility = true;
const map = (rva: number, size: number): number => {
requireRva(rva, size);
const offset = lookup.offset(base + BigInt(rva), size);
mappingCompatibility &&= compatibleRead(rva, size, offset);
return offset;
};
const format = bits === 64 ? ("pe32-plus" as const) : ("pe32" as const);
const result = {
artifact,
format,
machine: image.pe_machine,
bits,
architecture: image.architecture,
};
const issues: PeImportIssue[] = [];
const admitRva = (rva: number, size: number, label: string): boolean => {
if (fitsRva(rva, size)) return true;
issues.push({
code: "malformed_entry",
location: hex(rva),
message: `${label} span of ${String(size)} bytes extends beyond the 32-bit RVA space`,
});
return false;
};
const mapDescriptor = (
rva: number,
size: number,
label: string,
): number | null => {
if (!admitRva(rva, size, label)) return null;
try {
return map(rva, size);
} catch (cause) {
if (!(cause instanceof BinaryMappingFailure)) throw cause;
issues.push({
code: "descriptor_unreadable",
location: hex(rva),
message: `${label}: ${cause.message}`,
});
return null;
}
};
const retain = createRetention();
let examinedDlls = 0;
let examinedFunctions = 0;
let entries = 0;
const checkpoint = async (): Promise<void> => {
entries += 1;
if (entries % ENTRIES_PER_YIELD !== 0) return;
await setImmediate();
assertArtifactReadActive(signal);
};
const cString = (rva: number): string => {
requireRva(rva, 1);
const raw = lookup.readCString(
bytes,
base + BigInt(rva),
Math.min(MAXIMUM_NAME_BYTES, PE_RVA_END - rva - 1),
);
if (raw.length === 0)
throw new BinaryFormatFailure(
"malformed",
`Import name at RVA ${hex(rva)} is empty`,
);
// CString decoding may span physically discontiguous sections. A pefile
// hash is only justified when its single-section read sees the whole name.
if (mappingCompatibility) {
try {
const size = raw.length + 1;
const offset = lookup.offset(base + BigInt(rva), size);
mappingCompatibility = compatibleRead(rva, size, offset);
} catch (cause) {
if (!(cause instanceof BinaryFormatFailure)) throw cause;
mappingCompatibility = false;
}
}
return raw.toString("latin1");
};
const readName = (rva: number, label: string): string | null => {
try {
const name = cString(rva);
retain(name);
return name;
} catch (cause) {
issues.push({
code: "name_unreadable",
location: hex(rva),
message: `${label}: ${failureMessage(cause)}`,
});
return null;
}
};
const readFunctions = async (
nameTableRva: number,
addressTableRva: number,
): Promise<PeImportFunction[]> => {
const thunkWidth = bits === 64 ? 8 : 4;
const ordinalFlag = bits === 64 ? ORDINAL_FLAG64 : ORDINAL_FLAG32;
const functions: PeImportFunction[] = [];
for (let index = 0; ; index++) {
await checkpoint();
const nameRva = nameTableRva + index * thunkWidth;
const iatRva = addressTableRva + index * thunkWidth;
if (!admitRva(nameRva, thunkWidth, "Import lookup table")) break;
let thunk: bigint;
try {
thunk = reader.word(map(nameRva, thunkWidth), bits);
} catch (cause) {
issues.push({
code: "thunk_unreadable",
location: hex(nameRva),
message: failureMessage(cause),
});
break;
}
if (thunk === 0n) break;
if (!admitRva(iatRva, thunkWidth, "Import address table")) break;
examinedFunctions += 1;
functionBudget(examinedFunctions, input.max_functions);
const thunkHex = thunk.toString(16);
if ((thunk & ordinalFlag) !== 0n) {
functions.push({
hint: null,
name: null,
ordinal: Number(thunk & 0xffffn),
ilt_rva: nameTableRva === addressTableRva ? null : nameRva,
iat_rva: iatRva,
thunk_hex: thunkHex,
});
continue;
}
if (thunk > 0x7fffffffn) {
issues.push({
code: "malformed_entry",
location: `0x${thunkHex}`,
message:
"A named import thunk uses bits outside its 31-bit hint/name RVA",
});
continue;
}
const hintNameRva = Number(thunk);
try {
const hintAt = map(hintNameRva, 3);
const hint = reader.u16(hintAt);
const name = cString(hintNameRva + 2);
retain(name);
functions.push({
hint,
name,
ordinal: null,
ilt_rva: nameTableRva === addressTableRva ? null : nameRva,
iat_rva: iatRva,
thunk_hex: thunkHex,
});
} catch (cause) {
issues.push({
code: "name_unreadable",
location: hex(hintNameRva),
message: failureMessage(cause),
});
}
}
return functions;
};
// pefile counts every ILT and IAT thunk iteration against one image-wide cap
// and silently truncates tables past it. Replay that count (reading IAT
// entries only until the cap) to know when its import hash covers a prefix.
let pefileThunkReads = 0;
let pefileTruncatedDescriptor: number | null = null;
const pefileTableWalk = (rva: number, entries: number | null): number => {
const width = bits === 64 ? 8 : 4;
let processed = 0;
for (let index = 0; ; index++) {
if (pefileThunkReads > PEFILE_MAX_IMPORT_SYMBOLS) break;
pefileThunkReads += 1;
if (entries !== null) {
if (index === entries) break;
} else {
let word: bigint;
try {
word = reader.word(map(rva + index * width, width), bits);
} catch (cause) {
if (!(cause instanceof BinaryFormatFailure)) throw cause;
break;
}
if (word === 0n) break;
}
processed += 1;
}
return processed;
};
const replayPefileSymbolCap = (
index: number,
originalFirstThunk: number,
firstThunk: number,
entries: number,
): void => {
const table =
originalFirstThunk === 0
? pefileTableWalk(firstThunk, entries)
: pefileTableWalk(originalFirstThunk, entries);
if (originalFirstThunk !== 0) pefileTableWalk(firstThunk, null);
if (table < entries) pefileTruncatedDescriptor ??= index;
};
const missingTable = (descriptorRva: number, message: string): [] => {
issues.push({
code: "malformed_entry",
location: hex(descriptorRva),
message,
});
return [];
};
const importDirectory = image.pe_data_directories?.[PE_IMPORT_DIRECTORY];
const delayDirectory = image.pe_data_directories?.[PE_DELAY_IMPORT_DIRECTORY];
const imports: PeImportDll[] = [];
let importDirectoryResult: PeImports["import_directory"] = null;
if (importDirectory !== undefined && importDirectory.virtual_address !== 0) {
const spans: { offset: number; bytes: number }[] = [];
for (let index = 0; ; index++) {
await checkpoint();
const descriptorRva =
importDirectory.virtual_address + index * IMPORT_DESCRIPTOR_BYTES;
const descriptor = mapDescriptor(
descriptorRva,
IMPORT_DESCRIPTOR_BYTES,
"Import descriptor",
);
if (descriptor === null) break;
consume(spans, descriptor, IMPORT_DESCRIPTOR_BYTES);
const originalFirstThunk = reader.u32(descriptor);
const timeDateStamp = reader.u32(descriptor + 4);
const forwarderChain = reader.u32(descriptor + 8);
const nameRva = reader.u32(descriptor + 12);
const firstThunk = reader.u32(descriptor + 16);
if (
originalFirstThunk === 0 &&
timeDateStamp === 0 &&
forwarderChain === 0 &&
nameRva === 0 &&
firstThunk === 0
)
break;
examinedDlls += 1;
dllBudget(examinedDlls, input.max_dlls);
const name = readName(nameRva, "Import descriptor DLL name");
const functions =
firstThunk === 0
? missingTable(
descriptorRva,
originalFirstThunk === 0
? "Import descriptor declares neither a lookup table nor an import address table (FirstThunk is zero); its functions are unknown"
: `Import descriptor has a zero FirstThunk (import address table RVA); its lookup table at ${hex(originalFirstThunk)} is not decoded`,
)
: await readFunctions(
originalFirstThunk === 0 ? firstThunk : originalFirstThunk,
firstThunk,
);
if (firstThunk !== 0)
replayPefileSymbolCap(
index,
originalFirstThunk,
firstThunk,
functions.length,
);
imports.push({
index,
name,
name_rva: nameRva,
original_first_thunk_rva: originalFirstThunk,
first_thunk_rva: firstThunk,
time_date_stamp: timeDateStamp,
forwarder_chain: forwarderChain,
location: {
offset: descriptor,
bytes: IMPORT_DESCRIPTOR_BYTES,
},
functions,
});
}
importDirectoryResult = {
rva: importDirectory.virtual_address,
size: importDirectory.size,
locations: spans,
};
}
const standardIssues = issues.length;
const standardMappingCompatibility = mappingCompatibility;
const delayImports: PeDelayImportDll[] = [];
let delayDirectoryResult: PeImports["delay_import_directory"] = null;
if (delayDirectory !== undefined && delayDirectory.virtual_address !== 0) {
const spans: { offset: number; bytes: number }[] = [];
for (let index = 0; ; index++) {
await checkpoint();
const descriptorRva =
delayDirectory.virtual_address + index * DELAY_DESCRIPTOR_BYTES;
const descriptor = mapDescriptor(
descriptorRva,
DELAY_DESCRIPTOR_BYTES,
"Delay-load descriptor",
);
if (descriptor === null) break;
consume(spans, descriptor, DELAY_DESCRIPTOR_BYTES);
const attributes = reader.u32(descriptor);
const nameRva = reader.u32(descriptor + 4);
const moduleHandleRva = reader.u32(descriptor + 8);
const importAddressTableRva = reader.u32(descriptor + 12);
const importNameTableRva = reader.u32(descriptor + 16);
const boundImportRva = reader.u32(descriptor + 20);
const unloadInformationTableRva = reader.u32(descriptor + 24);
const timeDateStamp = reader.u32(descriptor + 28);
if (
attributes === 0 &&
nameRva === 0 &&
moduleHandleRva === 0 &&
importAddressTableRva === 0 &&
importNameTableRva === 0 &&
boundImportRva === 0 &&
unloadInformationTableRva === 0 &&
timeDateStamp === 0
)
break;
examinedDlls += 1;
dllBudget(examinedDlls, input.max_dlls);
if ((attributes & 1) === 0) {
issues.push({
code: "unsupported_form",
location: hex(descriptorRva),
message:
"Delay-load descriptor uses virtual addresses rather than RVAs; those entries are not decoded",
});
continue;
}
const name = readName(nameRva, "Delay-load DLL name");
delayImports.push({
index,
name,
name_rva: nameRva,
attributes,
import_address_table_rva: importAddressTableRva,
import_name_table_rva: importNameTableRva,
bound_import_rva: boundImportRva,
unload_information_table_rva: unloadInformationTableRva,
module_handle_rva: moduleHandleRva,
time_date_stamp: timeDateStamp,
location: { offset: descriptor, bytes: DELAY_DESCRIPTOR_BYTES },
functions:
importAddressTableRva === 0
? missingTable(
descriptorRva,
"Delay-load descriptor has a zero import address table RVA; its functions are not decoded",
)
: importNameTableRva === 0
? missingTable(
descriptorRva,
"Delay-load descriptor has a zero import name table RVA; its address table holds loader thunks rather than names, so its functions are not decoded",
)
: await readFunctions(importNameTableRva, importAddressTableRva),
});
}
delayDirectoryResult = {
rva: delayDirectory.virtual_address,
size: delayDirectory.size,
locations: spans,
};
}
const limitations =
importDirectoryResult === null
? [
"IMAGE_DIRECTORY_ENTRY_IMPORT is absent. That is not proof the image never imported APIs through another table or overlay.",
...PE_LIMITATIONS,
]
: PE_LIMITATIONS;
return {
...result,
import_directory: importDirectoryResult,
delay_import_directory: delayDirectoryResult,
imports,
delay_imports: delayImports,
imphash: pefileImphash(imports, {
standardIssues,
mappingCompatibility: standardMappingCompatibility,
truncatedDescriptor: pefileTruncatedDescriptor,
bits,
directoryRva: importDirectory?.virtual_address ?? 0,
fileBytes: bytes.length,
}),
coverage: coverage(
issues,
examinedDlls,
imports.reduce((count, dll) => count + dll.functions.length, 0),
delayImports.reduce((count, dll) => count + dll.functions.length, 0),
),
issues,
limitations,
};
};
+259
View File
@@ -0,0 +1,259 @@
import { isSelectedRegularFileError } from "../../filesystem/RegularFile.js";
import { ArtifactReaderFailure } from "../../artifacts/ArtifactReader.js";
import { readStableArtifact } from "../../artifacts/readStableArtifact.js";
import {
artifactFilePrimaryFacts,
type ArtifactFilePrimaryFacts,
assertArtifactReadActive,
bindArtifactFileCleanup,
artifactFileCancellationCause,
} from "../../artifacts/ArtifactFileOperation.js";
import { ArtifactResourceScope } from "../../artifacts/ArtifactResourceScope.js";
import { analysisErrorWithCleanupFailure } from "../../domain/analysisErrorCleanup.js";
import type { PeImportsPort } from "../../application/binaryDiagnostics/PeImportsPort.js";
import type { ExecutionOptions } from "../../application/AnalysisProvider.js";
import { AnalysisError } from "../../domain/analysisErrorBase.js";
import {
AnalysisAccessDeniedError,
AnalysisArtifactChangedError,
AnalysisCancelledError,
AnalysisInputError,
AnalysisResourceConstraintError,
AnalysisUnsupportedTargetError,
type ResourceConstraintRecovery,
} from "../../domain/analysisErrorCore.js";
import { fixedAllowanceOptions } from "../../domain/fixedAllowanceRemediation.js";
import {
MAX_INSPECT_PE_IMPORT_DLLS,
MAX_INSPECT_PE_IMPORT_FILE_BYTES,
MAX_INSPECT_PE_IMPORT_FUNCTIONS,
MAX_INSPECT_PE_IMPORT_RETAINED_TEXT_BYTES,
type InspectPeImportsInput,
} from "../../domain/native/peImports.js";
import { ProviderAdapterError } from "../../domain/providerAdapterError.js";
import { ProviderCleanupError } from "../../domain/providerCleanupError.js";
import { err, ok } from "../../domain/result.js";
import { PE_IMPORTS_PROVIDER } from "../../domain/investigationProviders.js";
import {
BinaryFormatFailure,
BinaryResourceFailure,
} from "../metadata/BinaryContainer.js";
import {
parsePeImports,
PeImportLimitFailure,
type PeImportLimit,
} from "./PeImportParser.js";
const OPERATION = "inspect_pe_imports";
/** Portable import-table reader over one bounded, identity-checked regular file. */
export class PeImportsProvider implements PeImportsPort {
readonly identity = PE_IMPORTS_PROVIDER;
readonly #resources = new ArtifactResourceScope();
async close(): Promise<void> {
try {
await this.#resources.close();
} catch (cause: unknown) {
if (
!(cause instanceof ArtifactReaderFailure) ||
cause.cleanup === undefined
)
throw cause;
const error = new ProviderCleanupError(
PE_IMPORTS_PROVIDER.id,
cause.cleanup.resources,
{ reason: cause.cleanup.reason },
{ cause, operation: OPERATION },
);
bindArtifactFileCleanup(cause, error);
throw error;
}
}
async inspect(input: InspectPeImportsInput, options?: ExecutionOptions) {
try {
return await this.#resources.run(async () => {
const snapshot = await readStableArtifact(
input.path,
input.max_file_bytes,
options?.signal,
undefined,
this.#resources,
);
assertArtifactReadActive(options?.signal);
const report = await parsePeImports(
snapshot.bytes,
{
path: input.path,
sha256: snapshot.sha256,
bytes: snapshot.bytes.length,
},
input,
options?.signal,
);
assertArtifactReadActive(options?.signal);
return ok(report);
});
} catch (cause) {
return err(peImportsFailure(cause, input));
}
}
}
const fileSizeRemediation = (input: InspectPeImportsInput): string =>
input.max_file_bytes >= MAX_INSPECT_PE_IMPORT_FILE_BYTES
? `The image exceeds the ${String(MAX_INSPECT_PE_IMPORT_FILE_BYTES)}-byte input ceiling this operation accepts. This release has no runtime override. 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.`;
const dllRemediation = (input: InspectPeImportsInput): string =>
input.max_dlls >= MAX_INSPECT_PE_IMPORT_DLLS
? `The image exceeds the ${String(MAX_INSPECT_PE_IMPORT_DLLS)}-entry import-descriptor ceiling this operation accepts; select a smaller artifact.`
: `Raise max_dlls above ${String(input.max_dlls)} or select a smaller artifact; the limit is the caller-selected DLL budget.`;
const functionRemediation = (input: InspectPeImportsInput): string =>
input.max_functions >= MAX_INSPECT_PE_IMPORT_FUNCTIONS
? `The image exceeds the ${String(MAX_INSPECT_PE_IMPORT_FUNCTIONS)}-entry imported-function ceiling this operation accepts; select a smaller artifact.`
: `Raise max_functions above ${String(input.max_functions)} or select a smaller artifact; the limit is the caller-selected function budget.`;
/** Recovery for one exhausted allowance, chosen from the parser's structured tag. */
const limitRecovery = (
limit: PeImportLimit | null,
input: InspectPeImportsInput,
): ResourceConstraintRecovery => {
switch (limit) {
case "dlls":
return { retryable: false, remediationAction: dllRemediation(input) };
case "functions":
return {
retryable: false,
remediationAction: functionRemediation(input),
};
case "retained_text":
return fixedAllowanceOptions(
`The ${String(MAX_INSPECT_PE_IMPORT_RETAINED_TEXT_BYTES)}-byte PE import retained-text allowance (this release has no runtime override; max_file_bytes, max_dlls and max_functions do not raise it)`,
"bytes",
);
case null:
return fixedAllowanceOptions(
"The PE structural decoding allowance",
"parser",
);
}
};
const peImportsFailurePrimary = (
facts: ArtifactFilePrimaryFacts,
input: InspectPeImportsInput,
): AnalysisError => {
const { cause, origin } = facts;
if (origin === "owned-or-classified" && cause instanceof AnalysisError)
return cause;
if (
origin === "owned-or-classified" &&
cause instanceof ArtifactReaderFailure &&
cause.reason === "integrity"
)
return new AnalysisArtifactChangedError(
OPERATION,
input.path,
cause.message,
{ cause },
);
if (
origin === "owned-or-classified" &&
cause instanceof ArtifactReaderFailure &&
cause.reason === "cancelled"
)
return new AnalysisCancelledError(OPERATION, {
cause: artifactFileCancellationCause(cause),
});
if (cause instanceof BinaryResourceFailure) {
const limit = cause instanceof PeImportLimitFailure ? cause.limit : null;
return new AnalysisResourceConstraintError(
OPERATION,
"capacity",
`Selected artifact ${input.path}: ${cause.message}`,
{ boundary: cause.boundary, maximum: cause.maximum_bytes, limit },
{ cause, ...limitRecovery(limit, input) },
);
}
if (cause instanceof BinaryFormatFailure)
return cause.kind === "unsupported"
? new AnalysisUnsupportedTargetError(
OPERATION,
input.path,
cause.message,
{ cause },
)
: new AnalysisInputError(OPERATION, { cause }, [
{
path: ["path"],
reason: "invalid_format",
message: `Selected artifact ${input.path}: ${cause.message}`,
},
]);
if (
origin === "owned-or-classified" &&
cause instanceof ArtifactReaderFailure
) {
if (cause.reason === "limit")
return new AnalysisResourceConstraintError(
OPERATION,
"file-size",
cause.message,
{ max_file_bytes: input.max_file_bytes },
{
cause,
retryable: false,
remediationAction: fileSizeRemediation(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 (isSelectedRegularFileError(cause))
return new AnalysisInputError(OPERATION, { cause }, [
{
path: ["path"],
reason: "invalid_value",
message: `Selected image could not be read (${String(cause.code)}): ${input.path}.`,
},
]);
}
return new ProviderAdapterError(PE_IMPORTS_PROVIDER.id, OPERATION, {
cause,
diagnostics: {
path: input.path,
reason: cause instanceof Error ? cause.message : String(cause),
},
});
};
const peImportsFailure = (
cause: unknown,
input: InspectPeImportsInput,
): AnalysisError => {
const primary = peImportsFailurePrimary(
artifactFilePrimaryFacts(cause),
input,
);
if (!(cause instanceof ArtifactReaderFailure) || cause.cleanup === undefined)
return primary;
const cleanup = new ProviderCleanupError(
PE_IMPORTS_PROVIDER.id,
cause.cleanup.resources,
{ reason: cause.cleanup.reason },
{ operation: OPERATION, cause },
);
const error = analysisErrorWithCleanupFailure(primary, cleanup, OPERATION);
bindArtifactFileCleanup(cause, error);
return error;
};
+78
View File
@@ -0,0 +1,78 @@
import type { PeImage } from "../metadata/PeImage.js";
/**
* Prove that a successful native read sees the same bytes as pefile 2024.8.26.
* pefile adjusts section addresses/raw pointers and clips each read at its
* selected section's raw end. Native mappings can legitimately span sections.
* Unsupported layout geometry leaves hash compatibility unknown, without
* discarding the native parser's observations.
*/
export const createPefileReadCompatibility = (
image: PeImage,
fileBytes: number,
): ((rva: number, size: number, offset: number) => boolean) => {
// pefile's section loop stops at MAX_SECTIONS, even when later mappings
// are structurally readable. It may also stop at a null/suspicious header.
if (image.sections.length > 0x800) return () => false;
const sections = [...image.sections].sort(
(left, right) => left.virtual_address - right.virtual_address,
);
const alignment =
image.pe_section_alignment < 0x1000
? image.pe_file_alignment
: image.pe_section_alignment;
let layoutAgrees = true;
let previousEnd = 0;
for (const section of sections) {
const start = section.virtual_address;
const emptyHeader =
section.name === "" &&
start === 0 &&
section.virtual_size === 0 &&
section.raw_size === 0 &&
section.raw_offset === 0 &&
section.characteristics === 0;
const sectionWarnings =
Number(section.raw_offset + section.raw_size > fileBytes) +
Number(Math.floor(section.raw_offset / 512) * 512 > fileBytes) +
Number(section.virtual_size > 0x10000000) +
Number(start > 0x10000000) +
Number(
image.pe_file_alignment !== 0 &&
section.raw_offset % image.pe_file_alignment !== 0,
);
if (
emptyHeader ||
sectionWarnings >= 3 ||
(alignment > 0 && start % alignment !== 0) ||
section.raw_offset % 0x200 !== 0 ||
start < previousEnd
)
layoutAgrees = false;
previousEnd = start + Math.max(section.raw_size, section.virtual_size);
}
return (rva, size, offset) => {
if (!layoutAgrees) return false;
let low = 0;
let high = sections.length;
while (low < high) {
const middle = Math.floor((low + high) / 2);
if ((sections[middle]?.virtual_address ?? Infinity) <= rva)
low = middle + 1;
else high = middle;
}
const section = sections[low - 1];
if (
section === undefined ||
rva >=
section.virtual_address +
Math.max(section.raw_size, section.virtual_size)
)
return offset === rva;
const relative = rva - section.virtual_address;
return (
offset === section.raw_offset + relative &&
size <= section.raw_size - relative
);
};
};
+200
View File
@@ -0,0 +1,200 @@
import { createHash } from "node:crypto";
import type {
PeImportDll,
PeImportFunction,
PeImports,
} from "../../domain/native/peImports.js";
import { PEFILE_ORDINAL_NAMES } from "./pefileOrdinalNames.js";
/** IMAGE_IMPORT_DESCRIPTOR size. */
export const IMPORT_DESCRIPTOR_BYTES = 20;
/** pefile 2024.8.26 parsing constants that change which rows its import hash sees. */
const PEFILE_MAX_NAME_BYTES = 0x200;
export const PEFILE_MAX_IMPORT_SYMBOLS = 0x2000;
const PEFILE_MAX_REPEATED_ADDRESSES = 15;
const PEFILE_MAX_ADDRESS_SPREAD = 128n * 1024n * 1024n;
const PEFILE_DLL_NAME = /^[A-Za-z0-9!#$%&'()\-@^_`{}~+,.;=[\]:\\/]+$/u;
const PEFILE_FUNCTION_NAME = /^[A-Za-z0-9._?@$()<>]+$/u;
const stripImportExtension = (name: string): string => {
const lower = name.toLowerCase();
for (const extension of [".dll", ".sys", ".ocx"]) {
if (lower.endsWith(extension)) return lower.slice(0, -extension.length);
}
return lower;
};
/** pefile `ordlookup.ordLookup(dll, ordinal, make_name=True)`. */
const pefileOrdinalName = (dll: string, ordinal: number): string => {
const key = dll.toLowerCase();
// Own keys only: "constructor" and "__proto__" are valid DLL names.
const names = Object.hasOwn(PEFILE_ORDINAL_NAMES, key)
? PEFILE_ORDINAL_NAMES[key]
: undefined;
return names?.get(ordinal) ?? `ord${String(ordinal)}`;
};
const withheld = (reason: string): PeImports["imphash"] => ({
algorithm: "pefile",
md5: null,
reason,
});
/**
* Why pefile would read a different row sequence from this complete
* descriptor than the one reported, or null when both agree. Mirrors
* `PE.get_import_table` and `PE.parse_imports` bounds and validity rules.
*/
const pefileTableDivergence = (
dll: PeImportDll,
bits: 32 | 64,
directoryRva: number,
fileBytes: number,
): string | null => {
const label = `Standard import descriptor ${String(dll.index)}`;
if (
dll.name === null ||
dll.name.length >= PEFILE_MAX_NAME_BYTES ||
!PEFILE_DLL_NAME.test(dll.name)
)
return `${label} DLL name is unreadable or has a length or characters pefile replaces with *invalid*.`;
if (dll.functions.length === 0)
return `${label} lists no functions; pefile then falls back to the import address table or drops the descriptor.`;
const width = bits / 8;
const firstThunk = dll.first_thunk_rva;
const originalFirstThunk = dll.original_first_thunk_rva;
const tableRva = originalFirstThunk === 0 ? firstThunk : originalFirstThunk;
const next = directoryRva + (dll.index + 1) * IMPORT_DESCRIPTOR_BYTES;
const maxLength =
next > originalFirstThunk || next > firstThunk
? Math.max(next - originalFirstThunk, next - firstThunk)
: fileBytes - dll.location.offset;
const named: bigint[] = [];
for (const [index, row] of dll.functions.entries()) {
if (index * width >= maxLength)
return `${label} lookup table runs past pefile's descriptor-relative bound.`;
const thunk = BigInt(`0x${row.thunk_hex}`);
const divergence = rowDivergence(
row,
thunk,
BigInt(tableRva),
BigInt(tableRva + index * width),
);
if (divergence !== null) return `${label} ${divergence}`;
if (row.ordinal === null) named.push(thunk);
}
const addresses = addressDivergence(named);
return addresses === null ? null : `${label} ${addresses}`;
};
/** One lookup entry pefile would drop, truncate at or discard its table for. */
const rowDivergence = (
row: PeImportFunction,
thunk: bigint,
tableRva: bigint,
entryRva: bigint,
): string | null => {
if (thunk >= tableRva && thunk <= entryRva)
return "has a thunk that points into its own lookup table; pefile truncates there.";
if (row.ordinal !== null) {
// Pinned pefile 2024.8.26 masks only 31 bits for both widths. In PE32+
// it ignores reserved bits 31–62; retain that compatibility behavior and
// the source thunk, without asserting that a loader accepts those bits.
if ((thunk & 0x7fffffffn) > 0xffffn)
return "has an ordinal thunk whose low 31-bit payload exceeds 16 bits; pefile discards the table.";
return row.ordinal === 0 ? "imports ordinal 0, which pefile omits." : null;
}
return row.name === null ||
row.name.length >= PEFILE_MAX_NAME_BYTES ||
!PEFILE_FUNCTION_NAME.test(row.name)
? "has a function name with a length or characters pefile treats as invalid and omits."
: null;
};
/** pefile's repeated-address and address-spread table rejection heuristics. */
const addressDivergence = (named: readonly bigint[]): string | null => {
const sets = { low: new Set<bigint>(), high: new Set<bigint>() };
let repeated = 0;
for (const thunk of named) {
const set = thunk >= 1n << 32n ? sets.high : sets.low;
if (set.has(thunk)) repeated += 1;
set.add(thunk);
}
if (repeated >= PEFILE_MAX_REPEATED_ADDRESSES)
return `repeats hint/name addresses ${String(repeated)} times; pefile discards such tables.`;
for (const set of [sets.low, sets.high]) {
if (set.size === 0) continue;
const values = [...set];
const low = values.reduce((left, right) => (right < left ? right : left));
const high = values.reduce((left, right) => (right > left ? right : left));
if (high - low > PEFILE_MAX_ADDRESS_SPREAD)
return "hint/name addresses spread beyond 128 MiB; pefile discards such tables.";
}
return null;
};
/** Parser facts pefile's row selection depends on beyond the reported rows. */
export interface PefileImphashFacts {
readonly mappingCompatibility: boolean;
/** Issues recorded while reading the standard import table. */
readonly standardIssues: number;
/** First descriptor pefile's import symbol cap truncates, if any. */
readonly truncatedDescriptor: number | null;
readonly bits: 32 | 64;
readonly directoryRva: number;
readonly fileBytes: number;
}
/**
* pefile `PE.get_imphash()` over a complete standard import table, or an
* explicit reason when the reported rows are not the sequence pefile hashes.
*/
export const pefileImphash = (
imports: readonly PeImportDll[],
{
standardIssues,
mappingCompatibility,
truncatedDescriptor,
bits,
directoryRva,
fileBytes,
}: PefileImphashFacts,
): PeImports["imphash"] => {
if (standardIssues > 0)
return withheld(
`The standard import table is incomplete (${String(standardIssues)} issue(s)); a hash of the retained rows would not be this image's import hash.`,
);
if (imports.length === 0)
return withheld("IMAGE_DIRECTORY_ENTRY_IMPORT is absent or empty.");
if (truncatedDescriptor !== null)
return withheld(
`Standard import descriptor ${String(truncatedDescriptor)} lies past pefile's ${String(PEFILE_MAX_IMPORT_SYMBOLS)}-thunk import symbol cap, so pefile hashes only a prefix of the table.`,
);
if (!mappingCompatibility)
return withheld(
"Byte mapping compatibility with pefile is not established for the standard import table or its names.",
);
const parts: string[] = [];
for (const dll of imports) {
const divergence = pefileTableDivergence(
dll,
bits,
directoryRva,
fileBytes,
);
if (divergence !== null) return withheld(divergence);
const dllName = dll.name as string;
const library = stripImportExtension(dllName);
for (const imported of dll.functions) {
const functionName =
imported.name ?? pefileOrdinalName(dllName, imported.ordinal as number);
parts.push(`${library}.${functionName.toLowerCase()}`);
}
}
return {
algorithm: "pefile",
md5: createHash("md5").update(parts.join(","), "utf8").digest("hex"),
reason: null,
};
};
+713
View File
@@ -0,0 +1,713 @@
/**
* Derived from pefile (c) 2004-2024 Ero Carrera, MIT.
* Retain licenses/pefile-2024.8.26-LICENSE.txt when redistributing these tables.
* Ordinal-to-name tables pefile 2024.8.26 consults in `ordlookup.ords` for
* its import hash. Ordinals missing here hash as `ord<decimal>`, matching
* `ordlookup.formatOrdString`. Regenerate from pefile when it changes.
*/
export const PEFILE_ORDINAL_NAMES: Readonly<
Record<string, ReadonlyMap<number, string>>
> = {
"oleaut32.dll": new Map<number, string>([
[2, "SysAllocString"],
[3, "SysReAllocString"],
[4, "SysAllocStringLen"],
[5, "SysReAllocStringLen"],
[6, "SysFreeString"],
[7, "SysStringLen"],
[8, "VariantInit"],
[9, "VariantClear"],
[10, "VariantCopy"],
[11, "VariantCopyInd"],
[12, "VariantChangeType"],
[13, "VariantTimeToDosDateTime"],
[14, "DosDateTimeToVariantTime"],
[15, "SafeArrayCreate"],
[16, "SafeArrayDestroy"],
[17, "SafeArrayGetDim"],
[18, "SafeArrayGetElemsize"],
[19, "SafeArrayGetUBound"],
[20, "SafeArrayGetLBound"],
[21, "SafeArrayLock"],
[22, "SafeArrayUnlock"],
[23, "SafeArrayAccessData"],
[24, "SafeArrayUnaccessData"],
[25, "SafeArrayGetElement"],
[26, "SafeArrayPutElement"],
[27, "SafeArrayCopy"],
[28, "DispGetParam"],
[29, "DispGetIDsOfNames"],
[30, "DispInvoke"],
[31, "CreateDispTypeInfo"],
[32, "CreateStdDispatch"],
[33, "RegisterActiveObject"],
[34, "RevokeActiveObject"],
[35, "GetActiveObject"],
[36, "SafeArrayAllocDescriptor"],
[37, "SafeArrayAllocData"],
[38, "SafeArrayDestroyDescriptor"],
[39, "SafeArrayDestroyData"],
[40, "SafeArrayRedim"],
[41, "SafeArrayAllocDescriptorEx"],
[42, "SafeArrayCreateEx"],
[43, "SafeArrayCreateVectorEx"],
[44, "SafeArraySetRecordInfo"],
[45, "SafeArrayGetRecordInfo"],
[46, "VarParseNumFromStr"],
[47, "VarNumFromParseNum"],
[48, "VarI2FromUI1"],
[49, "VarI2FromI4"],
[50, "VarI2FromR4"],
[51, "VarI2FromR8"],
[52, "VarI2FromCy"],
[53, "VarI2FromDate"],
[54, "VarI2FromStr"],
[55, "VarI2FromDisp"],
[56, "VarI2FromBool"],
[57, "SafeArraySetIID"],
[58, "VarI4FromUI1"],
[59, "VarI4FromI2"],
[60, "VarI4FromR4"],
[61, "VarI4FromR8"],
[62, "VarI4FromCy"],
[63, "VarI4FromDate"],
[64, "VarI4FromStr"],
[65, "VarI4FromDisp"],
[66, "VarI4FromBool"],
[67, "SafeArrayGetIID"],
[68, "VarR4FromUI1"],
[69, "VarR4FromI2"],
[70, "VarR4FromI4"],
[71, "VarR4FromR8"],
[72, "VarR4FromCy"],
[73, "VarR4FromDate"],
[74, "VarR4FromStr"],
[75, "VarR4FromDisp"],
[76, "VarR4FromBool"],
[77, "SafeArrayGetVartype"],
[78, "VarR8FromUI1"],
[79, "VarR8FromI2"],
[80, "VarR8FromI4"],
[81, "VarR8FromR4"],
[82, "VarR8FromCy"],
[83, "VarR8FromDate"],
[84, "VarR8FromStr"],
[85, "VarR8FromDisp"],
[86, "VarR8FromBool"],
[87, "VarFormat"],
[88, "VarDateFromUI1"],
[89, "VarDateFromI2"],
[90, "VarDateFromI4"],
[91, "VarDateFromR4"],
[92, "VarDateFromR8"],
[93, "VarDateFromCy"],
[94, "VarDateFromStr"],
[95, "VarDateFromDisp"],
[96, "VarDateFromBool"],
[97, "VarFormatDateTime"],
[98, "VarCyFromUI1"],
[99, "VarCyFromI2"],
[100, "VarCyFromI4"],
[101, "VarCyFromR4"],
[102, "VarCyFromR8"],
[103, "VarCyFromDate"],
[104, "VarCyFromStr"],
[105, "VarCyFromDisp"],
[106, "VarCyFromBool"],
[107, "VarFormatNumber"],
[108, "VarBstrFromUI1"],
[109, "VarBstrFromI2"],
[110, "VarBstrFromI4"],
[111, "VarBstrFromR4"],
[112, "VarBstrFromR8"],
[113, "VarBstrFromCy"],
[114, "VarBstrFromDate"],
[115, "VarBstrFromDisp"],
[116, "VarBstrFromBool"],
[117, "VarFormatPercent"],
[118, "VarBoolFromUI1"],
[119, "VarBoolFromI2"],
[120, "VarBoolFromI4"],
[121, "VarBoolFromR4"],
[122, "VarBoolFromR8"],
[123, "VarBoolFromDate"],
[124, "VarBoolFromCy"],
[125, "VarBoolFromStr"],
[126, "VarBoolFromDisp"],
[127, "VarFormatCurrency"],
[128, "VarWeekdayName"],
[129, "VarMonthName"],
[130, "VarUI1FromI2"],
[131, "VarUI1FromI4"],
[132, "VarUI1FromR4"],
[133, "VarUI1FromR8"],
[134, "VarUI1FromCy"],
[135, "VarUI1FromDate"],
[136, "VarUI1FromStr"],
[137, "VarUI1FromDisp"],
[138, "VarUI1FromBool"],
[139, "VarFormatFromTokens"],
[140, "VarTokenizeFormatString"],
[141, "VarAdd"],
[142, "VarAnd"],
[143, "VarDiv"],
[144, "BSTR_UserFree64"],
[145, "BSTR_UserMarshal64"],
[146, "DispCallFunc"],
[147, "VariantChangeTypeEx"],
[148, "SafeArrayPtrOfIndex"],
[149, "SysStringByteLen"],
[150, "SysAllocStringByteLen"],
[151, "BSTR_UserSize64"],
[152, "VarEqv"],
[153, "VarIdiv"],
[154, "VarImp"],
[155, "VarMod"],
[156, "VarMul"],
[157, "VarOr"],
[158, "VarPow"],
[159, "VarSub"],
[160, "CreateTypeLib"],
[161, "LoadTypeLib"],
[162, "LoadRegTypeLib"],
[163, "RegisterTypeLib"],
[164, "QueryPathOfRegTypeLib"],
[165, "LHashValOfNameSys"],
[166, "LHashValOfNameSysA"],
[167, "VarXor"],
[168, "VarAbs"],
[169, "VarFix"],
[170, "OaBuildVersion"],
[171, "ClearCustData"],
[172, "VarInt"],
[173, "VarNeg"],
[174, "VarNot"],
[175, "VarRound"],
[176, "VarCmp"],
[177, "VarDecAdd"],
[178, "VarDecDiv"],
[179, "VarDecMul"],
[180, "CreateTypeLib2"],
[181, "VarDecSub"],
[182, "VarDecAbs"],
[183, "LoadTypeLibEx"],
[184, "SystemTimeToVariantTime"],
[185, "VariantTimeToSystemTime"],
[186, "UnRegisterTypeLib"],
[187, "VarDecFix"],
[188, "VarDecInt"],
[189, "VarDecNeg"],
[190, "VarDecFromUI1"],
[191, "VarDecFromI2"],
[192, "VarDecFromI4"],
[193, "VarDecFromR4"],
[194, "VarDecFromR8"],
[195, "VarDecFromDate"],
[196, "VarDecFromCy"],
[197, "VarDecFromStr"],
[198, "VarDecFromDisp"],
[199, "VarDecFromBool"],
[200, "GetErrorInfo"],
[201, "SetErrorInfo"],
[202, "CreateErrorInfo"],
[203, "VarDecRound"],
[204, "VarDecCmp"],
[205, "VarI2FromI1"],
[206, "VarI2FromUI2"],
[207, "VarI2FromUI4"],
[208, "VarI2FromDec"],
[209, "VarI4FromI1"],
[210, "VarI4FromUI2"],
[211, "VarI4FromUI4"],
[212, "VarI4FromDec"],
[213, "VarR4FromI1"],
[214, "VarR4FromUI2"],
[215, "VarR4FromUI4"],
[216, "VarR4FromDec"],
[217, "VarR8FromI1"],
[218, "VarR8FromUI2"],
[219, "VarR8FromUI4"],
[220, "VarR8FromDec"],
[221, "VarDateFromI1"],
[222, "VarDateFromUI2"],
[223, "VarDateFromUI4"],
[224, "VarDateFromDec"],
[225, "VarCyFromI1"],
[226, "VarCyFromUI2"],
[227, "VarCyFromUI4"],
[228, "VarCyFromDec"],
[229, "VarBstrFromI1"],
[230, "VarBstrFromUI2"],
[231, "VarBstrFromUI4"],
[232, "VarBstrFromDec"],
[233, "VarBoolFromI1"],
[234, "VarBoolFromUI2"],
[235, "VarBoolFromUI4"],
[236, "VarBoolFromDec"],
[237, "VarUI1FromI1"],
[238, "VarUI1FromUI2"],
[239, "VarUI1FromUI4"],
[240, "VarUI1FromDec"],
[241, "VarDecFromI1"],
[242, "VarDecFromUI2"],
[243, "VarDecFromUI4"],
[244, "VarI1FromUI1"],
[245, "VarI1FromI2"],
[246, "VarI1FromI4"],
[247, "VarI1FromR4"],
[248, "VarI1FromR8"],
[249, "VarI1FromDate"],
[250, "VarI1FromCy"],
[251, "VarI1FromStr"],
[252, "VarI1FromDisp"],
[253, "VarI1FromBool"],
[254, "VarI1FromUI2"],
[255, "VarI1FromUI4"],
[256, "VarI1FromDec"],
[257, "VarUI2FromUI1"],
[258, "VarUI2FromI2"],
[259, "VarUI2FromI4"],
[260, "VarUI2FromR4"],
[261, "VarUI2FromR8"],
[262, "VarUI2FromDate"],
[263, "VarUI2FromCy"],
[264, "VarUI2FromStr"],
[265, "VarUI2FromDisp"],
[266, "VarUI2FromBool"],
[267, "VarUI2FromI1"],
[268, "VarUI2FromUI4"],
[269, "VarUI2FromDec"],
[270, "VarUI4FromUI1"],
[271, "VarUI4FromI2"],
[272, "VarUI4FromI4"],
[273, "VarUI4FromR4"],
[274, "VarUI4FromR8"],
[275, "VarUI4FromDate"],
[276, "VarUI4FromCy"],
[277, "VarUI4FromStr"],
[278, "VarUI4FromDisp"],
[279, "VarUI4FromBool"],
[280, "VarUI4FromI1"],
[281, "VarUI4FromUI2"],
[282, "VarUI4FromDec"],
[283, "BSTR_UserSize"],
[284, "BSTR_UserMarshal"],
[285, "BSTR_UserUnmarshal"],
[286, "BSTR_UserFree"],
[287, "VARIANT_UserSize"],
[288, "VARIANT_UserMarshal"],
[289, "VARIANT_UserUnmarshal"],
[290, "VARIANT_UserFree"],
[291, "LPSAFEARRAY_UserSize"],
[292, "LPSAFEARRAY_UserMarshal"],
[293, "LPSAFEARRAY_UserUnmarshal"],
[294, "LPSAFEARRAY_UserFree"],
[295, "LPSAFEARRAY_Size"],
[296, "LPSAFEARRAY_Marshal"],
[297, "LPSAFEARRAY_Unmarshal"],
[298, "VarDecCmpR8"],
[299, "VarCyAdd"],
[300, "BSTR_UserUnmarshal64"],
[301, "DllCanUnloadNow"],
[302, "DllGetClassObject"],
[303, "VarCyMul"],
[304, "VarCyMulI4"],
[305, "VarCySub"],
[306, "VarCyAbs"],
[307, "VarCyFix"],
[308, "VarCyInt"],
[309, "VarCyNeg"],
[310, "VarCyRound"],
[311, "VarCyCmp"],
[312, "VarCyCmpR8"],
[313, "VarBstrCat"],
[314, "VarBstrCmp"],
[315, "VarR8Pow"],
[316, "VarR4CmpR8"],
[317, "VarR8Round"],
[318, "VarCat"],
[319, "VarDateFromUdateEx"],
[320, "DllRegisterServer"],
[321, "DllUnregisterServer"],
[322, "GetRecordInfoFromGuids"],
[323, "GetRecordInfoFromTypeInfo"],
[324, "LPSAFEARRAY_UserFree64"],
[325, "SetVarConversionLocaleSetting"],
[326, "GetVarConversionLocaleSetting"],
[327, "SetOaNoCache"],
[328, "LPSAFEARRAY_UserMarshal64"],
[329, "VarCyMulI8"],
[330, "VarDateFromUdate"],
[331, "VarUdateFromDate"],
[332, "GetAltMonthNames"],
[333, "VarI8FromUI1"],
[334, "VarI8FromI2"],
[335, "VarI8FromR4"],
[336, "VarI8FromR8"],
[337, "VarI8FromCy"],
[338, "VarI8FromDate"],
[339, "VarI8FromStr"],
[340, "VarI8FromDisp"],
[341, "VarI8FromBool"],
[342, "VarI8FromI1"],
[343, "VarI8FromUI2"],
[344, "VarI8FromUI4"],
[345, "VarI8FromDec"],
[346, "VarI2FromI8"],
[347, "VarI2FromUI8"],
[348, "VarI4FromI8"],
[349, "VarI4FromUI8"],
[350, "LPSAFEARRAY_UserSize64"],
[351, "LPSAFEARRAY_UserUnmarshal64"],
[352, "OACreateTypeLib2"],
[353, "SafeArrayAddRef"],
[354, "SafeArrayReleaseData"],
[355, "SafeArrayReleaseDescriptor"],
[356, "SysAddRefString"],
[357, "SysReleaseString"],
[358, "VARIANT_UserFree64"],
[359, "VARIANT_UserMarshal64"],
[360, "VarR4FromI8"],
[361, "VarR4FromUI8"],
[362, "VarR8FromI8"],
[363, "VarR8FromUI8"],
[364, "VarDateFromI8"],
[365, "VarDateFromUI8"],
[366, "VarCyFromI8"],
[367, "VarCyFromUI8"],
[368, "VarBstrFromI8"],
[369, "VarBstrFromUI8"],
[370, "VarBoolFromI8"],
[371, "VarBoolFromUI8"],
[372, "VarUI1FromI8"],
[373, "VarUI1FromUI8"],
[374, "VarDecFromI8"],
[375, "VarDecFromUI8"],
[376, "VarI1FromI8"],
[377, "VarI1FromUI8"],
[378, "VarUI2FromI8"],
[379, "VarUI2FromUI8"],
[380, "VARIANT_UserSize64"],
[381, "VARIANT_UserUnmarshal64"],
[401, "OleLoadPictureEx"],
[402, "OleLoadPictureFileEx"],
[411, "SafeArrayCreateVector"],
[412, "SafeArrayCopyData"],
[413, "VectorFromBstr"],
[414, "BstrFromVector"],
[415, "OleIconToCursor"],
[416, "OleCreatePropertyFrameIndirect"],
[417, "OleCreatePropertyFrame"],
[418, "OleLoadPicture"],
[419, "OleCreatePictureIndirect"],
[420, "OleCreateFontIndirect"],
[421, "OleTranslateColor"],
[422, "OleLoadPictureFile"],
[423, "OleSavePictureFile"],
[424, "OleLoadPicturePath"],
[425, "VarUI4FromI8"],
[426, "VarUI4FromUI8"],
[427, "VarI8FromUI8"],
[428, "VarUI8FromI8"],
[429, "VarUI8FromUI1"],
[430, "VarUI8FromI2"],
[431, "VarUI8FromR4"],
[432, "VarUI8FromR8"],
[433, "VarUI8FromCy"],
[434, "VarUI8FromDate"],
[435, "VarUI8FromStr"],
[436, "VarUI8FromDisp"],
[437, "VarUI8FromBool"],
[438, "VarUI8FromI1"],
[439, "VarUI8FromUI2"],
[440, "VarUI8FromUI4"],
[441, "VarUI8FromDec"],
[442, "RegisterTypeLibForUser"],
[443, "UnRegisterTypeLibForUser"],
[444, "OaEnablePerUserTLibRegistration"],
[445, "HWND_UserFree"],
[446, "HWND_UserMarshal"],
[447, "HWND_UserSize"],
[448, "HWND_UserUnmarshal"],
[449, "HWND_UserFree64"],
[450, "HWND_UserMarshal64"],
[451, "HWND_UserSize64"],
[452, "HWND_UserUnmarshal64"],
[500, "OACleanup"],
]),
"ws2_32.dll": new Map<number, string>([
[1, "accept"],
[2, "bind"],
[3, "closesocket"],
[4, "connect"],
[5, "getpeername"],
[6, "getsockname"],
[7, "getsockopt"],
[8, "htonl"],
[9, "htons"],
[10, "ioctlsocket"],
[11, "inet_addr"],
[12, "inet_ntoa"],
[13, "listen"],
[14, "ntohl"],
[15, "ntohs"],
[16, "recv"],
[17, "recvfrom"],
[18, "select"],
[19, "send"],
[20, "sendto"],
[21, "setsockopt"],
[22, "shutdown"],
[23, "socket"],
[24, "WSApSetPostRoutine"],
[25, "FreeAddrInfoEx"],
[26, "FreeAddrInfoExW"],
[27, "FreeAddrInfoW"],
[28, "GetAddrInfoExA"],
[29, "GetAddrInfoExCancel"],
[30, "GetAddrInfoExOverlappedResult"],
[31, "GetAddrInfoExW"],
[32, "GetAddrInfoW"],
[33, "GetHostNameW"],
[34, "GetNameInfoW"],
[35, "InetNtopW"],
[36, "InetPtonW"],
[37, "ProcessSocketNotifications"],
[38, "SetAddrInfoExA"],
[39, "SetAddrInfoExW"],
[40, "WPUCompleteOverlappedRequest"],
[41, "WPUGetProviderPathEx"],
[42, "WSAAccept"],
[43, "WSAAddressToStringA"],
[44, "WSAAddressToStringW"],
[45, "WSAAdvertiseProvider"],
[46, "WSACloseEvent"],
[47, "WSAConnect"],
[48, "WSAConnectByList"],
[49, "WSAConnectByNameA"],
[50, "WSAConnectByNameW"],
[51, "gethostbyaddr"],
[52, "gethostbyname"],
[53, "getprotobyname"],
[54, "getprotobynumber"],
[55, "getservbyname"],
[56, "getservbyport"],
[57, "gethostname"],
[58, "WSACreateEvent"],
[59, "WSADuplicateSocketA"],
[60, "WSADuplicateSocketW"],
[61, "WSAEnumNameSpaceProvidersA"],
[62, "WSAEnumNameSpaceProvidersExA"],
[63, "WSAEnumNameSpaceProvidersExW"],
[64, "WSAEnumNameSpaceProvidersW"],
[65, "WSAEnumNetworkEvents"],
[66, "WSAEnumProtocolsA"],
[67, "WSAEnumProtocolsW"],
[68, "WSAEventSelect"],
[69, "WSAGetOverlappedResult"],
[70, "WSAGetQOSByName"],
[71, "WSAGetServiceClassInfoA"],
[72, "WSAGetServiceClassInfoW"],
[73, "WSAGetServiceClassNameByClassIdA"],
[74, "WSAGetServiceClassNameByClassIdW"],
[75, "WSAHtonl"],
[76, "WSAHtons"],
[77, "WSAInstallServiceClassA"],
[78, "WSAInstallServiceClassW"],
[79, "WSAIoctl"],
[80, "WSAJoinLeaf"],
[81, "WSALookupServiceBeginA"],
[82, "WSALookupServiceBeginW"],
[83, "WSALookupServiceEnd"],
[84, "WSALookupServiceNextA"],
[85, "WSALookupServiceNextW"],
[86, "WSANSPIoctl"],
[87, "WSANtohl"],
[88, "WSANtohs"],
[89, "WSAPoll"],
[90, "WSAProviderCompleteAsyncCall"],
[91, "WSAProviderConfigChange"],
[92, "WSARecv"],
[93, "WSARecvDisconnect"],
[94, "WSARecvFrom"],
[95, "WSARemoveServiceClass"],
[96, "WSAResetEvent"],
[97, "WSASend"],
[98, "WSASendDisconnect"],
[99, "WSASendMsg"],
[100, "WSASendTo"],
[101, "WSAAsyncSelect"],
[102, "WSAAsyncGetHostByAddr"],
[103, "WSAAsyncGetHostByName"],
[104, "WSAAsyncGetProtoByNumber"],
[105, "WSAAsyncGetProtoByName"],
[106, "WSAAsyncGetServByPort"],
[107, "WSAAsyncGetServByName"],
[108, "WSACancelAsyncRequest"],
[109, "WSASetBlockingHook"],
[110, "WSAUnhookBlockingHook"],
[111, "WSAGetLastError"],
[112, "WSASetLastError"],
[113, "WSACancelBlockingCall"],
[114, "WSAIsBlocking"],
[115, "WSAStartup"],
[116, "WSACleanup"],
[117, "WSASetEvent"],
[118, "WSASetServiceA"],
[119, "WSASetServiceW"],
[120, "WSASocketA"],
[121, "WSASocketW"],
[122, "WSAStringToAddressA"],
[123, "WSAStringToAddressW"],
[124, "WSAUnadvertiseProvider"],
[125, "WSAWaitForMultipleEvents"],
[126, "WSCDeinstallProvider"],
[127, "WSCDeinstallProvider32"],
[128, "WSCDeinstallProviderEx"],
[129, "WSCEnableNSProvider"],
[130, "WSCEnableNSProvider32"],
[131, "WSCEnumNameSpaceProviders32"],
[132, "WSCEnumNameSpaceProvidersEx32"],
[133, "WSCEnumProtocols"],
[134, "WSCEnumProtocols32"],
[135, "WSCEnumProtocolsEx"],
[136, "WSCGetApplicationCategory"],
[137, "WSCGetApplicationCategoryEx"],
[138, "WSCGetProviderInfo"],
[139, "WSCGetProviderInfo32"],
[140, "WSCGetProviderPath"],
[141, "WSCGetProviderPath32"],
[142, "WSCInstallNameSpace"],
[143, "WSCInstallNameSpace32"],
[144, "WSCInstallNameSpaceEx"],
[145, "WSCInstallNameSpaceEx2"],
[146, "WSCInstallNameSpaceEx32"],
[147, "WSCInstallProvider"],
[148, "WSCInstallProvider64_32"],
[149, "WSCInstallProviderAndChains64_32"],
[150, "WSCInstallProviderEx"],
[151, "__WSAFDIsSet"],
[152, "WSCSetApplicationCategory"],
[153, "WSCSetApplicationCategoryEx"],
[154, "WSCSetProviderInfo"],
[155, "WSCSetProviderInfo32"],
[156, "WSCUnInstallNameSpace"],
[157, "WSCUnInstallNameSpace32"],
[158, "WSCUnInstallNameSpaceEx2"],
[159, "WSCUpdateProvider"],
[160, "WSCUpdateProvider32"],
[161, "WSCUpdateProviderEx"],
[162, "WSCWriteNameSpaceOrder"],
[163, "WSCWriteNameSpaceOrder32"],
[164, "WSCWriteProviderOrder"],
[165, "WSCWriteProviderOrder32"],
[166, "WSCWriteProviderOrderEx"],
[167, "WahCloseApcHelper"],
[168, "WahCloseHandleHelper"],
[169, "WahCloseNotificationHandleHelper"],
[170, "WahCloseSocketHandle"],
[171, "WahCloseThread"],
[172, "WahCompleteRequest"],
[173, "WahCreateHandleContextTable"],
[174, "WahCreateNotificationHandle"],
[175, "WahCreateSocketHandle"],
[176, "WahDestroyHandleContextTable"],
[177, "WahDisableNonIFSHandleSupport"],
[178, "WahEnableNonIFSHandleSupport"],
[179, "WahEnumerateHandleContexts"],
[180, "WahInsertHandleContext"],
[181, "WahNotifyAllProcesses"],
[182, "WahOpenApcHelper"],
[183, "WahOpenCurrentThread"],
[184, "WahOpenHandleHelper"],
[185, "WahOpenNotificationHandleHelper"],
[186, "WahQueueUserApc"],
[187, "WahReferenceContextByHandle"],
[188, "WahRemoveHandleContext"],
[189, "WahWaitForNotification"],
[190, "WahWriteLSPEvent"],
[191, "freeaddrinfo"],
[192, "getaddrinfo"],
[193, "getnameinfo"],
[194, "inet_ntop"],
[195, "inet_pton"],
[500, "WEP"],
]),
"wsock32.dll": new Map<number, string>([
[1, "accept"],
[2, "bind"],
[3, "closesocket"],
[4, "connect"],
[5, "getpeername"],
[6, "getsockname"],
[7, "getsockopt"],
[8, "htonl"],
[9, "htons"],
[10, "inet_addr"],
[11, "inet_ntoa"],
[12, "ioctlsocket"],
[13, "listen"],
[14, "ntohl"],
[15, "ntohs"],
[16, "recv"],
[17, "recvfrom"],
[18, "select"],
[19, "send"],
[20, "sendto"],
[21, "setsockopt"],
[22, "shutdown"],
[23, "socket"],
[24, "MigrateWinsockConfiguration"],
[51, "gethostbyaddr"],
[52, "gethostbyname"],
[53, "getprotobyname"],
[54, "getprotobynumber"],
[55, "getservbyname"],
[56, "getservbyport"],
[57, "gethostname"],
[101, "WSAAsyncSelect"],
[102, "WSAAsyncGetHostByAddr"],
[103, "WSAAsyncGetHostByName"],
[104, "WSAAsyncGetProtoByNumber"],
[105, "WSAAsyncGetProtoByName"],
[106, "WSAAsyncGetServByPort"],
[107, "WSAAsyncGetServByName"],
[108, "WSACancelAsyncRequest"],
[109, "WSASetBlockingHook"],
[110, "WSAUnhookBlockingHook"],
[111, "WSAGetLastError"],
[112, "WSASetLastError"],
[113, "WSACancelBlockingCall"],
[114, "WSAIsBlocking"],
[115, "WSAStartup"],
[116, "WSACleanup"],
[151, "__WSAFDIsSet"],
[500, "WEP"],
[1000, "WSApSetPostRoutine"],
[1100, "inet_network"],
[1101, "getnetbyname"],
[1102, "rcmd"],
[1103, "rexec"],
[1104, "rresvport"],
[1105, "sethostname"],
[1106, "dn_expand"],
[1107, "WSARecvEx"],
[1108, "s_perror"],
[1109, "GetAddressByNameA"],
[1110, "GetAddressByNameW"],
[1111, "EnumProtocolsA"],
[1112, "EnumProtocolsW"],
[1113, "GetTypeByNameA"],
[1114, "GetTypeByNameW"],
[1115, "GetNameByTypeA"],
[1116, "GetNameByTypeW"],
[1117, "SetServiceA"],
[1118, "SetServiceW"],
[1119, "GetServiceA"],
[1120, "GetServiceW"],
[1130, "NPLoadNameSpaces"],
[1140, "TransmitFile"],
[1141, "AcceptEx"],
[1142, "GetAcceptExSockaddrs"],
]),
};
+15
View File
@@ -25,15 +25,18 @@ import { captureProviderEnvironment } from "./providerEnvironment.js";
import {
createElfNotesService,
createPeCertificatesService,
createPeImportsService,
createNativeExportsService,
createPeResourcesService,
} from "../composition/binaryDiagnostics.js";
import type { ElfNotesService } from "../application/binaryDiagnostics/ElfNotesService.js";
import type { PeCertificatesService } from "../application/binaryDiagnostics/PeCertificatesService.js";
import type { PeImportsService } from "../application/binaryDiagnostics/PeImportsService.js";
import type { PeResourcesService } from "../application/binaryDiagnostics/PeResourcesService.js";
import type { NativeExportsService } from "../application/binaryDiagnostics/NativeExportsService.js";
import { registerElfNotesTool } from "./registerElfNotesTool.js";
import { registerPeCertificatesTool } from "./registerPeCertificatesTool.js";
import { registerPeImportsTool } from "./registerPeImportsTool.js";
import { registerPeResourcesTool } from "./registerPeResourcesTool.js";
import { registerNativeExportsTool } from "./registerNativeExportsTool.js";
import type { EvmInterfaceService } from "../application/evm/EvmInterfaceService.js";
@@ -468,6 +471,7 @@ export const createServer = (
const nativeExports = createNativeExportsService();
const elfNotes = createElfNotesService();
const peCertificates = createPeCertificatesService();
const peImports = createPeImportsService();
const electronFuses = options.electronFuses ?? createElectronFuseService();
const pythonPyc = options.pythonPyc ?? createPythonPycService();
const webNetworkCapture =
@@ -579,6 +583,7 @@ export const createServer = (
nativeExports.close(),
elfNotes.close(),
peCertificates.close(),
peImports.close(),
electronFuses.close(),
pythonPyc.close(),
webNetworkCapture.close(),
@@ -647,6 +652,7 @@ export const createServer = (
nativeExports,
elfNotes,
peCertificates,
peImports,
electronFuses,
pythonPyc,
webNetworkCapture,
@@ -884,6 +890,7 @@ const registerConfiguredAnalysisTools = (
nativeExports,
elfNotes,
peCertificates,
peImports,
pythonPyc,
webNetworkCapture,
artifactResources,
@@ -957,6 +964,13 @@ const registerConfiguredAnalysisTools = (
recordEvidence,
withAdmittedAnalysis,
);
registerPeImportsTool(
server,
peImports,
toolLogger,
recordEvidence,
withAdmittedAnalysis,
);
registerGoTools(
server,
goBinary,
@@ -1113,6 +1127,7 @@ interface ServerToolContext extends ReturnType<typeof createSessionRecorders> {
readonly nativeExports: NativeExportsService;
readonly elfNotes: ElfNotesService;
readonly peCertificates: PeCertificatesService;
readonly peImports: PeImportsService;
readonly electronFuses: ElectronFuseService;
readonly pythonPyc: PythonPycService;
readonly webNetworkCapture: WebNetworkCaptureService;
+31
View File
@@ -0,0 +1,31 @@
import type { EvidenceMcpServer } from "./EvidenceMcpServer.js";
import type { PeImportsService } from "../application/binaryDiagnostics/PeImportsService.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 PE import-table workflow and retain its complete Evidence. */
export const registerPeImportsTool = (
server: EvidenceMcpServer,
service: PeImportsService,
logger: Logger,
recordEvidence?: EvidenceWriter["recordEvidence"],
withAdmittedAnalysis?: WithAdmittedAnalysis,
): void => {
const contract = toolContract("inspect_pe_imports");
const handler = createEvidenceToolHandler(
server,
(input, options) => service.inspect(input, options),
logger,
recordEvidence,
withAdmittedAnalysis,
);
server.registerTool(
contract.name,
toolRegistrationOptions(contract),
handler(contract),
);
};
+191
View File
@@ -0,0 +1,191 @@
import { writeFile } from "node:fs/promises";
import { join } from "node:path";
import { expect } from "vitest";
import { toolContract } from "../../src/contracts/toolContracts.js";
import {
peImportImage,
peImportFixtureOffset,
peImportLeadingSectionImage,
peImportManySectionsImage,
peImportDirectoryAtRva,
peImportDirectoryMappingStops,
peWithoutImportTableFixture,
} from "../fixtures/binaryDiagnostics/peImports.js";
import { createTestTempDirectory } from "../fixtures/temporaryDirectory.js";
import { cliTest } from "../support/cli/cliFixture.js";
cliTest("reports PE import names through the public CLI", async ({ cli }) => {
const root = await createTestTempDirectory("rea-pe-imports-cli-");
const path = join(root, "app.exe");
await writeFile(path, peImportImage());
const result = await cli.run({
arguments: ["inspect-pe-imports", path, "--json"],
});
expect(result.exitCode, result.stderr).toBe(0);
const evidence = toolContract("inspect_pe_imports").outputSchema.parse(
result.json,
);
expect(evidence.normalized_result.imports[0]?.name).toBe("KERNEL32.dll");
expect(evidence.normalized_result.imphash.md5).toBe(
"92cef6cc91a85825f3c8a0516517c66f",
);
});
cliTest(
"resolves a relative PE path against the working directory",
async ({ cli }) => {
const root = await createTestTempDirectory("rea-pe-imports-cli-rel-");
await writeFile(join(root, "unsigned.exe"), peWithoutImportTableFixture());
const result = await cli.run({
arguments: ["inspect-pe-imports", "unsigned.exe", "--json"],
cwd: root,
});
expect(result.exitCode, result.stderr).toBe(0);
const evidence = toolContract("inspect_pe_imports").outputSchema.parse(
result.json,
);
expect(evidence.normalized_result.import_directory).toBeNull();
expect(evidence.subject?.local_path).toBe(join(root, "unsigned.exe"));
},
);
cliTest("reports a truncated PE as invalid input", async ({ cli }) => {
const root = await createTestTempDirectory("rea-pe-imports-cli-bad-");
const path = join(root, "broken.exe");
await writeFile(path, Buffer.from("MZ"));
const result = await cli.run({
arguments: ["inspect-pe-imports", path, "--json"],
});
expect(result.exitCode).toBe(1);
expect(result.json).toMatchObject({
code: "invalid_request",
details: { operation: "inspect_pe_imports" },
});
});
cliTest(
"preserves directory declarations and partial rows at RVA and mapping boundaries",
async ({ cli }) => {
const root = await createTestTempDirectory(
"rea-pe-imports-cli-directory-rva-",
);
for (const [kind, rva, retained, mappingGap] of [
["standard", 0xffffffec, 1, false],
["standard", 0xfffffff0, 0, false],
["delay", 0xffffffe0, 1, false],
["delay", 0xfffffff0, 0, false],
["standard", 0x40000000, 0, true],
["standard", 0x40000000, 1, true],
["delay", 0x40000000, 0, true],
["delay", 0x40000000, 1, true],
] as const) {
const path = join(root, `${kind}-${String(rva)}-${String(retained)}.exe`);
await writeFile(
path,
mappingGap
? peImportDirectoryMappingStops(kind, retained)
: peImportDirectoryAtRva(kind, rva),
);
const result = await cli.run({
arguments: ["inspect-pe-imports", path, "--json"],
});
expect(result.exitCode, result.stderr).toBe(0);
const report = toolContract("inspect_pe_imports").outputSchema.parse(
result.json,
).normalized_result;
const directory =
kind === "standard"
? report.import_directory
: report.delay_import_directory;
const rows = kind === "standard" ? report.imports : report.delay_imports;
expect(directory).toMatchObject({ rva });
expect(directory?.locations).toHaveLength(retained);
expect(rows).toHaveLength(retained);
expect(report.coverage.status).toBe("partial");
expect(report.imphash.md5).toBe(
kind === "delay" ? "92cef6cc91a85825f3c8a0516517c66f" : null,
);
}
},
);
cliTest(
"retains a partial inventory when an IAT RVA overflows",
async ({ cli }) => {
const root = await createTestTempDirectory("rea-pe-imports-cli-overflow-");
for (const bits of [32, 64] as const) {
const bytes = peImportImage({ bits });
bytes.writeUInt32LE(
0x100000000 - bits / 8,
peImportFixtureOffset(0x1000) + 16,
);
const path = join(root, `app-${String(bits)}.exe`);
await writeFile(path, bytes);
const result = await cli.run({
arguments: ["inspect-pe-imports", path, "--json"],
});
expect(result.exitCode, result.stderr).toBe(0);
const report = toolContract("inspect_pe_imports").outputSchema.parse(
result.json,
).normalized_result;
expect(report.imports[0]?.functions.map((row) => row.name)).toEqual([
"GetProcAddress",
]);
expect(report.coverage.status).toBe("partial");
expect(report.issues).toEqual(
expect.arrayContaining([
expect.objectContaining({
code: "malformed_entry",
location: "0x100000000",
}),
]),
);
expect(report.imphash.md5).toBeNull();
}
},
);
cliTest(
"keeps complete facts when pefile cannot see the same import table",
async ({ cli }) => {
const root = await createTestTempDirectory(
"rea-pe-imports-cli-compatibility-",
);
const functions = Array.from({ length: 140 }, (_, i) => ({
name: `Function${String(i).padStart(3, "0")}`,
}));
const cases = [
[
"cross-section",
peImportImage({
sectionBytes: 0x4000,
splitAt: 0x1000,
dlls: [{ name: "KERNEL32.dll", functions }],
}),
140,
],
["section-cap", peImportManySectionsImage(2049, 2), 2],
["null-section", peImportLeadingSectionImage(false), 2],
] as const;
for (const [name, bytes, count] of cases) {
const path = join(root, `${name}.exe`);
await writeFile(path, bytes);
const result = await cli.run({
arguments: ["inspect-pe-imports", path, "--json"],
});
expect(result.exitCode, result.stderr).toBe(0);
const report = toolContract("inspect_pe_imports").outputSchema.parse(
result.json,
).normalized_result;
expect(report.imports[0]?.name).toBe("KERNEL32.dll");
expect(report.imports[0]?.functions).toHaveLength(count);
expect(report.coverage.status).toBe("complete");
expect(report.imphash).toMatchObject({
md5: null,
reason: expect.stringMatching(/mapping|compatibility/u),
});
if (name === "cross-section")
expect(report.imports[0]?.functions[128]?.name).toBe("Function128");
}
},
);
@@ -219,6 +219,7 @@ const pathRequests: readonly [string, Record<string, unknown>][] = [
],
["inspect_pe_resources", { path: nul }],
["inspect_pe_certificates", { path: nul }],
["inspect_pe_imports", { path: nul }],
["inspect_native_exports", { path: nul }],
["inspect_elf_notes", { path: nul }],
["inspect_python_pyc", { path: nul }],
+108
View File
@@ -0,0 +1,108 @@
import { symlink, writeFile } from "node:fs/promises";
import { join } from "node:path";
import { describe, expect, it } from "vitest";
import { createPeImportsService } from "../../../src/composition/binaryDiagnostics.js";
import { peImportImage } from "../../fixtures/binaryDiagnostics/peImports.js";
import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js";
describe("PE import stable filesystem boundary", () => {
it("reports non-file and malformed PE selections as caller input errors", async () => {
const root = await createTestTempDirectory("rea-pe-imports-files-");
const path = join(root, "app.exe");
const alias = join(root, "alias.exe");
const malformed = join(root, "malformed.exe");
await writeFile(path, peImportImage());
await writeFile(malformed, Buffer.from("not a pe"));
await symlink(path, alias);
const service = createPeImportsService();
for (const [candidate, reason] of [
[root, "invalid_format"],
[alias, "invalid_format"],
[malformed, "invalid_format"],
[join(root, "missing.exe"), "invalid_value"],
[join(path, "child.exe"), "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_pe_imports",
issues: [
{
path: ["path"],
reason,
message: expect.any(String),
},
],
});
}
});
it("returns typed resource and cancellation failures", async () => {
const root = await createTestTempDirectory("rea-pe-imports-budget-");
const path = join(root, "app.exe");
await writeFile(path, peImportImage());
const service = createPeImportsService();
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");
});
it("derives limit recovery from the parser's structured limit tag", async () => {
const root = await createTestTempDirectory("rea-pe-imports-limits-");
const twoDlls = join(root, "two.exe");
const longNames = join(root, "names.exe");
await writeFile(
twoDlls,
peImportImage({
dlls: [
{ name: "A.dll", functions: [{ name: "a" }] },
{ name: "B.dll", functions: [{ name: "b" }] },
],
}),
);
const name = "A".repeat(4096);
await writeFile(
longNames,
peImportImage({
sectionBytes: 0x1200000,
dlls: [
{
name: "KERNEL32.dll",
functions: Array.from({ length: 4097 }, () => ({ name })),
},
],
}),
);
const service = createPeImportsService();
const dlls = await service.inspect({ path: twoDlls, max_dlls: 1 });
if (dlls.ok) throw new Error("Expected DLL budget failure");
expect(dlls.error).toMatchObject({
_tag: "AnalysisResourceConstraintError",
reportedLimits: { limit: "dlls", maximum: 1 },
remediationAction: expect.stringContaining("Raise max_dlls above 1"),
retryable: false,
});
const text = await service.inspect({
path: longNames,
max_file_bytes: 536870912,
});
if (text.ok) throw new Error("Expected retained-text failure");
expect(text.error).toMatchObject({
_tag: "AnalysisResourceConstraintError",
reportedLimits: { limit: "retained_text", maximum: 16777216 },
remediationAction: undefined,
fixedAllowance: {
name: expect.stringContaining("16777216-byte PE import retained-text"),
kind: "bytes",
},
retryable: false,
});
});
});
+105
View File
@@ -0,0 +1,105 @@
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 {
peImportImage,
peImportDirectoryAtRva,
peImportDirectoryMappingStops,
} from "../../fixtures/binaryDiagnostics/peImports.js";
import { advertisedMcpToolSchemas } from "../../fixtures/advertisedMcpTool.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("PE import inspection must not start a deep provider");
});
const client = await mcp.connect(createServer({ kind: "session", session }));
return { client, session };
};
mcpTest(
"advertises inspect_pe_imports and reads named imports",
async ({ mcp }) => {
const { client, session } = await connect(mcp);
try {
const {
toolDefinition: advertised,
ajv,
outputSchema,
} = await advertisedMcpToolSchemas(client, "inspect_pe_imports");
expect(advertised?.annotations).toMatchObject({
readOnlyHint: false,
idempotentHint: true,
});
const root = await createTestTempDirectory("rea-pe-imports-mcp-");
const path = join(root, "app.exe");
await writeFile(path, peImportImage({ bits: 64 }));
const response = await client.callTool({
name: "inspect_pe_imports",
arguments: { path },
});
expect(response.isError, JSON.stringify(response.content)).not.toBe(true);
expect(
ajv.validate(outputSchema, response.structuredContent),
JSON.stringify(ajv.errors),
).toBe(true);
const evidence = toolContract("inspect_pe_imports").outputSchema.parse(
response.structuredContent,
);
expect(evidence.normalized_result.format).toBe("pe32-plus");
expect(evidence.normalized_result.imports[0]?.name).toBe("KERNEL32.dll");
expect(
evidence.normalized_result.imports[0]?.functions.map((row) => row.name),
).toEqual(["GetProcAddress", "ExitProcess"]);
for (const [label, bytes, rva] of [
["rva", peImportDirectoryAtRva("standard", 0xfffffff0), 0xfffffff0],
["mapping", peImportDirectoryMappingStops("standard", 0), 0x40000000],
] as const) {
const partialPath = join(root, `invalid-first-descriptor-${label}.exe`);
await writeFile(partialPath, bytes);
const partial = await client.callTool({
name: "inspect_pe_imports",
arguments: { path: partialPath },
});
expect(partial.isError, JSON.stringify(partial.content)).not.toBe(true);
expect(
ajv.validate(outputSchema, partial.structuredContent),
JSON.stringify(ajv.errors),
).toBe(true);
const partialReport = toolContract(
"inspect_pe_imports",
).outputSchema.parse(partial.structuredContent).normalized_result;
expect(partialReport.import_directory).toMatchObject({
rva,
locations: [],
});
expect(partialReport.coverage.status).toBe("partial");
expect(partialReport.imphash.md5).toBeNull();
}
} finally {
await session.close();
}
},
);
mcpTest("requires an absolute path for inspect_pe_imports", async ({ mcp }) => {
const { client, session } = await connect(mcp);
try {
const response = await client.callTool({
name: "inspect_pe_imports",
arguments: { path: "app.exe" },
});
expect(response.isError).toBe(true);
expect(parseMcpToolError(response).error).toMatchObject({
code: "invalid_request",
details: { operation: "inspect_pe_imports" },
});
} finally {
await session.close();
}
});
+314
View File
@@ -0,0 +1,314 @@
const SECTION_RVA = 0x1000;
const SECTION_OFFSET = 0x400;
const SECTION_BYTES = 0x800;
const HEADER_BYTES = 0x400;
const SPLIT_GAP_BYTES = 0x200;
/** File offset of an RVA in an unsplit fixture image. */
export const peImportFixtureOffset = (rva: number): number =>
rva - SECTION_RVA + SECTION_OFFSET;
export type PeImportFixtureFunction =
| { readonly name: string; readonly hint?: number }
| { readonly ordinal: number };
export interface PeImportFixtureDll {
readonly name: string;
readonly functions: readonly PeImportFixtureFunction[];
}
/**
* Minimal PE32/PE32+ image whose `.idata` section holds IMAGE_IMPORT_DESCRIPTOR
* and optional IMAGE_DELAYLOAD_DESCRIPTOR tables at RVA 0x1000.
*
* `splitAt` keeps the RVA layout but moves section bytes from that section
* offset onward into a second section at a later, discontiguous file offset,
* filling the vacated file bytes with 0xcc.
*/
export const peImportImage = (
options: {
readonly bits?: 32 | 64;
readonly dlls?: readonly PeImportFixtureDll[];
readonly delayDlls?: readonly PeImportFixtureDll[];
readonly delayAttributes?: number;
readonly importDirectory?: boolean;
readonly delayDirectory?: boolean;
readonly sectionBytes?: number;
readonly splitAt?: number;
} = {},
): Buffer => {
const sectionBytes = options.sectionBytes ?? SECTION_BYTES;
const bits = options.bits ?? 64;
const dlls = options.dlls ?? [
{
name: "KERNEL32.dll",
functions: [
{ name: "GetProcAddress", hint: 1 },
{ name: "ExitProcess", hint: 2 },
],
},
];
const delayDlls = options.delayDlls ?? [];
const bytes = Buffer.alloc(SECTION_OFFSET + sectionBytes);
const wide = bits === 64;
const thunkSize = wide ? 8 : 4;
bytes.write("MZ", 0, "ascii");
bytes.writeUInt32LE(128, 60);
bytes.write("PE\0\0", 128, "ascii");
bytes.writeUInt16LE(wide ? 0x8664 : 0x14c, 132);
bytes.writeUInt16LE(1, 134);
const directoriesAt = wide ? 112 : 96;
const optionalSize = directoriesAt + 16 * 8;
bytes.writeUInt16LE(optionalSize, 148);
const optional = 152;
bytes.writeUInt16LE(wide ? 0x20b : 0x10b, optional);
if (wide) bytes.writeBigUInt64LE(0x180000000n, optional + 24);
else bytes.writeUInt32LE(0x10000000, optional + 28);
bytes.writeUInt32LE(16, optional + directoriesAt - 4);
bytes.writeUInt32LE(HEADER_BYTES, optional + 60);
const section = optional + optionalSize;
bytes.write(".idata", section, "ascii");
bytes.writeUInt32LE(sectionBytes, section + 8);
bytes.writeUInt32LE(SECTION_RVA, section + 12);
bytes.writeUInt32LE(sectionBytes, section + 16);
bytes.writeUInt32LE(SECTION_OFFSET, section + 20);
bytes.writeUInt32LE(0x40000040, section + 36);
const at = (rva: number) => rva - SECTION_RVA + SECTION_OFFSET;
const writeThunk = (rva: number, value: bigint): void => {
if (wide) bytes.writeBigUInt64LE(value, at(rva));
else bytes.writeUInt32LE(Number(value), at(rva));
};
let cursor = SECTION_RVA;
const place = (size: number): number => {
const rva = cursor;
cursor += size;
return rva;
};
const importCount = options.importDirectory === false ? 0 : dlls.length;
const delayCount =
options.delayDirectory === false || delayDlls.length === 0
? 0
: delayDlls.length;
const importTable = importCount === 0 ? 0 : place((importCount + 1) * 20);
const delayTable = delayCount === 0 ? 0 : place((delayCount + 1) * 32);
if (options.importDirectory !== false) {
bytes.writeUInt32LE(importTable, optional + directoriesAt + 8);
bytes.writeUInt32LE((importCount + 1) * 20, optional + directoriesAt + 12);
}
if (delayCount > 0) {
bytes.writeUInt32LE(delayTable, optional + directoriesAt + 13 * 8);
bytes.writeUInt32LE(
(delayCount + 1) * 32,
optional + directoriesAt + 13 * 8 + 4,
);
}
const writeFunctions = (
functions: readonly PeImportFixtureFunction[],
): { readonly ilt: number; readonly iat: number } => {
const ilt = place((functions.length + 1) * thunkSize);
const iat = place((functions.length + 1) * thunkSize);
functions.forEach((entry, index) => {
let thunk: bigint;
if ("ordinal" in entry) {
thunk =
(wide ? 0x8000000000000000n : 0x80000000n) | BigInt(entry.ordinal);
} else {
const hintName = place(2 + entry.name.length + 1);
bytes.writeUInt16LE(entry.hint ?? 0, at(hintName));
bytes.write(`${entry.name}\0`, at(hintName) + 2, "latin1");
thunk = BigInt(hintName);
}
writeThunk(ilt + index * thunkSize, thunk);
writeThunk(iat + index * thunkSize, thunk);
});
return { ilt, iat };
};
dlls.slice(0, importCount).forEach((dll, index) => {
const { ilt, iat } = writeFunctions(dll.functions);
const nameRva = place(dll.name.length + 1);
bytes.write(`${dll.name}\0`, at(nameRva), "latin1");
const descriptor = at(importTable + index * 20);
bytes.writeUInt32LE(ilt, descriptor);
bytes.writeUInt32LE(nameRva, descriptor + 12);
bytes.writeUInt32LE(iat, descriptor + 16);
});
delayDlls.slice(0, delayCount).forEach((dll, index) => {
const { ilt, iat } = writeFunctions(dll.functions);
const nameRva = place(dll.name.length + 1);
bytes.write(`${dll.name}\0`, at(nameRva), "latin1");
const descriptor = at(delayTable + index * 32);
bytes.writeUInt32LE(options.delayAttributes ?? 1, descriptor);
bytes.writeUInt32LE(nameRva, descriptor + 4);
bytes.writeUInt32LE(iat, descriptor + 12);
bytes.writeUInt32LE(ilt, descriptor + 16);
});
if (options.splitAt === undefined) return bytes;
const head = options.splitAt;
const tail = sectionBytes - head;
const moved = SECTION_OFFSET + sectionBytes + SPLIT_GAP_BYTES;
const split = Buffer.alloc(moved + tail);
bytes.copy(split, 0, 0, SECTION_OFFSET + head);
split.fill(0xcc, SECTION_OFFSET + head, moved);
bytes.copy(split, moved, SECTION_OFFSET + head);
split.writeUInt16LE(2, 134);
split.writeUInt32LE(head, section + 8);
split.writeUInt32LE(head, section + 16);
const second = section + 40;
split.write(".idata2", second, "ascii");
split.writeUInt32LE(tail, second + 8);
split.writeUInt32LE(SECTION_RVA + head, second + 12);
split.writeUInt32LE(tail, second + 16);
split.writeUInt32LE(moved, second + 20);
split.writeUInt32LE(0x40000040, second + 36);
return split;
};
/** PE image whose optional header admits no import data directory. */
export const peWithoutImportTableFixture = (bits: 32 | 64 = 64): Buffer =>
peImportImage({ bits, importDirectory: false, delayDlls: [] });
/** Add a file-backed PE32+ range while preserving the ordinary low-RVA payload. */
const withHighImportRange = (
source: Buffer,
rva: number,
data: Buffer,
): Buffer => {
const offset = 0x1000;
const bytes = Buffer.alloc(offset + data.length);
source.copy(bytes);
data.copy(bytes, offset);
bytes.writeUInt16LE(2, 134);
const section = 152 + 240 + 40;
bytes.write(".high", section, "ascii");
bytes.writeUInt32LE(data.length, section + 8);
bytes.writeUInt32LE(rva, section + 12);
bytes.writeUInt32LE(data.length, section + 16);
bytes.writeUInt32LE(offset, section + 20);
bytes.writeUInt32LE(0x40000040, section + 36);
return bytes;
};
/** Directory descriptor/terminator whose mapped range crosses the uint32 RVA end. */
export const peImportDirectoryAtRva = (
kind: "standard" | "delay",
rva: number,
): Buffer => {
const source = peImportImage({
delayDlls:
kind === "delay"
? [{ name: "ADVAPI32.dll", functions: [{ name: "RegOpenKeyExW" }] }]
: [],
});
const width = kind === "standard" ? 20 : 32;
const directory = 152 + 112 + (kind === "standard" ? 1 : 13) * 8;
const oldOffset = peImportFixtureOffset(source.readUInt32LE(directory));
const bytes = withHighImportRange(
source,
rva,
source.subarray(oldOffset, oldOffset + 2 * width),
);
bytes.writeUInt32LE(rva, directory);
return bytes;
};
/** A directory starts in a gap, or its first descriptor is followed by a gap. */
export const peImportDirectoryMappingStops = (
kind: "standard" | "delay",
retained: 0 | 1,
): Buffer => {
const bytes = peImportDirectoryAtRva(kind, 0x40000000);
const section = 152 + 240 + 40;
const size = retained * (kind === "standard" ? 20 : 32);
bytes.writeUInt32LE(size, section + 8);
bytes.writeUInt32LE(size, section + 16);
return bytes;
};
/** A DLL name whose NUL lies outside the uint32 RVA space. */
export const peImportNameAtRva = (rva: number): Buffer => {
const bytes = withHighImportRange(
peImportImage(),
rva,
Buffer.from("KERNEL32.dll\0", "latin1"),
);
bytes.writeUInt32LE(rva, peImportFixtureOffset(0x1000) + 12);
return bytes;
};
/** Later import section behind an empty or suspicious first section header. */
export const peImportLeadingSectionImage = (suspicious: boolean): Buffer => {
const bytes = peImportImage();
const section = 152 + 240;
bytes.copy(bytes, section + 40, section, section + 40);
bytes.fill(0, section, section + 40);
bytes.writeUInt16LE(2, 134);
if (suspicious) {
bytes.writeUInt32LE(0x20000000, section + 8);
bytes.writeUInt32LE(0x20000000, section + 12);
bytes.writeUInt32LE(0xfffffe00, section + 20);
}
return bytes;
};
/** Independent PE32+ mappings with imports in the last section, for capacity checks. */
export const peImportManySectionsImage = (
sectionCount: number,
functionCount: number,
): Buffer => {
const rawSize = 0x20000;
const original = peImportImage({
sectionBytes: rawSize,
dlls: [
{
name: "KERNEL32.dll",
functions: Array.from({ length: functionCount }, (_, i) => ({
name: `Function${String(i).padStart(4, "0")}`,
})),
},
],
});
const optional = 152;
const sectionStart = optional + 240;
const headerBytes = Math.ceil((sectionStart + sectionCount * 40) / 512) * 512;
const finalRaw = headerBytes + (sectionCount - 1) * 512;
const firstRva = Math.ceil(headerBytes / 0x1000) * 0x1000;
const finalRva = firstRva + (sectionCount - 1) * 0x1000;
const delta = finalRva - SECTION_RVA;
const bytes = Buffer.alloc(finalRaw + rawSize);
original.copy(bytes, 0, 0, sectionStart);
bytes.writeUInt16LE(sectionCount, 134);
bytes.writeUInt32LE(0x1000, optional + 32);
bytes.writeUInt32LE(512, optional + 36);
bytes.writeUInt32LE(headerBytes, optional + 60);
bytes.writeUInt32LE(finalRva, optional + 112 + 8);
bytes.writeUInt32LE(finalRva + rawSize, optional + 56);
for (let i = 0; i < sectionCount; i++) {
const at = sectionStart + i * 40;
const size = i === sectionCount - 1 ? rawSize : 512;
bytes.write(i === sectionCount - 1 ? ".idata" : ".dummy", at, "ascii");
bytes.writeUInt32LE(size, at + 8);
bytes.writeUInt32LE(firstRva + i * 0x1000, at + 12);
bytes.writeUInt32LE(size, at + 16);
bytes.writeUInt32LE(headerBytes + i * 512, at + 20);
bytes.writeUInt32LE(0x40000040, at + 36);
}
const data = Buffer.from(original.subarray(SECTION_OFFSET));
const ilt = data.readUInt32LE(0);
const iat = data.readUInt32LE(16);
for (const field of [0, 12, 16])
data.writeUInt32LE(data.readUInt32LE(field) + delta, field);
for (const table of [ilt, iat])
for (let i = 0; i < functionCount; i++) {
const at = table - SECTION_RVA + i * 8;
data.writeBigUInt64LE(data.readBigUInt64LE(at) + BigInt(delta), at);
}
data.copy(bytes, finalRaw);
return bytes;
};
@@ -97,10 +97,12 @@ import { PeResourcesProvider } from "../../src/native/pe/PeResourcesProvider.js"
import { Il2cppMetadataProvider } from "../../src/il2cpp/Il2cppMetadataProvider.js";
import { ElfNotesProvider } from "../../src/native/elf/ElfNotesProvider.js";
import { PeCertificatesProvider } from "../../src/native/pe/PeCertificatesProvider.js";
import { PeImportsProvider } from "../../src/native/pe/PeImportsProvider.js";
import { NativeExportsProvider } from "../../src/native/exports/NativeExportsProvider.js";
import { BeamProvider } from "../../src/erlang/BeamProvider.js";
import { inspectElfNotesInputSchema } from "../../src/domain/native/elfNotes.js";
import { inspectPeCertificatesInputSchema } from "../../src/domain/native/peCertificates.js";
import { inspectPeImportsInputSchema } from "../../src/domain/native/peImports.js";
import { inspectNativeExportsInputSchema } from "../../src/domain/native/nativeExports.js";
import { inspectErlangBeamInputSchema } from "../../src/domain/erlang/beam.js";
import { ModelArtifactProvider } from "../../src/model/ModelArtifactProvider.js";
@@ -855,6 +857,7 @@ const stableArtifactProviders = [
...staticProviders,
["ELF notes", () => new ElfNotesProvider()],
["PE certificates", () => new PeCertificatesProvider()],
["PE imports", () => new PeImportsProvider()],
["native exports", () => new NativeExportsProvider()],
["Python pyc", () => new PythonPycProvider()],
] as const;
@@ -885,6 +888,11 @@ const inspectFileFailure = (
inspectPeCertificatesInputSchema.parse({ path }),
options,
);
if (provider instanceof PeImportsProvider)
return provider.inspect(
inspectPeImportsInputSchema.parse({ path }),
options,
);
if (provider instanceof NativeExportsProvider)
return provider.inspect(
inspectNativeExportsInputSchema.parse({ path }),