Reverse Engineering, OS Internals, Exploit Development, and ...

Posted on :: Tags: ,

Introduction

Performance counter in windows (PCW) is a system-provided mechanism to monitor, measure specific aspects of a system or an application's performance. It is generally accessible via perfmon, and programmatically via pdh, or perflib, etc. Performance Counters in and of itself is a loaded topic to go about, in this post the implementation of PCW in kernel mode is explored, and about how its implemented.

Reversing Time

This post is based upon pcw.sys having the following PDB signature 5FD86107-F60A-DB7F-5886-0580EDE7DE61

Initial Look

PCW (Performance Counter for windows) in kernel mode is implemented inside pcw.sys driver, which first registers the PcwObjectType via PcwInitialize function called at DriverEntry

NTSTATUS DriverEntry(_DRIVER_OBJECT *DriverObject, PUNICODE_STRING RegistryPath)
{
  // ...
  DeviceObject = 0LL;
  RtlInitializeUnicodeString(&DeviceName, LR"(\Device\PcwDrv)");
  // ...
  DriverObject->FastIoDispatch = (PFAST_IO_DISPATCH)&PcwpFastIoDispatchTable;
  DriverObject->MajorFunction[0] = (PDRIVER_DISPATCH)PcwpDispatchCreate;
  Status = PCW_SILO_CONTEXT::RegisterForSiloNotifications(&DeviceName);

  // ...
  Status = WdmlibIoCreateDeviceSecure(DriverObject, v4, &DeviceName, v6, v11, v12, v13, v14, &DeviceObject);

  Status = PcwInitialize(DriverObject); // [1]
  if ( Status < 0 )
  {
    IoDeleteDevice(DeviceObject);
    // ...

    PCW_SILO_CONTEXT::UnregisterForSiloNotifications();

    // ...

    McGenEventUnregister_EtwUnregister();
    return v7;
  }
  DeviceObject->Flags &= ~DO_DEVICE_INITIALIZING;
  return 0;
}

stepping inside [1] we can observe a kernel extension being registered at [2], beyond [3] members of _OBJECT_TYPE_INITIALIZER structure to be passed at [4].

NTSTATUS PcwInitialize(struct _DRIVER_OBJECT *DriverObject)
{

  // ...
  extensionReg.FunctionCount = 5;
  Extension = 0LL;
  DestinationString = 0LL;
  memset(&ObjectTypeInitializer, 0, sizeof(ObjectTypeInitializer));
  *&extensionReg.ExtensionId = 0x10001; // Id and Version in the same write 
  extensionReg.FunctionTable = &PcwpCallbackTable;
  extensionReg.HostInterface = 0LL;
  extensionReg.DriverObject = DriverObject;

  // Register an extension
  Status = ExRegisterExtension(&Extension, 0x10000u, &extensionReg); // [2]

  if (NT_FAILED(Status)) {
	  // bail
  }
  // [3]
  ObjectTypeInitializer.ObjectTypeFlags |= UnnamedObjectsOnly | UseDefaultObject;
  ObjectTypeInitializer.OpenProcedure = PcwpOpenObject;
  ObjectTypeInitializer.Length = 0x78;
  ObjectTypeInitializer.DeleteProcedure = PcwpDeleteObject;
  ObjectTypeInitializer.InvalidAttributes = 0x132;
  ObjectTypeInitializer.CloseProcedure = PcwpCloseObjectHandle;
  ObjectTypeInitializer.GenericMapping = PcwObjectGenericMap;
  ObjectTypeInitializer.ValidAccessMask = 0xF0003;
  ObjectTypeInitializer.PoolType = PagedPool;
  ObjectTypeInitializer.DefaultPagedPoolCharge = 0x38;
  RtlInitUnicodeString(&DestinationString, L"PcwObject");
  Status = ObCreateObjectType(&DestinationString, &ObjectTypeInitializer, 0LL, &PcwObjectType); // [4]
  if ( Status >= 0 )
    return 0; // STATUS_SUCCESS
  ExUnregisterExtension(Extension);
  if ( (Microsoft_Windows_Diagnosis_PCWEnableBits & 1) == 0 )
    return Status;
  n3 = 4LL;
  // If we get here bail
}

Likewise the Extension is unregistered if the creation of PcwObjectType fails. Looking at !drvobj

 kd> !drvobj pcw f
Driver object (ffffb784ac16e970) is for:
 \Driver\pcw

Driver Extension List: (id , addr)

Device Object list:
ffffb784ac0e8570  

DriverEntry:   fffff8040b152010	pcw!GsDriverEntry
DriverStartIo: 00000000	
DriverUnload:  00000000	
AddDevice:     00000000	

Dispatch routines:
[00] IRP_MJ_CREATE                      fffff8040b1469e0	pcw!PcwpDispatchCreate
[01] IRP_MJ_CREATE_NAMED_PIPE           fffff80406947c10	nt!IopInvalidDeviceRequest
[02] IRP_MJ_CLOSE                       fffff80406947c10	nt!IopInvalidDeviceRequest
[03] IRP_MJ_READ                        fffff80406947c10	nt!IopInvalidDeviceRequest
[04] IRP_MJ_WRITE                       fffff80406947c10	nt!IopInvalidDeviceRequest
[05] IRP_MJ_QUERY_INFORMATION           fffff80406947c10	nt!IopInvalidDeviceRequest
[06] IRP_MJ_SET_INFORMATION             fffff80406947c10	nt!IopInvalidDeviceRequest
[07] IRP_MJ_QUERY_EA                    fffff80406947c10	nt!IopInvalidDeviceRequest
[08] IRP_MJ_SET_EA                      fffff80406947c10	nt!IopInvalidDeviceRequest
[09] IRP_MJ_FLUSH_BUFFERS               fffff80406947c10	nt!IopInvalidDeviceRequest
[0a] IRP_MJ_QUERY_VOLUME_INFORMATION    fffff80406947c10	nt!IopInvalidDeviceRequest
[0b] IRP_MJ_SET_VOLUME_INFORMATION      fffff80406947c10	nt!IopInvalidDeviceRequest
[0c] IRP_MJ_DIRECTORY_CONTROL           fffff80406947c10	nt!IopInvalidDeviceRequest
[0d] IRP_MJ_FILE_SYSTEM_CONTROL         fffff80406947c10	nt!IopInvalidDeviceRequest
[0e] IRP_MJ_DEVICE_CONTROL              fffff80406947c10	nt!IopInvalidDeviceRequest
[0f] IRP_MJ_INTERNAL_DEVICE_CONTROL     fffff80406947c10	nt!IopInvalidDeviceRequest
[10] IRP_MJ_SHUTDOWN                    fffff80406947c10	nt!IopInvalidDeviceRequest
[11] IRP_MJ_LOCK_CONTROL                fffff80406947c10	nt!IopInvalidDeviceRequest
[12] IRP_MJ_CLEANUP                     fffff80406947c10	nt!IopInvalidDeviceRequest
[13] IRP_MJ_CREATE_MAILSLOT             fffff80406947c10	nt!IopInvalidDeviceRequest
[14] IRP_MJ_QUERY_SECURITY              fffff80406947c10	nt!IopInvalidDeviceRequest
[15] IRP_MJ_SET_SECURITY                fffff80406947c10	nt!IopInvalidDeviceRequest
[16] IRP_MJ_POWER                       fffff80406947c10	nt!IopInvalidDeviceRequest
[17] IRP_MJ_SYSTEM_CONTROL              fffff80406947c10	nt!IopInvalidDeviceRequest
[18] IRP_MJ_DEVICE_CHANGE               fffff80406947c10	nt!IopInvalidDeviceRequest
[19] IRP_MJ_QUERY_QUOTA                 fffff80406947c10	nt!IopInvalidDeviceRequest
[1a] IRP_MJ_SET_QUOTA                   fffff80406947c10	nt!IopInvalidDeviceRequest
[1b] IRP_MJ_PNP                         fffff80406947c10	nt!IopInvalidDeviceRequest

Fast I/O routines:
FastIoDeviceControl                     fffff8040b146a10	pcw!PcwpFastIoDeviceControl


Device Object stacks:

!devstack ffffb784ac0e8570 :
  !DevObj           !DrvObj            !DevExt           ObjectName
> ffffb784ac0e8570  \Driver\pcw        00000000  PcwDrv

Processed 1 device objects.

and looking at IRP_MJ_CREATE routine pcw!PcwpDispatchCreate, the implementation is pretty much like "yo you suceeded"

NTSTATUS  PcwpDispatchCreate(_DEVICE_OBJECT *DeviceObject, _IRP *Irp)
{
  Irp->IoStatus.Status = 0; // basically return STATUS_SUCCESS
  Irp->IoStatus.Information = 0LL;
  IofCompleteRequest(Irp, 0);
  return 0LL;
}

also given that _OBJECT_HEADER::SecurityDescriptor is null as shown below.

0: kd> !object \Device\PcwDrv
Object: ffffc48fe95d6570  Type: (ffffc48fe67f2e80) Device
    ObjectHeader: ffffc48fe95d6540 (new version)
    HandleCount: 0  PointerCount: 2
    Directory Object: ffffa10e6885c840  Name: PcwDrv
0: kd> dx ((nt!_OBJECT_HEADER*)0xffffc48fe95d6540)->SecurityDescriptor
((nt!_OBJECT_HEADER*)0xffffc48fe95d6540)->SecurityDescriptor : 0x0 [Type: void *]

If the discretionary access control list (DACL) that belongs to an object's security descriptor is set to NULL, a null DACL is created. A null DACL grants full access to any user that requests it; normal security checking is not performed with respect to the object. A null DACL should not be confused with an empty DACL. An empty DACL is a properly allocated and initialized DACL that contains no access control entries (ACEs). An empty DACL grants no access to the object it is assigned to

Opening a handle to \Device\PcwDrv

we can easily grab a handle to the device via the code below

NTSTATUS
Open(HANDLE *h) {
  IO_STATUS_BLOCK IoStatus = {0};
  UNICODE_STRING Name = {0};
  OBJECT_ATTRIBUTES ObjectAttributes = {0};

  RtlInitUnicodeString(&Name, LR"(\Device\PcwDrv)");

  InitializeObjectAttributes(&ObjectAttributes, &Name, 0, 0, 0);

  return NtCreateFile(h, MAXIMUM_ALLOWED, &ObjectAttributes, &g_IoStatusBlock,
                      nullptr, 0, 0, 0, 0, nullptr, 0);
}

Dispatch Function

BOOLEAN PcwpFastIoDeviceControl(
        _FILE_OBJECT *FileObject,
        BOOLEAN Wait,
        void *InputBuffer,
        unsigned int InputBufferLength,
        void *OutputBuffer,
        SIZE_T OutputBufferLength,
        ULONG IoctlCode,
        _IO_STATUS_BLOCK *IoStatusBlock)
{

  // ... 

  memset(localInputBuffer, 0, sizeof(v17));
  v18 = 0LL;
  v10 = IoStatusBlock;
  IoStatusBlock->Information = 0LL;
  if ( (IoctlCode | 0x3FFC) == 0x227FFF )
  {
    idx = (IoctlCode >> 2) & 0xFFF;
    IoctlCode = idx;
    if ( idx < 0x10 ) // [5]
    {
      if ( *((_DWORD *)&unk_1C0003000 + 4 * idx) == InputBufferLength // [6] 
        && ((v14 = *((_DWORD *)&unk_1C0003000 + 4 * idx + 1), v14 == (_DWORD)OutputBufferLength) || v14 == 0xFFFFFFFF) )
      {
        if ( OutputBuffer )
          ProbeForWrite(OutputBuffer, OutputBufferLength, 1u);
        memmove(localInputBuffer, InputBuffer, InputBufferLength);
        Status = funcs_1C0009B43[2 * idx]((union PCW_IOCTL_INPUT *)localInputBuffer, OutputBuffer, (unsigned int *)&OutputBufferLength); // [7]
        v10->Status = Status;
        v10->Information = OutputBufferLength;
        if ( Status >= 0 || (Microsoft_Windows_Diagnosis_PCWEnableBits & 0x10) == 0 )
          return 1;
      }
      else
      {
	      // ...

      }
    }
    else
    {
	    // ...

    }
    v11 = idx;
    goto Done;
  }
  v10->Status = 0xC0000010;
  if ( (Microsoft_Windows_Diagnosis_PCWEnableBits & 0x10) == 0 )
    return 1;
  v11 = 0xFFFFFFFFLL;
  v12 = 0xC0000010LL;
Done:
  McTemplateU0qq_EtwWriteTransfer(FileObject, &PcwIoctlFail, v12, v11);
  return 1;
}

and looking at funcs_1C0009B43 which goes beyond having 0x10 functions

funcs_1C0009B43

deducing from [5] , [6] , and [7] to create a script to map-out required functions and deduce their IoControlCodes as its visible to be something along the lines of simple structure containing the following:

struct IoctlHandler {
  ULONG InputBufferLength; 
  ULONG OutputBufferLength; 
  NTSTATUS (*HandlerFunction)(PVOID, PVOID, PULONG); 
}; 

and can be mapped in ida using the idapython script

import idaapi
import idc
import z3

method_names = [
    "METHOD_BUFFERED",
    "METHOD_IN_DIRECT",
    "METHOD_OUT_DIRECT",
    "METHOD_NEITHER",
]

def guess_first_ioctl():
    s = z3.Solver()

    x = z3.BitVec("x", 32)
    c1 = (x | 0x3FFC) == 0x227FFF
    c2 = ((x >> 2) & 0xFFF) < 0x10
    s.add(c1)
    s.add(c2)
    if s.check() == z3.sat:
        m = s.model()
        x_val = m.eval(x, model_completion=True).as_long()  # 0x224003
        return x_val


def main():
    idx_zero = guess_first_ioctl()
    l = [idx_zero]

    for i in range(1, 0x10):
        ioctl_code = ((idx_zero >> 2) | i) << 2
        l.append(ioctl_code + 0x03)

    base = idaapi.get_name_ea(idaapi.BADADDR, "IoctlHandler")

    assert base != idaapi.BADADDR, "IoctlHandler not found"

    for i in l:
        idx = i >> 2 & 0xFFF
        current_desc = base + 0x10 * idx
        size_of_routine_desc = 0x10
        routine = idaapi.get_qword(current_desc + 0x08)
        routine_name = idc.demangle_name(
            idaapi.get_ea_name(routine), idc.get_inf_attr(idc.INF_SHORT_DN)
        ).split("(")[0]

        print(f"#define IOCTL_{routine_name} {i:#X} // {method_names[i & 3]}")


if __name__ == "__main__":
    main()

"""
// returns 
#define IOCTL_PcwpIoctlCreateQuery 0X224003 // METHOD_NEITHER
#define IOCTL_PcwpIoctlAddQueryItem 0X224007 // METHOD_NEITHER
#define IOCTL_PcwpIoctlRemoveQueryItem 0X22400B // METHOD_NEITHER
#define IOCTL_PcwpIoctlModifyQueryItem 0X22400F // METHOD_NEITHER
#define IOCTL_PcwpIoctlCollect 0X224013 // METHOD_NEITHER
#define IOCTL_PcwpIoctlEnumerateInstances 0X224017 // METHOD_NEITHER
#define IOCTL_PcwpIoctlSetSecurity 0X22401B // METHOD_NEITHER
#define IOCTL_PcwpIoctlGetSecurity 0X22401F // METHOD_NEITHER
#define IOCTL_PcwpIoctlRegister 0X224023 // METHOD_NEITHER
#define IOCTL_PcwpIoctlReadNotificationData 0X224027 // METHOD_NEITHER
#define IOCTL_PcwpIoctlCompleteNotification 0X22402B // METHOD_NEITHER
#define IOCTL_PcwpIoctlDisconnect 0X22402F // METHOD_NEITHER
#define IOCTL_PcwpIoctlCreateNotifier 0X224033 // METHOD_NEITHER
#define IOCTL_PcwpIoctlNotify 0X224037 // METHOD_NEITHER
#define IOCTL_PcwpIoctlStatelessNotify 0X22403B // METHOD_NEITHER
#define IOCTL_PcwpIoctlCheckNotifier 0X22403F // METHOD_NEITHER
"""

The output can be added to the header of any "client" we'd want to write to call the functions through. I will be adding more information on this soon™.

Talking to pcw.sys

Open a handle to \Device\PcwDrv is quite simple, creating a query object is quite simple as well.

NTSTATUS __fastcall PcwpIoctlCreateQuery(PVOID InputBuffer, PVOID OutputBuffer, unsigned int *UNREFERENCED_PARAMETER)
{
  Object = 0LL;
  Handle = 0LL;
  EventHandle = *InputBuffer;
  if ( EventHandle
    && (v6 = ReferenceObjectByHandle__KEVENT_(EventHandle, OutputBuffer, ExEventObjectType, v3, &Object), v6 < 0) ) // [8]
  {
    if ( Object )
      ObfDereferenceObject(Object);
    return v6;
  }
  else
  {
    result = PCW_QUERY::Create(&Handle, OutputBuffer, &Object, v3);
    if ( result >= 0 )
    {
      *OutputBuffer = Handle;
      return 0;
    }
  }
  return result;
}

at [8] the function checks if the call to ReferenceObjectByHandle__KEVENT_ is successful if not successful it just dereferences the given _KEVENT. Judging from that as well as the IoctlHandler for the function had 8 byte input and output so the output structure could deterministically can be.

struct _PCW_CREATE_QUERY_INFO {
	HANDLE EventHandle; 
} PCW_CREATE_QUERY_INFO, *PPCW_CREATE_QUERY_INFO; 

Therefore, the following function should create a PCW_QUERY object and return us the handle.

NTSTATUS IssueControl(HANDLE h, ULONG IoctlCode, PVOID InBuffer, ULONG InLen,
                      PVOID OutBuf, ULONG OutLen) {
  return NtDeviceIoControlFile(h, g_Event, nullptr, nullptr, &g_IoStatusBlock,
                               IoctlCode, InBuffer, InLen, OutBuf, OutLen);
}

NTSTATUS PcwpCreateQuery(HANDLE PcwDrvHandle, _Out_ PHANDLE QueryHandle, _Out_ PHANDLE EventHandle) {
  
  auto s = NtCreateEvent(EventHandle,  MAXIMUM_ALLOWED , nullptr , NotificationEvent , false); 
  
  if (!NT_SUCCESS(s)) {
    return s; 
  }

  return IssueControl( PcwDrvHandle, IOCTL_PcwpIoctlCreateQuery , EventHandle, sizeof(HANDLE), QueryHandle, sizeof(HANDLE)); 
}

and that successfully does it.

Fin

So, we've basically taken a look at creation of PcwObjectType from the DriverEntry of the pcw driver as well as taken a look at its major function codes including IRP_MJ_CREATE and its FAST_IO_DEVICE_CONTROL function. I will be posting a follow up(or follow ups?) diving more into this drivers functionality

References