EntryPoint Hijacking

Published by

on

The technique of EntryPoint Hijacking introduces a stealthier approach to code injection as it doesn’t use API calls that create a new thread within the context of a process, and it independent of the attack chain. Arbitrary code is written in memory, but it is executed only when a thread is created by the process legitimately. The technique enables threat actors to evade EDR defences and extend the dwell time in the affected environment.

Playbook

Windows processes dynamically load multiple modules (DLLs) into memory at runtime. Each module contains a DllMain() function that the operating system automatically invokes in response to process and thread creation or termination events. The Windows loader function (ntdll!Lrdp*) maintains a record of each DLL loaded, with its properties to manage these invocations, including the EntryPoint address. Sophisticated threat actors can overwrite the EntryPoint function of the targeted DLL to redirect the execution flow to attacker-controlled code whenever the loader function calls the DllMain(). However, hijacking the EntryPoint introduces challenges for threat actors, such as process stability issues, race conditions and crashes.

Kurosh Dabbagh Escalante released the EPI (EntryPoint Injection) proof of concept in 2023 and introduced a documented method to abuse the EntryPoint property of a DLL. EPI patches the EntryPoint of a loaded DLL (kernelbase.dll) and uses the QueueUserWorkItem from inside the redirected EntryPoint. The malicious code is executed on a thread-pool thread. Hugo Valette approached the same technique during x33fcon 2025, and released two proof-of-concepts examples called LdrShuffle, demonstrating EntryPoint Hijacking within the same and remote processes. It should be noted that LdrShuffle handles the execution differently, even though both proof of concepts hijack the same property.

Windows identifies the DllMain() of DLLs via the EntryPoint property. Purple team operators can identify the memory address of the EntryPoint by executing the following commands. In the example below, the kernelbase.dll was used.

lm m kernelbase
dt nt!_LDR_DATA_TABLE_ENTRY 0x7ffdc640bcb0
DLL EntryPoint

The code of the LdrShuffle has been analysed to understand the technique internals. The DontCallForThreads == 0 is a boolean configuration check that allows thread-related calls in the process. If the setting is set to 1, threading is blocked. It should be highlighted that more complex APIs such as the InternetOpenW will cause the process to crash due to thread-syncing issues (deadlocks). Therefore, APIs receiving C2 callbacks must run in a separate thread. The i>5 is used to skip from hijacking the first five DLLs of the process for stability reasons.

(pDte->EntryPoint != NULL && pDte->DontCallForThreads == 0 && i > 5)

The EntryPoint is restored promptly to prevent process crashes.

BOOL RestoreLdr(IN ULONG_PTR dllBase) {
	//PEB
#ifdef _WIN64
	PPEB pPeb = (PPEB)(__readgsqword(0x60));
#elif _WIN32
	PPEB pPeb = (PPEB)(__readgsqword(0x30));
#endif

	PDATA_T pDataT = NULL;
	PEB_LDR_DATA* pPebLdr = (PEB_LDR_DATA*)pPeb->pLdr;
	//First element of list:
	PLDR_DATA_TABLE_ENTRY2 pDte = (PLDR_DATA_TABLE_ENTRY2)((ULONG_PTR)pPebLdr->InMemoryOrderModuleList.Flink - 0x10);

	while (pDte) {
		if (pDte->BaseDllName.Length != NULL) {

			if (pDte->DllBase == (PVOID)dllBase) {
				pDataT = (PDATA_T)pDte->OriginalBase;

				// restore the two modified fields to their original values kept in PDATA_T struct
				pDte->EntryPoint = (PLDR_INIT_ROUTINE)pDataT->bakEntryPoint;
				pDte->OriginalBase = pDataT->bakOriginalBase;

				return TRUE;
			}
		}
		else {
			break;
		}

		//travelling to next pDte:
		pDte = *(PLDR_DATA_TABLE_ENTRY2*)(pDte);
	}
	return FALSE;
}

Execution and arguments are passed via a Runner(), a helper application that is invoked to conduct API calls. The data structure resides in the heap, a dynamically managed region where a process allocates and frees memory at runtime. The Runner() accesses this memory region to determine what to execute.

typedef struct _DATA_T {
    // LDR structures manipulation
    ULONG_PTR   runner;             // malicious entry point to execute
    ULONG_PTR   bakOriginalBase;    // backup of overwritten OriginalBase
    ULONG_PTR   bakEntryPoint;      // backup of overwritten EntryPoint
    HANDLE      event;              // event signalling that the Runner has executed
    // function call
    ULONG_PTR   ret;                // return value
    DWORD       createThread;       // run this API call in a new thread (required for wininet/winhttp)
    ULONG_PTR   function;           // Windows API to call
    DWORD       dwArgs;             // number of args
    ULONG_PTR   args[MAX_ARGS];     // array of args
} DATA_T, * PDATA_T;

The LdrShuffle proof of concept can be executed from a console with no arguments to conduct the process injecting within the same process.

LdrShuffle.exe
LdrShuffle – EntryPoint Hijacking

The second proof-of-concept released (part of the LdrShuffle repository) enables operators to target a remote process within the system and inject shellcode.

LdrInject.exe 3612 demon.x64.bin
LdrInject – Shellcode
LdrInject

The diagram below visualizes the technique internals:

EntryPoint Hijacking – Technique Internals

Similarly, the EPI proof of concept allocates a memory space and writes a loader that decrypts, allocates, and runs the shellcode reliably. The PEB of the target address is then patched, and execution occurs using the process thread pool, to prevent new threads point to the shellcode for evasion. It should be noted that the loader restores the PEB to its previous state to enable the process to continue its normal execution.

 epi.exe -p 6832
EntryPoint Injection – PoC

Similarly, the EntryPoint hijack is the iPurple version that emulates the technique. The tool lists all running processes and prompts the user to enter the target PID for injection.

EntryPoint Hijack – Remote Process Injection

The proof of concepts uses the NtQueryInformationProcess API to retrieve the PEB address of the target process and patches the EntryPoint of the kernelbase.dll.

EntryPoint Hijack
EntryPoint Hijacking – MessageBox

The diagram below visualizes the stages of EntryPoint Hijacking technique.

EntryPoint Hijacking – Diagram
[[Playbook.EntryPoint Hijacking]]
id = "1.0.0"
name = "1.0.0 - EntryPoint Hijacking"
description = "Inject code to the EntryPoint property of a legitimate DLL module"
tooling.name = "LdrInject, EPI"
tooling.references = [
    "https://github.com/RWXstoned/LdrShuffle"
    "https://github.com/Kudaes/EPI/"
]
executionSteps = [
    "LdrInject.exe <PID> <shellcode>.bin"
    "epi.exe -p <PID>"
]
executionRequirements = [
    "None"
]

Detection

Modification and restoration of the EntryPoint is a core behaviour of the EntryPoint Hijacking technique. However, since the EntryPoint is restored, a point-in-time memory scan by the EDR will only detect the technique if it occurs during the small hijack window. Effective detection requires combining integrity checks on PEB loader structures, detection of the runner stub in private memory, and telemetry on the write (WriteProcessMemory) primitive.

A reliable detection requires to compare the OriginalBase with DllBase so if OriginalBase != DllBase the entry point has been tampered.

LDR_DATA_TABLE_ENTRY.EntryPoint == DllBase + IMAGE_NT_HEADERS.OptionalHeader.AddressOfEntryPoint

The LdrShuffleDetect (a detection-based tool) runs continuously on the target and every 10 seconds all the processes are scanned. The tool takes a snapshot of all processes running on the host by using the CreateToolhelp32Snapshot API. The functions Process32FirstW and Process32NextW enumerate the list of processes.

 HANDLE hSnap = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);
    if (hSnap == INVALID_HANDLE_VALUE) return procs;

    PROCESSENTRY32W pe = {sizeof(pe)};
    if (Process32FirstW(hSnap, &pe)) {
        do {
            if (pe.th32ProcessID == 0 || pe.th32ProcessID == 4) continue;
            ProcEntry e;
            e.pid = pe.th32ProcessID;
            wcscpy_s(e.name, pe.szExeFile);
            procs.push_back(e);
        } while (Process32NextW(hSnap, &pe));
    }

A handle to the targeted process or processes is opened via the OpenProcess API to query process/processes information and read the memory of these processes via ReadProcessMemory API calls.

 HANDLE hTmp = OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION,
                                      FALSE, targetPid);
            if (hTmp) {
                DWORD sz = MAX_PATH;
                QueryFullProcessImageNameW(hTmp, 0, e.name, &sz);
                CloseHandle(hTmp);

A series of ReadProcessMemory calls is performed to read process memory starting from the PEB address, including the LDR_DATA_TABLE_ENTRY to obtain the Entrypoint address, and monitors for any changes. The tool can be executed from the command line and raises an alert when one or more of the three conditions are met:

  1. The memory address of the EntryPoint property is outside the range of the DllBase.
  2. The EntryPoint memory type is changed from MEM_IMAGE (0x1000000) to MEM_PRIVATE (0x20000) for shellcodes running in private heap.
  3. The OriginalBase is not valid.

The tool was executed against the two publicly available proof of concepts (LdrShuflle + EPI) and the private tool from iPurple EntryPointHijacking, and has successfully detected all EntryPoint hijacking attempts.

LdrShuffleDetect.exe
LdrShuffleDetect
EntryPoint Injection – Detection
EntryPoint Hijacking – Detection

The table below summarizes the conditions that trigger an alert during EntryPoint tampering.

ConditionActivityConfidence
1, 2, 3All three conditions failCritical
1, 2EP outside memory range & Private MemoryCritical
3OriginalBase is a heap pointerHigh
2EntryPoint is not in image memoryMedium
NoneAll checks passClean

Running LdrShuffleDetect on a schedule against high-value processes (lsass, browsers, office, EDR) that beacon externally offers an effective detection method for EntryPoint Hijacking.

Another indicator of EntryPoint Hijacking is to hunt for handles opened with GrantedAccess containing 0x143A and correlate this behaviour with outbound traffic from these processes. The access mask 0x143A grants the caller process permissions to read (PROCESS_VM_READ), write (PROCESS_VM_WRITE), and manipulate (PROCESS_VM_OPERATION) the target image process. The GrantedAccess field is contained under Sysmon Event ID 10.

Sysmon Event ID 10

The code injection technique of EntryPoint Hijacking is pushing the boundaries of in-memory detection capabilities. Endpoint Detection and Response technologies should no longer chase common APIs that are used in malware, but correlate multiple other behaviours to identify tampering of the EntryPoint property. Organizations that want to elevate their detection capabilities should include EntryPoint Hijacking in their purple team operations backlog, in order to investigate if their EDR deployment can detect this code injection technique effectively.

Alternatively, if the current implementation of the EDR technology lacks this capability, organizations should consider the replication of the detection proof-of-concept and deployment across multiple endpoints with log-forwarding to their SIEM for early threat detection. Detecting threats during initial access enables SOC teams to respond and isolate threats faster, reducing the blast radius of a breach.

Leave a comment