Merge pull request #36 from YamatoSecurity/feature/message_display

Feature/message display
This commit is contained in:
nishikawaakira
2020-12-02 02:34:48 +00:00
committed by GitHub
45 changed files with 1384 additions and 2161 deletions

4
.gitignore vendored
View File

@@ -1,3 +1,5 @@
/target
/samples
*.test
*.test
/.vscode/
.DS_Store

1
Cargo.lock generated
View File

@@ -1192,6 +1192,7 @@ dependencies = [
"evtx",
"flate2",
"lazy_static",
"linked-hash-map",
"quick-xml 0.17.2",
"regex",
"serde",

View File

@@ -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"

19
config/eventkey_alias.txt Normal file
View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -1,4 +0,0 @@
[rule]
severity = "high"
name = "4103"
message = "Execute Pipeline"

View File

@@ -1,4 +0,0 @@
[rule]
severity = "high"
name = "4104"
message = "Excute Remote Command"

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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<dyn Error>> {
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());
}
}

View File

@@ -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<String, String>,
) {
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");
}
}
}
}

View File

@@ -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<String, String>,
) {
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);
}
}

View File

@@ -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<String, String>,
}
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<String, String>) {
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();
}
}

View File

@@ -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<Vec<String>>,
pub whitelist: Vec<Vec<String>>,
pub args: ArgMatches<'static>,
pub event_key_alias_config: EventKeyAliasConfig,
}
pub fn singleton() -> Box<SingletonReader> {
@@ -17,9 +15,8 @@ pub fn singleton() -> Box<SingletonReader> {
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<Vec<String>> {
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<String, String>,
}
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;
}

View File

@@ -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<String, String>,
}
pub struct Detection {}
impl Detection {
pub fn new() -> Detection {
Detection {
timeline_list: BTreeMap::new(),
}
Detection {}
}
pub fn start(&mut self, mut parser: EvtxParser<std::fs::File>) -> 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<std::fs::File>) {
// serialize from .etvx to jsons
let event_records: Vec<Value> = 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<Value, Error> =
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<RuleNode> = 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();
}
}

View File

@@ -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;

View File

@@ -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<String, String>,
) {
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<String, String>) {
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<String, String>) {
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);
}
}
}
}

View File

@@ -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<Utc>, 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<String, String> = 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::<String>();
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<Utc>) -> Vec<String> {
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<DateTime<Utc>, Vec<String>> {
&self.map
}
fn get_event_time(event_record: &Value) -> Option<DateTime<Utc>> {
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);
}
}

582
src/detections/rule.rs Normal file
View File

@@ -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<DetectionNode> {
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>) -> String {
return key_list
.iter()
.fold("detection -> selection".to_string(), |mut acc, cur| {
acc = acc + " -> " + cur;
return acc;
});
}
fn parse_selection(yaml: &Yaml) -> Option<Box<dyn SelectionNode>> {
// 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<String>, yaml: &Yaml) -> Box<dyn SelectionNode> {
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<DetectionNode>,
}
impl RuleNode {
pub fn init(&mut self) -> Result<(), Vec<String>> {
let mut errmsgs: Vec<String> = 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<Box<dyn SelectionNode>>,
}
impl DetectionNode {
fn init(&mut self) -> Result<(), Vec<String>> {
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<String>>;
}
// detection - selection配下でAND条件を表すード
struct AndSelectionNode {
pub child_nodes: Vec<Box<dyn SelectionNode>>,
}
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<String>> {
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<String>, cur: Vec<String>| -> Vec<String> {
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<Box<dyn SelectionNode>>,
}
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<String>> {
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<String>, cur: Vec<String>| -> Vec<String> {
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<String>,
select_value: Yaml,
matcher: Option<Box<dyn LeafMatcher>>,
}
impl LeafSelectionNode {
fn new(key_list: Vec<String>, 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<Box<dyn LeafMatcher>> {
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<String>> {
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<String>) -> bool;
fn is_match(&mut self, event_value: Option<&Value>) -> bool;
fn init(&mut self, key_list: &Vec<String>, select_value: &Yaml) -> Result<(), Vec<String>>;
}
// 正規表現で比較するロジックを表すクラス
struct RegexMatcher {
re: Option<Regex>,
}
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<String>) -> 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<String>, select_value: &Yaml) -> Result<(), Vec<String>> {
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<String>) -> bool {
if key_list.len() != 1 {
return false;
}
return key_list.get(0).unwrap() == "min_length";
}
fn init(&mut self, key_list: &Vec<String>, select_value: &Yaml) -> Result<(), Vec<String>> {
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<Vec<String>>,
}
impl RegexesFileMatcher {
fn new() -> RegexesFileMatcher {
return RegexesFileMatcher {
regexes_csv_content: vec![],
};
}
}
impl LeafMatcher for RegexesFileMatcher {
fn is_target_key(&self, key_list: &Vec<String>) -> bool {
if key_list.len() != 1 {
return false;
}
return key_list.get(0).unwrap() == "regexes";
}
fn init(&mut self, key_list: &Vec<String>, select_value: &Yaml) -> Result<(), Vec<String>> {
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<Vec<String>>,
}
impl WhitelistFileMatcher {
fn new() -> WhitelistFileMatcher {
return WhitelistFileMatcher {
whitelist_csv_content: vec![],
};
}
}
impl LeafMatcher for WhitelistFileMatcher {
fn is_target_key(&self, key_list: &Vec<String>) -> bool {
if key_list.len() != 1 {
return false;
}
return key_list.get(0).unwrap() == "whitelist";
}
fn init(&mut self, key_list: &Vec<String>, select_value: &Yaml) -> Result<(), Vec<String>> {
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,
};
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -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<String, String>,
) {
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<String, String>) {
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<String, String>,
) {
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);
}
};
}
}

View File

@@ -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<String, String>,
) {
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<String, String>) {
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<String, String>,
) {
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<String, String>) {
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<String, String>) {
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."
);
}
}
}
}
}
}

View File

@@ -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<Vec<String>>,
) -> 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<Vec<String>>) -> 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<Vec<Vec<String>>, 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, &regexes);
assert!(regextext == "Resource File To COFF Object Conversion Utility cvtres.exe\n");
let regextext = utils::check_regex("\\hogehoge.exe", 0, &regexes);
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));
}
}

View File

@@ -1,5 +1,4 @@
pub mod afterfact;
pub mod detections;
pub mod models;
pub mod omikuji;
pub mod yaml;

View File

@@ -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);
}

View File

@@ -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<String>,
#[serde(rename = "$value")]
pub text: Option<String>,
}
#[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<String>,
#[serde(rename = "Guid")]
guid: Option<String>,
}
#[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<String>,
#[serde(rename = "Level")]
level: String,
#[serde(rename = "Task")]
task: String,
#[serde(rename = "Opcode")]
opcode: Option<String>,
#[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<String>,
#[serde(rename = "Execution")]
execution: Option<Execution>,
#[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<String>,
}
#[derive(Debug, Deserialize, PartialEq)]
pub struct EventData {
#[serde(rename = "Data")]
pub data: Option<Vec<Data>>,
}
#[derive(Debug, Deserialize, PartialEq)]
pub struct UserData {
#[serde(rename = "LogFileCleared")]
pub log_file_cleared: Option<LogFileCleared>,
}
#[derive(Debug, Deserialize, PartialEq)]
pub struct LogFileCleared {
#[serde(rename = "SubjectUserSid")]
pub subject_user_sid: Option<String>,
#[serde(rename = "SubjectUserName")]
pub subject_user_name: Option<String>,
#[serde(rename = "SubjectDomainName")]
pub subject_domain_name: Option<String>,
#[serde(rename = "SubjectLogonId")]
pub subject_logon_id: Option<String>,
}
#[derive(Debug, Deserialize, PartialEq)]
pub struct Evtx {
#[serde(rename = "System")]
pub system: System,
#[serde(rename = "EventData")]
pub event_data: Option<EventData>,
#[serde(rename = "UserData")]
pub user_data: Option<UserData>,
}
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<String, String> {
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
}
}

View File

@@ -1 +0,0 @@
pub mod event;

View File

@@ -8,12 +8,12 @@ use std::path::{Path, PathBuf};
use yaml_rust::YamlLoader;
pub struct ParseYaml {
pub rules: Vec<yaml_rust::Yaml>,
pub files: Vec<yaml_rust::Yaml>,
}
impl ParseYaml {
pub fn new() -> ParseYaml {
ParseYaml { rules: Vec::new() }
ParseYaml { files: Vec::new() }
}
pub fn read_file(&self, path: PathBuf) -> Result<String, String> {
@@ -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!(
"*",