kdmp_parser/
parse.rs

1// Axel '0vercl0k' Souchet - February 25 2024
2//! This has all the parsing logic for parsing kernel crash-dumps.
3use core::slice;
4use std::cell::RefCell;
5use std::collections::HashMap;
6use std::fmt::{self, Debug};
7use std::fs::File;
8use std::mem::MaybeUninit;
9use std::ops::Range;
10use std::path::Path;
11use std::{io, mem};
12
13use crate::bits::Bits;
14use crate::error::{Error, Result};
15use crate::gxa::{Gpa, Gva};
16use crate::map::{MappedFileReader, Reader};
17use crate::pxe::Pfn;
18use crate::structs::{
19    BmpHeader64, Context, DUMP_HEADER64_EXPECTED_SIGNATURE, DUMP_HEADER64_EXPECTED_VALID_DUMP,
20    DumpType, ExceptionRecord64, FullRdmpHeader64, Header64, KdDebuggerData64, KernelRdmpHeader64,
21    PageKind, PfnRange, PhysmemDesc, PhysmemMap, PhysmemRun, Pod,
22};
23use crate::virt;
24use crate::virt_utils::{
25    ModuleMap, try_extract_kernel_modules, try_extract_user_modules, try_find_prcb,
26};
27
28fn gpa_from_bitmap(bitmap_idx: u64, bit_idx: usize) -> Option<Gpa> {
29    let pfn = Pfn::new(
30        bitmap_idx
31            .checked_mul(8)?
32            .checked_add(bit_idx.try_into().ok()?)?,
33    );
34
35    Some(pfn.gpa())
36}
37
38fn gpa_from_pfn_range(pfn_range: &PfnRange, page_idx: u64) -> Option<Gpa> {
39    let offset = page_idx.checked_mul(PageKind::Normal.size())?;
40
41    Some(Pfn::new(pfn_range.page_file_number).gpa_with_offset(offset))
42}
43
44/// Read a `T` from the cursor.
45fn read_struct<T: Pod>(reader: &mut impl Reader) -> Result<T> {
46    let mut s: MaybeUninit<T> = MaybeUninit::uninit();
47    let size_of_s = size_of_val(&s);
48    let slice_over_s = unsafe { slice::from_raw_parts_mut(s.as_mut_ptr().cast::<u8>(), size_of_s) };
49    reader.read_exact(slice_over_s)?;
50
51    Ok(unsafe { s.assume_init() })
52}
53
54/// A kernel dump parser that gives access to the physical memory space stored
55/// in the dump. It also offers virtual to physical memory translation as well
56/// as a virtual read facility.
57pub struct KernelDumpParser {
58    /// Which type of dump is it?
59    dump_type: DumpType,
60    /// Context header.
61    context: Box<Context>,
62    /// The dump headers.
63    headers: Box<Header64>,
64    /// This maps a physical address to a file offset. Seeking there gives the
65    /// page content.
66    pub(crate) physmem: PhysmemMap,
67    /// The [`Reader`] object that allows us to seek / read the dump file which
68    /// could be memory mapped, read from a file, etc.
69    reader: RefCell<Box<dyn Reader>>,
70    /// The driver modules loaded when the crash-dump was taken. Extracted from
71    /// the nt!PsLoadedModuleList.
72    kernel_modules: ModuleMap,
73    /// The user modules / DLLs loaded when the crash-dump was taken. Extract
74    /// from the current PEB.Ldr.InLoadOrderModuleList.
75    user_modules: ModuleMap,
76}
77
78impl Debug for KernelDumpParser {
79    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
80        f.debug_struct("KernelDumpParser")
81            .field("dump_type", &self.dump_type)
82            .finish_non_exhaustive()
83    }
84}
85
86impl KernelDumpParser {
87    /// Create an instance from a [`Reader`] & parse the file.
88    pub fn with_reader(mut reader: impl Reader + 'static) -> Result<Self> {
89        // Parse the dump header and check if things look right.
90        let headers = Box::new(read_struct::<Header64>(&mut reader)?);
91        if headers.signature != DUMP_HEADER64_EXPECTED_SIGNATURE {
92            return Err(Error::InvalidSignature(headers.signature));
93        }
94
95        if headers.valid_dump != DUMP_HEADER64_EXPECTED_VALID_DUMP {
96            return Err(Error::InvalidValidDump(headers.valid_dump));
97        }
98
99        // Grab the dump type and make sure it is one we support.
100        let dump_type = DumpType::try_from(headers.dump_type)?;
101
102        // Let's figure out how to get physical memory out of this dump now.
103        let physmem = Self::build_physmem(dump_type, &headers, &mut reader)?;
104
105        // Read the context record.
106        let context = Box::new(read_struct(&mut io::Cursor::new(
107            headers.context_record_buffer.as_slice(),
108        ))?);
109
110        let reader: RefCell<Box<dyn Reader>> = RefCell::new(Box::new(reader));
111        let mut parser = Self {
112            dump_type,
113            context,
114            headers,
115            physmem,
116            reader,
117            kernel_modules: HashMap::default(),
118            user_modules: HashMap::default(),
119        };
120
121        // Extract the kernel modules if we can. If it fails because of a memory
122        // translation error we'll keep going, otherwise we'll error out.
123        if let Some(kernel_modules) = try_extract_kernel_modules(&parser)? {
124            parser.kernel_modules.extend(kernel_modules);
125        }
126
127        // Now let's try to find out user-modules. For that we need the
128        // `KDDEBUGGER_DATA_BLOCK` structure to know where a bunch of things are.
129        // If we can't read the block, we'll have to stop the adventure here as we won't
130        // be able to read the things we need to keep going.
131        let virt_reader = virt::Reader::new(&parser);
132        let Some(kd_debugger_data_block) = virt_reader
133            .try_read_struct::<KdDebuggerData64>(parser.headers().kd_debugger_data_block.into())?
134        else {
135            return Ok(parser);
136        };
137        let kd_debugger_data_block = Box::new(kd_debugger_data_block);
138
139        // We need to figure out which PRCB is the one that crashed.
140        let Some(prcb_addr) = try_find_prcb(&parser, &kd_debugger_data_block)? else {
141            return Ok(parser);
142        };
143
144        // Finally, we're ready to extract the user modules!
145        let Some(user_modules) =
146            try_extract_user_modules(&virt_reader, &kd_debugger_data_block, prcb_addr)?
147        else {
148            return Ok(parser);
149        };
150
151        parser.user_modules.extend(user_modules);
152
153        Ok(parser)
154    }
155
156    /// Create an instance from a file path; depending on the file size, it'll
157    /// either memory maps it or open it as a regular file.
158    pub fn new(dump_path: impl AsRef<Path>) -> Result<Self> {
159        const FOUR_GIGS: u64 = 1_024 * 1_024 * 1_024 * 4;
160        // We'll assume that if you are opening a dump file larger than 4gb, you don't
161        // want it memory mapped.
162        let size = dump_path.as_ref().metadata()?.len();
163
164        if let 0..=FOUR_GIGS = size {
165            let mapped_file = MappedFileReader::new(dump_path.as_ref())?;
166
167            Self::with_reader(mapped_file)
168        } else {
169            let file = File::open(dump_path)?;
170
171            Self::with_reader(file)
172        }
173    }
174
175    /// Physical memory map that maps page aligned [`Gpa`] to `offset` where the
176    /// content of the page can be found. The offset is relevant with the
177    /// associated `reader`.
178    pub fn physmem(&self) -> impl ExactSizeIterator<Item = (Gpa, u64)> + '_ {
179        self.physmem.iter().map(|(&k, &v)| (k, v))
180    }
181
182    /// Kernel modules loaded when the dump was taken.
183    pub fn kernel_modules(&self) -> impl ExactSizeIterator<Item = (&Range<Gva>, &str)> + '_ {
184        self.kernel_modules.iter().map(|(k, v)| (k, v.as_str()))
185    }
186
187    /// User modules loaded when the dump was taken.
188    pub fn user_modules(&self) -> impl ExactSizeIterator<Item = (&Range<Gva>, &str)> + '_ {
189        self.user_modules.iter().map(|(k, v)| (k, v.as_str()))
190    }
191
192    /// What kind of dump is it?
193    pub fn dump_type(&self) -> DumpType {
194        self.dump_type
195    }
196
197    /// Get the dump headers.
198    pub fn headers(&self) -> &Header64 {
199        &self.headers
200    }
201
202    /// Get the exception record.
203    pub fn exception_record(&self) -> &ExceptionRecord64 {
204        &self.headers.exception
205    }
206
207    /// Get the context record.
208    pub fn context_record(&self) -> &Context {
209        &self.context
210    }
211
212    /// Seek to `pos`.
213    pub(crate) fn seek(&self, pos: io::SeekFrom) -> Result<u64> {
214        Ok(self.reader.borrow_mut().seek(pos)?)
215    }
216
217    /// Read however many bytes in `buf` and returns the amount of bytes read.
218    pub(crate) fn read_exact(&self, buf: &mut [u8]) -> Result<()> {
219        Ok(self.reader.borrow_mut().read_exact(buf)?)
220    }
221
222    /// Build the physical memory map for a [`DumpType::Full`] dump.
223    ///
224    /// Here is how runs works. Every `runs` document a number of consecutive
225    /// physical pages starting at a `PFN`. This means that you can have
226    /// "holes" in the physical address space and you don't need to write any
227    /// data for them. Here is a small example:
228    ///   - `Run[0]`: `BasePage = 1_337`, `PageCount = 2`
229    ///   - `Run[1]`: `BasePage = 1_400`, `PageCount = 1`
230    ///
231    /// In the above, there is a "hole" between the two runs. It has `2+1`
232    /// memory pages at: `Pfn(1_337+0)`, `Pfn(1_337+1)` and `Pfn(1_400+0)`
233    /// (but nothing at `Pfn(1_339)`).
234    ///
235    /// In terms of the content of those physical memory pages, they are packed
236    /// and stored one after another. If the first page of the first run is
237    /// at file offset `0x2_000`, then the first page of the second run is at
238    /// file offset `0x2_000+(2*0x1_000)`.
239    fn full_physmem(headers: &Header64, reader: &mut impl Reader) -> Result<PhysmemMap> {
240        let mut page_offset = reader.stream_position()?;
241        let mut run_cursor = io::Cursor::new(headers.physical_memory_block_buffer);
242        let physmem_desc = read_struct::<PhysmemDesc>(&mut run_cursor)?;
243        let mut physmem = PhysmemMap::new();
244
245        for run_idx in 0..physmem_desc.number_of_runs {
246            let run = read_struct::<PhysmemRun>(&mut run_cursor)?;
247            for page_idx in 0..run.page_count {
248                // Calculate the physical address.
249                let phys_addr = run
250                    .phys_addr(page_idx)
251                    .ok_or(Error::PhysAddrOverflow(run_idx, page_idx))?;
252
253                // We now know where this page lives at, insert it into the physmem map.
254                if physmem.insert(phys_addr, page_offset).is_some() {
255                    return Err(Error::DuplicateGpa(phys_addr));
256                }
257
258                // Move the page offset along.
259                page_offset = page_offset
260                    .checked_add(PageKind::Normal.size())
261                    .ok_or(Error::PageOffsetOverflow(run_idx, page_idx))?;
262            }
263        }
264
265        Ok(physmem)
266    }
267
268    /// Build the physical memory map for a [`DumpType::Bmp`] dump.
269    fn bmp_physmem(reader: &mut impl Reader) -> Result<PhysmemMap> {
270        let bmp_header = read_struct::<BmpHeader64>(reader)?;
271        if !bmp_header.looks_good() {
272            return Err(Error::InvalidData("bmp header doesn't look right"));
273        }
274
275        let remaining_bits = bmp_header.pages % 8;
276        let bitmap_size = bmp_header.pages.next_multiple_of(8) / 8;
277        let mut page_offset = bmp_header.first_page;
278        let mut physmem = PhysmemMap::new();
279
280        // Walk the bitmap byte per byte..
281        for bitmap_idx in 0..bitmap_size {
282            let mut byte = [0u8];
283            reader.read_exact(&mut byte)?;
284            // ..if this is the last byte, and we have a few more bits to read..
285            let last_byte = bitmap_idx == bitmap_size - 1;
286            if last_byte && remaining_bits != 0 {
287                // ..let's mask out the ones we don't care about.
288                let mask = (1u8 << remaining_bits).wrapping_sub(1);
289                byte[0] &= mask;
290            }
291
292            let byte = byte[0];
293            // Walk every bits.
294            for bit_idx in 0..8 {
295                // If it's not set, go to the next.
296                if byte.bit(bit_idx) == 0 {
297                    continue;
298                }
299
300                // Calculate where the page is.
301                let pa =
302                    gpa_from_bitmap(bitmap_idx, bit_idx).ok_or(Error::Overflow("pfn in bitmap"))?;
303
304                let insert = physmem.insert(pa, page_offset);
305                debug_assert!(insert.is_none());
306                page_offset = page_offset
307                    .checked_add(PageKind::Normal.size())
308                    .ok_or(Error::BitmapPageOffsetOverflow(bitmap_idx, bit_idx))?;
309            }
310        }
311
312        Ok(physmem)
313    }
314
315    /// Build the physical memory map for [`DumpType::KernelMemory`] /
316    /// [`DumpType::KernelAndUserMemory`] and [`DumpType::CompleteMemory`] dump.
317    fn kernel_physmem(dump_type: DumpType, reader: &mut impl Reader) -> Result<PhysmemMap> {
318        use DumpType as D;
319        let mut page_count = 0u64;
320        let (mut page_offset, metadata_size, total_number_of_pages) = match dump_type {
321            D::KernelMemory | D::KernelAndUserMemory => {
322                let kernel_hdr = read_struct::<KernelRdmpHeader64>(reader)?;
323                if !kernel_hdr.hdr.looks_good() {
324                    return Err(Error::InvalidData("RdmpHeader64 doesn't look right"));
325                }
326
327                (
328                    kernel_hdr.hdr.first_page_offset,
329                    kernel_hdr.hdr.metadata_size,
330                    0,
331                )
332            }
333            D::CompleteMemory => {
334                let full_hdr = read_struct::<FullRdmpHeader64>(reader)?;
335                if !full_hdr.hdr.looks_good() {
336                    return Err(Error::InvalidData("FullRdmpHeader64 doesn't look right"));
337                }
338
339                (
340                    full_hdr.hdr.first_page_offset,
341                    full_hdr.hdr.metadata_size,
342                    full_hdr.total_number_of_pages,
343                )
344            }
345            _ => unreachable!(),
346        };
347
348        if page_offset == 0 || metadata_size == 0 {
349            return Err(Error::InvalidData("no first page or metadata size"));
350        }
351
352        let pfn_range_size = mem::size_of::<PfnRange>();
353        if (metadata_size % pfn_range_size as u64) != 0 {
354            return Err(Error::InvalidData("metadata size is not a multiple of 8"));
355        }
356
357        let number_pfns = metadata_size / pfn_range_size as u64;
358        let mut physmem = PhysmemMap::new();
359
360        for _ in 0..number_pfns {
361            if dump_type == D::CompleteMemory {
362                // `CompleteMemoryDump` type seems to be bound by the `total_number_of_pages`
363                // field, *not* by `metadata_size`.
364                if page_count == total_number_of_pages {
365                    break;
366                }
367
368                if page_count > total_number_of_pages {
369                    return Err(Error::InvalidData("page_count > total_number_of_pages"));
370                }
371            }
372
373            let pfn_range = read_struct::<PfnRange>(reader)?;
374            if pfn_range.page_file_number == 0 {
375                break;
376            }
377
378            for page_idx in 0..pfn_range.number_of_pages {
379                let gpa = gpa_from_pfn_range(&pfn_range, page_idx)
380                    .ok_or(Error::Overflow("w/ pfn_range"))?;
381                let insert = physmem.insert(gpa, page_offset);
382                debug_assert!(insert.is_none());
383                page_offset = page_offset
384                    .checked_add(PageKind::Normal.size())
385                    .ok_or(Error::Overflow("w/ page_offset"))?;
386            }
387
388            page_count = page_count
389                .checked_add(pfn_range.number_of_pages)
390                .ok_or(Error::Overflow("w/ page_count"))?;
391        }
392
393        Ok(physmem)
394    }
395
396    fn build_physmem(
397        dump_type: DumpType,
398        headers: &Header64,
399        reader: &mut impl Reader,
400    ) -> Result<PhysmemMap> {
401        use DumpType as D;
402        match dump_type {
403            D::Full => Self::full_physmem(headers, reader),
404            D::Bmp | D::LiveKernelMemory => Self::bmp_physmem(reader),
405            D::KernelMemory | D::KernelAndUserMemory | D::CompleteMemory => {
406                Self::kernel_physmem(dump_type, reader)
407            }
408        }
409    }
410}