├── 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 |
--------------------------------------------------------------------------------