├── ExitPatcher ├── patcher │ ├── ExitPatcher.hpp │ └── ExitPatcher.cpp ├── ExitPatcher.vcxproj.user ├── main.cpp ├── ExitPatcher.vcxproj.filters └── ExitPatcher.vcxproj ├── ExitPatcher.sln └── README.md /ExitPatcher/patcher/ExitPatcher.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | namespace ExitPatcher { 4 | bool PatchExit(); 5 | void ResetExitFunctions(); 6 | } 7 | -------------------------------------------------------------------------------- /ExitPatcher/ExitPatcher.vcxproj.user: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | true 5 | 6 | -------------------------------------------------------------------------------- /ExitPatcher/main.cpp: -------------------------------------------------------------------------------- 1 | //based on : https://www.mdsec.co.uk/2020/08/massaging-your-clr-preventing-environment-exit-in-in-process-net-assemblies/ 2 | #define WIN32_LEAN_AND_MEAN 3 | #include 4 | #include 5 | #include "patcher/ExitPatcher.hpp" 6 | 7 | int main() { 8 | if (ExitPatcher::PatchExit()) { 9 | MessageBoxA(NULL, "exit functions patched successfully!", "success", MB_OK); 10 | } else { 11 | MessageBoxA(NULL, "failed to patch exit functions!", "error", MB_OK); 12 | } 13 | MessageBoxA(NULL, "resetting rn!", "success", MB_OK); 14 | ExitPatcher::ResetExitFunctions(); 15 | return 0; 16 | } 17 | -------------------------------------------------------------------------------- /ExitPatcher.sln: -------------------------------------------------------------------------------- 1 | 2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio Version 17 4 | VisualStudioVersion = 17.14.36203.30 d17.14 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "ExitPatcher", "ExitPatcher\ExitPatcher.vcxproj", "{03BCDB38-5A8F-4CD6-A168-32679674E680}" 7 | EndProject 8 | Global 9 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 10 | Debug|x64 = Debug|x64 11 | Debug|x86 = Debug|x86 12 | Release|x64 = Release|x64 13 | Release|x86 = Release|x86 14 | EndGlobalSection 15 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 16 | {03BCDB38-5A8F-4CD6-A168-32679674E680}.Debug|x64.ActiveCfg = Debug|x64 17 | {03BCDB38-5A8F-4CD6-A168-32679674E680}.Debug|x64.Build.0 = Debug|x64 18 | {03BCDB38-5A8F-4CD6-A168-32679674E680}.Debug|x86.ActiveCfg = Debug|Win32 19 | {03BCDB38-5A8F-4CD6-A168-32679674E680}.Debug|x86.Build.0 = Debug|Win32 20 | {03BCDB38-5A8F-4CD6-A168-32679674E680}.Release|x64.ActiveCfg = Release|x64 21 | {03BCDB38-5A8F-4CD6-A168-32679674E680}.Release|x64.Build.0 = Release|x64 22 | {03BCDB38-5A8F-4CD6-A168-32679674E680}.Release|x86.ActiveCfg = Release|Win32 23 | {03BCDB38-5A8F-4CD6-A168-32679674E680}.Release|x86.Build.0 = Release|Win32 24 | EndGlobalSection 25 | GlobalSection(SolutionProperties) = preSolution 26 | HideSolutionNode = FALSE 27 | EndGlobalSection 28 | GlobalSection(ExtensibilityGlobals) = postSolution 29 | SolutionGuid = {21D71EE0-729C-4D0A-9EA6-C4F35AB43B55} 30 | EndGlobalSection 31 | EndGlobal 32 | -------------------------------------------------------------------------------- /ExitPatcher/ExitPatcher.vcxproj.filters: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | {4FC737F1-C7A5-4376-A066-2A32D752A2FF} 6 | cpp;c;cc;cxx;c++;cppm;ixx;def;odl;idl;hpj;bat;asm;asmx 7 | 8 | 9 | {93995380-89BD-4b04-88EB-625FBE52EBFB} 10 | h;hh;hpp;hxx;h++;hm;inl;inc;ipp;xsd 11 | 12 | 13 | {67DA6AB6-F800-4c08-8B7A-83BB121AAD01} 14 | rc;ico;cur;bmp;dlg;rc2;rct;bin;rgs;gif;jpg;jpeg;jpe;resx;tiff;tif;png;wav;mfcribbon-ms 15 | 16 | 17 | 18 | 19 | Source Files 20 | 21 | 22 | Source Files 23 | 24 | 25 | Source Files 26 | 27 | 28 | 29 | 30 | Header Files 31 | 32 | 33 | Header Files 34 | 35 | 36 | -------------------------------------------------------------------------------- /ExitPatcher/patcher/ExitPatcher.cpp: -------------------------------------------------------------------------------- 1 | #include "ExitPatcher.hpp" 2 | #include 3 | #include 4 | #include 5 | 6 | namespace ExitPatcher { 7 | struct ExitFunction { 8 | const wchar_t* module; 9 | const char* function; 10 | BYTE ORGBytes[19]; 11 | bool patched; 12 | }; 13 | 14 | static ExitFunction ExitFunctions[] = { 15 | { L"kernelbase.dll", "TerminateProcess", {0}, false }, 16 | { L"kernel32.dll", "TerminateProcess", {0}, false }, 17 | { L"kernelbase.dll", "ExitProcess", {0}, false }, 18 | { L"kernel32.dll", "ExitProcess", {0}, false }, 19 | { L"mscoree.dll", "CorExitProcess", {0}, false }, 20 | { L"ntdll.dll", "NtTerminateProcess", {0}, false }, 21 | { L"ntdll.dll", "RtlExitUserProcess", {0}, false } 22 | }; 23 | 24 | static const size_t EXIT_FUNCTION_COUNT = 7; 25 | static size_t gshcsize = 0; 26 | 27 | static PVOID GetFunctionAddress(const wchar_t* moduleName, const char* functionName) { 28 | return (PVOID)GetProcAddress(GetModuleHandleW(moduleName), functionName); 29 | } 30 | 31 | static bool ReadMemory(PVOID address, void* buffer, size_t size) { 32 | __try { 33 | memcpy(buffer, address, size); 34 | return true; 35 | } 36 | __except(EXCEPTION_EXECUTE_HANDLER) { 37 | return false; 38 | } 39 | } 40 | 41 | static bool WriteMemory(PVOID address, const void* data, size_t size) { 42 | DWORD oldProtect; 43 | if (!VirtualProtect(address, size, PAGE_EXECUTE_READWRITE, &oldProtect)) { 44 | return false; 45 | } 46 | 47 | __try { 48 | memcpy(address, data, size); 49 | VirtualProtect(address, size, oldProtect, &oldProtect); 50 | return true; 51 | } 52 | __except(EXCEPTION_EXECUTE_HANDLER) { 53 | VirtualProtect(address, size, oldProtect, &oldProtect); 54 | return false; 55 | } 56 | } 57 | 58 | bool PatchExit() { 59 | PVOID ExitThreadAddr = GetFunctionAddress(L"kernelbase.dll", "ExitThread"); 60 | if (!ExitThreadAddr) { 61 | ExitThreadAddr = GetFunctionAddress(L"kernel32.dll", "ExitThread"); 62 | } 63 | if (!ExitThreadAddr) return false; 64 | 65 | #ifdef _WIN64 66 | std::vector shc; 67 | shc.push_back(0x48); shc.push_back(0xC7); shc.push_back(0xC1); shc.push_back(0x00); shc.push_back(0x00); shc.push_back(0x00); shc.push_back(0x00); // MOV RCX, 0 68 | shc.push_back(0x48); shc.push_back(0xB8); 69 | uint64_t ExitThreadAddr64 = reinterpret_cast(ExitThreadAddr); 70 | BYTE* addrBytes = reinterpret_cast(&ExitThreadAddr64); 71 | shc.insert(shc.end(), addrBytes, addrBytes + 8); 72 | shc.push_back(0xFF); 73 | shc.push_back(0xE0); 74 | #else 75 | std::vector shc; 76 | shc.push_back(0xB9); shc.push_back(0x00); shc.push_back(0x00); shc.push_back(0x00); shc.push_back(0x00); 77 | shc.push_back(0xB8); 78 | uint32_t ExitThreadAddr32 = reinterpret_cast(ExitThreadAddr); 79 | BYTE* addrBytes = reinterpret_cast(&ExitThreadAddr32); 80 | shc.insert(shc.end(), addrBytes, addrBytes + 4); 81 | shc.push_back(0xFF); 82 | shc.push_back(0xE0); 83 | #endif 84 | 85 | gshcsize = shc.size(); 86 | 87 | for (size_t i = 0; i < EXIT_FUNCTION_COUNT; i++) { 88 | PVOID FCEAddr = GetFunctionAddress(ExitFunctions[i].module, ExitFunctions[i].function); 89 | if (!FCEAddr) continue; 90 | 91 | if (!ReadMemory(FCEAddr, ExitFunctions[i].ORGBytes, gshcsize)) { 92 | ResetExitFunctions(); 93 | return false; 94 | } 95 | 96 | if (!WriteMemory(FCEAddr, shc.data(), gshcsize)) { 97 | ResetExitFunctions(); 98 | return false; 99 | } 100 | 101 | ExitFunctions[i].patched = true; 102 | } 103 | 104 | return true; 105 | } 106 | 107 | void ResetExitFunctions() { 108 | if (gshcsize == 0) return; 109 | for (size_t i = 0; i < EXIT_FUNCTION_COUNT; i++) { 110 | if (!ExitFunctions[i].patched) continue; 111 | PVOID FCEAddr = GetFunctionAddress(ExitFunctions[i].module, ExitFunctions[i].function); 112 | if (FCEAddr) { 113 | WriteMemory(FCEAddr, ExitFunctions[i].ORGBytes, gshcsize); 114 | } 115 | ExitFunctions[i].patched = false; 116 | } 117 | } 118 | } 119 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ExitPatcher 2 | 3 | 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. 4 | 5 | Based on the technique described in [MDSec's article on preventing Environment.Exit in in-process .NET assemblies](https://www.mdsec.co.uk/2020/08/massaging-your-clr-preventing-environment-exit-in-in-process-net-assemblies/). 6 | 7 | ## Explanation 8 | 9 | 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. 10 | 11 | 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). 12 | 13 | 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()`. 14 | 15 | ## Target Functions 16 | 17 | The implementation patches the following exit functions across multiple DLLs: 18 | 19 | - `kernelbase.dll::TerminateProcess` 20 | - `kernel32.dll::TerminateProcess` 21 | - `kernelbase.dll::ExitProcess` 22 | - `kernel32.dll::ExitProcess` 23 | - `mscoree.dll::CorExitProcess` 24 | - `ntdll.dll::NtTerminateProcess` 25 | - `ntdll.dll::RtlExitUserProcess` 26 | 27 | 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. 28 | 29 | ## Shellcode Generation 30 | 31 | 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. 32 | 33 | ### x64 Implementation 34 | 35 | ```asm 36 | MOV RCX, 0 ; 48 C7 C1 00 00 00 00 37 | MOV RAX, ExitThreadAddr ; 48 B8 [8-byte address] 38 | JMP RAX ; FF E0 39 | ``` 40 | 41 | Total size: 17 bytes 42 | 43 | ### x86 Implementation 44 | 45 | ```asm 46 | MOV ECX, 0 ; B9 00 00 00 00 47 | MOV EAX, ExitThreadAddr ; B8 [4-byte address] 48 | JMP EAX ; FF E0 49 | ``` 50 | 51 | Total size: 12 bytes 52 | 53 | 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. 54 | 55 | ## Memory Protection and Patching 56 | 57 | The patching process uses Structured Exception Handling (SEH) to handle potential access violations during memory operations: 58 | 59 | 1. **ReadMemory**: Reads original function bytes into a buffer using `memcpy` wrapped in `__try/__except` to handle read violations gracefully. 60 | 61 | 2. **WriteMemory**: 62 | - Calls `VirtualProtect` to change memory protection from `PAGE_EXECUTE_READ` to `PAGE_EXECUTE_READWRITE` 63 | - Writes shellcode bytes using `memcpy` wrapped in SEH 64 | - Restores original memory protection flags via `VirtualProtect` 65 | - If any step fails, original protection is restored and the function returns false 66 | 67 | 3. **Restoration**: `ResetExitFunctions()` iterates through all patched functions and writes the original bytes back, effectively undoing all patches. 68 | 69 | ## Implementation Details 70 | 71 | The patching process follows this sequence: 72 | 73 | 1. Resolve `ExitThread` address from `kernelbase.dll` or `kernel32.dll` 74 | 2. Generate architecture-specific shellcode with embedded `ExitThread` address 75 | 3. For each target function: 76 | - Resolve function address via `GetModuleHandleW` and `GetProcAddress` 77 | - Read original function bytes (up to shellcode size, maximum 19 bytes) 78 | - Modify memory protection to `PAGE_EXECUTE_READWRITE` 79 | - Write shellcode bytes 80 | - Restore original memory protection 81 | - Mark function as patched 82 | 4. If any patch fails, `ResetExitFunctions()` is called to restore previously patched functions 83 | 84 | 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. 85 | 86 | ## Key Functions 87 | 88 | - `ExitPatcher::PatchExit()` - Patches all target exit functions with redirect shellcode. Returns `true` if all patches succeed, `false` otherwise. Automatically restores patches on failure. 89 | 90 | - `ExitPatcher::ResetExitFunctions()` - Restores original function bytes for all previously patched functions. Safe to call multiple times. 91 | 92 | - `GetFunctionAddress(moduleName, functionName)` - Internal helper that resolves function addresses using `GetModuleHandleW` and `GetProcAddress`. Returns `nullptr` if module or function not found. 93 | 94 | - `ReadMemory(address, buffer, size)` - Safely reads memory using SEH to handle access violations. Returns `true` on success. 95 | 96 | - `WriteMemory(address, data, size)` - Modifies memory protection, writes data, and restores protection. Uses SEH for error handling. Returns `true` on success. 97 | 98 | ## Disclaimer 99 | 100 | 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. 101 | 102 | ## License 103 | 104 | MIT 105 | 106 | ## Credits 107 | 108 | - MDSec for the original research on preventing Environment.Exit in in-process .NET assemblies 109 | - Based on techniques described in [Massaging your CLR: Preventing Environment.Exit in In-Process .NET Assemblies](https://www.mdsec.co.uk/2020/08/massaging-your-clr-preventing-environment-exit-in-in-process-net-assemblies/) 110 | 111 | -------------------------------------------------------------------------------- /ExitPatcher/ExitPatcher.vcxproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Debug 6 | Win32 7 | 8 | 9 | Release 10 | Win32 11 | 12 | 13 | Debug 14 | x64 15 | 16 | 17 | Release 18 | x64 19 | 20 | 21 | 22 | 17.0 23 | Win32Proj 24 | {03bcdb38-5a8f-4cd6-a168-32679674e680} 25 | ExitPatcher 26 | 10.0 27 | 28 | 29 | 30 | Application 31 | true 32 | v143 33 | Unicode 34 | 35 | 36 | Application 37 | false 38 | v143 39 | true 40 | Unicode 41 | 42 | 43 | Application 44 | true 45 | v143 46 | Unicode 47 | 48 | 49 | Application 50 | false 51 | v143 52 | true 53 | Unicode 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | Level3 76 | true 77 | WIN32;_DEBUG;_CONSOLE;%(PreprocessorDefinitions) 78 | true 79 | 80 | 81 | Console 82 | true 83 | 84 | 85 | 86 | 87 | Level3 88 | true 89 | true 90 | true 91 | WIN32;NDEBUG;_CONSOLE;%(PreprocessorDefinitions) 92 | true 93 | 94 | 95 | Console 96 | true 97 | 98 | 99 | 100 | 101 | Level3 102 | true 103 | _DEBUG;_CONSOLE;%(PreprocessorDefinitions) 104 | true 105 | 106 | 107 | Console 108 | true 109 | 110 | 111 | 112 | 113 | Level3 114 | true 115 | true 116 | true 117 | NDEBUG;_CONSOLE;%(PreprocessorDefinitions) 118 | true 119 | 120 | 121 | Console 122 | true 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | --------------------------------------------------------------------------------