导语:Windows Notification Facility(WNF)是一个不是很知名的内核组件,在整个系统中的主要任务就是用于通知,相当于通知中心。本文将对这个组件进行详细介绍。

前言

关于Windows Notification Facility (WNF)组件,或许我和你一样,从来不知道这个组件,并且互联网上关于该组件的信息也很少。能查到的也只是如下信息:

14. Reverse engineer the following Windows kernel functions.
    The result of this exercise will be used in the next exercise.

    • ExSubscribeWnfStateChange
    • ExQueryWnfStateData

15. Write a driver that gets notified when an application is using the microphone.
    Print its pid.

Hints:
    • check contentDeliveryManager_Utilities.dll for the WNF StateName.
    • some interesting info is available here:

http://redplait.blogspot.com/2017/08/wnf-ids-from-perfntcdll.html

Windows Notification Facility(WNF)是一个不是很知名的内核组件,在整个系统中的主要任务就是用于通知,相当于通知中心。它既可以在内核模式中使用,也可以在用户空间 (USER-LAND)中使用,其中包含一组导出的但显然没有文档化的API函数和相关的数据结构。应用程序可以订阅特定类型的事件(由StateName标识),以便在每次发生状态更改时进行通知(可以与StateData关联)。另一方面,发布通知的组件负责提供与通知一起发送的数据并触发事件。

应该注意的是,WNF状态名称可以实例化(Scope)到单个进程,silo(Windows容器)或整个设备中。例如,如果应用程序在silo内运行,则只会通知其自身容器内发生的SiloScope内的事件。

数据结构

WNF中涉及很多数据结构,以下就是它们在内存中关系的简化图。

wnf_struct2.jpg

WNF_NAME_INSTANCE结构在内存中表示事件或WNF状态名称的实例,这些结构在二叉树中排序,并链接到事件发生的scope,scope属性(使用 scope 属性,可以将数据单元格与表头单元格联系起来。)确定组件能够查看或访问的信息,它们还支持为相同的状态名实例化不同的数据。

有五种可能的scope类型,定义如下。

typedef enum _WNF_DATA_SCOPE{
  WnfDataScopeSystem = 0x0,
  WnfDataScopeSession = 0x1,
  WnfDataScopeUser = 0x2,
  WnfDataScopeProcess = 0x3,
  WnfDataScopeMachine = 0x4,} WNF_DATA_SCOPE;

使用WNF_SCOPE_INSTANCE结构标识的作用域按类型存储在双向链接列表中,并且它们的表头保存在特定于silo的WNF_SCOPE_MAP中。

当组件订阅WNF状态名称时,将创建新的WNF_SUBSCRIPTION结构并将其添加到属于关联的WNF_NAME_INSTANCE的链接列表中。如果通知订阅者使用的是低级API(例如下面描述的API),则会在WNF_SUBSCRIPTION中添加回调,并在需要通知组件时调用。

WNF_PROCESS_CONTEXT对象会跟踪特定通知订阅者进程涉及的所有不同结构。它还存储用于通知进程的KEVENT。可以通过EPROCESS对象访问此上下文,也可以通过抓取nt!ExpWnfProcessesListHead指向的双向链表来访问此上下文,你可以在下面找到这些连接的示意图。

22.jpg

0x906指的是什么呢?它与WNF使用的所有结构都有一个描述结构类型和大小的小头(Windows文件系统相关数据结构中常见的情况)有关。

typedef struct _WNF_CONTEXT_HEADER{
    CSHORT NodeTypeCode;
    CSHORT NodeByteSize;} WNF_CONTEXT_HEADER, *PWNF_CONTEXT_HEADER;

这个头文件在调试时非常方便,因为很容易发现内存中的对象,下面是一些WNF结构的节点类型代码。

#define WNF_SCOPE_MAP_CODE          ((CSHORT)0x901)
#define WNF_SCOPE_INSTANCE_CODE     ((CSHORT)0x902)
#define WNF_NAME_INSTANCE_CODE      ((CSHORT)0x903)
#define WNF_STATE_DATA_CODE         ((CSHORT)0x904)
#define WNF_SUBSCRIPTION_CODE       ((CSHORT)0x905)
#define WNF_PROCESS_CONTEXT_CODE    ((CSHORT)0x906)

反转函数

掌握了这些背景知识后,让我们开始学习如何使用该组件,第一部分实际上是反转下列函数,以便了解其目的:

· ExSubscribeWnfStateChange

· ExQueryWnfStateData

ExSubscribeWnfStateChange

NTSTATUSExSubscribeWnfStateChange (
    _Out_ptr_ PWNF_SUBSCRIPTION* Subscription,
    _In_ PWNF_STATE_NAME StateName,
    _In_ ULONG DeliveryOption,
    _In_ WNF_CHANGE_STAMP CurrentChangeStamp,
    _In_ PWNF_CALLBACK Callback,
    _In_opt_ PVOID CallbackContext
    );

ExSubscribeWnfStateChange允许在WNF引擎中注册新的订阅,它将StateName作为参数之一,指定我们感兴趣的事件类型,并在触发通知时调用回调函数。它还会返回一个新的订阅指针,该指针可用于查询与通知关联的数据。

在内部,此函数仅将执行流转移到private counterpart(ExpWnfSubscribeWnfStateChange),由它处理所有进程。

由于WNF状态名称以不透明格式存储,因此ExpWnfSubscribeWnfStateChange首先使用ExpCaptureWnfStateName解码ID的“清洁”版本。

这个“清洁”的WNF状态名称可以解码如下:

#define WNF_XOR_KEY 0x41C64E6DA3BC0074

ClearStateName = StateName ^ WNF_XOR_KEY;
Version = ClearStateName & 0xf;
LifeTime = (ClearStateName >> 4) & 0x3;
DataScope = (ClearStateName >> 6) & 0xf;
IsPermanent = (ClearStateName >> 0xa) & 0x1;
Unique = ClearStateName >> 0xb;

要是格式再正规一点,解码的结构则如下所示:

typedef struct _WNF_STATE_NAME_INTERNAL
{
    ULONG64 Version : 4;
    ULONG64 Lifetime : 2;
    ULONG64 DataScope : 4;
    ULONG64 IsPermanent : 1;
    ULONG64 Unique : 53;
} WNF_STATE_NAME_INTERNAL, *PWNF_STATE_NAME_INTERNAL;

然后,ExpWnfSubscribeWnfStateChange调用ExpWnfResolveScopeInstance。后者检索Server Silo Globals(或者在没有服务器silo的情况下为nt!PspHostSiloGlobals)并通过几个结构来查找名称实例所属的WNF_SCOPE_INSTANCE。如果此Scope实例(Scope Instance)不存在,则会创建该Scope并将其添加到相应的WNF_SCOPE_MAP列表中,如下所示。

33.png

在此Scope实例结构中,ExpWnfSubscribeWnfStateChange搜索(使用ExpWnfLookupNameInstance)与给定WNF状态名称匹配的WNF_NAME_INSTANCE。

44.png

如果没有找到匹配项,则使用ExpWnfCreateNameInstance创建一个新的WNF_NAME_INSTANCE,此新实例将添加到以WNF_SCOPE_INSTANCE为根的二叉树中。

该函数的下一步是调用ExpWnfSubscribeNameInstance来创建新的订阅对象。如上所述,此对象将保存引擎所需的所有信息以触发通知。

最后,ExpWnfSubscribeWnfStateChange调用ExpWnfNotifySubscription将新的订阅插入等待队列(pending queue)并触发通知。

ExQueryWnfStateData

NTSTATUS
ExQueryWnfStateData (
    _In_ PWNF_SUBSCRIPTION Subscription,
    _Out_ PWNF_CHANGE_STAMP ChangeStamp,
    _Out_ PVOID OutputBuffer,
    _Out_ OPULONG OutputBufferSize
);

这个函数非常简单,因为它只执行两个操作。首先,它使用ExpWnfAcquireSubscriptionNameInstance从订阅中检索WNF_NAME_INSTANCE。然后,它使用ExpWnfReadStateData读取存储在其中的数据,并尝试将其复制到缓冲区中。如果缓冲区太小,它将只写入OuputBufferSize所需的大小,并返回STATUS_BUFFER_TOO_SMALL。

为了记录,所有WNF状态名称都将其数据存储在WNF_STATE_DATA结构下的内存中。该结构包含各种元数据,例如数据大小和它被更新的次数(称为ChangeStamp)。指向WNF_STATE_DATA的指针直接保存在WNF_NAME_INSTANCE中,如下所示。

55.jpg

另外,我还可以将WNF状态名称标记为持久性(persistent),这意味着数据(和更改标记)将在重新启动时被保留(显然是通过使用辅助数据存储)。有关详细信息,请点此了解。

编码过程

在我掌握了函数的反转后,就应该能够注册一个新的订阅,并通知任何其他合法的应用使用WNF。

但是,此时我仍然缺少一个元素,就是找到microphone类输入所需的WNF状态名。

本文,我只会详细介绍与WNF交互相关的驱动程序。如果你对Windows上的驱动程序开发感兴趣,你可能需要查看Windows驱动程序工具包文档及其示例。

寻找合适的WNF状态名

至于如何检索WNF状态名称,我也是阅读了此博客,并了解了库名称(contentDeliveryManager_Utilities.dll)。

在这篇文章中,Redplait定义了WNF使用的几个状态名。不幸的是,我正在寻找的那个状态名却没有被列出。然而,这仍然给了我一个很好的启发,因为我最起码知道了WNF状态名是什么样的。

经过摸索,找到我们正在寻找的WNF状态名的一个简单的方法就是对contentDeliveryManager_Utilities.dll中的其中一个WNF状态名称实施grep,以此找到其他关联的ID,幸运的是,这个方法非常有效!通过对IDA中匹配模式的交叉引用,我们可以得到DLL中引用的WNF状态名的完整列表。这个列表中的每个条目都自带其名称和描述,这对我们来说非常方便! (此列表由GetWellKnownWnfStateByName使用)。

66.PNG

我们现在只需要找一个特定的microphone类既可:

.rdata:00000001800E3680  dq offset WNF_AUDC_CAPTURE
.rdata:00000001800E3688  dq offset aWnf_audc_captu ; "WNF_AUDC_CAPTURE"
.rdata:00000001800E3690  dq offset aReportsTheNu_0 ; "Reports the number of, and process ids "...
// “Reports the number of, and process ids of all applications currently capturing audio.
//  Returns a WNF_CAPTURE_STREAM_EVENT_HEADER data structure”

应该注意的是,Windows性能分析器附带的“perf_nt_c.dll“库也可以使用相同的表。

订阅事件

要订阅一个新事件,我们只需要在我们的驱动程序中使用我们从上面找到的WNF状态名称调用ExSubscribeWnfStateChange既可。此函数虽然已导出,但还未在任何标题中进行定义,因此我们必须通过粘贴上面的定义来对它作出说明。请注意,ntoskrnl.lib包含导入库存根,所以不需要手动检索它的地址。

此时,唯一需要做的就是使用正确的参数调用函数:

NTSTATUSCallExSubscribeWnfStateChange (
    VOID
    ){
    PWNF_SUBSCRIPTION wnfSubscription= NULL;
    WNF_STATE_NATE stateName;
    NTSTATUS status;

    stateName.Data = 0x2821B2CA3BC4075; // WNF_AUDC_CAPTURE

    status = ExSubscribeWnfStateChange(&wnfSubscription, &stateName, 0x1, NULL, &notifCallback, NULL);
    if (NT_SUCCESS(status)) DbgPrint("Subscription address: %p\n", Subscription_addr);

    return status;}

回调机制的定义

正如我们之前看到的,ExSubscribeWnfStateChange在其参数中包含一个回调机制,每次触发事件时都会调用该回调,此回调将用于获取和处理与通知相关的事件数据。

回调原型如下所示:

NTSTATUSnotifCallback (
    _In_ PWNF_SUBSCRIPTION Subscription,
    _In_ PWNF_STATE_NAME StateName,
    _In_ ULONG SubscribedEventSet,
    _In_ WNF_CHANGE_STAMP ChangeStamp,
    _In_opt_ PWNF_TYPE_ID TypeId,
    _In_opt_ PVOID CallbackContext
    );

要获得回调中的数据,我们必须调用ExQueryWnfStateDataName。同样,这个函数是导出的,但没有在任何标头中定义,所以我们必须自己定义。

NTSTATUSExQueryWnfStateData (
    _In_ PWNF_SUBSCRIPTION Subscription,
    _Out_ PWNF_CHANGE_STAMP CurrentChangeStamp,
    _Out_writes_bytes_to_opt_(*OutputBufferSize, *OutputBufferSize) PVOID OutputBuffer,
    _Inout_ PULONG OutputBufferSize
    );[...]

我们需要调用此API两次,一次是为了获得为数据分配缓冲区所需的大小,另一次是为了实际检索数据。

NTSTATUSnotifCallback(...){
    NTSTATUS status = STATUS_SUCCESS;
    ULONG bufferSize = 0x0;
    PVOID pStateData;
    WNF_CHANGE_STAMP changeStamp = 0;

    status = ExQueryWnfStateDataFunc(Subscription, &changeStamp, NULL, &bufferSize);
    if (status != STATUS_BUFFER_TOO_SMALL) goto Exit;

    pStateData = ExAllocatePoolWithTag(PagedPool, bufferSize, 'LULZ');
    if (pStateData == NULL) {
        status = STATUS_UNSUCCESSFUL;
        goto Exit;
    }

    status = ExQueryWnfStateDataFunc(Subscription, &changeStamp, pStateData, &bufferSize);
    if (NT_SUCCESS(status)) DbgPrint("## Data processed: %S\n", pStateData);

    [...] // do stuff with the dataExit:
    if (pStateData != nullptr) ExFreePoolWithTag(pStateData, 'LULZ');
    return status;}

卸载驱动程序时记得将其清理干净

如果你盲目的尝试上面的代码,你得到将是一个死机的蓝屏状态,这是我在第一次尝试练习并卸载我的驱动程序时所得到的结果。请注意,我们需要提前删除订阅。

为此,我们可以在驱动程序卸载例程中调用ExUnsubscribeWnfStateChange,并确保将PWNF_SUBSCRIPTION wnfSubscription设置为全局变量。

PVOIDExUnsubscribeWnfStateChange (
    _In_ PWNF_SUBSCRIPTION Subscription
    );VOIDDriverUnload (
    _In_ PDRIVER_OBJECT DriverObject
    ){
   [...]
   ExUnsubscribeWnfStateChange(g_WnfSubscription);}

失败的实践尝试

我们现在要做的就是在驱动程序中,启用Cortana,用它来触发事件。等了一会儿,触发还是没有响应,因为我忘记了我的虚拟机上没有声卡,可能是我无法启动任何声音相关应用程序的原因吧。最重要的是,由于我的主机配置,我根本无法让它工作。

尽管如此,为了确保我的驱动程序正常工作,我必须选择另一个事件,并由于我的主机配置,我根本无法让它工作并开始使用WNF_SHEL_DESKTOP_APPLICATION_STARTED。每当桌面应用程序启动时,都会发出此通知。作为反应,它只输出已启动的应用程序名称。有了这个WNF状态名,就很容易得到一些结果。

77.PNG

与WNF保持同步

我在上文展示了一种查找WNF名称的简单方法,只需在其中一个包含该表的DLL中搜索IDA中的名称,更可靠和可扩展的方法是通过解析DLL来查找表并转储它来检索WNF状态名。虽然这不是我在练习中使用的方法,但是为了能够轻松跟上WNF状态名称(添加/删除/修改)的变化,我制作了一个脚本。此脚本可用于区分两个DLL并快速获取表中的差异,以及从单个DLL转储表数据。除此之外,他也可以很容易的适用于其他C和Python程序。

$ python .\StateNamediffer.py -h
usage: StateNamediffer.py [-h] [-dump | -diff] [-v] [-o OUTPUT] [-py]
file1 [file2]

Stupid little script to dump or diff wnf name table from dll

positional arguments:
    file1
    file2 Only used when diffing

optional arguments:
    -h, --help show this help message and exit
    -dump Dump the table into a file
    -diff Diff two tables and dump the difference into a file
    -v, --verbose Print the description of the keys
    -o OUTPUT, --output OUTPUT Output file (Default: output.txt)

输出示例:

typedef struct _WNF_NAME{
    PCHAR Name;
    ULONG64 Value;} WNF_NAME, *PWNF_NAME;WNF_NAME g_WellKnownWnfNames[] ={
    {"WNF_A2A_APPURIHANDLER_INSTALLED", 0x41877c2ca3bc0875}, // An app implementing windows.AppUriHandler contract has been installed
    {"WNF_AAD_DEVICE_REGISTRATION_STATUS_CHANGE", 0x41820f2ca3bc0875}, // This event is signalled when device changes status of registration in Azure Active Directory.
    {"WNF_AA_CURATED_TILE_COLLECTION_STATUS", 0x41c60f2ca3bc1075}, // Curate tile collection for all allowed apps for current AssignedAccess account has been created
    {"WNF_AA_LOCKDOWN_CHANGED", 0x41c60f2ca3bc0875}, // Mobile lockdown configuration has been changed
    [...]}

总结

经过这次的实践,我终于有机会深入研究一个我根本不知道的内核组件,该组件非常有趣。我学到了很多东西,并且非常喜欢弄清楚如何使用WNF的过程。

源链接

Hacking more

...