From 1abdbafb5a32c2e7d565a314277465a53cdc9a7e Mon Sep 17 00:00:00 2001 From: ichiichi11 Date: Sat, 21 Nov 2020 15:04:28 +0900 Subject: [PATCH] under constructing --- .gitignore | 3 +- Cargo.lock | 1 + Cargo.toml | 1 + config/eventkey_alias.txt | 12 ++ rules/deep_blue_cli/powershell/4103.yml | 20 ++ rules/deep_blue_cli/powershell/4104.yml | 19 ++ .../powershell/powershell4103.toml | 4 - .../powershell/powershell4104.toml | 4 - src/detections/configs.rs | 42 +++- src/detections/detection.rs | 112 +++++----- src/detections/mod.rs | 1 + src/detections/rule.rs | 204 ++++++++++++++++++ src/yaml.rs | 8 +- 13 files changed, 360 insertions(+), 71 deletions(-) create mode 100644 config/eventkey_alias.txt create mode 100644 rules/deep_blue_cli/powershell/4103.yml create mode 100644 rules/deep_blue_cli/powershell/4104.yml delete mode 100644 rules/deep_blue_cli/powershell/powershell4103.toml delete mode 100644 rules/deep_blue_cli/powershell/powershell4104.toml create mode 100644 src/detections/rule.rs diff --git a/.gitignore b/.gitignore index 5bfa00b5..5ad768f8 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ /target /samples -*.test \ No newline at end of file +*.test +/.vscode/ \ 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..dda9ee24 --- /dev/null +++ b/config/eventkey_alias.txt @@ -0,0 +1,12 @@ +alias,event_key +EventID,Event.System.EventId +Channel,Event.System.Channel +CommandLine,Event.EventData.CommandLine +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 \ No newline at end of file diff --git a/rules/deep_blue_cli/powershell/4103.yml b/rules/deep_blue_cli/powershell/4103.yml new file mode 100644 index 00000000..709e360a --- /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: + EventLog: 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..0b34b70f --- /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: + EventLog: PowerShell + EventID: 4104 + Path: '' + ScriptBlockText: '.' + 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/src/detections/configs.rs b/src/detections/configs.rs index 763f50f1..a3d97eb1 100644 --- a/src/detections/configs.rs +++ b/src/detections/configs.rs @@ -2,12 +2,12 @@ use clap::{App, AppSettings, Arg, ArgMatches}; use std::fs::File; use std::io::prelude::*; use std::sync::Once; +use std::collections::HashMap; #[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 +17,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,6 +55,41 @@ fn build_app() -> clap::App<'static, 'static> { .arg(Arg::from_usage("--credits 'Zachary Mathis, Akira Nishikawa'")) } +#[derive(Clone)] +pub struct EventKeyAliasConfig { + key_to_eventkey: HashMap, +} + +impl EventKeyAliasConfig { + pub fn new() -> EventKeyAliasConfig { + return EventKeyAliasConfig{ key_to_eventkey: HashMap::new() }; + } + + pub fn get_event_key(&self, alias: String ) -> Option<&String> { + return self.key_to_eventkey.get(&alias); + } +} + +fn load_eventkey_alias() -> EventKeyAliasConfig { + let config = EventKeyAliasConfig::new(); + + read_csv("config/eventkey_alias.txt").into_iter().for_each( | line| { + if line.len() != 2 { + return; + } + + let alias = line[0]; + let event_key = line[1]; + if alias.len() == 0 || event_key.len() == 0 { + return; + } + + config.key_to_eventkey.insert(alias, event_key); + }); + + return config; +} + fn read_csv(filename: &str) -> Vec> { let mut f = File::open(filename).expect("file not found!!!"); let mut contents: String = String::new(); diff --git a/src/detections/detection.rs b/src/detections/detection.rs index 0c3ce7cc..45d7612a 100644 --- a/src/detections/detection.rs +++ b/src/detections/detection.rs @@ -1,74 +1,78 @@ extern crate csv; -extern crate quick_xml; +extern crate chrono; -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::rule; +use crate::detections::rule::RuleNode; +use crate::detections::print::{Message}; +use crate::yaml::ParseYaml; + +use chrono::{TimeZone, Utc}; use evtx::EvtxParser; -use quick_xml::de::DeError; -use std::collections::BTreeMap; +use serde_json::{Error, Value}; + +const DIRPATH_RULES: &str = "rules"; #[derive(Debug)] pub struct Detection { - timeline_list: BTreeMap, + } impl Detection { pub fn new() -> Detection { Detection { - timeline_list: BTreeMap::new(), } } - 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) { + // from .etvx to json + 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), - } + + //// refer 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(); + + event_records.iter().for_each(|event_rec| { + println!("{}", event_rec["Event"]); + }); + + // 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 rules: Vec = rulefile_loader + .files + .into_iter() + .map(|rule_file| rule::parse_rule(rule_file)) + .collect(); - return Ok(()); + // selection rule files and collect log + let mut message = Message::new(); + rules.iter().for_each(|rule| { + &event_records + .iter() + .filter(|event_record| rule.detection.select(event_record)) + .for_each(|event_record| message.insert(Utc.ymd(1996, 2, 27).and_hms(1, 5, 1), event_record.to_string())); + }); + + // output message + message.debug(); } } diff --git a/src/detections/mod.rs b/src/detections/mod.rs index 4aaee9ff..c11cdb13 100644 --- a/src/detections/mod.rs +++ b/src/detections/mod.rs @@ -5,6 +5,7 @@ pub mod configs; pub mod detection; mod powershell; pub mod print; +mod rule; mod security; mod sysmon; mod system; diff --git a/src/detections/rule.rs b/src/detections/rule.rs new file mode 100644 index 00000000..8d263ff3 --- /dev/null +++ b/src/detections/rule.rs @@ -0,0 +1,204 @@ +use serde_json::Value; +use yaml_rust::Yaml; +use crate::detections::configs; + +pub fn parse_rule(yaml: Yaml) -> RuleNode { + let selection = parse_selection(&yaml); + return RuleNode { + yaml: yaml, + detection: DetectionNode { + selection: selection, + }, + }; +} + +fn parse_selection(yaml: &Yaml) -> Option> { + let selection_yaml = &yaml["detection"]["selection"]; + return Option::Some(parse_selection_recursively(vec![], &selection_yaml)); +} + +fn parse_selection_recursively(mut key_list: Vec, yaml: &Yaml) -> Box { + if yaml.as_hash().is_some() { + 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() { + 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(FieldSelectionNode::new(key_list, yaml.clone())); + } +} + +/////////////// RuleNode +pub struct RuleNode { + pub yaml: Yaml, + pub detection: DetectionNode, +} + +//////////////// Detection Node +pub struct DetectionNode { + selection: Option>, +} + +impl DetectionNode { + pub fn select(&self, event_record: &Value) -> bool { + if self.selection.is_none() { + return false; + } + + return self.selection.as_ref().unwrap().select(event_record); + } +} + +//////////// Selection Node +trait SelectionNode { + fn select(&self, event_record: &Value) -> bool; +} + +///////////////// AndSelectionNode +struct AndSelectionNode { + pub child_nodes: Vec>, +} + +impl AndSelectionNode { + pub fn new() -> AndSelectionNode { + return AndSelectionNode { + child_nodes: vec![], + }; + } +} + +impl SelectionNode for AndSelectionNode { + fn select(&self, event_record: &Value) -> bool { + return self.child_nodes.iter().all(|child_node| { + return child_node.as_ref().select(event_record); + }); + } +} + +////////// OrSelectionNode +struct OrSelectionNode { + pub child_nodes: Vec>, +} + +impl OrSelectionNode { + pub fn new() -> OrSelectionNode { + return OrSelectionNode { + child_nodes: vec![], + }; + } +} + +impl SelectionNode for OrSelectionNode { + fn select(&self, event_record: &Value) -> bool { + return self.child_nodes.iter().any(|child_node| { + return child_node.as_ref().select(event_record); + }); + } +} + +////////////// Field Selection Node +struct FieldSelectionNode { + key_list: Vec, + select_value: Yaml, +} + +impl FieldSelectionNode { + fn new(key_list: Vec, value_yaml: Yaml) -> FieldSelectionNode { + return FieldSelectionNode { + key_list: key_list, + select_value: value_yaml, + }; + } + + // JSON形式のEventJSONから値を取得する関数 aliasも考慮されている。 + // TODO Messageを出力する際も利用するので、共通して使えるようにrefactoringする。 + fn get_event_value<'a>(&self, event_value: &'a Value) -> Option<&'a Value> { + if self.key_list.is_empty() { + return Option::None; + } + + let key: &str = &self.key_list[0]; + if key.len() == 0 { + return Option::None; + } + + let event_key = match configs::singleton().event_key_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); + } + + // TODO Matcherのインスタンスが都度生成されないようにする。 + fn get_matchers(&self) -> Vec> { + return vec![Box::new(ValueMatcher {})]; + } +} + +impl SelectionNode for FieldSelectionNode { + fn select(&self, event_record: &Value) -> bool { + let matchers = self.get_matchers(); + let matcher = matchers + .into_iter() + .find(|matcher| matcher.is_target_key(&self.key_list)); + if matcher.is_none() { + return false; + } + + let event_value = self.get_event_value(event_record); + return matcher + .unwrap() + .is_match(&self.key_list, &self.select_value, event_value); + } +} + +trait FieldSelectionMatcher { + fn is_target_key(&self, key_list: &Vec) -> bool; + fn is_match( + &self, + key_list: &Vec, + select_value: &Yaml, + event_value: Option<&Value>, + ) -> bool; +} + +struct ValueMatcher {} + +impl FieldSelectionMatcher for ValueMatcher { + fn is_target_key(&self, key_list: &Vec) -> bool { + return key_list.is_empty(); + } + + fn is_match( + &self, + key_list: &Vec, + select_value: &Yaml, + event_value: Option<&Value>, + ) -> bool { + return true; + } +} \ No newline at end of file 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!( "*",