Basics: Processes

List running processes

First program: list every running process on the machine — exactly what Task Manager does. We use the Toolhelp32 API, which takes a snapshot of the system and lets you walk through it.

Read the whole thing first, then we'll go line by line.

use windows::Win32::Foundation::CloseHandle;
use windows::Win32::System::Diagnostics::ToolHelp::{
    CreateToolhelp32Snapshot, Process32First, Process32Next,
    PROCESSENTRY32, TH32CS_SNAPPROCESS,
};

fn main() {
    println!("{:<8} {}", "PID", "NAME");
    println!("{}", "-".repeat(40));

    unsafe {
        // 1. Take a snapshot of all processes right now
        let snapshot = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0)
            .expect("Failed to create snapshot");

        // 2. Create an empty PROCESSENTRY32 struct to hold each result
        //    dwSize MUST be set to the size of the struct — Windows requires it
        let mut entry = PROCESSENTRY32::default();
        entry.dwSize = std::mem::size_of::<PROCESSENTRY32>() as u32;

        // 3. Get the FIRST process in the snapshot
        if Process32First(snapshot, &mut entry).is_ok() {

            // 4. Loop: keep getting the NEXT process until there are none
            loop {
                // entry.szExeFile is the process name as raw bytes [i8; 260]
                // We convert it to a Rust String for printing
                let name = entry.szExeFile
                    .iter()
                    .take_while(|&&c| c != 0)   // stop at null terminator
                    .map(|&c| c as u8 as char)
                    .collect::<String>();

                println!("{:<8} {}", entry.th32ProcessID, name);

                // Move to next process; break if no more
                if Process32Next(snapshot, &mut entry).is_err() {
                    break;
                }
            }
        }

        // 5. Always close handles when done — resource leak otherwise
        let _ = CloseHandle(snapshot);
    }
}

entry.dwSize — don't forget this. It's a quirk of old Windows APIs. Before you pass a struct to Windows, you must set its dwSize field to the struct's size in bytes. Windows uses this to know which version of the struct you're using. If you forget → crash.

New Rust thing: structs. A struct is just a named group of variables. PROCESSENTRY32 is a struct with fields like th32ProcessID and szExeFile. You access them with a dot: entry.th32ProcessID. That's all you need to know for now.

🔴 RED TEAM — WHY THIS MATTERS

▸This is exactly how injection tools find their target. Before injecting into explorer.exe, they enumerate processes to find explorer.exe's PID — using this exact API.

▸th32ParentProcessID (PPID) is how you do PPID spoofing — make your malware appear to be a child of explorer.exe instead of cmd.exe, fooling process-tree-based detections.

▸CloseHandle at the end is critical. Handle leaks are detectable. A process with thousands of open handles is suspicious — EDRs flag it.

▸This code runs with NO special privileges. Any user can snapshot processes. The sensitive part comes next: opening a handle TO a specific process.


Open a Handle to a Process

Listing processes is read-only. Now we go further: open a real handle to a process and read information from it. This is Step 1 of process injection. We'll combine both skills: first enumerate to find a PID by name, then open a handle to that process.

New Rust: Option<T>. A function that might not return a value returns Option. It's either Some(value) if it found something, or None if it didn't. The match statement handles both cases — like an if/else but for values. No null pointer crashes possible — Rust won't let you ignore None.

New Rust: functions. fn find_pid(target_name: &str) -> Option<u32> means: "a function named find_pid that takes a string reference and returns an Option containing a u32 (unsigned 32-bit integer = a PID)." The last expression without a semicolon is the return value.

for lsass:

🔴 RED TEAM — WHY THIS MATTERS

▸This find_pid() function is the real first step of every injector. Copy it — you'll reuse it in every tool you write in Phase 2.

▸PROCESS_VM_READ is the right that lets you call ReadProcessMemory — the function Mimikatz uses to suck credentials out of lsass. You now have that handle. Reading the memory is one more function call away.

▸Requesting PROCESS_ALL_ACCESS on lsass = immediate EDR alert. Requesting only PROCESS_QUERY_LIMITED = much quieter. Access right minimization is a real evasion technique.

▸The Err(e) branch for lsass.exe is exactly the wall you'll break through in Phase 2 via privilege escalation — getting SeDebugPrivilege lets OpenProcess succeed on protected processes.

Summary

what next:

rust concepts:

You are two lessons away from writing a working process injector in Rust. The only things between you and that are: virtual memory allocation (VirtualAllocEx) and writing memory (WriteProcessMemory). Both follow the exact same pattern you just learned.

HW

🔴 RED TEAM — WHY THIS MATTERS

▸The homework exercises #3 and #4 are real skills: enumerating all instances of a process, and probing which processes your current privilege level can access.

▸Printing parent PIDs (exercise 2) is how you'd detect PPID spoofing — if cmd.exe claims its parent is explorer.exe but was launched from a Word macro, something is wrong.

▸Every tool you'll build in Phase 2 starts with find_pid(). Get comfortable with it — it's the foundation.

  • 1: we added these lnes:

  • 2: we updated the function to store and return the vector filled with pid

Virtual Memory

Last lesson you built find_pid() and open_and_inspect(). You can find any process by name and hold a live handle to it. That's Step 1 of injection done. Here's the full injection chain. You've done the first box. This lesson covers the next two.

By the end of this lesson you will have written bytes into another process's memory from Rust. That is the core primitive of every injection technique — everything else is a variation on this.

VirtualAllocEx — Carving Space

Every process has its own private virtual address space. Your process cannot just write into another process's memory — there's no shared pointer between them. You first need to ask Windows to create a new empty region inside the target, then get back the address of that region. That's what VirtualAllocEx does.

[Q] So if there's a shared pointer b/w them can we write to another process in it's memory?

The function signature — what you hand to it:

NOTE: Why PAGE_READWRITE and not PAGE_EXECUTE_READWRITE? Allocating RWX memory is the loudest possible signal to EDR tools — it's flagged immediately. The stealthy approach: allocate RW, write the shellcode, then flip to RX with VirtualProtectEx. You'll learn that in Phase 3. For now we use RW to understand the mechanism first. VirtualAlloc vs VirtualAllocEx. Without the "Ex", VirtualAlloc allocates in your own process. The "Ex" means "Extended" — it takes a process handle so you can allocate in any process you have a handle to.

🔴 RED TEAM — WHY THIS MATTERS

▸The address VirtualAllocEx returns is your write target. You hand it directly to WriteProcessMemory in the next step — it's the bridge between both calls.

▸PAGE_READWRITE → VirtualProtectEx(PAGE_EXECUTE_READ) is called the RW→RX technique. It splits the 'write' and 'execute' permissions across time, defeating many static detections.

▸MEM_COMMIT | MEM_RESERVE together is the standard combo. MEM_RESERVE alone just reserves the address range without backing it — a trick used to create large fake memory gaps that confuse heap scanners.

WriteProcessMemory — Filling the Space

You have a handle. You have an address inside the target. Now WriteProcessMemory is the function that actually copies bytes from your process into that address. It crosses the process boundary — your bytes land in their memory.

Think of it exactly like memcpy — but across a process boundary. Windows handles the page table translation internally. You just say: "copy these bytes to that address in that process." 🔑The handle needs two rights for this to work: PROCESS_VM_WRITE (to write) AND PROCESS_VM_OPERATION (to operate on the virtual memory). Both are required. If you open with only VM_WRITE, the call fails. Update your OpenProcess rights.

[IDEA] [Q] See if the process that copies the bytes to another becomes suspicious , EDR catches it , what if we make a kamikaze process which is meant to die , which spawns another process which copies the memory which is legitimate so EDR won't it , masking our payloads. Better: We make the process sleep also , or let it do some random maths functionality which can take its eyes off from EDR. But what about pattern based detections: PDR which kaspersky is notoriously uses: Process Dynamic detections which say if virtualalloc -> virtualprotect -> rw / rx -> execute = red flag

🔴 RED TEAM — WHY THIS MATTERS

▸WriteProcessMemory is one of the most watched Win32 calls. EDRs log: which process called it, what was the source, and what was the destination address. That's why advanced techniques avoid it entirely using mapped views (NtMapViewOfSection) — same result, no WriteProcessMemory call.

▸The 0x90 bytes (NOP instructions) you'll write are not random — a NOP sled gives shellcode a soft landing. If execution lands anywhere in the NOP sled, it slides down to the real payload.

▸0xCC is INT3 — a software breakpoint. When the CPU hits it, it pauses execution and raises an exception. Writing 0xCC as a payload is safe for testing — nothing harmful happens, but you'll see the crash in the target process confirming your bytes executed.

Rust Concepts: Raw Pointers

One new Rust concept before the code. Both VirtualAllocEx and WriteProcessMemory deal in raw pointers — memory addresses as plain numbers. Rust has them, but keeps them inside unsafe because it can't guarantee they're valid. 🦀 RUST — RAW POINTERS — MUT T AND CONST T Rust has two kinds of raw pointer: const T → read-only pointer to type T (like C's const T) mut T → read-write pointer to type T (like C's T) You get them with 'as *mut T' casts, or from Windows API return values.

The key insight: raw pointers are just numbers (memory addresses). 'as mut c_void' just tells the compiler "treat this number as a void pointer". c_void = C's void type = "I don't care what type is at this address". Windows APIs use void everywhere because they were designed in C.

🦀 RUST — OPTION AND NULL POINTERS VirtualAllocEx returns Option<*mut c_void>. Some(ptr) = allocation succeeded, ptr is the address None = allocation failed (not enough memory, access denied, etc.) You unwrap it with match or .expect() — same as you did above.

Program : Toml File:

🦀New Rust: the | operator on flags. Windows access rights are bit flags — each right is a single bit in a number. PROCESS_VM_WRITE | PROCESS_VM_OPERATION uses bitwise OR to combine them into one number. The windows crate defines each constant as a struct that implements the | operator, so it looks and works exactly like C.

🔴 RED TEAM — WHY THIS MATTERS

▸You just did what every injector does for steps 1–3. The only difference in a real tool is the payload contains actual shellcode instead of NOPs + INT3.

▸remote_addr is the most important value in the whole program — it's the bridge. VirtualAllocEx gives it to you, WriteProcessMemory uses it, CreateRemoteThread (next lesson) executes from it.

▸VirtualFreeEx at the end is intentional cleanup. Real implants DON'T free — but they DO close the handle. An open handle to a target process sitting in your process's handle table is a forensic artifact.

▸Try running without Administrator — it will fail at OpenProcess. This is the exact wall SeDebugPrivilege breaks through.

Homework — 3 exercises:

  1. CHANGE THE PAYLOAD SIZE Allocate 1024 bytes (1KB) instead of 12. Fill the whole Vec with 0x90 (NOP sled). → What changes in the output? What stays the same? Hint: vec![0x90u8; 1024] ← fills 1024 bytes all with 0x90

  2. VERIFY THE WRITE (reading back) After WriteProcessMemory succeeds, use ReadProcessMemory to read back from remote_addr and print those bytes. Does what you read match what you wrote? (You already have PROCESS_VM_READ in your rights — use it)

    Signature to look up: ReadProcessMemory(handle, remote_addr, buf_ptr, size, bytes_read)

  3. UNDERSTAND THE FAILURE Remove PROCESS_VM_OPERATION from the rights in OpenProcess. Run again. What error does WriteProcessMemory give? Why does removing one flag break everything? (This teaches you exactly what each right does)

🔴 RED TEAM — WHY THIS MATTERS

▸Homework #2 (ReadProcessMemory) is actually a key forensics skill — blue teams verify suspicious memory regions exactly this way. You're learning both sides at once.

▸Homework #3 shows you that access rights are not optional extras — each one unlocks a specific kernel operation. Missing one = specific error code. Memorize which right does what.

▸The 1024-byte NOP sled in exercise #1 is a classic shellcode staging pattern. Real shellcode payloads are typically 200–500 bytes. A 4096-byte allocation is standard to give padding room.

HW:

Thread

You've put bytes into the target's memory. Right now they're just data — the CPU doesn't know they exist. A thread is what makes the CPU actually execute them.

Every thread has one job: execute instructions starting from a given address and keep going until it returns or is killed. CreateRemoteThread creates a new thread inside another process, starting at an address you specify. You specify remote_addr — the address VirtualAllocEx gave you. The thread wakes up, and your payload runs.

The crash from 0xCC (INT3) is expected and intentional. It proves execution reached your payload. In a real implant, the payload is shellcode that does something useful — opens a reverse shell, loads a DLL, etc. The mechanism is identical.

🔴 RED TEAM — WHY THIS MATTERS

▸CreateRemoteThread is the oldest injection technique (1996). It's so well known that every EDR flags it instantly on sensitive targets like explorer.exe or svchost.exe.

▸But it still works on less-monitored processes, and understanding it is required before learning the stealthier variants: QueueUserAPC, NtCreateThreadEx, Thread Hijacking. All of them do the same job — make foreign code run.

▸The INT3 crash you'll see IS the confirmation. In a CTF or engagement, seeing a target crash after injection means your shellcode landed and executed. Then you swap 0xCC for real shellcode.

Function:

The only tricky part is the start address. Windows expects a function pointer — specifically a LPTHREAD_START_ROUTINE. Your remote_addr is a *mut c_void. You cast it:

⚠️transmute is powerful and dangerous. It reinterprets raw bytes as any type you ask for — no checks. We use it here because there's no safe cast between *mut c_void and a function pointer. It's valid as long as remote_addr actually contains executable code at runtime.

One more thing: after you create the thread, you should call WaitForSingleObject(thread_handle, INFINITE)to wait for it to finish before cleaning up. Otherwise your process might free the memory while the thread is still running it — instant crash.

🔴 RED TEAM — WHY THIS MATTERS

▸LPTHREAD_START_ROUTINE is why shellcode must follow the Windows thread function calling convention — it's called like a function. Real shellcode starts with a proper prologue for this reason.

▸Creation flag 0x4 (CREATE_SUSPENDED) creates the thread paused. Used in more advanced techniques — create suspended, modify the thread context (registers), then resume. That's thread hijacking.

▸The thread handle returned by CreateRemoteThread is itself a forensic artifact. Real implants close it immediately after WaitForSingleObject returns.

Basics:

Last updated