diff --git a/Cargo.lock b/Cargo.lock index c3e1647b..6b63ca2e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -70,12 +70,30 @@ dependencies = [ "rustc-demangle", ] +[[package]] +name = "base64" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3441f0f7b02788e948e47f457ca01f1d7e6d92c693bc132c22b087d3141c03ff" + [[package]] name = "bitflags" version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cf1de2fe8c75bc145a2f577add951f8134889b4795d47466a54a5c846d691693" +[[package]] +name = "bstr" +version = "0.2.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "31accafdb70df7871592c058eca3985b71104e15ac32f64706022c58867da931" +dependencies = [ + "lazy_static", + "memchr", + "regex-automata", + "serde", +] + [[package]] name = "build_const" version = "0.2.1" @@ -172,6 +190,15 @@ dependencies = [ "build_const", ] +[[package]] +name = "crc32fast" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba125de2af0df55319f41944744ad91c71113bf74a4646efff39afe1f6842db1" +dependencies = [ + "cfg-if", +] + [[package]] name = "crossbeam-channel" version = "0.4.4" @@ -219,6 +246,28 @@ dependencies = [ "lazy_static", ] +[[package]] +name = "csv" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00affe7f6ab566df61b4be3ce8cf16bc2576bca0963ceb0955e45d514bf9a279" +dependencies = [ + "bstr", + "csv-core", + "itoa", + "ryu", + "serde", +] + +[[package]] +name = "csv-core" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b2466559f260f48ad25fe6317b3c8dac77b5bdb5763ac7d9d6103530663bc90" +dependencies = [ + "memchr", +] + [[package]] name = "dialoguer" version = "0.6.2" @@ -355,6 +404,18 @@ dependencies = [ "winstructs", ] +[[package]] +name = "flate2" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da80be589a72651dcda34d8b35bcdc9b7254ad06325611074d9cc0fbb19f60ee" +dependencies = [ + "cfg-if", + "crc32fast", + "libc", + "miniz_oxide", +] + [[package]] name = "fs_extra" version = "1.2.0" @@ -747,6 +808,15 @@ dependencies = [ "thread_local", ] +[[package]] +name = "regex-automata" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae1ded71d66a4a97f5e961fd0cb25a5f366a42a41570d16a763a69c092c26ae4" +dependencies = [ + "byteorder", +] + [[package]] name = "regex-syntax" version = "0.6.18" @@ -1107,8 +1177,11 @@ dependencies = [ name = "yamato_event_analyzer" version = "0.1.0" dependencies = [ + "base64", "clap", + "csv", "evtx", + "flate2", "quick-xml 0.17.2", "regex", "serde", diff --git a/Cargo.toml b/Cargo.toml index 04818b2c..863e7911 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -13,3 +13,6 @@ serde = { version = "1.0", features = ["derive"] } serde_json = { version = "1.0"} clap = "*" regex = "1" +csv = "1.1" +base64 = "*" +flate2 = "1.0" \ No newline at end of file diff --git a/regexes.txt b/regexes.txt new file mode 100644 index 00000000..f59a3792 --- /dev/null +++ b/regexes.txt @@ -0,0 +1,19 @@ +Type,regex,string +0,^cmd.exe /c echo [a-z]{6} > \\\\.\\pipe\\[a-z]{6}$,Metasploit-style cmd with pipe (possible use of Meterpreter 'getsystem') +0,^%SYSTEMROOT%\\[a-zA-Z]{8}\.exe$,Metasploit-style %SYSTEMROOT% image path (possible use of Metasploit 'Native upload' exploit payload) +0,powershell.*FromBase64String.*IO.Compression.GzipStream,Metasploit-style base64 encoded/compressed PowerShell function (possible use of Metasploit PowerShell exploit payload) +0,DownloadString\(.http,Download via Net.WebClient DownloadString +0,mimikatz,Command referencing Mimikatz +0,Invoke-Mimikatz.ps,PowerSploit Invoke-Mimikatz.ps1 +0,PowerSploit.*ps1,Use of PowerSploit +0,User-Agent,User-Agent set via command line +0,[a-zA-Z0-9/+=]{500},500+ consecutive Base64 characters +0,powershell.exe.*Hidden.*Enc,Base64 encoded and hidden PowerShell command +# Generic csc.exe alert, comment out if experiencing false positives +0,\\csc\.exe,Use of C Sharp compiler csc.exe +0,\\csc\.exe.*\\Appdata\\Local\\Temp\\[a-z0-9]{8}\.cmdline,PSAttack-style command via csc.exe +# Generic cvtres.exe alert, comment out if experiencing false positives +0,\\cvtres\.exe.*,Resource File To COFF Object Conversion Utility cvtres.exe +0,\\cvtres\.exe.*\\AppData\\Local\\Temp\\[A-Z0-9]{7}\.tmp,PSAttack-style command via cvtres.exe +1,^[a-zA-Z]{22}$,Metasploit-style service name: 22 characters, [A-Za-z] +1,^[a-zA-Z]{16}$,Metasploit-style service name: 16 characters, [A-Za-z] \ No newline at end of file diff --git a/src/detections/mod.rs b/src/detections/mod.rs index 7238b4aa..ba8b0f90 100644 --- a/src/detections/mod.rs +++ b/src/detections/mod.rs @@ -3,3 +3,4 @@ mod common; pub mod detection; mod security; mod system; +mod utils; diff --git a/src/detections/utils.rs b/src/detections/utils.rs new file mode 100644 index 00000000..9a2ffdc3 --- /dev/null +++ b/src/detections/utils.rs @@ -0,0 +1,240 @@ +extern crate base64; +extern crate csv; +extern crate regex; + +use flate2::read::GzDecoder; +use regex::Regex; +use std::fs::File; +use std::io::prelude::*; +use std::str; +use std::string::String; + +pub fn check_command( + event_id: usize, + commandline: &str, + minlength: usize, + servicecmd: usize, + servicename: &str, + creator: &str, + mut rdr: csv::Reader<&[u8]>, +) { + let mut text = "".to_string(); + let mut base64 = "".to_string(); + + for entry in rdr.records() { + if let Ok(_data) = entry { + if let Ok(_re) = Regex::new(&_data[0]) { + if _re.is_match(commandline) { + return; + } + } + } + } + if commandline.len() > minlength { + text.push_str("Long Command Line: greater than "); + text.push_str(&minlength.to_string()); + text.push_str("bytes\n"); + } + text.push_str(&check_obfu(commandline)); + text.push_str(&check_regex(commandline, 0)); + text.push_str(&check_creator(commandline, creator)); + if Regex::new(r"\-enc.*[A-Za-z0-9/+=]{100}") + .unwrap() + .is_match(commandline) + { + let re = Regex::new(r"^.* \-Enc(odedCommand)? ").unwrap(); + base64.push_str(&re.replace_all(commandline, "")); + } else if Regex::new(r":FromBase64String\(") + .unwrap() + .is_match(commandline) + { + let re = Regex::new(r"^^.*:FromBase64String\(\'*").unwrap(); + base64.push_str(&re.replace_all(commandline, "")); + let re = Regex::new(r"\'.*$").unwrap(); + base64.push_str(&re.replace_all(&base64.to_string(), "")); + } + if !base64.is_empty() { + if Regex::new(r"Compression.GzipStream.*Decompress") + .unwrap() + .is_match(commandline) + { + let decoded = base64::decode(base64).unwrap(); + let mut d = GzDecoder::new(decoded.as_slice()); + let mut uncompressed = String::new(); + d.read_to_string(&mut uncompressed).unwrap(); + println!("Decoded : {}", uncompressed); + text.push_str("Base64-encoded and compressed function\n"); + } else { + let decoded = base64::decode(base64).unwrap(); + println!("Decoded : {}", str::from_utf8(decoded.as_slice()).unwrap()); + text.push_str("Base64-encoded function\n"); + text.push_str(&check_obfu(str::from_utf8(decoded.as_slice()).unwrap())); + text.push_str(&check_regex(str::from_utf8(decoded.as_slice()).unwrap(), 0)); + } + } + if !text.is_empty() { + if servicecmd != 0 { + println!("Message : Suspicious Service Command"); + println!("Results : Service name: {}\n", servicename); + } else { + println!("Message : Suspicious Command Line"); + } + println!("command : {}", commandline); + println!("result : {}", text); + println!("EventID : {}", event_id); + } +} + +fn check_obfu(string: &str) -> std::string::String { + let mut obfutext = "".to_string(); + let lowercasestring = string.to_lowercase(); + let length = lowercasestring.len(); + let mut minpercent = 0.65; + let maxbinary = 0.50; + + let mut re = Regex::new(r"[a-z0-9/¥;:|.]").unwrap(); + let mut noalphastring = ""; + if let Some(_caps) = re.captures(&lowercasestring) { + if let Some(_data) = _caps.get(0) { + noalphastring = _data.as_str(); + } + } + + re = Regex::new(r"[01]").unwrap(); + let mut nobinarystring = ""; + if let Some(_caps) = re.captures(&lowercasestring) { + if let Some(_data) = _caps.get(0) { + nobinarystring = _data.as_str(); + } + } + + if length > 0 { + let mut percent = (length - noalphastring.len()) / length; + if ((length / 100) as f64) < minpercent { + minpercent = (length / 100) as f64; + } + if percent < minpercent as usize { + obfutext.push_str("Possible command obfuscation: only "); + + re = Regex::new(r"\{0:P0}").unwrap(); + let percent = &percent.to_string(); + if let Some(_caps) = re.captures(percent) { + if let Some(_data) = _caps.get(0) { + obfutext.push_str(_data.as_str()); + } + } + + obfutext.push_str("alphanumeric and common symbols\n"); + } + percent = (nobinarystring.len().wrapping_sub(length) / length) / length; + let binarypercent = 1_usize.wrapping_sub(percent); + if binarypercent > maxbinary as usize { + obfutext.push_str("Possible command obfuscation: "); + + re = Regex::new(r"\{0:P0}").unwrap(); + let binarypercent = &binarypercent.to_string(); + if let Some(_caps) = re.captures(binarypercent) { + if let Some(_data) = _caps.get(0) { + obfutext.push_str(_data.as_str()); + } + } + + obfutext.push_str("zeroes and ones (possible numeric or binary encoding)\n"); + } + } + return obfutext; +} + +fn check_regex(string: &str, r#type: usize) -> std::string::String { + let mut f = File::open("regexes.txt").expect("file not found"); + let mut contents = String::new(); + f.read_to_string(&mut contents); + + let mut rdr = csv::Reader::from_reader(contents.as_bytes()); + + let mut regextext = "".to_string(); + for regex in rdr.records() { + if let Ok(_data) = regex { + /* + data[0] is type in csv. + data[1] is regex in csv. + data[2] is string in csv. + */ + if &_data[0] == r#type.to_string() { + if let Ok(_re) = Regex::new(&_data[1]) { + if _re.is_match(string) { + regextext.push_str(&_data[2]); + regextext.push_str("\n"); + } + } + } + } + } + return regextext; +} + +fn check_creator(command: &str, creator: &str) -> std::string::String { + let mut creatortext = "".to_string(); + if !creator.is_empty() { + if command == "powershell" { + if creator == "PSEXESVC" { + creatortext.push_str("PowerShell launched via PsExec: "); + creatortext.push_str(creator); + creatortext.push_str("\n"); + } else if creator == "WmiPrvSE" { + creatortext.push_str("PowerShell launched via WMI: "); + creatortext.push_str(creator); + creatortext.push_str("\n"); + } + } + } + return creatortext; +} + +#[cfg(test)] +mod tests { + use crate::detections::utils; + use std::fs::File; + use std::io::Read; + #[test] + fn test_check_regex() { + let regextext = utils::check_regex("\\cvtres.exe", 0); + assert!(regextext == "Resource File To COFF Object Conversion Utility cvtres.exe\n"); + } + + #[test] + fn test_check_creator() { + let mut creatortext = utils::check_creator("powershell", "PSEXESVC"); + assert!(creatortext == "PowerShell launched via PsExec: PSEXESVC\n"); + creatortext = utils::check_creator("powershell", "WmiPrvSE"); + assert!(creatortext == "PowerShell launched via WMI: WmiPrvSE\n"); + } + + #[test] + fn test_check_obfu() { + let obfutext = utils::check_obfu("dir01"); + assert!(obfutext == "Possible command obfuscation: zeroes and ones (possible numeric or binary encoding)\n"); + } + + #[test] + fn test_check_command() { + let mut f = File::open("whitelist.txt").expect("file not found"); + let mut contents = String::new(); + f.read_to_string(&mut contents); + + let rdr = csv::Reader::from_reader(contents.as_bytes()); + utils::check_command(1, "dir", 100, 100, "dir", "dir", rdr); + + let rdr = csv::Reader::from_reader(contents.as_bytes()); + //test return with whitelist. + utils::check_command( + 1, + "\"C:\\Program Files\\Google\\Update\\GoogleUpdate.exe\"", + 100, + 100, + "dir", + "dir", + rdr, + ); + } +} diff --git a/whitelist.txt b/whitelist.txt new file mode 100644 index 00000000..a6165f49 --- /dev/null +++ b/whitelist.txt @@ -0,0 +1,3 @@ +regex +^"C:\\Program Files\\Google\\Chrome\\Application\\chrome\.exe" +^"C:\\Program Files\\Google\\Update\\GoogleUpdate\.exe"