diff --git a/src/detections/detection.rs b/src/detections/detection.rs index 7d61a1d4..90d9cc25 100644 --- a/src/detections/detection.rs +++ b/src/detections/detection.rs @@ -5,6 +5,7 @@ use crate::detections::application; 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 evtx::EvtxParser; @@ -30,6 +31,7 @@ impl Detection { let mut security = security::Security::new(); let mut system = system::System::new(); let mut application = application::Application::new(); + let mut sysmon = sysmon::Sysmon::new(); let mut powershell = powershell::PowerShell::new(); let mut f = File::open("whitelist.txt").expect("file not found"); @@ -49,13 +51,15 @@ impl Detection { &common.detection(&event.system, &event_data); //&common.detection(&event.system, &event_data); if channel == "Security" { - &security.detection(event_id, &event.system, event_data); + &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, &mut rdr); + } else if channel == "Microsoft-Windows-Sysmon/Operational" { + &sysmon.detection(event_id, &event.system, event_data); } else { //&other.detection(); } diff --git a/src/detections/mod.rs b/src/detections/mod.rs index af7ef760..d2012ac3 100644 --- a/src/detections/mod.rs +++ b/src/detections/mod.rs @@ -3,5 +3,6 @@ mod common; pub mod detection; mod powershell; mod security; +mod sysmon; mod system; mod utils; diff --git a/src/detections/security.rs b/src/detections/security.rs index 16426937..97a30ce5 100644 --- a/src/detections/security.rs +++ b/src/detections/security.rs @@ -3,45 +3,133 @@ 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) { - if self.total_admin_logons > 0 { - println!("total_admin_logons:{}", self.total_admin_logons); - println!("admin_logons:{:?}", self.admin_logons); - println!("multiple_admin_logons:{:?}", self.multiple_admin_logons); + 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, + _system: &event::System, + user_data: &Option, event_data: HashMap, ) { - if event_id == "4672" { - &self.se_debug_privilege(event_data); + 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; } + // TODO Check-Commnad + return; } // // Special privileges assigned to new logon (possible admin access) // - fn se_debug_privilege(&mut self, event_data: HashMap) { + 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が有効であれば、標準出力して知らせる @@ -86,4 +174,1100 @@ impl Security { } } } + + // 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 new file mode 100644 index 00000000..165e39db --- /dev/null +++ b/src/detections/sysmon.rs @@ -0,0 +1,82 @@ +use crate::models::event; +use std::collections::HashMap; + +pub struct Sysmon { + checkunsigned: u64, +} + +impl Sysmon { + pub fn new() -> Sysmon { + Sysmon { + //checkunsigned: 0, // DeepBlueでは0固定 + checkunsigned: 1, // 開発用に1 + } + } + + pub fn detection( + &mut self, + event_id: String, + system: &event::System, + event_data: HashMap, + ) { + if event_id == "1" { + &self.check_command_lines(event_data); + } else if event_id == "7" { + &self.check_for_unsigned_files(event_data); + } + } + + fn check_command_lines(&mut self, event_data: HashMap) { + // Check command lines + if let Some(_command_line) = event_data.get("CommandLine") { + if let Some(_date) = event_data.get("UtcTime") { + println!("Date : {} (UTC)", _date); + } + println!("Log : Sysmon"); + //if let Some(_creater) = event_data.get("ParentImage") { + // println!("_creater : {}", _image); + //} + self.check_command("1".to_string(), _command_line.to_string()); + println!(""); + } + } + + fn check_for_unsigned_files(&mut self, event_data: HashMap) { + // Check for unsigned EXEs/DLLs: + // This can be very chatty, so it's disabled. + // Set $checkunsigned to 1 (global variable section) to enable: + if self.checkunsigned == 1 { + if let Some(_signed) = event_data.get("Signed") { + if _signed == "false" { + if let Some(_date) = event_data.get("UtcTime") { + println!("Date : {} (UTC)", _date); + } + println!("Log : Sysmon"); + println!("EventID : 7"); + println!("Message : Unsigned Image (DLL)"); + if let Some(_image) = event_data.get("Image") { + println!("Result : Loaded by: {}", _image); + } + if let Some(_command_line) = event_data.get("ImageLoaded") { + println!("Command : {}", _command_line); + } + println!(""); + } + } + } + } + + fn check_command(&mut self, _event_id: String, _command_line: String) { + let _result = "(TBD)"; + let _decoded = "(TBD)"; + + // TBD + + // Write-Output $obj + println!("EventID : {}", _event_id); + println!("Message : Suspicious Command Line"); + println!("Result : {}", _result); + println!("Command : {}", _command_line); + println!("Decoded : {}", _decoded); + } +} diff --git a/src/models/event.rs b/src/models/event.rs index 113157b6..aaea9312 100644 --- a/src/models/event.rs +++ b/src/models/event.rs @@ -72,12 +72,32 @@ pub struct EventData { 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 {