diff --git a/skills/exe-reverse/SKILL.md b/skills/exe-reverse/SKILL.md index c053581..d95141a 100644 --- a/skills/exe-reverse/SKILL.md +++ b/skills/exe-reverse/SKILL.md @@ -133,6 +133,8 @@ Load the named file **before your next command**, not after your next three atte | It starts, renders, logs nothing wrong — and one feature silently never works | `references/license-mechanisms.md` | | Your edit is present in the file but clearly not in effect at run time | `references/dynamic-debugging.md` | | A breakpoint at the entry point is never hit, yet the process runs | `references/pe-structure.md` | +| The entry point is runtime startup code, and `main` is still unknown | `references/crt-startup-and-main.md` | +| A routine's epilogue checks a value XORed from the frame, or the entry installs an `fs:[0]` chain | `references/crt-startup-and-main.md` | | The process dies when a debugger attaches, and only then | `references/anti-analysis.md` | | It dies on its own after a delay, with a crash that looks like an ordinary bug | `references/anti-analysis.md` | | Every licence check you patch is followed by another one | `references/license-mechanisms.md` | @@ -166,6 +168,8 @@ Load the named file **before your next command**, not after your next three atte | A detour or code cave crashes its callee, or the arguments arrive at wrong offsets | `references/assembly-idioms.md` | | A 32-bit target's licence file or registry key is absent from the path the documentation gives | `references/assembly-idioms.md` | | A prologue does not look like the compiler the headers named | `references/assembly-idioms.md` | +| A dispatch reads its target from a table, or one value is compared against a long chain | `references/assembly-idioms.md` | +| A callee receives a copy of a structure, or an argument the source never declared | `references/assembly-idioms.md` | | A routine compares the CPUID vendor string, or branches on a flag the manual calls undefined | `references/cpu-vendor-divergence.md` | | You are about to conclude "it must be server-side" | `references/network-activated-licensing.md` | | You cannot tell whether a failure is your patch or the target's own behaviour | `references/verification.md` | diff --git a/skills/exe-reverse/references/assembly-idioms.md b/skills/exe-reverse/references/assembly-idioms.md index fc1f296..8f34a48 100644 --- a/skills/exe-reverse/references/assembly-idioms.md +++ b/skills/exe-reverse/references/assembly-idioms.md @@ -9,8 +9,9 @@ before deciding where to patch and what the patch must preserve. Load this when a routine's parameters or return value do not line up with the convention the toolchain implies; when a detour, stub or code cave must keep its callee working; when a prologue does not match the compiler the headers named; when a body is dense with string instructions or -floating-point stack operations; when a virtual call has no direct target; and when a 32-bit target's -file or registry access does not land where the documentation says it should. +floating-point stack operations; when a virtual call has no direct target; when a routine's constants +look like array scales or structure offsets; when one value selects among many bodies; and when a +32-bit target's file or registry access does not land where the documentation says it should. Grades: the conventions and encodings are documented architecture, and applying one to a listing is `inferred`. Any statement about what a particular build emits is `unverified` until it is checked @@ -176,6 +177,45 @@ that model the vtable pointer as a named first field, or place it before the obj mechanism rather than the ABI: in a real Microsoft build the vptr is at offset 0 and the first data member follows it, which is what the decompiler will show. +## Structure and stride: the numbers that name the type + +A structure is an offset table and an array is a scale factor, so a listing's constants carry most of +the type information that survived compilation — even when every name did not. + +| What the listing does | What it fixes | +|---|---| +| `[base + index*4]` | an array of 4-byte elements: the scale is the element width | +| `[base + index*8]` on x64 | 8-byte elements — a pointer array, a `double`, or a 64-bit integer | +| `lea ecx, [eax+eax*2]`, `lea rax, [rax+rax*4]` | index arithmetic by 3 or 5; `lea` is being used as a multiply, not to form an address | +| `shl ecx, 4` then `lea edx, [ebp+ecx-30h]` | a row stride of 16: a two-dimensional array of four 4-byte elements per row | +| `imul ecx, ecx, 20h` in the same position | a row stride of 32: five 4-byte elements, or eight of another width | +| `add rax, 8` on a pointer | the pointee is 8 bytes wide — that is what `p++` compiled to | +| `sub esp, N`, `mov ecx, N/4`, `rep movs`, `add esp, N` | a structure passed by value, size N; the copy *is* the argument | +| an argument that is written to and also returned in RAX/EAX | a structure returned through a hidden pointer (RCX on x64): the source wrote `return s;` | +| offsets that skip bytes between members | padding, or a pack setting that changed it | + +Three rules generate every layout under the default `/Zp8`: + +- The first member sits at offset 0. +- Each later member starts at the next offset divisible by `min(pack, sizeof member)`. +- The whole structure is rounded up to a multiple of `min(pack, largest member alignment)`, which is + where tail padding comes from. + +So `{char, int, char}` is 12 bytes by default — 1, 3 of padding, 4, 1, 3 of tail — and 6 bytes under +`#pragma pack(1)`. A listing that stores the last `char` at `+5` and copies 6 bytes is telling you the +pack setting, not that the structure is six bytes wide by nature. + +What changes on x64: only structures of 1, 2, 4 and 8 bytes travel in a register; anything larger is +passed by reference in RCX, and a structure returned by value arrives through a hidden RCX that +shifts every other argument one register to the right. An optimised compiler also copies small +structures field by field or with SSE loads, so the `rep movs` shape above is the unoptimised case, +not the rule. + +`inferred` for the layout arithmetic, which is documented `/Zp` behaviour; `unverified` for the +per-build shapes, which come from the tutorial corpus named in +`references/crt-startup-and-main.md`. Neither recovers the declaration: offsets bound the type, they +do not restore the source (`references/coverage-and-limits.md`). + ## WOW64: the two views a 32-bit target actually uses A 32-bit process on a 64-bit Windows runs under WOW64, and its file and registry paths are redirected @@ -207,6 +247,40 @@ What this changes during analysis: a per-user store. A setting that "was written successfully" and cannot be found afterwards can be in either of two places; check both before calling the write a failure. +## A dispatch is a shape, not a keyword + +One value selecting among many bodies compiles to one of four shapes, and the shape states something +about the case set even when the source is gone. + +| Shape | What it is | What it tells you | +|---|---|---| +| `cmp eax, k; je L` repeated, ending in `jmp` | comparison chain | each compared constant is a case value; an if/else ladder and a small `switch` are the same listing, so the shape does not prove `switch` | +| `sub eax, min; cmp eax, max-min; ja L_default; jmp [table + eax*4]` | bounds check, then an address table | the case set spans `min..max`; the table has `max-min+1` entries; `ja` is unsigned, so a value below `min` becomes huge and lands in `default` | +| `movzx eax, byte [base+eax]; jmp [table + eax*4]` | offset table in front of the address table | holes in the range wasted too much space, so a byte-wide table maps each index to an entry | +| a balanced `cmp`/`je`/`jl` tree | binary search over the cases | the values are sparse enough that log n comparisons beat a table | + +Reading one: + +- **The bounds check is the cheapest statement of the case set.** `sub eax, min` and + `cmp eax, max-min` give the lowest case, the span and therefore the table length, without reading + a single entry. +- **The table is data, not code.** Decode it as an array of addresses, not as instructions + (`references/static-analysis.md`); an entry equal to the default's address is a hole, and two + entries with the same target are two cases sharing a body. +- **A target inside another case's body is a fall-through**, the `case 1: case 2:` shape, and the + reason the number of distinct targets can be smaller than the number of cases. +- **Patching one case is patching one entry**, which changes that case and nothing else — but the + table is normally read-only, so a file patch has to change the section's characteristics + (`references/patching.md`, the data-table row in its patch-site table). + +The thresholds usually quoted for this shape — "three cases or fewer", "a gap of six" — come from one +VC++ 6.0 build and are `unverified`; they also contradict the corpus's own examples, where a case set +of 1..10 with one hole still produced a table. What survives across compilers is the arithmetic: a +`sub`/`cmp`/unsigned-`ja` triple followed by an indirect jump is a table lookup, and its operands give +the range. On x64 the entries are eight bytes and the addressing is +`jmp qword ptr [rax*8+table]`; the linker may also merge identical bodies, so several cases can +share one address without the source having said so. + ## Idioms worth recognising on sight | Listing | Means | @@ -236,6 +310,12 @@ What this changes during analysis: an idiom — it is `references/cpu-vendor-divergence.md`. - **A reconstructed frame is a hypothesis.** The listing gives the shape a compiler intended; the unwinder gives the one the processor used. Where they disagree, the run is authoritative. +- **A recovered layout is an interpretation, not the source.** Offsets and strides bound a type; + they do not restore a declaration, and `references/coverage-and-limits.md` rules out presenting them + as the original program text. +- **A dispatch shape is not the source keyword.** A chain of comparisons is what an if/else ladder, a + short switch and a sparse switch all compile to; the listing supports the case set, not the keyword + that produced it. - **Conventions describe calls the compiler emitted.** A stub written by hand, a callback invoked by the system, and an exported function called from another language each need their convention confirmed from the call site, not assumed from the language the target is written in. diff --git a/skills/exe-reverse/references/coverage-and-limits.md b/skills/exe-reverse/references/coverage-and-limits.md index c7df64d..f96845c 100644 --- a/skills/exe-reverse/references/coverage-and-limits.md +++ b/skills/exe-reverse/references/coverage-and-limits.md @@ -42,6 +42,7 @@ named gap. |---|---|---|---| | Static PE structure analysis | `observed` for the reader; `unverified` per target | the shipped reader (`scripts/peutil.py`) parses the DOS header and stub, compiler fingerprint, NT and optional headers, data directories, sections, both offset directions, imports, delay imports, exports, resources, base relocations, TLS callbacks, debug directory and CodeView/PDB, load configuration, the certificate table location, a recomputed file checksum and per-section entropy (`observed`: a summary run over a local sample returned architecture, signature state, declared-versus-computed checksum agreement and the populated directory set) | a disassembler or decompiler for anything past the headers; a second reader when two tools disagree about one field | | Instruction-level reading | `inferred` | the calling conventions, frame shapes and idiom meanings are documented architecture and toolchain behaviour, stated in `references/assembly-idioms.md` so a listing can be checked against them; no target was disassembled here | a disassembler or decompiler, and a build whose toolchain is known — the producer claim stays `inferred` until a second fingerprint agrees | +| C runtime startup and `main` discovery | `inferred` for the chain; `unverified` for the build shapes | the entry symbols and the order of startup are stated from the linker's documented defaults and the runtime's own source, and the `/GS` construction from the same source, in `references/crt-startup-and-main.md`; the VC++ 6.0 prologue, `__chkesp` and SEH shapes come from a public tutorial corpus, and no debugger was run on a target here | a disassembler, a debugger, and a build whose runtime version is known — the call into `main` is confirmed only by running it | | CPU-vendor divergence | `inferred` | the differences in undefined results, system-instruction fault timing, feature-gated decoding and model-specific cost are stated from both vendors' specifications in `references/cpu-vendor-divergence.md`; no divergent behaviour was measured, because every run here came from one Intel host | a host of the other vendor (or a model pinned to one), and a target whose two paths can both be exercised | | Byte patching | `observed` for the patcher's guards; `unverified` on any target | the shipped patcher (`scripts/pepatch.py`) opens the input read-only, writes a new file, refuses a length change without an explicit flag, refuses an ambiguous pattern, pins the expected bytes, re-parses its own output and re-reads every patched byte, and recomputes the checksum (`observed`: a dry run printed the site, the byte pair, the unproven-length note, and wrote no output file) | a debugger, to establish that the patched site is the site that executes | | AOB pattern work | `observed` as a matching rule; `inferred` as a method | a three-byte pattern matched 64 sites and the tool refused the patch, naming three ways out (select an index, accept all, or extend the pattern with context); exact-once is the default (`observed`) | a disassembler, to argue that a pattern's identity is stable across the builds that matter | diff --git a/skills/exe-reverse/references/coverage/route-inventory.md b/skills/exe-reverse/references/coverage/route-inventory.md index 13c12fe..4e713d2 100644 --- a/skills/exe-reverse/references/coverage/route-inventory.md +++ b/skills/exe-reverse/references/coverage/route-inventory.md @@ -26,6 +26,7 @@ becomes `run` only when someone runs it and records the output. | Triage | first contact with an unknown binary: identity, build evidence, protective signals, surface | Python only | `run` | | Toolchain fingerprinting | deciding which toolchain produced a binary, and what that implies for reading it | the binary; a disassembler only if the fingerprint is inconclusive | `run` for the header-level signals, `argued` for the toolchain-specific structure | | Instruction-level reading | reading a routine's convention, frame and idiom so that a patch or a detour preserves what the callee expects | a disassembler or decompiler — the repository ships none | `argued` | +| C runtime startup reading | finding the program's own entry routine behind the runtime's startup, and telling runtime code from program code by its imports, exception frame and `/GS` cookie | a disassembler; a debugger to confirm the call into `main` — the repository ships none | `argued` | | CPU-vendor divergence reading | deciding whether an observed result is architectural or one implementation's choice, and reading a vendor-string dispatch | the binary; a disassembler with its target CPU pinned; a second-vendor host to confirm | `argued` | | Byte patching | making a change that fits an existing instruction or constant slot | Python only, plus a copy to write to | `run` | | Signature location | finding a site across builds rather than at one address | Python only; a second build for the comparison | `run` | diff --git a/skills/exe-reverse/references/crt-startup-and-main.md b/skills/exe-reverse/references/crt-startup-and-main.md new file mode 100644 index 0000000..cd47c41 --- /dev/null +++ b/skills/exe-reverse/references/crt-startup-and-main.md @@ -0,0 +1,185 @@ +# C runtime startup: the code that runs before `main`, and how to find `main` + +Goal: know why the routine at `AddressOfEntryPoint` is not the program's own code, what it has and +has not done by the time it calls `main`, and how to reach `main` when the image has no symbols — +before a patch is placed at the entry point on the assumption that the C runtime is already usable +there. + +## When to load this + +Load this when a debugger stops at the entry point and the code there belongs to the C runtime; when +the image is stripped and the routine the programmer wrote still has to be found; when a prologue +installs an exception frame by hand, ends in `__chkesp`, or XORs a stack slot with a global; when a +patch at the entry point crashes before it does anything; and when deciding whether a target has a +`main` at all. + +Grades: the entry symbols, the chain and the `/GS` construction are `inferred` from Microsoft's +documented linker defaults and from the C runtime's own source. The 32-bit VC++ 6.0 shapes under +"Recognising a 6.0-era startup" come from a public Chinese tutorial series on C-to-assembly, +collected outside this repository, and are `unverified` here: no build was reproduced against them. +No target was started under a debugger to write this file, so every row is a shape to look for, and +the call into `main` is confirmed only by running the image. + +## The entry point is the runtime's code + +`AddressOfEntryPoint` is a linker decision, and the default is the C runtime's own startup routine, +not the function the programmer wrote. + +| Subsystem, character set | Symbol the linker places | Eventually calls | +|---|---|---| +| console, ANSI | `mainCRTStartup` | `main` | +| console, Unicode | `wmainCRTStartup` | `wmain` | +| GUI, ANSI | `WinMainCRTStartup` | `WinMain` | +| GUI, Unicode | `wWinMainCRTStartup` | `wWinMain` | +| DLL | `_DllMainCRTStartup` | `DllMain` | + +Two things override the table, and both are worth checking before believing anything below: the +`/ENTRY` linker option names another routine outright, and a protector or packer rewrites the field +to its own stub, whose job is to rebuild the original image before the real startup ever runs +(`references/protectors-and-packers.md`). A copy of a non-standard entry point is not evidence of a +protector by itself — a build script can set `/ENTRY` too — but it does mean the chain below is not +the one to read. + +## The chain, in the order it runs + +The names after `mainCRTStartup` are the Visual Studio 2015 and later restructure. Visual Studio +2005 to 2013 named the intermediate `_tmainCRTStartup`, and earlier runtimes did the work in the +startup routine itself. The shape is what matters, not the spelling. + +``` +mainCRTStartup the entry point + __scrt_common_main one implementation for exe and dll startup + __scrt_common_main_seh initialises, inside a structured exception handler + _initterm C++ static constructors and C initialisers + ... per-thread data, atexit registration + invoke_main the only stage that knows the program's signature + main the routine the programmer wrote + exit(main result) the process ends here, not at main's ret +``` + +What that order changes: + +- **The arguments do not exist yet at the entry point.** `argc`, `argv` and the environment are + parsed by the startup routine; before it has run, the three arguments `main` will receive have not + been computed from the command line. +- **Static constructors run before `main`, in `_initterm`.** A global object whose constructor is + the real initialisation has already run by the first instruction of `main`; looking for it there + is looking in the wrong function. +- **`main`'s `ret` is not the end of the process.** It returns to the runtime, which calls `exit` + with that value: `atexit` handlers, stream flushing and static destructors all run after it. A + breakpoint on "process exit" is on `exit`, and a patch that intercepts `main`'s return value is + intercepting one intermediate result. +- **TLS callbacks run before the entry point itself** (`references/pe-structure.md`), so "the first + code in the program" is a claim about a layer, not about the file. +- **A stub placed at the entry point runs before any of the above.** It cannot rely on the C runtime + being initialised — heap, stdio, locale and constructors are all still pending. This is why an + entry-point patch that calls a CRT function is a crash rather than a patch, and why the same patch + moved into `main` behaves. + +## Recognising a 6.0-era startup + +The far end of the supported range is a 32-bit, unoptimised VC++ 6.0 build, and its startup has +tells that are not documented as such — they are what that runtime emitted (`unverified`, from the +tutorial corpus named above). + +| Shape in the listing | Reads as | +|---|---| +| `push ebp; mov ebp, esp; push -1; push handler; mov eax, fs:[0]; push eax; mov fs:[0], esp` | an exception registration record installed by hand: the function owns a `__try`. At the entry point it is the runtime wrapping `main` | +| `mov eax, fs:[0]` / `mov fs:[0], eax` anywhere | walking or restoring the SEH chain. Common in startup code; on its own it is not anti-debug | +| `cmp ebp, esp; call __chkesp` after the epilogue's `mov esp, ebp` | `/GZ` stack-frame checking, a debug build of that era | +| `mov eax, 0CCCCCCCCh; mov ecx, N/4; lea edi, [ebp-N]; rep stosd` | uninitialised stack filled with `int 3` bytes — the frame table in `references/assembly-idioms.md` | +| imports of `GetVersion`, `GetCommandLineA/W`, `GetStartupInfoA/W`, `SetUnhandledExceptionFilter`, `HeapSetInformation` | the runtime initialising itself, not program logic | +| names or strings `__set_app_type`, `_controlfp`, `__setusermatherr`, `_setargv`, `_setenvp`, `_initterm` | the stages of the table above, in a static-CRT build | + +Two boundaries on this table. It is 32-bit only: x64 has no `fs:[0]` chain at all, because +exception handling is table-based and dispatched through `__C_specific_handler`; on x64 the startup +is recognised from the import set and from the `__scrt_common_main_seh` name where a PDB or a +string survives. And the debug marks are exactly that — a release build of the same era has neither +`__chkesp` nor the `0xCC` fill, so their absence says nothing about which compiler produced the +file. + +## `/GS`: the cookie explains a crash at the epilogue + +`/GS` stores a value derived from the stack pointer in the frame and re-derives it before returning. +The derivation is the point: the saved value is tied to *this* frame, so it cannot be moved or +replayed. + +``` +x86 x64 +mov eax, dword ptr [__security_cookie] mov rax, qword ptr [__security_cookie] +xor eax, ebp xor rax, rsp +mov dword ptr [ebp-4], eax mov qword ptr [rsp+28h], rax +... ... +mov ecx, dword ptr [ebp-4] mov rcx, qword ptr [rsp+28h] +xor ecx, ebp xor rcx, rsp +call __security_check_cookie call __security_check_cookie +``` + +On x86 the XOR operand is whichever register holds the frame base — EBP when the frame pointer is +kept, ESP in an optimised build — and the epilogue must use the same one as the prologue. + +What it changes: + +- **It is a frame tell, not a local.** A slot written once at entry and read once before the + epilogue, with a module global on both sides of an XOR, is the cookie. Reading it as an ordinary + local and patching its value breaks the derivation, and the failure appears in + `__security_check_cookie`, which reads as "my patch crashed the function". +- **A detour must not skip or repeat the prologue's half of it.** A hook that jumps over the cookie + store but returns through the epilogue, or that runs the prologue twice, leaves a value the check + did not write. This is the same constraint as the frame itself + (`references/assembly-idioms.md` §Frames: what the prologue says about the producer). +- **An uninitialised cookie has a default.** The runtime fills `__security_cookie` once during + startup; before that, a static-CRT image shows the built-in default (`0xBB40E64E` on 32-bit, + `0x00002B992DDFA232` on 64-bit, from the runtime's source; `unverified` here). A slot equal to a + default means "startup has not run yet", not "this routine has no `/GS`". +- **`__security_check_cookie` is also a symbol.** In a stripped image the call target is an address + inside the image, and its body is a compare against the cookie global followed by the failure + path; the compare is what identifies it. + +## Finding `main` when there are no symbols + +The startup chain is the map; the call into `main` is the one edge that leaves the runtime. + +1. **Confirm the entry point is the runtime.** Import set and the tells above. If it is a packer + stub, rebuild first; the chain is not visible until then. +2. **Find the argument globals.** `invoke_main` is the only stage that reads the parsed arguments, + so the routine that loads `__argc` and `__argv` and then tail-calls a routine that is not an + import is the one holding the call to `main`. In an optimised build this may be the entire + visible remnant of the chain. +3. **Or run it.** Break at the entry point, break on `GetCommandLineW`/`GetCommandLineA`, return, + and step until the first `call` whose target is not an import. The other direction works too: + break on a user-visible API the program touches early and walk up until the frame is no longer + runtime code. +4. **Confirm the signature rather than the address.** The C runtime always passes three arguments — + `int main(int argc, char **argv, char **envp)` — even when the source declares two, so the callee + reads `[ebp+8]`, `[ebp+0Ch]` and `[ebp+10h]` (x86), or RCX, RDX and R8 (x64), and returns an + `int` in EAX/RAX that the caller hands to `exit`. A GUI target's `WinMain(HINSTANCE, HINSTANCE, + LPSTR, int)` takes four arguments, and its first comes from `GetModuleHandle` rather than from + the caller's computation. +5. **Check the claim against the process, not the listing.** Set a breakpoint on the candidate and + confirm it is reached once, after the runtime's initialisation, with the command line's + arguments. + A candidate reached before `_initterm` is a constructor or a TLS callback, not `main`. + +**Not every PE has a `main`.** A DLL's program code is `DllMain` and its exports; an `/ENTRY` +override, a packer stub, a managed assembly with a native host, an IL2CPP player and a Go or Rust +image each have their own entry convention (`references/handoff-boundaries.md`, +`references/managed-dotnet.md`, `references/unity-il2cpp.md`, +`references/compiler-fingerprints.md`). Finding "the first program function" in those is a different +question with a different answer, and reporting the runtime's entry point as the program's is the +error this file exists to prevent. + +## What this file does not do + +- **No address and no version.** The chain is a shape; the offsets, the number of stages and the + spelling of each symbol belong to one runtime version, and a build that inlines or folds them can + leave only the call to `main` visible. +- **It does not make the startup readable as business logic.** Initialisation code is where a + protector's own checks often sit, and that is `references/protectors-and-packers.md` and + `references/anti-analysis.md`. +- **A recovered `main` is a candidate until it is run.** The confirmation step above is the only + thing that moves the claim off `unverified`, and it is recorded like any other run + (`references/verification.md`). +- **The 32-bit rows are third-party.** They are a hypothesis about one toolchain's output, useful + for recognition and not a measurement; where they disagree with what the target actually does, + the target wins. diff --git a/skills/exe-reverse/references/evidence-summary.md b/skills/exe-reverse/references/evidence-summary.md index f044842..09b5044 100644 --- a/skills/exe-reverse/references/evidence-summary.md +++ b/skills/exe-reverse/references/evidence-summary.md @@ -101,6 +101,7 @@ that has been exercised. | Unpacking a packaged Python program | no packaged sample was analysed here; the container behaviour is documented by the packagers' own tooling | | Recovering code objects from a versioned wrapper | no wrapped sample was obtained; the mechanism is quoted from one public analysis and is version-specific | | Instruction-level reading of a listing | no target was disassembled here; the conventions are documented architecture, and a producer claim (compiled here, assembled there) is `inferred` until a build whose toolchain is known confirms it | +| C runtime startup and locating `main` | no target was started under a debugger here; the chain is documented in the runtime's own source, and the VC++ 6.0 startup shapes come from a public tutorial corpus and were not reproduced | | CPU-vendor divergence | no AMD host was available; the divergences are stated from both vendors' specifications and one public CVE analysis, and nothing was measured across vendors | ## Where the corpus comes from diff --git a/skills/exe-reverse/references/routing.md b/skills/exe-reverse/references/routing.md index 3f643a6..f46b171 100644 --- a/skills/exe-reverse/references/routing.md +++ b/skills/exe-reverse/references/routing.md @@ -23,6 +23,8 @@ without a check. | It starts, renders, logs nothing wrong — and one feature silently never works | `references/license-mechanisms.md` | | Your edit is present in the file but clearly not in effect at run time | `references/dynamic-debugging.md` | | A breakpoint at the entry point is never hit, yet the process runs | `references/pe-structure.md` | +| The entry point is runtime startup code, and `main` is still unknown | `references/crt-startup-and-main.md` | +| A routine's epilogue checks a value XORed from the frame, or the entry installs an `fs:[0]` chain | `references/crt-startup-and-main.md` | | The process dies when a debugger attaches, and only then | `references/anti-analysis.md` | | It dies on its own after a delay, with a crash that looks like an ordinary bug | `references/anti-analysis.md` | | Every licence check you patch is followed by another one | `references/license-mechanisms.md` | @@ -56,6 +58,8 @@ without a check. | A detour or code cave crashes its callee, or the arguments arrive at wrong offsets | `references/assembly-idioms.md` | | A 32-bit target's licence file or registry key is absent from the path the documentation gives | `references/assembly-idioms.md` | | A prologue does not look like the compiler the headers named | `references/assembly-idioms.md` | +| A dispatch reads its target from a table, or one value is compared against a long chain | `references/assembly-idioms.md` | +| A callee receives a copy of a structure, or an argument the source never declared | `references/assembly-idioms.md` | | A routine compares the CPUID vendor string, or branches on a flag the manual calls undefined | `references/cpu-vendor-divergence.md` | | You are about to conclude "it must be server-side" | `references/network-activated-licensing.md` | | You cannot tell whether a failure is your patch or the target's own behaviour | `references/verification.md` | @@ -79,6 +83,7 @@ reference that is loaded at the wrong moment costs context and answers nothing. | `references/static-analysis.md` | reading code: disassembly, decompilation, structure recovery, and where a listing lies | | `references/compiler-fingerprints.md` | the toolchain is unidentified, or the call structure does not look like C or C++ | | `references/assembly-idioms.md` | a listing must be read at the instruction level: the convention moves the arguments, the prologue says who produced the code, a string instruction stands in for a library routine, or a detour must preserve the frame | +| `references/crt-startup-and-main.md` | the entry point is runtime code: what has run before `main`, where the `/GS` cookie and the SEH frame come from, and how to find `main` in a stripped image | | `references/cpu-vendor-divergence.md` | a result may belong to one processor's implementation rather than to x86-64: undefined flags, vendor-string dispatch, feature-gated decoding, system-instruction fault timing, or a specification about to be handed to another team | | `references/pe-diff-and-versions.md` | two builds must be compared, or a finding has to survive a version change | | `references/patching.md` | any byte edit: site choice, equal-length discipline, checksum, and proving the patch is reached |