mirror of
https://github.com/morluto/rea.git
synced 2026-10-26 17:35:11 +00:00
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:
66 files changed
+4811
-18
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
|
||||
|
||||
@@ -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` |
|
||||
|
||||
@@ -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). |
|
||||
|
||||
@@ -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
@@ -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
@@ -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) |
|
||||
|
||||
@@ -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.
|
||||
@@ -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) |
|
||||
|
||||
@@ -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) |
|
||||
|
||||
@@ -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` |
|
||||
|
||||
@@ -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) |
|
||||
|
||||
@@ -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) |
|
||||
|
||||
@@ -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) |
|
||||
|
||||
@@ -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) |
|
||||
|
||||
@@ -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) |
|
||||
|
||||
@@ -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) |
|
||||
|
||||
@@ -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` |
|
||||
|
||||
@@ -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` |
|
||||
|
||||
@@ -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) |
|
||||
|
||||
@@ -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) |
|
||||
|
||||
@@ -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) |
|
||||
|
||||
@@ -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) |
|
||||
|
||||
@@ -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` |
|
||||
|
||||
@@ -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) |
|
||||
|
||||
@@ -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` |
|
||||
|
||||
@@ -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) |
|
||||
|
||||
@@ -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) |
|
||||
|
||||
@@ -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) |
|
||||
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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"
|
||||
}
|
||||
]
|
||||
@@ -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:
|
||||
|
||||
@@ -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);
|
||||
};
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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"),
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
@@ -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>;
|
||||
@@ -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,7 +117,9 @@ 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", () => {
|
||||
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",
|
||||
@@ -43,10 +130,45 @@ it("reads strings across discontiguous source ranges without examining bytes aft
|
||||
zero_fills: [{ address: 106n, size: 2n }],
|
||||
sections: [],
|
||||
};
|
||||
expect(
|
||||
readMappedCString(Buffer.from("ABCDxxxxE\0xx"), image, 102n, 8).toString(),
|
||||
).toBe("CDE");
|
||||
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)),
|
||||
);
|
||||
},
|
||||
);
|
||||
@@ -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;
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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);
|
||||
},
|
||||
);
|
||||
@@ -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,
|
||||
};
|
||||
};
|
||||
@@ -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;
|
||||
};
|
||||
@@ -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
|
||||
);
|
||||
};
|
||||
};
|
||||
@@ -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,
|
||||
};
|
||||
};
|
||||
@@ -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"],
|
||||
]),
|
||||
};
|
||||
@@ -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;
|
||||
|
||||
@@ -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),
|
||||
);
|
||||
};
|
||||
@@ -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 }],
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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
@@ -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 }),
|
||||
|
||||
Reference in new issue
Block a user