IR Payload Encryption

August 3, 2025 · View on GitHub

LLVM IR in memory can be susceptible to reverse engineering or detection, especially when used in advanced payload or emulator scenarios. Encrypting the IR payload before runtime helps in reducing this attack surface. This example demonstrates encrypting LLVM IR using AES, embedding it as a C array, and decrypting it in-memory during JIT execution using ORCJIT. This provides another layer of stealth during LLVM IR-based code execution.

Execution Flow

Here's an explanation of how the AES-encrypted IR is loaded and executed using LLVM's ORCJIT engine in main.cpp.

  1. Define the Encrypted IR and AES Key: Before compiling, the IR is encrypted using a Python script, and the output is embedded into the C++ source as a byte array.

    char AESkey[] = { 0xcc, 0xcc };
    unsigned char AESIR[] = { 0xcc, 0xcc };
    unsigned int AESIR_len = sizeof(AESIR);
    
  2. AES Decryption Function: The encrypted IR is decrypted in memory using Windows CryptoAPI (CryptAcquireContext, CryptDeriveKey, CryptDecrypt, etc.). The decrypted output is returned as a byte buffer.

    std::vector<char> DecryptAESBuffer(const BYTE* encrypted, DWORD len, const char* key, DWORD keyLen);
    
  3. LLVM Initialization: Standard LLVM initialization steps are done to prepare the JIT engine.

    InitLLVM X(argc, argv);
    InitializeNativeTarget();
    InitializeNativeTargetAsmPrinter();
    InitializeNativeTargetAsmParser();
    
  4. Shared Library Support (Optional): The program supports loading external DLLs if needed for the IR’s execution.

    if (sys::DynamicLibrary::LoadLibraryPermanently(sharedLibPath.c_str())) {
        // Error
    }
    
  5. In-Memory IR Parsing: The decrypted buffer is wrapped into an LLVM MemoryBuffer, which is then parsed into a Module.

    auto memBuf = MemoryBuffer::getMemBufferCopy(...);
    auto mod = parseIR(memBuf->getMemBufferRef(), err, *ctx);
    
  6. ThreadSafeModule Setup: The parsed IR module is then wrapped into a ThreadSafeModule, required for thread-safe JIT execution.

    auto tsm = ThreadSafeModule(std::move(mod), std::move(ctx));
    
  7. Create the JIT Engine: A new LLJIT instance is created using the builder pattern. This handles compilation and symbol resolution internally.

    auto jitOrErr = LLJITBuilder().create();
    auto jit = std::move(*jitOrErr);
    
  8. Load Process Symbols: The host process symbols are loaded to allow linkage with libc or Windows system functions if needed.

    sys::DynamicLibrary::LoadLibraryPermanently(nullptr);
    
  9. Add the Decrypted Module to JIT: Once prepared, the IR module is added to the JIT engine for compilation and execution.

    jit->addIRModule(std::move(tsm));
    
  10. Lookup and Execute main: The main symbol is looked up and cast into a native function pointer, then executed with arguments passed from the command line.

    auto mainSym = jit->lookup("main");
    auto mainFn = mainSym->toPtr<MainFuncType>();
    int result = mainFn(argc, argv);
    

Build Steps

A supporting Python script called encryptIR.py can encrypt a .ll file using AES and emit C-style arrays for embedding.

IRvana\Maldev\IREncryption> python encryptIR.py input.ll

This will output:

char AESkey[] = { ... };
unsigned char AESIR[] = { ... };
unsigned int AESIR_len = sizeof(AESIR);

Copy and paste this into your main.cpp to replace the existing stub. Use cmake to generate and build the Visual Studio project after statically embedding generated IR in main.cpp.

## Setup project
IRvana\Maldev\IREncryption> mkdir build
IRvana\Maldev\IREncryption> cd build
IRvana\Maldev\IREncryption\build> cmake -G "Visual Studio 17 2022" -A x64 ..

## Compile (Release only)
IRvana\Maldev\IREncryption\build> cmake --build . --config Release

Test Execution

## Standard execution
IRvana\Maldev\IREncryption\build\Release> ORCJITExec.exe

## With optional DLL injection
IRvana\Maldev\IREncryption\build\Release> ORCJITExec.exe --load=user32.dll