I am having troubles launching process with WindowsAPI

Hello everyone, this is my first post here.
I am having trouble spawning a windows process from the API. I want to spawn a process, insert my own bytes into the process and then restart it so my original bytes are ran. I tried with .exe and .bin file. Its a simple dialogue box and i am 100% sure the file is valid and runnable.

this is the error:

error: process didn't exit successfully: target\debug\windows_rust_bytes.exe (exit code: 0xc0000005, STATUS_ACCESS_VIOLATION)

Process finished with exit code -1073741819 (0xC0000005)

and this is my code:

use std::ffi::OsStr;
use std::io::Read;
use std::mem;
use std::os::windows::ffi::OsStrExt;
use std::ptr;

use windows::core::{PCWSTR, PWSTR};
use windows::Win32::Foundation::{BOOL, CloseHandle, GetLastError};
use windows::Win32::System::Diagnostics::Debug::WriteProcessMemory;
use windows::Win32::System::Memory::{MEM_COMMIT, MEM_RELEASE, MEM_RESERVE, PAGE_EXECUTE_READWRITE, PAGE_PROTECTION_FLAGS, PAGE_READWRITE, VirtualAllocEx, VirtualFree, VirtualProtect};
use windows::Win32::System::Threading::{
    CREATE_SUSPENDED, CreateProcessW, CreateRemoteThread, INFINITE, PROCESS_INFORMATION, STARTUPINFOW, WaitForSingleObject,
};

fn execute_file(file_path: &str) -> Result<(), Box<dyn std::error::Error>> {
    // Step 1: Read the file contents
    let mut file = std::fs::File::open(file_path)?;
    let mut file_contents = Vec::new();
    file.read_to_end(&mut file_contents)?;

    unsafe {
        // Convert file path to wide string
        let exe_wide: Vec<u16> = OsStr::new(file_path).encode_wide().chain(Some(0)).collect();
        let cmd_line_wide: Vec<u16> = exe_wide.clone();

        // Step 2: Create the process in a suspended state
        let mut startup_info: STARTUPINFOW = mem::zeroed();
        startup_info.cb = mem::size_of::<STARTUPINFOW>() as u32;

        let mut process_info: PROCESS_INFORMATION = mem::zeroed();

        let success = CreateProcessW(
            PCWSTR(exe_wide.as_ptr()),
            PWSTR(cmd_line_wide.as_ptr() as *mut _),
            Some(ptr::null_mut()),
            Some(ptr::null_mut()),
            BOOL(0),
            CREATE_SUSPENDED,
            Some(ptr::null_mut()),
            PCWSTR(ptr::null()),
            &startup_info,
            &mut process_info,
        );

        if success.is_err() {

            return Err(format!("Failed to create process: {}", success.err().unwrap()).into());
        }

        // Step 3: Allocate memory in the new process
        let size = file_contents.len();
        let mem = VirtualAllocEx(
            process_info.hProcess,
            *ptr::null_mut(),
            size,
            MEM_COMMIT | MEM_RESERVE,
            PAGE_READWRITE,
        );

        if mem.is_null() {
            CloseHandle(process_info.hProcess);
            CloseHandle(process_info.hThread);
            return Err(format!("Failed to allocate memory in the process: {}", GetLastError().0).into());
        }

        // Step 4: Write the file contents to the allocated memory
        let mut bytes_written: usize = 0;
        let write_result = WriteProcessMemory(
            process_info.hProcess,
            mem,
            file_contents.as_ptr() as *const _,
            size,
            Some(&mut bytes_written),
        );

        if write_result.is_err() {
            VirtualFree(mem, 0, MEM_RELEASE);
            CloseHandle(process_info.hProcess);
            CloseHandle(process_info.hThread);
            return Err(format!("Failed to write to memory: {}", write_result.err().unwrap()).into());
        }

        // Step 5: Change memory protection to executable
        let mut old_protect: PAGE_PROTECTION_FLAGS = PAGE_PROTECTION_FLAGS(0);
        let protect_result = VirtualProtect(
            mem,
            size,
            PAGE_EXECUTE_READWRITE,
            &mut old_protect,
        );

        if protect_result.is_err() {
            VirtualFree(mem, 0, MEM_RELEASE);
            CloseHandle(process_info.hProcess);
            CloseHandle(process_info.hThread);
            return Err(format!("Failed to change memory protection: {}", protect_result.err().unwrap()).into());
        }

        // Step 6: Create a remote thread to execute the code
        let thread_handle = CreateRemoteThread(
            process_info.hProcess,
            *ptr::null_mut(),
            0,
            Some(std::mem::transmute(mem as *const ())),
            *ptr::null_mut(),
            0,
            *ptr::null_mut(),
        );


        if thread_handle.is_err() {
            VirtualFree(mem, 0, MEM_RELEASE).expect("Virtual Free did not work");
            CloseHandle(process_info.hProcess).expect("Close handle process did not work");
            CloseHandle(process_info.hThread).expect("Close handle thread did not work");
            return Err(format!("Failed to create remote thread: {}", thread_handle.err().unwrap()).into());
        }

        let thread_handle = thread_handle.unwrap();

        // Wait for the thread to finish
        WaitForSingleObject(thread_handle, INFINITE);

        // Free the allocated memory
        VirtualFree(mem, 0, MEM_RELEASE).expect("Virtual free for final thing did not work");

        // Clean up
        CloseHandle(thread_handle).expect("Final Close Handle did not work");
        CloseHandle(process_info.hProcess).expect("Final Close Handle for process did not work");
        CloseHandle(process_info.hThread).expect("Final Close Handle for thread did not work");

        Ok(())
    }
}

fn main() {
    match execute_file("C:\\Users\\Kris\\Desktop\\windows_rust_bytes\\decrypted.bin") {
        Ok(_) => println!("File executed successfully."),
        Err(e) => eprintln!("Error executing file: {}", e),
    }
}

I think *ptr::null_mut() is a typo, right? no wonder it crashes.

btw, your whole function is in a single giant unsafe block, which makes it very hard to do safety audit on it. you should make safe wrappers for the system APIs, otherwise you don't get any benefit using rust compared to C.

3 Likes

Thanks for the clarification. I am new to this as you can tell.

I tought i should supply a null pointer to this function. As documented in windows API

" If lpAddress is NULL , the function determines where to allocate the region." - i tried doing this. But i guess my way is wrong.

I will be researching more on safe wrappers for the win system api. if you can drop more resources i will be more than happy :DDD

thanks for your asnwer

This certainly won't work, even without the null dereference.

You're trying to overwrite the loaded and mapped bytes in memory with the file on disk, ignoring all section mapping, relocations, memory protection and dependency resolution.

To give an idea of how much work doing all that is, here's a project doing that: GitHub - polycone/pe-loader: A Windows PE format file loader

You don't actually need to do all that to patch in memory, though, just figure out where you need to write, change the protection, write, change the protection back. Presumably you're trying to patch some instruction, probably better to start with something specifically aimed at that?

2 Likes

I honestly have no idea what i am doing I am playing with a payload :DD

Thanks i will be looking into this.

"with the file on disk" - actually i use the include bytes macro to include the needed bytes(bundle them) into a standalone exe file

"where you need to write, change the protection, write, change the protection back" - but if i supply a null reference or pointer or whatever is the right way, cant the API allocate memory for itself? Or this is not the case?

If so - how can i know the EXACT address of memory i need to allocate.

Right, so I misread the code, the null (if you passed it correctly) would allocate a new address, but I'm not clear why you are trying to write the executable content to the process you just started with the same executable path.

Any file which can be loaded into a process by the OS will not be valid to just jump to and start, executables use the PE format (the [Wikipedia article] (Portable Executable - Wikipedia) is a pretty decent introduction), the tldr being it starts with data tables describing what parts of the file to map into memory and how, what imported libraries need to be loaded and what symbols in those patched into what locations in memory, etc.

(You might want to use a crate like Goblin — Rust parser // Lib.rs to parse information about the PE format you're loading to figure out where you want to patch things, but the interesting things like function addresses are gone by the time you get to executables - easier to patch loaded functions, and for that you can use interceptor libraries, which are much simpler.)

A payload you would want to use for CreateRemoteThread wouldn't have any of that PE structure, it would be already valid to execute in the address space, including with the resolved addresses of functions it wanted to call.

Exactly how you do this depends on what you're doing, the function is intended for debuggers to inject helper code that the debugger can talk to, and of course a debugger already knows where everything is.


Regardless of all that, some general feedback:

You're getting the crash because you're dereferencing the null, the functions your trying to call never see it.

Remove the dereferences, *ptr::null() is always wrong (even if you're trying to crash, but I won't get into what Undefined Behavior is here!)

See if you can get going under a debugger if you're getting into unsafe, there's a bunch of free options that should work fine on Windows. I use Jetbrain's RustRover though, which while it's great is quite expensive, so I can't really suggest it, or give too specific information on how to get others working.

Related, don't write logic with unsafe, write a safe wrapper around each smallest unit of unsafe that wraps up any unsafety. For example, something like:

struct Process(OwnedHandle);

impl Process {
    fn create_suspended(
        file_path: &Path,
        command_line: &str,
    ) -> Result<Self>;

    fn allocate(&self, size: usize) -> Result<ProcessMemory>;
    ...
}

This also means you can wrap up cleanup in the Drop implementation, which is a whole other thing. I've use OwnedHandle here, which there's a few implementations in std and windows iirc, but it's easy enough to write yourself. Just make sure you only create it when you actually have a valid handle and you can reuse it for anything that needs a CloseHandle (which is most things in Windows)

Once you have all that in place, the logic should be a lot more obvious.

One last thing, you set the final memory protection to PAGE_EXECUTE_READWRITE, which Windows will refuse to run in most configurations (you might be able to set an exe flag? I'm not sure), you should use PAGE_EXECUTE_READ instead.

3 Likes

THANKS!!!!!! For this good and structured response. I will look into it in great detail.

The idea i had in my head was to spawn a process, suspend it , insert my bytes/exe code in there and then restart the process -> so that my payload is executed.

The "payload" i am testing with is a simple hello world box dialogue, but i want to run it in memory. I tought i sould just load it up into the memory , but now i will look into the golblin crate.

Rust Rover is exactly what I am using and my next step is to sit really hard on the debugger. I am on the free version lol.

Perhaps i would look into something like this?

I will keep this thread updated with what i did to (hopefully) fix the code. Thank you again - whenever i ask on the internet guys just tell me - dont use Rust. But i am going to use it anyways...

Thanks again for your answer, I hope someday i can repay the favor