From ba635e3d5bd434ed8300dd29bdbaed7bc4cc4427 Mon Sep 17 00:00:00 2001 From: dwattttt Date: Wed, 30 Oct 2024 16:22:07 +1100 Subject: [PATCH 1/5] 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/5] 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")?; From c7a84f596631d4a0c9ba8b7a9bdc29126bd43bb6 Mon Sep 17 00:00:00 2001 From: dwattttt Date: Thu, 31 Oct 2024 14:15:36 +1100 Subject: [PATCH 3/5] Make PE parsing for PDB more flexible, in the expectation of parsing PDBs from PEs not on disk --- src/main.rs | 19 +++++++++++++++++-- src/pe.rs | 21 ++++++++++----------- 2 files changed, 27 insertions(+), 13 deletions(-) diff --git a/src/main.rs b/src/main.rs index df9ad31..1ae7f7c 100644 --- a/src/main.rs +++ b/src/main.rs @@ -103,7 +103,9 @@ fn get_pdb_path>(pdbname: P) -> anyhow::Result { } fn get_file_path(filename: &Path) -> anyhow::Result { - let (_, _, pe_header, image_size, _) = pe::parse_pe(filename)?; + let fd = std::fs::File::open(filename)?; + + let (_, _, pe_header, image_size, _) = pe::parse_pe(fd)?; let filename = filename .file_name() @@ -137,7 +139,20 @@ fn get_file_path(filename: &Path) -> anyhow::Result { /// Returns a string which is the same representation you get from `symchk` /// when outputting a manifest for the PDB ",,1" fn get_pdb(filename: &Path) -> anyhow::Result { - let (mut fd, mz_header, pe_header, _, num_tables) = pe::parse_pe(filename)?; + let fd = std::fs::File::open(filename)?; + + get_pdb_from_reader(fd) +} + +/// Given a `reader`, attempt to parse out any mention of a PDB file in it. +/// +/// This returns success if it successfully parses the MZ, PE, finds a debug +/// header, matches RSDS signature, and contains a valid reference to a PDB. +/// +/// Returns a string which is the same representation you get from `symchk` +/// when outputting a manifest for the PDB ",,1" +fn get_pdb_from_reader(fd: impl Read + Seek) -> anyhow::Result { + let (mut fd, mz_header, pe_header, _, num_tables) = pe::parse_pe(fd)?; /* Load all the data directories into a vector */ let mut data_dirs = Vec::new(); diff --git a/src/pe.rs b/src/pe.rs index 6879e25..3c5ab63 100644 --- a/src/pe.rs +++ b/src/pe.rs @@ -1,6 +1,5 @@ //! Contains functionality for parsing MZ/PE files use std::io::{self, Read, Seek, SeekFrom}; -use std::path::Path; use zerocopy::{AsBytes, FromBytes}; @@ -160,29 +159,29 @@ pub const IMAGE_DEBUG_TYPE_CODEVIEW: u32 = 2; /// Read a structure from a file stream, directly interpreting the raw bytes /// of the file as T. -pub fn read_struct(fd: &mut std::fs::File) -> io::Result { +pub fn read_struct(fd: &mut (impl Read + Seek)) -> io::Result { let mut ret: T = T::new_zeroed(); fd.read_exact(ret.as_bytes_mut())?; Ok(ret) } -pub fn parse_pe(filename: &Path) -> anyhow::Result<(std::fs::File, MZHeader, PEHeader, u32, u32)> { - let mut fd = std::fs::File::open(filename)?; - +pub fn parse_pe( + mut reader: T, +) -> anyhow::Result<(T, MZHeader, PEHeader, u32, u32)> { /* Check for an MZ header */ - let mz_header: MZHeader = read_struct(&mut fd)?; + let mz_header: MZHeader = read_struct(&mut reader)?; if &mz_header.signature != b"MZ" { anyhow::bail!("No MZ header present"); } /* Seek to where the PE header should be */ - if fd.seek(SeekFrom::Start(mz_header.new_header as u64))? != mz_header.new_header as u64 { + if reader.seek(SeekFrom::Start(mz_header.new_header as u64))? != mz_header.new_header as u64 { anyhow::bail!("Failed to seek to PE header"); } /* Check for a PE header */ - let pe_header: PEHeader = read_struct(&mut fd)?; + let pe_header: PEHeader = read_struct(&mut reader)?; if &pe_header.signature != b"PE\0\0" { anyhow::bail!("No PE header present"); } @@ -190,15 +189,15 @@ pub fn parse_pe(filename: &Path) -> anyhow::Result<(std::fs::File, MZHeader, PEH /* Grab the number of tables from the bitness-specific table */ let (image_size, num_tables) = match pe_header.machine { IMAGE_FILE_MACHINE_I386 => { - let opthdr: WindowsPEHeader32 = read_struct(&mut fd)?; + let opthdr: WindowsPEHeader32 = read_struct(&mut reader)?; (opthdr.size_of_image, opthdr.num_tables) } IMAGE_FILE_MACHINE_IA64 | IMAGE_FILE_MACHINE_AMD64 => { - let opthdr: WindowsPEHeader64 = read_struct(&mut fd)?; + let opthdr: WindowsPEHeader64 = read_struct(&mut reader)?; (opthdr.size_of_image, opthdr.num_tables) } _ => anyhow::bail!("Unsupported PE machine type"), }; - Ok((fd, mz_header, pe_header, image_size, num_tables)) + Ok((reader, mz_header, pe_header, image_size, num_tables)) } From 56ff3fef5b7277ca520a11ebe1694aa5116c1e86 Mon Sep 17 00:00:00 2001 From: dwattttt Date: Thu, 31 Oct 2024 20:50:26 +1100 Subject: [PATCH 4/5] Kernel crashdump support (for all current types other than small/minidump) --- Cargo.lock | 23 ++++++-- Cargo.toml | 1 + src/kernel_crashdump.rs | 122 ++++++++++++++++++++++++++++++++++++++++ src/main.rs | 37 ++++++++++++ src/pe.rs | 2 +- 5 files changed, 178 insertions(+), 7 deletions(-) create mode 100644 src/kernel_crashdump.rs diff --git a/Cargo.lock b/Cargo.lock index 8b57e06..89433ea 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -112,9 +112,9 @@ checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" [[package]] name = "bitflags" -version = "2.4.1" +version = "2.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "327762f6e5a765692301e5bb513e0d9fef63be86bbc14528052b1cd3e6f03e07" +checksum = "b048fb63fd8b5923fc5aa7b340d8e156aec7ec02f0c78fa8a6ddc2613f6f71de" [[package]] name = "bumpalo" @@ -599,6 +599,16 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "kdmp-parser" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9020e118f80785d03e42307666fbbff8cb5cf11ac06f8a662c528191ac040404" +dependencies = [ + "bitflags 2.6.0", + "thiserror", +] + [[package]] name = "lazy_static" version = "1.4.0" @@ -680,7 +690,7 @@ version = "0.22.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0cde999358cca8fb9397c4298bb27c4a603c13ee6b3a646da514b20be582a21e" dependencies = [ - "bitflags 2.4.1", + "bitflags 2.6.0", "debugid", "num-derive", "num-traits", @@ -790,7 +800,7 @@ version = "0.10.66" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9529f4786b70a3e8c61e11179af17ab6188ad8d0ded78c5529441ed39d4bd9c1" dependencies = [ - "bitflags 2.4.1", + "bitflags 2.6.0", "cfg-if", "foreign-types", "libc", @@ -871,6 +881,7 @@ dependencies = [ "clap", "futures", "indicatif", + "kdmp-parser", "mime", "minidump", "pdb", @@ -940,7 +951,7 @@ version = "0.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "239df02d8349b06fc07398a3a1697b06418223b1c7725085e801e7c0fc6a12ec" dependencies = [ - "bitflags 2.4.1", + "bitflags 2.6.0", "hex", ] @@ -1051,7 +1062,7 @@ version = "0.38.28" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "72e572a5e8ca657d7366229cdde4bd14c4eb5499a9573d4d366fe1b599daa316" dependencies = [ - "bitflags 2.4.1", + "bitflags 2.6.0", "errno", "libc", "linux-raw-sys", diff --git a/Cargo.toml b/Cargo.toml index 7385a6e..1b9c8fb 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -14,6 +14,7 @@ base64 = "0.13" clap = { version = "4.4.11", features = ["derive"] } futures = "0.3" indicatif = { version = "0.17.2", features = ["tokio"] } +kdmp-parser = "0.5.0" mime = "0.3" minidump = "0.22.2" pdb = "0.8.0" diff --git a/src/kernel_crashdump.rs b/src/kernel_crashdump.rs new file mode 100644 index 0000000..7a2da79 --- /dev/null +++ b/src/kernel_crashdump.rs @@ -0,0 +1,122 @@ +use std::{ + convert::TryInto, + ffi::OsStr, + io::{ErrorKind, Read, Seek}, + ops::Range, + path::Path, +}; + +use anyhow::Context; +use kdmp_parser::{Gva, Gxa}; + +use crate::{get_pdb_from_reader, pe}; + +struct DumpPE<'a> { + dump: &'a kdmp_parser::KernelDumpParser, + pe_range: &'a Range, + offset: isize, +} + +impl<'a> Read for &mut DumpPE<'a> { + fn read(&mut self, buf: &mut [u8]) -> std::io::Result { + // Trim the read to the end of the buffer in case it exceeds the PE in memory + let pe_len: isize = (self.pe_range.end.u64() - self.pe_range.start.u64()) + .try_into() + .unwrap(); + let read_size: usize = std::cmp::min(buf.len(), (pe_len - self.offset).try_into().unwrap()); + + // Calculate the Gva to perform the read at + let read_addr = Gva::new(self.pe_range.start.u64() + self.offset as u64); + + // Do the read + match self.dump.virt_read(read_addr, &mut buf[0..read_size]) { + Ok(read_bytes) => { + let read_bytes_signed: isize = read_bytes.try_into().unwrap(); + self.offset += read_bytes_signed; + Ok(read_bytes) + } + Err(err) => Err(std::io::Error::new(ErrorKind::Other, err)), + } + } +} + +impl<'a> Seek for &mut DumpPE<'a> { + fn seek(&mut self, pos: std::io::SeekFrom) -> std::io::Result { + let new_offset: isize = match pos { + std::io::SeekFrom::Current(x) => { + let cur: i64 = self.offset.try_into().unwrap(); + (cur + x).try_into().unwrap() + } + std::io::SeekFrom::End(x) => { + let end: i64 = self.pe_range.end.u64().try_into().unwrap(); + (end + x).try_into().unwrap() + } + std::io::SeekFrom::Start(x) => x.try_into().unwrap(), + }; + + self.offset = new_offset.try_into().unwrap(); + + Ok(self.offset.try_into().unwrap()) + } +} + +pub(crate) fn get_module_list_from_kernel_crash( + crash: &Path, + binaries: bool, + include_user: bool, +) -> anyhow::Result> { + // Map the crashdump + let reader = + kdmp_parser::MappedFileReader::new(crash).context("failed to map kernel crashdump")?; + let dump = kdmp_parser::KernelDumpParser::with_reader(reader) + .context("failed to parse kernel crashdump")?; + + let mut manifest = Vec::new(); + + let module_iter = dump.kernel_modules().chain( + // If we're including user modules, chain it in + if include_user { + Some(dump.user_modules()) + } else { + None + } + .into_iter() + .flatten(), + ); + + for (module_range, module) in module_iter { + let mut module_reader = DumpPE { + dump: &dump, + offset: 0, + pe_range: module_range, + }; + + // If we're getting binaries too, record that now + if binaries { + // Get the base file name of the module + let mut bin_name = module; + if let Some(stem) = Path::new(module).file_name().and_then(OsStr::to_str) { + bin_name = stem; + } + + match pe::parse_pe(&mut module_reader) { + Ok((_, _, pe_header, image_size, _)) => { + let timestamp = pe_header.timestamp; + manifest.push(format!("{},{:x}{:x},2", bin_name, timestamp, image_size)); + } + Err(err) => { + eprintln!("Failed to get PE for module '{}': {}", module, err) + } + } + + (&mut module_reader).seek(std::io::SeekFrom::Start(0))?; + } + + match get_pdb_from_reader(&mut module_reader) { + Ok(manifest_entry) => manifest.push(manifest_entry), + Err(err) => eprintln!("Failed to get PDB for module '{}': {}", module, err), + } + } + + Ok(manifest) +} diff --git a/src/main.rs b/src/main.rs index 1ae7f7c..69d8d32 100644 --- a/src/main.rs +++ b/src/main.rs @@ -14,6 +14,7 @@ use anyhow::Context; use clap::{Parser, Subcommand, ValueEnum}; use crashdump::get_module_list_from_crash; use indicatif::{MultiProgress, ProgressStyle}; +use kernel_crashdump::get_module_list_from_kernel_crash; use symsrv::{SymSrvList, SymSrvSpec}; use std::io::SeekFrom; @@ -31,6 +32,7 @@ use tokio::{ use symsrv::{nonblocking::SymSrv, DownloadError, DownloadStatus, SymFileInfo}; mod crashdump; +mod kernel_crashdump; mod pe; #[allow(dead_code)] mod symsrv; @@ -494,6 +496,20 @@ enum Command { #[arg(short, long)] binaries: bool, }, + /// Generate a manifest file for a kernel crashdump, optionally including binaries in the manifest + /// Does not support small/kernel-minidumps + KernelCrashdump { + /// 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, + /// Include usermode dlls if present + #[arg(short, long)] + include_user: bool, + }, } #[derive(Subcommand, Clone, Debug)] @@ -740,6 +756,27 @@ async fn run() -> anyhow::Result<()> { .context("Failed to write to output manifest file")?; } } + Command::KernelCrashdump { + crashdump_path, + manifest_path, + binaries, + include_user, + } => { + let manifest_data = + get_module_list_from_kernel_crash(&crashdump_path, binaries, include_user) + .context("Failed to generate manifest for kernel 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(()) diff --git a/src/pe.rs b/src/pe.rs index 3c5ab63..b17e6eb 100644 --- a/src/pe.rs +++ b/src/pe.rs @@ -159,7 +159,7 @@ pub const IMAGE_DEBUG_TYPE_CODEVIEW: u32 = 2; /// Read a structure from a file stream, directly interpreting the raw bytes /// of the file as T. -pub fn read_struct(fd: &mut (impl Read + Seek)) -> io::Result { +pub fn read_struct(mut fd: impl Read + Seek) -> io::Result { let mut ret: T = T::new_zeroed(); fd.read_exact(ret.as_bytes_mut())?; From 79731e49f0039717b215571a64f4fe77e816fed4 Mon Sep 17 00:00:00 2001 From: dwattttt Date: Sun, 17 Nov 2024 20:14:48 +1100 Subject: [PATCH 5/5] Made manifest output path optional --- src/main.rs | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/main.rs b/src/main.rs index 69d8d32..169d902 100644 --- a/src/main.rs +++ b/src/main.rs @@ -501,8 +501,8 @@ enum Command { KernelCrashdump { /// 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, @@ -758,10 +758,12 @@ async fn run() -> anyhow::Result<()> { } Command::KernelCrashdump { crashdump_path, - manifest_path, + manifest, binaries, include_user, } => { + let manifest_path = manifest.unwrap_or(PathBuf::from("manifest")); + let manifest_data = get_module_list_from_kernel_crash(&crashdump_path, binaries, include_user) .context("Failed to generate manifest for kernel crashdump")?;