diff --git a/Cargo.lock b/Cargo.lock index a67cedfd..e04349ee 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -23,7 +23,7 @@ version = "0.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fcb51a0695d8f838b1ee009b3fbf66bda078cd64590202a864a8f3e8c4315c47" dependencies = [ - "getrandom", + "getrandom 0.2.6", "once_cell", "version_check", ] @@ -52,6 +52,18 @@ version = "1.0.56" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4361135be9122e0870de935d7c439aef945b9f9ddd4199a553b5270b49c82a27" +[[package]] +name = "arrayref" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4c527152e37cf757a3f78aae5a06fbeefdb07ccc535c980a3208ee3060dd544" + +[[package]] +name = "arrayvec" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23b62fc65de8e4e7f52534fb52b0f3ed04746ae267519eef2a83941e8085068b" + [[package]] name = "atty" version = "0.2.14" @@ -120,6 +132,17 @@ version = "1.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" +[[package]] +name = "blake2b_simd" +version = "0.5.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "afa748e348ad3be8263be728124b24a24f268266f6f5d58af9d75f6a40b5c587" +dependencies = [ + "arrayref", + "arrayvec", + "constant_time_eq", +] + [[package]] name = "bstr" version = "0.2.17" @@ -295,6 +318,12 @@ version = "0.4.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fbdcdcb6d86f71c5e97409ad45898af11cbc995b4ee8112d59095a28d376c935" +[[package]] +name = "constant_time_eq" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "245097e9a4535ee1e3e3931fcfcd55a796a44c643e8596ff6566d68f09b87bbc" + [[package]] name = "cookie" version = "0.12.0" @@ -484,6 +513,17 @@ dependencies = [ "zeroize", ] +[[package]] +name = "dirs" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3fd78930633bd1c6e35c4b42b1df7b0cbc6bc191146e512bb3bedf243fcc3901" +dependencies = [ + "libc", + "redox_users", + "winapi 0.3.9", +] + [[package]] name = "discard" version = "1.0.4" @@ -769,6 +809,17 @@ dependencies = [ "num_cpus", ] +[[package]] +name = "getrandom" +version = "0.1.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fc3cb4d91f53b50155bdcfd23f6a4c39ae1969c2ae85982b135750cccaf5fce" +dependencies = [ + "cfg-if 1.0.0", + "libc", + "wasi 0.9.0+wasi-snapshot-preview1", +] + [[package]] name = "getrandom" version = "0.2.6" @@ -863,6 +914,7 @@ dependencies = [ "num_cpus", "openssl", "pbr", + "prettytable-rs", "quick-xml", "regex", "serde", @@ -1528,6 +1580,20 @@ version = "0.3.25" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1df8c4ec4b0627e53bdf214615ad287367e482558cf84b109250b37464dc03ae" +[[package]] +name = "prettytable-rs" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fd04b170004fa2daccf418a7f8253aaf033c27760b5f225889024cf66d7ac2e" +dependencies = [ + "atty", + "csv", + "encode_unicode", + "lazy_static", + "term", + "unicode-width", +] + [[package]] name = "proc-macro-hack" version = "0.5.19" @@ -1743,6 +1809,17 @@ dependencies = [ "bitflags", ] +[[package]] +name = "redox_users" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "de0737333e7a9502c789a36d7c7fa6092a49895d4faa31ca5df163857ded2e9d" +dependencies = [ + "getrandom 0.1.16", + "redox_syscall 0.1.57", + "rust-argon2", +] + [[package]] name = "regex" version = "1.5.5" @@ -1829,6 +1906,18 @@ dependencies = [ "pkg-config", ] +[[package]] +name = "rust-argon2" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4b18820d944b33caa75a71378964ac46f58517c92b6ae5f762636247c09e78fb" +dependencies = [ + "base64 0.13.0", + "blake2b_simd", + "constant_time_eq", + "crossbeam-utils 0.8.8", +] + [[package]] name = "rustc-demangle" version = "0.1.21" @@ -2178,6 +2267,17 @@ dependencies = [ "winapi 0.3.9", ] +[[package]] +name = "term" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "edd106a334b7657c10b7c540a0106114feadeb4dc314513e97df481d5d966f42" +dependencies = [ + "byteorder", + "dirs", + "winapi 0.3.9", +] + [[package]] name = "termcolor" version = "1.1.3" @@ -2593,6 +2693,12 @@ dependencies = [ "try-lock", ] +[[package]] +name = "wasi" +version = "0.9.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cccddf32554fecc6acb585f82a32a72e28b48f8c4c1883ddfeeeaa96f7d8e519" + [[package]] name = "wasi" version = "0.10.0+wasi-snapshot-preview1" diff --git a/Cargo.toml b/Cargo.toml index e36b1f6e..1eff3321 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -32,6 +32,7 @@ hashbrown = "0.12.*" colored = "2.*" hex = "0.4.*" git2="0.13" +prettytable-rs = "0.8" [target.'cfg(windows)'.dependencies] is_elevated = "0.1.2" diff --git a/README-Japanese.md b/README-Japanese.md index 2818d91a..3293a281 100644 --- a/README-Japanese.md +++ b/README-Japanese.md @@ -56,6 +56,7 @@ Hayabusaは、日本の[Yamato Security](https://yamatosecurity.connpass.com/) - [コマンドラインオプション](#コマンドラインオプション) - [使用例](#使用例) - [ピボットキーワードの作成](#ピボットキーワードの作成) + - [ログオン情報の要約](#ログオン情報の要約) - [サンプルevtxファイルでHayabusaをテストする](#サンプルevtxファイルでhayabusaをテストする) - [Hayabusaの出力](#hayabusaの出力) - [MITRE ATT&CK戦術の省略](#mitre-attck戦術の省略) @@ -327,6 +328,7 @@ USAGE: -U --utc 'UTC形式で日付と時刻を出力する。(デフォルト: 現地時間)' -t --thread-number=[NUMBER] 'スレッド数。(デフォルト: パフォーマンスに最適な数値)' -s --statistics 'イベント ID の統計情報を表示する。' + -L --logon-summary 'ユーザのログオン情報の要約を出力' -q --quiet 'Quietモード。起動バナーを表示しない。' -Q --quiet-errors 'Quiet errorsモード。エラーログを保存しない。' --level-tuning 'ルールlevelのチューニング [default: ./rules/config/level_tuning.txt]' @@ -451,6 +453,10 @@ Processes.Image 形式は`KeywordName.FieldName`となっています。例えばデフォルトの設定では、`Users`というリストは検知したイベントから`SubjectUserName`、 `TargetUserName` 、 `User`のフィールドの値が一覧として出力されます。hayabusaのデフォルトでは検知したすべてのイベントから結果を出力するため、`--pivot-keyword-list`オプションを使うときには `-m` もしくは `--min-level` オプションを併せて使って検知するイベントのレベルを指定することをおすすめします。まず`-m critical`を指定して、最も高い`critical`レベルのアラートのみを対象として、レベルを必要に応じて下げていくとよいでしょう。結果に正常なイベントにもある共通のキーワードが入っている可能性が高いため、手動で結果を確認してから、不審なイベントにありそうなキーワードリストを1つのファイルに保存し、`grep -f keywords.txt timeline.csv`等のコマンドで不審なアクティビティに絞ったタイムラインを作成することができます。 +## ログオン情報の要約 + +`-L` または `--logon-summary` オプションを使うことでログオン情報の要約(ユーザ名、ログイン成功数、ログイン失敗数)の画面出力ができます。`-d` オプションを合わせて使うことでevtxファイルごとのログイン情報の要約を出力できます。 + # サンプルevtxファイルでHayabusaをテストする Hayabusaをテストしたり、新しいルールを作成したりするためのサンプルevtxファイルをいくつか提供しています: [https://github.com/Yamato-Security/Hayabusa-sample-evtx](https://github.com/Yamato-Security/Hayabusa-sample-evtx) diff --git a/README.md b/README.md index 47c8b374..088259d5 100644 --- a/README.md +++ b/README.md @@ -55,6 +55,7 @@ Hayabusa is a **Windows event log fast forensics timeline generator** and **thre - [Command Line Options](#command-line-options) - [Usage Examples](#usage-examples) - [Pivot Keyword Generator](#pivot-keyword-generator) + - [Logon Summary Generator](#logon-summary-generator) - [Testing Hayabusa on Sample Evtx Files](#testing-hayabusa-on-sample-evtx-files) - [Hayabusa Output](#hayabusa-output) - [MITRE ATT&CK Tactics Abbreviations](#mitre-attck-tactics-abbreviations) @@ -319,6 +320,7 @@ USAGE: -U --utc 'Output time in UTC format. (Default: local time)' -t --thread-number=[NUMBER] 'Thread number. (Default: Optimal number for performance.)' -s --statistics 'Prints statistics of event IDs.' + -L --logon-summary 'User logon and failed logon summary' -q --quiet 'Quiet mode. Do not display the launch banner.' -Q --quiet-errors 'Quiet errors mode. Do not save error logs.' --level-tuning 'Tune the rule level [default: ./rules/config/level_tuning.txt]' @@ -443,6 +445,11 @@ Processes.Image The format is `KeywordName.FieldName`. For example, when creating the list of `Users`, hayabusa will list up all the values in the `SubjectUserName`, `TargetUserName` and `User` fields. By default, hayabusa will return results from all events (informational and higher) so we highly recommend combining the `--pivot-keyword-list` option with the `-m` or `--min-level` option. For example, start off with only creating keywords from `critical` alerts with `-m critical` and then continue with `-m high`, `-m medium`, etc... There will most likely be common keywords in your results that will match on many normal events, so after manually checking the results and creating a list of unique keywords in a single file, you can then create a narrowed down timeline of suspicious activity with a command like `grep -f keywords.txt timeline.csv`. +## Logon Summary Generator + +You can use the `-L` or `--logon-summary` option to output logon information summary(logon username, logon success and logon failed count). +You can get logon information each evtx file with `-d` option. + # Testing Hayabusa on Sample Evtx Files We have provided some sample evtx files for you to test hayabusa and/or create new rules at [https://github.com/Yamato-Security/hayabusa-sample-evtx](https://github.com/Yamato-Security/hayabusa-sample-evtx) diff --git a/rules b/rules index ea59e261..4144fb3e 160000 --- a/rules +++ b/rules @@ -1 +1 @@ -Subproject commit ea59e261dcb97b0ebcf971e024fbbbe7ee6d6cb3 +Subproject commit 4144fb3ea403de8096bb7ef85d31964e4e5ded73 diff --git a/src/detections/configs.rs b/src/detections/configs.rs index 9ef53a10..a6389005 100644 --- a/src/detections/configs.rs +++ b/src/detections/configs.rs @@ -90,6 +90,7 @@ fn build_app<'a>() -> ArgMatches<'a> { -U --utc 'Output time in UTC format. (Default: local time)' -t --thread-number=[NUMBER] 'Thread number. (Default: Optimal number for performance.)' -s --statistics 'Prints statistics of event IDs.' + -L --logon-summary 'User logon and failed logon summary' -q --quiet 'Quiet mode. Do not display the launch banner.' -Q --quiet-errors 'Quiet errors mode. Do not save error logs.' -p --pivot-keywords-list 'Create a list of pivot keywords.' diff --git a/src/detections/detection.rs b/src/detections/detection.rs index b453f3b6..f020cb91 100644 --- a/src/detections/detection.rs +++ b/src/detections/detection.rs @@ -121,11 +121,18 @@ impl Detection { .map(|rule_file_tuple| rule::create_rule(rule_file_tuple.0, rule_file_tuple.1)) .filter_map(return_if_success) .collect(); - Detection::print_rule_load_info( - &rulefile_loader.rulecounter, - &parseerror_count, - &rulefile_loader.ignorerule_count, - ); + if !configs::CONFIG + .read() + .unwrap() + .args + .is_present("logon-summary") + { + Detection::print_rule_load_info( + &rulefile_loader.rulecounter, + &parseerror_count, + &rulefile_loader.ignorerule_count, + ); + } ret } diff --git a/src/detections/print.rs b/src/detections/print.rs index 4ba395c2..8c8bf18d 100644 --- a/src/detections/print.rs +++ b/src/detections/print.rs @@ -55,6 +55,11 @@ lazy_static! { .unwrap() .args .is_present("statistics"); + pub static ref LOGONSUMMARY_FLAG: bool = configs::CONFIG + .read() + .unwrap() + .args + .is_present("logon-summary"); pub static ref TAGS_CONFIG: HashMap = Message::create_output_filter_config("config/output_tag.txt"); pub static ref CH_CONFIG: HashMap = diff --git a/src/lib.rs b/src/lib.rs index 5faf0723..45a8b1e5 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -6,3 +6,5 @@ pub mod omikuji; pub mod options; pub mod timeline; pub mod yaml; +#[macro_use] +extern crate prettytable; diff --git a/src/main.rs b/src/main.rs index 3ba25b25..d8951cf2 100644 --- a/src/main.rs +++ b/src/main.rs @@ -13,8 +13,8 @@ use hayabusa::detections::configs::load_pivot_keywords; use hayabusa::detections::detection::{self, EvtxRecordInfo}; use hayabusa::detections::pivot::PIVOT_KEYWORD; use hayabusa::detections::print::{ - AlertMessage, ERROR_LOG_PATH, ERROR_LOG_STACK, PIVOT_KEYWORD_LIST_FLAG, QUIET_ERRORS_FLAG, - STATISTICS_FLAG, + AlertMessage, ERROR_LOG_PATH, ERROR_LOG_STACK, LOGONSUMMARY_FLAG, PIVOT_KEYWORD_LIST_FLAG, + QUIET_ERRORS_FLAG, STATISTICS_FLAG, }; use hayabusa::detections::rule::{get_detection_keys, RuleNode}; use hayabusa::filter; @@ -176,6 +176,10 @@ impl App { println!("Generating Event ID Statistics"); println!(); } + if *LOGONSUMMARY_FLAG { + println!("Generating Logons Summary"); + println!(); + } if configs::CONFIG .read() .unwrap() @@ -229,6 +233,9 @@ impl App { .unwrap() .args .is_present("level-tuning") + && std::env::args() + .into_iter() + .any(|arg| arg.contains("level-tuning")) { let level_tuning_config_path = configs::CONFIG .read() @@ -458,7 +465,7 @@ impl App { pb.inc(); } detection.add_aggcondition_msges(&self.rt); - if !*STATISTICS_FLAG && !*PIVOT_KEYWORD_LIST_FLAG { + if !(*STATISTICS_FLAG || *LOGONSUMMARY_FLAG || *PIVOT_KEYWORD_LIST_FLAG) { after_fact(); } } @@ -531,13 +538,14 @@ impl App { // timeline機能の実行 tl.start(&records_per_detect); - if !*STATISTICS_FLAG { + if !(*STATISTICS_FLAG || *LOGONSUMMARY_FLAG) { // ruleファイルの検知 detection = detection.start(&self.rt, records_per_detect); } } tl.tm_stats_dsp_msg(); + tl.tm_logon_stats_dsp_msg(); detection } diff --git a/src/timeline/statistics.rs b/src/timeline/statistics.rs index 7ca04960..ab9e60bf 100644 --- a/src/timeline/statistics.rs +++ b/src/timeline/statistics.rs @@ -8,6 +8,7 @@ pub struct EventStatistics { pub start_time: String, pub end_time: String, pub stats_list: HashMap, + pub stats_login_list: HashMap, } /** * Windows Event Logの統計情報を出力する @@ -19,6 +20,7 @@ impl EventStatistics { start_time: String, end_time: String, stats_list: HashMap, + stats_login_list: HashMap, ) -> EventStatistics { EventStatistics { total, @@ -26,10 +28,11 @@ impl EventStatistics { start_time, end_time, stats_list, + stats_login_list, } } - pub fn start(&mut self, records: &[EvtxRecordInfo]) { + pub fn evt_stats_start(&mut self, records: &[EvtxRecordInfo]) { // 引数でstatisticsオプションが指定されている時だけ、統計情報を出力する。 if !configs::CONFIG .read() @@ -49,6 +52,22 @@ impl EventStatistics { self.stats_eventid(records); } + pub fn logon_stats_start(&mut self, records: &[EvtxRecordInfo]) { + // 引数でstatisticsオプションが指定されている時だけ、統計情報を出力する。 + if !configs::CONFIG + .read() + .unwrap() + .args + .is_present("logon-summary") + { + return; + } + + self.stats_time_cnt(records); + + self.stats_login_eventid(records); + } + fn stats_time_cnt(&mut self, records: &[EvtxRecordInfo]) { if records.is_empty() { return; @@ -93,4 +112,29 @@ impl EventStatistics { } // return evtstat_map; } + // Login event + fn stats_login_eventid(&mut self, records: &[EvtxRecordInfo]) { + for record in records.iter() { + let evtid = utils::get_event_value("EventID", &record.record); + if evtid.is_none() { + continue; + } + let username = utils::get_event_value("TargetUserName", &record.record); + let idnum = evtid.unwrap(); + let countlist: [usize; 2] = [0, 0]; + if idnum == 4624 { + let count: &mut [usize; 2] = self + .stats_login_list + .entry(username.unwrap().to_string()) + .or_insert(countlist); + count[0] += 1; + } else if idnum == 4625 { + let count: &mut [usize; 2] = self + .stats_login_list + .entry(username.unwrap().to_string()) + .or_insert(countlist); + count[1] += 1; + } + } + } } diff --git a/src/timeline/timelines.rs b/src/timeline/timelines.rs index 2883bee9..20e1ea77 100644 --- a/src/timeline/timelines.rs +++ b/src/timeline/timelines.rs @@ -1,4 +1,5 @@ use crate::detections::{configs, detection::EvtxRecordInfo}; +use prettytable::{Cell, Row, Table}; use super::statistics::EventStatistics; use hashbrown::HashMap; @@ -21,13 +22,16 @@ impl Timeline { let starttm = "".to_string(); let endtm = "".to_string(); let statslst = HashMap::new(); + let statsloginlst = HashMap::new(); - let statistic = EventStatistics::new(totalcnt, filepath, starttm, endtm, statslst); + let statistic = + EventStatistics::new(totalcnt, filepath, starttm, endtm, statslst, statsloginlst); Timeline { stats: statistic } } pub fn start(&mut self, records: &[EvtxRecordInfo]) { - self.stats.start(records); + self.stats.evt_stats_start(records); + self.stats.logon_stats_start(records); } pub fn tm_stats_dsp_msg(&mut self) { @@ -64,6 +68,31 @@ impl Timeline { println!("{}", msgprint); } } + + pub fn tm_logon_stats_dsp_msg(&mut self) { + if !configs::CONFIG + .read() + .unwrap() + .args + .is_present("logon-summary") + { + return; + } + // 出力メッセージ作成 + let mut sammsges: Vec = Vec::new(); + sammsges.push("---------------------------------------".to_string()); + sammsges.push(format!("Evtx File Path: {}", self.stats.filepath)); + sammsges.push(format!("Total Event Records: {}\n", self.stats.total)); + sammsges.push(format!("First Timestamp: {}", self.stats.start_time)); + sammsges.push(format!("Last Timestamp: {}\n", self.stats.end_time)); + sammsges.push("---------------------------------------".to_string()); + for msgprint in sammsges.iter() { + println!("{}", msgprint); + } + + self.tm_loginstats_tb_set_msg(); + } + // イベントID毎の出力メッセージ生成 fn tm_stats_set_msg(&self, mapsorted: Vec<(&std::string::String, &usize)>) -> Vec { let mut msges: Vec = Vec::new(); @@ -101,4 +130,38 @@ impl Timeline { msges.push("---------------------------------------".to_string()); msges } + // ユーザ毎のログイン統計情報出力メッセージ生成 + fn tm_loginstats_tb_set_msg(&self) { + println!("Logon Summary"); + if self.stats.stats_login_list.is_empty() { + let mut loginmsges: Vec = Vec::new(); + loginmsges.push("-----------------------------------------".to_string()); + loginmsges.push("| No logon events were detected. |".to_string()); + loginmsges.push("-----------------------------------------\n".to_string()); + for msgprint in loginmsges.iter() { + println!("{}", msgprint); + } + } else { + let mut logins_stats_tb = Table::new(); + logins_stats_tb.set_titles(row!["User", "Failed", "Successful"]); + // 集計件数でソート + let mut mapsorted: Vec<_> = self.stats.stats_login_list.iter().collect(); + mapsorted.sort_by(|x, y| x.0.cmp(y.0)); + + for (key, values) in &mapsorted { + let mut username: String = key.to_string(); + //key.to_string().retain(|c| c != '\"'); + //key.to_string().pop(); + username.pop(); + username.remove(0); + logins_stats_tb.add_row(Row::new(vec![ + Cell::new(&username), + Cell::new(&values[1].to_string()), + Cell::new(&values[0].to_string()), + ])); + } + logins_stats_tb.printstd(); + println!(); + } + } }