├── .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 | --------------------------------------------------------------------------------