对cve-2019-13698的简要分析
调试环境
1 | git checkout 7.4.301 |
patch了一些check,以便调试:
1 | diff --git a/src/objects.h b/src/objects.h |
args.gn:
1 | is_debug = true |
漏洞成因
1 | // src/runtime/runtime-regexp.cc |
漏洞函数在[1]检查该RegExp对象是不被修改的,但是又在[2]处调用了Object::ToString,该调用可以直接修改掉RegExp对象(即可以改变RegExp对象的内存布局),后续在第[3]处调用set_last_index的时候会导致溢出(因为还是按照原来的偏移来进行赋值,但是RegExp的内存布局已经被我们改变了),覆盖下一个对象的Map,以下是变化前后的RegExp对象的DebugPrint(注意其instance size):
改变前:
1 | DebugPrint: 0x428ae80f169: [JSRegExp] |
改变后:
1 | DebugPrint: 0x428ae810b01: [JSRegExp] |
使其Map变换,从[FastProperties]变成[DictionaryProperties],instance size变小了8个字节,而原先偏移48的地址存放的就是lastIndex的值,等于我们可以越界写一个lastIndex的值,但是其地址是紧邻着RegExp对象的
漏洞利用
根据issue中提到思路:
1 | To exploit this, we overwrite the map of the keys object in KeyAccumulator::GetKeys. |
poc
和作者给的exp,这里把exp改一改得出一份poc:
1 | function go(){ |
入口点就是Object.keys(p),对应v8源码中的Builtins_ObjectKeys(src/builtins/builtins-object-gen.cc)
KeyAccumulator::CollectOwnJSProxyKeys
在一系列调用后,会来到Maybe<bool> KeyAccumulator::CollectOwnJSProxyKeys(Handle<JSReceiver> receiver,Handle<JSProxy> proxy)
:
1 | // https://github.com/v8/v8/blob/7.4.301/src/keys.cc#L835 |
该函数在[4]处获取到Proxy对象的ownkeys方法,接着在[5]处进行调用:
1 | // static |
该调用会触发对o对象length属性的访问,接着会访问到o[0],o[1],结合poc中的代码,我们在对o对象的length进行访问的时候创建了RegExp对象rgx:
1 | o.__defineGetter__("length", ()=>{ |
接着在对o[1]进行访问的时候:
1 | o.__defineGetter__(1, ()=>{ |
调用了RegExp对象的replace方法,也就是漏洞代码处,这里会调用RegExp[Symble.replace]函数第二个参数对象的toString函数两次,第二次发生在漏洞函数的[6]处:
1 | // src/runtime/runtime-regexp.cc |
在第二次调用toString的时候,js代码中直接覆写了rgx对象的lastIndex方法,在漏洞函数后续调用lastIndex方法[7]处的时候改变其Map类型,在通过一次gc的操作使得KeyAccumulator::GetKeys函数中使用的key_对象(后面会分析)内存紧邻着rgx对象,在js代码中加入一个断点并打印gc后的内存布局:
1 | rgx[Symbol.replace]("AAAAAAAA", { |
gc后的内存布局:
1 | pwndbg> c |
在gc后,紧邻着reg对象内存地址的是一个FixedArray对象,也就是后面KeyAccumulator::GetKeys函数中使用的key_对象,接着在调用regexp->set_last_index(Smi::FromInt(end_index), SKIP_WRITE_BARRIER);
来更新rgx.lastIndex的时候,由于我们改变了rgx的Map,这里就导致了溢出,调用前:
rgx对象内存布局:
调用后:
可以看到该调用直接把下一个对象的Map给改成lastIndex的值,也就是8了
V8_WARN_UNUSED_RESULT MaybeHandle<String> RegExpReplace
结束后会回到KeyAccumulator::CollectOwnJSProxyKeys
后续在
1 | Maybe<bool> KeyAccumulator::CollectOwnJSProxyKeys(Handle<JSReceiver> receiver, |
[8]处会把我们覆盖Map的对象赋值给KeyAccumulator对象keys_成员,在KeyAccumulator::CollectOwnJSProxyKeys
返回到:
1 | MaybeHandle<FixedArray> FastKeyAccumulator::GetKeysSlow( |
[9]处,跟进:
1 | Handle<FixedArray> KeyAccumulator::GetKeys(GetKeysConversion convert) { |
OrderedHashSet::ConvertToKeysArray
由于keys_的Map被我们改变,导致[10]处的判断为false,从而进入[11]处的OrderedHashSet::ConvertToKeysArray
函数:
1 | Handle<FixedArray> OrderedHashSet::ConvertToKeysArray( |
从OrderedHashSet::ConvertToKeysArray
函数的参数类型可以看到,我们的keys_被当作OrderedHashSet对象来处理,从而导致了类型混淆,而后调用table->NumberOfElements()
和 table->NumberOfBuckets()
取得的值会变得很大,从而在后续的get和set操作中导致堆溢出
这里的堆溢出会把keys_后续的内存都赋值成执行字符串“0”的指针,当作smi取值的话就是一个很大的数
依靠此堆溢出我们就可以覆盖掉double_array的elements属性(poc中的dbl_arr)的length,从而就可以得到一个越界的double_array,在利用该越界的double_array修改一个TypedArray,我们就可以得到任意地址读写的效果了
后记
在chromium测试该漏洞的时候一直没有成功,原因好像是不知道为什么在chromium中,RegExp对象的data属性的数组一直分配在紧邻着RegExp对象地址下方,而不像在d8中那样是在RegExp地址的上方(低地址):
可以看到紧邻着RegExp对象的是它自己的data数组,之后才是keys_,而我们覆盖的是data数组的Map,故一直没有利用成功;后面换成chrome测试,一开始可以触发,后面在怎么试都不行了。。。不知道有没有方法让RegExp的data分配在RegExp地址上方
参考链接
https://bugs.chromium.org/p/chromium/issues/detail?id=944971