Preventing process termination by patching Windows exit functions through in-memory code modification. Redirects execution from process-terminating APIs to ExitThread to prevent premature application shutdown.
Based on the technique described in MDSec's article on preventing Environment.Exit in in-process .NET assemblies.
The implementation intercepts Windows API functions that terminate the current process by patching their function bodies with shellcode that redirects execution to ExitThread instead. When any patched exit function is called, the shellcode executes which calls ExitThread(0), terminating only the calling thread rather than the entire process.
This technique prevents in-process code (e.g., .NET assemblies loaded into the same process) from terminating the host by patching Windows exit APIs to call ExitThread. Unlike CLR-level patches of System.Environment.Exit, this works at the Windows API level—but it only intercepts exit calls made inside the same process (it does not stop external or kernel-level termination).
The shellcode is written directly into the target function's memory space using VirtualProtect to modify memory protection flags and memcpy to overwrite function bytes. Original function bytes are preserved for restoration via ResetExitFunctions().
The implementation patches the following exit functions across multiple DLLs:
kernelbase.dll::TerminateProcesskernel32.dll::TerminateProcesskernelbase.dll::ExitProcesskernel32.dll::ExitProcessmscoree.dll::CorExitProcessntdll.dll::NtTerminateProcessntdll.dll::RtlExitUserProcess
Function addresses are resolved using GetModuleHandleW and GetProcAddress. If a module is not loaded or a function is not found, that specific entry is skipped without affecting other patches.
Shellcode is generated dynamically based on the architecture. The implementation builds position-independent code that loads the address of ExitThread into a register and jumps to it.
MOV RCX, 0 ; 48 C7 C1 00 00 00 00
MOV RAX, ExitThreadAddr ; 48 B8 [8-byte address]
JMP RAX ; FF E0Total size: 17 bytes
MOV ECX, 0 ; B9 00 00 00 00
MOV EAX, ExitThreadAddr ; B8 [4-byte address]
JMP EAX ; FF E0Total size: 12 bytes
The ExitThread address is resolved at runtime by querying kernelbase.dll first, falling back to kernel32.dll if not found. The address is embedded directly into the shellcode as an immediate value, allowing execution without additional memory dereferences.
The patching process uses Structured Exception Handling (SEH) to handle potential access violations during memory operations:
-
ReadMemory: Reads original function bytes into a buffer using
memcpywrapped in__try/__exceptto handle read violations gracefully. -
WriteMemory:
- Calls
VirtualProtectto change memory protection fromPAGE_EXECUTE_READtoPAGE_EXECUTE_READWRITE - Writes shellcode bytes using
memcpywrapped in SEH - Restores original memory protection flags via
VirtualProtect - If any step fails, original protection is restored and the function returns false
- Calls
-
Restoration:
ResetExitFunctions()iterates through all patched functions and writes the original bytes back, effectively undoing all patches.
The patching process follows this sequence:
- Resolve
ExitThreadaddress fromkernelbase.dllorkernel32.dll - Generate architecture-specific shellcode with embedded
ExitThreadaddress - For each target function:
- Resolve function address via
GetModuleHandleWandGetProcAddress - Read original function bytes (up to shellcode size, maximum 19 bytes)
- Modify memory protection to
PAGE_EXECUTE_READWRITE - Write shellcode bytes
- Restore original memory protection
- Mark function as patched
- Resolve function address via
- If any patch fails,
ResetExitFunctions()is called to restore previously patched functions
The implementation stores original bytes in a static array (ExitFunction::ORGBytes[19]) which is sufficient for both x86 and x64 shellcode variants. The actual shellcode size is stored in gshcsize to handle variable-length shellcode correctly.
-
ExitPatcher::PatchExit()- Patches all target exit functions with redirect shellcode. Returnstrueif all patches succeed,falseotherwise. Automatically restores patches on failure. -
ExitPatcher::ResetExitFunctions()- Restores original function bytes for all previously patched functions. Safe to call multiple times. -
GetFunctionAddress(moduleName, functionName)- Internal helper that resolves function addresses usingGetModuleHandleWandGetProcAddress. Returnsnullptrif module or function not found. -
ReadMemory(address, buffer, size)- Safely reads memory using SEH to handle access violations. Returnstrueon success. -
WriteMemory(address, data, size)- Modifies memory protection, writes data, and restores protection. Uses SEH for error handling. Returnstrueon success.
This implementation demonstrates process termination prevention through API patching. The technique modifies executable code in memory and can be detected through code integrity monitoring, memory protection analysis, or behavioral detection. Use as part of a defensive strategy for protecting host processes from premature termination by loaded code.
MIT
- MDSec for the original research on preventing Environment.Exit in in-process .NET assemblies
- Based on techniques described in Massaging your CLR: Preventing Environment.Exit in In-Process .NET Assemblies