mirror of
https://codeberg.org/reverse-skill-lab/exe-reverse
synced 2026-11-15 20:34:25 +08:00
Add the import-layer repair and the Themida absorption
references/iat-virtualization-and-call-site-rewriting.md records the two edits one protector family makes to a native image: the import slot stops holding an address and the indirect call that read it becomes a relative call to a stub. It carries the pairing rule -- a site's target equals a slot's value -- the three placements of the filler byte in a six-byte rewrite and the refusal when there is none, the divergence between builds that modify the code section and builds that only redirect the table, the small set of imports redirected twice, the choice between repairing, extracting a payload, observing and reporting, and the limits an automatic unpacker inherits from its own dump. scripts/iat_callsite_restore.py implements the mechanical half offline: inventory the slots from three sources, pair by value, decide the six-byte window per site, refuse an ambiguous slot and a site whose sixth byte is the next instruction, rewrite, then re-decode every written site from the file it wrote. It is report-only unless --out is given, and it does not unpack: a virtualised entry has no site list to restore. Registered in both symptom tables, the routing, reference and script indexes, the capability registry as call_site_restore, and the coverage, route-inventory and evidence tables. dynamic-debugging.md gains the instrument rule the second write-up demonstrates (a target that runs at full speed and dies when stopped has a perturbation detector), memory-and-dumps.md gains the call-site step of a rebuild, environment.md gains the compatibility-shim trap, and protectors-and-packers.md gains one recognition row plus a pointer rather than the detail, because that file makes no product-level claims. The target-behaviour claims stay unverified: no protected sample was analysed here. What is observed is the tool's behaviour on a generated image -- both filler positions, both refusals, both displacement forms -- and what the source material states about itself. FINDINGS F16 records the defect this pass produced, a slot's magnitude read as its meaning, with the test that fails without the fix. .gitignore gains ref/, which was not covered by the existing rule because that rule carries a leading dot.
This commit is contained in:
@@ -0,0 +1,75 @@
|
||||
# Extension: absorbing an import-virtualisation pass
|
||||
|
||||
What this pass covered, what it verified externally, and what it deliberately did not assert. The
|
||||
material was three public write-ups of one protector family plus the source tree of an open-source
|
||||
unpacker for it, read locally. No URL and no author is recorded here: neither was opened in this
|
||||
session, and a citation that was not verified is a claim dressed as a reference.
|
||||
|
||||
The organising question is the one that decides what can be written down at all: **what is
|
||||
mechanically true of an import layer once a protector rewrites it, and what belongs only to the
|
||||
build the writer happened to have?**
|
||||
|
||||
## What was verified externally
|
||||
|
||||
| Fact | How it was established | Grade |
|
||||
|---|---|---|
|
||||
| The import layer is changed in two places, not one: the slot stops holding an address, and the call that read it becomes a relative call | Two write-ups describe the same pair of edits on the same product family, on different builds | `unverified` as target behaviour; `observed` that the sources say it |
|
||||
| A repair can pair a site to a slot by comparing the site's computed target address with the table's values, then write an indirect call back | Read in the unpacker's fixup routine: it builds an address-to-slot map from the table, reads the relative target, and writes `FF 15`/`FF 25` | `observed` (of the source) |
|
||||
| A newer generation computes each slot's value from a per-slot constant by arithmetic that is itself virtualised, so the handle-to-API mapping cannot be patched out | The same source states it in a comment at the point where it chooses to trace instead of patch | `observed` (of the source) |
|
||||
| That unpacker repairs one anti-dump type only, and states that a target virtualising other regions will very likely still crash | Comment at the top of its anti-dump fixer | `observed` (of the source) |
|
||||
| A dump of a target whose entry is virtualised does not run, and the tool says so rather than shipping it | Its own documentation | `observed` (of the documentation) |
|
||||
| A working dump of such a target can be bound to the machine and session it was taken on, because some anti-dump checks compare module base addresses | The same documentation | `observed` (of the documentation) |
|
||||
| A DLL dump loses relocation information | The same documentation | `observed` (of the documentation) |
|
||||
| Targets from the late 2000s may only unpack on the OS generation they were built for | The same documentation | `observed` (of the documentation) |
|
||||
| The operating system applies compatibility fixes keyed to an executable's path, so a program that crashed once can behave differently later with identical bytes | The same documentation names the mechanism and the store | `observed` (of the documentation); the store itself was not read here |
|
||||
| One later build leaves the first code section untouched and only redirects the table | A write-up, on one build | `unverified` |
|
||||
| A small, fixed set of runtime-owned imports is redirected twice | A write-up, on one build and one compiler | `unverified` |
|
||||
| The filler byte of the 6-to-5 byte rewrite appears after the call, before it, and not at all, in different builds | Two write-ups plus the unpacker's handling of a site whose call is not at offset 0 | `unverified` |
|
||||
| One write-up's version and date are internally inconsistent — it dates the build to a year, then calls the same build more than a decade old | Read here, in the source text | `observed` |
|
||||
|
||||
The last row is the reason the material was absorbed as mechanism only. A source that is wrong about
|
||||
its own subject in one place can be wrong in another, and the wrong part does not announce itself.
|
||||
|
||||
## What this changed in the skill
|
||||
|
||||
- **New reference**, `references/iat-virtualization-and-call-site-rewriting.md`: the two-edit model,
|
||||
the pairing rule, the six-byte window and its three cases, the version-divergence table, the
|
||||
double-redirected set, the deliverable decision (repair / extract / observe / report), the limits
|
||||
an automatic unpacker inherits, and the verification rules.
|
||||
- **Entry point**: five symptom rows and one route row, because "the import table parses but holds
|
||||
handles" and "a dump dies at its first imported call" are not findable from the existing rows.
|
||||
- **`references/protectors-and-packers.md`**: one recognition row and a short section. The file states
|
||||
that it makes no product-level claims, so the mechanism stayed there and the detail did not.
|
||||
- **`references/memory-and-dumps.md`**: call sites are part of a dump, not a detail of the import
|
||||
table, so the rebuild step now says that repairing the slots can restore a table nothing reads.
|
||||
- **`references/dynamic-debugging.md`**: the instrument rule the second write-up actually
|
||||
demonstrates — a target that runs at full speed and dies when stopped has a perturbation detector,
|
||||
and the response is a different breakpoint family, not more hiding options.
|
||||
- **`references/environment.md`**: the compatibility-shim fact, because it changes what "it ran
|
||||
once" means and it lives in neither file.
|
||||
- **`scripts/iat_callsite_restore.py`** with a unit and a CLI layer, registered in the capability
|
||||
registry as `call_site_restore`.
|
||||
|
||||
## What was deliberately not asserted
|
||||
|
||||
- **No address, register name, opcode listing or script from any single build.** Those are the parts
|
||||
of the source material that stop being true first, and the skill already carries a rule that a
|
||||
build-level claim travels in the same sentence as the hash it was observed on.
|
||||
- **No claim that the script works on a real protected sample.** It was exercised against a
|
||||
generated image whose three layout cases were written on purpose, so its rows are fixture-observed
|
||||
and say so. The route it implements is `unverified` against a protector until someone runs it.
|
||||
- **No claim that repairing the call sites produces a working binary.** Virtualisation, anti-dump and
|
||||
self-integrity checks are separate problems and the reference says so in its own last section.
|
||||
- **No claim that the filler byte is always a NOP.** One of the three placements has no filler, which
|
||||
is exactly why the script refuses that case instead of writing five bytes and a guess.
|
||||
- **No product-strength opinion**, and none of the numeric claims from the write-ups (counts of
|
||||
redirected imports, section growth, timings) survived into the reference.
|
||||
|
||||
## Why this route is probed rather than declared
|
||||
|
||||
The repair is arithmetic over a file, so it can be tested without a target: a generated image, three
|
||||
sites with deliberately different neighbours, and a slot range the import directory does not
|
||||
describe. That is what the tests do, and it is why the script ships with refusals rather than
|
||||
fallbacks — every fallback in this area (pick the first matching slot, overwrite the sixth byte, skip
|
||||
a site silently) produces a file that loads and misbehaves later, which is the failure shape this
|
||||
repository exists to make visible.
|
||||
@@ -154,6 +154,30 @@ on this host rather than an error.
|
||||
*Fix and check:* long values wrap onto continuation lines instead of being cut. A test asserts a long
|
||||
value survives into the output intact.
|
||||
|
||||
## Repair-script defects
|
||||
|
||||
**F16. A slot's magnitude was read as its meaning, so an ordinary import table looked protected.**
|
||||
|
||||
*Symptom:* the import-layer repair's first version classified every slot whose value was below the
|
||||
image base as a handle, and its verdict rule turned that class into `needs-trace` — a
|
||||
protected-image verdict for a file that was never protected. Nothing about the output looked wrong:
|
||||
the classes were counted, the verdict followed from them, and the report was internally consistent.
|
||||
|
||||
*Root cause:* an unbound import table holds RVAs, and an RVA is also a number below the image base.
|
||||
The classifier inferred intent from magnitude instead of testing whether the value resolves to
|
||||
something in the image, or whether a call targets it. The same mistake in the other direction would
|
||||
have called a real handle an address.
|
||||
|
||||
*How it surfaced:* while reading `peutil.py`'s import walk to build the slot inventory. The reader
|
||||
returns the table's entries as they are in the file, which is what made "below the base" visibly
|
||||
ambiguous rather than obviously a handle.
|
||||
|
||||
*Fix and check:* the classes are positions (`zero`, `below_image`, `in_image`, `above_image`) and the
|
||||
verdict follows from the pairing rather than from a class. The regression is
|
||||
`tests/unit/test_iat_callsite_restore.py::test_a_clean_image_reports_no_pairs`, which asserts
|
||||
`no-pairs` on a generated unbound image — the case the old rule reported as `needs-trace`. The
|
||||
reference states the correction in prose, because the same inference is easy to repeat by hand.
|
||||
|
||||
## Gate, harness and lint defects
|
||||
|
||||
**F11. The hard index-row limit was computed and then unable to fail anything.**
|
||||
|
||||
Reference in New Issue
Block a user