Code Injection and Hooking
Code Injection and Hooking
In this chapter, you will learn how malicious programs inject code into
another process (called target process or remote process) to perform
malicious actions.
1. Virtual Memory
• When you double-click a program containing a sequence of instructions, a process is created.
• The Windows operating system provides each new process created with its own private memory address space (called
the process memory).
• The process memory is a part of virtual memory; virtual memory is not real memory, but an illusion created by the
operating system's memory manager.
• It is because of this illusion that each process thinks that it has its own private memory space.
• During runtime, the Windows memory manager, with the help of hardware, translates the virtual address into the physical
address (in RAM) where the actual data resides; to manage the memory, it pages some of the memory to the disk.
• When the process's thread accesses the virtual address that is paged to the disk, the memory manager loads it from the disk
back to the memory.
The following diagram illustrates two processes, A and B, whose process memories are mapped to
the physical memory while some parts are paged to the disk
• Virtual memory is segregated into process memory (process space or user space) and kernel memory
(kernel space or system space). The size of the virtual memory address space depends on the hardware
platform.
• For example, on a 32-bit architecture, by default, the total virtual address space (for both process and kernel
memory) is a maximum of 4 GB. The lower half (lower 2 GB), ranging from 0x00000000 through
0x7FFFFFFF, is reserved for user processes (process memory or user space), and the upper half of the
address (upper 2 GB), ranging from 0x80000000 through 0xFFFFFFFF, is reserved for kernel memory
(kernel space).
• On a 32-bit system, out of the 4 GB virtual address space, each process thinks that it has 2 GB of process
memory, ranging from 0x00000000 - 0x7FFFFFFF. Since each process thinks that it has its own private
virtual address space (which ultimately gets mapped to physical memory), the total virtual address gets much
larger than the available physical memory (RAM).
The following diagram shows the memory layout of 32-bit architecture .
• You may notice a 64 KB gap between the user and kernel space; this region is not accessible and ensures
that the kernel does not accidentally cross the boundary and corrupt the user-space.
Even though the virtual address range is the same for each process (x00000000 0x7FFFFFFF), both the
hardware and Windows make sure that the physical addresses mapped to this range are different for each process.
1.1 Process Memory Components (User Space)
• Process memory is the memory used by user applications. The following screenshot shows two processes
and gives a high-level overview of the components which reside in the process memory.
• In the following screenshot, the kernel space is deliberately left blank for simplicity (we will fill in that blank
in the next section). Keep in mind that processes share the same kernel space:
Process memory consists of the following major components:
• Process executable: This region contains the executable associated with the application. When a program on the disk is
double-clicked, a process is created, and the executable associated with the program is loaded into the process memory.
• Dynamic Linked Libraries (DLLs): When a process is created, all its associated DLLs get loaded into the process memory.
This region represents all DLLs associated with a process.
• Process environment variables: This memory region stores the process's environment variables, such as the temporary
directories, home directory, AppData directory, and so on.
• Process heap(s): This region specifies the process heap. Each process has a single heap and can create additional heaps as
required. This region specifies the dynamic input that the process receives.
• Thread stack(s): This region represents the dedicated range of process memory allocated to each thread, called its runtime
stack. Each thread gets its own stack, and this is where function arguments, local variables, and return addresses can be found.
• Process Environment Block (PEB): This region represents the PEB structure, which contains information about where the
executable is loaded, its full path on the disk, and where to find the DLLs in memory.
• You can examine the contents of a process memory by using the Process Hacker (https://
processhacker.sourceforge.io/) tool. To do that, launch Process Hacker, right-click on the desired process,
• The following screenshot shows the user-space and kernel space components. In this section, we will mainly
focus on the kernel space components.
The kernel memory consists of the following key components:
• hal.dll: The hardware abstraction layer (HAL) is implemented in the loadable kernel module hal.dll. HAL
isolates the operating system from the hardware; It primarily provides services to the Windows executive,
kernel, and kernel mode device drivers. The kernel mode device drivers invoke functions exposed by hal.dll
to interact with the hard.ware, instead of directly communicating with the hardware.
• Ntoskrnl.exe is a system process, and it's also known as the “Windows NT Operating System Kernel
Executable”. It's related to a kernel, which is a piece of software that connects hardware and
software. The ntoskrnl.exe binary provides two types of functionality: the executive and the kernel. The
executive implements major operating system components, such as the memory manager, I/O manager,
object manager, process/thread manager, and so on. The kernel implements low-level operating system
services and exposes sets of routines, which are built upon by the executive to provide high-level services.
• Win32K.sys: This kernel mode driver implements UI and graphics device interface (GDI) services,
which are used to render graphics on output devices (such as monitors). It exposes functions for GUI
applications
2. User Mode And Kernel Mode
• In the previous section, we saw how virtual memory is divided into user-space (process memory) and
kernel space (kernel memory).
• The user-space contains code (such as executable and DLL) that runs with restricted access, known as the
user mode. In other words, the executable or DLL code that runs in the user space cannot access anything in
the kernel space or directly interact with the hardware.
• The kernel space contains the kernel itself (ntoskrnl.exe) and the device drivers. The code running in the
kernel space executes with a high privilege, known as kernel mode, and it can access both the user-space and
the kernel space. By providing the kernel with a high privilege level, the operating system ensures that a user-
mode application cannot cause system instability by accessing protected memory or I/O ports.
3. Code Injection Techniques
Code injection is a common technique used in cybersecurity attacks, where
malicious code is inserted into a legitimate program or application, often to
exploit vulnerabilities and gain unauthorized access or control over a system.
3. Code Injection Techniques
Code injection techniques provide many benefits for attackers; once the code is injected into the remote
process, an adversary can do the following things:
The objective of a code injection technique is to inject code into the remote process memory and execute
the injected code within the context of a remote process. The injected code could be a module such as an
executable, DLL, or even shellcode
1. Force the remote process to execute the injected code to perform malicious actions (such as
downloading additional files or stealing keystrokes).
2. Injecting code into an already running process allows an adversary to achieve persistence.
3.Injecting code into trusted processes allows an attacker to bypass security products (such as
whitelisting software) and hide from the user.
4.Inject a malicious module (such as a DLL) and redirect the API call made by the remote process to a
malicious function in the injected module.
Shell code is a small piece of
executable code to exploit
vulnerabilities in a system
In the following code injection techniques, there is a malware process (launcher or loader) that injects code,
and a legitimate process (such as explorer.exe) into which the code will be injected. Before performing code
injection, the launcher needs to first identify the process to inject the code.
This is typically done by enumerating the processes running on the system; it uses three API calls:
CreateToolhelp32Snapshot(), Process32First(), and Process32Next().
The Process32First() and Process32Next() APIs get information about the process, such as the executable name, the
process ID, and the parent process ID; this information can be used by malware to determine whether it is the target
process or not.
3.1 Remote DLL Injection
DLL injection is a technique for executing code within the address space of another process by forcing a
dynamic link library to load.
How Does DLL Injection Work?
The DLL file to be injected into a process is placed on the target system.
Space is reserved on the target process’s memory to specify the path to the DLL file.
The directory path where the DLL file is located is copied to the process memory and the appropriate
memory addresses are determined.
The DLL file is run and starts executing its processes through the injected process.
3.2 DLL Injection Using APC (APC Injection)
• The APC injection technique is similar to remote DLL injection, but instead of using
CreateRemoteThread(), a malware makes use of Asynchronous Procedure Calls (APCs) to force
the thread of a target process to load the malicious DLL.
• DLL Injection using Asynchronous Procedure Call (APC Injection) involves several steps to inject
a dynamic-link library (DLL) into a target process. Here are the steps involved:
1.Locate Target Process: Identify the target process into which you want to inject the DLL.
This process should have the necessary permissions for injection.
2.Locate Target Thread: Choose a thread within the target process where you will queue
the APC. Typically, this thread should be in a state where it is about to enter an alertable
state (e.g., waiting for an event, semaphore, or mutex).
4.Write DLL Path to Allocated Memory: Write the full path of the DLL to be injected into the
allocated memory space within the target process.
5.Queue APC to Target Thread:Use functions like QueueUserAPC to queue an APC to the
selected thread within the target process. The APC function should point to a function within the
target process that will load the DLL.
6.Execute APC: Trigger the execution of the queued APC. This will cause the target thread to
execute the designated function, which loads the DLL into the target processes address space
7.DLL Loaded into Target Process: Once the APC is executed, the designated function within the
target process loads the DLL into the process's address space.
8.Clean Up Resources: Free any allocated memory and close handles to the target process and
thread if necessary.
3.3 DLL Injection Using SetWindowsHookEx()
The attacker initiates the process by loading the DLL into memory, then proceeds to create a remote thread
within the target process to execute code that loads the DLL. Additionally, a hook is set using
SetWindowsHookEx() to trigger the execution of code within the injected DLL. Once the hook is triggered, the
DLL's code is executed within the context of the target process.
3.4 Hollow Process Injection (Process Hollowing)
It involves creating a suspended process in a target system and replacing its memory with the code and
data of another process, typically malicious, without triggering security alerts.
Description:
1. Create Suspended Process: Start by creating a new process in a suspended state using functions like
CreateProcess(). This process will serve as the host for the injected code.
2. Allocate Memory in the Suspended Process: Allocate memory within the suspended process's address
space using functions like VirtualAllocEx(). This memory region will store the code and data of the process
to be injected.
3. Write Code and Data to Allocated Memory:Write the code and data of the process to be injected into the
allocated memory space using functions like WriteProcessMemory().
4. Set Thread Context: Use functions like GetThreadContext() and SetThreadContext() to modify the context
of the suspended process's main thread. This involves setting the instruction pointer (EIP/RIP) to point to
the entry point of the injected code.
5. Resume Execution: Finally, resume the execution of the suspended process using functions like
ResumeThread(). This allows the injected code to run within the context of the target process.