Windows通知工具:记录最薄的攻击面

切下的是Alex IonescuGabrielle VialaBlackHat 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, //      _In_ ULONG MaximumStateSize, //   4-  _In_ PSECURITY_DESCRIPTOR SecurityDescriptor // **  ); 

有一个对称的系统调用ZwDeleteWnfStateName,您可以使用它删除已注册的状态名(同样,众所周知的名称除外)。



发布WNF状态数据


要设置或更改WNF状态名称数据,可以使用ZwUpdateWnfStateData系统调用:


 NTSTATUS ZwUpdateWnfStateData ( _In_ PCWNF_STATE_NAME StateName, _In_reads_bytes_opt_(Length) const VOID* Buffer, _In_opt_ ULONG Length, //   ,   MaximumSize,    _In_opt_ PCWNF_TYPE_ID TypeId, //      _In_opt_ const PVOID ExplicitScope, //  , SID ,  (ID)  _In_ WNF_CHANGE_STAMP MatchingChangeStamp, //     _In_ LOGICAL CheckStamp //         ); 

有一个对称系统调用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 //   0,      ); 

真正的优势在于,Update和Query API函数实际上不需要注册的 WNF状态名称。 如果名称不是临时的(并且调用代码具有足够的特权),则可以实时注册名称实例!



WNF通知


到目前为止,我们已经假定用户知道何时调用数据获取功能。 但是,也存在阻止阅读的功能 ,该功能可以使用通知系统(更接近真正的发布者-订阅者模型)来工作。


首先,进程必须通过调用ZwSetWnfProcessNotificationEvent函数来注册事件。 然后,您需要调用ZwSubscribeWnfStateChange函数,并指定事件掩码以获取输出上的订阅标识符。 事件可以有两种类型:


  • 数据通知:
    • 0x01-数据外观
    • 0x10-名称破坏
  • 元元符号
    • 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

: 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

Source: https://habr.com/ru/post/zh-CN459626/


All Articles