原文:https://mem2019.github.io/jekyll/update/2020/09/19/QWB-GooExec.html
前言简单的吐个槽,跟赛题无关。这次单人参加了在郑州的强网杯线下,是第一次但应该也是最后一次了。总体的来讲无论是从赛题质量还是赛制设计都是不错的。比赛一共有两个部分:传统Attack Defense和Real World Jeopardy题目。我在第一天几乎一直在看AD的题,然后从第二天早上开始就几乎完全投入到了这道GooExec
的V8题目上面来了,导致最后一道AD我连附件下都没下载。而更可惜的是这道题目我在15:40
拿到了一个object faking primitive。我原以为是17:00
之前都可以上台演示,那么就还有一个多小时写利用(这完全够了),但是16点就不能申请演示了,我在15:59
申请了一次结果16:04
就把我叫上去了,而这样的话我就完全没准备好,于是这道题就没有做出来。换句话说他这个本质上RW项目是16:00
就结束了,并不是17:00
结束。而我则等于第二天所花的精力完全没有拿到分,还不如做第四道AD题。
这道题修改了load-elimination.cc
中Reduce
两个Node
的代码,把KillMaps
函数删除了。这导致一些Node
的Map
会被错误估计,从而导致CheckMaps
会被错误地删除掉。通过这个我们可以把一个unboxed double写到一个FixedArray
里面去,于是再访问那个array
就可以fake任意object了。由于开启pointer compression的V8并没有把存在GC堆中的低32位地址随机化,我们可以通过分配大Array
来fake object,从而获得任意读写。
这道题目的patch点涉及到几个Load Elimination Phase的类,所以在分析patch前我会先分析这几个相关的类。
此类代表在某个node的effect之后的状态,在这里我们只关心他的AbstractMaps const* maps_
成员。其中被删除的KillMaps
函数代码如下:
此类代表在某个effect状态时所有node所可能有的maps。很明显其ZoneMap<Node*, ZoneHandleSet<Map>> info_for_node_
成员变量就是用来存储这个信息的:把Node
映射到一个Map
的集合。这边注意,假如某个Node没有对应的映射,那么代表这个Node的Map信息JIT是不知道的;假如某个Node存在对应映射,那么这个Node在当前状态的Map必然是这个set中的其中一个。换句话说,假如某个可能的Map没有被包含在 这个set里面,那么就有可能造成类型混淆。
接着再来看看AbstractMaps::Kill
的实现,即KillMaps
所调用的那个函数:
简单地说,MayAlias
函数返回是否两个Node可能refer到同一个对象。而AliasStateInfo
则代表用来比较的其中一个Node。
MayAlias
具体的实现我就不放这里了,稍微有点长,只要知道它在两个Node一定不是同一个对象 的时候返回false
就好了。
在分析漏洞之前,可能还是得先看看相关函数所reduce的TransitionElementsKind
是个什么东西。这是当Element类型改变的时候,用来改变Array的element存储方式的操作,所以同时也会改变其map。比如arr = [1.1, 2.2]
的element会是FixedDoubleArray
,而经过arr[0]={}
操作后,element会变成一个FixedArray
,这个时候就需要TransitionElementsKind
这个操作来做这个转换。
接下来可以看看他的这个patch了,虽然他patch了两个函数,但实际上似乎我的利用只用到了其中一个函数,不知道这是不是非预期解。所以我这篇Writeup只讲解那个patch,虽然另一个也是一个KillMaps
的删除,可能也差不多。但是那个更难触发到,需要通过Array.prototype.map
来触发。
很明显,KillMaps
的删除导致某些本应该没有map信息的一些node仍然保留了信息,那么假如所保留的Map信息是个错误估计,即某些可能存在的Map没有被存储在其中,那么就可能造成类型混淆。比如说ReduceCheckMaps
这里,就有可能导致错误地删除CheckMaps
:
那么如何让state
去保留一个错误估计呢?在TransitionElementsKind
后object
的Map插入了一个新的target_map
,那么假如有另一个Node和object
指向同一个对象,它的state就即不会被KillMaps
删除也不可能被插入这个新的target_map
,于是就可以导致一个错误估计。
接着就需要尝试构造PoC了。首先需要解决的问题是如何构造两个Node指向同一个object的情况,我尝试了使用一些JIT的方法让sea of nodes去生成一些aliasing nodes,但是好像特别难构造。最后我发现了一个最简单的方法,就是给两个不同的函数参数传入同一个对象,那么当JIT访问不同的Parameter时,就可以构成不同Node指向同一个对象的条件。
写成JavaScript大概是这样:声明function test(arr, arr2) {...}
,调用arr=Array(0);test(arr, arr);
关于PoC的JIT函数,大概思路如下:
接着就按照上面的思路写这么一个函数:
然后b load-elimination.cc:866
下断点,--no-enable-slow-asserts
关闭slow check后通过p state->Print()
查看map_
的状态:
执行state = state->SetMaps(object, object_maps, zone());
前:
执行state = state->SetMaps(object, object_maps, zone());
后:
说明第2步所需要达成的条件确实被成功触发了,arr2
仍然被当做是一个double elements的Array。
但是实际上这个并没有触发第3步,所生成的store操作是把一个double object的指针存放到FixedArray
里面去,而不是直接存unboxed double。卡了很久之后,我猜想到的原因是在profiling JIT所用的类型信息时,执行arr2[0] = 1.1
的时候因为arr2
已经是个FixedArray
的Array了,所以收集到的就是arr2
是<Map(HOLEY_ELEMENTS)>
的类型信息,导致生成的就是处理arr2
的Element是FixedArray
的JIT代码。所以要防止这一点,我们必须得保证在JIT前执行arr2[0] = 1.1
的时候arr2
必须是<Map(HOLEY_DOUBLE_ELEMENTS)>
,经过尝试,发现可以这么构造:
执行f(true, true, a, a)
后,a[0]
就已经是unboxed的13.37
的低32位了,所以访问a[0]
就会导致crash,这就是一个object faking primitive。
因为目前的V8都是有pointer compression的,而在这个模式下指针的低32位是不随机的,所以只需要一个object faking不需要任何泄露就能实现代码执行。具体思路如下:
LoadElimination::AbstractState const
*
LoadElimination::AbstractState::KillMaps(
const AliasStateInfo& alias_info, Zone
*
zone) const {
if
(this
-
>maps_) {
AbstractMaps const
*
that_maps
=
this
-
>maps_
-
>Kill(alias_info, zone);
/
/
本质上就是调用maps_的Kill函数
if
(this
-
>maps_ !
=
that_maps) {
AbstractState
*
that
=
zone
-
>New<AbstractState>(
*
this);
that
-
>maps_
=
that_maps;
return
that;
/
/
如果不一样才返回一个新的
}
}
return
this;
}
LoadElimination::AbstractState const
*
LoadElimination::AbstractState::KillMaps(
Node
*
object
, Zone
*
zone) const {
AliasStateInfo alias_info(this,
object
);
return
KillMaps(alias_info, zone);
}
LoadElimination::AbstractState const
*
LoadElimination::AbstractState::KillMaps(
const AliasStateInfo& alias_info, Zone
*
zone) const {
if
(this
-
>maps_) {
AbstractMaps const
*
that_maps
=
this
-
>maps_
-
>Kill(alias_info, zone);
/
/
本质上就是调用maps_的Kill函数
if
(this
-
>maps_ !
=
that_maps) {
AbstractState
*
that
=
zone
-
>New<AbstractState>(
*
this);
that
-
>maps_
=
that_maps;
return
that;
/
/
如果不一样才返回一个新的
}
}
return
this;
}
LoadElimination::AbstractState const
*
LoadElimination::AbstractState::KillMaps(
Node
*
object
, Zone
*
zone) const {
AliasStateInfo alias_info(this,
object
);
return
KillMaps(alias_info, zone);
}
LoadElimination::AbstractMaps const
*
LoadElimination::AbstractMaps::Kill(
const AliasStateInfo& alias_info, Zone
*
zone) const {
for
(auto pair : this
-
>info_for_node_) {
if
(alias_info.MayAlias(pair.first)) {
/
/
if
one of nodes may alias
AbstractMaps
*
that
=
zone
-
>New<AbstractMaps>(zone);
for
(auto pair : this
-
>info_for_node_) {
if
(!alias_info.MayAlias(pair.first)) that
-
>info_for_node_.insert(pair);
}
/
/
keep
all
except
the ones that may alias
return
that;
}
}
return
this;
}
LoadElimination::AbstractMaps const
*
LoadElimination::AbstractMaps::Kill(
const AliasStateInfo& alias_info, Zone
*
zone) const {
for
(auto pair : this
-
>info_for_node_) {
if
(alias_info.MayAlias(pair.first)) {
/
/
if
one of nodes may alias
AbstractMaps
*
that
=
zone
-
>New<AbstractMaps>(zone);
for
(auto pair : this
-
>info_for_node_) {
if
(!alias_info.MayAlias(pair.first)) that
-
>info_for_node_.insert(pair);
}
/
/
keep
all
except
the ones that may alias
return
that;
}
}
return
this;
}
class
LoadElimination::AliasStateInfo {
public:
AliasStateInfo(const AbstractState
*
state, Node
*
object
, Handle<
Map
>
map
)
: state_(state), object_(
object
), map_(
map
) {}
AliasStateInfo(const AbstractState
*
state, Node
*
object
)
: state_(state), object_(
object
) {}
bool
MayAlias(Node
*
other) const;
private:
const AbstractState
*
state_;
Node
*
object_;
/
/
用来比较的Node
MaybeHandle<
Map
> map_;
};
class
LoadElimination::AliasStateInfo {
public:
AliasStateInfo(const AbstractState
*
state, Node
*
object
, Handle<
Map
>
map
)
: state_(state), object_(
object
), map_(
map
) {}
AliasStateInfo(const AbstractState
*
state, Node
*
object
)
: state_(state), object_(
object
) {}
bool
MayAlias(Node
*
other) const;
private:
const AbstractState
*
state_;
Node
*
object_;
/
/
用来比较的Node
MaybeHandle<
Map
> map_;
};
/
/
LoadElimination::ReduceTransitionElementsKind
if
(object_maps.contains(ZoneHandleSet<
Map
>(source_map))) {
object_maps.remove(source_map, zone());
object_maps.insert(target_map, zone());
/
/
AliasStateInfo alias_info(state,
object
, source_map);
/
/
state
=
state
-
>KillMaps(alias_info, zone());
state
=
state
-
>SetMaps(
object
, object_maps, zone());
}
/
/
LoadElimination::ReduceTransitionElementsKind
if
(object_maps.contains(ZoneHandleSet<
Map
>(source_map))) {
object_maps.remove(source_map, zone());
object_maps.insert(target_map, zone());
/
/
AliasStateInfo alias_info(state,
object
, source_map);
/
/
state
=
state
-
>KillMaps(alias_info, zone());
state
=
state
-
>SetMaps(
object
, object_maps, zone());
}
Reduction LoadElimination::ReduceCheckMaps(Node
*
node) {
ZoneHandleSet<
Map
> const& maps
=
CheckMapsParametersOf(node
-
>op()).maps();
Node
*
const
object
=
NodeProperties::GetValueInput(node,
0
);
Node
*
const effect
=
NodeProperties::GetEffectInput(node);
AbstractState const
*
state
=
node_states_.Get(effect);
if
(state
=
=
nullptr)
return
NoChange();
ZoneHandleSet<
Map
> object_maps;
/
/
假如object_maps的
Map
信息并不完整,可能导致maps.contains错误地返回true
if
(state
-
>LookupMaps(
object
, &object_maps)) {
if
(maps.contains(object_maps))
return
Replace(effect);
/
/
TODO(turbofan): Compute the intersection.
}
state
=
state
-
>SetMaps(
object
, maps, zone());
return
UpdateState(node, state);
}
Reduction LoadElimination::ReduceCheckMaps(Node
*
node) {
ZoneHandleSet<
Map
> const& maps
=
CheckMapsParametersOf(node
-
>op()).maps();
Node
*
const
object
=
NodeProperties::GetValueInput(node,
0
);
Node
*
const effect
=
NodeProperties::GetEffectInput(node);
AbstractState const
*
state
=
node_states_.Get(effect);
if
(state
=
=
nullptr)
return
NoChange();
ZoneHandleSet<
Map
> object_maps;
[注意]传递专业知识、拓宽行业人脉——看雪讲师团队等你加入!
最后于 2020-9-21 17:39
被holing编辑
,原因: