导语: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_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指向的双向链表来访问此上下文,你可以在下面找到这些连接的示意图。
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列表中,如下所示。
在此Scope实例结构中,ExpWnfSubscribeWnfStateChange搜索(使用ExpWnfLookupNameInstance)与给定WNF状态名称匹配的WNF_NAME_INSTANCE。
如果没有找到匹配项,则使用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中,如下所示。
另外,我还可以将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使用)。
我们现在只需要找一个特定的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, ¬ifCallback, 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状态名,就很容易得到一些结果。
与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的过程。