From ba635e3d5bd434ed8300dd29bdbaed7bc4cc4427 Mon Sep 17 00:00:00 2001 From: dwattttt Date: Wed, 30 Oct 2024 16:22:07 +1100 Subject: [PATCH 1/2] Add support for usermode crashdumps --- Cargo.lock | 197 ++++++++++++++++++++++++++++++++++++++++++++++- Cargo.toml | 1 + src/crashdump.rs | 101 ++++++++++++++++++++++++ src/main.rs | 31 ++++++++ 4 files changed, 329 insertions(+), 1 deletion(-) create mode 100644 src/crashdump.rs diff --git a/Cargo.lock b/Cargo.lock index ce19950..8b57e06 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -224,6 +224,24 @@ version = "0.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "06ea2b9bc92be3c2baa9334a323ebca2d6f074ff852cd1d7b11064035cd3868f" +[[package]] +name = "debugid" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef552e6f588e446098f6ba40d89ac146c8c7b64aade83c051ee00bb5d2bc18d" +dependencies = [ + "uuid", +] + +[[package]] +name = "deranged" +version = "0.3.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b42b6fa04a440b495c8b04d0e71b707c585f83cb9cb28cf8cd0d976c315e31b4" +dependencies = [ + "powerfmt", +] + [[package]] name = "encode_unicode" version = "0.3.6" @@ -440,6 +458,12 @@ version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d77f7ec81a6d05a3abb01ab6eb7590f6083d08449fe5a1c8b1e620283546ccb7" +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" + [[package]] name = "http" version = "0.2.11" @@ -615,12 +639,56 @@ version = "2.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f665ee40bc4a3c5590afb1e9677db74a508659dfd71e126420da8274909a0167" +[[package]] +name = "memmap2" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd3f7eed9d3848f8b98834af67102b720745c4ec028fcd0aa0239277e7de374f" +dependencies = [ + "libc", +] + [[package]] name = "mime" version = "0.3.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" +[[package]] +name = "minidump" +version = "0.22.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc7268c0afe5aa2fd6fde310e6cda158f0b3581f317c5a5c7d170861f9b90a6" +dependencies = [ + "debugid", + "encoding_rs", + "memmap2", + "minidump-common", + "num-traits", + "procfs-core", + "range-map", + "scroll 0.12.0", + "thiserror", + "time", + "tracing", + "uuid", +] + +[[package]] +name = "minidump-common" +version = "0.22.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0cde999358cca8fb9397c4298bb27c4a603c13ee6b3a646da514b20be582a21e" +dependencies = [ + "bitflags 2.4.1", + "debugid", + "num-derive", + "num-traits", + "range-map", + "scroll 0.12.0", + "smart-default", +] + [[package]] name = "miniz_oxide" version = "0.7.1" @@ -659,6 +727,32 @@ dependencies = [ "tempfile", ] +[[package]] +name = "num-conv" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" + +[[package]] +name = "num-derive" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed3955f1a9c7c0c15e092f9c887db08b1fc683305fdf6eb6684f22555355e202" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", +] + [[package]] name = "num_cpus" version = "1.16.0" @@ -764,7 +858,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "82040a392923abe6279c00ab4aff62d5250d1c8555dc780e4b02783a7aa74863" dependencies = [ "fallible-iterator", - "scroll", + "scroll 0.11.0", "uuid", ] @@ -778,6 +872,7 @@ dependencies = [ "futures", "indicatif", "mime", + "minidump", "pdb", "rand", "reqwest", @@ -818,6 +913,12 @@ version = "1.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7170ef9988bc169ba16dd36a7fa041e5c4cbeb6a35b76d4c03daded371eae7c0" +[[package]] +name = "powerfmt" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" + [[package]] name = "ppv-lite86" version = "0.2.17" @@ -833,6 +934,16 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "procfs-core" +version = "0.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "239df02d8349b06fc07398a3a1697b06418223b1c7725085e801e7c0fc6a12ec" +dependencies = [ + "bitflags 2.4.1", + "hex", +] + [[package]] name = "quote" version = "1.0.33" @@ -872,6 +983,15 @@ dependencies = [ "getrandom", ] +[[package]] +name = "range-map" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "12a5a2d6c7039059af621472a4389be1215a816df61aa4d531cfe85264aee95f" +dependencies = [ + "num-traits", +] + [[package]] name = "redox_syscall" version = "0.4.1" @@ -965,6 +1085,26 @@ version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "04c565b551bafbef4157586fa379538366e4385d42082f255bfd96e4fe8519da" +[[package]] +name = "scroll" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ab8598aa408498679922eff7fa985c25d58a90771bd6be794434c5277eab1a6" +dependencies = [ + "scroll_derive", +] + +[[package]] +name = "scroll_derive" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f81c2fde025af7e69b1d1420531c8a8811ca898919db177141a85313b1cb932" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "security-framework" version = "2.9.2" @@ -1055,6 +1195,17 @@ version = "1.11.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4dccd0940a2dcdf68d092b8cbab7dc0ad8fa938bf95787e1b916b0e3d0e8e970" +[[package]] +name = "smart-default" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eb01866308440fc64d6c44d9e86c5cc17adfe33c4d6eed55da9145044d0ffc1" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "socket2" version = "0.4.10" @@ -1146,6 +1297,37 @@ dependencies = [ "syn", ] +[[package]] +name = "time" +version = "0.3.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5dfd88e563464686c916c7e46e623e520ddc6d79fa6641390f2e3fa86e83e885" +dependencies = [ + "deranged", + "itoa", + "num-conv", + "powerfmt", + "serde", + "time-core", + "time-macros", +] + +[[package]] +name = "time-core" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef927ca75afb808a4d64dd374f00a2adf8d0fcff8e7b184af886c3c87ec4a3f3" + +[[package]] +name = "time-macros" +version = "0.2.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f252a68540fde3a3877aeea552b832b40ab9a69e318efd078774a01ddee1ccf" +dependencies = [ + "num-conv", + "time-core", +] + [[package]] name = "tinyvec" version = "1.6.0" @@ -1227,10 +1409,23 @@ version = "0.1.40" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c3523ab5a71916ccf420eebdf5521fcef02141234bbc0b8a49f2fdc4544364ef" dependencies = [ + "log", "pin-project-lite", + "tracing-attributes", "tracing-core", ] +[[package]] +name = "tracing-attributes" +version = "0.1.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34704c8d6ebcbc939824180af020566b01a7c01f80641264eba0999f6c2b6be7" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "tracing-core" version = "0.1.32" diff --git a/Cargo.toml b/Cargo.toml index 10fd230..7385a6e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -15,6 +15,7 @@ clap = { version = "4.4.11", features = ["derive"] } futures = "0.3" indicatif = { version = "0.17.2", features = ["tokio"] } mime = "0.3" +minidump = "0.22.2" pdb = "0.8.0" rand = "0.8" reqwest = "0.11.13" diff --git a/src/crashdump.rs b/src/crashdump.rs new file mode 100644 index 0000000..6934e66 --- /dev/null +++ b/src/crashdump.rs @@ -0,0 +1,101 @@ +use core::str; +use std::{ + ffi::OsStr, + path::{Path, PathBuf}, + str::FromStr, +}; + +use anyhow::Context; +use minidump::{CodeView, MinidumpModuleList}; + +pub(crate) fn get_module_list_from_crash( + crash: &Path, + binaries: bool, +) -> anyhow::Result> { + // Map the crashdump + let dump = minidump::Minidump::read_path(crash).context("failed to parse crashdump")?; + + let mut manifest = Vec::new(); + + for module in dump + .get_stream::() + .context("failed to get module list from crashdump")? + .iter() + { + if binaries { + match PathBuf::from_str(&module.name) { + Ok(bin_path) => match bin_path.file_name().and_then(|x| x.to_str()) { + Some(bin_filename) => { + manifest.push(format!( + "{},{:x}{:x},2", + bin_filename, module.raw.time_date_stamp, module.raw.size_of_image + )); + } + None => println!("Module '{}' binary is missing path stem", module.name), + }, + Err(_) => println!("Module '{}' is an invalid path", module.name), + } + } + + match &module.codeview_info { + Some(CodeView::Pdb70(info)) => { + // Get the pdb name + // Sometimes this includes a path, so we clean that off + // Sometimes this glob of data also includes non-null stuff after a null (e.g. '000000000000000'), so we strip that too + let pdb_path_slice = + if let Some(null_offset) = info.pdb_file_name.iter().position(|x| *x == 0) { + &info.pdb_file_name[0..null_offset] + } else { + &info.pdb_file_name + }; + + if let Ok(mut pdb) = str::from_utf8(pdb_path_slice) { + // Extract the file name from the pdb name, if it exists + if let Some(stem) = Path::new(pdb).file_name().and_then(OsStr::to_str) { + pdb = stem; + } + + manifest.push(format!( + "{},{:08X}{:04X}{:04X}{:02X}{:02X}{:02X}{:02X}{:02X}{:02X}{:02X}{:02X}{:x},1", + pdb, + info.signature.data1, + info.signature.data2, + info.signature.data3, + info.signature.data4[0], + info.signature.data4[1], + info.signature.data4[2], + info.signature.data4[3], + info.signature.data4[4], + info.signature.data4[5], + info.signature.data4[6], + info.signature.data4[7], + info.age + )); + } else { + println!( + "Module '{}' has invalid pdb path in codeview information", + module.name + ) + } + } + Some(CodeView::Pdb20(_)) => { + println!( + "Module '{}' has old and unhandled PDB codeview information", + module.name + ) + } + Some(CodeView::Elf(_)) => { + println!("Module '{}' has ELF codeview information (?!)", module.name) + } + Some(CodeView::Unknown(_)) => { + println!("Module '{}' has unknown codeview information", module.name) + } + None => println!( + "Module '{}' does not contain codeview information", + module.name + ), + } + } + + Ok(manifest) +} diff --git a/src/main.rs b/src/main.rs index c6cca35..7a24d79 100644 --- a/src/main.rs +++ b/src/main.rs @@ -12,6 +12,7 @@ use anyhow::Context; use clap::{Parser, Subcommand, ValueEnum}; +use crashdump::get_module_list_from_crash; use indicatif::{MultiProgress, ProgressStyle}; use symsrv::{SymSrvList, SymSrvSpec}; @@ -29,6 +30,7 @@ use tokio::{ use symsrv::{nonblocking::SymSrv, DownloadError, DownloadStatus, SymFileInfo}; +mod crashdump; mod pe; #[allow(dead_code)] mod symsrv; @@ -467,6 +469,16 @@ enum Command { /// Various information-related subcommands #[command(subcommand)] Info(InfoCommand), + /// Generate a manifest file for a crashdump, optionally including binaries in the manifest + Crashdump { + /// The crashdump file to process + crashdump_path: PathBuf, + /// Manifest file to generate + manifest_path: PathBuf, + /// Download binaries as well as well as symbols + #[arg(short, long)] + binaries: bool, + }, } #[derive(Subcommand, Clone, Debug)] @@ -692,6 +704,25 @@ async fn run() -> anyhow::Result<()> { println!("{}", pdb); } }, + Command::Crashdump { + crashdump_path, + manifest_path, + binaries, + } => { + let manifest_data = get_module_list_from_crash(&crashdump_path, binaries) + .context("Failed to generate manifest for crashdump")?; + + let mut output_file = tokio::fs::File::create(manifest_path) + .await + .context("Failed to create output manifest file")?; + + for manifest_entry in manifest_data { + output_file + .write(format!("{}\n", &manifest_entry).as_bytes()) + .await + .context("Failed to write to output manifest file")?; + } + } } Ok(()) From be39d7071a948a7f10c4037d8ec9bb1fb5b51c21 Mon Sep 17 00:00:00 2001 From: dwattttt Date: Sun, 17 Nov 2024 20:17:21 +1100 Subject: [PATCH 2/2] Made manifest output optional for crashdumps --- src/main.rs | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/main.rs b/src/main.rs index 7a24d79..df9ad31 100644 --- a/src/main.rs +++ b/src/main.rs @@ -473,8 +473,8 @@ enum Command { Crashdump { /// The crashdump file to process crashdump_path: PathBuf, - /// Manifest file to generate - manifest_path: PathBuf, + /// The manifest path + manifest: Option, /// Download binaries as well as well as symbols #[arg(short, long)] binaries: bool, @@ -706,9 +706,11 @@ async fn run() -> anyhow::Result<()> { }, Command::Crashdump { crashdump_path, - manifest_path, + manifest, binaries, } => { + let manifest_path = manifest.unwrap_or(PathBuf::from("manifest")); + let manifest_data = get_module_list_from_crash(&crashdump_path, binaries) .context("Failed to generate manifest for crashdump")?;