Skip to content

Commit

Permalink
Merge pull request #56 from dfir-dd/53-stack-trace-error-in-evtxanaly…
Browse files Browse the repository at this point in the history
…ze-pstree-function

53 stack trace error in evtxanalyze pstree function
  • Loading branch information
janstarke authored Jul 29, 2024
2 parents 4176a93 + cb837c4 commit b994ef0
Show file tree
Hide file tree
Showing 10 changed files with 289 additions and 147 deletions.
124 changes: 55 additions & 69 deletions Cargo.lock

Large diffs are not rendered by default.

5 changes: 3 additions & 2 deletions Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "dfir-toolkit"
version = "0.11.1"
version = "0.11.2"
edition = "2021"
authors = ["Jan Starke <jan.starke@posteo.de>", "Deborah Mahn <deborah.mahn@dfir-dd.de>"]
description = "CLI tools for digital forensics and incident response"
Expand Down Expand Up @@ -106,7 +106,7 @@ pol_export = []
evtxscan = ["evtx"]
evtxcat = ["evtx", "colored_json", "term-table", "termsize"]
evtxls = ["evtx", "colored", "lazy-regex", "regex", "sigpipe", "dfirtk-eventdata"]
evtxanalyze = ["evtx", "dfirtk-sessionevent-derive", "dfirtk-eventdata"]
evtxanalyze = ["evtx", "dfirtk-sessionevent-derive", "dfirtk-eventdata", "exitcode"]
evtx2bodyfile = ["evtx", "getset", "ouroboros", "indicatif"]
ipgrep = []
ts2date = ["regex"]
Expand Down Expand Up @@ -161,6 +161,7 @@ colored = {version = "2", optional=true}
lazy-regex = {version = "3.0.0", optional=true}
sigpipe = {version = "0", optional=true}
phf = {version = "0.11", optional=true}
exitcode = {version="1.1.2", optional=true}

# evtx2bodyfile
indicatif = {version="0.17", optional=true}
Expand Down
34 changes: 31 additions & 3 deletions src/bin/evtxanalyze/cli.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
use std::{io::stdout, path::PathBuf};

use anyhow::bail;
use clap::{Parser, Subcommand, ValueEnum, ValueHint};
use dfir_toolkit::common::HasVerboseFlag;
use log::LevelFilter;
Expand Down Expand Up @@ -82,6 +83,20 @@ impl Cli {
evtx_files_dir,
session_id,
} => {
if !evtx_files_dir.exists() {
bail!(
"directory '{}' does not exist. Aborting now.",
evtx_files_dir.to_string_lossy()
)
}

if !evtx_files_dir.is_dir() {
bail!(
"'{}' is no directory. Aborting now.",
evtx_files_dir.to_string_lossy()
);
}

let sessions = SessionStore::import(evtx_files_dir, true)?;
match sessions.find_session(session_id) {
None => log::error!("no value found for session id {session_id}"),
Expand All @@ -105,6 +120,20 @@ impl Cli {
evtx_files_dir,
include_anonymous,
} => {
if !evtx_files_dir.exists() {
bail!(
"directory '{}' does not exist. Aborting now.",
evtx_files_dir.to_string_lossy()
)
}

if !evtx_files_dir.is_dir() {
bail!(
"'{}' is no directory. Aborting now.",
evtx_files_dir.to_string_lossy()
);
}

let sessions = SessionStore::import(evtx_files_dir, *include_anonymous)?;

let mut csv_writer = csv::Writer::from_writer(stdout());
Expand All @@ -119,9 +148,8 @@ impl Cli {
}
}


impl HasVerboseFlag for Cli {
fn log_level_filter(&self)-> LevelFilter {
fn log_level_filter(&self) -> LevelFilter {
self.verbose.log_level_filter()
}
}
}
20 changes: 18 additions & 2 deletions src/bin/evtxanalyze/main.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
use cli::{Cli, Command};
use pstree::display_pstree;
use dfir_toolkit::common::FancyParser;
use log::log_enabled;
use pstree::display_pstree;

mod cli;
mod pstree;
Expand All @@ -9,10 +10,25 @@ mod sessions;
fn main() -> anyhow::Result<()> {
let cli = Cli::parse_cli();

match &cli.command {
let result = match &cli.command {
//TODO: move `display_pstree` into `impl Cli`
Command::PsTree { .. } => display_pstree(&cli),
Command::Sessions { .. } => cli.display_sessions(),
Command::Session { .. } => cli.display_single_session(),
};

if let Err(why) = result {
log::error!("{why}");
if let Some(cause) = why.source() {
log::error!("caused by: {cause}");
}
if log_enabled!(log::Level::Warn) {
for line in format!("{}", why.backtrace()).lines() {
log::warn!("{line}");
}
}
std::process::exit(exitcode::DATAERR);
}

Ok(())
}
68 changes: 50 additions & 18 deletions src/bin/evtxanalyze/pstree/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ pub mod unique_pid;
use std::{
cell::RefCell,
collections::{BTreeMap, HashMap, HashSet},
io::stdout,
ops::Deref,
rc::{Rc, Weak},
};

Expand All @@ -12,7 +14,10 @@ pub(crate) use process::*;
use regex::Regex;
use serde_json::{json, Value};

use crate::{cli::{Command, Format}, pstree::unique_pid::UniquePid};
use crate::{
cli::{Command, Format},
pstree::unique_pid::UniquePid,
};

use super::Cli;

Expand All @@ -21,7 +26,7 @@ pub(crate) fn display_pstree(cli: &Cli) -> anyhow::Result<()> {
Command::PsTree {
username,
evtx_file,
format
format,
} => {
let username_regex = username
.as_ref()
Expand All @@ -37,21 +42,39 @@ pub(crate) fn display_pstree(cli: &Cli) -> anyhow::Result<()> {

let mut parser = EvtxParser::from_path(evtx_file)?;
let mut unique_pids = HashMap::new();
let events: HashMap<_, _> = parser
.records_json_value()
.map(|r| r.expect("error reading event"))
.map(Process::try_from)
.filter_map(|r| r.expect("invalid event"))
.filter(has_username)
.map(|e| {
let pid = UniquePid::from(&e);
unique_pids
.entry(e.new_process_id)
.or_insert_with(HashSet::new)
.insert(pid.clone());
(pid, Rc::new(RefCell::new(e)))
})
.collect();
let mut events = HashMap::new();
let mut handled_records = 0;
let mut expected_records: usize = 0;
for record in parser.records_json_value() {
expected_records += 1;
match record {
Err(why) => {
log::error!("error while parsing a record; read {handled_records} until now. I'll try to continue with the next record");
log::warn!("{why}")
}
Ok(record) => match Process::try_from(record) {
Err(why) => log::error!("{why}"),
Ok(Some(process)) => {
if has_username(&process) {
let pid = UniquePid::from(&process);
unique_pids
.entry(process.new_process_id)
.or_insert_with(HashSet::new)
.insert(pid.clone());
events.insert(pid, Rc::new(RefCell::new(process)));
}
handled_records += 1;
}
Ok(None) => handled_records += 1,
},
}
}

log::info!("finished reading all records");

if handled_records < expected_records {
log::warn!("I expected {expected_records}, but only {handled_records} could be handled.")
}

log::warn!("found {} process creations", events.len());

Expand Down Expand Up @@ -110,7 +133,16 @@ pub(crate) fn display_pstree(cli: &Cli) -> anyhow::Result<()> {
println!("{}", serde_json::to_string_pretty(&procs_as_json)?);
}

Format::Csv => unimplemented!(),
Format::Csv => {
let mut wtr = csv::Writer::from_writer(stdout().lock());
let processes: HashSet<_> = events
.values()
.map(|p| ProcessTableEntry::from(p.as_ref().borrow().deref()))
.collect();
for process in processes {
wtr.serialize(process)?;
}
}

Format::Markdown => {
let root_processes: BTreeMap<_, _> = events
Expand Down
114 changes: 88 additions & 26 deletions src/bin/evtxanalyze/pstree/process.rs
Original file line number Diff line number Diff line change
@@ -1,31 +1,93 @@
use std::{collections::{BTreeMap, HashMap}, rc::Weak, cell::RefCell, fmt::Display};
use std::{
cell::RefCell,
collections::{BTreeMap, HashMap},
fmt::Display,
hash::Hash,
rc::Weak,
};

use anyhow::bail;
use chrono::{DateTime, Utc};
use evtx::SerializedEvtxRecord;
use serde_json::{Value, json};


pub (crate) struct Process {
pub (crate) timestamp: DateTime<Utc>,
pub (crate) event_record_id: u64,
pub (crate) subject_user_sid: String,
pub (crate) subject_user_name: String,
pub (crate) subject_domain_name: String,
pub (crate) subject_logon_id: String,
pub (crate) new_process_id: u64,
pub (crate) new_process_name: String,
pub (crate) token_elevation_type: String,
pub (crate) process_id: u64,
pub (crate) command_line: String,
pub (crate) target_user_sid: String,
pub (crate) target_user_name: String,
pub (crate) target_domain_name: String,
pub (crate) target_logon_id: String,
pub (crate) parent_process_name: Option<String>,
pub (crate) mandatory_label: Option<String>,
pub (crate) children: BTreeMap<DateTime<Utc>, Weak<RefCell<Self>>>,
pub (crate) is_root: bool,
use serde::Serialize;
use serde_json::{json, Value};

pub(crate) struct Process {
pub(crate) timestamp: DateTime<Utc>,
pub(crate) event_record_id: u64,
pub(crate) subject_user_sid: String,
pub(crate) subject_user_name: String,
pub(crate) subject_domain_name: String,
pub(crate) subject_logon_id: String,
pub(crate) new_process_id: u64,
pub(crate) new_process_name: String,
pub(crate) token_elevation_type: String,
pub(crate) process_id: u64,
pub(crate) command_line: String,
pub(crate) target_user_sid: String,
pub(crate) target_user_name: String,
pub(crate) target_domain_name: String,
pub(crate) target_logon_id: String,
pub(crate) parent_process_name: Option<String>,
pub(crate) mandatory_label: Option<String>,
pub(crate) children: BTreeMap<DateTime<Utc>, Weak<RefCell<Self>>>,
pub(crate) is_root: bool,
}

#[derive(Serialize, Clone)]
pub(crate) struct ProcessTableEntry {
pub(crate) subject_user_sid: String,
pub(crate) subject_user_name: String,
pub(crate) subject_domain_name: String,
pub(crate) subject_logon_id: String,
pub(crate) new_process_id: u64,
pub(crate) new_process_name: String,
pub(crate) token_elevation_type: String,
pub(crate) process_id: u64,
pub(crate) command_line: String,
pub(crate) target_user_sid: String,
pub(crate) target_user_name: String,
pub(crate) target_domain_name: String,
pub(crate) target_logon_id: String,
pub(crate) parent_process_name: Option<String>,
}

impl From<&Process> for ProcessTableEntry {
fn from(value: &Process) -> Self {
Self {
subject_user_sid: value.subject_user_sid.clone(),
subject_user_name: value.subject_user_name.clone(),
subject_domain_name: value.subject_domain_name.clone(),
subject_logon_id: value.subject_logon_id.clone(),
new_process_id: value.new_process_id,
new_process_name: value.new_process_name.clone(),
token_elevation_type: value.token_elevation_type.clone(),
process_id: value.process_id,
command_line: value.command_line.clone(),
target_user_sid: value.target_user_sid.clone(),
target_user_name: value.target_user_name.clone(),
target_domain_name: value.target_domain_name.clone(),
target_logon_id: value.target_logon_id.clone(),
parent_process_name: value.parent_process_name.clone(),
}
}
}

impl Hash for ProcessTableEntry {
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
self.process_id.hash(state);
self.command_line.hash(state);
self.parent_process_name.hash(state);
}
}

impl Eq for ProcessTableEntry {}
impl PartialEq for ProcessTableEntry {
fn eq(&self, other: &Self) -> bool {
self.process_id == other.process_id
&& self.command_line == other.command_line
&& self.parent_process_name == other.parent_process_name
}
}

impl From<&Process> for Value {
Expand Down Expand Up @@ -207,10 +269,10 @@ impl Process {
.into();
let parent_process_name = from_json_or_null!(event_data, "ParentProcessName")
.as_str()
.map(|s|s.to_owned());
.map(|s| s.to_owned());
let mandatory_label = from_json_or_null!(event_data, "MandatoryLabel")
.as_str()
.map(|s|s.to_owned());
.map(|s| s.to_owned());

Ok(Some(Self {
timestamp: record.timestamp,
Expand Down
10 changes: 8 additions & 2 deletions src/bin/evtxanalyze/sessions/session_store.rs
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,14 @@ impl SessionStore {

for filename in KNOWN_FILES {
let path = evtx_files_dir.join(filename);
if !(path.exists() && path.is_file()) {
log::warn!("unable to read file {}", path.display());

if ! path.exists() {
log::error!("expected file '{}', but it does not exist. Omitting it...", path.display());
continue;
}

if ! path.is_file() {
log::error!("tried to read '{}', but it is not a file. Omiting it...", path.display());
continue;
}

Expand Down
Loading

0 comments on commit b994ef0

Please sign in to comment.