[翻译] Kernel Attacks through User-Mode Callbacks

前言

本篇属于前置知识,在后续的CVE复现中需要使用到。原文为英文,一边翻译一边记笔记了!尽量用中文描述的浅显易懂一些。

1. 背景知识

1.1 Win32k.sys介绍

​ Win32k本质上由三个主要组件组成:图形设备接口(GDI)、窗口管理器(用户)和支持XP/2000和Longhorn (Vista)显示驱动程序模型的DirectX api(有时也被认为是GDI的一部分)。窗口管理器负责管理Windows用户界面,例如控制窗口显示、管理屏幕输出、从鼠标和键盘收集输入以及向应用程序传递消息;GDI主要关注图形渲染,并实现GDI对象(画笔、钢笔、表面、设备上下文等)、图形渲染引擎(Gre)、打印支持、ICM颜色匹配、浮点数学库和字体支持。

​ 为了正确地与NT执行程序接口,win32k注册了几个callout (PsEstablishWin32Callout)来支持面向GUI的对象,例如桌面和窗口站。win32k还为线程和进程注册了callout,以定义GUI子系统使用的每个线程和进程的结构。

GUI线程和进程。并不是所有线程都使用GUI子系统,因此预先为所有线程分配GUI结构将是一种空间浪费。因此,Windows上的所有线程都以非gui线程(12 KB堆栈)的形式启动。如果一个线程访问任何用户或GDI系统调用(编号>= 0x1000),Windows将该线程提升到一个GUI线程(nt!PsConvertToGuiThread),并调用进程和线程调用。值得注意的是,GUI线程具有更大的线程堆栈,以便更好地处理win32k的递归特性,并支持用户模式回调,这可能需要额外的堆栈空间来存放陷阱帧和其他元数据。

​ 当进程的第一个线程被转换成GUI线程并调用W32pProcessCallout 时,win32k调用win32k!xxxInitProcessInfo初始化每个进程的W32PROCESS/PROCESSINFO2结构。具体来说,这个结构保存每个进程的GUI相关信息,比如相关的桌面、窗口站、用户和GDI句柄计数。函数先在win32k!xxxUserProcessCallout中初始化用户相关字段,然后在GdiProcessCallout中初始化GDI相关字段,然后在win32k!xxxUserProcessCallout中分配结构本身。

​ 此外,win32k还为所有被转换成GUI线程的线程初始化一个W32THREAD/THREADINFO结构。该结构保存与GUI子系统相关的特定于线程的信息,如线程消息队列、注册的windows钩子、所有者桌面、菜单状态等信息。这里,W32pThreadCallout调用win32k!AllocateW32Thread来分配结构,然后是GdiThreadCallout和UserThreadCallout来初始化GDI和用户子系统特有的信息。在这个过程中最重要的函数是win32k!xxxCreateThreadInfo,它负责初始化线程信息结构。

1.2 窗口管理器(Windows Manager)

​ 窗口管理器的一个重要功能是跟踪用户实体,如窗口、菜单、指针等。它将这些实体表示为用户对象,并维护它自己的句柄表,以跟踪它们在用户会话中的使用。因此,当应用程序请求对用户实体执行操作时,它提供句柄值,句柄管理器可以有效地将其映射到内核内存中的相应对象。

​ 用户对象将用户对象分为不同的类型,因此对象都具有具有自己特定于类型的结构。例如,所有窗口对象都是由win32k!tagWND结构定义,而菜单是由win32k!tagMENU结构定义。尽管对象类型在结构上是不同的,但是它们都共享一个commonheader,即HEAD结构。

typedef struct _HEAD {
HANDLE h;
ULONG32 cLockObj;
} HEAD, *PHEAD;

​ HEAD结构保存了句柄值(h)的副本以及锁计数(cLockObj),每当使用对象时,该计数都会递增。当某个特定组件不再使用该对象时,该对象的锁计数将减少。当锁计数达到0时,窗口管理器会释放它。

​ 尽管HEAD结构相当小,但对象很多时候都会使用较大的线程或进程特定的头结构,如THRDESKHEAD和PROCDESKHEAD。这些结构提供了额外的字段,例如指向线程信息结构tagTHREADINFO的指针和指向关联桌面对象(tagDESKTOP)的指针。在提供此信息时,Windows可以限制对其他桌面上的对象的访问,从而在桌面之间提供隔离。类似地,由于对象总是由线程或进程拥有,所以可以实现共存于同一桌面的线程或进程之间的隔离。例如,一个给定的线程不能通过简单地调用DestroyWindow来销毁其他线程的窗口对象。相反,它需要发送一个窗口消息,该消息需要进行额外的验证,如完整性级别检查。然而,由于对象隔离不是以统一和集中的方式提供的,任何未执行所需检查的函数都可能允许攻击者绕过此限制。这无疑是特权服务和登录用户会话之间的会话分离(Vista和更高版本)的原因之一。由于同一会话中的所有进程和线程共享同一个用户句柄表,低权限进程可能会向高权限进程拥有的对象传递消息或与之交互。

句柄表。所有的用户对象都被索引到一个会话句柄表中。句柄表在win32k!Win32UserInitialize中初始化,该函数会在加载win32k的新实例时调用。句柄表本身存储在共享部分(win32k!gpvSharedBase)的底部,也是由Win32UserInitialize设置的。此部分随后被映射到每个新的GUI进程,从而允许进程从用户模式访问处理表信息,而不必求助于系统调用。将共享部分映射到用户模式的决定被视为一种性能优势,并且也用于非基于内核的Win32子系统设计中,以防止在客户机应用程序和客户机-服务器运行时子系统进程(CSRSS)之间进行过多的上下文切换。在Windows 7中,指向句柄表的指针存储在共享信息结构中(win32k!tagSHAREDINFO)。这个结构的指针可以从用户模式(user32!gSharedInfo3)和内核模式(win32k!gSharedInfo)中获得。

​ 用户句柄表中的每个条目都由一个HANDLEENTRY结构表示。这个结构包含该句柄的对象信息,比如指向对象本身的指针(phead)、它的所有者(pOwner)和对象类型(bType)。owner字段是一个指向线程或进程信息结构的指针,如果它为空,则它是一个会话范围的对象。比如显示器或键盘布局/文件对象,它们被认为是会话的全局对象。

typedef struct _HANDLEENTRY {
struct _HEAD* phead;
VOID* pOwner;
UINT8 bType;
UINT8 bFlags;
UINT16 wUniq;
} HANDLEENTRY, *PHANDLEENTRY

​ 用户对象的实际类型由bType值定义,在Windows 7下的值范围为0到21。bFlags定义了附加的对象标志,通常用于指示对象是否已被销毁。如果一个对象被请求销毁,但仍然保存在内存中,这可能是这样的情况,因为它的锁计数是非零的。最后,wUniq值用作计算句柄值的惟一性计数器。句柄值被计算为 handle =table entry id | (wUniq << 0x10)。当释放对象时,计数器改变,以避免后续对象立即重用前一个句柄。需要注意的是,这个机制不能被看作是一个安全特性,因为wUniq计数器只有16位,因此当分配和释放了足够多的对象时,它就会出错。

在这里插入图片描述

​ 为了验证句柄,窗口管理器可以调用任何 HMValidateHandle API。这些函数将句柄值和句柄类型作为参数,并在句柄表中查找相应的条目。如果请求对象,则函数返回对象指针。

内存中的用户对象。在Windows中,用户对象及其相关的数据结构可以驻留在桌面堆、共享堆或会话池中。一般的规则是,与特定桌面关联的对象存储在桌面堆中,其余的对象存储在共享堆或会话池中。每个对象类型的实际位置是由句柄类型信息表(win32k!ghati)定义的。此表保存每个对象类型的属性,供句柄管理器在分配或释放用户对象时使用。具体来说,句柄类型信息表中的每个条目都由一个不透明的结构定义(未列出),该结构包含对象分配标记、类型标志和一个指向特定类型销毁例程的指针。当对象的锁计数达到零时,窗口管理器将调用特定于类型的销毁例程来正确释放对象。

临界区。与NT执行器管理的对象不同,窗口管理器不独占锁定每个用户对象。相反,它在win32k中使用临界区(资源)为每个会话实现一个全局锁。每个对用户对象或用户管理结构(通常是NtUser系统调用)进行操作的内核例程必须首先进入用户临界区获取资源(即获取win32k!gpresUser资源)。例如,更新内核模式结构的函数必须首先调用UserEnterUserCritSec,并在修改数据之前获取用于独占访问的用户资源。为了减少窗口管理器中锁争用的数量,只执行读操作的系统调用可以进入共享临界区(EnterSharedCrit)。这允许win32k在全局锁设计的情况下实现某种并行性,因为多个线程可能同时执行NtUser调用。

1.3 用户模式回调( User-Mode Callbacks)

​ Win32k需要多次使用用户模式回调来执行任务,比如调用应用程序定义钩子,提供事件通知,将数据从用户模式复制到别处。该机制本身是在KeUserModeCallback中实现的,由NT执行体导出,其操作非常类似于逆向系统调用。

NTSTATUS KeUserModeCallback (
IN ULONG ApiNumber,
IN PVOID InputBuffer,
IN ULONG InputLength,
OUT PVOID *OutputBuffer,
IN PULONG OutputLength );

​ 当win32k执行一个用户模式回调时,它会使用它想要调用的用户模式函数的ApiNumber来调用KeUserModeCallback。在初始化USER32.dll的过程中,ApiNumber是函数指针表(USER32 ! apfnDispatch)的索引,在通过该索引得到函数地址后将地址复制到进程环境块中的回调表中(PEB.KernelCallbackTable)。Win32k提供了相应的回调函数的输入参数通过填充InputBuffer OutputBuffer接收用户模式的输出。如下为:用户模式回调函数在USER32.dll中的分布

0:004> dps poi($peb+58)
00000000‘77b49500 00000000‘77ac6f74 USER32!_fnCOPYDATA
00000000‘77b49508 00000000‘77b0f760 USER32!_fnCOPYGLOBALDATA
00000000‘77b49510 00000000‘77ad67fc USER32!_fnDWORD
00000000‘77b49518 00000000‘77accb7c USER32!_fnNCDESTROY
00000000‘77b49520 00000000‘77adf470 USER32!_fnDWORDOPTINLPMSG
00000000‘77b49528 00000000‘77b0f878 USER32!_fnINOUTDRAG
00000000‘77b49530 00000000‘77ae85a0 USER32!_fnGETTEXTLENGTHS
00000000‘77b49538 00000000‘77b0fb9c USER32!_fnINCNTOUTSTRING
...

​ 在调用系统调用时,nt!KiSystemService或nt !KiFastCallEntry在内核线程堆栈上存储一个陷阱框架来保存当前线程上下文,并且能够在返回到用户模式时恢复寄存器。为了在用户模式回调中转换回用户模式,KeUserModeCallback首先使用线程对象持有的陷阱帧信息将输入缓冲区复制到用户模式堆栈,然后它创建一个新的陷阱框架并将EIP设置为ntdll!KiUserCallbackDispatcher,然后替换线程对象的陷阱框架指针,最后调用nt!KiServiceExit将执行返回给用户模式回调调度器。

​ 由于用户模式回调需要存储线程状态信息(如陷阱帧)的位置,Windows XP和2003将增加内核堆栈,以确保有足够的空间可用。因为通过递归调用回调,堆栈空间很快就会被耗尽,所以Vista和Windows 7在每个用户模式回调上创建一个新的内核线程堆栈。为了跟踪以前的堆栈等等,Windows在堆栈的底部为KSTACK区域结构保留了空间,后面是伪造的陷阱帧。

kd> dt nt!_KSTACK_AREA
+0x000 FnArea : _FNSAVE_FORMAT
+0x000 NpxFrame : _FXSAVE_FORMAT
+0x1e0 StackControl : _KERNEL_STACK_CONTROL
+0x1fc Cr0NpxState : Uint4B
+0x200 Padding : [4] Uint4B
kd> dt nt!_KERNEL_STACK_CONTROL -b
+0x000 PreviousTrapFrame : Ptr32
+0x000 PreviousExceptionList : Ptr32
+0x004 StackControlFlags : Uint4B
+0x004 PreviousLargeStack : Pos 0, 1 Bit
+0x004 PreviousSegmentsPresent : Pos 1, 1 Bit
+0x004 ExpandCalloutStack : Pos 2, 1 Bit
+0x008 Previous : _KERNEL_STACK_SEGMENT
+0x000 StackBase : Uint4B
+0x004 StackLimit : Uint4B
+0x008 KernelStack : Uint4B
+0x00c InitialStack : Uint4B
+0x010 ActualLimit : Uint4B

​ 一旦用户模式回调完成,它就会调用NtCallbackReturn来继续在内核中执行。该函数将回调的结果复制回原始内核堆栈,并使用内核堆栈控制结构中包含的信息恢复原始陷阱帧(PreviousTrapFrame)和内核堆栈。在跳转到之前停止的位置(nt!KiCallUserMode)之前,内核回调堆栈被删除。

NTSTATUS NtCallbackReturn (
IN PVOID Result OPTIONAL,
IN ULONG ResultLength,
IN NTSTATUS Status );

​ 递归或嵌套的回调可能导致内核堆栈增长无限(XP)或创建任意数量的堆栈,内核跟踪回调的深度保存在每个正在运行的线程在线程对象结构(KTHREAD - > CallbackDepth)中。在每次回调时,已经在线程堆栈上使用的字节(堆栈基堆栈指针)被添加到CallbackDepth变量中。每当内核试图迁移到新的堆栈时,nt!KiMigrateToNewKernelStack确保总回调深度永远不会超过0xC000字节,否则将返回状态堆栈溢出错误代码。

2. 通过用户模式回调完成内核攻击

​ 在本节中,我们将介绍几个攻击手法,它们可能允许对手通过用户模式回调执行特权升级攻击。我们首先查看用户模式回调如何处理用户临界区,然后再更详细地讨论每个攻击手法。

2.1 Win32k 命名约定

​ 如第1.2节所述,窗口管理器在内部管理结构上操作时使用关键部分和全局锁。由于用户模式回调可能会使应用程序冻结GUI子系统,所以win32k总是在回调到用户模式之前离开临界区。这样在执行用户模式代码时,win32k可以执行其他任务。从回调函数返回后,win32k在函数在内核中继续执行之前重新进入临界区。我们可以在任何调用KeUserModeCallback的函数中观察这种行为。如下所示

call _UserSessionSwitchLeaveCrit@0
lea eax, [ebp+var_4]
push eax
lea eax, [ebp+var_8]
push eax
push 0
push 0
push 43h
call ds:__imp__KeUserModeCallback@20 ;离开临界区
call _UserEnterUserCritSec@0 ;回到临界区

​ 从用户模式回调返回时,win32k必须确保引用的对象和数据结构仍然处于异常状态。由于在输入回调之前保留了关键部分,所以用户模式代码可以自由地更改对象的属性、或重新分配数组等等。例如,回调可以调用SetParent()来更改窗口的父级。比如果内核在调用回调之前存储对父类的引用,并且在返回后继续对其进行操作,而没有执行适当的检查或对象锁定,那么这可能会导致安全漏洞。

​ 由于跟踪可能会调用回用户模式的函数非常重要(以便开发人员采取必要的预防措施),所以win32ksys使用自己的函数命名约定。特别是,函数的前缀xxx或zzz取决于函数调用用户模式回调的方式。在大多数情况下,前缀xxx的函数将离开临界区并调用用户模式回调。但是,在某些情况下,函数可能需要一组特定的参数,以便分支到实际调用回调的路径。有时会看到非前缀函数调用xxx函数,但它们提供给xxx函数的参数永远不会导致回调。

​ 前缀为zzz的函数调用异步或延迟回调。这通常是某些类型的窗口事件的情况,由于各种原因不能或不应该立即处理。在本例中,win32k调用xxxFlushDeferredWindowEvents来刷新事件队列。关于zzz函数需要注意的一件重要事情是,在调用xxxWindowEvent之前,win32k!gdwDeferWinEvent不能为空。否则需要立即处理回调。

​ win32k使用的命名约定的问题是缺乏一致性。win32k中有几个函数调用回调,但是没有标记回调。原因尚不清楚,但一种可能的解释是,随着时间的推移函数已经被修改,但没有对函数名进行必要的更新。因此,开发人员可能会认为一个函数可能永远不会真正调用回调,从而避免进行看似不必要的验证(例如,对象保持未锁定,指针没有重新验证)。在解决MS11-034[7]的漏洞时,更新了几个函数名,以正确反映它们对用户模式回调的使用(下表)。

在这里插入图片描述

2.2 用户对象锁定

​ 正如在1.2节中解释的,用户对象实现引用计数来跟踪对象何时被使用,何时应该从内存中释放。因此,在内核离开用户临界区之后,预期有效的对象必须被锁定。通常,锁有两种形式——线程锁和赋值锁。

线程锁定。线程锁定通常用于锁定函数中的对象或缓冲区。每个线程锁定条目都存储在一个线程锁结构(win32k!TL)中,该结构位于一个单链表式的线程锁列表中,由线程信息结构(THREADINFO.ptl)指向。线程锁列表的操作非常类似于FIFO队列,其中条目被推入并弹出列表。在win32k中线程锁定通常是内联执行,并且可以被内联指针增量,通常在xxx函数中。当一个给定的函数在win32k不再需要的对象或缓冲区,它调用ThreadUnlock()来删除锁定线程锁列表条目。

​ 如果对象已被锁定但未被正确解锁(例如,由于处理用户模式回调时的进程终止),win32k将处理线程锁列表,以释放线程终止时的所有剩余条目。可以在xxxDestroyThreadInfo函数调用DestroyThreadsObjects时观察到。

mov ecx, _gptiCurrent
add ecx, tagTHREADINFO.ptl ; 线程信息结构
mov edx, [ecx]				;取出当前线程信息结构中的线程锁结构
mov [ebp+tl.next], edx	;保存当前的线程信息结构中的线程锁到线程锁列表的Next指针
lea edx, [ebp+tl]		;取出当前线程的锁
mov [ecx], edx ; push new entry on list 将当前线程的锁加入到线程锁列表
mov [ebp+tl.pobj], eax ; window object
inc [eax+tagWND.head.cLockObj]
push [ebp+arg_8]
push [ebp+arg_4]
push eax
call _xxxDragDetect@12 ; xxxDragDetect(x,x,x)
mov esi, eax
call _ThreadUnlock1@0 ; ThreadUnlock1()

赋值锁定。与线程锁不同,赋值锁用于更长期的用户对象锁定。例如,当在桌面中创建一个窗口时,win32k赋值锁会将桌面对象锁定在窗口对象结构中适当的偏移位置。赋值锁定项不是对列表进行操作,而是简单地将指针(指向锁定的对象)存储在内存中。在win32k需要分配锁定对象的地方已经存在一个指针,模块在锁定之前解锁现有的条目,并将其替换为所请求的条目。

​ 句柄管理器提供了赋值锁定和解锁的功能。在锁定一个对象时,win32k调用HMAssignmentLock(Address, object)和类似的HMAssignmentUnlock(Address)来释放对象引用。值得注意的是,赋值锁定并不像线程锁定那样提供安全网。如果一个线程在回调中终止,线程或用户对象清理例程本身负责单独释放这些引用。如果不这样可能会导致内存泄漏,或者如果操作可以任意重复,引用计数可能会溢出。

窗口对象 Use-After-Free (CVE-2011-1237) 在安装基于计算机的训练(CBT)钩子时,应用程序可能会收到关于窗口处理、键盘和鼠标输入以及消息队列处理的各种通知。例如,在创建新窗口之前,HCBT CREATEWND回调允许应用程序使用提供的CBT CREATEWND5结构检查和修改用于确定窗口大小和方向的参数。这个结构还允许应用程序选择窗口的z-order链,方法是提供窗口的句柄,在此之后将插入新窗口(hwndInsertAfter)。在设置这个句柄时,xxxCreateWindowEx获得相应的对象指针,然后在将新窗口链接到z-order链中时使用它。但是,由于函数未能正确锁定该指针,攻击者可能会在随后的回调中破坏hwndInsertAfter中提供的窗口,并强制win32k在返回时对释放的内存进行操作。

​ 在下面的代码中,xxxCreateWindowEx调用PWInsertAfter来获取在CBT CREATEWND钩子结构中提供的hwndInsertAfter句柄的窗口对象指针(使用HMValidateHandleNoSecure)。然后该函数将对象指针存储在一个本地变量中。

.text:BF892EA1 push [ebp+cbt.hwndInsertAfter]
.text:BF892EA4 call _PWInsertAfter@4 ; PWInsertAfter(x)
.text:BF892EA9 mov [ebp+pwndInsertAfter], eax ; window object

​ 由于win32k没有锁定pwndInsertAfter,攻击者可以在随后的回调中释放CBT钩子中提供的窗口(例如,通过调用DestroyWindow)。在下面代码函数的末尾,xxxCreateWindowEx使用窗口对象指针并试图将其链接到窗口z-order链(通过LinkWindow)。由于窗口对象可能不再存在,这就变成了use-after - free漏洞,可能允许攻击者在内核上下文中执行任意代码。

.text:BF893924 push esi ; parent window
.text:BF893925 push [ebp+pwndInsertAfter]
.text:BF893928 push ebx ; new window
.text:BF893929 call _LinkWindow@12 ; LinkWindow(x,x,x)

键盘布局对象Use-After-Free(CVE-2011-1241) 键盘布局对象用于设置线程或进程的活动键盘布局。在加载键盘布局时,应用程序调用LoadKeyboardLayout并指定要加载的输入本地标识符的名称。Windows还提供了未文档化的LoadKeyboardLayoutEx函数,该函数接受一个额外的键盘布局句柄参数,win32k在加载新布局之前首先尝试卸载该参数。在提供此句柄时,win32k未能锁定相应的键盘布局对象。因此,攻击者可以在用户模式回调中卸载所提供的键盘布局,并触发一个use-after-free条件。

​ 在下面代码中,LoadKeyboardLayoutEx获取键盘布局的句柄来首次卸载,并调用HKLToPKL来获取键盘布局对象指针。HKLtoPKL遍历活动键盘布局列表(THREADINFO.spklActive),直到找到与提供的句柄匹配的那个。LoadKeyboardLayoutEx然后将对象指针存储在堆栈上的一个本地变量中。

text:BF8150C7 push [ebp+hkl]
.text:BF8150CA push edi
.text:BF8150CB call _HKLtoPKL@8 ; get keyboard layout object
.text:BF8150D0 mov ebx, eax
.text:BF8150D2 mov [ebp+pkl], ebx ; store pointer

由于LoadKeyboardLayoutEx没有充分锁定键盘布局对象指针,攻击者可以在用户模式回调中卸载键盘布局,从而释放该对象。这个函数后来被称为xxxClientGetCharsetInfo,它可以从用户模式中检索字符集信息。在下面代码中,LoadKeyboardLayoutEx继续使用之前存储的键盘布局对象指针,因此可以在释放的内存上操作。

text:BF8153FC mov ebx, [ebp+pkl] ; KL object pointer
.text:BF81541D mov eax, [edi+tagTHREADINFO.ptl]
.text:BF815423 mov [ebp+tl.next], eax
.text:BF815426 lea eax, [ebp+tl]
.text:BF815429 push ebx
.text:BF81542A mov [edi+tagTHREADINFO.ptl], eax
.text:BF815430 inc [ebx+tagKL.head.cLockObj] ; freed memory ?

2.3 对象状态的验证

​ 为了跟踪对象是如何使用的,win32k将几个标志以及指针与用户对象相关联。假定对象处于某种状态,应该始终验证其状态。用户模式回调可能会更改对象的状态和更新对象的属性,例如更改窗口的父级,导致下拉菜单不再处于活动状态,或者终止DDE对话中的合作伙伴。缺少状态检查可能导致缺陷如空指针解引用和use-after-frees,取决于win32k使用对象。

​ 动态数据交换(DDE)协议是一种遗留协议,它使用消息和共享内存在应用程序之间交换数据。DDE对话在内部由窗口管理器表示为DDE对话对象,该对象是为发送方和接收方定义的。为了跟踪对象与哪些对象以及与哪些对象进行对话,对话对象结构(未公开)持有指向对方对话对象的指针(使用赋值锁定)。如果拥有对话对象的窗口或线程终止,则其在合作伙伴对象中的赋值锁定指针被解锁(清除)。

DDE会话状态漏洞。当DDE对话以用户模式存储数据时,它们依赖于用户模式回调来在用户模式之间来回复制数据。发送DDE消息后,win32k调用xxxCopyDdeIn从用户模式复制数据。类似地,在接收DDE消息时,win32k调用xxxCopyDDEOut将数据复制回用户模式。如果操作已经发生,win32k可能会通知合作伙伴对话对象对数据进行操作,例如,它期望得到响应。

​ 处理用户模式后回调复制数据或从用户模式,几个函数未能正确地重新验证对方的谈话对象。攻击者可以在用户模式回调中终止会话,从而从发送方或接收方的对象结构中释放伙伴会话对象。在下面代码中,我们可以看到在xxxCopyDdeIn中可以调用回调,但是该函数在将对话对象指针传递给AnticapatePost之前没有重新验证它。这进而导致一个空指针解引用,并允许攻击者在映射空页时控制会话对象。

.text:BF8FB8A7 push eax
.text:BF8FB8A8 push dword ptr [edi]
.text:BF8FB8AA call _xxxCopyDdeIn@16
.text:BF8FB8AF mov ebx, eax
.text:BF8FB8B1 cmp ebx, 2
.text:BF8FB8B4 jnz short loc_BF8FB8FC
.text:BF8FB8C5 push 0 ; int
.text:BF8FB8C7 push [ebp+arg_4] ; int
.text:BF8FB8CA push offset _xxxExecuteAck@12
.text:BF8FB8CF push dword ptr [esi+10h] ; conversation object
.text:BF8FB8D2 call _AnticipatePost@24

菜单状态处理漏洞。菜单管理是win32k中最复杂的组件之一,其中包含了可以追溯到现代Windows操作系统早期的未知代码。虽然菜单对象(tagMENU)本身非常简单,只包含与实际菜单项相关的信息,但是整个菜单处理依赖于多个相当复杂的函数和结构。例如,在创建弹出菜单时,应用程序调用TrackPopupMenuEx6来创建一个菜单类窗口,其中显示菜单内容。然后,菜单窗口通过系统定义的菜单窗口类过程(win32k!xxxMenuWindowProc)处理消息输入,以处理各种特定于菜单的消息。此外,为了跟踪菜单是如何使用的,win32k还将菜单状态结构(tagMENUSTATE)与当前活动的菜单相关联。这样,函数就可以知道一个菜单是否包含在一个拖放操作中,在一个菜单循环中,或者将要终止,等等。

​ 在处理各种类型的菜单消息时,win32k在用户模式回调后没有正确验证菜单。具体来说,在处理最后一个菜单消息时调用回调函数(例如,通过向菜单窗口类过程发送MN ENDMENU消息),win32k在很多情况下无法正确检查菜单状态是否仍然处于活动状态,或者相关结构(如弹出菜单结构(win32k!tagPOPUPMENU))引用的对象指针是否为非空。在下面代码中,win32k尝试通过调用xxxHandleMenuMessages来处理某些类型的菜单消息。由于此函数可能会调用回调,因此后续使用菜单状态指针(ESI)将导致win32k对释放的内存进行操作。这种特殊情况可以通过使用tagMENUSTATE结构(未公开)的dwLockCount变量锁定菜单状态来避免。

push [esi+tagMENUSTATE.pGlobalPopupMenu]
or [esi+tagMENUSTATE._bf4], 200h ; fInCallHandleMenuMessages
push esi
lea eax, [ebp+var_1C]
push eax
mov [ebp+var_C], edi
mov [ebp+var_8], edi
call _xxxHandleMenuMessages@12 ; xxxHandleMenuMessages(x,x,x)
and [esi+tagMENUSTATE._bf4], 0FFFFFDFFh ; <-- may have been freed
mov ebx, eax
mov eax, [esi+tagMENUSTATE._bf4]
cmp ebx, edi
jz short loc_BF968B0B ; message processed?

2.4 缓冲区重新分配

​ 许多用户对象都有与之相关联的项数组或其他形式的缓冲区。添加或删除元素的项数组通常会调整缓冲区大小以保存内存。例如,如果元素的数量高于或低于某个阈值,就会以更合适的大小重新分配缓冲区。类似地,如果数组被清空,缓冲区就被释放。重要的是,在回调期间可以重新分配或释放的缓冲区必须在返回时重新检查(如下图)。任何没有做到这一点的函数都可能在释放的内存上操作,因此允许攻击者控制赋值锁定指针或破坏后续分配的内存。

在这里插入图片描述

菜单项数组Use-After-Free。为了跟踪由弹出菜单或下拉菜单保存的菜单项,菜单对象(win32k!tagMENU)定义一个指向菜单项数组的指针(rgItems)。每个菜单项(win32k!tagITEM)定义属性,如显示的文本字符串、嵌入的图像、指向子菜单的指针等。菜单对象结构跟踪cItems数组变量中包含的项的数量,以及在已调用的变量中分配的缓冲区中可以容纳的项的数量。在从菜单项数组中添加或删除元素时,例如通过调用InsertMenuItem()或DeleteMenu(),如果win32k注意到cAlloced将小于cItems(如下图所示),或者cItems和cAllocated之间的差异超过8个项,它会尝试调整数组的大小。
在这里插入图片描述

​ 在用户模式回调之后,win32k中的几个函数没有充分验证菜单项数组缓冲区。没有办法“锁定”菜单项,例如用户对象,任何可以调用回调的函数都需要重新验证菜单项数组。这也适用于将菜单项作为参数的函数。如果菜单项数组缓冲区在用户模式回调中重新分配,则后续代码可以在已释放的内存或由攻击者控制的数据上操作。

​ SetMenuInfo函数允许应用程序设置指定菜单的各种属性。在设置所提供的菜单信息结构(MENUINFO)中的MIM APPLYTOSUBMENUS标志掩码值时,win32k还将更新应用于菜单的所有子菜单。这种行为可以在xxxSetMenuInfo中观察到,因为该函数遍历每个菜单项条目并递归处理每个子菜单以传播更新的设置。在处理菜单项数组并进行递归调用之前,xxxSetMenuInfo在本地变量/寄存器中存储菜单项(cItems)的数量以及菜单项数组指针(rgItems)(如以下代码所示)。

.text:BF89C779 mov eax, [esi+tagMENU.cItems]
.text:BF89C77C mov ebx, [esi+tagMENU.rgItems]
.text:BF89C77F mov [ebp+cItems], eax
.text:BF89C782 cmp eax, edx
.text:BF89C784 jz short loc_BF89C7CC

​ 一旦xxxSetMenuInfo到达最里面的菜单,递归就会停止,条目就会被处理。此时,函数可能在调用xxxMNUpdateShownMenu时调用一个用户模式回调,因此可能允许调整菜单项数组的大小。但是,当xxxMNUpdateShownMenu返回时,当从递归调用返回时,xxxSetMenuInfo无法充分验证菜单项数组缓冲区以及数组所包含的项的数量。如果攻击者在xxxMNUpdateShownMenu调用的回调中调用InsertMenuItem()或DeleteMenu()来调整菜单项数组的大小,下面代码中的ebx可能指向释放的内存。此外,由于cItems反映了在调用函数时数组所包含的元素的数量,xxxSetMenuInfo可以对分配的数组之外的项进行操作。

.text:BF89C786 add ebx, tagITEM.spSubMenu
.text:BF89C789 mov eax, [ebx] ; spSubMenu
.text:BF89C78B dec [ebp+cItems]
.text:BF89C78E cmp eax, edx
.text:BF89C790 jz short loc_BF89C7C4
...
.text:BF89C7B2 push edi
.text:BF89C7B3 push dword ptr [ebx]
.text:BF89C7B5 call _xxxSetMenuInfo@8 ; xxxSetMenuInfo(x,x)
.text:BF89C7BA call _ThreadUnlock1@0 ; ThreadUnlock1()
.text:BF89C7BF xor ecx, ecx
.text:BF89C7C1 inc ecx
.text:BF89C7C2 xor edx, edx
...
.text:BF89C7C4 add ebx, 6Ch ; next menu item
.text:BF89C7C7 cmp [ebp+cItems], edx ; more items ?
.text:BF89C7CA jnz short loc_BF89C789

​ 为了解决涉及菜单项处理的漏洞,微软在win32k中引入了新的MNGetpItemFromIndex函数。该函数将菜单对象指针和请求的菜单项索引作为参数,并根据菜单对象中提供的信息返回项。

SetWindowPos 数组 Use-After-Free。Windows允许应用程序延迟窗口位置更新,以便多个窗口可以同时更新。为此,Windows使用一个特殊的SetWindowsPos对象,该对象持有一个指向窗口位置结构数组的指针。当应用程序调用BeginDeferWindowPos()时,将初始化SetWindowPos对象和这个数组。BeginDeferWindowPos()接受数组元素(窗口位置结构)的数量进行预分配。然后通过调用DeferWindowPos()来延迟窗口位置更新,其中填充下一个可用的位置结构。如果请求的延迟更新数量超过预先分配的条目数量,则win32k会以更合适的大小(4个附加条目)重新分配数组。一旦所有请求的窗口位置更新都被延迟,应用程序就会调用EndDeferWindowPos()来处理要更新的窗口列表。

​ 在对SMWP数组进行操作时,win32k并不总是在用户模式回调之后正确地验证数组指针。在调用EndDeferWindowPos来处理多个窗口位置结构时,win32k调用xxxCalcValidRects来计算SMWP数组中引用的每个窗口的位置和大小。该函数遍历每个条目并执行各种操作,例如通知每个窗口其位置正在更改(WM WINDOWPOSCHANGING)。由于此消息可能会调用用户模式回调,因此攻击者可能会对同一个SWP对象进行多个DeferWindowPos调用,从而导致SMWP要重新分配的数组(如以下代码)。xxxCalcValidRects将窗口句柄写回原始缓冲区,造成use after free。

.text:BF8A37B8 mov ebx, [esi+14h] ; SMWP array
.text:BF8A37BB mov [ebp+var_20], 1
.text:BF8A37C2 mov [ebp+cItems], eax ; SMWP array count
.text:BF8A37C5 js loc_BF8A3DE3 ; exit if no entries
...
.text:BF8A3839 push ebx
.text:BF8A383A push eax
.text:BF8A383B push WM_WINDOWPOSCHANGING
.text:BF8A383D push esi
.text:BF8A383E call _xxxSendMessage@16 ; user-mode callback
.text:BF8A3843 mov eax, [ebx+4]
.text:BF8A3846 mov [ebx], edi ; window handle
...
.text:BF8A3DD7 add ebx, 60h ; get next entry
.text:BF8A3DDA dec [ebp+cItems] ; decrement cItems
.text:BF8A3DDD jns loc_BF8A37CB

​ 与菜单项不同,涉及SMWP数组处理的漏洞是通过不允许在处理SMWP数组时重新分配缓冲区来解决。在win32kDeferWindowPos中可以看到,该函数现在检查一个“进行处理”的标志,只有在不导致缓冲区重新分配的情况下,才允许添加条目。

明日计划

继续学习内核漏洞知识

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章