Learning Rust pt. 3 — Structures and Functions
Now we are getting to the good stuff! One of my favorite things about the DFIR community is the plethora of tools that are constantly being created by analysts. Now, this is also one of the more complicated aspects as well. Output between tools can differ so widely it is hard to support an automated analytical system. If you caught our talk at OSDFCon this year, you saw how we used output from different tools to link up artifacts to attribute Shellbags to Volumes via the Linkfiles. The problem is this system can often break with an update due to changing output format. A great example of this is that this week Willi Ballenthin released a new version of EVTXtract. An amazing tool for recovering Event Logs. A tool that helped Dave and I greatly in the DEFCON forensics CTF. We then added some functionality to EventMonkey so it could parse the recovered event logs from EVTXtract. But the new version has changed the output format… so now its time to go mod EventMonkey up again because it is no longer compatible. Needless to say, while this is difficult sometimes, its not a bad thing. It means that DFIR tools are constantly evolving and more data is becoming known! All of this, just to say, if you are making DFIR tools, its pry because you want to parse binary data to make it human readable.
So how do we do this in Rust? First we need to learn how to create some structures to store data and how to give them functionality. Though, I have to warn you, I still don’t know if I am implementing it the best way. Maybe some of the guys in the Rust community can help me form some best practice habits.
So the first question is, “what artifact do we need to parse”? For RustyUsn, its the Windows USN Journal. Again, I went with this as a first project because the data structure is extremely simple and for now we are just sticking with the version 2 records. Here is the record structure: https://msdn.microsoft.com/en-us/library/windows/desktop/aa365722(v=vs.85).aspx. This makes a great exercise because we will learn not only how to parse binary, but, also deal with DateTime structs and UTF-16. Something many Windows data structures contain.
Quick note: I want this code to be a reference for different components that a parsing tool can have. I try to keep the code well documented so you can see what I am doing. Looking at a code’s source is the best way to see concepts in action. I am not showing the full code in this post and it can change, so instead i am just looking at different sections and how it implements core concepts that a make a tool.
Today we look at the usn.rs which is where the magic happens. First we import in the libraries we will be using (some of these make up our structures):
use usnpkg::chrono::*;
use usnpkg::byteorder::{ReadBytesExt, LittleEndian};
use std::fs::File;
use std::io::{Error, ErrorKind};
use std::io::Read;
use std::io::Seek;
use std::io::SeekFrom;
use std::slice;
use std::mem;
chrono is for DateTime objects;
byteorder is for reading our binary data that is in little endian;
std::fs::File is our file handle;
std::io::{Error, ErrorKind} for our error handling;
Read, Seek, SeekFrom for IO operations for our std::fs::File;
std::slice for going from binary to structures;
std::mem for initializing our USN struct;
Structures
Now lets dive into the structures.
#[derive(Debug)]
pub struct UsnRecordV2 {
// 0
record_length: u32,
major_version: u16,
minor_version: u16,
file_reference_number: u64,
parent_file_reference_number: u64,
usn: u64,
// 32
timestamp: NaiveDateTime,
// 40
reason: u32,
source_info: u32,
security_id: u32,
file_attributes: u32,
file_name_length: u16,
file_name_offset: u16,
// 60
file_name: String
}
This is our USN structure. We have two important types here out side of the normal unsigned integer business. NaiveDateTime for holding our datetime and String for holding our unicode. The #[derive(Debug)] allows us to print out our structure. I added this so I could output values till I put in proper output formatting.
Next is a structure keep track of where we are reading from in the file.
// maintain UsnConnection info
pub struct UsnConnection {
filehandle: File,
_offset: u64,
_size: u64
}
I should note at this point that Rust is not object oriented. Instead, you implement functions for your structures or types. Read more about it in the docs.
We want our UsnConnection to have a function called get_next_record() that we can use to iterate through to journal. You see this in main.rs:
while let Ok(record) = usn_connection.get_next_record(){
println!("USN structure {}: {:#?}",cnt,record);
cnt += 1;
};
How do we implement get_next_record() to work on UsnConnection type?
// implement UsnConnection Functionality
impl UsnConnection {
// function for getting a record
pub fn get_next_record(&mut self)->Result<UsnRecordV2,Error>{
...
}
}
To be honest. I am not sure if I am supposed to be implementing this as an iterator. Still so much to learn.
The get_next_record() function returns a Result, which is one way to error check in Rust. You either have Ok, or you have Err. In this case Ok has UsnRecordV2 structure, and Err has an Error type.
So now we have structures and we have implemented some structure functionality. Next is to parse binary data into it!