写在前面
问题出现场景:
假设对象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
比如函数:
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运行的效率。
比如上面这段JS代码里面,class A里面定义了prop函数,class B继承了class A,v8是通过super属性来获取class A中的属性x,严格来说,是获得A.prototype.x,然后返回给调用者的,v8有针对这种super索引的父类的情况有做专门的优化处理,这个处理阶段叫做superIC。
在这段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代码:
如果每次传入的obj都为对象a,那么v8 IC之后,会标记该属性为MONOMORPHIC。如果传入的obj有a对象,b对象两种情况,会标记为POLYMORPHIC,如果大于4种情况,则会标记为MEGAMORPHIC。
如上述代码所示,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。
如上述代码所示,在随后的操作中:
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()结果没什么差别)。
这个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更多的是验证这种代码的问题。
如上所示如果receiver为String对象,在SuperIC过程中,会将receiver传进去,然后执行的为LoadIC_StringWrapperLength。
[招生]科锐逆向工程师培训(2024年11月15日实地,远程教学同时开班, 第51期)
最后于 2023-5-30 11:30
被苏啊树编辑
,原因: