diff --git a/skills/exe-reverse/references/compiler-fingerprints.md b/skills/exe-reverse/references/compiler-fingerprints.md index 29bfd20..e9edc73 100644 --- a/skills/exe-reverse/references/compiler-fingerprints.md +++ b/skills/exe-reverse/references/compiler-fingerprints.md @@ -54,7 +54,8 @@ and the versioned C++ runtime (both present on this host as `vcruntime140.dll` a `observed` by path existence) indicates a C/C++ build whose exceptions and STL use the matching runtime. Symbol names in `.pdb`-style C++ mangling, RTTI records, and a `.pdata` array covering every non-leaf function give you function extents for free — read the exception directory before -disassembling, because it bounds functions the disassembler will otherwise join. +disassembling (`references/static-analysis.md` reads it as a function table), because it bounds +functions the disassembler will otherwise join. **The timestamp caution.** A `/Brepro`-style deterministic build writes a hash into date/time fields, and the file declares that with a debug entry of type 16. The specification's wording is explicit that such diff --git a/skills/exe-reverse/references/dotnet-decompilation.md b/skills/exe-reverse/references/dotnet-decompilation.md index 35f1d61..0c374e5 100644 --- a/skills/exe-reverse/references/dotnet-decompilation.md +++ b/skills/exe-reverse/references/dotnet-decompilation.md @@ -435,9 +435,10 @@ the feature observed directly. `unknown`, and it is worth stating precisely: **no .NET tool was installed on the host that wrote this file**, so every command above is quoted from its project's or vendor's documentation and none was executed here. `observed` in this file therefore means "the named document says so", -not "a run reproduced it". The versions behind the claims are the ILSpyCmd 11.0.0 help text, the +not "a run reproduced it". The versions behind the claims are the ILSpyCmd command-line help, the Microsoft Learn pages for `ildasm` and `ilasm`, the de4dot README at the 2015-era source range, -and the dnSpyEx README, read on 2026-10-24. +and the dnSpyEx README, read on 2026-10-24; the exact revision of each is recorded with its +source in `skills/exe-reverse/evidence/tool-sources.md`. Also not covered: diff --git a/skills/exe-reverse/references/injection-and-hooking.md b/skills/exe-reverse/references/injection-and-hooking.md index f4612cb..0e64714 100644 --- a/skills/exe-reverse/references/injection-and-hooking.md +++ b/skills/exe-reverse/references/injection-and-hooking.md @@ -118,6 +118,50 @@ whole instructions. Concretely: be suspended while you write (`inferred`; the failure it produces is a crash inside the target that disappears under a debugger). +## Building the stub: where it lives, how it reaches, and what it clobbers + +The trampoline above is the reading problem. This is the authoring problem, and it has four +independent decisions: where the bytes live, which jump encoding can reach them, what the stub must +preserve, and in what order the writes land. + +**Placement.** + +| Where | Reached by | Cost | +|---|---|---| +| a run of padding inside an executable section — a "cave" | a relative jump, if the distance is in range | no structural change; the cave may sit in a page the target hashes | +| a new section | a relative jump, because the RVA is chosen with it | structural: section table, alignment and size fields all change, and the file grows (`references/patching.md`) | +| memory allocated in the target | the jump is written into the target's code and points at the allocation | memory patch only; the allocation must be made executable, and a private executable region is visible to a memory scan | +| an existing routine that is close enough | a routine-pointer or call-site edit | no stub at all — the smallest edit, when such a routine exists | + +**Reach.** The jump has to encode the distance, and on x64 the distance is the problem: a relative +displacement is 32-bit, so it covers ±2 GB, while an image and a separately allocated region are +commonly farther apart than that (`inferred` from the encoding width). + +| Encoding | Bytes | Range | Notes | +|---|---|---|---| +| `EB rel8` | 2 | ±127 | cheapest and the most fragile; a rebuild moves the target | +| `E9 rel32` | 5 | ±2 GB | the default hook; needs five bytes of whole instructions at the site | +| `FF 25 00000000` + an 8-byte address | 14 | the full 64-bit space | reads the address out of the instruction stream; needs 14 bytes at the site, which a function head rarely has | +| `mov rax, imm64; jmp rax` | 12 | the full 64-bit space | clobbers RAX, which the callee may be about to use | +| a relay: the 14-byte form parked in a cave, reached by `E9 rel32` | 5 at the site | ±2 GB to the cave | the usual answer when the stub is far away and the head is short | + +**What the stub must preserve.** The non-volatile registers it touches, and the flags if it branches +before restoring them (`pushfq`/`popfq`). A stub that calls anything needs the x64 frame — shadow +space and 16-byte alignment — described in `references/assembly-idioms.md`. Position independence is +not optional: the image base moves between runs, so the stub resolves what it needs at run time, +either from data placed beside it or through the export tables. + +**Order of writes.** The stub body goes in first and the hook bytes last, so the site never points at +a half-written routine. A five-byte write is not atomic against a thread executing there: suspend the +other threads, or make the site's page non-executable for the duration. Where the site is 8-byte +aligned, one aligned 64-bit store installs the five-byte jump plus three filler bytes as a single +atomic write, which is why that alignment is worth checking before the site is chosen. + +**Producing the bytes.** Assemble the stub to an object, take the `.text` raw bytes, and disassemble +those bytes back before using them: an assembler that emitted a relocation, or a section extracted at +the wrong file offset, still produces something that assembles. Then give the hex to the patcher's +`--bytes` on the file route, or write it into the allocation on the memory route. + ## Managed methods are a different target A managed method's code may not exist yet when you want to hook it: it is produced at run time, can be diff --git a/skills/exe-reverse/references/recon.md b/skills/exe-reverse/references/recon.md index 0ffda7c..7bb0af2 100644 --- a/skills/exe-reverse/references/recon.md +++ b/skills/exe-reverse/references/recon.md @@ -143,6 +143,38 @@ The import list is the cheapest structural evidence in the file, and the most mi usually a library the request cares about; a stripped export table on a `DLL` subsystem image is itself a finding. +### The import list names the mechanism, read in clusters + +One API proves nothing; three that only make sense together are the mechanism. `GetTickCount` is in +every program, and `GetTickCount` read twice around `Sleep` and compared to a constant is a timing +check. Name the cluster, then find one call site before believing it — mechanism without a site is a +hypothesis (`inferred`). + +| Cluster in the import table | Mechanism it points at | Route that continues | +|---|---|---| +| `OpenProcess` + `VirtualAllocEx` + `WriteProcessMemory` + `CreateRemoteThread`, or `NtCreateThreadEx`, `QueueUserAPC`, `SetThreadContext` | injection into another process: a loader, a launcher, or the program's own agent | `references/injection-and-hooking.md` | +| `CreateToolhelp32Snapshot` + `Process32First/Next`, `EnumProcesses`, `OpenProcess` | enumerating processes, usually to find the analysis tool or the game before it | `references/anti-analysis.md` | +| `IsDebuggerPresent`, `CheckRemoteDebuggerPresent`, `NtQueryInformationProcess`, `OutputDebugString` | anti-debug reads, direct or through the PEB | `references/anti-analysis.md` | +| `LoadLibrary` + `GetProcAddress`, or little else | a stub that resolves the rest at run time | `references/protectors-and-packers.md` | +| `RegOpenKeyEx`, `RegQueryValueEx`, `RegCreateKeyEx`, `RegSetValueEx` | local configuration or licence state — in the redirected 32-bit key when the image is 32-bit | `references/license-mechanisms.md` and `references/assembly-idioms.md` | +| `CryptAcquireContext`, `CryptHashData`, `BCrypt*` | hashing or encryption of something local: a stored licence, an integrity digest, a challenge | `references/license-mechanisms.md` and `references/integrity-checks.md` | +| `WinHttpOpen`, `InternetOpen`, `HttpSendRequest`, `WSAStartup` with `socket`/`send`/`recv` | a network-activated decision, or an update check | `references/network-activated-licensing.md` | +| `GetVolumeInformation`, `GetAdaptersInfo`, `GetSystemFirmwareTable`, `WbemLocator` | machine binding: a fingerprint that the licence is scoped to | `references/license-mechanisms.md` | +| `GetTickCount`, `QueryPerformanceCounter`, `timeGetTime` around `Sleep` or `NtDelayExecution` | timing checks: anti-debug or a trial clock | `references/anti-analysis.md` | +| `FindWindow`, `GetForegroundWindow`, `GetWindowText`, `EnumWindows` | watching for a debugger, a monitor, or another product's window | `references/anti-analysis.md` | +| `CreateFileMapping`, `MapViewOfFile`, `CreateMutex`, `CreateEvent` with a fixed name | one-instance enforcement, a local IPC handshake, or state shared with a helper process | `references/injection-and-hooking.md` | +| `GetModuleFileName`, `GetTempPath`, `SHGetFolderPath`, `GetEnvironmentVariable` | where its state actually lives — resolve it before searching, and take the 32-bit view into account | `references/assembly-idioms.md` | +| `CreateProcess`, `ShellExecuteEx`, `WinExec` | a relaunch, an elevation, an updater, or the launcher that extracts a payload | `references/installers-and-packaging.md` | +| `SetWindowsHookEx`, `SetWinEventHook`, `RegisterHotKey` | system-wide observation or input capture | `references/injection-and-hooking.md` | +| `OpenProcessToken` + `AdjustTokenPrivileges`, `IsUserAnAdmin` | privilege manipulation, or an elevation check with a second code path behind it | `references/environment.md` | +| `VirtualProtect`, `FlushInstructionCache`, `VirtualAlloc` with an execute protection | code written at run time: a JIT, a trampoline, or a decryptor | `references/protectors-and-packers.md` | + +Two cautions. The same cluster appears in legitimate software — an installer injects, a security +product enumerates processes — so the cluster is a direction, not a verdict. And an unresolved name is +not an absent capability: delay-loaded and manually resolved imports never appear in this table, so a +cluster that should be present and is not is evidence about the resolver. The search you ran is the +finding, not the absence. + ## What the program talks to Do this from the file, before any execution, because a first execution of an unknown binary should diff --git a/skills/exe-reverse/references/static-analysis.md b/skills/exe-reverse/references/static-analysis.md index 99bdc0e..fc01d93 100644 --- a/skills/exe-reverse/references/static-analysis.md +++ b/skills/exe-reverse/references/static-analysis.md @@ -78,6 +78,37 @@ Consequences for your search: - Record which search you ran and over what. A negative result is `unknown` with a named search, never "the program does not contain it". +## The exception directory is a function table (x64) + +Every x64 function the toolchain gave unwind data to has a **runtime function entry** in `.pdata`, and +data directory 3 (the exception directory) points at the array. Each entry is three 32-bit RVAs — begin +address, end address, unwind-info address — and the array is sorted by begin address (`inferred` from +the x64 unwind convention; `observed` that the shipped reader reports the directory and its size). + +That makes it the only **exhaustive** function table in a stripped image, and it is worth reading before +any disassembly: + +- It gives real boundaries, so the tail-call merge and the shared epilogue described above stop being + guesses. +- It bounds a function the disassembler would otherwise run past, because the end address is recorded. +- It gives the frame without executing it: the unwind record carries the prologue size, the frame + register — zero when the frame pointer was omitted — and one code per non-volatile register that was + saved, with where. + +Read it before disassembling. `python scripts/peutil.py APP.exe --summary` reports whether directory 3 +is populated and how large it is, and `--json` gives the section table so the RVAs can be mapped to +file offsets. Three checks say whether what you parsed is really a table: begin below end, end inside an +executable section, and entries in ascending order. A record that fails one of them means a different +record layout or a packed image, not a table to be repaired. + +Three limits. **Leaf functions may have no entry**, because there is nothing to unwind; a call target +outside every range is therefore a leaf, a hand-written routine assembled without unwind data, or a +splice, and which one it is needs a second observation. **x86 has no such table** — the directory is +normally empty and the exception mechanism is stack-linked instead, so on 32-bit the boundaries come +from exports, symbols, call sites and the frame shapes in `references/assembly-idioms.md`. And **the +record layout is architecture-specific**: an ARM64 image populates the same directory with a packed +encoding, so parse the 12-byte x64 record only when the machine field says `0x8664`. + ## Call graph and caller counting The call graph is the cheapest structure in the file, and it is the one that prevents the single most