├── .cargo
└── config.toml
├── .github
└── workflows
│ └── snapshot.yml
├── .gitignore
├── Cargo.toml
├── LICENSE
├── README.md
├── pics
└── snapshot.gif
├── rustfmt.toml
└── src
├── lib.rs
└── state.rs
/.cargo/config.toml:
--------------------------------------------------------------------------------
1 | [build]
2 | rustflags = ["-C", "target-feature=+crt-static"]
--------------------------------------------------------------------------------
/.github/workflows/snapshot.yml:
--------------------------------------------------------------------------------
1 | name: Builds
2 |
3 | on: [push, pull_request]
4 |
5 | jobs:
6 | fmt:
7 | runs-on: windows-latest
8 | name: fmt
9 | steps:
10 | - name: Checkout
11 | uses: actions/checkout@v4
12 |
13 | - name: Set up rust
14 | run: rustup default stable
15 |
16 | - name: cargo fmt
17 | run: cargo fmt --check
18 |
19 | clippy:
20 | name: clippy
21 | runs-on: windows-latest
22 | steps:
23 | - name: Checkout
24 | uses: actions/checkout@v4
25 |
26 | - name: Set up rust
27 | run: rustup default stable
28 |
29 | - name: cargo clippy
30 | env:
31 | RUSTFLAGS: "-Dwarnings"
32 | run: cargo clippy
33 |
34 | build:
35 | runs-on: windows-latest
36 | name: build & test
37 | steps:
38 | - name: Checkout
39 | uses: actions/checkout@v4
40 |
41 | - name: Set up rust
42 | run: rustup default stable
43 |
44 | - name: cargo test
45 | run: cargo test
46 |
47 | - name: cargo build
48 | run: cargo build --release
49 |
50 | - name: Upload artifacts
51 | uses: actions/upload-artifact@v4
52 | with:
53 | name: snapshot
54 | path: |
55 | target/release/snapshot.dll
56 | target/release/snapshot.pdb
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | /*
2 | !/.cargo
3 | !/.github
4 | !/pics
5 | !/src
6 | !/.gitignore
7 | !/Cargo.lock
8 | !/Cargo.toml
9 | !/LICENSE
10 | !/README.md
11 |
--------------------------------------------------------------------------------
/Cargo.toml:
--------------------------------------------------------------------------------
1 | [package]
2 | name = "snapshot"
3 | version = "0.2.4"
4 | edition = "2024"
5 | authors = ["Axel '0vercl0k' Souchet"]
6 |
7 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
8 | [lib]
9 | crate-type = ["cdylib"]
10 |
11 | [dependencies]
12 | anyhow = { version = "1.0" }
13 | chrono = "0.4.42"
14 | clap = { version = "4.5.0", features = ["derive"] }
15 | dbgeng = { version = "0.4", features = ["serde"] }
16 | serde = { version = "1.0.228", features = ["derive"] }
17 | serde_json = { version = "1.0.145"}
18 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2024 Axel Souchet
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 |
snapshot
3 |
4 | A Rust WinDbg extension that takes a snapshot of a running VM.
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 | `snapshot` is a WinDbg extension written in Rust that dumps both the state of a CPU (GPRs, relevant MSRs, FPU state, segments, etc.) and the physical memory of a running VM (via a crash-dump). This snapshot is meant to be used by snapshot-based fuzzers and more particularly by [wtf](https://github.com/0vercl0k/wtf).
15 |
16 | This code base is also meant to show case how to write a WinDbg extension in Rust 🦀.
17 |
18 | ## Building
19 | You can build the extension with the below:
20 | ```text
21 | c:\>git clone https://github.com/0vercl0k/snapshot.git
22 | c:\>cd snapshot
23 | c:\snapshot>cargo build --release
24 | ```
25 |
26 | If you would rather grab a pre-built extension, grab one on the [releases](https://github.com/0vercl0k/snapshot/releases) page.
27 |
28 | ## Grabbing a snapshot
29 | Once you have the extension downloaded / compiled, you can load it in WinDbg with the below:
30 | ```text
31 | kd> .load \path\to\snapshot\target\release\snapshot.dll
32 |
33 | kd> !snapshot -h
34 | [snapshot] Usage: snapshot [OPTIONS] [STATE_PATH]
35 |
36 | Arguments:
37 | [STATE_PATH] The path to save the snapshot to
38 |
39 | Options:
40 | -k, --kind The kind of snapshot to take [default: full] [possible values: active-kernel, full]
41 | -h, --help Print help
42 | ```
43 |
44 | Generate a full-kernel snapshot in the `c:\foo` directory with the below:
45 | ```text
46 | kd> !snapshot c:\foo
47 | [snapshot] Dumping the CPU state into c:\foo\state.19041.1.amd64fre.vb_release.191206-1406.20240205_173527\regs.json..
48 | [snapshot] Dumping the memory state into c:\foo\state.19041.1.amd64fre.vb_release.191206-1406.20240205_173527\mem.dmp..
49 | Creating c:\\foo\\state.19041.1.amd64fre.vb_release.191206-1406.20240205_173527\\mem.dmp - Full memory range dump
50 | 0% written.
51 | 5% written. 1 min 12 sec remaining.
52 | 10% written. 1 min 4 sec remaining.
53 | [...]
54 | 90% written. 6 sec remaining.
55 | 95% written. 3 sec remaining.
56 | Wrote 4.0 GB in 1 min 11 sec.
57 | The average transfer rate was 57.7 MB/s.
58 | Dump successfully written
59 | [snapshot] Done!
60 | ```
61 |
62 | There is also `!snapshot_active_kernel` if you would prefer to grab an active kernel crash-dump.
63 |
--------------------------------------------------------------------------------
/pics/snapshot.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/0vercl0k/snapshot/400e96150aee3ad4248a162def91956b26464da8/pics/snapshot.gif
--------------------------------------------------------------------------------
/rustfmt.toml:
--------------------------------------------------------------------------------
1 | reorder_modules = true
2 | use_field_init_shorthand = true
3 |
4 | unstable_features = true
5 | indent_style = "Block"
6 | reorder_imports = true
7 | imports_granularity = "Module"
8 | normalize_comments = true
9 | normalize_doc_attributes = true
10 | overflow_delimited_expr = true
11 | reorder_impl_items = true
12 | group_imports = "StdExternalCrate"
13 | wrap_comments = true
--------------------------------------------------------------------------------
/src/lib.rs:
--------------------------------------------------------------------------------
1 | // Axel '0vercl0k' Souchet - January 15 2024
2 | // Special cheers to @erynian for the inspiration 🙏
3 | mod state;
4 |
5 | use std::collections::HashMap;
6 | use std::fs::{self, File};
7 | use std::path::PathBuf;
8 | use std::{env, mem};
9 |
10 | use anyhow::{Result, bail};
11 | use chrono::Local;
12 | use clap::{Parser, ValueEnum};
13 | use dbgeng::client::DebugClient;
14 | use dbgeng::dlogln;
15 | use dbgeng::windows::Win32::Foundation::{E_ABORT, S_OK};
16 | use dbgeng::windows::Win32::System::Diagnostics::Debug::Extensions::{
17 | DEBUG_CLASS_KERNEL, DEBUG_KERNEL_CONNECTION, DEBUG_KERNEL_EXDI_DRIVER, DEBUG_KERNEL_LOCAL,
18 | };
19 | use dbgeng::windows::Win32::System::SystemInformation::IMAGE_FILE_MACHINE_AMD64;
20 | use dbgeng::windows::core::{HRESULT, IUnknown, Interface, PCSTR};
21 | use serde_json::Value;
22 | use state::{Float80, GlobalSeg, State, Zmm};
23 |
24 | mod msr {
25 | pub const TSC: u32 = 0x0000_0010;
26 | pub const APIC_BASE: u32 = 0x0000_001b;
27 | pub const SYSENTER_CS: u32 = 0x0000_0174;
28 | pub const SYSENTER_ESP: u32 = 0x0000_0175;
29 | pub const SYSENTER_EIP: u32 = 0x0000_0176;
30 | pub const PAT: u32 = 0x0000_0277;
31 | pub const EFER: u32 = 0xc000_0080;
32 | pub const STAR: u32 = 0xc000_0081;
33 | pub const LSTAR: u32 = 0xc000_0082;
34 | pub const CSTAR: u32 = 0xc000_0083;
35 | pub const SFMASK: u32 = 0xc000_0084;
36 | pub const FS_BASE: u32 = 0xc000_0100;
37 | pub const GS_BASE: u32 = 0xc000_0101;
38 | pub const KERNEL_GS_BASE: u32 = 0xc000_0102;
39 | pub const TSC_AUX: u32 = 0xc000_0103;
40 | }
41 |
42 | /// Check if an address lives in user-mode.
43 | /// https://learn.microsoft.com/en-us/windows-hardware/drivers/gettingstarted/virtual-address-spaces
44 | fn is_usermode_addr(addr: u64) -> bool {
45 | addr <= 0x7FFF_FFFFFFFF
46 | }
47 |
48 | /// This function converts WinDbg's fptw register encoded value into what the
49 | /// CPU expects fptw to be.
50 | ///
51 | /// # Longer explanation
52 | /// After a bit of reverse-engineering and dynamic analysis it would appear
53 | /// that WinDbg's @fptw value is actually encoded the way `fxsave` saves it
54 | /// as. You can read about `fxsave` here: https://www.felixcloutier.com/x86/fxsave.
55 | ///
56 | /// In theory, @fptw is 16-bit integer and 2-bit patterns describe the
57 | /// state of each of the 8 available FPU stack slot. This explains why the
58 | /// register is 16-bit long as 2-bit per slot and there are 8 of them.
59 | ///
60 | /// If you write assembly code to push values onto the FPU stack and dump the
61 | /// value of @fptw, you'll see that it doesn't have the value you would
62 | /// expect. Here is MASM VS x64 assembly to dump those values for the
63 | /// curious readers:
64 | ///
65 | /// ```
66 | /// _TEXT SEGMENT
67 | /// PUBLIC _fld
68 | /// PUBLIC _fstenv
69 | ///
70 | /// _fld PROC
71 | /// fld qword ptr [rcx]
72 | /// ret
73 | /// _fld ENDP
74 | ///
75 | /// _fstenv PROC
76 | /// fstenv qword ptr [rcx]
77 | /// ret
78 | /// _fstenv ENDP
79 | /// _TEXT ENDS
80 | /// END
81 | /// ```
82 | ///
83 | /// And the C code to reads the value:
84 | ///
85 | /// ```
86 | /// #include
87 | /// #include
88 | /// #include
89 | ///
90 | /// extern "C" void _fld(const uint64_t*);
91 | /// extern "C" void _fstenv(void*);
92 | ///
93 | /// #pragma pack(1)
94 | /// struct Fnstenv_t {
95 | /// uint16_t fpcw;
96 | /// uint16_t reserved0;
97 | /// uint16_t fpsw;
98 | /// uint16_t reserved1;
99 | /// uint16_t fptw;
100 | /// uint16_t reserved2;
101 | /// uint32_t fpip;
102 | /// uint16_t fpop_selector;
103 | /// uint16_t fpop;
104 | /// uint32_t fpdp;
105 | /// uint16_t fpds;
106 | /// uint16_t reserved3;
107 | /// };
108 | ///
109 | /// static_assert(sizeof(Fnstenv_t) == 28, "");
110 | ///
111 | /// int main() {
112 | ///
113 | /// Fnstenv_t f = {};
114 | /// for (uint64_t Idx = 0; Idx < 8; Idx++) {
115 | /// _fstenv(&f);
116 | /// printf("real fptw: %x\n", f.fptw);
117 | /// __debugbreak();
118 | /// const uint64_t v = 1337 + Idx;
119 | /// _fld(&v);
120 | ///
121 | /// }
122 | /// return 0;
123 | /// }
124 | /// ```
125 | ///
126 | /// Below is a table I compiled that shows you the real @fptw register value
127 | /// and what WinDbg gives you.
128 | /// ```
129 | /// +----------------+----------------+--------------------------------+
130 | /// | State of stack | WinDbg's @fptw | Real fptw dumped with `fstenv` |
131 | /// +----------------+----------------+--------------------------------+
132 | /// | empty stack | 0b00000000 + 0b11111111_11111111 |
133 | /// +----------------+----------------+--------------------------------+
134 | /// | 1 push | 0b10000000 + 0b00111111_11111111 |
135 | /// +----------------+----------------+--------------------------------+
136 | /// | 2 push | 0b11000000 + 0b00001111_11111111 |
137 | /// +----------------+----------------+--------------------------------+
138 | /// | 3 push | 0b11100000 + 0b00000011_11111111 |
139 | /// +----------------+----------------+--------------------------------+
140 | /// | 4 push | 0b11110000 + 0b00000000_11111111 |
141 | /// +----------------+----------------+--------------------------------+
142 | /// | 5 push | 0b11111000 + 0b00000000_00111111 |
143 | /// +----------------+----------------+--------------------------------+
144 | /// | 6 push | 0b11111100 + 0b00000000_00001111 |
145 | /// +----------------+----------------+--------------------------------+
146 | /// | 7 push | 0b11111110 + 0b00000000_00000011 |
147 | /// +----------------+----------------+--------------------------------+
148 | /// | 9 push | 0b11111111 + 0b00000000_00000000 |
149 | /// +----------------+----------------+--------------------------------+
150 | /// ```
151 | fn fptw(windbg_fptw: u64) -> u64 {
152 | let mut out = 0;
153 | for bit_idx in 0..8 {
154 | let bits = (windbg_fptw >> bit_idx) & 0b1;
155 | out |= if bits == 1 { 0b00 } else { 0b11 } << (bit_idx * 2);
156 | }
157 |
158 | out
159 | }
160 |
161 | /// Generate a directory name where we'll store the CPU state / memory dump.
162 | fn gen_state_folder_name(dbg: &DebugClient) -> Result {
163 | let addr = dbg.get_address_by_name("nt!NtBuildLabEx").unwrap_or(0);
164 | let build_name = dbg.read_cstring(addr).unwrap_or("UnknownBuild".to_string());
165 | let now = Local::now();
166 |
167 | Ok(format!("state.{build_name}.{}", now.format("%Y%m%d_%H%M")))
168 | }
169 |
170 | /// Dump the register state.
171 | fn state(dbg: &DebugClient) -> Result> {
172 | const CR4_OSXSAVE: u64 = 1 << 18;
173 | let mut regs = dbg.regs64_dict(&[
174 | "rax", "rbx", "rcx", "rdx", "rsi", "rdi", "rip", "rsp", "rbp", "r8", "r9", "r10", "r11",
175 | "r12", "r13", "r14", "r15", "fpcw", "fpsw", "cr0", "cr2", "cr3", "cr4", "cr8", "dr0",
176 | "dr1", "dr2", "dr3", "dr6", "dr7", "mxcsr",
177 | ])?;
178 |
179 | let xcr0 = if (regs.get("cr4").unwrap() & CR4_OSXSAVE) != 0 {
180 | dbg.reg64("xcr0").unwrap()
181 | } else {
182 | 0
183 | };
184 |
185 | assert!(
186 | regs.insert("xcr0", xcr0).is_none(),
187 | "xcr0 shouldn't be in regs"
188 | );
189 |
190 | assert!(
191 | regs.insert("rflags", dbg.reg64("efl")?).is_none(),
192 | "efl shouldn't be in regs"
193 | );
194 |
195 | let fptw = fptw(dbg.reg64("fptw")?);
196 | assert!(
197 | regs.insert("fptw", fptw).is_none(),
198 | "fptw shouldn't be in regs"
199 | );
200 |
201 | // While looking into `dbgeng.dll` I found an array that gives the registers /
202 | // indices for each of the supported platforms. The relevant one for 64-bit
203 | // Intel is called `struct REGDEF near * g_Amd64Defs`. As far as I can tell,
204 | // there's no mechanism available to dump @fpip / @fpipsel / @fpdp / @fpdpsel
205 | // registers :-/... so we'll set it to zero.
206 | assert!(
207 | regs.insert("fpop", 0).is_none(),
208 | "fpop shouldn't be in regs"
209 | );
210 |
211 | assert!(
212 | regs.insert("fpip", 0).is_none(),
213 | "fpip shouldn't be in regs"
214 | );
215 |
216 | assert!(
217 | // Default value from linux kernel (stolen from `bdump.js`'s code):
218 | // https://elixir.bootlin.com/linux/latest/source/arch/x86/kernel/fpu/init.c#L117
219 | regs.insert("mxcsr_mask", 0xffbf).is_none(),
220 | "mxcsr_mask shouldn't be in regs"
221 | );
222 |
223 | let fpst = dbg
224 | .regs128(&["st0", "st1", "st2", "st3", "st4", "st5", "st6", "st7"])?
225 | .into_iter()
226 | .map(Float80::from)
227 | .collect();
228 |
229 | let mut msrs = HashMap::from([
230 | ("tsc", msr::TSC),
231 | ("apic_base", msr::APIC_BASE),
232 | ("sysenter_cs", msr::SYSENTER_CS),
233 | ("sysenter_esp", msr::SYSENTER_ESP),
234 | ("sysenter_eip", msr::SYSENTER_EIP),
235 | ("pat", msr::PAT),
236 | ("efer", msr::EFER),
237 | ("star", msr::STAR),
238 | ("lstar", msr::LSTAR),
239 | ("cstar", msr::CSTAR),
240 | ("sfmask", msr::SFMASK),
241 | ("kernel_gs_base", msr::KERNEL_GS_BASE),
242 | ("tsc_aux", msr::TSC_AUX),
243 | ])
244 | .into_iter()
245 | .map(|(name, msr)| Ok((name, dbg.msr(msr)?)))
246 | .collect::>>()?;
247 |
248 | let gdt = GlobalSeg::from(dbg.regs64(&["gdtr", "gdtl"])?);
249 | let idt = GlobalSeg::from(dbg.regs64(&["idtr", "idtl"])?);
250 |
251 | let selectors = dbg.regs64_dict(&["es", "cs", "ss", "ds", "tr", "gs", "fs", "ldtr"])?;
252 | let mut segs = selectors
253 | .into_iter()
254 | .map(|(name, selector)| Ok((name, dbg.gdt_entry(gdt.base, gdt.limit, selector)?)))
255 | .collect::>>()?;
256 |
257 | // Fix up @gs / @fs base with the appropriate MSRs.
258 | let mut gs_base = dbg.msr(msr::GS_BASE)?;
259 | segs.get_mut("gs").unwrap().base = gs_base;
260 | segs.get_mut("fs").unwrap().base = dbg.msr(msr::FS_BASE)?;
261 |
262 | let rip = *regs.get("rip").unwrap();
263 | let gs = segs.get_mut("gs").unwrap();
264 | let mode_matches = is_usermode_addr(rip) == is_usermode_addr(gs.base);
265 | if !mode_matches {
266 | let kernel_gs_base = msrs.get_mut("kernel_gs_base").unwrap();
267 | mem::swap(&mut gs_base, kernel_gs_base);
268 | segs.get_mut("gs").unwrap().base = gs_base;
269 | }
270 |
271 | // Fix up @cr8 if it isn't zero and the debugger is currently stopped in
272 | // user-mode.
273 | let cr8 = regs.get_mut("cr8").unwrap();
274 | if is_usermode_addr(rip) && *cr8 != 0 {
275 | *cr8 = 0;
276 | }
277 |
278 | let gsegs = HashMap::from([("gdtr", gdt), ("idtr", idt)]);
279 | let sse = dbg
280 | .regs128_dict(&[
281 | "xmm0", "xmm1", "xmm2", "xmm3", "xmm4", "xmm5", "xmm6", "xmm7", "xmm8", "xmm9",
282 | "xmm10", "xmm11", "xmm12", "xmm13", "xmm14", "xmm15",
283 | ])?
284 | .into_iter()
285 | .map(|(k, v)| (k, Zmm::from(v)))
286 | .collect();
287 |
288 | let generated_by = format!("{} {}", env!("CARGO_PKG_NAME"), env!("CARGO_PKG_VERSION"));
289 | Ok(State {
290 | regs,
291 | segs,
292 | gsegs,
293 | fpst,
294 | msrs,
295 | sse,
296 | generated_by,
297 | })
298 | }
299 |
300 | #[derive(Clone, Default, ValueEnum)]
301 | enum SnapshotKind {
302 | #[default]
303 | ActiveKernel,
304 | Full,
305 | }
306 |
307 | #[derive(Parser)]
308 | #[clap(no_binary_name = true)]
309 | struct SnapshotArgs {
310 | /// The kind of snapshot to take.
311 | #[clap(default_value = "full", long, short)]
312 | kind: SnapshotKind,
313 | /// The path to save the snapshot to.
314 | state_path: Option,
315 | }
316 |
317 | /// This is where the meat is - this function generates the `state` folder made
318 | /// of the CPU register as well as the memory dump.
319 | fn snapshot_inner(dbg: &DebugClient, args: SnapshotArgs) -> Result<()> {
320 | // Let's make sure this is a live kernel, not a dump, etc..
321 | let is_live_kernel = matches!(
322 | dbg.debuggee_type()?,
323 | (
324 | DEBUG_CLASS_KERNEL,
325 | DEBUG_KERNEL_EXDI_DRIVER | DEBUG_KERNEL_LOCAL | DEBUG_KERNEL_CONNECTION
326 | )
327 | );
328 |
329 | if !is_live_kernel {
330 | bail!("expected a live kernel debugging session");
331 | }
332 |
333 | // ... and the target is an x64 architecture..
334 | let is_intel64 = matches!(dbg.processor_type()?, IMAGE_FILE_MACHINE_AMD64);
335 | if !is_intel64 {
336 | bail!("expected an Intel 64-bit guest target");
337 | }
338 |
339 | // ... and the amount of processors of the target.
340 | if dbg.processor_number()? > 1 {
341 | bail!("expected to have only one core to dump the state of");
342 | }
343 |
344 | // Build the state path.
345 | let state_path = {
346 | // Grab the path the user gave or the temp directory.
347 | let base_path = args.state_path.unwrap_or(env::temp_dir());
348 | if base_path.exists() {
349 | // If the user specified a path that exists, then generate a directory name for
350 | // them.
351 | base_path.join(gen_state_folder_name(dbg)?)
352 | } else {
353 | // Otherwise, we use it as is
354 | base_path
355 | }
356 | };
357 |
358 | fs::create_dir(&state_path)?;
359 |
360 | // Build the `regs.json` / `mem.dmp` path.
361 | let regs_path = state_path.join("regs.json");
362 | let mem_path = state_path.join("mem.dmp");
363 |
364 | if regs_path.exists() {
365 | bail!("{:?} already exists", regs_path);
366 | }
367 |
368 | if mem_path.exists() {
369 | bail!("{:?} already exists", mem_path);
370 | }
371 |
372 | // All right, let's get to work now. First, grab the CPU state.
373 | let state = state(dbg)?;
374 |
375 | // Turn the state into a JSON value.
376 | let mut json = serde_json::to_value(state)?;
377 |
378 | // Walk a JSON `Value` and turn every `Number` into a `String`.
379 | // This is useful to turn every integer in your JSON document into an hex
380 | // encoded string.
381 | fn fix_number_nodes(v: &mut serde_json::Value) {
382 | match v {
383 | Value::Number(n) => *v = Value::String(format!("{:#x}", n.as_u64().unwrap())),
384 | Value::Array(a) => a.iter_mut().for_each(fix_number_nodes),
385 | Value::Object(o) => o.values_mut().for_each(fix_number_nodes),
386 | _ => {}
387 | }
388 | }
389 |
390 | fix_number_nodes(&mut json);
391 |
392 | // Dump the CPU register into a `regs.json` file.
393 | dlogln!(dbg, "Dumping the CPU state into {}..", regs_path.display())?;
394 |
395 | let regs_file = File::create(regs_path)?;
396 | serde_json::to_writer_pretty(regs_file, &json)?;
397 |
398 | dlogln!(
399 | dbg,
400 | "Dumping the memory state into {}..",
401 | mem_path.display()
402 | )?;
403 |
404 | // Generate the `mem.dmp`.
405 | dbg.exec(format!(
406 | ".dump /{} {:?}",
407 | match args.kind {
408 | // Create a dump with active kernel and user mode memory.
409 | SnapshotKind::ActiveKernel => "ka",
410 | // A Complete Memory Dump is the largest kernel-mode dump file. This file includes all
411 | // of the physical memory that is used by Windows. A complete memory dump does not, by
412 | // default, include physical memory that is used by the platform firmware.
413 | SnapshotKind::Full => "f",
414 | },
415 | mem_path
416 | ))?;
417 |
418 | dlogln!(dbg, "Done!")?;
419 |
420 | Ok(())
421 | }
422 |
423 | pub type RawIUnknown = *mut std::ffi::c_void;
424 |
425 | /// This is a wrapper function made to be able to display the error in case the
426 | /// inner function fails.
427 | fn wrap(
428 | raw_client: RawIUnknown,
429 | args: PCSTR,
430 | callback: impl FnOnce(&DebugClient, P) -> Result<()>,
431 | ) -> HRESULT {
432 | // We do not own the `raw_client` interface so we want to created a borrow. If
433 | // we don't, the object will get Release()'d when it gets dropped which will
434 | // lead to a use-after-free.
435 | let Some(client) = (unsafe { IUnknown::from_raw_borrowed(&raw_client) }) else {
436 | return E_ABORT;
437 | };
438 |
439 | let Ok(dbg) = DebugClient::new(client) else {
440 | return E_ABORT;
441 | };
442 |
443 | let Ok(args) = (unsafe { args.to_string() }) else {
444 | return E_ABORT;
445 | };
446 |
447 | // Parse the arguments using `clap`. Currently splitting arguments by
448 | // whitespaces.
449 | let args = match P::try_parse_from(args.split_whitespace()) {
450 | Ok(a) => a,
451 | Err(e) => {
452 | let _ = dlogln!(dbg, "{e}");
453 | return E_ABORT;
454 | }
455 | };
456 |
457 | match callback(&dbg, args) {
458 | Err(e) => {
459 | let _ = dlogln!(dbg, "Ran into an error: {e:?}");
460 |
461 | E_ABORT
462 | }
463 | Ok(_) => S_OK,
464 | }
465 | }
466 |
467 | #[unsafe(no_mangle)]
468 | extern "C" fn snapshot(raw_client: RawIUnknown, args: PCSTR) -> HRESULT {
469 | wrap(raw_client, args, snapshot_inner)
470 | }
471 |
472 | /// The DebugExtensionInitialize callback function is called by the engine after
473 | /// loading a DbgEng extension DLL. https://learn.microsoft.com/en-us/windows-hardware/drivers/ddi/dbgeng/nc-dbgeng-pdebug_extension_initialize
474 | #[unsafe(no_mangle)]
475 | extern "C" fn DebugExtensionInitialize(_version: *mut u32, _flags: *mut u32) -> HRESULT {
476 | S_OK
477 | }
478 |
479 | #[unsafe(no_mangle)]
480 | extern "C" fn DebugExtensionUninitialize() {}
481 |
482 | #[cfg(test)]
483 | mod tests {
484 | use std::collections::BTreeMap;
485 |
486 | #[test]
487 | fn fptw() {
488 | let expected = BTreeMap::from([
489 | (0b00000000, 0b1111111111111111),
490 | (0b10000000, 0b0011111111111111),
491 | (0b11000000, 0b0000111111111111),
492 | (0b11100000, 0b0000001111111111),
493 | (0b11110000, 0b0000000011111111),
494 | (0b11111000, 0b0000000000111111),
495 | (0b11111100, 0b0000000000001111),
496 | (0b11111110, 0b0000000000000011),
497 | (0b11111111, 0b0000000000000000),
498 | ]);
499 |
500 | for (windbg, expected_fptw) in expected {
501 | let fptw = super::fptw(windbg);
502 | assert_eq!(
503 | fptw, expected_fptw,
504 | "fptw({}) returned {} instead of expect {}",
505 | windbg, fptw, expected_fptw
506 | );
507 | }
508 | }
509 | }
510 |
--------------------------------------------------------------------------------
/src/state.rs:
--------------------------------------------------------------------------------
1 | // Axel '0vercl0k' Souchet - January 21 2024
2 | use std::collections::HashMap;
3 |
4 | use dbgeng::client::Seg;
5 | use serde::Serialize;
6 |
7 | #[derive(Default, Debug, Serialize)]
8 | pub struct Zmm([u64; 8]);
9 |
10 | impl From for Zmm {
11 | fn from(value: u128) -> Self {
12 | let q0 = u64::try_from(value & 0xff_ff_ff_ff_ff_ff_ff_ff).unwrap();
13 | let q1 = u64::try_from(value >> 64).unwrap();
14 |
15 | Zmm([q0, q1, 0, 0, 0, 0, 0, 0])
16 | }
17 | }
18 |
19 | #[derive(Default, Debug, Serialize)]
20 | pub struct GlobalSeg {
21 | pub base: u64,
22 | pub limit: u16,
23 | }
24 |
25 | impl From> for GlobalSeg {
26 | fn from(value: Vec) -> Self {
27 | assert!(
28 | value.len() == 2,
29 | "the vector should have the base and the limit"
30 | );
31 |
32 | Self {
33 | base: value[0],
34 | limit: value[1] as u16,
35 | }
36 | }
37 | }
38 |
39 | #[derive(Default, Debug, Serialize)]
40 | pub struct Float80 {
41 | fraction: u64,
42 | exp: u16,
43 | }
44 |
45 | impl From for Float80 {
46 | fn from(value: u128) -> Self {
47 | let fraction = (value & 0xff_ff_ff_ff_ff_ff_ff_ff).try_into().unwrap();
48 | let exp = ((value >> 64) & 0xff_ff).try_into().unwrap();
49 |
50 | Self { fraction, exp }
51 | }
52 | }
53 |
54 | #[derive(Default, Debug, Serialize)]
55 | pub struct State<'a> {
56 | #[serde(flatten)]
57 | pub regs: HashMap<&'a str, u64>,
58 |
59 | #[serde(flatten)]
60 | pub segs: HashMap<&'a str, Seg>,
61 |
62 | #[serde(flatten)]
63 | pub gsegs: HashMap<&'a str, GlobalSeg>,
64 |
65 | #[serde(flatten)]
66 | pub sse: HashMap<&'a str, Zmm>,
67 |
68 | pub fpst: Vec,
69 |
70 | #[serde(flatten)]
71 | pub msrs: HashMap<&'a str, u64>,
72 |
73 | pub generated_by: String,
74 | }
75 |
--------------------------------------------------------------------------------