Tracing a WMI Query to the Kernel: How Get-PhysicalDisk Reads Serial Number and UID

About the author. I’m Colin Xiao — an independent reverse engineer, mostly working solo on contract jobs. I dig into compiled code of every kind, including but not limited to: Android and iOS apps, x86/x64/ARM native binaries, packers and obfuscators, DRM and license checks, network protocols and APIs, crypto and signature algorithms, malware, game binaries, and embedded/IoT firmware. I also handle large-scale data collection and vulnerability research. If you have any project in mind, I’d be glad to take it on — whatever it is, as long as it’s within my wheelhouse. Just reach out. You can reach me on X at @colinxiao_lab or on Telegram at https://t.me/ColinXiao666.

This research is published for educational and defensive purposes. The target sample is anonymized throughout.

Earlier I showed how to pull a disk’s serial number and UID with:

Get-PhysicalDisk | Select-Object FriendlyName, SerialNumber, UniqueId

or equivalently:

Get-CimInstance -ClassName MSFT_PhysicalDisk -Namespace root/Microsoft/Windows/Storage | Select-Object FriendlyName, SerialNumber, UniqueId

But I never explained how that query actually gets into the kernel to read the serial number and UID. This closes that gap — and more usefully, it lays out a repeatable method for tracing any query like this back to the driver and structures that answer it.

Pinpointing the kernel memory

Run the query in PowerShell in a VM:

PowerShell disk query

Copy the serial number into Cheat Engine and search for a wchar_t*:

Searching for the wchar_t string

To confirm which address the query actually reads, tweak candidates one at a time and re-run the query until the result changes with the edit. (Spoiler: the address lives in the MiVaNonPagedPool range.)

Located address in MiVaNonPagedPool

Editing the first character to 5 changes the query output accordingly, so this kernel address is the one our query reads.

Hardware breakpoint

Set a one-byte read breakpoint on that address in WinDbg:

ba r1 0xFFFFD402ED8E49D0 "u @Rip L5; g"

ba is the hardware breakpoint command, r = read (also w write, e execute), 1 = one byte, u @Rip L5 disassembles five instructions at the current RIP, and g continues. One caveat: close Cheat Engine first (or stop it from reading that address), otherwise you’ll just keep hitting CE’s own driver reads:

Breakpoint hit by Cheat Engine’s reads

If that happens, bl to list breakpoints, then clear the offending one. Then re-set the breakpoint without g and re-run the query:

ba r1 0xFFFFD402ED8E49D0 "u @Rip L5"

Run the query again in PowerShell and the breakpoint lands inside spaceport.sys (this VM is Windows 11 24H2), in RtlStringCbCopyW — the routine that copies the string from argument 3 to argument 1. Use kv to dump the stack, or just print the stack on the breakpoint by swapping u @Rip for k:

Call stack into spaceport.sys

That gives us the full call flow from user mode into the kernel and down to the address that holds the serial number.

From the driver to the structure

Drop the VM’s spaceport.sys into IDA and go to spaceport!SpIoctlGetDriveInfo+0xa3b. RtlStringCbCopyW itself isn’t interesting — it’s just a string copy:

SpIoctlGetDriveInfo+0xa3b

The thing to chase is the source of v13 (+10 in the listing is +0x50). You can set breakpoints on suspected origins of v13 to see which actually executes; the answer is that v13 comes from the return value of SpFindDriveByTempId:

DriveByTempId = (struct SP_DRIVE *)SpFindDriveByTempId((struct _GUID *)&v123[20]);
v13 = DriveByTempId;

v13 = SpFindDriveByTempId(…)

You could chase the source of v123[20] next, but first step into SpFindDriveByTempId:

SpFindDriveByTempId

The pseudocode does use the parameter, plus a global variable for structure resolution. This is where a key lesson kicks in: WinDbg is better than IDA for resolving globals.

IDA’s symbol resolution gives you the global, but you’d still have to resolve the structure by hand:

IDA symbol for the global

WinDbg’s symbol resolution is cleaner. Walking the code with the resolved global:

WinDbg symbol resolution

rdx = 0xffffd402`ed2ac1f8
add  rdx, 0xF0
rdx = 0xffffd402`ed2ac2e8
mov  r9, [rdx]
r9  = 0xffffd402`ed71d868
lea  rax, [r9-0x248]
rax = 0xffffd402`ed71d620

That’s v13. Add back the original 0x50 offset:

v13 + 0x50

And you’ve reached the exact kernel address the query reads.

The general method

The same approach works for any hardware and any query: locate the answer string in kernel memory, hardware-breakpoint the read, walk the stack to the owning driver, then resolve the structure in IDA (and lean on WinDbg for global-variable resolution). One practical note: some hardware serial numbers only refresh after the WMI service restarts. Also, since my VM has a single disk, SpFindDriveByTempId didn’t need to loop — with multiple physical disks it iterates over drives, which is worth a look if you’re on real hardware.

Colin Xiao — I take on contract reverse-engineering work, and writeups like this are how I show what I can do. If you need help with anything in this space — including but not limited to the areas I cover above — I’m available. No project is too big or too small — if it’s reverse engineering and I can do it, I’ll take it. DM me on X (@colinxiao_lab) or Telegram (@ColinXiao666).

Get in touch