切下的是Alex Ionescu和Gabrielle Viala在BlackHat 2018大会上 发表的演讲 “ Windows通知功能:迄今为止最无证的内核攻击面”的翻译。


什么是Windows通知功能(WNF)
Windows Notification Facility是一种通知机制(在内核和用户模式下均可用),它建立在发布者-订阅者模型( pubsub ,Publisher / Subscriber)上。 该机制已在Windows 8中添加:部分是为了解决OS中一些长期存在的设计限制,但它也应该作为实现类似于iOS / Android的推送通知的基础。
它的关键特征是它是一种盲目 (大多数情况下没有注册)的模型,允许无序订阅和发布。 这意味着消费者甚至可以在通知由其来源发布之前就订阅该通知。 而且,生成事件的人不需要事先“注册”通知。
另外,该机制支持:
- 永久和临时通知
- 单调增加的唯一标识符
- 每个事件的有效负载缓冲区(最大4 KB)
- 具有基于组的序列化的线程池通知模型
- 基于范围的安全模型,该模型通过标准DACL / SACL机制实现安全描述符
为什么出现WNF
考虑一个典型的例子:有一个驱动程序想知道已经连接了具有读写访问权限的卷。 为了通知您,Autochk(在Windows上类似于fsck )报告一个称为VolumesSafeForWriteAccess的事件。 但是为了报告事件,您必须首先创建事件对象本身。
我们还需要知道Autochk已经在处理该卷,但是尚未发出我们正在等待的事件的信号。 糟糕的解决方案:与sleep()一起循环,检查事件的存在以及创建事件的时间-等待它。
但是退出Windows应用程序后,其所有描述符都将关闭。 并且当对象没有描述符时,它将被销毁。 那么谁来举办这个活动呢?
如果没有WNF,解决方案是让OS内核在加载任何驱动程序之前生成一个事件,并让Autochk像使用者一样打开它,但是它应该发信号通知该事件,而不是等待。
州名WNF
在WNF世界中,州名是64位数字。 但是有一个窍门-实际上这是一个编码结构。 状态名称具有版本 , 生存期 , 范围 , 数据持久性标志和唯一的序列号 。
typedef struct _WNF_STATE_NAME_INTERNAL { ULONG64 Version:4; ULONG64 NameLifetime:2; ULONG64 DataScope:4; ULONG64 PermanentData:1; ULONG64 Unique:53; } WNF_STATE_NAME_INTERNAL, *PWNF_STATE_NAME_INTERNAL;
但是只有当我们对一个带有魔术常数的64位数字进行XOR运算时,此数据才可用:
#define WNF_STATE_KEY 0x41C64E6DA3BC0074
状态名称的生存期
WNF状态名称可以是(WNF_STATE_NAME_LIFETIME):
前三个与注册表中相应的项相关联,注册表中将存储状态信息:
- HKLM \ SYSTEM \ CURRENTCONTROLSET \ CONTROL \通知中居住着知名的名字
- 永久名称存在于HKLM \ SOFTWARE \ Microsoft \ Windows NT \ CurrentVersion \ Notifications中
- 永久名称存在于HKLM \ SOFTWARE \ Microsoft \ Windows NT \ CurrentVersion \ VolatileNotifications中
知名名称有其独特之处:无法注册。 在系统引导时,这样的名称应该已经出现在注册表中。 持久名称和持久名称需要包含的SeCreatePermanentPrivilege特权(像其他全局对象一样)才能创建它们。 永久名称不存在于注册服务商流程中,而永久名称在系统重新启动后仍然存在。
数据范围
数据范围定义了WNF状态名称周围的第一个安全边界;它确定了谁可以看到它并可以访问它。 状态名称的范围可以是:
除了提供安全边界,WNF范围还可用于为同一名称提供不同的数据实例。 内核(与其他安全机制一样)绕过状态访问检查。 TCB特权允许跨范围访问WNF状态名称。
范围“系统”和范围“机器”是全局范围。 它们没有自己的标识符(它们使用不同的全局容器)。 用户会话的范围使用会话标识符(会话ID)作为ID。 特定用户的范围使用该用户的SID作为标识符。 EPROCESS对象的地址是过程范围的标识符。
序列号
为了确保唯一性,每个状态名称都有一个唯一的51位序列号。 众所周知的名称在其序列号中包含一个4个字符的家庭标签,其余的21位用作唯一标识符。 永久名称使用注册表值“ SequenceNumber”存储其递增编号。 持久名称和临时名称使用公共增量计数器,该计数器位于全局变量中。 此数据是针对每个容器(每个仓库)分别存储和处理的,可在PspHostSiloGlobals-> WnfSiloState中获得。
在Microsoft内部,每个WNF名称都有一个在代码中使用的“友好”标识符,有时它以相同的名称存储在全局命名空间中。 例如,符号nt!WNF_BOOT_DIRTY_SHUTDOWN,其值为0x1589012fa3bc0875。 与魔常数WNF_STATE_KEY进行XOR之后,我们得到的值为0x544f4f4200000801,可以按位解释为:
BOOT1, Well-Known Lifetime, System Scope, Version 1
使用WNF的系统调用
内核系统调用使您可以注册和删除WNF状态名称,发布和接收WNF状态名称数据,还可以接收来自WNF的各种通知。
注册WNF状态名称
除了众所周知的名称(如前所述)外,可以在操作系统运行时注册WNF状态名称:
NTSTATUS ZwCreateWnfStateName ( _Out_ PWNF_STATE_NAME StateName, _In_ WNF_STATE_NAME_LIFETIME NameLifetime, _In_ WNF_DATA_SCOPE DataScope, _In_ BOOLEAN PersistData, _In_opt_ PCWNF_TYPE_ID TypeId,
有一个对称的系统调用ZwDeleteWnfStateName,您可以使用它删除已注册的状态名(同样,众所周知的名称除外)。
发布WNF状态数据
要设置或更改WNF状态名称数据,可以使用ZwUpdateWnfStateData系统调用:
NTSTATUS ZwUpdateWnfStateData ( _In_ PCWNF_STATE_NAME StateName, _In_reads_bytes_opt_(Length) const VOID* Buffer, _In_opt_ ULONG Length,
有一个对称系统调用ZwDeleteWnfStateData来删除(清除)WNF状态名称的数据。
获取WNF状态数据
为了请求WNF状态名称数据,可以使用以下系统调用(大多数参数类似于Update函数):
NTSTATUS ZwQueryWnfStateData ( _In_ PCWNF_STATE_NAME StateName, _In_opt_ PCWNF_TYPE_ID TypeId, _In_opt_ const VOID* ExplicitScope, _Out_ PWNF_CHANGE_STAMP ChangeStamp, _Out_writes_bytes_to_opt_(*BufferSize, *BufferSize) PVOID Buffer, _Inout_ PULONG BufferSize
真正的优势在于,Update和Query API函数实际上不需要注册的 WNF状态名称。 如果名称不是临时的(并且调用代码具有足够的特权),则可以实时注册名称实例!
WNF通知
到目前为止,我们已经假定用户知道何时调用数据获取功能。 但是,也存在阻止阅读的功能 ,该功能可以使用通知系统(更接近真正的发布者-订阅者模型)来工作。
首先,进程必须通过调用ZwSetWnfProcessNotificationEvent函数来注册事件。 然后,您需要调用ZwSubscribeWnfStateChange函数,并指定事件掩码以获取输出上的订阅标识符。 事件可以有两种类型:
- 数据通知:
- 元元符号
- 0x02-接收数据通知的订户的外观(数据订户)
- 0x04-接收元通知的订户的外观(元订户)
- 0x08-接收数据通知和元通知的订户的外观(通用订户)
然后,您需要等待记录的事件。 并且每次事件变为信号时,您都需要调用ZwGetCompleteWnfStateSubscription函数,该函数返回WNF_DELIVERY_DESCRIPTOR。
但是这些低级API函数有一个问题(感谢Gabi对其进行调查):每个进程只能有一个注册事件。
高级用户模式API(ntdll)
当涉及到通知时,事情变得很复杂,因此ntdll.dll的rtl层提供了一个更简单的接口:
NTSTATUS RtlSubscribeWnfStateChangeNotification ( _Outptr_ PWNF_USER_SUBSCRIPTION* Subscription, _In_ WNF_STATE_NAME StateName, _In_ WNF_CHANGE_STAMP ChangeStamp, _In_ PWNF_USER_CALLBACK Callback, _In_opt_ PVOID CallbackContext, _In_opt_ PCWNF_TYPE_ID TypeId, _In_opt_ ULONG SerializationGroup, _In_opt_ ULONG Unknown );
实际上,不需要直接调用系统服务:只需使用单个ntdll.dll驱动的事件队列。
在后台,WNF_DELIVERY_DESCRIPTOR的内容被转换为回调参数:
typedef NTSTATUS (*PWNF_USER_CALLBACK) ( _In_ WNF_STATE_NAME StateName, _In_ WNF_CHANGE_STAMP ChangeStamp, _In_opt_ PWNF_TYPE_ID TypeId, _In_opt_ PVOID CallbackContext, _In_ PVOID Buffer, _In_ ULONG BufferSize);
对于每个新订阅,都会创建一个条目,并将其放置在全局变量RtlpWnfProcessSubscriptions指向的列表中。 该列表建立在字段WNF_NAME_SUBSCRIPTION之一(类型为LIST_ENTRY)上。 每个WNF_NAME_SUBSCRIPTION依次具有另一个LIST_ENTRY字段,用于组织带有回调和上下文的WNF_USER_SUBSCRIPTION列表。
高级内核级API(Ex)
WNF还为内核模式代码(可以从驱动程序使用)提供几乎相同的功能:通过导出的系统调用和运行时(Ex-layer)中的高级API函数。
ExSubscribeWnfStateChange函数接受状态名称,类型掩码和回调函数+上下文的地址作为输入,并返回订阅描述符。 回调函数接收目标名称,事件掩码,更改标签,但不接收缓冲区或其大小。
ExQueryWnfStateData函数基于传递的订阅描述符,读取当前状态数据。 实际上,每个回调最终都会调用ExQueryWnfStateData函数来获取与通知关联的数据。
对于内核模式订阅和用户模式订阅,WNF(用于跟踪订阅)都会创建WNF_SUBSCRIPTION结构的实例。 但是对于用户模式,某些字段将不会填写,例如Callback和Context,因为对于用户模式,处理程序的地址由ntdll.dll存储和处理。
WNF数据结构

来自翻译者 :请参阅下一节。
WNF分析实用程序
译者的话 :这里值得再次回忆一下,演讲不仅是由Alex主持的,而且是由Gabrielle Viala主持的。 特别是,其作者权属于下面描述的WnfCom模块。 此外,Gabrielle还充分详细地描述了WNF的内部结构(请参阅上一节中的插图)。 不幸的是,它的大多数幻灯片都不在演示文稿的pdf中(指示为原始)或仅由标题指示。 但是:
并从翻译者那里 :如果有人想用Gabrielle幻灯片的内容来补充当前的翻译,或者从演讲视频的任何部分扩展速记翻译,欢迎您。 为了方便添加/更改大块,我可以在github(或其他版本控制服务器)上发布翻译源。
Wnfcom
WnfCom是一个Python模块( github源代码 ),它显示了通过WNF的互操作性。 主要特点:
- 允许您从现有实例实例读取/写入数据
- 允许您创建临时状态名称(作为服务器 )
- 允许您获取客户端对象的实例,该实例将处理有关更改名称的特定实例的通知
用法示例:
>>> from wnfcomimport Wnfcom >>> wnfserver = Wnfcom() >>> wnfserver.CreateServer() [SERVER] StateNamecreated: 41c64e6da5559945 >>> wnfserver.Write(b"potatosoup") Encoded Name: 41c64e6da5559945, Clear Name: 6e99931 Version: 1, Permanent: No, Scope: Machine, Lifetime: Temporary, Unique: 56627 State update: 11 bytes written
>>> from wnfcomimport Wnfcom >>> wnfclient = Wnfcom() >>> wnfclient.SetStateName("41c64e6da5559945") >>> wnfclient.Listen() [CLIENT] Event registered: 440 [CLIENT] Timestamp: 0x1 Size: 0xb Data:00000000: 70 6F 74 61 74 6F 20 73 6F 75 70 potato soup
Wnfdump
WnfDump是用C编写的命令行实用程序。可执行文件可以通过选择所需位深度的子目录在https://github.com/ionescu007/wnfun中找到。 该实用程序可用于搜索有关WNF状态名称的信息:
- -d(转储)使用基于注册表的枚举转储所有WNF状态名称。 可以补充以下选项:
- -v( V erbose)详细输出,其中包括WNF状态数据的十六进制转储;
- -s(安全性)安全描述符-WNF状态名称的权限的SDDL字符串。
- -b(暴力)直接枚举临时WNF状态名称(有关此内容,请参见下文)
- -i(信息)显示有关单个指定的WNF状态名称的信息
- -r( R ead)从指定的WNF状态名称读取数据
- -w( W rite)将数据写入指定的WNF状态名称
- -n(注意)为指定的WNF状态名称注册通知订户(以下将是Edge的更特定用例)
WNF攻击面
本节(更确切地说是其子节)将讨论可能的攻击和有趣的敏感WNF数据。
特权数据披露
读取系统中存在的数千个WNF状态名称,有几种,其中的数据看起来非常有趣。 其中一些数据可疑类似于指针或其他特权数据。
在多台机器上玩了之后,在某些情况下,可能会发现一堆,堆栈和其他特权信息,这些信息在特权边界之间公开。 错误/漏洞报告已于7月提交给MSRC,但在11月(演示后)已得到纠正。 例如:通过WNF_AUDC *事件泄漏了4 KB的堆栈!
主要问题与我们从j00ro,taviso等人的先前研究中看到的相同。 某些WNF状态名称包含带有各种填充和/或对齐问题的编码数据结构。 在某些情况下,未初始化的内存泄漏。
从译者 那里:该文档的引言部分 的翻译 : Mateusz Jurczyk aka j00ro的x86仿真和污点跟踪检测内核内存泄露 。
发现状态名称和权限
第一种方法是发现所有可能被恶意操纵的状态名称。 对于知名的,永久的和永久的名称,可以通过枚举注册表项来枚举。 然后可以将找到的值与友好的标识符进行比较(可以在几个地方找到它们:)
然后,我们还可以查看注册表中的安全描述符(这是数据缓冲区中的第一件事)。 安全描述符不是规范的:它没有所有者和组,因此在技术上无效。 但是用假的所有者和组来修复安全描述符没有问题。
检测临时状态名称及其权限
但是使用临时名称,上述技巧将不起作用:它们不在注册表中。 而且只有内核将它们的数据结构(!Wnf)存储在内存中。 但是临时名称实际上并不难被强行使用:
- 版本总是很重要1
- 生命总是很重要WnfTemporaryStateName
- 永久标记始终被清除(临时状态名称不能包含永久数据)
- 范围(范围)可以采用4个值之一
是的,但是剩余的序列号是51位! 确实……但不要忘记序列号是单调增长的。 对于临时名称,该序列在每次引导时都会重置为0。 通常,您可以使用一百万个序列号的窗口:在循环中,通过使用请求的信息类WnfInfoStateNameExist调用ZwQueryWnfStateNameInformation来检查每个名称的存在(从0开始)(假设访问错误也表明存在名称)。 如果不存在另外一百万个名称,则可以停止搜索。
临时名称安全描述符(像其他临时名称数据一样)存储在内核中。 因此,请求它们的唯一方法是调试内核模式时的!Wnf扩展名。 但是我们可以:
- 在尝试读取数据时得出有关读取权限的结论。
- 可以得出结论,可以通过尝试写入数据来进行记录。 但是值得考虑的是,成功写入偶数0字节会破坏实际使用者尚未设法获取的数据。 再有一个窍门:我们可以应用适当的变更标记。 我们正在尝试使用标签0xFFFFFFFF进行写操作:在访问检查之后检查了标签,因此,错误值会导致写许可权泄漏。
这并不能为我们提供完整的安全描述符,但是通过以不同的特权运行代码,我们可以了解不同系统帐户(低IL /用户/管理员/系统)的限制。
上市订户
在WNF_PROCESS_CONTEXT结构中,字段之一是此过程的所有订阅的列表头(LIST_ENTRY)。 每个订阅都是WNF_SUBSCRIPTION的单独实例。
内核模式订户主要由系统进程拥有。 我们可以使用!List调试器命令来转储处理程序及其在WNF_SUBSCRIPTION系统进程中注册的参数。 值得注意的是,在某些情况下,使用事件聚合器(CEA.SYS),该事件聚合器在其上下文结构中隐藏了实际的回调地址。
我们可以对用户模式进程重复此方法,但是回调地址将为NULL,因为它们是用户模式订户。 因此,我们需要加入进程的用户空间,获取RtlpWnfProcessSubscriptions表,然后转储WNF_USER_SUBSCRIPTION实例的列表,每个实例已包含回调地址。 不幸的是,这个字符是静态的,这意味着它不是开放字符,但是可以通过反汇编找到。 再次值得一提的是(类似于CEA.SYS内核模式),许多用户模式处理程序使用事件聚合器(EventAggregation.dll),该事件聚合器将回调存储在其上下文中。
有趣且敏感的WNF状态名称
本节将提供一些有趣的示例,说明一些WNF状态名称如何揭示系统信息。
使用WNF确定系统状态和用户行为
一些WNF标识符可用于获取有关您感兴趣的计算机状态的信息:
- WNF_WIFI_CONNECTION_STATUS-无线状态
- WNF_BLTH_BLUETOOTH_STATUS-类似,但用于蓝牙(也是WNF_TETH_TETHERING_STATE)
- WNF_UBPM_POWER_SOURCE-显示电源(电池或电源适配器)
- WNF_SEB_BATTERY_LEVEL-包含电池电量
- Windows Phone上的WNF_CELL_ *-包含有关以下信息:网络,号码,信号强度,EDGE或3G,...
WNF :
- WNF_AUDC_CAPTURE/RENDER — ( PID), /
- WNF_TKBN_TOUCH_EVENT — ,
- WNF_SEB_USER_PRESENT/WNF_SEB_USER_PRESENCE_CHANGED — Windows
API
, API , API , , /. WNF . , , WNF .
: WNF_SHEL_(DESKTOP)_APPLICATION_(STARTED/TERMINATED) modern- ( , ) DCOM, Win32. — ShellExecute: Explorer, cmd.exe, ...
, WNF API , :
- WNF_SHEL_LOCKSCREEN_ACTIVE —
- WNF_EDGE_LAST_NAVIGATED_HOST — URL, ( ) Edge
WNF
WNF, . : WNF_FSRL_OPLOCK_BREAK — , (/), PID' !
WNF , . : WNF_SHEL_DDC_(WNS/SMS)_COMMAND – 4 , .
, WNF, . : WNF_CERT_FLUSH_CACHE_TRIGGER ( ), WNF_BOOT_MEMORY_PARTITIONS_RESTORE, WNF_RTDS_RPC_INTERFACE_TRIGGER_CHANGED, ...
WNF
:
- WriteProcessMemory —
- ( ) —
- (Atom) —
- — , WM_COPYDATA DDE,
- GUI — ( ) ,
WNF :
- WNF, (, )
- Rtl/ZwQueryWnfStateData WNF
, :
- APC s
- (Remote Threads)
- (Changing Thread Context)
- " window long " — , ,
WNF_USER_SUBSCRIPTION ( WNF_NAME_SUBSCRIPTION, RtlpWnfProcessSubscriptions). ( CFG ), ( 5 6 ).
, : , , , -.
WNF SEB_, ( S ystem E vents B roker). SystemEventsBrokerServer.dll SystemEventsBrokerClient.dll API . , SEB SEB, .
CEA.SYS EventAggregation.dll. " " (Event Aggregation Library), , : , , WNF , . WNF, . .
: .
, Windows Notification Facility Alex' Gabrielle. ( ) redp .

WNF ( ) wincheck . , Gabrielle Viala , redp, : http://redplait.blogspot.com/search/label/wnf .
PoC ( github ) explorer ( — notepad). modexp : Callback WNF_USER_SUBSCRIPTION. :
- explorer.exe
- WNF_USER_SUBSCRIPTION
- RWX- , WriteProcessMemory (, VirtualAllocEx + WriteProcessMemory)
- WNF_USER_SUBSCRIPTION ( WriteProcessMemory)
- ntdll!NtUpdateWnfStateData(...) ,
- WNF_USER_SUBSCRIPTION