CVE-2015-0057分析——从user-mode callback attack到write-what-where

从win32k说起

在Windows NT 4.0后,window manager和GDI(Graphic Device Interface)的实现从CSRSS迁移到了内核模式中,来提高图形渲染性能并减少内存需求,而实现它们的内核模块就是win32k.sys。其中,window manager负责管理windows用户接口,例如控制窗口显示,屏幕输出,收集鼠标和键盘的输入并传递消息到应用程序。GDI主要关心图形渲染和GDI对象的实现,包括图形渲染引擎,对打印的支持,颜色匹配,浮点数数学库,对字体的支持等。

win32k注册了一些callouts来面向desktop和windows stations等GUI对象,以及为线程和进程注册callouts来定义GUI子系统使用的每个线程与进程的结构。

GUI Thread and Processes

普通线程在进行USER或GDI的系统调用时会提升到GUI线程(nt!PsConvertToGuiThread),当一个进程的第一个线程转换到GUI线程并调用W32pProcessCallout时,win32k调用win32k!xxxInitProcessInfo来初始化每一个进程的W32PROCESS/PROCESSINFO结构。它维护GDI的相关信息,如连接的桌面,窗口位置,GDI句柄数量等。这个结构在xxxUserProcessCallout初始化User相关字段之前由win32k!AllocateW32Process分配,紧跟着的是GdiProcessCallout,它初始化GDI相关字段。

类似的每个GUI线程有W32THREAD/THREADINFO结构,它维护GUI子系统的相关信息,如消息队列,已注册的windows hook,菜单状态等。W32pThreadCallout调用win32k!AllocateW32Thread来申请结构,紧接着的是GdiThreadCallout和UserThreadCallout来分别初始化GDI和USER子系统信息。

Windows Manager

windows manager的一个重要功能是追踪用户实体,如窗口,菜单,光标等。它通过代表对应实体的user object并维护一张句柄表来追踪它们使用的user session。这样,一旦应用在用户实体上请求一个动作,它就会提供句柄值,然后句柄管理器会将其映射到相应的内核对象中。

user object都有各自的类型,比如window object是win32k!tagWND结构,菜单是win32k!tagMENU结构。尽管对象的类型不通,但它们都有一个共同的头部,称为HEAD结构。HEAD结构具有句柄值的副本和lock count,每当它被使用,这个lock count就会增加,当某个特定组件不再使用该对象时,该count就会减少,当减少到0时,windows manager就会销毁它,即它本质上是一个引用计数对象。

所有user object都被索引到一个per-session handle table中。handle table被win32k!Win32UserInitialize初始化,一旦加载新的win32k实例,该函数就会被调用,并将该table设置到共享区段(win32k!gpvSharedBase)。这个共享区段会映射到所有GUI进程中,因此用户模式的进程不需要经过系统调用就能访问该表。

typedef struct _HEAD {
	HANDLE h;
	ULONG32 cLockObj;
}HEAD;

虽然这个HEAD很小,但很多时候会使用更大的线程或进程头结构,比如THRDESKHEAD和PROCDESKHEAD。这些结构提供了附加字段比如指向线程信息的tagTHREADINFO和指向相关的desktop object(tagDESKTOP)。通过提供这些信息,Windows可以限制对其他桌面对象的访问,从而在桌面之间提供隔离。类似的,由于对象总是由线程或进程拥有,所以可以实现共存于同一桌面的线程或进程之间的隔离。

User-Mode Callback

win32k通过调用KeUserModeCallback来进行用户模式回调。

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

其中,ApiNumber是USER32!apfnDispatch表中的索引,该表在USER32.dll初始化时从PEB.KernelCallbackTable中复制而来。

在进行系统调用时,nt!KiSystemService或nt!KiFastCallEntry在内核线程栈上存储一个TRAP_FRAME来保存当前线程上下文,并在返回用户模式时恢复寄存器。

在user-mode callback中,为了转换到用户模式,KeUserModeCallback首先通过TRAP_FRAME将input buffer复制到user-mode栈上,然后创建一个新的TRAP_FRAME并将EIP设置为ntdll!KiUserCallbackDispatcher,来替换thread object的TrapFrame指针,最后调用Nt!KiServiceExit返回到用户模式回调分配器。

一旦用户模式回调完成了,就会调用NtCallbackReturn来恢复内核执行。该函数将callback的结果复制到原始的内核栈中并恢复原始的TRAP_FRAME,还通过KERNEL_STACK_CONTROL结构来恢复内核栈。

user-mode callback attack

CVE-2015-0057分析——从user-mode callback attack到write-what-where_第1张图片
这里是本次漏洞的触发点,此处会进行user-mode callback,但之后却并未检验对象的有效性就直接使用,若我们对其调用的用户模式函数进行hook,然后释放内核中使用的结构,就会导致一个UAF
CVE-2015-0057分析——从user-mode callback attack到write-what-where_第2张图片

虽然只是操作其成员字段的两个bit,但通过精心构造后续结构,可以导致一个基本的缓冲区溢出。

那么,如何找到需要的用户模式函数呢?经过调试可以发现,在xxxDrawScrollBar中调用的user mode函数有很多,那么 如何选择就成了一个问题,在我参考的文章中,都是对_ClientLoadLibrary这个函数进行hook。

_ClientLoadLibary是win32k.sys用于将uxtheme.dll注入到正在运行的进程,允许操作系统将可视化样式应用于程序中,然后调用其ThemeInitApiHook函数,使uxtheme.dll有机会为user32使用的各种函数提供替代实现。一般来说,拦截SetWindowsHookEx这样的全局消息钩子的一种方法就是hook _ClientLoadLibrary函数,因为它用于将对应DLL注入到窗口中。

利用思路

试想改变两个bit能做到什么,通常想法是增加缓冲区大小,或者减少引用计数对象的count。在tagWND中有一个PropList的字段,其结构如下

typedef struct tagPROPLIST{
    UINT cEntries;
    UINT iFirstFree;
    PROP props[1];
}PROPLIST;

typedef struct tagPROP{
    HANDLE hData;
    ATOM atomKey;
    WORD fs;
}PROP;

其中cEntries表示可变数组props的大小,而iFirstFree表示目前用到了多少。我们可以通过SetPropA函数来设置tagWND的PropList字段,若该属性已经设置过,则直接修改其数据,若未设置过,则在数组中添加一个条目;若添加条目时发现,cEntries和iFirstFree相等,则表示props数组已满,此时会重新分配堆空间,并将原来的数据复制进去。如果我们利用UAF增大cEntries的值,在数组已满的情况下,再次调用SetPropA函数,就会导致缓冲区溢出。

进一步利用

我们可以考虑最终使用menu来进行任意地址写的操作。SetMenuItemInfoA函数可以对MENU.rgItems位置进行写操作,一旦这个位置可控,则能完成任意地址写。

首先来考察我们在用户态能使用的用于分配Desktop Heap的接口有哪些

  • CreateWindowEx。创建tagWND,最基本的接口。Desktop Heap的分配都需要依靠window来进行
  • SetPropA。设置tagWND中的PropList结构,当属性标识符不存在时,向Prop数组中添加一个;当存在时,仅仅修改其数据
  • CreateMenu。创建一个menu,包含堆头,该结构在desktop heap中占0xa0个字节。
  • NtUserDefSetText,设置tagWND的strName字段,由于调用该函数时会分配对应字符数的desktop heap空间,所以可做任意大小desktop heap分配的接口。

在触发UAF后,我们新创建的的PropList的cEntries会被增大到0xe,此时,若我们再次调用SetPropA,则可以进行缓冲区溢出。这样的缓冲区溢出是在desktop heap中进行的,但由于cEntries的限制,我们能覆盖到的空间有限,且并非连续覆盖,而是每16字节仅有前8字节可以稳定覆盖。

而我们的目标是自定义一个可用的menu,如何将8字节的缓冲区溢出转变为一个可控的menu呢?这里我们可以覆盖下一个堆块的头部,由于非空闲堆块前8字节会被数据填充,所以8字节溢出可以覆盖到size处(需要bypass heap cookie),这将导致一个overlay,若我们能在下一个堆块之后放置一个menu,在overlay并free后,该menu的句柄是不会变的,这本质上再次构造了一个UAF。为此,我们希望堆结构如下:

------------
  PropList
------------
  something
------------
    Menu
------------

这样,当覆盖了something的堆头后,将其free掉,然后重新分配大小为sizeof(something)+sizeof(Menu)的堆块,很容易覆盖到该位置,此时的Menu就是我们自定义的了。

heap feng shui

所以我们需要构造这样的堆布局,但布局这件事是先确定一个轮廓,然后在内部进行细微调整的,所以我们来考察这三个东西一开始应该是什么。首先需要声明的是,以下结构的分配均在desktop heap中,这是布局的先决条件。

以上布局中,PropList是触发了UAF之后,所以在这之前它应该是tagSBInfo,不过该tagSBInfo总是伴随着CreateWindowEx函数一起生成的,不太好单独分配。Khalife的exp给出的一种方法是,我们可以先将此处用PropList占位,然后Destroy相应的window,这导致该PropList的释放,此时开始分配带有tagSBInfo的tagWindow,就有可能将tagSBInfo分配到对应的堆布局中,这里有个细节问题,即我们需要调控PropList的大小使其与tagSBInfo大小一致。当然最初的PropList和最终的PropList完全不是一个东西,因为最终的PropList是触发了UAF的结果,其cEntries比实际要大,用于进行缓冲区溢出。

something这个东西就是个工具堆块,我们只需要为其分配合适的大小即可,为了方便控制堆块大小,可以用NtUserDefSetText来进行分配。

Menu这个结构比较关键,我在调试时不太清楚如何通过其句柄来获取其内核地址,不过可以监视堆的内存情况,当调用CreateMenu函数时,堆中就会初始化一块内存,仔细观察就会发现其大小为0xa0(包括堆头)

在我们的堆喷中,理想情况下,堆分布的情况如下,strName1用于隔离…

PropList 0x30
strName0  0x100
tagMenu   0xa0
strName1  0x100

PropList 0x30
strName0 0x100
tagMenu  0xa0
strName1 0x100

PropList 0x30
tagMenu	 0xa0

PropList 0x30
tagMenu	 0xa0
······
PropList 0x30
tagMenu	 0xa0

PropList 0x30
tagMenu	 0xa0

PropList 0x30
strName0 0x100	
tagMenu  0xa0
strName1 0x100

PropList 0x30
tagMenu	 0xa0

PropList 0x30
tagMenu	 0xa0

而由于PropList所需空间较小,所以可能被分配到其他内存碎片中,以至于出现以下情况

PropList 0x30
tagMenu  0xa0
tagMenu  0xa0
PropList 0x30
tagMenu  0xa0

然后我们将以下这种块的PropList释放,这会导致一个大小为0x30的空洞,供tagSBInfo分配

PropList 0x30
strName0 0x100	
tagMenu  0xa0
strName1 0x100

在CreateWindowEx函数调用后,tagWnd对应的tagSBInfo被分配到这样的结构中
CVE-2015-0057分析——从user-mode callback attack到write-what-where_第3张图片

可以看到tagSBInfo刚好被分配到了之前释放的大小为0x30的PropList中。继续执行,使得hook函数触发UAF,新创建PropList,然后在内核层xxxEnable中,其cEntries字段由于异或而增大,导致以下的情况
CVE-2015-0057分析——从user-mode callback attack到write-what-where_第4张图片
可以看到这里的第一个字节是0x0e了,而它的数组依然只有2个元素,这之后再调用SetPropA就会溢出了,如下
CVE-2015-0057分析——从user-mode callback attack到write-what-where_第5张图片

可以看到以0xfffff90140b06798开始的下一个堆块头已经被改变了,使其大小覆盖到之后的Menu处,然后新创建一个Menu,这就是我们自定义的Menu,其rgItems字段的值为HalDispatchTable+4
CVE-2015-0057分析——从user-mode callback attack到write-what-where_第6张图片
之后我们将strName0对应的tagWND销毁,这将导致之后的Menu一并释放,然后用NtUserDefText新申请同样大小的堆块,则我们自定义的Menu就会放入
CVE-2015-0057分析——从user-mode callback attack到write-what-where_第7张图片在这里插入图片描述

至此,由于Menu本来的句柄是不变的,我们可以用该句柄进行任意地址写,当然,由于这是win8的利用,所以需要绕过SMEP保护,即我们不能直接将处于ring3的shellcode地址写入HalDispatchTable,而应该先利用ROP技术关闭SMEP,再执行shellcode。

参考

  • https://blog.ensilo.com/one-bit-to-rule-them-all-bypassing-windows-10-protections-using-a-single-bit

  • https://media.blackhat.com/bh-us-11/Mandt/BH_US_11_Mandt_win32k_WP.pdf

  • https://www.nccgroup.trust/globalassets/newsroom/uk/blog/documents/2015/07/exploiting-cve-2015.pdf

  • https://bbs.pediy.com/thread-247281.htm

  • https://www.anquanke.com/post/id/163973

  • http://hdwsec.fr/blog/20151217-ms15-010/

  • https://www.blackhat.com/docs/asia-16/materials/asia-16-Wang-A-New-CVE-2015-0057-Exploit-Technology-wp.pdf

  • https://thisissecurity.stormshield.com/2014/04/08/how-to-run-userland-code-from-the-kernel-on-windows/

你可能感兴趣的:(安全)