对Chrome issue 1793的较为详细的分析
环境搭建
1 | git reset --hard dd68954 |
调试:
1 | r --allow-natives-syntax issue-1973.js |
为了能看到源码,所以选择编译的是debug版本的d8,里面那些DCHECK的宏需要自己跳过去或者对源码打patch;
后面会跟踪调试到Builtin里的函数,Builtin的函数是不能源码调试的,我的选择是把libv8.so拖进ida,然后和源码对比着看,比如源码中的BUILTIN(ArrayPrototypeFill)
函数,下断点只能下到开头,但是调试的时候源码是不会跟着动的,所以在ida中找到其对应的v8::internal::Builtin_Impl_ArrayPrototypeFill
函数对着调试:
1 | v54[0] = a1; |
漏洞分析
漏洞函数:
1 | Handle<FixedArrayBase> Factory::NewFixedDoubleArray(int length, |
该函数在(1)处对传入的length进行了check,但是DCHECK
只在debug
中起作用,在release
版本中并不起作用;所以当传入的length为负数时,可以绕过(2)处的检查(FixedDoubleArray::kMaxLength
的类型为int,而不是unsigned int);接着在(3)处会用length来计算处需要分配的内存大小,如果我们合理的控制length的值,就能使得算出来的size为一个正数,以下是FixedDoubleArray::SizeFor
的实现:
1 | // Garbage collection support. |
如果我们传入的length为0x80000000,则会返回0x10 + 0x80000000 * 8 = 0x10
,导致后续的AllocateRawWithImmortalMap
函数只分配了0x10大小的内存空间。
触发漏洞函数
v8/src/builtins/builtins-array.cc
文件中的ArrayPrototypeFill
函数在特定情况下会调用到漏洞函数,我们以poc中的代码来跟踪该过程:
poc.js:
1 | array = []; |
BUILTIN(ArrayPrototypeFill)
1 | BUILTIN(ArrayPrototypeFill) { |
函数在(4)处获得最初数组的长度,在(5)和(6)处调用GetRelativeIndex
函数取得start_index和end_index,而GetRelativeIndex
函数可以触发用户自定义的JS函数:
GetRelativeIndex
1 | V8_WARN_UNUSED_RESULT Maybe<double> GetRelativeIndex(Isolate* isolate, |
GetRelativeIndex
在(7)处会触发用户自定义的JS函数,比如valueOf函数,该自定义函数可能会把数组的length改变,而第(8)处的判断用的还是原来传入进来的length,导致返回值计算不正确;
TryFastArrayFill
在取得了start_index和end_index后,BUILTIN(ArrayPrototypeFill)
会调用TryFastArrayFill(isolate, &args, receiver, value, start_index,end_index)
,其中receiver就是我们的数组对象:
1 | In file: /home/ruan/v8_build/v8_src/v8/src/builtins/builtins-array.cc |
可以看到该数组对象的length已经被改成了256,但是此时的start_index和end_index参数还是因为计算错误而传进来的值:
1 | pwndbg> p/x $ymm0 |
接着TryFastArrayFill
在经过一系列检查后,调用具体的Fill函数来进行填充:
1 | V8_WARN_UNUSED_RESULT bool TryFastArrayFill( |
调用Fill前的参数:
1 | In file: /home/ruan/v8_build/v8_src/v8/src/builtins/builtins-array.cc |
接着进入到:
static Object FillImpl
1 | static Object FillImpl(Handle<JSObject> receiver, Handle<Object> obj_value, |
该函数在(9)处取得原来数组elements的长度,在例子中是0x100,由于end和capacity是无符号的比较,所以会进入到(10)处,Subclass::GrowCapacityAndConvertImpl(receiver, end);
经过多层warpper函数,最终会走到:
ConvertElementsWithCapacity
1 | // /home/ruan/v8_build/v8_src/v8/src/elements.cc |
在(11)处调用漏洞函数,且参数capacity就是我们传入的0x80000000,这里给出栈回溯:
后续在void BasicGrowCapacityAndConvertImpl(Handle<JSObject> object, Handle<FixedArrayBase> old_elements,ElementsKind from_kind, ElementsKind to_kind, uint32_t capacity)
函数中的:
1 | static void BasicGrowCapacityAndConvertImpl( |
(12)处会将刚刚分配的array(elements参数)赋值给Array对象:
1 | in file: /home/ruan/v8_build/v8_src/v8/src/elements.cc |
执行完JSObject::SetMapAndElements(object, new_map, elements);
后:
1 | In file: /home/ruan/v8_build/v8_src/v8/src/elements.cc |
漏洞利用
在触发漏洞后继续申请一些对象,利用数组的越界读写来构造处任意地址读写;
poc:
1 | // test on d8 release version |
在gdb中:
1 | pwndbg> r --allow-natives-syntax issue-1973.js |
我们可以直接利用array来对变量a和ab进行修改以达到任意地址读写的效果:
1 | var buf = new ArrayBuffer(16); |
有了任意地址读写,最后只需要覆盖WASM的code就能执行shellcode了
参考链接
https://bugs.chromium.org/p/project-zero/issues/detail?id=1793