继续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 许可协议。转载请注明出处!