Skip to content

fix(dotnet): Resolve JIT vtable slot issues and add comprehensive V2-V5 ABI translation support (1.1 to 10.0) - #188

Draft
doomedraven wants to merge 14 commits into
kevoreilly:capemonfrom
doomedraven:fixes_174
Draft

fix(dotnet): Resolve JIT vtable slot issues and add comprehensive V2-V5 ABI translation support (1.1 to 10.0)#188
doomedraven wants to merge 14 commits into
kevoreilly:capemonfrom
doomedraven:fixes_174

Conversation

@doomedraven

Copy link
Copy Markdown
Contributor

Will be testing this fixes tomorrow

The crash in SafeGetMethodName during detonation (especially on samples like Formbook that hook, inject, or tamper with .NET runtimes) typically happens due to three core issues in x86 CLR interaction:


1. Calling Convention Mismatch (__thiscall vs __stdcall) on x86

In 32-bit MSVC, methods in ICorJitInfo / ICorMethodInfo use the __thiscall convention (where this is passed in register ECX), unless explicitly declared as COM STDMETHODCALLTYPE.

When defined as:

typedef const char* (__stdcall *fnGetMethodName)(PVOID _this, PVOID ftn, const char** moduleName);
  • What happens: compHnd is pushed onto the stack as the first argument instead of being loaded into ECX.
  • Inside clr.dll: The method reads whatever garbage was in ECX as its this pointer (CEEJitInfo*), and reads compHnd from the stack thinking it is ftn (CORINFO_METHOD_HANDLE).
  • Stack imbalance: The callee cleans 8 bytes (ret 8), but your caller pushed 12 bytes. This corrupts ESP, leading to an immediate crash or an uncatchable stack corruption.

Fix for x86 MSVC: Use __thiscall with compHnd passed as the this argument:

typedef const char* (__thiscall *fnGetMethodName)(PVOID _this, PVOID ftn, const char** moduleName);

2. CLR FailFast Bypasses __try / __except

SEH (__try / __except) cannot catch Windows Fast Fail exceptions (STATUS_FAIL_FAST_EXCEPTION / __fastfail).

Inside clr.dll, CEEJitInfo::getMethodName performs sanity checks on the ftn pointer (casting it internally to MethodDesc*). Formbook frequently performs process hollowing, runtime injection, or dynamic method invoke with synthetic/obfuscated tokens:

  • If ftn is not a valid MethodDesc or points to a dynamic stub without metadata, CLR's internal runtime contracts/assertions trigger an immediate **EEPolicy::HandleFatalError / FailFast**.
  • The OS immediately terminates the process without unwinding the stack to your __except handler.

3. Vtable Index Instability Across CLR Versions

vtable[0] is not guaranteed to be getMethodName across every .NET CLR release:

  • In ICorMethodInfo (defined in corinfo.h), getMethodName is preceded by getMethodDescFromMethod or debugging methods depending on the CLR build and architecture.
  • In some .NET 4.x clr.dll builds, index 0 is getMethodDescFromMethod (virtual MethodDesc* getMethodDescFromMethod(CORINFO_METHOD_HANDLE ftn)), which expects 1 parameter, not 2.

Recommended Defensive Implementation

To prevent detonation termination, verify compHnd memory readability, ensure __thiscall on x86, and validate that ftn resides in valid readable memory before invoking:

#if defined(_M_IX86)
typedef const char* (__thiscall *fnGetMethodName)(PVOID _this, PVOID ftn, const char** moduleName);
#else
typedef const char* (__fastcall *fnGetMethodName)(PVOID _this, PVOID ftn, const char** moduleName);
#endif

static const char* SafeGetMethodName(PVOID compHnd, PVOID ftn, const char** moduleName) {
    const char* name = NULL;
    if (moduleName)
        *moduleName = NULL;

    if (!compHnd || !ftn)
        return NULL;

    __try {
        // 1. Verify compHnd and vtable pointers are readable
        PVOID* vtable = *(PVOID**)compHnd;
        if (!vtable || !vtable[0])
            return NULL;

        // 2. Ensure ftn points to readable memory before passing to CLR
        if (IsBadReadPtr(ftn, sizeof(PVOID)))
            return NULL;

        fnGetMethodName getMethodName = (fnGetMethodName)vtable[0];
        name = getMethodName(compHnd, ftn, moduleName);

        // 3. Validate string pointers
        if (name && IsBadStringPtrA(name, 256))
            name = NULL;

        if (moduleName && *moduleName && IsBadStringPtrA(*moduleName, 256))
            *moduleName = NULL;
    }
    __except (EXCEPTION_EXECUTE_HANDLER) {
        name = NULL;
        if (moduleName)
            *moduleName = NULL;
    }

    return name;
}

Introduce g_dotnet_jit_lock CRITICAL_SECTION and initialize it in DllMain to serialize access to the JIT-related shared state. Protect concurrent access to the lock-free g_dotnet_jit lookup and DotNetCacheDumpCount in compileMethod with Enter/LeaveCriticalSection. Add basic .NET runtime detection scaffolding (runtime enum, version string, GetMethodName slot handling) and ResolveDotNetRuntime() to identify CoreCLR/Framework and derive a version token. Harden SafeGetMethodName to consult the resolved vtable slot. Add <string.h> include and explanatory comments. GetMethodNameSlot currently contains TODO placeholders for verified slot values.
Add runtime-specific ICorJitInfo method-name ABI detection and validation for .NET Framework and Core, including .NET 9 vtable layout handling and safer name filtering. This prevents invalid vtable calls and distinguishes namespace/class/method names in compileMethod logging.

Also serialize the .NET JIT dump teardown path with a critical section so DumpInterestingRegions cannot race the compileMethod hook while both touch the shared JIT dump metadata and DotNetCacheDumpCount.
@doomedraven

Copy link
Copy Markdown
Contributor Author

note for myself to get the slot id

The only missing piece is one integer: the vtable slot of CEEJitInfo::getMethodName in clr.dll. Fill it into GetMethodNameSlot's Framework case and it works, x86 and x64.

Getting the number (pick one)

Static, reproducible, no malware needed — load clr.dll in IDA/Ghidra with the Microsoft PDB (.symfix equivalent / symbol server), find CEEJitInfo::getMethodName, find the xref from the vtable ??_7CEEJitInfo@@6B@, slot = (entry − vtable_base) / ptr_size.

Live, WinDbg:

0:000> .symfix ; .reload
0:000> sxe ld:clrjit ; g
0:000> bp clr!CEEJitInfo::compileMethodHelper   ; (or bm clr!*compileMethod*)
0:000> g
;  x64: comp (ICorJitInfo*) is in rdx    ->  dps poi(@rdx) L140
;  x86: comp is at poi(@esp+8)           ->  dps poi(poi(@esp+8)) L140
;  find the row that resolves to clr!CEEJitInfo::getMethodName
;  slot = (row_addr - vtable_base) / @$ptrsize

Do it on the clr.dll builds that actually matter — realistically just 4.8.x (Win10/11) covers ~all modern samples; add 4.7.2 / 4.6.x only if you analyse older images. If 4.8 servicing turns out to shift it, we add file-version keying then (that's ~40 lines to read clr.dll's RT_VERSION resource without touching hooked APIs — I'll do it if/when needed).

Once you have the slot(s), it's:

case DOTNET_RT_FRAMEWORK:
    *abi = METHOD_NAME_ABI_FRAMEWORK_V2;
    return <slot>;   // clr.dll 4.8.x, verified via dps <date>

@doomedraven
doomedraven marked this pull request as draft August 30, 2026 19:32
…IT vtable slots (1.1 to 10.0 and Framework 4.8)
Rename Get-DotNetVTableSlots.ps1 → Extract-DotNetJitLayout.ps1 and add method-signature extraction (captures argument types). Remove old dotnet-vtable-extraction.md and add vtables.md with expanded ABI, offsets and signatures. Update hook_clr.c to introduce METHOD_NAME_ABI_CORE_V3, add v3 function pointer typedefs, return CORE_V3 in GetMethodNameSlot for CoreCLR 2.1/2.2, and invoke the new v3 slot path in SafeGetMethodName. Enables correct handling of the 3-arg getMethodNameFromMetadata shape.
@doomedraven doomedraven changed the title fix #174 fix(dotnet): Resolve JIT vtable slot issues and add comprehensive V2-V5 ABI translation support (1.1 to 10.0) Aug 31, 2026
@doomedraven

Copy link
Copy Markdown
Contributor Author

@kevoreilly the docs structure is fine for you? is for future versions handling semi-automatically

…NET 11.0

- Update hook_clr.c with robust memory-based PE resource parsing (GetDllVersion) to extract file versions dynamically from clr.dll and mscorwks.dll.
- Map exact JIT compile vtable slots for all .NET Framework versions: .NET 1.1 (slot 105), .NET 2.0-3.5 (slots 16/110), .NET 4.0-4.5.2 (slot 101), .NET 4.6-4.6.2 (slot 102), .NET 4.7-4.7.2 (slot 106), and .NET 4.8-4.8.1 (slot 113).
- Implement support for .NET 11.0 Preview (slot 123) under ABI V5.
- Consolidate .NET documentation and tools: merge vtables.md into README.md, rename get_all.py to extract_dotnet_runtimes.py, and remove legacy/obsolete scripts.
doomedraven added a commit to doomedraven/capemon that referenced this pull request Aug 31, 2026
Document x86 vs x64 calling convention differences for COM interfaces (ICorJitInfo, etc):
- x86 methods use __thiscall (this in ECX register), not __stdcall
- Wrong convention causes stack imbalance and ESP corruption crashes
- Must validate vtable/parameter pointers with IsBadReadPtr before CLR calls
- Addresses issue from PR kevoreilly#188 (dotnet JIT vtable hooking)

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CYuhA1ZnEMA7waWWJKBgZy
doomedraven added a commit to doomedraven/capemon that referenced this pull request Aug 31, 2026
1. Common Crash Patterns & Prevention:
   - Static TLS Corruption (wrong declspec(thread) usage in post-loaded DLLs)
   - Calling Convention Mismatch (__thiscall vs __stdcall on x86)
   - Heap Allocation Under Lock (re-entrancy deadlock, process freeze)
   Each with symptoms, root cause, bad example, and fix

2. Synchronization & Lock Safety:
   - Critical section usage rules
   - Lock ordering discipline
   - Nested lock prevention
   - Shared state vs code region protection

3. Stack-Based Allocation (SBO) Pattern:
   - Why: prevent re-entrancy deadlock
   - Pattern examples: stack buffers, pre-allocated TLS
   - Verification: grep for malloc/calloc in hook callbacks

Based on real crash fixes in recent PRs:
- PR decoupled-logging-v2: static TLS crashes
- PR kevoreilly#188: calling convention crashes
- PR kevoreilly#162: re-entrancy deadlock from heap allocation

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CYuhA1ZnEMA7waWWJKBgZy
CAPE: Add local flags and tighten g_dotnet_jit access with Enter/LeaveCriticalSection to avoid races when detecting/dumping .NET images and JIT native caches; move debug output and protect increment of DotNetCacheDumpCount with the lock.

hook_clr: Make version parsing accept single-number versions (major only); fix IsPlausibleName loop to correctly detect NUL within 256 bytes and avoid off-by-one; and require g_dotnet_runtime_resolved before reading pointers in SafeGetMethodName to prevent invalid reads.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant