diff --git a/.gitignore b/.gitignore index 5bfa00b5..7a8a82ab 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,5 @@ /target /samples -*.test \ No newline at end of file +*.test +/.vscode/ +.DS_Store \ No newline at end of file diff --git a/Cargo.lock b/Cargo.lock index 988e8b95..62bf1e6a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1192,6 +1192,7 @@ dependencies = [ "evtx", "flate2", "lazy_static", + "linked-hash-map", "quick-xml 0.17.2", "regex", "serde", diff --git a/Cargo.toml b/Cargo.toml index 51077334..6bd34971 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -20,6 +20,7 @@ flate2 = "1.0" lazy_static = "1.4.0" chrono = "0.4.19" yaml-rust = "0.4" +linked-hash-map = "0.5.3" [target.x86_64-pc-windows-gnu] linker = "x86_64-w64-mingw32-gcc" diff --git a/config/eventkey_alias.txt b/config/eventkey_alias.txt new file mode 100644 index 00000000..12fb0ef3 --- /dev/null +++ b/config/eventkey_alias.txt @@ -0,0 +1,19 @@ +alias,event_key +EventID,Event.System.EventID +Channel,Event.System.Channel +CommandLine,Event.EventData.CommandLine +ParentProcessName,Event.EventData.ParentProcessName +Signed,Event.EventData.Signed +ProcessName,Event.EventData.ProcessName +AccessMask,Event.EventData.AccessMask +TargetUserName,Event.EventData.TargetUserName +param1,Event.EventData.param1 +param2,Event.EventData.param2 +ServiceName,Event.EventData.ServiceName +ImagePath,Event.EventData.ImagePath +ContextInfo,Event.EventData.ContextInfo +Path,Event.EventData.Path +ScriptBlockText,Event.EventData.ScriptBlockText#Name +MemberName,Event.EventData.SubjectUserName +MemberSid,Event.EventData.SubjectUserSid +TargetSid,Event.EventData.TargetSid diff --git a/rules/deep_blue_cli/powershell/4103.yml b/rules/deep_blue_cli/powershell/4103.yml new file mode 100644 index 00000000..8bcefcff --- /dev/null +++ b/rules/deep_blue_cli/powershell/4103.yml @@ -0,0 +1,20 @@ +title: PowerShell Execution Pipeline +description: hogehoge +enabled: true +author: Yea +logsource: + product: windows +detection: + selection: + Channel: PowerShell + EventID: 4103 + ContextInfo: + - Host Application + - ホスト アプリケーション + # condition: selection +falsepositives: + - unknown +level: medium +output: 'command=%CommandLine%' +creation_date: 2020/11/8 +uodated_date: 2020/11/8 diff --git a/rules/deep_blue_cli/powershell/4104.yml b/rules/deep_blue_cli/powershell/4104.yml new file mode 100644 index 00000000..d9d3a8b4 --- /dev/null +++ b/rules/deep_blue_cli/powershell/4104.yml @@ -0,0 +1,19 @@ +title: PowerShell Execution Remote Command +description: hogehoge +enabled: true +author: Yea +logsource: + product: windows +detection: + selection: + Channel: PowerShell + EventID: 4104 + Path: null + ScriptBlockText: null + # condition: selection +falsepositives: + - unknown +level: medium +output: 'command=%CommandLine%' +creation_date: 2020/11/8 +uodated_date: 2020/11/8 diff --git a/rules/deep_blue_cli/powershell/powershell4103.toml b/rules/deep_blue_cli/powershell/powershell4103.toml deleted file mode 100644 index d70c52bf..00000000 --- a/rules/deep_blue_cli/powershell/powershell4103.toml +++ /dev/null @@ -1,4 +0,0 @@ -[rule] -severity = "high" -name = "4103" -message = "Execute Pipeline" \ No newline at end of file diff --git a/rules/deep_blue_cli/powershell/powershell4104.toml b/rules/deep_blue_cli/powershell/powershell4104.toml deleted file mode 100644 index a4262b86..00000000 --- a/rules/deep_blue_cli/powershell/powershell4104.toml +++ /dev/null @@ -1,4 +0,0 @@ -[rule] -severity = "high" -name = "4104" -message = "Excute Remote Command" \ No newline at end of file diff --git a/rules/deep_blue_cli/security/1102.yml b/rules/deep_blue_cli/security/1102.yml new file mode 100644 index 00000000..b7281755 --- /dev/null +++ b/rules/deep_blue_cli/security/1102.yml @@ -0,0 +1,17 @@ +title: The Audit log file was cleared +description: hogehoge +enabled: true +author: Yea +logsource: + product: windows +detection: + selection: + Channel: Security + EventID: 1102 + # condition: selection +falsepositives: + - unknown +level: medium +output: 'Audit Log Clear¥n The Audit log was cleared.¥m%user_data.log_file_cleared%%user_data.subject_user_name%' +creation_date: 2020/11/8 +uodated_date: 2020/11/8 diff --git a/rules/deep_blue_cli/security/4673.yml b/rules/deep_blue_cli/security/4673.yml new file mode 100644 index 00000000..35d813bc --- /dev/null +++ b/rules/deep_blue_cli/security/4673.yml @@ -0,0 +1,17 @@ +title: Sensitive Privilede Use (Mimikatz) +description: hogehoge +enabled: true +author: Yea +logsource: + product: windows +detection: + selection: + Channel: Security + EventID: 4673 + # condition: selection | count(EventID) > 4 +falsepositives: + - unknown +level: medium +output: 'Sensitive Privilege Use Exceeds Threshold¥n Potentially indicative of Mimikatz, multiple sensitive priviledge calls have been made.¥nUserName:SubjectUserName% Domain Name:%DomainName%' +creation_date: 2020/11/8 +uodated_date: 2020/11/8 diff --git a/rules/deep_blue_cli/security/4674.yml b/rules/deep_blue_cli/security/4674.yml new file mode 100644 index 00000000..eb169a0d --- /dev/null +++ b/rules/deep_blue_cli/security/4674.yml @@ -0,0 +1,19 @@ +title: An Operation was attempted on a privileged object +description: hogehoge +enabled: true +author: Yea +logsource: + product: windows +detection: + selection: + Channel: Security + EventID: 4674 + ProcessName: '(?i)C:\WINDOWS\SYSTEM32\SERVICE.EXE' # (?i) means case insesitive for Rust Regex + AccessMask: '%%1539' + # condition: selection +falsepositives: + - unknown +level: medium +output: 'Possible Hidden Service Attempt¥nUser requested to modify the Dynamic Access Control (DAC) permissions of a service, possibly to hide it from view.¥nUser: %SubjectUserName%¥nTarget service:%ObjectName¥nDesired Access:WRITE_DAC' +creation_date: 2020/11/8 +uodated_date: 2020/11/8 diff --git a/rules/deep_blue_cli/security/4688.yml b/rules/deep_blue_cli/security/4688.yml new file mode 100644 index 00000000..7a4d6f3a --- /dev/null +++ b/rules/deep_blue_cli/security/4688.yml @@ -0,0 +1,18 @@ +title: Command Line Logging +description: hogehoge +enabled: true +author: Yea +logsource: + product: windows +detection: + selection: + Channel: Security + EventID: 4688 + CommandLine: '.+' + # condition: selection +falsepositives: + - unknown +level: medium +output: 'CommandLine:%CommandLine% ParentProcessName:%ParentProcessName%' +creation_date: 2020/11/8 +uodated_date: 2020/11/8 diff --git a/rules/deep_blue_cli/security/4720.yml b/rules/deep_blue_cli/security/4720.yml new file mode 100644 index 00000000..e35d5c05 --- /dev/null +++ b/rules/deep_blue_cli/security/4720.yml @@ -0,0 +1,17 @@ +title: A user account was created. +description: hogehoge +enabled: true +author: Yea +logsource: + product: windows +detection: + selection: + Channel: Security + EventID: 4720 + # condition: selection +falsepositives: + - unknown +level: low +output: 'New User Created UserName:%TargetUserName% SID:%TargetSid%' +creation_date: 2020/11/8 +uodated_date: 2020/11/8 diff --git a/rules/deep_blue_cli/security/4728.yml b/rules/deep_blue_cli/security/4728.yml new file mode 100644 index 00000000..47ed1866 --- /dev/null +++ b/rules/deep_blue_cli/security/4728.yml @@ -0,0 +1,18 @@ +title: A member was added to a security-enabled global group. +description: hogehoge +enabled: true +author: Yea +logsource: + product: windows +detection: + selection: + Channel: Security + EventID: 4728 + TargetUserName: Administrators + # condition: selection +falsepositives: + - unknown +level: low +output: 'user added to global Administrators UserName: %MemberName% SID: %MemberSid%' +creation_date: 2020/11/8 +uodated_date: 2020/11/8 diff --git a/rules/deep_blue_cli/security/4732.yml b/rules/deep_blue_cli/security/4732.yml new file mode 100644 index 00000000..05daa583 --- /dev/null +++ b/rules/deep_blue_cli/security/4732.yml @@ -0,0 +1,18 @@ +title: A member was added to a security-enabled local group. +description: hogehoge +enabled: true +author: Yea +logsource: + product: windows +detection: + selection: + Channel: Security + EventID: 4732 + TargetUserName: Administrators + # condition: selection +falsepositives: + - unknown +level: low +output: 'user added to local Administrators UserName: %MemberName% SID: %MemberSid%' +creation_date: 2020/11/8 +uodated_date: 2020/11/8 diff --git a/rules/deep_blue_cli/security/4756.yml b/rules/deep_blue_cli/security/4756.yml new file mode 100644 index 00000000..c7af8718 --- /dev/null +++ b/rules/deep_blue_cli/security/4756.yml @@ -0,0 +1,18 @@ +title: A member was added to a security-enabled universal group. +description: hogehoge +enabled: true +author: Yea +logsource: + product: windows +detection: + selection: + Channel: Security + EventID: 4756 + TargetUserName: Administrators + # condition: selection +falsepositives: + - unknown +level: low +output: 'user added to universal Administrators UserName: %MemberName% SID: %MemberSid%' +creation_date: 2020/11/8 +uodated_date: 2020/11/8 diff --git a/rules/deep_blue_cli/security/_4625.yml b/rules/deep_blue_cli/security/_4625.yml new file mode 100644 index 00000000..0fbabcd4 --- /dev/null +++ b/rules/deep_blue_cli/security/_4625.yml @@ -0,0 +1,17 @@ +title: An account failed to log on +description: hogehoge +enabled: false +author: Yea +logsource: + product: windows +detection: + selection: + Channel: Security + EventID: 4625 + # condition: selection | count(TargetUserName) > 3 +falsepositives: + - unknown +level: medium +output: 'High number of logon failures for one account UserName:%event_data.SubjectUserName% Total logon faiures:%count%' +creation_date: 2020/11/8 +uodated_date: 2020/11/8 diff --git a/rules/deep_blue_cli/security/_4648.yml b/rules/deep_blue_cli/security/_4648.yml new file mode 100644 index 00000000..4b9735c6 --- /dev/null +++ b/rules/deep_blue_cli/security/_4648.yml @@ -0,0 +1,17 @@ +title: An account failed to log on +description: hogehoge +enabled: false +author: Yea +logsource: + product: windows +detection: + selection: + Channel: Security + EventID: 4648 + # condition: selection | count(TargetUserName) > 3 +falsepositives: + - unknown +level: High +output: 'Distributed Account Explicit Credential Use (Password Spray Attack)¥n The use of multiple user account access attempts with explicit credentials is ¥nan indicator of a password spray attack.¥nTarget Usernames:%TargetUserName$¥nAccessing Username: %SubjectUserName%¥nAccessing Host Name: %SubjectDomainName%' +creation_date: 2020/11/8 +uodated_date: 2020/11/8 diff --git a/rules/deep_blue_cli/security/_4672.yml b/rules/deep_blue_cli/security/_4672.yml new file mode 100644 index 00000000..b1763b97 --- /dev/null +++ b/rules/deep_blue_cli/security/_4672.yml @@ -0,0 +1,19 @@ +title: Command Line Logging +description: hogehoge +enabled: false +author: Yea +logsource: + product: windows +detection: + selection: + Channel: Security + EventID: 4672 + PrivilegeList: + contain: SeDebugPrivilege + # condition: selection +falsepositives: + - unknown +level: medium +output: 'CommandLine:%CommandLine% ParentProcessName:%ParentProcessName%' +creation_date: 2020/11/8 +uodated_date: 2020/11/8 diff --git a/rules/deep_blue_cli/sysmon/1.yml b/rules/deep_blue_cli/sysmon/1.yml new file mode 100644 index 00000000..3f2b36c6 --- /dev/null +++ b/rules/deep_blue_cli/sysmon/1.yml @@ -0,0 +1,19 @@ +title: Sysmon Check command lines +description: hogehoge +enabled: true +author: Yea +logsource: + product: windows +detection: + selection: + Channel: Sysmon + EventID: 1 + CommandLine: '.+' + # condition: selection +falsepositives: + - unknown +level: medium +output: 'CommandLine=%CommandLine%¥nParentImage=%ParentImage%' +creation_date: 2020/11/8 +uodated_date: 2020/11/8 + diff --git a/rules/deep_blue_cli/sysmon/7.yml b/rules/deep_blue_cli/sysmon/7.yml new file mode 100644 index 00000000..4f321af4 --- /dev/null +++ b/rules/deep_blue_cli/sysmon/7.yml @@ -0,0 +1,18 @@ +title: Check for unsigned EXEs/DLLs +description: hogehoge +enabled: true +author: Yea +logsource: + product: windows +detection: + selection: + Channel: Sysmon + EventID: 7 + Signed: "false" # Compare by string + # condition: selection +falsepositives: + - unknown +level: low +output: 'Message: Unsigned Image(DLL)¥n Result : Loaded by: %event_data.Image%¥nCommand : %event_data.ImageLoaded%' +creation_date: 2020/11/8 +uodated_date: 2020/11/8 diff --git a/rules/deep_blue_cli/system/104.yml b/rules/deep_blue_cli/system/104.yml new file mode 100644 index 00000000..19301965 --- /dev/null +++ b/rules/deep_blue_cli/system/104.yml @@ -0,0 +1,17 @@ +title: The System log file was cleared +description: hogehoge +enabled: true +author: Yea +logsource: + product: windows +detection: + selection: + Channel: System + EventID: 104 + # condition: selection +falsepositives: + - unknown +level: medium +output: 'System Log Clear¥nThe System log was cleared.' +creation_date: 2020/11/8 +uodated_date: 2020/11/8 diff --git a/rules/deep_blue_cli/system/7030.yml b/rules/deep_blue_cli/system/7030.yml new file mode 100644 index 00000000..5e168427 --- /dev/null +++ b/rules/deep_blue_cli/system/7030.yml @@ -0,0 +1,19 @@ +title: This service may not function properly +description: hogehoge +enabled: true +author: Yea +logsource: + product: windows +detection: + selection: + Channel: System + EventID: 7030 + param1: + regexes: ./regexes.txt + # condition: selection +falsepositives: + - unknown +level: low +output: 'Interactive service warning¥nService name: %ServiceName%¥nMalware (and some third party software) trigger this warning' +creation_date: 2020/11/8 +uodated_date: 2020/11/8 diff --git a/rules/deep_blue_cli/system/7036.yml b/rules/deep_blue_cli/system/7036.yml new file mode 100644 index 00000000..f17ee1ad --- /dev/null +++ b/rules/deep_blue_cli/system/7036.yml @@ -0,0 +1,19 @@ +title: The ... service entered the stopped|running state +description: hogehoge +enabled: true +author: Yea +logsource: + product: windows +detection: + selection: + Channel: System + EventID: 7036 + param1: + regexes: ./regexes.txt + condition: selection +falsepositives: + - unknown +level: low +output: 'Suspicious Service Name¥nService name: %ServiceName%' +creation_date: 2020/11/8 +uodated_date: 2020/11/8 diff --git a/rules/deep_blue_cli/system/7040.yml b/rules/deep_blue_cli/system/7040.yml new file mode 100644 index 00000000..7ff699f2 --- /dev/null +++ b/rules/deep_blue_cli/system/7040.yml @@ -0,0 +1,21 @@ +title: The start type of the Windows Event Log service was changed from auto start to disabled +description: hogehoge +enabled: true +author: Yea +logsource: + product: windows +detection: + selection: + Channel: System + EventID: 7040 + param1: 'Windows Event Log' + param2: + - "disabled" + - "auto start" + condition: selection +falsepositives: + - unknown +level: low +output: 'Service name : %param1%¥nMessage : Event Log Service Stopped¥nResults: Selective event log manipulation may follow this event.' +creation_date: 2020/11/8 +uodated_date: 2020/11/8 diff --git a/rules/deep_blue_cli/system/7045.yml b/rules/deep_blue_cli/system/7045.yml new file mode 100644 index 00000000..a733cf54 --- /dev/null +++ b/rules/deep_blue_cli/system/7045.yml @@ -0,0 +1,22 @@ +title: A service was installed in the system +description: hogehoge +enabled: true +author: Yea +logsource: + product: windows +detection: + selection: + Channel: System + EventID: 7045 + ServiceName: + regexes: ./regexes.txt + ImagePath: + min_length: 1000 + whitelist: ./whitelist.txt + condition: selection +falsepositives: + - unknown +level: low +output: 'New Service Created¥n%ImagePath¥nService name: %ServiceName%' +creation_date: 2020/11/8 +uodated_date: 2020/11/8 diff --git a/src/afterfact.rs b/src/afterfact.rs index 38b31e5f..002d3d1a 100644 --- a/src/afterfact.rs +++ b/src/afterfact.rs @@ -1,6 +1,6 @@ use crate::detections::configs; use crate::detections::print; -use chrono::{DateTime, TimeZone, Utc}; +use chrono::{DateTime, Utc}; use serde::Serialize; use std::error::Error; use std::process; @@ -37,27 +37,50 @@ fn emit_csv(path: &str) -> Result<(), Box> { Ok(()) } -use std::fs::{read_to_string, remove_file}; -use std::io::Read; +#[cfg(test)] +mod tests { -#[test] -fn test_emit_csv() { - { - let mut messages = print::MESSAGES.lock().unwrap(); - let poke = Utc.ymd(1996, 2, 27).and_hms(1, 5, 1); - messages.insert(poke, "pokepoke".to_string()); - } + use crate::afterfact::emit_csv; + use crate::detections::print; + use serde_json::Value; + use std::fs::{read_to_string, remove_file}; - let expect = "Time,Message + #[test] + fn test_emit_csv() { + { + let mut messages = print::MESSAGES.lock().unwrap(); + let json_str = r##" + { + "Event": { + "EventData": { + "CommandLine": "hoge" + }, + "System": { + "TimeCreated": { + "#attributes":{ + "SystemTime": "1996-02-27T01:05:01Z" + } + } + } + } + } + "##; + let event_record: Value = serde_json::from_str(json_str).unwrap(); + + messages.insert(&event_record, "pokepoke".to_string()); + } + + let expect = "Time,Message 1996-02-27T01:05:01Z,pokepoke "; - assert!(emit_csv(&"./test_emit_csv.csv".to_string()).is_ok()); + assert!(emit_csv(&"./test_emit_csv.csv".to_string()).is_ok()); - match read_to_string("./test_emit_csv.csv") { - Err(_) => panic!("Failed to open file"), - Ok(s) => assert_eq!(s, expect), - }; + match read_to_string("./test_emit_csv.csv") { + Err(_) => panic!("Failed to open file"), + Ok(s) => assert_eq!(s, expect), + }; - assert!(remove_file("./test_emit_csv.csv").is_ok()); + assert!(remove_file("./test_emit_csv.csv").is_ok()); + } } diff --git a/src/detections/application.rs b/src/detections/application.rs deleted file mode 100644 index 624c5997..00000000 --- a/src/detections/application.rs +++ /dev/null @@ -1,57 +0,0 @@ -extern crate regex; - -use crate::models::event; -use regex::Regex; -use std::collections::HashMap; - -pub struct Application {} - -impl Application { - pub fn new() -> Application { - Application {} - } - - pub fn detection( - &mut self, - event_id: String, - system: &event::System, - event_data: HashMap, - ) { - self.emet(&event_id, system); - } - - fn emet(&mut self, event_id: &String, system: &event::System) { - if event_id != "2" { - return; - } - - match &system.provider.name { - Some(name) => { - if name != "EMET" { - return; - } - } - None => return, - } - match &system.message { - Some(message) => { - let message_split: Vec<&str> = message.split("\n").collect(); - if !message_split.is_empty() && message_split.len() >= 5 { - let text = message_split[0]; - let application = message_split[3]; - let re = Regex::new(r"^Application: ").unwrap(); - let command = re.replace_all(application, ""); - let username = message_split[4]; - - println!("Message EMET Block"); - println!("Command : {}", command); - println!("Results : {}", text); - println!("Results : {}", username); - } - } - None => { - println!("Warning: EMET Message field is blank. Install EMET locally to see full details of this alert"); - } - } - } -} diff --git a/src/detections/applocker.rs b/src/detections/applocker.rs deleted file mode 100644 index 2862bc3c..00000000 --- a/src/detections/applocker.rs +++ /dev/null @@ -1,53 +0,0 @@ -extern crate regex; - -use crate::models::event; -use regex::Regex; -use std::collections::HashMap; - -pub struct AppLocker {} - -impl AppLocker { - pub fn new() -> AppLocker { - AppLocker {} - } - - pub fn detection( - &mut self, - event_id: String, - _system: &event::System, - _event_data: HashMap, - ) { - self.applocker_log_warning(&event_id, &_system); - self.applocker_log_block(&event_id, &_system); - } - - fn applocker_log_warning(&mut self, event_id: &String, system: &event::System) { - if event_id != "8003" { - return; - } - - let re = Regex::new(r" was .*$").unwrap(); - let default = "".to_string(); - let message = &system.message.as_ref().unwrap_or(&default); - let command = re.replace_all(&message, ""); - - println!("Message Applocker Warning"); - println!("Command : {}", command); - println!("Results : {}", message); - } - - fn applocker_log_block(&mut self, event_id: &String, system: &event::System) { - if event_id != "8004" { - return; - } - - let re = Regex::new(r" was .*$").unwrap(); - let default = "".to_string(); - let message = &system.message.as_ref().unwrap_or(&default); - let command = re.replace_all(&message, ""); - - println!("Message Applocker Block"); - println!("Command : {}", command); - println!("Results : {}", message); - } -} diff --git a/src/detections/common.rs b/src/detections/common.rs deleted file mode 100644 index bd8782ef..00000000 --- a/src/detections/common.rs +++ /dev/null @@ -1,44 +0,0 @@ -use crate::models::event; -use std::collections::HashMap; - -#[derive(Debug)] -pub struct Common { - record_id: u64, - date: String, - record_id_list: HashMap, -} - -impl Common { - pub fn new() -> Common { - Common { - record_id: 0, - date: "".to_string(), - record_id_list: HashMap::new(), - } - } - - pub fn disp(&self) { - for (record_id, date) in self.record_id_list.iter() { - println!("date:{:?} record-id: {:?}", date, record_id); - } - } - - pub fn detection(&mut self, system: &event::System, event_data: &HashMap) { - self.check_record_id(system); - } - - // - // Record IDがシーケンスになっているかチェック - // - fn check_record_id(&mut self, system: &event::System) { - let event_record_id: u64 = system.event_record_id.parse().unwrap(); - if self.record_id > 0 && event_record_id - self.record_id > 1 { - self.record_id_list.insert( - self.record_id.to_string() + " - " + &system.event_record_id.to_string(), - self.date.to_string() + " - " + &system.time_created.system_time.to_string(), - ); - } - self.record_id = event_record_id; - self.date = system.time_created.system_time.to_string(); - } -} diff --git a/src/detections/configs.rs b/src/detections/configs.rs index 763f50f1..d4879f12 100644 --- a/src/detections/configs.rs +++ b/src/detections/configs.rs @@ -1,13 +1,11 @@ +use crate::detections::utils; use clap::{App, AppSettings, Arg, ArgMatches}; -use std::fs::File; -use std::io::prelude::*; +use std::collections::HashMap; use std::sync::Once; - #[derive(Clone)] pub struct SingletonReader { - pub regex: Vec>, - pub whitelist: Vec>, pub args: ArgMatches<'static>, + pub event_key_alias_config: EventKeyAliasConfig, } pub fn singleton() -> Box { @@ -17,9 +15,8 @@ pub fn singleton() -> Box { unsafe { ONCE.call_once(|| { let singleton = SingletonReader { - regex: read_csv("regexes.txt"), - whitelist: read_csv("whitelist.txt"), args: build_app().get_matches(), + event_key_alias_config: load_eventkey_alias(), }; SINGLETON = Some(Box::new(singleton)); @@ -56,25 +53,44 @@ fn build_app() -> clap::App<'static, 'static> { .arg(Arg::from_usage("--credits 'Zachary Mathis, Akira Nishikawa'")) } -fn read_csv(filename: &str) -> Vec> { - let mut f = File::open(filename).expect("file not found!!!"); - let mut contents: String = String::new(); - let mut ret = vec![]; - if f.read_to_string(&mut contents).is_err() { - return ret; +#[derive(Debug, Clone)] +pub struct EventKeyAliasConfig { + key_to_eventkey: HashMap, +} + +impl EventKeyAliasConfig { + pub fn new() -> EventKeyAliasConfig { + return EventKeyAliasConfig { + key_to_eventkey: HashMap::new(), + }; } - let mut rdr = csv::Reader::from_reader(contents.as_bytes()); - rdr.records().for_each(|r| { - if r.is_err() { + pub fn get_event_key(&self, alias: String) -> Option<&String> { + return self.key_to_eventkey.get(&alias); + } +} + +fn load_eventkey_alias() -> EventKeyAliasConfig { + let mut config = EventKeyAliasConfig::new(); + + let read_result = utils::read_csv("config/eventkey_alias.txt"); + // eventkey_aliasが読み込めなかったらエラーで終了とする。 + read_result.unwrap().into_iter().for_each(|line| { + if line.len() != 2 { return; } - let line = r.unwrap(); - let mut v = vec![]; - line.iter().for_each(|s| v.push(s.to_string())); - ret.push(v); + let empty = &"".to_string(); + let alias = line.get(0).unwrap_or(empty); + let event_key = line.get(1).unwrap_or(empty); + if alias.len() == 0 || event_key.len() == 0 { + return; + } + + config + .key_to_eventkey + .insert(alias.to_owned(), event_key.to_owned()); }); - return ret; + return config; } diff --git a/src/detections/detection.rs b/src/detections/detection.rs index 0c3ce7cc..20977de4 100644 --- a/src/detections/detection.rs +++ b/src/detections/detection.rs @@ -1,74 +1,101 @@ +extern crate chrono; extern crate csv; -extern crate quick_xml; -use crate::detections::application; -use crate::detections::applocker; -use crate::detections::common; -use crate::detections::powershell; -use crate::detections::security; -use crate::detections::sysmon; -use crate::detections::system; -use crate::models::event; +use crate::detections::print::Message; +use crate::detections::rule; +use crate::detections::rule::RuleNode; +use crate::yaml::ParseYaml; + +use chrono::{DateTime, FixedOffset, TimeZone, Utc}; use evtx::EvtxParser; -use quick_xml::de::DeError; -use std::collections::BTreeMap; +use serde_json::{Error, Value}; +const DIRPATH_RULES: &str = "rules"; + +// TODO テストケースかかなきゃ... #[derive(Debug)] -pub struct Detection { - timeline_list: BTreeMap, -} +pub struct Detection {} impl Detection { pub fn new() -> Detection { - Detection { - timeline_list: BTreeMap::new(), - } + Detection {} } - pub fn start(&mut self, mut parser: EvtxParser) -> Result<(), DeError> { - let mut common: common::Common = common::Common::new(); - let mut security = security::Security::new(); - let mut system = system::System::new(); - let mut application = application::Application::new(); - let mut applocker = applocker::AppLocker::new(); - let mut sysmon = sysmon::Sysmon::new(); - let mut powershell = powershell::PowerShell::new(); - - for record in parser.records() { - match record { - Ok(r) => { - let event: event::Evtx = quick_xml::de::from_str(&r.data)?; - let event_id = event.system.event_id.to_string(); - let channel = event.system.channel.to_string(); - let event_data = event.parse_event_data(); - - &common.detection(&event.system, &event_data); - if channel == "Security" { - &security.detection(event_id, &event.system, &event.user_data, event_data); - } else if channel == "System" { - &system.detection(event_id, &event.system, event_data); - } else if channel == "Application" { - &application.detection(event_id, &event.system, event_data); - } else if channel == "Microsoft-Windows-PowerShell/Operational" { - &powershell.detection(event_id, &event.system, event_data); - } else if channel == "Microsoft-Windows-Sysmon/Operational" { - &sysmon.detection(event_id, &event.system, event_data); - } else if channel == "Microsoft-Windows-AppLocker/EXE and DLL" { - &applocker.detection(event_id, &event.system, event_data); - } else { - //&other.detection(); - } + pub fn start(&mut self, mut parser: EvtxParser) { + // serialize from .etvx to jsons + let event_records: Vec = parser + .records_json() + .filter_map(|result_record| { + if result_record.is_err() { + eprintln!("{}", result_record.unwrap_err()); + return Option::None; } - Err(e) => eprintln!("{}", e), - } + + //// https://rust-lang-nursery.github.io/rust-cookbook/encoding/complex.html + let result_json: Result = + serde_json::from_str(&result_record.unwrap().data); + if result_json.is_err() { + eprintln!("{}", result_json.unwrap_err()); + return Option::None; + } + return result_json.ok(); + }) + .collect(); + + // load rule files + let mut rulefile_loader = ParseYaml::new(); + let resutl_readdir = rulefile_loader.read_dir(DIRPATH_RULES); + if resutl_readdir.is_err() { + eprintln!("{}", resutl_readdir.unwrap_err()); + return; } - //////////////////////////// - // 表示 - //////////////////////////// - common.disp(); - security.disp(); + // parse rule files + let mut selection_rules: Vec = rulefile_loader + .files + .into_iter() + .map(|rule_file| rule::parse_rule(rule_file)) + .filter_map(|mut rule| { + let err_msgs_result = rule.init(); + if err_msgs_result.is_ok() { + return Option::Some(rule); + } - return Ok(()); + // ruleファイルの初期化失敗時のエラーを表示する部分 + err_msgs_result.err().iter().for_each(|err_msgs| { + // TODO 本当はファイルパスを出力したい + // ParseYamlの変更が必要なので、一旦yamlのタイトルを表示。 + + // TODO エラーの出力方法を統一したい。 + // エラー出力用のクラスを作成してもいいかも + println!( + "[ERROR] Failed to parse Rule file. (Error Rule Title : {})", + rule.yaml["title"].as_str().unwrap_or("") + ); + err_msgs.iter().for_each(|err_msg| println!("{}", err_msg)); + println!(""); + }); + + return Option::None; + }) + .collect(); + + // selection rule files and collect message + let mut message = Message::new(); + selection_rules.iter_mut().for_each(|rule| { + event_records.iter().for_each(|event_record| { + if !rule.select(event_record) { + return; + } + + message.insert( + event_record, + rule.yaml["output"].as_str().unwrap_or("").to_string(), + ) + }); + }); + + // output message + message.print(); } } diff --git a/src/detections/mod.rs b/src/detections/mod.rs index 4aaee9ff..48034d62 100644 --- a/src/detections/mod.rs +++ b/src/detections/mod.rs @@ -1,11 +1,5 @@ -mod application; -mod applocker; -mod common; pub mod configs; pub mod detection; -mod powershell; pub mod print; -mod security; -mod sysmon; -mod system; +mod rule; mod utils; diff --git a/src/detections/powershell.rs b/src/detections/powershell.rs deleted file mode 100644 index 4fa956b0..00000000 --- a/src/detections/powershell.rs +++ /dev/null @@ -1,61 +0,0 @@ -use crate::detections::utils; -use crate::models::event; -use regex::Regex; -use std::collections::HashMap; - -pub struct PowerShell {} - -impl PowerShell { - pub fn new() -> PowerShell { - PowerShell {} - } - - pub fn detection( - &mut self, - event_id: String, - _system: &event::System, - event_data: HashMap, - ) { - self.execute_pipeline(&event_id, &event_data); - self.execute_remote_command(&event_id, &event_data); - } - - fn execute_pipeline(&mut self, event_id: &String, event_data: &HashMap) { - if event_id != "4103" { - return; - } - - let default = String::from(""); - let commandline = event_data.get("ContextInfo").unwrap_or(&default); - - if commandline.contains("Host Application") - || commandline.contains("ホスト アプリケーション") - { - let rm_before = - Regex::new("(?ms)^.*(ホスト アプリケーション|Host Application) = ").unwrap(); - let rm_after = Regex::new("(?ms)\n.*$").unwrap(); - - let temp_command_with_extra = rm_before.replace_all(commandline, ""); - let command = rm_after.replace_all(&temp_command_with_extra, ""); - - if command != "" { - utils::check_command(4103, &command, 1000, 0, &default, &default); - } - } - } - - fn execute_remote_command(&mut self, event_id: &String, event_data: &HashMap) { - if event_id != "4104" { - return; - } - - let default = String::from(""); - let path = event_data.get("Path").unwrap().to_string(); - if path == "".to_string() { - let commandline = event_data.get("ScriptBlockText").unwrap_or(&default); - if commandline.to_string() != default { - utils::check_command(4104, &commandline, 1000, 0, &default, &default); - } - } - } -} diff --git a/src/detections/print.rs b/src/detections/print.rs index fc1a4955..606d9f6e 100644 --- a/src/detections/print.rs +++ b/src/detections/print.rs @@ -1,8 +1,12 @@ extern crate chrono; extern crate lazy_static; +use crate::detections::configs; use chrono::{DateTime, TimeZone, Utc}; use lazy_static::lazy_static; +use regex::Regex; +use serde_json::Value; use std::collections::BTreeMap; +use std::collections::HashMap; use std::sync::Mutex; #[derive(Debug)] @@ -21,7 +25,15 @@ impl Message { } /// メッセージを設定 - pub fn insert(&mut self, time: DateTime, message: String) { + pub fn insert(&mut self, event_record: &Value, output: String) { + if output.is_empty() { + return; + } + + let message = &self.parse_message(event_record, output); + let default_time = Utc.ymd(1970, 1, 1).and_hms(0, 0, 0); + let time = Message::get_event_time(event_record).unwrap_or(default_time); + match self.map.get_mut(&time) { Some(v) => { v.push(message.to_string()); @@ -33,6 +45,44 @@ impl Message { } } + fn parse_message(&mut self, event_record: &Value, output: String) -> String { + let mut return_message: String = output; + let mut hash_map: HashMap = HashMap::new(); + let re = Regex::new(r"%[a-zA-Z0-9-_]+%").unwrap(); + for caps in re.captures_iter(&return_message) { + let full_target_str = &caps[0]; + let target_length = full_target_str.chars().count() - 2; // The meaning of 2 is two percent + let target_str = full_target_str + .chars() + .skip(1) + .take(target_length) + .collect::(); + + if let Some(array_str) = configs::singleton() + .event_key_alias_config + .get_event_key(target_str.to_string()) + { + let split: Vec<&str> = array_str.split(".").collect(); + let mut tmp_event_record: &Value = event_record.into(); + for s in split { + if let Some(record) = tmp_event_record.get(s) { + tmp_event_record = record; + } + } + hash_map.insert( + full_target_str.to_string(), + tmp_event_record.as_str().unwrap_or("").to_string(), + ); + } + } + + for (k, v) in &hash_map { + return_message = return_message.replace(k, v); + } + + return_message + } + /// メッセージを返す pub fn get(&self, time: DateTime) -> Vec { match self.map.get(&time) { @@ -46,22 +96,122 @@ impl Message { println!("{:?}", self.map); } + /// 最後に表示を行う + pub fn print(&self) { + for (key, values) in self.map.iter() { + for value in values.iter() { + println!("{} : {}", key, value); + } + } + } + pub fn iter(&self) -> &BTreeMap, Vec> { &self.map } + + fn get_event_time(event_record: &Value) -> Option> { + let system_time = + &event_record["Event"]["System"]["TimeCreated"]["#attributes"]["SystemTime"]; + let system_time_str = system_time.as_str().unwrap_or(""); + if system_time_str.is_empty() { + return Option::None; + } + + let rfc3339_time = DateTime::parse_from_rfc3339(system_time_str); + if rfc3339_time.is_err() { + return Option::None; + } + let datetime = Utc + .from_local_datetime(&rfc3339_time.unwrap().naive_utc()) + .single(); + if datetime.is_none() { + return Option::None; + } else { + return Option::Some(datetime.unwrap()); + } + } } -#[test] -fn test_create_and_append_message() { - let mut message = Message::new(); - let poke = Utc.ymd(1996, 2, 27).and_hms(1, 5, 1); - let taka = Utc.ymd(2000, 1, 21).and_hms(9, 6, 1); +#[cfg(test)] +mod tests { + use crate::detections::print::Message; + use serde_json::Value; - message.insert(poke, "TEST".to_string()); - message.insert(poke, "TEST2".to_string()); - message.insert(taka, "TEST3".to_string()); + #[test] + fn test_create_and_append_message() { + let mut message = Message::new(); + let json_str_1 = r##" + { + "Event": { + "EventData": { + "CommandLine": "hoge" + }, + "System": { + "TimeCreated": { + "#attributes":{ + "SystemTime": "1996-02-27T01:05:01Z" + } + } + } + } + } + "##; + let event_record_1: Value = serde_json::from_str(json_str_1).unwrap(); + message.insert(&event_record_1, "CommandLine1: %CommandLine%".to_string()); - let display = format!("{}", format_args!("{:?}", message)); - let expect = "Message { map: {1996-02-27T01:05:01Z: [\"TEST\", \"TEST2\"], 2000-01-21T09:06:01Z: [\"TEST3\"]} }"; - assert_eq!(display, expect); + let json_str_2 = r##" + { + "Event": { + "EventData": { + "CommandLine": "hoge" + }, + "System": { + "TimeCreated": { + "#attributes":{ + "SystemTime": "1996-02-27T01:05:01Z" + } + } + } + } + } + "##; + let event_record_2: Value = serde_json::from_str(json_str_2).unwrap(); + message.insert(&event_record_2, "CommandLine2: %CommandLine%".to_string()); + + let json_str_3 = r##" + { + "Event": { + "EventData": { + "CommandLine": "hoge" + }, + "System": { + "TimeCreated": { + "#attributes":{ + "SystemTime": "2000-01-21T09:06:01Z" + } + } + } + } + } + "##; + let event_record_3: Value = serde_json::from_str(json_str_3).unwrap(); + message.insert(&event_record_3, "CommandLine3: %CommandLine%".to_string()); + + let json_str_4 = r##" + { + "Event": { + "EventData": { + "CommandLine": "hoge" + } + } + } + "##; + let event_record_4: Value = serde_json::from_str(json_str_4).unwrap(); + message.insert(&event_record_4, "CommandLine4: %CommandLine%".to_string()); + + let display = format!("{}", format_args!("{:?}", message)); + println!("display::::{}", display); + let expect = "Message { map: {1970-01-01T00:00:00Z: [\"CommandLine4: hoge\"], 1996-02-27T01:05:01Z: [\"CommandLine1: hoge\", \"CommandLine2: hoge\"], 2000-01-21T09:06:01Z: [\"CommandLine3: hoge\"]} }"; + assert_eq!(display, expect); + } } diff --git a/src/detections/rule.rs b/src/detections/rule.rs new file mode 100644 index 00000000..c65e492a --- /dev/null +++ b/src/detections/rule.rs @@ -0,0 +1,582 @@ +extern crate regex; + +use crate::detections::utils; + +use regex::Regex; +use serde_json::Value; +use yaml_rust::Yaml; + +// TODO テストケースかかなきゃ... +pub fn parse_rule(yaml: Yaml) -> RuleNode { + let detection = parse_detection(&yaml); + + return RuleNode { + yaml: yaml, + detection: detection, + }; +} + +fn parse_detection(yaml: &Yaml) -> Option { + if yaml["detection"].is_badvalue() { + return Option::None; + } else { + let node = DetectionNode { + selection: parse_selection(&yaml), + }; + return Option::Some(node); + } +} + +fn concat_selection_key(key_list: &Vec) -> String { + return key_list + .iter() + .fold("detection -> selection".to_string(), |mut acc, cur| { + acc = acc + " -> " + cur; + return acc; + }); +} + +fn parse_selection(yaml: &Yaml) -> Option> { + // TODO detection-selectionが存在しない場合のチェック + let selection_yaml = &yaml["detection"]["selection"]; + if selection_yaml.is_badvalue() { + return Option::None; + } + return Option::Some(parse_selection_recursively(vec![], &selection_yaml)); +} + +fn parse_selection_recursively(key_list: Vec, yaml: &Yaml) -> Box { + if yaml.as_hash().is_some() { + // 連想配列はAND条件と解釈する + let yaml_hash = yaml.as_hash().unwrap(); + let mut and_node = AndSelectionNode::new(); + + yaml_hash.keys().for_each(|hash_key| { + let child_yaml = yaml_hash.get(hash_key).unwrap(); + let mut child_key_list = key_list.clone(); + child_key_list.push(hash_key.as_str().unwrap().to_string()); + let child_node = parse_selection_recursively(child_key_list, child_yaml); + and_node.child_nodes.push(child_node); + }); + return Box::new(and_node); + } else if yaml.as_vec().is_some() { + // 配列はOR条件と解釈する。 + let mut or_node = OrSelectionNode::new(); + yaml.as_vec().unwrap().iter().for_each(|child_yaml| { + let child_node = parse_selection_recursively(key_list.clone(), child_yaml); + or_node.child_nodes.push(child_node); + }); + + return Box::new(or_node); + } else { + // 連想配列と配列以外は末端ノード + return Box::new(LeafSelectionNode::new(key_list, yaml.clone())); + } +} + +// Ruleファイルを表すノード +pub struct RuleNode { + pub yaml: Yaml, + detection: Option, +} + +impl RuleNode { + pub fn init(&mut self) -> Result<(), Vec> { + let mut errmsgs: Vec = vec![]; + + // field check + if self.yaml["output"].as_str().unwrap_or("").is_empty() { + errmsgs.push("Cannot find required key. key:output".to_string()); + } + + // detection node initialization + self.detection.as_mut().and_then(|detection| { + let detection_result = detection.init(); + if detection_result.is_err() { + errmsgs.extend(detection_result.unwrap_err()); + } + return Option::Some(detection); + }); + + if errmsgs.is_empty() { + return Result::Ok(()); + } else { + return Result::Err(errmsgs); + } + } + + pub fn select(&mut self, event_record: &Value) -> bool { + let selection = self + .detection + .as_mut() + .and_then(|detect_node| detect_node.selection.as_mut()); + if selection.is_none() { + return false; + } + + return selection.unwrap().select(event_record); + } +} + +// Ruleファイルのdetectionを表すノード +struct DetectionNode { + pub selection: Option>, +} + +impl DetectionNode { + fn init(&mut self) -> Result<(), Vec> { + if self.selection.is_none() { + return Result::Ok(()); + } + + return self.selection.as_mut().unwrap().init(); + } +} + +// Ruleファイルの detection- selection配下のノードはこのtraitを実装する。 +trait SelectionNode { + fn select(&mut self, event_record: &Value) -> bool; + fn init(&mut self) -> Result<(), Vec>; +} + +// detection - selection配下でAND条件を表すノード +struct AndSelectionNode { + pub child_nodes: Vec>, +} + +impl AndSelectionNode { + pub fn new() -> AndSelectionNode { + return AndSelectionNode { + child_nodes: vec![], + }; + } +} + +impl SelectionNode for AndSelectionNode { + fn select(&mut self, event_record: &Value) -> bool { + return self.child_nodes.iter_mut().all(|child_node| { + return child_node.select(event_record); + }); + } + + fn init(&mut self) -> Result<(), Vec> { + let err_msgs = self + .child_nodes + .iter_mut() + .map(|child_node| { + let res = child_node.init(); + if res.is_err() { + return res.unwrap_err(); + } else { + return vec![]; + } + }) + .fold( + vec![], + |mut acc: Vec, cur: Vec| -> Vec { + acc.extend(cur.into_iter()); + return acc; + }, + ); + + if err_msgs.is_empty() { + return Result::Ok(()); + } else { + return Result::Err(err_msgs); + } + } +} + +// detection - selection配下でOr条件を表すノード +struct OrSelectionNode { + pub child_nodes: Vec>, +} + +impl OrSelectionNode { + pub fn new() -> OrSelectionNode { + return OrSelectionNode { + child_nodes: vec![], + }; + } +} + +impl SelectionNode for OrSelectionNode { + fn select(&mut self, event_record: &Value) -> bool { + return self.child_nodes.iter_mut().any(|child_node| { + return child_node.select(event_record); + }); + } + + fn init(&mut self) -> Result<(), Vec> { + let err_msgs = self + .child_nodes + .iter_mut() + .map(|child_node| { + let res = child_node.init(); + if res.is_err() { + return res.unwrap_err(); + } else { + return vec![]; + } + }) + .fold( + vec![], + |mut acc: Vec, cur: Vec| -> Vec { + acc.extend(cur.into_iter()); + return acc; + }, + ); + + if err_msgs.is_empty() { + return Result::Ok(()); + } else { + return Result::Err(err_msgs); + } + } +} + +// detection - selection配下の末端ノード +struct LeafSelectionNode { + key_list: Vec, + select_value: Yaml, + matcher: Option>, +} + +impl LeafSelectionNode { + fn new(key_list: Vec, value_yaml: Yaml) -> LeafSelectionNode { + return LeafSelectionNode { + key_list: key_list, + select_value: value_yaml, + matcher: Option::None, + }; + } + + // JSON形式のEventJSONから値を取得する関数 aliasも考慮されている。 + fn get_event_value<'a>(&self, event_value: &'a Value) -> Option<&'a Value> { + if self.key_list.is_empty() { + return Option::None; + } + + return utils::get_event_value(&self.key_list[0].to_string(), event_value); + } + + // LeafMatcherの一覧を取得する。 + fn get_matchers(&self) -> Vec> { + return vec![ + Box::new(RegexMatcher::new()), + Box::new(MinlengthMatcher::new()), + Box::new(RegexesFileMatcher::new()), + Box::new(WhitelistFileMatcher::new()), + ]; + } +} + +impl SelectionNode for LeafSelectionNode { + fn select(&mut self, event_record: &Value) -> bool { + if self.matcher.is_none() { + return false; + } + + let event_value = self.get_event_value(event_record); + return self.matcher.as_mut().unwrap().is_match(event_value); + } + + fn init(&mut self) -> Result<(), Vec> { + let matchers = self.get_matchers(); + let mut match_key_list = self.key_list.clone(); + match_key_list.remove(0); + self.matcher = matchers + .into_iter() + .find(|matcher| matcher.is_target_key(&match_key_list)); + // 一致するmatcherが見つからないエラー + if self.matcher.is_none() { + return Result::Err(vec![format!( + "Found unknown key. key:{}", + concat_selection_key(&match_key_list) + )]); + } + + if self.select_value.is_badvalue() { + return Result::Err(vec![format!( + "Cannot parse yaml file. key:{}", + concat_selection_key(&match_key_list) + )]); + } + + return self + .matcher + .as_mut() + .unwrap() + .init(&match_key_list, &self.select_value); + } +} + +// 末端ノードがEventLogの値を比較するロジックを表す。 +// 正規条件のマッチや文字数制限など、比較ロジック毎にこのtraitを実装したクラスが存在する。 +// +// 新規にLeafMatcherを実装するクラスを作成した場合、 +// LeafSelectionNodeのget_matchersクラスの戻り値の配列に新規作成したクラスのインスタンスを追加する。 +trait LeafMatcher { + fn is_target_key(&self, key_list: &Vec) -> bool; + + fn is_match(&mut self, event_value: Option<&Value>) -> bool; + + fn init(&mut self, key_list: &Vec, select_value: &Yaml) -> Result<(), Vec>; +} + +// 正規表現で比較するロジックを表すクラス +struct RegexMatcher { + re: Option, +} + +impl RegexMatcher { + fn new() -> RegexMatcher { + return RegexMatcher { + re: Option::None, // empty + }; + } + fn is_regex_fullmatch(&self, re: &Regex, value: String) -> bool { + return re.find_iter(&value).any(|match_obj| { + return match_obj.as_str().to_string() == value; + }); + } +} + +impl LeafMatcher for RegexMatcher { + fn is_target_key(&self, key_list: &Vec) -> bool { + if key_list.is_empty() { + return true; + } + + if key_list.len() == 1 { + return key_list.get(0).unwrap_or(&"".to_string()) == &"regex".to_string(); + } else { + return false; + } + } + + fn init(&mut self, key_list: &Vec, select_value: &Yaml) -> Result<(), Vec> { + if select_value.is_null() { + self.re = Option::None; + return Result::Ok(()); + } + + // stringで比較する。 + let yaml_value = match select_value { + Yaml::Boolean(b) => Option::Some(b.to_string()), + Yaml::Integer(i) => Option::Some(i.to_string()), + Yaml::Real(r) => Option::Some(r.to_string()), + Yaml::String(s) => Option::Some(s.to_owned()), + _ => Option::None, + }; + // ここには来ないはず + if yaml_value.is_none() { + let errmsg = format!( + "unknown error occured. [key:{}]", + concat_selection_key(key_list) + ); + return Result::Err(vec![errmsg]); + } + + // 指定された正規表現が間違っていて、パースに失敗した場合 + let yaml_str = yaml_value.unwrap(); + let re_result = Regex::new(&yaml_str); + if re_result.is_err() { + let errmsg = format!( + "cannot parse regex. [regex:{}, key:{}]", + yaml_str, + concat_selection_key(key_list) + ); + return Result::Err(vec![errmsg]); + } + self.re = re_result.ok(); + + return Result::Ok(()); + } + + fn is_match(&mut self, event_value: Option<&Value>) -> bool { + // unwrap_orの引数に""ではなく" "を指定しているのは、 + // event_valueが文字列じゃない場合にis_event_value_nullの値がfalseになるように、len() == 0とならない値を指定している。 + let is_event_value_null = event_value.is_none() + || event_value.unwrap().is_null() + || event_value.unwrap().as_str().unwrap_or(" ").len() == 0; + + // yamlにnullが設定されていた場合 + if self.re.is_none() { + return is_event_value_null; + } + + return match event_value.unwrap_or(&Value::Null) { + Value::Bool(b) => self.is_regex_fullmatch(self.re.as_ref().unwrap(), b.to_string()), + Value::String(s) => self.is_regex_fullmatch(self.re.as_ref().unwrap(), s.to_owned()), + Value::Number(n) => self.is_regex_fullmatch(self.re.as_ref().unwrap(), n.to_string()), + _ => false, + }; + } +} + +// 指定された文字数以上であることをチェックするクラス。 +struct MinlengthMatcher { + min_len: i64, +} + +impl MinlengthMatcher { + fn new() -> MinlengthMatcher { + return MinlengthMatcher { min_len: 0 }; + } +} + +impl LeafMatcher for MinlengthMatcher { + fn is_target_key(&self, key_list: &Vec) -> bool { + if key_list.len() != 1 { + return false; + } + + return key_list.get(0).unwrap() == "min_length"; + } + + fn init(&mut self, key_list: &Vec, select_value: &Yaml) -> Result<(), Vec> { + let min_length = select_value.as_i64(); + if min_length.is_none() { + let errmsg = format!( + "min_length value should be Integer. [key:{}]", + concat_selection_key(key_list) + ); + return Result::Err(vec![errmsg]); + } + + self.min_len = min_length.unwrap(); + return Result::Ok(()); + } + + fn is_match(&mut self, event_value: Option<&Value>) -> bool { + return match event_value.unwrap_or(&Value::Null) { + Value::String(s) => s.len() as i64 >= self.min_len, + Value::Number(n) => n.to_string().len() as i64 >= self.min_len, + _ => false, + }; + } +} + +// 正規表現のリストが記載されたファイルを読み取って、比較するロジックを表すクラス +// DeepBlueCLIのcheck_cmdメソッドの一部に同様の処理が実装されていた。 +struct RegexesFileMatcher { + regexes_csv_content: Vec>, +} + +impl RegexesFileMatcher { + fn new() -> RegexesFileMatcher { + return RegexesFileMatcher { + regexes_csv_content: vec![], + }; + } +} + +impl LeafMatcher for RegexesFileMatcher { + fn is_target_key(&self, key_list: &Vec) -> bool { + if key_list.len() != 1 { + return false; + } + + return key_list.get(0).unwrap() == "regexes"; + } + + fn init(&mut self, key_list: &Vec, select_value: &Yaml) -> Result<(), Vec> { + let value = match select_value { + Yaml::String(s) => Option::Some(s.to_owned()), + Yaml::Integer(i) => Option::Some(i.to_string()), + Yaml::Real(r) => Option::Some(r.to_owned()), + _ => Option::None, + }; + if value.is_none() { + let errmsg = format!( + "regexes value should be String. [key:{}]", + concat_selection_key(key_list) + ); + return Result::Err(vec![errmsg]); + } + + let csv_content = utils::read_csv(&value.unwrap()); + if csv_content.is_err() { + let errmsg = format!( + "cannot read regexes file. [key:{}]", + concat_selection_key(key_list) + ); + return Result::Err(vec![errmsg]); + } + self.regexes_csv_content = csv_content.unwrap(); + + return Result::Ok(()); + } + + fn is_match(&mut self, event_value: Option<&Value>) -> bool { + return match event_value.unwrap_or(&Value::Null) { + Value::String(s) => !utils::check_regex(s, 0, &self.regexes_csv_content).is_empty(), + Value::Number(n) => { + !utils::check_regex(&n.to_string(), 0, &self.regexes_csv_content).is_empty() + } + _ => false, + }; + } +} + +// ファイルに列挙された文字列に一致する場合に検知するロジックを表す +// DeepBlueCLIのcheck_cmdメソッドの一部に同様の処理が実装されていた。 +struct WhitelistFileMatcher { + whitelist_csv_content: Vec>, +} + +impl WhitelistFileMatcher { + fn new() -> WhitelistFileMatcher { + return WhitelistFileMatcher { + whitelist_csv_content: vec![], + }; + } +} + +impl LeafMatcher for WhitelistFileMatcher { + fn is_target_key(&self, key_list: &Vec) -> bool { + if key_list.len() != 1 { + return false; + } + + return key_list.get(0).unwrap() == "whitelist"; + } + + fn init(&mut self, key_list: &Vec, select_value: &Yaml) -> Result<(), Vec> { + let value = match select_value { + Yaml::String(s) => Option::Some(s.to_owned()), + Yaml::Integer(i) => Option::Some(i.to_string()), + Yaml::Real(r) => Option::Some(r.to_owned()), + _ => Option::None, + }; + if value.is_none() { + let errmsg = format!( + "whitelist value should be String. [key:{}]", + concat_selection_key(key_list) + ); + return Result::Err(vec![errmsg]); + } + + let csv_content = utils::read_csv(&value.unwrap()); + if csv_content.is_err() { + let errmsg = format!( + "cannot read whitelist file. [key:{}]", + concat_selection_key(key_list) + ); + return Result::Err(vec![errmsg]); + } + self.whitelist_csv_content = csv_content.unwrap(); + + return Result::Ok(()); + } + + fn is_match(&mut self, event_value: Option<&Value>) -> bool { + return match event_value.unwrap_or(&Value::Null) { + Value::String(s) => utils::check_whitelist(s, &self.whitelist_csv_content), + Value::Number(n) => utils::check_whitelist(&n.to_string(), &self.whitelist_csv_content), + Value::Bool(b) => utils::check_whitelist(&b.to_string(), &self.whitelist_csv_content), + _ => false, + }; + } +} diff --git a/src/detections/security.rs b/src/detections/security.rs deleted file mode 100644 index 44ce23ae..00000000 --- a/src/detections/security.rs +++ /dev/null @@ -1,1348 +0,0 @@ -use crate::detections::utils; -use crate::models::event; -use std::collections::HashMap; - -#[derive(Debug)] -pub struct Security { - max_total_sensitive_privuse: i32, - max_passspray_login: i32, - max_passspray_uniquser: i32, - max_failed_logons: i32, - alert_all_admin: i32, - total_admin_logons: i32, - total_failed_logons: i32, - total_failed_account: i32, - total_sensitive_privuse: i32, - admin_logons: HashMap>, - multiple_admin_logons: HashMap, - account_2_failedcnt: HashMap, - passspray_2_user: HashMap, - empty_str: String, -} - -impl Security { - pub fn new() -> Security { - Security { - max_total_sensitive_privuse: 4, - max_passspray_login: 6, - max_passspray_uniquser: 6, - max_failed_logons: 5, - alert_all_admin: 0, - total_admin_logons: 0, - total_failed_logons: 0, - total_failed_account: 0, - total_sensitive_privuse: 0, - admin_logons: HashMap::new(), - multiple_admin_logons: HashMap::new(), - account_2_failedcnt: HashMap::new(), - passspray_2_user: HashMap::new(), - empty_str: String::default(), - } - } - - pub fn disp(&self) { - self.disp_admin_logons().and_then(Security::print_console); - self.disp_login_failed().and_then(Security::print_console); - } - - fn disp_admin_logons(&self) -> Option> { - if self.total_admin_logons < 1 { - return Option::None; - } - - let mut msges: Vec = Vec::new(); - msges.push(format!("total_admin_logons:{}", self.total_admin_logons)); - msges.push(format!("admin_logons:{:?}", self.admin_logons)); - msges.push(format!( - "multiple_admin_logons:{:?}", - self.multiple_admin_logons - )); - - return Option::Some(msges); - } - - fn disp_login_failed(&self) -> Option> { - let exceed_failed_logons = self.total_failed_logons <= self.max_failed_logons; - - let exist_failed_account = self.account_2_failedcnt.keys().count() as i32 <= 1; - if exceed_failed_logons || exist_failed_account { - return Option::None; - } - - let mut msges: Vec = Vec::new(); - msges.push(format!( - "High number of total logon failures for multiple accounts" - )); - msges.push(format!( - "Total accounts: {}", - self.account_2_failedcnt.keys().count() - )); - msges.push(format!( - "Total logon failures: {}", - self.total_failed_logons - )); - - return Option::Some(msges); - } - - pub fn detection( - &mut self, - event_id: String, - _system: &event::System, - user_data: &Option, - event_data: HashMap, - ) { - self.process_created(&event_id, &event_data); - self.se_debug_privilege(&event_id, &event_data); - self.account_created(&event_id, &event_data) - .and_then(Security::print_console); - self.add_member_security_group(&event_id, &event_data) - .and_then(Security::print_console); - self.failed_logon(&event_id, &event_data); - self.sensitive_priviledge(&event_id, &event_data) - .and_then(Security::print_console); - self.attempt_priviledge(&event_id, &event_data) - .and_then(Security::print_console); - self.pass_spray(&event_id, &event_data) - .and_then(Security::print_console); - self.audit_log_cleared(&event_id, &user_data) - .and_then(Security::print_console); - } - - fn print_console(v: Vec) -> Option> { - v.iter().for_each(|s| println!("{}", s)); - println!("\n"); - return Option::Some(v); - } - - fn process_created(&mut self, event_id: &String, event_data: &HashMap) { - if event_id != "4688" { - return; - } - - let commandline = event_data.get("CommandLine").unwrap_or(&self.empty_str); - let creator = event_data - .get("ParentProcessName") - .unwrap_or(&self.empty_str); - utils::check_command(4688, &commandline, 1000, 0, &self.empty_str, &creator); - } - - // - // Special privileges assigned to new logon (possible admin access) - // - fn se_debug_privilege(&mut self, event_id: &String, event_data: &HashMap) { - if event_id != "4672" { - return; - } - - if let Some(privileage_list) = event_data.get("PrivilegeList") { - if let Some(_data) = privileage_list.find("SeDebugPrivilege") { - // alert_all_adminが有効であれば、標準出力して知らせる - // DeepBlueCLIでは必ず0になっていて、基本的には表示されない。 - if self.alert_all_admin == 1 { - println!("Logon with SeDebugPrivilege (admin access)"); - println!("Username:{}", event_data["SubjectUserName"]); - println!("Domain:{}", event_data["SubjectDomainName"]); - println!("User SID:{}", event_data["SubjectUserSid"]); - println!("Domain:{}", event_data["PrivilegeList"]); - } - - self.total_admin_logons += 1; - - // admin_logons配列にusernameが含まれているか確認 - match self.admin_logons.get(&event_data["SubjectUserName"]) { - Some(sid) => { - // 含まれていれば、マルチユーザが管理者としてログインしているか確認 - // マルチログオンのデータをセット - if event_data["SubjectUserName"] != event_data["SubjectUserSid"] { - // One username with multiple admin logon SIDs - self.multiple_admin_logons - .insert(event_data["SubjectUserName"].to_string(), 1); - - let mut count_hash: HashMap = HashMap::new(); - count_hash.insert( - event_data["SubjectUserSid"].to_string(), - sid[&event_data["SubjectUserSid"]] + 1, - ); - self.admin_logons - .insert(event_data["SubjectUserName"].to_string(), count_hash); - } - } - None => { - // admin_logons配列にセットUserNameとSIDとカウンタをセット - let mut count_hash: HashMap = HashMap::new(); - count_hash.insert(event_data["SubjectUserSid"].to_string(), 1); - self.admin_logons - .insert(event_data["SubjectUserName"].to_string(), count_hash); - } - } - } - } - } - - // account craeted:OK - fn account_created( - &mut self, - event_id: &String, - event_data: &HashMap, - ) -> Option> { - if event_id != "4720" { - return Option::None; - } - - let mut msges: Vec = Vec::new(); - msges.push("New User Created".to_string()); - - let username = event_data.get("TargetUserName").unwrap_or(&self.empty_str); - msges.push(format!("Username: {}", username)); - let sid = event_data.get("TargetSid").unwrap_or(&self.empty_str); - msges.push(format!("User SID: {}", sid)); - - return Option::Some(msges); - } - - // add member to security group - fn add_member_security_group( - &mut self, - event_id: &String, - event_data: &HashMap, - ) -> Option> { - // check if group is Administrator, may later expand to all groups - if event_data.get("TargetUserName").unwrap_or(&self.empty_str) != "Administrators" { - return Option::None; - } - - // A member was added to a security-enabled (global|local|universal) group. - let mut msges: Vec = Vec::new(); - if event_id == "4728" { - msges.push("User added to global Administrators group".to_string()); - } else if event_id == "4732" { - msges.push("User added to local Administrators group".to_string()); - } else if event_id == "4756" { - msges.push("User added to universal Administrators group".to_string()); - } else { - return Option::None; - } - - let username = event_data.get("MemberName").unwrap_or(&self.empty_str); - msges.push(format!("Username: {}", username)); - let sid = event_data.get("MemberSid").unwrap_or(&self.empty_str); - msges.push(format!("User SID: {}", sid)); - - return Option::Some(msges); - } - - // An account failed to log on.:OK - // Requires auditing logon failures - // https://technet.microsoft.com/en-us/library/cc976395.aspx - fn failed_logon(&mut self, event_id: &String, event_data: &HashMap) { - if event_id != "4625" { - return; - } - - // see fn disp() - self.total_failed_logons += 1; - let username = event_data.get("TargetUserName").unwrap_or(&self.empty_str); - let failed_cnt = self.account_2_failedcnt.get(username).unwrap_or(&0) + 1; - self.account_2_failedcnt - .insert(username.to_string(), failed_cnt); - } - - // Sensitive Privilege Use (Mimikatz) - fn sensitive_priviledge( - &mut self, - event_id: &String, - event_data: &HashMap, - ) -> Option> { - if event_id != "4673" { - return Option::None; - } - - self.total_sensitive_privuse += 1; - let mut msges: Vec = Vec::new(); - // use == operator here to avoid multiple log notices - if self.max_total_sensitive_privuse != self.total_sensitive_privuse { - return Option::None; - } - - msges.push("Sensititive Privilege Use Exceeds Threshold".to_string()); - msges.push( - "Potentially indicative of Mimikatz, multiple sensitive privilege calls have been made" - .to_string(), - ); - - let username = event_data.get("SubjectUserName").unwrap_or(&self.empty_str); - msges.push(format!("Username: {}", username)); - - let domainname = event_data - .get("SubjectDomainName") - .unwrap_or(&self.empty_str); - msges.push(format!("Domain Name: {}", domainname)); - - return Option::Some(msges); - } - - fn attempt_priviledge( - &mut self, - event_id: &String, - event_data: &HashMap, - ) -> Option> { - if event_id != "4674" { - return Option::None; - } - - // "%%1539" means WRITE_DAC(see detail: https://docs.microsoft.com/ja-jp/windows/security/threat-protection/auditing/event-4663) - let servicename = event_data - .get("ProcessName") - .unwrap_or(&self.empty_str) - .to_uppercase(); - let accessname = event_data.get("AccessMask").unwrap_or(&self.empty_str); - if servicename != r"C:\WINDOWS\SYSTEM32\SERVICES.EXE" || accessname != "%%1539" { - return Option::None; - } - - let mut msges: Vec = Vec::new(); - msges.push("Possible Hidden Service Attempt".to_string()); - msges.push("User requested to modify the Dynamic Access Control (DAC) permissions of a sevice, possibly to hide it from view".to_string()); - - let username = event_data.get("SubjectUserName").unwrap_or(&self.empty_str); - msges.push(format!("User: {}", username)); - - let servicename = event_data.get("ObjectName").unwrap_or(&self.empty_str); - msges.push(format!("Target service: {}", servicename)); - - msges.push("WRITE_DAC".to_string()); - - return Option::Some(msges); - } - - // A logon was attempted using explicit credentials. - fn pass_spray( - &mut self, - event_id: &String, - event_data: &HashMap, - ) -> Option> { - if event_id != "4648" { - return Option::None; - } - - let targetusername = event_data.get("TargetUserName").unwrap_or(&self.empty_str); - let spray_cnt = self.passspray_2_user.get(targetusername).unwrap_or(&0) + 1; - self.passspray_2_user - .insert(targetusername.to_string(), spray_cnt); - - // check exceeded targetuser count. - let spray_uniq_user = self - .passspray_2_user - .values() - .filter(|value| value > &&self.max_passspray_login) - .count() as i32; - if spray_uniq_user <= self.max_passspray_uniquser { - return Option::None; - } - - // let v_username = Vec::new(); - let mut v_username = Vec::new(); - self.passspray_2_user - .keys() - .for_each(|u| v_username.push(u)); - v_username.sort(); - let usernames: String = v_username.iter().fold( - self.empty_str.to_string(), - |mut acc: String, cur| -> String { - acc.push_str(cur); - acc.push_str(" "); - return acc; - }, - ); - - let mut msges: Vec = Vec::new(); - msges.push( - "Distributed Account Explicit Credential Use (Password Spray Attack)".to_string(), - ); - msges.push( - "The use of multiple user account access attempts with explicit credentials is " - .to_string(), - ); - msges.push("an indicator of a password spray attack".to_string()); - - msges.push(format!("Target Usernames: {}", usernames.trim())); - let access_username = event_data.get("SubjectUserName").unwrap_or(&self.empty_str); - - msges.push(format!("Accessing Username: {}", access_username)); - - let access_hostname = event_data - .get("SubjectDomainName") - .unwrap_or(&self.empty_str); - msges.push(format!("Accessing Host Name: {}", access_hostname)); - - // reset - self.passspray_2_user = HashMap::new(); - - return Option::Some(msges); - } - - fn audit_log_cleared( - &mut self, - event_id: &String, - user_data: &Option, - ) -> Option> { - if event_id != "1102" { - return Option::None; - } - - let mut msges: Vec = Vec::new(); - msges.push("Audit Log Clear".to_string()); - msges.push("The Audit log was cleared".to_string()); - let username = user_data - .as_ref() - .and_then(|u| u.log_file_cleared.as_ref()) - .and_then(|l| l.subject_user_name.as_ref()); - msges.push(format!( - "Security ID: {}", - username.unwrap_or(&self.empty_str) - )); - - return Option::Some(msges); - } -} - -#[cfg(test)] -mod tests { - extern crate quick_xml; - - use crate::detections::security; - use crate::models::event; - - // 正しくヒットするパターン - #[test] - fn test_account_created_hit() { - let xml_str = get_account_created_xml(); - let event: event::Evtx = quick_xml::de::from_str(&xml_str) - .map_err(|e| { - println!("{}", e.to_string()); - }) - .unwrap(); - - let mut sec = security::Security::new(); - let option_v = sec.account_created( - &event.system.event_id.to_string(), - &event.parse_event_data(), - ); - - let v = option_v.unwrap(); - let mut ite = v.iter(); - assert_eq!( - &"New User Created".to_string(), - ite.next().unwrap_or(&"".to_string()) - ); - assert_eq!( - &"Username: IEUser".to_string(), - ite.next().unwrap_or(&"".to_string()) - ); - assert_eq!( - &"User SID: S-1-5-21-3463664321-2923530833-3546627382-1000", - ite.next().unwrap_or(&"".to_string()) - ); - assert_eq!(Option::None, ite.next()); - } - - // event idが異なるパターン - #[test] - fn test_account_created_noteq_eventid() { - let xml_str = - get_account_created_xml().replace("4720", "4721"); - let event: event::Evtx = quick_xml::de::from_str(&xml_str) - .map_err(|e| { - println!("{}", e.to_string()); - }) - .unwrap(); - - let mut sec = security::Security::new(); - let option_v = sec.account_created( - &event.system.event_id.to_string(), - &event.parse_event_data(), - ); - - assert_eq!(Option::None, option_v); - } - - // 実在するかどうか不明だが、EventDataの必要なフィールドがないパターン - #[test] - fn test_account_created_none_check() { - let xml_str = r#" - - - - - 4720 - 0 - 0 - 13824 - 0 - 0x8020000000000000 - - 112 - - - Security - IE8Win7 - - - - "#; - let event: event::Evtx = quick_xml::de::from_str(&xml_str) - .map_err(|e| { - println!("{}", e.to_string()); - }) - .unwrap(); - - let mut sec = security::Security::new(); - let option_v = sec.account_created( - &event.system.event_id.to_string(), - &event.parse_event_data(), - ); - - let v = option_v.unwrap(); - let mut ite = v.iter(); - assert_eq!( - &"New User Created".to_string(), - ite.next().unwrap_or(&"".to_string()) - ); - assert_eq!( - &"Username: ".to_string(), - ite.next().unwrap_or(&"".to_string()) - ); - assert_eq!(&"User SID: ", ite.next().unwrap_or(&"".to_string())); - assert_eq!(Option::None, ite.next()); - } - - fn get_account_created_xml() -> String { - return r#" - - - - - 4720 - 0 - 0 - 13824 - 0 - 0x8020000000000000 - - 112 - - - Security - IE8Win7 - - - - IEUser - IE8Win7 - S-1-5-21-3463664321-2923530833-3546627382-1000 - S-1-5-18 - WIN-QALA5Q3KJ43$ - WORKGROUP - 0x3e7 - - - IEUserSam - %%1793 - - - %%1793 - %%1793 - %%1793 - %%1793 - %%1793 - %%1794 - %%1794 - 513 - - - 0x0 - 0x15 - - %%2080 - %%2082 - %%2084 - %%1793 - - - %%1797 - - "#.to_string(); - } - - // 正しくヒットするパターン(eventid=4732) - #[test] - fn test_add_member_security_group_hit_4732() { - let xml_str = get_add_member_security_group_xml(); - let event: event::Evtx = quick_xml::de::from_str(&xml_str) - .map_err(|e| { - println!("{}", e.to_string()); - }) - .unwrap(); - - let mut sec = security::Security::new(); - let option_v = sec.add_member_security_group( - &event.system.event_id.to_string(), - &event.parse_event_data(), - ); - - let v = option_v.unwrap(); - let mut ite = v.iter(); - assert_eq!( - &"User added to local Administrators group".to_string(), - ite.next().unwrap_or(&"".to_string()) - ); - assert_eq!( - &"Username: testnamess".to_string(), - ite.next().unwrap_or(&"".to_string()) - ); - assert_eq!( - &"User SID: S-1-5-21-3463664321-2923530833-3546627382-1000", - ite.next().unwrap_or(&"".to_string()) - ); - assert_eq!(Option::None, ite.next()); - } - - // 正しくヒットするパターン(eventid=4728は一行目が変わる) - #[test] - fn test_add_member_security_group_hit_4728() { - let xml_str = get_add_member_security_group_xml() - .replace(r"4732", r"4728"); - let event: event::Evtx = quick_xml::de::from_str(&xml_str) - .map_err(|e| { - println!("{}", e.to_string()); - }) - .unwrap(); - - let mut sec = security::Security::new(); - let option_v = sec.add_member_security_group( - &event.system.event_id.to_string(), - &event.parse_event_data(), - ); - - let v = option_v.unwrap(); - let mut ite = v.iter(); - assert_eq!( - &"User added to global Administrators group".to_string(), - ite.next().unwrap_or(&"".to_string()) - ); - assert_eq!( - &"Username: testnamess".to_string(), - ite.next().unwrap_or(&"".to_string()) - ); - assert_eq!( - &"User SID: S-1-5-21-3463664321-2923530833-3546627382-1000", - ite.next().unwrap_or(&"".to_string()) - ); - assert_eq!(Option::None, ite.next()); - } - - // 正しくヒットするパターン(eventid=4756は一行目が変わる) - #[test] - fn test_add_member_security_group_hit_4756() { - let xml_str = get_add_member_security_group_xml() - .replace(r"4732", r"4756"); - let event: event::Evtx = quick_xml::de::from_str(&xml_str) - .map_err(|e| { - println!("{}", e.to_string()); - }) - .unwrap(); - - let mut sec = security::Security::new(); - let option_v = sec.add_member_security_group( - &event.system.event_id.to_string(), - &event.parse_event_data(), - ); - - let v = option_v.unwrap(); - let mut ite = v.iter(); - assert_eq!( - &"User added to universal Administrators group".to_string(), - ite.next().unwrap_or(&"".to_string()) - ); - assert_eq!( - &"Username: testnamess".to_string(), - ite.next().unwrap_or(&"".to_string()) - ); - assert_eq!( - &"User SID: S-1-5-21-3463664321-2923530833-3546627382-1000", - ite.next().unwrap_or(&"".to_string()) - ); - assert_eq!(Option::None, ite.next()); - } - - // eventidが異なりヒットしないパターン - #[test] - fn test_add_member_security_group_noteq_eventid() { - let xml_str = get_add_member_security_group_xml() - .replace(r"4732", r"4757"); - let event: event::Evtx = quick_xml::de::from_str(&xml_str) - .map_err(|e| { - println!("{}", e.to_string()); - }) - .unwrap(); - - let mut sec = security::Security::new(); - let option_v = sec.add_member_security_group( - &event.system.event_id.to_string(), - &event.parse_event_data(), - ); - assert_eq!(Option::None, option_v); - } - - // グループがAdministratorsじゃなくてHitしないパターン - #[test] - fn test_add_member_security_not_administrators() { - let xml_str = get_add_member_security_group_xml().replace( - r"Administrators", - r"local", - ); - let event: event::Evtx = quick_xml::de::from_str(&xml_str) - .map_err(|e| { - println!("{}", e.to_string()); - }) - .unwrap(); - - let mut sec = security::Security::new(); - let option_v = sec.add_member_security_group( - &event.system.event_id.to_string(), - &event.parse_event_data(), - ); - assert_eq!(Option::None, option_v); - } - - // hitするけど表示するフィールドがない場合 - #[test] - fn test_add_member_security_group_none() { - let xml_str = r#" - - - - - 4732 - 0 - 0 - 13826 - 0 - 0x8020000000000000 - - 116 - - - Security - IE8Win7 - - - - Administrators - - "#; - let event: event::Evtx = quick_xml::de::from_str(&xml_str) - .map_err(|e| { - println!("{}", e.to_string()); - }) - .unwrap(); - - let mut sec = security::Security::new(); - let option_v = sec.add_member_security_group( - &event.system.event_id.to_string(), - &event.parse_event_data(), - ); - - let v = option_v.unwrap(); - let mut ite = v.iter(); - assert_eq!( - &"User added to local Administrators group".to_string(), - ite.next().unwrap_or(&"".to_string()) - ); - assert_eq!( - &"Username: ".to_string(), - ite.next().unwrap_or(&"".to_string()) - ); - assert_eq!(&"User SID: ", ite.next().unwrap_or(&"".to_string())); - assert_eq!(Option::None, ite.next()); - } - - fn get_add_member_security_group_xml() -> String { - return r#" - - - - - 4732 - 0 - 0 - 13826 - 0 - 0x8020000000000000 - - 116 - - - Security - IE8Win7 - - - - testnamess - S-1-5-21-3463664321-2923530833-3546627382-1000 - Administrators - Builtin - S-1-5-32-544 - S-1-5-18 - WIN-QALA5Q3KJ43$ - WORKGROUP - 0x3e7 - - - - "#.to_string(); - } - - // ユーザー数が一つなら、ログ数が幾らあっても、メッセージは表示されないはず。 - #[test] - fn test_failed_logon_nothit_onlyoneuser() { - let xml_str = get_failed_logon_xml(); - let event: event::Evtx = quick_xml::de::from_str(&xml_str).unwrap(); - - let mut sec = security::Security::new(); - - sec.max_failed_logons = 5; - let ite = [1, 2, 3, 4, 5, 6, 7].iter(); - ite.for_each(|i| { - sec.failed_logon( - &event.system.event_id.to_string(), - &event.parse_event_data(), - ); - assert_eq!(i, &sec.total_failed_logons); - assert_eq!(Option::None, sec.disp_login_failed()); - }); - } - - // 失敗回数を増やしていき、境界値でメッセージが表示されることのテスト。 - #[test] - fn test_failed_logon_hit() { - let xml_str = get_failed_logon_xml(); - let event: event::Evtx = quick_xml::de::from_str(&xml_str).unwrap(); - let event_another: event::Evtx = quick_xml::de::from_str(&xml_str.replace( - r"Administrator", - r"localuser", - )) - .unwrap(); - - let mut sec = security::Security::new(); - sec.max_failed_logons = 5; - - // メッセージが表示されるには2ユーザー以上失敗している必要がある。まず一人目 - sec.failed_logon( - &event.system.event_id.to_string(), - &event.parse_event_data(), - ); - assert_eq!(1, sec.total_failed_logons); - - let ite = [1, 2, 3, 4, 5, 6, 7].iter(); - ite.for_each(|i| { - sec.failed_logon( - &event_another.system.event_id.to_string(), - &event_another.parse_event_data(), - ); - let fail_cnt = i + 1; - assert_eq!(fail_cnt, sec.total_failed_logons); - if fail_cnt > 5 { - let v = sec.disp_login_failed().unwrap(); - let mut ite = v.iter(); - assert_eq!( - &"High number of total logon failures for multiple accounts".to_string(), - ite.next().unwrap_or(&"".to_string()) - ); - assert_eq!( - &"Total accounts: 2".to_string(), - ite.next().unwrap_or(&"".to_string()) - ); - assert_eq!( - &format!("Total logon failures: {}", fail_cnt), - ite.next().unwrap_or(&"".to_string()) - ); - // assert_eq!(Option::None, ite.next()); - } else { - assert_eq!(Option::None, sec.disp_login_failed()); - } - }); - - // hitするけど表示するフィールドがない場合 - let xml_nofield = r#" - - - 4625001254400x80100000000000006016SecurityDESKTOP-M5SN04R - - "#; - - // エラーにならなければOK - let event_nofield: event::Evtx = quick_xml::de::from_str(xml_nofield).unwrap(); - sec.failed_logon( - &event_nofield.system.event_id.to_string(), - &event_nofield.parse_event_data(), - ); - } - - // 失敗回数を増やしていき、境界値でメッセージが表示されることのテスト。 - #[test] - fn test_failed_logon_noteq_eventid() { - let xml_str = get_failed_logon_xml(); - let event: event::Evtx = quick_xml::de::from_str( - &xml_str.replace(r"4625", r"4626"), - ) - .unwrap(); - let event_another: event::Evtx = quick_xml::de::from_str( - &xml_str - .replace(r"4625", r"4626") - .replace( - r"Administrator", - r"localuser", - ), - ) - .unwrap(); - - let mut sec = security::Security::new(); - sec.max_failed_logons = 5; - - // メッセージが表示されるには2ユーザー以上失敗している必要がある。まず一人目 - sec.failed_logon( - &event.system.event_id.to_string(), - &event.parse_event_data(), - ); - assert_eq!(0, sec.total_failed_logons); - - let ite = [1, 2, 3, 4, 5, 6, 7].iter(); - ite.for_each(|_i| { - sec.failed_logon( - &event_another.system.event_id.to_string(), - &event_another.parse_event_data(), - ); - assert_eq!(0, sec.total_failed_logons); - assert_eq!(Option::None, sec.disp_login_failed()); - }); - } - - fn get_failed_logon_xml() -> String { - return r#" - - - - 4625 - 0 - 0 - 12544 - 0 - 0x8010000000000000 - - 6016 - - - Security - DESKTOP-M5SN04R - - - - S-1-0-0 - - - - - 0x0 - S-1-0-0 - Administrator - . - 0xc000006d - %%2313 - 0xc000006a - 3 - NtLmSsp - NTLM - fpEbpiox2Q3Qf8av - - - - - 0 - 0x0 - - - 192.168.198.149 - 33083 - - "# - .to_string(); - } - - // Hitするパターンとしないパターンをまとめてテスト - #[test] - fn test_sensitive_priviledge_hit() { - let xml_str = get_sensitive_prividedge_hit(); - let event: event::Evtx = quick_xml::de::from_str(&xml_str).unwrap(); - - let mut sec = security::Security::new(); - sec.max_total_sensitive_privuse = 6; - - let ite = [1, 2, 3, 4, 5, 6, 7].iter(); - ite.for_each(|i| { - let msg = sec.sensitive_priviledge(&event.system.event_id.to_string(), &event.parse_event_data()); - // i == 7ときにHitしない - if i == &6 { - let v = msg.unwrap(); - let mut ite = v.iter(); - assert_eq!( - &"Sensititive Privilege Use Exceeds Threshold".to_string(), - ite.next().unwrap_or(&"".to_string()) - ); - assert_eq!( - &"Potentially indicative of Mimikatz, multiple sensitive privilege calls have been made".to_string(), - ite.next().unwrap_or(&"".to_string()) - ); - assert_eq!( - &"Username: Sec504".to_string(), - ite.next().unwrap_or(&"".to_string()) - ); - assert_eq!( - &"Domain Name: SEC504STUDENT", - ite.next().unwrap_or(&"".to_string()) - ); - } else { - assert_eq!(Option::None, msg); - } - }); - } - - // eventidが異なるので、Hitしないテスト - #[test] - fn test_sensitive_priviledge_noteq_eventid() { - let xml_str = get_sensitive_prividedge_hit() - .replace(r"4673", r"4674"); - let event: event::Evtx = quick_xml::de::from_str(&xml_str).unwrap(); - - let mut sec = security::Security::new(); - sec.max_total_sensitive_privuse = 6; - - let ite = [1, 2, 3, 4, 5, 6, 7].iter(); - ite.for_each(|_i| { - let msg = sec.sensitive_priviledge( - &event.system.event_id.to_string(), - &event.parse_event_data(), - ); - assert_eq!(Option::None, msg); - }); - } - - fn get_sensitive_prividedge_hit() -> String { - return r#" - - - - 4673 - 0 - 0 - 13056 - 0 - 0x8010000000000000 - - 8936 - - - Security - Sec504Student - - - - S-1-5-21-2977773840-2930198165-1551093962-1000 - Sec504 - SEC504STUDENT - 0x1e3dd - Security - - - SeTcbPrivilege - 0x15a8 - C:\Tools\mimikatz\mimikatz.exe - - "#.to_string(); - } - - // Hitするテスト - #[test] - fn test_attempt_priviledge_hit() { - let xml_str = get_attempt_priviledge_xml(); - let event: event::Evtx = quick_xml::de::from_str(&xml_str).unwrap(); - - let mut sec = security::Security::new(); - let msg = sec.attempt_priviledge( - &event.system.event_id.to_string(), - &event.parse_event_data(), - ); - - assert_ne!(Option::None, msg); - let v = msg.unwrap(); - let mut ite = v.iter(); - assert_eq!( - &"Possible Hidden Service Attempt".to_string(), - ite.next().unwrap_or(&"".to_string()) - ); - assert_eq!(&"User requested to modify the Dynamic Access Control (DAC) permissions of a sevice, possibly to hide it from view".to_string(), ite.next().unwrap_or(&"".to_string())); - assert_eq!( - &"User: Sec504".to_string(), - ite.next().unwrap_or(&"".to_string()) - ); - assert_eq!( - &"Target service: nginx".to_string(), - ite.next().unwrap_or(&"".to_string()) - ); - assert_eq!( - &"WRITE_DAC".to_string(), - ite.next().unwrap_or(&"".to_string()) - ); - assert_eq!(Option::None, ite.next()); - } - - // accessmaskが異なるので、Hitしないテスト - #[test] - fn test_attempt_priviledge_noteq_accessmask() { - let xml_str = get_attempt_priviledge_xml(); - let event: event::Evtx = quick_xml::de::from_str(&xml_str.replace( - r"%%1539", - r"%%1538", - )) - .unwrap(); - - let mut sec = security::Security::new(); - let msg = sec.attempt_priviledge( - &event.system.event_id.to_string(), - &event.parse_event_data(), - ); - - assert_eq!(Option::None, msg); - } - - // Serviceが違うのでHitしないテスト - #[test] - fn test_attempt_priviledge_noteq_service() { - let xml_str = get_attempt_priviledge_xml(); - let event: event::Evtx = quick_xml::de::from_str(&xml_str.replace( - r"C:\Windows\System32\services.exe", - r"C:\Windows\System32\lsass.exe", - )) - .unwrap(); - - let mut sec = security::Security::new(); - let msg = sec.attempt_priviledge( - &event.system.event_id.to_string(), - &event.parse_event_data(), - ); - - assert_eq!(Option::None, msg); - } - - // EventIDが違うのでHitしないテスト - #[test] - fn test_attempt_priviledge_noteq_eventid() { - let xml_str = get_attempt_priviledge_xml(); - let event: event::Evtx = quick_xml::de::from_str( - &xml_str.replace(r"4674", r"4675"), - ) - .unwrap(); - - let mut sec = security::Security::new(); - let msg = sec.attempt_priviledge( - &event.system.event_id.to_string(), - &event.parse_event_data(), - ); - - assert_eq!(Option::None, msg); - } - - fn get_attempt_priviledge_xml() -> String { - return r#" - - - - 4674 - 0 - 0 - 13056 - 0 - 0x8020000000000000 - - 39406 - - - Security - Sec504Student - - - - S-1-5-21-2977773840-2930198165-1551093962-1000 - Sec504 - SEC504STUDENT - 0x99e3d - SC Manager - SERVICE OBJECT - nginx - 0xffff820cb1d95928 - %%1539 - - SeSecurityPrivilege - 0x21c - C:\Windows\System32\services.exe - - "#.to_string(); - } - - #[test] - fn test_pass_spray_hit() { - let mut sec = security::Security::new(); - // 6ユーザまでは表示されず、7ユーザー以上で表示されるようになる。 - sec.max_passspray_login = 6; - sec.max_passspray_uniquser = 6; - - test_pass_spray_hit_1cycle(&mut sec, "4648".to_string(), true); - // counterがreset確認のため、2回実行 - test_pass_spray_hit_1cycle(&mut sec, "4648".to_string(), true); - } - - // eventid異なるので、Hitしないはず - #[test] - fn test_pass_spray_noteq_eventid() { - let mut sec = security::Security::new(); - // 6ユーザまでは表示されず、7ユーザー以上で表示されるようになる。 - sec.max_passspray_login = 6; - sec.max_passspray_uniquser = 6; - - test_pass_spray_hit_1cycle(&mut sec, "4649".to_string(), false); - // counterがreset確認のため、2回実行 - test_pass_spray_hit_1cycle(&mut sec, "4649".to_string(), false); - } - - fn test_pass_spray_hit_1cycle(sec: &mut security::Security, event_id: String, is_eq: bool) { - [1,2,3,4,5,6,7].iter().for_each(|i| { - let rep_str = format!(r#"smisenar{}"#,i); - let event_id_tag = format!("{}", event_id); - let xml_str = get_passs_pray_hit().replace(r#"smisenar"#, &rep_str).replace(r"4648", &event_id_tag); - let event: event::Evtx = quick_xml::de::from_str(&xml_str).unwrap(); - [1,2,3,4,5,6,7].iter().for_each(|k|{ - let ret = sec.pass_spray(&event.system.event_id.to_string(), &event.parse_event_data()); - if i == &7 && k == &7 && is_eq { - let v = ret.unwrap(); - let mut ret_ite = v.iter(); - assert_eq!(&"Distributed Account Explicit Credential Use (Password Spray Attack)".to_string(),ret_ite.next().unwrap()); - assert_eq!(&"The use of multiple user account access attempts with explicit credentials is ".to_string(),ret_ite.next().unwrap()); - assert_eq!(&"an indicator of a password spray attack".to_string(),ret_ite.next().unwrap()); - assert_eq!("Target Usernames: smisenar1 smisenar2 smisenar3 smisenar4 smisenar5 smisenar6 smisenar7",ret_ite.next().unwrap()); - assert_eq!(&"Accessing Username: jwrig".to_string(),ret_ite.next().unwrap()); - assert_eq!(&"Accessing Host Name: DESKTOP-JR78RLP".to_string(),ret_ite.next().unwrap()); - assert_eq!(Option::None,ret_ite.next()); - } else { - assert_eq!(Option::None,ret); - } - }); - }); - } - - fn get_passs_pray_hit() -> String { - return r#" - - - - 4648 - 0 - 0 - 12544 - 0 - 0x8020000000000000 - - 43097 - - - Security - DESKTOP-JR78RLP - - - - S-1-5-21-979008924-657238111-836329461-1002 - jwrig - DESKTOP-JR78RLP - 0x3069d - {00000000-0000-0000-0000-000000000000} - smisenar - DOMAIN - {00000000-0000-0000-0000-000000000000} - DESKTOP-JR78RLP - DESKTOP-JR78RLP - 0x4 - - 172.16.144.128 - 445 - - "#.to_string(); - } - - // 普通にHitするテスト - #[test] - fn test_audit_log_cleared_hit() { - let xml_str = get_audit_log_cleared_xml(); - let event: event::Evtx = quick_xml::de::from_str(&xml_str).unwrap(); - - let mut sec = security::Security::new(); - let msg = sec.audit_log_cleared(&event.system.event_id.to_string(), &event.user_data); - - assert_ne!(Option::None, msg); - let v = msg.unwrap(); - let mut ite = v.iter(); - assert_eq!( - &"Audit Log Clear".to_string(), - ite.next().unwrap_or(&"".to_string()) - ); - assert_eq!( - &"The Audit log was cleared".to_string(), - ite.next().unwrap_or(&"".to_string()) - ); - assert_eq!( - &"Security ID: jwrig".to_string(), - ite.next().unwrap_or(&"".to_string()) - ); - assert_eq!(Option::None, ite.next()); - } - - // eventid違うのでHitしないはず - #[test] - fn test_audit_log_cleared_noteq_eventid() { - let xml_str = get_audit_log_cleared_xml() - .replace(r"1102", r"1103"); - let event: event::Evtx = quick_xml::de::from_str(&xml_str).unwrap(); - - let mut sec = security::Security::new(); - let msg = sec.audit_log_cleared(&event.system.event_id.to_string(), &event.user_data); - assert_eq!(Option::None, msg); - } - - fn get_audit_log_cleared_xml() -> String { - return r#" - - - - 1102 - 0 - 4 - 104 - 0 - 0x4020000000000000 - - 42803 - - - Security - DESKTOP-JR78RLP - - - - - S-1-5-21-979008924-657238111-836329461-1002 - jwrig - DESKTOP-JR78RLP - 0x30550 - - - "#.to_string(); - } -} diff --git a/src/detections/sysmon.rs b/src/detections/sysmon.rs deleted file mode 100644 index 37f01874..00000000 --- a/src/detections/sysmon.rs +++ /dev/null @@ -1,59 +0,0 @@ -use crate::detections::utils::check_command; -use crate::models::event; -use std::collections::HashMap; - -pub struct Sysmon { - checkunsigned: u16, -} - -impl Sysmon { - pub fn new() -> Sysmon { - Sysmon { checkunsigned: 0 } - } - - pub fn detection( - &mut self, - event_id: String, - _system: &event::System, - event_data: HashMap, - ) { - self.check_command_lines(&event_id, &event_data); - self.check_for_unsigned_files(&event_id, &event_data); - } - - fn check_command_lines(&mut self, event_id: &String, event_data: &HashMap) { - if event_id != "1" { - return; - } - - if let Some(_command_line) = event_data.get("CommandLine") { - let default = "".to_string(); - let _creater = event_data.get("ParentImage").unwrap_or(&default); - - check_command(1, _command_line, 1000, 0, "", _creater); - } - } - - fn check_for_unsigned_files( - &mut self, - event_id: &String, - event_data: &HashMap, - ) { - if event_id != "7" { - return; - } - - if self.checkunsigned == 1 { - let default = "".to_string(); - let _signed = event_data.get("Signed").unwrap_or(&default); - if _signed == "false" { - let _image = event_data.get("Image").unwrap_or(&default); - let _command_line = event_data.get("ImageLoaded").unwrap_or(&default); - - println!("Message : Unsigned Image (DLL)"); - println!("Result : Loaded by: {}", _image); - println!("Command : {}", _command_line); - } - }; - } -} diff --git a/src/detections/system.rs b/src/detections/system.rs deleted file mode 100644 index 14619d60..00000000 --- a/src/detections/system.rs +++ /dev/null @@ -1,110 +0,0 @@ -use crate::detections::utils; -use crate::models::event; -use std::collections::HashMap; - -pub struct System {} - -impl System { - pub fn new() -> System { - System {} - } - - pub fn detection( - &mut self, - event_id: String, - system: &event::System, - event_data: HashMap, - ) { - self.system_log_clear(&event_id); - self.windows_event_log(&event_id, &event_data); - self.new_service_created(&event_id, &event_data); - self.interactive_service_warning(&event_id, &event_data); - self.suspicious_service_name(&event_id, &event_data); - } - - fn new_service_created(&mut self, event_id: &String, event_data: &HashMap) { - if event_id != "7045" { - return; - } - - let default = String::from(""); - let servicename = &event_data.get("ServiceName").unwrap_or(&default); - let commandline = &event_data.get("ImagePath").unwrap_or(&default); - let text = utils::check_regex(&servicename, 1); - if !text.is_empty() { - println!("Message : New Service Created"); - println!("Command : {}", commandline); - println!("Results : Service name: {}", servicename); - println!("Results : {}", text); - } - if !commandline.is_empty() { - utils::check_command(7045, &commandline, 1000, 0, &servicename, &""); - } - } - - fn interactive_service_warning( - &mut self, - event_id: &String, - event_data: &HashMap, - ) { - if event_id != "7030" { - return; - } - - let default = String::from(""); - let servicename = &event_data.get("param1").unwrap_or(&default); - println!("Message : Interactive service warning"); - println!("Results : Service name: {}", servicename); - println!("Results : Malware (and some third party software) trigger this warning"); - println!("{}", utils::check_regex(&servicename, 1)); - } - - fn suspicious_service_name(&mut self, event_id: &String, event_data: &HashMap) { - if event_id != "7036" { - return; - } - - let default = String::from(""); - let servicename = &event_data.get("param1").unwrap_or(&default); - let text = utils::check_regex(&servicename, 1); - if !text.is_empty() { - println!("Message : Suspicious Service Name"); - println!("Results : Service name: {}", servicename); - println!("Results : {}", text); - } - } - - fn system_log_clear(&mut self, event_id: &String) { - if event_id != "104" { - return; - } - - println!("Message : System Log Clear"); - println!("Results : The System log was cleared."); - } - - fn windows_event_log(&mut self, event_id: &String, event_data: &HashMap) { - if event_id != "7040" { - return; - } - - if let Some(_param1) = event_data.get("param1") { - if _param1 == "Windows Event Log" { - println!("Service name : {}", _param1); - if let Some(_param2) = event_data.get("param2") { - if _param2 == "disabled" { - println!("Message : Event Log Service Stopped"); - println!( - "Results : Selective event log manipulation may follow this event." - ); - } else if _param2 == "auto start" { - println!("Message : Event Log Service Started"); - println!( - "Results : Selective event log manipulation may precede this event." - ); - } - } - } - } - } -} diff --git a/src/detections/utils.rs b/src/detections/utils.rs index 2e7026e0..704716a6 100644 --- a/src/detections/utils.rs +++ b/src/detections/utils.rs @@ -3,133 +3,22 @@ extern crate csv; extern crate regex; use crate::detections::configs; -use flate2::read::GzDecoder; + use regex::Regex; +use serde_json::Value; +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, -) { - let mut text = "".to_string(); - let mut base64 = "".to_string(); - - let empty = "".to_string(); - for line in configs::singleton().whitelist { - let r_str = line.get(0).unwrap_or(&empty); - if r_str.is_empty() { - continue; - } - - let r = Regex::new(r_str); - if r.is_ok() && r.unwrap().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 let Ok(decoded) = base64::decode(&base64) { - if !base64.is_empty() { - if Regex::new(r"Compression.GzipStream.*Decompress") - .unwrap() - .is_match(commandline) - { - 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 { - 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() { - println!("EventID : {}", event_id); - 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); - } -} - -fn check_obfu(string: &str) -> std::string::String { - let mut obfutext = "".to_string(); - let lowercasestring = string.to_lowercase(); - let length = lowercasestring.len() as f64; - let mut minpercent = 0.65; - let maxbinary = 0.50; - - let mut re = Regex::new(r"[a-z0-9/¥;:|.]").unwrap(); - let noalphastring = re.replace_all(&lowercasestring, ""); - - re = Regex::new(r"[01]").unwrap(); - let nobinarystring = re.replace_all(&lowercasestring, ""); - - if length > 0.0 { - let mut percent = (length - noalphastring.len() as f64) / length; - if ((length / 100.0) as f64) < minpercent { - minpercent = length / 100.0; - } - - if percent < minpercent { - obfutext.push_str("Possible command obfuscation: only "); - let percent = (percent * 100.0) as usize; - obfutext.push_str(&percent.to_string()); - obfutext.push_str("% alphanumeric and common symbols\n"); - } - - percent = ((nobinarystring.len().wrapping_sub(length as usize) as f64) / length) / length; - let binarypercent = 1.0 - percent; - if binarypercent > maxbinary { - obfutext.push_str("Possible command obfuscation: "); - let binarypercent = (binarypercent * 100.0) as usize; - obfutext.push_str(&binarypercent.to_string()); - obfutext.push_str("% zeroes and ones (possible numeric or binary encoding)\n"); - } - } - return obfutext; -} - -pub fn check_regex(string: &str, r#type: usize) -> std::string::String { +pub fn check_regex( + string: &str, + r#type: usize, + regex_list: &Vec>, +) -> std::string::String { let empty = "".to_string(); let mut regextext = "".to_string(); - for line in configs::singleton().regex { + for line in regex_list { let type_str = line.get(0).unwrap_or(&empty); if type_str != &r#type.to_string() { continue; @@ -157,22 +46,67 @@ pub fn check_regex(string: &str, r#type: usize) -> std::string::String { 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"); - } +pub fn check_whitelist(target: &str, whitelist: &Vec>) -> bool { + let empty = "".to_string(); + for line in whitelist { + let r_str = line.get(0).unwrap_or(&empty); + if r_str.is_empty() { + continue; + } + + let r = Regex::new(r_str); + if r.is_ok() && r.unwrap().is_match(target) { + return true; } } - return creatortext; + + return false; +} + +pub fn read_csv(filename: &str) -> Result>, String> { + let mut f = File::open(filename).expect("file not found!!!"); + let mut contents: String = String::new(); + let mut ret = vec![]; + let read_res = f.read_to_string(&mut contents); + if f.read_to_string(&mut contents).is_err() { + return Result::Err(read_res.unwrap_err().to_string()); + } + + let mut rdr = csv::Reader::from_reader(contents.as_bytes()); + rdr.records().for_each(|r| { + if r.is_err() { + return; + } + + let line = r.unwrap(); + let mut v = vec![]; + line.iter().for_each(|s| v.push(s.to_string())); + ret.push(v); + }); + + return Result::Ok(ret); +} + +pub fn get_event_value<'a>(key: &String, event_value: &'a Value) -> Option<&'a Value> { + if key.len() == 0 { + return Option::None; + } + + let alias_config = configs::singleton().event_key_alias_config; + let event_key = match alias_config.get_event_key(key.to_string()) { + Some(alias_event_key) => alias_event_key, + None => key, + }; + + let mut ret: &Value = event_value; + for key in event_key.split(".") { + if ret.is_object() == false { + return Option::None; + } + ret = &ret[key]; + } + + return Option::Some(ret); } #[cfg(test)] @@ -180,36 +114,21 @@ mod tests { use crate::detections::utils; #[test] fn test_check_regex() { - let regextext = utils::check_regex("\\cvtres.exe", 0); + let regexes = utils::read_csv("regexes.txt").unwrap(); + let regextext = utils::check_regex("\\cvtres.exe", 0, ®exes); assert!(regextext == "Resource File To COFF Object Conversion Utility cvtres.exe\n"); + + let regextext = utils::check_regex("\\hogehoge.exe", 0, ®exes); + assert!(regextext == ""); } #[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"); - } + fn test_check_whitelist() { + let commandline = "\"C:\\Program Files\\Google\\Update\\GoogleUpdate.exe\""; + let whitelist = utils::read_csv("whitelist.txt").unwrap(); + assert!(true == utils::check_whitelist(commandline, &whitelist)); - #[test] - fn test_check_obfu() { - let obfutext = utils::check_obfu("string"); - assert!(obfutext == "Possible command obfuscation: 100% zeroes and ones (possible numeric or binary encoding)\n"); - } - - #[test] - fn test_check_command() { - utils::check_command(1, "dir", 100, 100, "dir", "dir"); - - //test return with whitelist. - utils::check_command( - 1, - "\"C:\\Program Files\\Google\\Update\\GoogleUpdate.exe\"", - 100, - 100, - "dir", - "dir", - ); + let commandline = "\"C:\\Program Files\\Google\\Update\\GoogleUpdate2.exe\""; + assert!(false == utils::check_whitelist(commandline, &whitelist)); } } diff --git a/src/lib.rs b/src/lib.rs index 2f0bb054..06371c5a 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,5 +1,4 @@ pub mod afterfact; pub mod detections; -pub mod models; pub mod omikuji; pub mod yaml; diff --git a/src/main.rs b/src/main.rs index ddfc4c4e..0a66e2e7 100644 --- a/src/main.rs +++ b/src/main.rs @@ -11,12 +11,7 @@ use yamato_event_analyzer::detections::detection; use yamato_event_analyzer::omikuji::Omikuji; fn main() -> Result<(), DeError> { - let filepath: String = configs::singleton() - .args - .value_of("filepath") - .unwrap_or("") - .to_string(); - if filepath != "" { + if let Some(filepath) = configs::singleton().args.value_of("filepath") { parse_file(&filepath); } diff --git a/src/models/event.rs b/src/models/event.rs deleted file mode 100644 index aaea9312..00000000 --- a/src/models/event.rs +++ /dev/null @@ -1,133 +0,0 @@ -extern crate serde; -use serde::Deserialize; -use std::collections::HashMap; - -#[derive(Debug, Deserialize, PartialEq)] -pub struct Data { - #[serde(rename = "Name")] - pub name: Option, - #[serde(rename = "$value")] - pub text: Option, -} - -#[derive(Debug, Deserialize, PartialEq)] -pub struct TimeCreated { - #[serde(rename = "SystemTime")] - pub system_time: String, -} - -#[derive(Debug, Deserialize, PartialEq)] -struct Execution { - #[serde(rename = "ProcessID")] - process_id: i32, - #[serde(rename = "ThreadID")] - thread_id: i32, -} - -#[derive(Debug, Deserialize, PartialEq)] -pub struct Provider { - #[serde(rename = "Name")] - pub name: Option, - #[serde(rename = "Guid")] - guid: Option, -} - -#[derive(Debug, Deserialize, PartialEq)] -pub struct System { - #[serde(rename = "Provider")] - pub provider: Provider, - #[serde(rename = "EventID")] - pub event_id: String, - #[serde(rename = "Version")] - version: Option, - #[serde(rename = "Level")] - level: String, - #[serde(rename = "Task")] - task: String, - #[serde(rename = "Opcode")] - opcode: Option, - #[serde(rename = "Keywords")] - keywords: String, - #[serde(rename = "TimeCreated")] - pub time_created: TimeCreated, - #[serde(rename = "EventRecordID")] - pub event_record_id: String, - #[serde(rename = "Correlation")] - correlation: Option, - #[serde(rename = "Execution")] - execution: Option, - #[serde(rename = "Channel")] - pub channel: String, // Security, System, Application ...etc - #[serde(rename = "Computer")] - computer: String, - #[serde(rename = "Security")] - security: String, - #[serde(rename = "Message")] - pub message: Option, -} - -#[derive(Debug, Deserialize, PartialEq)] -pub struct EventData { - #[serde(rename = "Data")] - pub data: Option>, -} - -#[derive(Debug, Deserialize, PartialEq)] -pub struct UserData { - #[serde(rename = "LogFileCleared")] - pub log_file_cleared: Option, -} - -#[derive(Debug, Deserialize, PartialEq)] -pub struct LogFileCleared { - #[serde(rename = "SubjectUserSid")] - pub subject_user_sid: Option, - #[serde(rename = "SubjectUserName")] - pub subject_user_name: Option, - #[serde(rename = "SubjectDomainName")] - pub subject_domain_name: Option, - #[serde(rename = "SubjectLogonId")] - pub subject_logon_id: Option, -} - -#[derive(Debug, Deserialize, PartialEq)] -pub struct Evtx { - #[serde(rename = "System")] - pub system: System, - #[serde(rename = "EventData")] - pub event_data: Option, - #[serde(rename = "UserData")] - pub user_data: Option, -} - -impl Evtx { - // - // 文字列データを取得する - // - fn get_string(v: &Data) -> String { - let mut ret = "".to_string(); - if let Some(text) = &v.text { - ret = text.to_string(); - } - return ret; - } - - // - // EventDataをHashMapとして取得する - // - pub fn parse_event_data(&self) -> HashMap { - let mut values = HashMap::new(); - - if let Some(event_data) = &self.event_data { - if let Some(data) = &event_data.data { - for v in data.iter() { - if let Some(name) = &v.name { - values.insert(name.to_string(), Evtx::get_string(v)); - } - } - } - } - - values - } -} diff --git a/src/models/mod.rs b/src/models/mod.rs deleted file mode 100644 index 53f11265..00000000 --- a/src/models/mod.rs +++ /dev/null @@ -1 +0,0 @@ -pub mod event; diff --git a/src/yaml.rs b/src/yaml.rs index 8636eb9a..781a5100 100644 --- a/src/yaml.rs +++ b/src/yaml.rs @@ -8,12 +8,12 @@ use std::path::{Path, PathBuf}; use yaml_rust::YamlLoader; pub struct ParseYaml { - pub rules: Vec, + pub files: Vec, } impl ParseYaml { pub fn new() -> ParseYaml { - ParseYaml { rules: Vec::new() } + ParseYaml { files: Vec::new() } } pub fn read_file(&self, path: PathBuf) -> Result { @@ -39,7 +39,7 @@ impl ParseYaml { let docs = YamlLoader::load_from_str(&s).unwrap(); for i in docs { if i["enabled"].as_bool().unwrap() { - &self.rules.push(i); + &self.files.push(i); } } } @@ -64,7 +64,7 @@ mod tests { fn test_read_yaml() { let mut yaml = yaml::ParseYaml::new(); &yaml.read_dir("test_files/rules/yaml/".to_string()); - for rule in yaml.rules { + for rule in yaml.files { if rule["title"].as_str().unwrap() == "Sysmon Check command lines" { assert_eq!( "*",