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
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
- Creating Kernel Object Type, Pavel Yosifovich
- Performance Counters, MSDN
- Z3Py, ericpony
- Experimenting with Object Initializers in Windows, Daax