Code injection refers to the act of injecting arbitrary external code in an application. There are two types of code injection:
- Injection into vulnerable programs.
- Injection into non-vulnerable programs.
If code injection is done in vulnerable applications, it is done via exploitation of a bug which occurs when processing invalid data. In this case, the extent of code injection is dependent on the bug in the application, which we also refer to as “vulnerability”. The problem with this scenario is that the application should have a bug that can be leveraged to gain code execution.
This series targets the second scenario, where the application is not expected/required to have vulnerability. This, as can be expected, gives a very wide area of application as well as opportunity of abusing this functionality provided by the operating system.
Malwares typically use this method as it does not require any conditions to be met before performing code injection as well as it is fully supported by the operating system itself. All major operating systems provide ABIs and APIs to interact with other process, control them, read and write memory in the process space of other programs.
Code Injection via CreateRemoteThread API
CreateRemoteThread is a function provided by Win32 API for creating threads in another processes. There are two conditions that must be met before creating the thread in another application, and they are:
- The process attempting to create a thread in another process must have permissions to create thread. In simpler terms, this translates to having the same or higher privileges than the victim process (target refers to the process in which we want to create a thread).
- Both the processes (target and attacker processes) must reside in the same session. If the session identifiers do not match, thread will not be created.
In case either of the above-mentioned conditions are not met, the process of code injection will be denied by the operating system itself.
This is not a security hole in the architecture of the Windows OS, rather it is provided functionality by the operating system. Since we cannot modify the processes having higher privileges than we are having, no security boundary is crossed.
In order to achieve code injection via CreateRemoteThread API, we follow this algorithm:
- Select a process as the victim process.
- Obtain access to process using OpenProcess function with parameter PROCESS_ALL_ACCESS to be able to perform required operations.
- If OpenProcess fails, exit.
- Allocate memory in the process space of victim process by using VirtualAllocEx function.
- If VirtualAllocEx fails, exit.
- Write the shellcode to the memory location allocated by VirtualAllocEx.
- If shellcode writing fails or shellcode written partially, exit.
- After the shellcode has been written, call CreateRemoteThread with necessary parameters, pointing the address of shellcode as the LPTHREAD_START_ROUTINE.
For the purpose of this series, I have the following environment setup:
- Visual Studio 2019
- Windows 10 RS6 (x64)
Note: The code can be accessed from my github repository – here.
In-depth working of code injection
Accessing Remote Process
To perform memory manipulation on any process, we must have access to it. It can be obtained by using the function OpenProcess which is prototyped as:
It takes three parameters:
- dwDesiredAccess: The requested access to the process object. It is checked against the security token of the victim process. There are handful of desired access values that one can specify but specifying PROCESS_ALL_ACCESS encompasses all the possible access rights for the process.
- bInheritHandle: It is a Boolean value which dictates whether the process created by this process will inherit this handle or not.
- dwProcessId: This is the process identifier of the victim process.
This process returns a HANDLE to other process (on success) which can be used by other API functions to manipulate the memory of the victim process. On failure, it returns NULL.
Allocating space for shellcode
Once we obtain the handle to the victim process, we move on to allocating space for our shellcode in the victim process memory. This is done by using VirtualAllocEx API call.
VirtualAllocEx is prototyped as:
It takes five parameters:
- hProcess: Handle to the process inside which we want to allocate memory.
- lpAddress: A pointer to the specified address in the victim process memory. If the parameter is specified as NULL, then the function automatically selects a memory page to allocate to.
- dwSize: The size of the memory region to be allocated. It is specified in bytes.
- flAllocationType: It specifies the type of memory to be allocated. The parameter must contain one of the three values: MEM_COMMIT, MEM_RESERVE, MEM_RESET, MEM_RESET_UNDO.
- flProtect: It specifies the memory protection of the allocation. For our purposes, since it will contain our code to be executed, and we want it readable as well writable, we will set this to PAGE_EXECUTE_READWRITE.
The function returns the base address of allocation on success, while on failure, it returns NULL.
At this point, we have successfully managed to allocate executable memory in the victim process.
Writing shellcode in remote process
Now, we need to write our shellcode in the allocated region. To achieve this, we have a function called WriteProcessMemory.
WriteProcessMemory is a function that writes data to an area of memory of the specified process by the caller. A caveat to this is that the entire memory area must be writable else the function fails, and that is why we allocated the memory as writable, in conjunction with readable and executable.
WriteProcessMemory is prototyped as:
WriteProcessMemory takes five parameters:
- hProcess: Handle to the process inside which we want to write data to.
- lpBaseAddress: The address (in form of pointer) where we want to write our data at.
- lpBuffer: Pointer to data that must be written. The pointer must be a const pointer.
- nSize: The amount of data that must be written (in bytes).
- *lpNumberOfBytesWritten: A pointer to a SIZE_T which will store the number of bytes written in that target.
It will return false if the function fails due to some reason and will return a true if it succeeds.
At this point, stage is all set and all that is required is to create a thread in the remote process and run it.
Executing our shellcode
For creating a thread in a remote process, we use the function CreateRemoteThread provided by Win32 API.
CreateRemoteThread is a function that is used to create a thread that runs in the virtual space of another process. CreateRemoteThread provides limited functionality and to gain access to extended attributes which can be specified to threads, CreateRemoteThreadEx can be used but in our case, the former will suffice.
CreateRemoteThread is prototyped as:
CreateRemoteThread takes seven parameters out of which only three are of interest to us. The rest can have default values though by tweaking them, more control can be taken on the newly created thread.
The parameters of our interest are:
- hProcess: The handle to the victim process inside which we want to create the thread.
- lpStartAddress: It is a pointer to THREAD_START_ROUTINE which is the location from which the thread will start executing code once created.
- lpParameters: A pointer to the parameters expected by LPTHREAD_START_ROUTINE. Since in this case, it is a plain shellcode, it does not expect any arguments and therefore, we will keep it NULL. This parameter will be of value in DLL Injection.
For the demo, I am using a shellcode to launch calc.exe (generated using msfvenom), on Windows 10 RS6 (x64).
Code injection using CreateRemoteThread has several advantages over other code injection techniques. Some of them are:
- Easiest to implement.
- Does not require address resolution of library functions.
- Does not wait for events.
- No need of manipulating file structure in memory.
Along with advantages, there are several disadvantages which does not make it a go-to code injection technique. Some of them are:
- Only binary code can be injected.
- Requires a lot of changes when the shellcode is changed.
- There is a limit on how much you can achieve using a shellcode.