An author never does more damage to his readers than when he hides a difficulty. (Evariste Galois)
You can download this tutorial and all necessary files in an archive file from here. The PDF version is also available here.
File password:
exists4all.github.io
Table of Contents
Introduction
Abstract
After a long time being inactive I decided to finish my break due to some personal reasons. And one other reason was that it was getting closer to release date of Windows 24H2. After the release I noticed old spoofers cause problems and lead to BSOD so it’s the best time to write a part 2 of the static spoofer tutorial. We are going to use amazing old work of other people to make our life simpler. We are basically going to do some reverse engineering and modify old spoofers to work on a new build of Windows and explain the philosophy of it. You can download most of the used resources and important files in this tutorial on my website. I also really appreciate any suggestion, and pointing out any errors since no one is prone to making mistakes. You can do that using this form.
Required tools
As usual, we are going to use the most used tools. I list them below.
- Windows 11 24H2
- Visial Studio
- Compatible SDK + WDK with Windows 11 24H2
- Windbg Preview
- Hyper-V with host OS
- IDA
- WinObj tool
- An Intel CPU with Intel Virtualization Technology
Required knowledge
Before starting, take a look at [Disk Serial Spoofer on Windows part 1].
Purpose
In this tutorial we are mainly trying to analyze WIndows internals and learn about Hard disks more. These methods by no means are enough to bypass modern kernel anti cheats. There are lots of ways to track modifications and get DiskSerials later. For example Vanguard anti-cheat will start before most drivers and will track all the HWIDs before even spoofing them with these outdated methods. However, this is a good opportunity to learn the basics and develop more advanced spoofers by merging new methods and tools with it.
Windows Internal and Hard disks
Device Unique Identifiers (DUIDs) for Storage Devices
Techniques to identify storage devices become inadequate as file system architecture becomes more complex, the number of operating system components multiplies, and initiators access storage targets through increasingly diverse hardware and software paths.
For example, the Plug and Play (PnP) manager generates an instance identifier (ID) for each device in the computer. Each instance ID corresponds to a single device node in the device tree and uniquely identifies a device, if the device remains in the same location. Instance IDs persist when a computer is restarted, but they do not remain the same if you move the device to a different bus or a different computer. As a result, instance IDs are inadequate for applications in storage area networks (SANs) and for some newer system components, such as the Windows Vista Diagnostic Service, that operate in environments with distributed storage. When a hard disk drive predicts a SMART failure, it generates an event for the diagnostic service. This event must contain an identifier that uniquely identifies the failing hard disk on all computers that the disk could be in and on all buses that it could be attached to. Instance IDs and any of the other device identification strings are inadequate for this purpose.
Some applications and system services, such as the Microsoft Cluster Service MSCS and the Partition Manager, use the device layout signature STORAGE_DEVICE_LAYOUT_SIGNATURE to uniquely identify a storage device in a cluster. But the device layout signature is inadequate for this purpose, under certain circumstances, and includes the following limitations:
1. The signature might change or be cleared.
2. The signature might be unavailable if the device is not spinning or has problems accessing the sectors where the signature resides.
3. The signature is unavailable if the disk is reserved by another cluster node. MSCS can read the drive layout of only disks that are associated with the node that MSCS is running on. Software that must access disks in different cluster nodes must use an alternative to the disk layout signature.
4. Drive layout signatures cannot help distinguish between a logical unit number (LUN) and its snapshot. Because a LUN and its snapshot have identical content, their drive layout signatures will be the same.
A serial number is sometimes a reliable technique of uniquely identifying a storage device that does not depend on the location of the device. The serial number is often available as a part of a device’s inquiry data. Initiators can query for the inquiry data with a IOCTL_STORAGE_QUERY_PROPERTY request, and the port driver reports the results of the query in a STORAGE_DEVICE_DESCRIPTOR structure. However, this technique does not help identify devices, such as tape drives, that do not report inquiry data.[2]
One can see now how serial numbers can be obtained by the last part of the referenced quote above.
Looking inside kernel architecture
As I mentioned before, Kernel-Mode Driver Architecture Design Guide is a very important resource to study. The natural question before spoofing serial numbers is how windows actually get them when the system starts, to answer this question we have to go through the mentioned guide of Microsoft. Almost all of the following paragraphs are direct quotes from Microsoft.
Windows manages its own internal with Objects, thus we have multiple kinds of objects that windows use, among them we can name Device objects,Symbolic links,Registry keys and more. Each object has a header (containing information about the object such as its name, type, and location), and a body (containing data in a format determined by each type of object).[3][4]
All the Internals of Windows are treated as Objects. Drivers are Object structures themselves that contain sub structures as well.
The I/O manager creates a driver object for each driver that has been installed and loaded. Driver objects are defined using DRIVER_OBJECT structures. When the I/O manager calls a driver’s DriverEntry routine, it supplies the address of the driver’s driver object. The driver object contains storage for entry points to many of a driver’s standard routines. The driver is responsible for filling in these entry points[5].
Kernel-mode objects are either named or unnamed and named objects are organized into a hierarchy. For example, \KernelObjects\LowMemoryCondition is the name of the standard event object that signals when the amount of free memory in the system is low. Drivers that create named objects do so in specific object directories[6].
Each driver can create multiple objects and contain predefined objects as well which usually obey hierarchy. So there can be multiple objects that are related to a single Driver. Also there could be a single Object which is related to multiple Drivers.
An object directory is a named object that is used solely to contain other named objects. For example, the \Device object directory contains the named device objects created by drivers. The following is a list of the top-level object directories that contain objects drivers might create or use:
- \Callbacks
- \Device
- \KernelObjects
- \DosDevices
Drivers can create new object directories by calling the ZwCreateDirectoryObject routine[7].
Multiple Drivers can create different objects in the same directory. Directories help to maintain object oriented kernels.
Object manager has to keep track of references, count them and maintain them, it also has to do the same for handles. Some anticheats for example can use the number of open handles to keep track of suspicious programs. The Object manager also destroys objects if they do not have any reference. To be able to see how the Object and Object directory work better you can use the WinObj tool, which we will do later to find hard disks[8 p13-p15].
As we pointed out before, Drivers treated as objects. The defined structure for a driver Object is as below:
typedef struct _DRIVER_OBJECT {
CSHORT Type;
CSHORT Size;
PDEVICE_OBJECT DeviceObject;
ULONG Flags;
PVOID DriverStart;
ULONG DriverSize;
PVOID DriverSection;
PDRIVER_EXTENSION DriverExtension;
UNICODE_STRING DriverName;
PUNICODE_STRING HardwareDatabase;
PFAST_IO_DISPATCH FastIoDispatch;
PDRIVER_INITIALIZE DriverInit;
PDRIVER_STARTIO DriverStartIo;
PDRIVER_UNLOAD DriverUnload;
PDRIVER_DISPATCH MajorFunction[IRP_MJ_MAXIMUM_FUNCTION + 1];
}
DRIVER_OBJECT, * PDRIVER_OBJECT;
It’s clear now each Driver has at least one Device Object. The operating system represents devices by device objects. One or more device objects are associated with each device. Device objects serve as the target of all operations on the device. Kernel-mode drivers must create at least one device object for each device, with the few exceptions[9].
There are three kinds of WDM device objects:
- Physical Device Object (PDO) – represents a device on a bus to a bus driver.
- Functional Device Object (FDO) – represents a device to a function driver.
- Filter Device Object (filter DO) – represents a device to a filter driver[10].
And finally the DEVICE_OBJECT struct is defined as below:
typedef struct _DEVICE_OBJECT {
CSHORT Type;
USHORT Size;
LONG ReferenceCount;
struct _DRIVER_OBJECT * DriverObject;
struct _DEVICE_OBJECT * NextDevice;
struct _DEVICE_OBJECT * AttachedDevice;
struct _IRP * CurrentIrp;
PIO_TIMER Timer;
ULONG Flags;
ULONG Characteristics;
__volatile PVPB Vpb;
PVOID DeviceExtension;
DEVICE_TYPE DeviceType;
CCHAR StackSize;
union {
LIST_ENTRY ListEntry;
WAIT_CONTEXT_BLOCK Wcb;
}
Queue;
ULONG AlignmentRequirement;
KDEVICE_QUEUE DeviceQueue;
KDPC Dpc;
ULONG ActiveThreadCount;
PSECURITY_DESCRIPTOR SecurityDescriptor;
KEVENT DeviceLock;
USHORT SectorSize;
USHORT Spare1;
struct _DEVOBJ_EXTENSION * DeviceObjectExtension;
PVOID Reserved;
}
DEVICE_OBJECT, * PDEVICE_OBJECT;
The windows start identifying hardwares at the boot time and keep tracking them in an object, called Device tree. The PnP manager maintains a device tree that keeps track of the devices in the system. The following figure shows the device tree for a sample system configuration. The device tree contains information about the devices present on the system. The PnP manager builds this tree when the machine boots, using information from drivers and other components, and updates the tree as devices are added or removed. Each node of the device tree is called a device node, or devnode. A devnode consists of the device objects for the device’s drivers, plus internal information maintained by the system. Therefore, there is a devnode for each device stack[11].
The PnP manager will find hard disks, and add them to the Device tree at boot time. It also keeps track of hardwares in case new hardware is added or unplugged.
Find hard disks objects and The responsible driver that gets disk serials
To be able to see where the hard disk objects are, we use the WinObJ tool from sysinternals[12]. Simply download the tool and run it as administrator.
Simply download the tool and run it as administrator. As we mentioned before, \Device is one of the important object directories. Simply looking inside that directory you can see different kinds of identified hardwares there. In the picture of Device Tree you can see Hard disks are related to the SCSI port. Thus by searching the \Device directory you will be able to see symbolic links starting with the name ScsiPort name and then numbered in order. And there are actually Objects called RaidPort in the directory. So our job is to find and modify these Objects property.
As for the second part, which we explained in part 1 already, we just use a simple C++ code that gets a hard disk serial. Putting a breakpoint in the main function and keep debugging it using pc and t command till we reach a syscall. Then using the SSD table we find the kernel function that is going to be called and put a breakpoint on it. In part one I explain that after this you just have to keep using pc and t command till you find the function, however to make our own life easier, this time we are going to use script this time.
Simply boot up your Hyper-V and connect Windbg preview to it. If you do not know how to set these things up, read here to setup Hyper-V and here to set up WDK before installing Windbg Preview and after that install Windbg Preview. After that download this C++ code, put __debugbreak() in the main and compile it. Then Move the compiled file into the VM and run it.
You can add the path of the source and PDB of the compiled file to the Windbg to be able to debug it by source. After running the file, the breakpoint will get hit, use !sym noisy and .reload command and then simply simply use pc and t command till you hit a syscall.
After hitting the syscall, we have to find which kernel function is going to be called using the SSD table. For more details about the SSD table read here. I am just going to explain it in the cookie cutter format. First look at the RAX register value, in my case it is RAX=0x7. Next you have to execute the following command:
dd nt!KiServiceTable+0xRAXValue*0x4 L1
So in my case it looks like that dd nt!KiServiceTable+0x7*0x4 L1, that will cause windbg return first value of table. In my case the result is fffff806`804d40bc 07b4bf06. Use the second value in the following command:
u KiServiceTable + (0xReturnedValue>>4)
So in my case it looks like that u KiServiceTable + (0x7b4bf06»4). After that you will see the instructions like below:
nt!NtDeviceIoControlFile:
fffff806`80c88c90 4883ec68 sub rsp,68h
fffff806`80c88c94 8b8424b8000000 mov eax,dword ptr [rsp+0B8h]
fffff806`80c88c9b c744245800000000 mov dword ptr [rsp+58h],0
fffff806`80c88ca3 c644245001 mov byte ptr [rsp+50h],1
fffff806`80c88ca8 89442448 mov dword ptr [rsp+48h],eax
fffff806`80c88cac 488b8424b0000000 mov rax,qword ptr [rsp+0B0h]
fffff806`80c88cb4 4889442440 mov qword ptr [rsp+40h],rax
fffff806`80c88cb9 8b8424a8000000 mov eax,dword ptr [rsp+0A8h]
Now we just put a breakpoint on this kernel function with this bp nt!NtDeviceIoControlFile. Then hit g command to continue execution. Keep in mind after each hit, we have to check execution context to be sure that this breakpoint that we landed on is related to our program with this command !process -1 0. After you confirm the landing breakpoint is for our program disable it. Then we need to use a script to execute t and pc commands automatically many times so we don’t have to do it manually. To do this we create a file named Script.js and paste the following code in it:
function invokeScript()
{
var ctl = host.namespace.Debugger.Utility.Control;
var output;
for(var i=1; i <= 20; i+=2)
{
output = ctl.ExecuteCommand("pc");
host.diagnostics.debugLog(" ", i, "\n");
output = ctl.ExecuteCommand("t");
host.diagnostics.debugLog(" ", i+1, "\n");
}
}
To learn more about scripting you can read here. No simply running the script with .scriptrun ScriptPath\textbackslash Script.js, Where ScriptPath should point to the absolute location of the script. After each execution of this script, we have to check stack call to see interesting functions. Then based on our experience we use t and pc commands manually to slowly observe what will happen. This script is just used to get rid of lot’s of obvious executions in the early stages. Keep in mind there might be BSOD happening a few times. So you just have to start all over again if that happens. If the script is stuck, pausing and unpausing the Hyper-V can help sometimes. After 4 times script execution and a few manual execution commands I arrived at the following calls:
storport!RaGetUnitStorageDeviceProperty+0x4d
storport!RaUnitStorageQueryPropertyIoctl+0x172
storport!RaUnitDeviceControlIrp+0x19d
storport!RaDriverDeviceControlIrp+0x8c
nt!IofCallDriver+0xbe
CLASSPNP!ClassDeviceControl+0x51a
disk!DiskDeviceControl+0xf4
CLASSPNP!ClassDeviceControlDispatch+0x91
CLASSPNP!ClassGlobalDispatch+0x3a
nt!IofCallDriver+0xbe
partmgr!PmIoctlQueryProperty+0x55
partmgr!PmFilterDeviceControl+0x15e
partmgr!PmGlobalDispatch+0x101
nt!IofCallDriver+0xbe
nt!IopSynchronousServiceTail+0x1c8
nt!IopXxxControlFile+0x940
nt!NtDeviceIoControlFile+0x5e
nt!KiSystemServiceCopyEnd+0x25
ntdll!NtDeviceIoControlFile+0x14
KERNELBASE!DeviceIoControl+0x73
KERNEL32!DeviceIoControlImplementation+0x75
C__DiskSerialAPI!getFirstHddSerialNumber+0x181
Which is the same result that we achieved in part 1. As a result we copy storport.sys with its pdb an usie IDA to analyze it. As a result we copy storport.sys with its pdb an usie IDA to analyze it. To download the PDB, we use symchk.exe which usually can be found in the following path C:\textbackslash Program Files (x86)\textbackslash Windows Kits\textbackslash 10\textbackslash Debuggers\textbackslash x64. So open CMD as administrator, go to the mentioned path with:
cd C:\Program Files (x86)\Windows Kits\10\Debuggers\x64
Then simply copy storport.sys in C:\textbackslash and execute the following command:
symchk.exe "C:\storport.sys" /s SRV*C:\*http://msdl.microsoft.com/download/symbols
After that you will get PDB in the same location, just copy it alongside the driver and use IDA to analyze the functions.
As we mentioned before, Windows keeps tracking everything as Objects. We also mentioned that the function storport!RaGetUnitStorageDeviceProperty extracts data from a big struct called _RAID_UNIT_EXTENSION. Now the natural question is what is this struct and where does it belong to? The obvious answer to this question is, this structure is related to the Hard disks and it’s values initialized by Windows at boot time. To be able to see this object which passes the _RAID_UNIT_EXTENSION to storport.sys we have to look at the call stack and choose a good function to analyze it. After some search you will find out nt!IofCallDriver is the best candidate. It’s worth mentioning that WMIC is no longer available in 2024H2. To get all hard disks data you can use Windows PowerShell with the Get-Disk command.
Now simply execute the compiled program again to hit the debug breakpoint. Then put a breakpoint on nt!NtDeviceIoControlFile with bp nt!NtDeviceIoControlFile and use g command to land on it. Then put a breakpoint on nt!IofCallDriver and disable the nt!NtDeviceIoControlFile break point simply using bl command to list all the breakpoints and clicking on disable. As we see in the call stack, the nt!IofCallDriver gets called 3 times. We want to land on the 3rd call. So just keep executing g command and tracking execution context with !process -1 0 till you land on the 3rd call. The nt!IofCallDriver definition is as below:
NTSTATUS IofCallDriver(PDEVICE_OBJECT DeviceObject,
__drv_aliasesMem PIRP Irp);
Thus by knowing the x64 ABI conventions of Windows, the value in RCX is the pointer to the passed Object. In my case that is RCX = 0xFFFF948DF615D050. By checking that address in memory you can see the Object.
The first two bytes with the value 0x3 show that we are in the correct location. Since this is the DEVICE_OBJECT and we should have CSHORT = 0x3. Now execute the following command to get offsets of DEVICE_OBJECT members for easier calculations:
dt _DEVICE_OBJECT
Thus to see _DEVOBJ_EXTENSION pointer we just have to check RCX+0x40 (0x40 is the offset of DeviceExtension) and check that location in memory. In my case that will be 0xFFFF948DF615D050 + 0x40. By checking this location in memory we see the following:
Pointers in x64 Operating systems are 8 bytes, and in Windows they are stored in reverse bytes. Thus in my case DeviceExtension = 0xFFFF948DF615D1A0. By checking this location, which looks like _RAID_UNIT_EXTENSION you will see the struct:
Keep in mind, sometimes use a single step command like p to update Windbg values if you feel results are not okay. Sometimes Windbg freezes. The last remaining things for this Chapter is to verify the above data are indeed _RAID_UNIT_EXTENSION and to find out why RaGetUnitStorageDeviceProperty uses _RAID_UNIT_EXTENSION which has been answered already here. The first part is also easy, just put a breakpoint on storport!RaGetUnitStorageDeviceProperty and continue execution till you land on it. After that check RCX which points to the _RAID_UNIT_EXTENSION, compare this address to the previous address that we obtained above, they are the same.
Reimplementing spoofer
n0Lin (@Alex3434) and btbd spoofer
Most of the static spoofers are based on btbd and Alex3434 works. While these two are changing Disk Serials, it’s still traceable. And we have to mention that these two cause BSOD on the 24H2 build of Windows 11. We are trying to explain step by step how to rebuild a spoofer based on these two which works fine on 24H2. There is also work by SamuelTulach, which is a variant of some tools and uses Alex3434 static spoofer base code.
Logic of implementation
Now we are ready to explain the logic of spoofer, we are going to access the Disk kernel objects by searching the Device directory. We know that the name associated Objects are started by Raidport and followed by a number. After accessing the Object, We should modify the Serial Number of its extension. Now, to be able to find desired functions to implement our spoofer we just get ntoskrnl.exe PDB using SymChk.exe and use IDA.
After that, since Drivers that are related to Hardware have a database in the registry(HardwareDatabase in _DRIVER_OBJECT struct), we have to update the registry by the modified structs. To do this we have to find an appropriate function in storport.sys, this is a trial and error game but in IDA we search Register keyword for function names we can see RaidUnitRegisterInterfaces is a good candidate to start with.
Updating SamuelTulach project
Clone his project with the following command:
git clone --recurse-submodules https://github.com/SamuelTulach/mutante.git
Configure the project to use C++20 and C17. To be able to see the kernel printing function in Windbg we have to take a few steps as well.
The component filter mask can be accessed in the registry key HKEY_LOCAL_MACHINE\textbackslash SYSTEM\textbackslash CurrentControlSet\textbackslash Control\textbackslash Session Manager\textbackslash Debug Print Filter. Using a registry editor, create or open this key. Under this key, create a value with the name of the desired component, in uppercase. Set it equal to the DWORD value that you wish to use as the component filter mask[13].
I created a DWORD called DEFAULT in the above path with the value 8. After that you have to restart the system. Please note that, after adding this registry key, you can’t use breakpoints in the normal way. For instance putting a breakpoint on some kernel functions might not work properly. So it is a good practice to export your key, delete it when you do not need it, and import it again when you need it for driver debugging. I exported my key in the project archive. In case you are too lazy to do it, you can just import it to your own registry key with the registry editor.
His source code for disk spoofing with my explanations are pretty much clear. However, two important parts need more explanation. The first important part is to use RAID_UNIT_EXTENSION in our code. This struct is huge and we only need a few parts of it in our code. To acces the strict simply execute below command in Windbg.
dt storport!_RAID_UNIT_EXTENSION -b
It is obvious that this struct might change in different builds of WIndows, and if that happens we might likely see a BSOD when using our spoofer. Thus we have to keep track of this struct. The things that we need are serial numbers in this struct. You simply find it in the output of Windbg and add it as a struct to your code. You will fill the parts that we do not need with normal bytes as char, if you need to add other parts in future to exported struct you need to calculate offset of places that you will fill with normal bytes. So the structure looks like this:
typedef struct _STOR_SCSI_IDENTITY
{
char Space[0x8]; // +0x008 SerialNumber : _STRING
STRING SerialNumber;
} STOR_SCSI_IDENTITY, * PSTOR_SCSI_IDENTITY;
typedef struct _RAID_UNIT_EXTENSION
{
union
{
struct
{
char Space[0x70]; // +0x070 Identity _STOR_IDENTITY
STOR_SCSI_IDENTITY Identity;
} _Identity;
};
} RAID_UNIT_EXTENSION, * PRAID_UNIT_EXTENSION;
As you can see in the old project of SamuelTulach, the offset is not the same. In the older builds of windows this offset is 0x68 but in the current build at the writing time it is 0x70. Modify it accordingly.
The second part is the way that we call RaidUnitRegisterInterfaces from storport.sys. As you might know if we have the address of a function and know how many arguments it takes, you can call that function by address. In the SamuelTulach project, he finds the driver address in memory and finds the function offset by signature scanning. So we just borrow his codes to do the things. The other way to do this is, rebase the driver in IDA to zero and calculate the offset there, and use that offset in your code. You just have to get the storport.sys base in memory. To understand how you can call a function by its address I am going to explain it a little bit more. To do this we simply use function pointers, but before doing it you have to be comfortable reading C/C++ Declarations. To understand them more please read here and here. Now consider below example from here:
typedef int func(void);
func* f = (func*)0xdeadbeef;
int i = f();
We simply use typedef for declaration of a function that takes no argument and returns an int. Then using an alias, define a pointer to the function with the same declaration with its address (0xdeadbeef). Please notice that we can also use using from C++11. Like below:
using FuncPtr = int (*)();
FuncPtr f = (FuncPtr)0xdeadbeef;
int i = f();
For example:
#include <iostream>
using FuncPtr = int( * )();
int a() {
return 1;
}
int b() {
return 2;
}
int main() {
std::cout << "The address of a function: " << (void * ) a << std::endl;
std::cout << "The address of b function: " << (void * ) b << std::endl;
unsigned long int Adr = (unsigned long int)(void * ) a;
std::cout << "The value of Adr variable:0x" << std::hex << Adr << std::endl;
FuncPtr g = a;
int i = g();
std::cout << i << endl;
g = b;
i = g();
std::cout << i << endl;
g = (int( * )())(Adr);
i = g();
std::cout << i << std::endl;
return 0;
}
Example output:
The address of a function: 0x562b7ca1b2d0
The address of b function: 0x562b7ca1b2e0
The value of Adr variable: 0x562b7ca1b2d0
1
2
1
Ofcourse the address of funtions wil can change at each execution, this is just an example. Thus to call the function one can use:
typedef __int64(__fastcall* RaidUnitRegisterInterfaces)(PRAID_UNIT_EXTENSION a1);
Or:
using RaidUnitRegisterInterfaces = __int64 (__fastcall*)(PRAID_UNIT_EXTENSION a1);
Now, you have to know for obvious reasons, Virtual Machines do not act like real physical hardware. As a result it’s better to always do serious testing on real physical hardware. Thus to test this spoofer, I tested it on physical hardware rather than virtual machines. Simply execute the driver with your preferred method, and then get the disk serials in Windows PowerShell with Get-Disk (WMIC is deprecated in 2024H2). After offset modification from 0x68 to 0x70 and execution of the driver you can see the disk serial spoofed on the latest build of WIndows at the time of writing.
Conclusion
I hope this tutorial can help you in any way. The author is grateful for the great work of all the resources in this tutorial including a,b,c. Keep in mind, my main purpose never was to recreate spoofer from scratch nor to bypass any Anti-Cheat but to help understand Windows internals and storages a little bit better. Though I am really interested in working on Anti-Cheats, at the moment I do not have enough time for it. But surely I will try to make some tutorials about them in future as well. After all, it always seems impossible until it’s done.
References
https://exists4all.github.io/posts/simple_disk_serial_spoofer_part_1/ ↩︎
https://learn.microsoft.com/en-us/windows-hardware/drivers/storage/device-unique-identifiers--duids--for-storage-devices ↩︎
https://learn.microsoft.com/en-us/windows-hardware/drivers/kernel/windows-kernel-mode-objectmanager ↩︎
https://learn.microsoft.com/en-us/windows-hardware/drivers/kernel/managing-kernel-objects ↩︎
https://learn.microsoft.com/en-us/windows-hardware/drivers/kernel/introduction-to-driverobjects ↩︎
https://learn.microsoft.com/en-us/windows-hardware/drivers/kernel/object-names ↩︎
https://learn.microsoft.com/en-us/windows-hardware/drivers/kernel/object-directories ↩︎
Windows Kernel Programming by Pavel Yosifovich ↩︎
https://learn.microsoft.com/en-us/windows-hardware/drivers/kernel/introduction-to-deviceobjects ↩︎
https://learn.microsoft.com/en-us/windows-hardware/drivers/kernel/types-of-wdm-device-objects ↩︎
https://learn.microsoft.com/en-us/windows-hardware/drivers/kernel/device-tree ↩︎
https://learn.microsoft.com/en-us/sysinternals/downloads/winobj ↩︎
https://learn.microsoft.com/en-us/windows-hardware/drivers/debugger/reading-and-filteringdebugging-messages ↩︎