继续V8学习,继续踩着姚老板和征哥的小车车出发
环境搭建
使用如下方法进行编译
1 | git reset --hard 0ec93e047216979431bd6f147ab5956bb729afa2 |
漏洞分析
题目给出了如下patch文件:
1 | diff --git a/src/builtins/builtins-array.cc b/src/builtins/builtins-array.cc |
patch的主要逻辑在上面已经标注出来,总的来说:
1、 首先判断参数的个数是否为3,除去默认的第0个参数,coin函数中应该需要两个参数;
2、 然后将数组的elements保存到 局部变量中;
3、 接着获取用户输入的第一个参数作为length,第二个参数作为value
4、 最后判断数组的长度是否大于37,是则利用保存在局部变量中的elements将其第37个元素设置为value的值
这里的问题出现在Object::ToNumber()这个函数中,该函数可以通过valueOf触发callback回调,在回调函数中重新设置数组的length,就可以让数组重新分配elements的内存空间。
如果最开始让数组的长度小于37,回调函数中将其设置为一个大于37的值,这样在判断数组长度的时候是满足大于37这个条件的,但是其中的set操作是对保存在局部变量中的elements进行操作的。由于数组重新分配了elements的空间,所以保存在局部变量中的elements相当于一个已经释放了的指针,对该指针指向的elements的第37个元素进行set操作,就会导致越界写数据。
Object::ToNumber的分析
前面提到漏洞的关键在于Object::ToNumber能触发回调函数,下面从源码的角度来大致分析是在什么地方触发回调函数。
Object::ToNumber定义如下,该函数首先判断输入input是否为数字,如果是则直接返回。否则调用ConverToNumberOrNumeric函数
1 | // static |
进入ConverToNumberOrNumeric,该函数是一个while循环,依次判断输入input是否为Number、String、Iddball、Symbol以及BigInt。如果是则调用对应的处理函数,返回相应类型值。如果不是,则在最后调用JSReceiver::ToPrimitive函数,经过JSReceiver::ToPrimitive函数处理的结果将保存到input中,经while循环再次判断input是否是Number、String等。
1 | // static |
继续跟进JSReceiver::ToPrimitive,在这个函数中会通过Object::GetMethod从参数receiver中获取一个函数,如果跟进Object::GetMethod也会发现它调用了JSReceiver::GetProperty从receiver中获取属性信息,判断其是否可以调用的函数isCallable(),并将其返回。最后会用Execution::Call来调用获取到的函数,这应该就是前面提到的调用回调函数的地方。
1 | // static |
漏洞利用
前面提到的漏洞点在于数组UAF,在ToNumber的回调中增加数组的长度来让其重新分配空间造成一个UAF,如果数组开始的长度小于37的话将会发生越界写。如果越界写的刚好是另一个数组的长度字段,那就有一个很大的数组越界了。
POC如下
1 | var length = { |
我想通过在debug版本中源码下断点,然后跟踪ToNumber函数回调的过程,但是会在中途报错。可能是debug版本与release版本之间某些检查的不同导致的。
所以,这里就只能使用release版本来直接看UAF后,array的长度,以及在回调函数中申请的victim的长度。
1 | //array: |
从上图中可以看到,原有的array数组的elements空间就在array对象之后。经过coin函数后,array数组会重新申请更大空间的elements,size也会变为0x10000000。而array原来的elements空间被victim对象所覆盖。然后会执行向array原有的elements[37]处写入一个大数,这里就刚好将victim对象的length覆盖为一个大数。
总结:那么我们就实现了一个数组的越界写,且这个越界写的范围很大。
addressOf
要想实现addressOf方法很简单,创建一个array对象a,修改a[0]=obj,随后使用victim去越界读obj对象的地址。
1 | let a = new Array(0x12345678, 0); |
任意地址读写
首先是通过victim越界读,获取ArrayBuffer ab的backsorte地址。然后通过victim越界写修改backstore为我们想要读写的地址,就能实现任意地址读写。
1 | let backstore_ptr_idx = victim.indexOf(i2f(8n)) + 1; |
getshell
提权的方法,还是使用wasmcode一把梭。不过在实现写入shellcode的时候,不能直接使用我们上面的arb_write,会出现异常。这里修改为了
1 | var data_buf = new ArrayBuffer(32); |
这里仍然是先创建ArrayBuffer和DataView,通过addressOf函数获取ArrayBuffer的back_store地址。最后使用setBigUint64写入shellcode。
EXP
1 | var buf =new ArrayBuffer(16); |
参考
- 本文作者: A1ex
- 本文链接: http://yoursite.com/2021/09/22/2019-数字经济大赛决赛-Browser/
- 版权声明: 本博客所有文章除特别声明外,均采用 MIT 许可协议。转载请注明出处!