BabyAnti 详解

重要:这里有个更正从WMCTF2023下载的附件应该出错了应该是BabyAnti1.0的附件,后面我出flag和发现没碰到mincore的问题才发现

java层发现了一些反调试的东西,并且在lib下发现了anticheat.so

libapp.so直接搜索3000
image-20260306203453326

用blutter分析下

1
python3 blutter.py path/to/app/lib/arm64-v8a out_dir

然后ida导入脚本add_names,就能恢复符号了,但是问题在于不能建立交叉引用

image-20260306165732247

1
.rodata:0000000000020C78 39 39 C4 43 6F 6E 67 72 61 74 75 6C 61 74 69 6F ...

里面确实能看出明文:

1
Congratulation! Here is your flag:

为什么没有普通字符串交叉引用,在普通 native 程序里,常见情况是:

1
2
ADR   X0, aHello
BL puts

这里代码直接拿到了字符串地址,所以 IDA 很容易建立:代码 -> 字符串 的 xref

但在 Dart AOT 里,字符串通常不是这么用的。

Flutter 里更常见的是这条链:

1
2
3
4
代码
-> ObjectPool / 常量池
-> 某个 Dart String 对象
-> String 对象内部再指向或内嵌字符数据

也就是说,代码引用的往往不是你看到的这片字符本身,而是:

  • 某个 Dart 对象
  • 某个 ObjectPool slot
  • 某个 snapshot 里的句柄/偏移

因此 IDA 看不到代码直接取这个 rodata 地址,自然就不给你普通 xref。

在 Flutter release 的 libapp.so 里,字符串一般属于 IsolateSnapshotData 里的对象图。也就是说,它更像:

1
String object header + length/tag + payload bytes

而不是:

1
纯裸字符串 + \0

这里前面的:

1
39 39 C4

就很像字符串对象前缀、长度编码、标记位,或者 snapshot 序列化格式的一部分。后面的:

1
8A 6A

也说明它未必以 \0 结尾,后面可能紧跟别的对象或字段。

所以 IDA 看到的只是:

1
2
3
4
5
DCB 0x39
DCB 0x39
DCB 0xC4
DCB 0x43 ; C
...

而不是自动识别成一个标准字符串字面量。

打个比方,一个 Dart String 对象可能在内存里像这样:

1
[header][class id][length][hash][payload bytes...]

现在肉眼看到的是:

1
payload bytes = "Congratulation! Here is your flag:"

但代码引用的是,对象起始地址,不是 payload 起始地址。

pp.txt 记录的是 Object Pool(对象常量池)。在 Dart AOT 代码里,大量常量对象(字符串、Type、Class、Field、List、Map 等)不会直接写死地址,而是通过 PP(Pool Pointer) 间接访问。

在 ARM64 的 AOT 代码中经常看到这种指令:

1
LDR X0, [X27,#0xbb00]

这里:

1
X27 = PP (Pool Pointer)

也就是:

1
对象地址 = *(PP + offset)

所以:

1
[pp + 0xbb00]

就是 Object Pool 的一个 slot

obj.txt 是 snapshot 里的所有 Dart 对象的反序列化结果

换句话说:

1
obj.txt = Dart heap object graph

IDA 看到的是:

1
2
rodata
└── 字符串payload

但 Flutter 实际引用路径是:

1
2
3
4
AOT code
└── ObjectPool slot
└── Dart Object
└── String payload

也就是:

1
2
3
4
5
6
7
8
9
code

pp+0xbb00

Text object

String object

"Congratulation! Here is your flag:"

我们之前已经发现了3000的校验,如何处理可以有以下几种方法

静态分析 [BabyAnti-1]

在asm目录下发现了有个dino_run\widgets\get_flag_hint.dart文件,里面是有关调用flag的hint

看第一段 closure,0x26645c

1
2
3
4
5
6
0x2664b4: r16 = "GetFlag"
add x16, PP, #9, lsl #12 ; [pp+0x9f70] "GetFlag"
ldr x16, [x16, #0xf70]
0x2664bc: stp x16, x0, [SP, #-0x10]!
0x2664c0: r0 = remove()
bl #0x2621ec ; OverlayManager::remove

这说明这里调用的是:

1
OverlayManager.remove("GetFlag")
1
2
3
4
0x266500: r16 = "MainMenu"
0x266508: stp x16, x0, [SP, #-0x10]!
0x26650c: r0 = add()
bl #0x2620b0 ; OverlayManager::add

就是:

1
OverlayManager.add("MainMenu")

第二段:

1
2
3
4
0x26663c: r16 = "Hud"
0x266644: stp x16, x0, [SP, #-0x10]!
0x266648: r0 = add()
bl #0x2620b0 ; OverlayManager::add

就是:

1
OverlayManager.add("Hud")

所以这里已经很清楚了:

  • OverlayManager::add 的一个参数就是 overlay 名字字符串
  • OverlayManager::remove 的一个参数也是 overlay 名字字符串

我们再去搜索下GetFlag

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
D:\Matriy\Desktop\WMCTF2023\babyAnti\BabyAnti-2.0\lib\arm64-v8a\asm>findstr /s /n "GetFlag" *
dino_run\game\dino_run.dart:500: // 0x2cbb90: r16 = "GetFlag"
dino_run\game\dino_run.dart:501: // 0x2cbb90: add x16, PP, #9, lsl #12 ; [pp+0x9f70] "GetFlag"
dino_run\game\dino_run.dart:1237: // 0x33ebf4: r16 = "GetFlag"
dino_run\game\dino_run.dart:1238: // 0x33ebf4: add x16, PP, #9, lsl #12 ; [pp+0x9f70] "GetFlag"
dino_run\main.dart:530: // 0x23db74: r17 = "GetFlag"
dino_run\main.dart:531: // 0x23db74: add x17, PP, #9, lsl #12 ; [pp+0x9f70] "GetFlag"
dino_run\main.dart:741: [closure] GetFlag <anonymous closure>(dynamic, BuildContext, DinoRun) {
dino_run\main.dart:746: // 0x25d10c: r0 = GetFlag()
dino_run\main.dart:747: // 0x25d10c: bl #0x25d124 ; AllocateGetFlagStub -> GetFlag (size=0x10)
dino_run\widgets\get_flag_hint.dart:9:class GetFlag extends StatelessWidget {
dino_run\widgets\get_flag_hint.dart:131: // 0x265fd4: add x1, PP, #0xb, lsl #12 ; [pp+0xbae0] AnonymousClosure: (0x266598), in [package:dino_run/widgets/get_flag_hint.dart] GetFlag::build (0x265eb0)
dino_run\widgets\get_flag_hint.dart:159: // 0x26601c: add x1, PP, #0xb, lsl #12 ; [pp+0xbaf0] AnonymousClosure: (0x26645c), in [package:dino_run/widgets/get_flag_hint.dart] GetFlag::build (0x265eb0)
dino_run\widgets\get_flag_hint.dart:392: // 0x2664b4: r16 = "GetFlag"
dino_run\widgets\get_flag_hint.dart:393: // 0x2664b4: add x16, PP, #9, lsl #12 ; [pp+0x9f70] "GetFlag"
dino_run\widgets\get_flag_hint.dart:519: // 0x2665f0: r16 = "GetFlag"
dino_run\widgets\get_flag_hint.dart:520: // 0x2665f0: add x16, PP, #9, lsl #12 ; [pp+0x9f70] "GetFlag"

inoRunApp::build 里有一个 overlay builder 的映射表,里面明确注册了:

  • "MainMenu"
  • "PauseMenu"
  • "Hud"
  • "GameOverMenu"
  • "SettingsMenu"
  • "CheatDetectedHint"
  • "GetFlag"

其中 "GetFlag" 对应的构造 closure 就是:

1
0x25d104  [closure] GetFlag <anonymous closure>(dynamic, BuildContext, DinoRun)

这个 closure 做的事非常简单:

1
2
0x25d10c: r0 = GetFlag()
0x25d114: StoreField: r0->field_b = r1

也就是:

1
(dynamic, BuildContext, DinoRun) => GetFlag(dinoRun)

所以现在已经可以确认:

  1. "GetFlag" 是一个合法 overlay 名字
  2. 它在 main.dart 的 overlay map 中被注册了
  3. OverlayManager.add("GetFlag") 被调用时,最终会构造 GetFlag widget
  4. GetFlag::build() 里会显示:
    • 标题 "Congratulation! Here is your flag:"
    • 真正的 flag 文本 Feistel.list2str(Feistel.decList())
1
2
3
4
5
6
7
8
9
10
11
12
13
OverlayManager.add("GetFlag")
->
main.dart 里的 overlay builder
->
GetFlag(...)
->
GetFlag::build()
->
Feistel.decList()
->
Feistel.list2str()
->
Text(flag)

其实是这里可以静态出flag了

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
enc = [
-3410880308463995411, 3621865693784557948,
-3413413583204059674, 3622147168761268603,
-3422139307851188795, 3629465518155745633,
-3411443259709262353, 3608636369879157035,
-3415665384376699426, 3608917844855867690,
-3408347033338055180, 3636502392573512008,
-3415665383202294306, 3624117493598243188,
-3408347034982222348, 3608917844855867690,
-3415665383370066466, 3625806343458507118,
-3413695059539724825, 3608917844855867690,
-3417635707754056235, 3637346817503643973,
-3412850633401633308, 3624680443551664498,
-3422702257334848057, 3637346817503643973,
-3408065560005511693, 3608917844855867690,
-3407784083736955406, 3621021268854425983,
-3415665383370066466, 3625806343458507118,
-3389206734720404048, 3630872893039298908,
-3411724733528345120, 3624117493598243188,
-3410880308631767571, 3623554543644821878,
-3415946858397108769, 3628902568202324323,
-3415946857893792289, 3637346817503643973,
-3402999009535527551, 3627776668295481703
]

K1 = 0x46189eeb29628b44
K2 = 0xf146ebe323524130
SUM0 = 0xcac259cd5baebd5b
C1 = 0x5dbc4440b35079c1
DELTA = 0x611e312b161f3967

MASK = 0xffffffffffffffff

def s64(x):
x &= MASK
return x if x < (1 << 63) else x - (1 << 64)

def u64(x):
return x & MASK

def dec_pair(a, b):
x0 = u64(a) ^ K1
x5 = u64(b) ^ K2
s = SUM0

while s != 0:
t = ((u64(s + C1)) ^ ((s if s < (1<<63) else s-(1<<64)) >> 30) ^ u64(x0 << 24)) & MASK
x1 = x5 ^ t
s = u64(s + DELTA)
x5 = x0
x0 = x1

return (x0 ^ K1) & MASK, (x5 ^ K2) & MASK

out = []
for i in range(0, len(enc), 2):
a, b = dec_pair(enc[i], enc[i+1])
out.extend([a, b])

flag = ''.join(chr(x) for x in out if x != 0)
print(flag)
1
flag{D1n0_Run_0ut_0f_The_F0rest_F1nally^_^}

Frida Hook [BabyAnti-1]

blutter给了一份js,这个js跟我们手写的frida hook区别是什么呢?

  • 手写 hook 适合普通 native 函数、纯 C 参数
  • blutter hook 模板 适合 Dart AOT 对象、Flutter widget、String、List 这种对象解析

如何hook有几个思路,一是hook这的3000,二是主动调用获得flag的方法

显示Cheat Detected

说明需要绕过反调试

1
2
3
4
5
6
7
8
9
__int64 is_device_rooted()
{
unsigned __int8 *v0; // x19

v0 = (unsigned __int8 *)qword_D0A58;
sub_5F494(qword_D0A58);
sub_5F5AC(v0);
return *v0;
}
1
2
3
4
5
6
7
bool __fastcall is_device_modified(int a1)
{
if ( dword_D0A60 == a1 )
return *(_BYTE *)(qword_D0A58 + 1) != 0;
*(_BYTE *)(qword_D0A58 + 1) = 1;
return 1;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
bool is_device_injected()
{
__int64 v0; // x22
FILE *stream; // x19
char s[1024]; // [xsp+8h] [xbp-408h] BYREF
__int64 v4; // [xsp+408h] [xbp-8h]

v4 = *(_QWORD *)(_ReadStatusReg(TPIDR_EL0) + 40);
v0 = qword_D0A58;
stream = fopen("/proc/self/maps", "r");
while ( fgets(s, 1024, stream) )
{
if ( strstr(s, "frida") )
*(_BYTE *)(v0 + 2) = 1;
}
return *(_BYTE *)(v0 + 2) != 0;
}

写出hook代码,用的是blutter_frida.js在上面改的

nop掉了校验

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
const ShowNullField = false;
const MaxDepth = 5;
var libapp = null;

var anticheat_hooked = false;

function killAntiCheat() {
if (anticheat_hooked) return;

var anticheat_mod = Process.findModuleByName("libanticheat.so");
if (anticheat_mod) {
console.log("[+] 发现 libanticheat.so,准备替换...");

var targets = [
{ name: "is_device_rooted", offset: 0x5F360, argTypes: [] },
{ name: "is_device_modified", offset: 0x5F394, argTypes: ['int'] },
{ name: "is_device_injected", offset: 0x5F3C8, argTypes: [] }
];

targets.forEach(function(target) {
var funcAddr = anticheat_mod.base.add(target.offset);

try {
Interceptor.replace(funcAddr, new NativeCallback(function () {
return 0;
}, 'int', target.argTypes));

console.log("[+] 已强杀 " + target.name + " @ " + funcAddr);
} catch (e) {
console.log("[-] 替换 " + target.name + " 失败: " + e);
}
});

anticheat_hooked = true;
}
}

function onLibappLoaded() {
console.log("[+] libapp.so loaded at: " + libapp);
const patch_offset = 0x33EBCC;
const patchAddr = libapp.add(patch_offset);

try {
Memory.protect(patchAddr, 4, 'rwx');
patchAddr.writeByteArray([0x1F, 0x20, 0x03, 0xD5]);

console.log("[+] 已将 0x" + patch_offset.toString(16) + " 处的跳转替换为 NOP。");
} catch(e) {
console.error("[-] Patch 失败: " + e);
}
}

function tryLoadLibapp() {
killAntiCheat();
try {
libapp = Module.findBaseAddress('libapp.so');
} catch (e) {
if (e instanceof TypeError && e.message === "not a function") {
libapp = Process.findModuleByName('libapp.so');
if (libapp != null) {
libapp = libapp.base;
}
} else {
throw e;
}
}
if (libapp === null)
setTimeout(tryLoadLibapp, 500);
else
onLibappLoaded();
}
tryLoadLibapp();

image-20260307161454899

patch分数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
function onLibappLoaded() {
console.log("[+] libapp.so loaded at: " + libapp);

const patch_offset = 0x33EBC8;
const patchAddr = libapp.add(patch_offset);

try {
Memory.protect(patchAddr, 4, 'rwx');

// 写入 CMP X2, #0 的机器码 (5F 00 00 F1)
patchAddr.writeByteArray([0x5F, 0x00, 0x00, 0xF1]);

console.log("[+] 🎯 已将 0x" + patch_offset.toString(16) + " 处的比较条件从 3000 改为 0!");
} catch(e) {
console.error("[-] Patch 失败: " + e);
}
}

但是其实是有问题的,因为hook发现第一次进入时仍然是cheat detected第二次进入才是flag,因为先加载了libanticheat.so

这是一个在 Android 动态注入里非常经典的“时间差(Timing Issue)”问题。

在你之前的脚本中,我们使用了 setTimeout(tryLoadLibapp, 500) 来每隔 500 毫秒轮询一次内存,看看模块加载了没有。实际发生的事情是这样的:

  1. 游戏启动。
  2. 刚好在一个 500ms 的等待空隙里,游戏加载了 libanticheat.so
  3. Dart 引擎立马调用了 is_device_injected,此时你的 Hook 还没来得及打上去,于是反作弊函数返回了 1 (True)。
  4. Dart 引擎收到 True,立刻往屏幕上扔了一个 CheatDetectedHint 弹窗。
  5. 几毫秒后,你的 Frida 脚本醒了,把反作弊给 Hook 掉了,也把 libapp.so 的分数 3000 改成了 0。
  6. 紧接着游戏引擎更新画面(此时分数为 0),触发了你刚改好的 CMP X2, #0 逻辑,于是又弹出了 GetFlag 弹窗,把作弊警告给盖住了。

要解决这个时间差,要主动出击,监听 Android 系统的底层加载器(Linker)

当应用试图加载任何一个 .so 文件时,底层都会调用 android_dlopen_ext(或 dlopen)函数。我们只要 Hook 住这个底层函数,当它刚刚把 libanticheat.so 搬进内存、Dart 还没来得及拿到句柄去调用它之前,瞬间执行 killAntiCheat

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
const ShowNullField = false;
const MaxDepth = 5;
var libapp = null;
var anticheat_hooked = false;

// 1. 强杀反作弊逻辑
function killAntiCheat() {
if (anticheat_hooked) return;

var anticheat_mod = Process.findModuleByName("libanticheat.so");
if (anticheat_mod) {
console.log("[+] 发现 libanticheat.so...");
var targets = [
{ name: "is_device_rooted", offset: 0x5F360, argTypes: [] },
{ name: "is_device_modified", offset: 0x5F394, argTypes: ['int'] },
{ name: "is_device_injected", offset: 0x5F3C8, argTypes: [] }
];

targets.forEach(function(target) {
var funcAddr = anticheat_mod.base.add(target.offset);
try {
Interceptor.replace(funcAddr, new NativeCallback(function () { return 0; }, 'int', target.argTypes));
console.log("[+] 已强杀 " + target.name);
} catch (e) {}
});
anticheat_hooked = true;
}
}

// 2. Patch 游戏逻辑
function onLibappLoaded() {
if (libapp === null) {
var mod = Process.findModuleByName('libapp.so');
if (mod) libapp = mod.base;
else return;
}

console.log("[+] libapp.so loaded at: " + libapp);
const patch_offset = 0x33EBC8;
const patchAddr = libapp.add(patch_offset);

try {
Memory.protect(patchAddr, 4, 'rwx');
// 写入 CMP X2, #0 的机器码 (5F 00 00 F1)
patchAddr.writeByteArray([0x5F, 0x00, 0x00, 0xF1]);
console.log("[+] 已将 0x" + patch_offset.toString(16) + " 处的比较条件从 3000 改为 0!");
} catch(e) {
console.error("[-] Patch 失败: " + e);
}
}

// 3. 底层监控 dlopen,消除时间差
function hook_dlopen() {
var dlopen = Module.findExportByName(null, "android_dlopen_ext") || Module.findExportByName(null, "dlopen");
if (dlopen) {
Interceptor.attach(dlopen, {
onEnter: function (args) {
this.path = args[0].readCString();
},
onLeave: function (retval) {
if (this.path) {
if (this.path.indexOf("libanticheat.so") !== -1) {
killAntiCheat();
} else if (this.path.indexOf("libapp.so") !== -1) {
onLibappLoaded();
}
}
}
});
console.log("[*] 系统 dlopen 监控已启动...");
}
}

hook_dlopen(); // 注册底层监听

// 防一手:如果使用附加模式,可能 JS 注入时 SO 已经加载完了
killAntiCheat();
if (!Process.findModuleByName('libapp.so')) {
// 兜底的轮询机制,把间隔缩短到 50ms
function fallbackPoll() {
if (!anticheat_hooked) killAntiCheat();
if (!libapp && Process.findModuleByName('libapp.so')) {
onLibappLoaded();
} else if (!libapp) {
setTimeout(fallbackPoll, 50);
}
}
fallbackPoll();
} else {
onLibappLoaded();
}

这里不能主动调用,因为

1.致命的寄存器依赖 (THR & PP)

Dart 引擎在执行任何 AOT 编译的函数时,绝对依赖两个特殊的寄存器:

  • **X28 (THR - Thread Register)**:必须指向当前合法的 Dart 线程结构体。
  • **X27 (PP - Pool Pointer)**:必须指向当前 Isolate 的对象池结构体。 Frida 自己的线程(或者你在 Frida 控制台敲代码触发的执行流)是一个纯粹的 C/Linux 线程。如果直接 Call 过去,Dart 引擎一读 X28 发现是空指针或者乱码,瞬间就会抛出 SIGSEGV(段错误)闪退。

2.Dart ABI 传参约定

普通的 C 函数传参是按照 AAPCS64 来的(x0, x1, x2…)。但 Dart AOT 经常会有各种魔改优化,比如对象是否装箱(Boxed/Unboxed),谁放在寄存器,谁压进栈里,一旦传错一个比特,引擎底层校验就会失败(比如你之前汇编里看到的 BL .__stack_chk_fail)。

3.GC(垃圾回收)屏障

当你主动调用传参时,如果传入的是一个在 C 堆上伪造的指针,Dart 的写屏障(Write Barrier)在扫描时发现这个对象不在它的堆内存管理范围内,立马就会抛出 Fatal Error 强行中断进程。

WMCTF 2023 BabyAnti2.0 碰到的问题

发现github的附件居然是错的,幸好在战队内找到了正确的附件,BabyAnti2.0应该仍能用静态方式解开

一些博客还提到了mincore

mincore 是一个 Linux 系统调用(Syscall 编号通常是 232),原本的作用是检查某段内存页面是否驻留在物理 RAM 中。

真实意图:出题人想要扫描内存里有没有 Frida,但他知道读取 /proc/self/maps 太容易被 Hook 了。于是他利用了 mincore 的一个副作用:如果传给 mincore 的内存地址根本没有被映射,内核会返回 ENOMEM 错误;如果映射了,就会返回成功。

mincore 是 Linux/Android 的一个系统调用,用来查询:

一段虚拟内存是否已经映射到物理内存页。

函数原型大致是:

1
int mincore(void *addr, size_t length, unsigned char *vec);

正常用途是:让程序知道某段内存是否在物理内存里(是否在 page cache 中)。

但是它还有一个副作用:

  • 如果地址没有被映射 → 返回 -1,并设置 errno = ENOMEM
  • 如果地址是合法映射 → 返回 0

因此它可以被用来 探测一个地址是否存在映射

svc类似syscall

很多壳是这样写的:

1
libapp.so - >动态生成 shellcode --> 直接执行 svc

例如:

1
2
3
4
5
6
7
8
9
libapp.so

mmap

写入机器码

mprotect

jump shellcode

shellcode 内容:

1
2
mov x8, __NR_mincore
svc #0

这时候调用路径是:

1
2
3
4
5
6
7
libapp.so

shellcode

svc

kernel

shellcode 做的是:

1
2
for 每个内存页:
syscall(mincore)

kernel 会返回:

1
这个页有没有映射

如果有映射:shellcode 再读取该页内容

然后搜索frida-agent

这套动态生成的 Shellcode,通过一个循环,暴力枚举从 0x00000000 到 0xFFFFFFFF 的所有内存页,不断地用 svc 触发 mincore。一旦发现某个内存页有效,就直接去读取那块内存里的数据,暴力搜索 Frida 的特征码(比如 frida-agent)。这是一种完全不依赖文件系统、极难被常规手段拦截的内存扫描方案。

其实就是很多壳会这样做:

  1. mmap 一块内存
  2. 写入机器码
  3. mprotect 改为可执行
  4. 跳转执行

例如:

1
2
3
4
5
6
7
void* buf = mmap(...);

memcpy(buf, shellcode, size);

mprotect(buf, size, PROT_EXEC);

((void(*)())buf)();

这样IDA 里可能看不到完整逻辑,Frida 也很难提前 hook

因为代码是 运行时生成的

仅剩的最简单的方法是静态分析和patch so文件然后apk tools打包回去 跑一下

我之前是在我自己的真机上跑的,2.0似乎有限制跑不起来找了更多的模拟器 雷电模拟器的2024的5.0版本可以跑但是是是X86架构的

注意事项,blutter似乎不支持x86_64

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
Traceback (most recent call last):
File "/home/kali/Desktop/blutter/blutter.py", line 238, in <module>
main(args.indir, args.outdir, args.rebuild, args.vs_sln, args.no_analysis)
File "/home/kali/Desktop/blutter/blutter.py", line 220, in main
main2(libapp_file, libflutter_file, outdir, rebuild_blutter, create_vs_sln, no_analysis)
File "/home/kali/Desktop/blutter/blutter.py", line 209, in main2
dart_info = get_dart_lib_info(libapp_path, libflutter_path)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/home/kali/Desktop/blutter/blutter.py", line 160, in get_dart_lib_info
dart_version, snapshot_hash, flags, arch, os_name = extract_dart_info(libapp_path, libflutter_path)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/home/kali/Desktop/blutter/extract_dart_info.py", line 110, in extract_dart_info
engine_ids, dart_version, arch, os_name = extract_libflutter_info(libflutter_file)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/home/kali/Desktop/blutter/extract_dart_info.py", line 38, in extract_libflutter_info
assert False, f"Unsupport architecture: {elf.header.e_machine}"
^^^^^
AssertionError: Unsupport architecture: EM_X86_64

太麻烦了,放弃了有兴趣的参考下Android环境下Seccomp对系统调用的监控 | LLeaves Blog

后面还写了几版

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
var libapp = null;
var anticheat_hooked = false;

// 1. 强杀反作弊逻辑 (适配 x86_64 偏移)
function killAntiCheat() {
if (anticheat_hooked) return;

var anticheat_mod = Process.findModuleByName("libanticheat.so");
if (anticheat_mod) {
console.log("[+] 发现 libanticheat.so (x86_64)...");

// 替换为 x86_64 版本的真实偏移
var targets = [
{ name: "is_device_rooted", offset: 0x62CC0, argTypes: [] },
{ name: "is_device_modified", offset: 0x62D00, argTypes: ['int'] },
{ name: "is_device_injected", offset: 0x62D40, argTypes: [] }
];

targets.forEach(function(target) {
var funcAddr = anticheat_mod.base.add(target.offset);
try {
Interceptor.replace(funcAddr, new NativeCallback(function () { return 0; }, 'int', target.argTypes));
console.log("[+] 已强杀 " + target.name);
} catch (e) {}
});

// 顺手致盲底层的 mincore 内存扫描,防止闪退
var mincore = Module.findExportByName(null, 'mincore');
if (mincore) {
Interceptor.attach(mincore, {
onEnter: function (args) {
this.vec = args[2];
},
onLeave: function (retval) {
if (this.vec) this.vec.writeU8(0);
}
});
console.log("[+] 已致盲mincore");
}

anticheat_hooked = true;
}
}

// 2. Patch 游戏逻辑 (适配 x86_64 机器码)
function onLibappLoaded() {
if (libapp === null) {
var mod = Process.findModuleByName('libapp.so');
if (mod) libapp = mod.base;
else return;
}

console.log("[+] libapp.so loaded at: " + libapp);

// x86_64 的比较指令偏移
const patch_offset = 0x31E292;
const patchAddr = libapp.add(patch_offset);

try {
// x86_64 的 cmp rdx, 1388h 长度为 7 个字节
Memory.protect(patchAddr, 7, 'rwx');

// 原始指令: 48 81 FA 88 13 00 00 (cmp rdx, 5000)
// 目标指令: 48 81 FA 00 00 00 00 (cmp rdx, 0)
patchAddr.writeByteArray([0x48, 0x81, 0xFA, 0x00, 0x00, 0x00, 0x00]);
console.log("[+] 已将 0x" + patch_offset.toString(16) + " 处的比较条件从 5000 改为 0!");
} catch(e) {
console.error("[-] Patch 失败: " + e);
}
}

// 3. 底层监控 dlopen,消除时间差
function hook_dlopen() {
var dlopen = Module.findExportByName(null, "android_dlopen_ext") || Module.findExportByName(null, "dlopen");
if (dlopen) {
Interceptor.attach(dlopen, {
onEnter: function (args) {
this.path = args[0].readCString();
},
onLeave: function (retval) {
if (this.path) {
if (this.path.indexOf("libanticheat.so") !== -1) {
killAntiCheat();
} else if (this.path.indexOf("libapp.so") !== -1) {
onLibappLoaded();
}
}
}
});
console.log("[*] 系统 dlopen 监控已启动 ...");
}
}

// ==========================================
// 脚本启动入口
// ==========================================
hook_dlopen();

killAntiCheat();
if (!Process.findModuleByName('libapp.so')) {
function fallbackPoll() {
if (!anticheat_hooked) killAntiCheat();
if (!libapp && Process.findModuleByName('libapp.so')) {
onLibappLoaded();
} else if (!libapp) {
setTimeout(fallbackPoll, 50);
}
}
fallbackPoll();
} else {
onLibappLoaded();
}

下面的代码还缺一部分就是hook svc的部分,可能就是在,但是没有展现

1
var antiCheatPlugin = Java.use("com.WMCTF2023.anti_cheat.AntiCheatPlugin");

WMCTF 2023 Writeup - gaoyucan - 博客园

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
const android_dlopen_ext = Module.findExportByName(null, "android_dlopen_ext");
console.log(android_dlopen_ext);
if (android_dlopen_ext != null) {
Interceptor.attach(android_dlopen_ext, {
onEnter: function (args) {
var soName = args[0].readCString();
if (soName.indexOf("libanticheat.so") != -1) {
this.hook = true;
}
},
onLeave: function (retval) {
if (this.hook) {
hook_func();
};
}
});
}


function hook_pthread_create() {
var pt_create_func = Module.findExportByName(null, 'pthread_create');
var detect_frida_loop_addr = null;
console.log('pt_create_func:', pt_create_func);

Interceptor.attach(pt_create_func, {
onEnter: function () {
if (detect_frida_loop_addr == null) {
var base_addr = Module.getBaseAddress('libanticheat.so');
if (base_addr != null) {
detect_frida_loop_addr = base_addr.add(0x5EA48)
console.log('this.context.x2: ', detect_frida_loop_addr, this.context.x2);
if (this.context.x2.compare(detect_frida_loop_addr) == 0) {
hook_anti_frida_replace(this.context.x2);
}
}

}

},
onLeave: function (retval) {
// console.log('retval',retval);
}
})


}

function hook_anti_frida_replace(addr) {
console.log('replace anti_addr :', addr);
Interceptor.replace(addr, new NativeCallback(function (a1) {
console.log('replace success');
return;
}, 'pointer', []));

}
Java.perform(function () {
var antiCheatPlugin = Java.use("com.WMCTF2023.anti_cheat.AntiCheatPlugin");
antiCheatPlugin["b"].implementation = function (params) {
console.log("b is called");
return true;
}
})

setImmediate(hook_pthread_create());
function hook_func() {
var base_addr = Module.getBaseAddress("libanticheat.so");

var is_device_rooted_hidden = base_addr.add(0x5CF6C);
var is_device_modified_hidden = base_addr.add(0x5CFCC);
var is_device_injected_hidden = base_addr.add(0x5D0C8);

var is_device_rooted = base_addr.add(0x5CF38);
var is_device_modified = base_addr.add(0x5CF98);
var is_device_injected = base_addr.add(0x5CFFC);
var memtrap = base_addr.add(0x5ECC0);
var init_memtrap = base_addr.add(0x5EC74);

var mincore = Module.findExportByName(null, 'mincore');

Interceptor.attach(init_memtrap, {
onEnter: function (args) {
console.log(`init_memtrap called ${args[0]}`);
},
onLeave: function (retval) {
console.log(`init_memtrap retval ${retval}`);

}
});
// WMCTF{We1c0me_t0_Th3_W0r1d_0f_MemTr4p#^-^}
var aa = null
Interceptor.attach(memtrap, {
onEnter: function (args) {
// console.log(`memtrap called ${args[0]}`);
if (aa == null) {
aa = Interceptor.attach(mincore, {
onEnter: function (args) {
// console.log(`mincore called`);
this.vec = args[2];
},
onLeave: function (retval) {
// console.log(`mincore before modify retval ${this.vec.readU8()}, ${retval}`);
this.vec.writeU8(0);
// console.log(`mincore after modify retval ${this.vec.readU8()}, ${retval}`);
}
});

var base_addr_app = Module.getBaseAddress("libapp.so");
Interceptor.attach(base_addr_app.add(0x314F08), {
onEnter: function (args) {
console.log(`cmp called ${this.context.x2}`);
this.context.x2 = 5000;
},
})
}
},
onLeave: function (retval) {
// console.log(`memtrap called ${retval}`);

}
});

Interceptor.attach(is_device_rooted, {
onLeave: function (retval) {
// console.log('is_device_rooted called');
retval.replace(0);
}
});

Interceptor.attach(is_device_modified, {
onLeave: function (retval) {
// console.log('is_device_modified called');
retval.replace(0);
}
});

Interceptor.attach(is_device_injected, {
onLeave: function (retval) {
// console.log('is_device_modified called');
retval.replace(0);
}
});

// ---- 应该是没用,最开始看到就都 hook了 ---------
Interceptor.attach(is_device_rooted_hidden, {
onLeave: function (retval) {
console.log('is_device_rooted_hidden called');
retval.replace(0);
}
});

Interceptor.attach(is_device_modified_hidden, {
onLeave: function (retval) {
console.log('is_device_modified_hidden called');
retval.replace(0);
}
});

Interceptor.attach(is_device_injected_hidden, {
onLeave: function (retval) {
console.log('is_device_modified_hidden called');
retval.replace(0);
}
});
}

有空再学习下了

由于题目中mincore 不止存在直接libc调用,而且存在svc指令的调用,这种svc指令相当于是直接的系统调用,不能被一般的钩子挂住,从而无法监视和修改调用参数返回值。而且题目设计使用申请的内存空间修改为可执行后放置svc指令,一直循环监测。除此之外,题目还是flutter写的,逆向逻辑难上加难。经过大量逆向和调试工作后,利用frida的内存搜索功能匹配svc指令,最终能够拦截并且不被检测到,但是鉴于太复杂,想着有没有什么通用办法,不需要逆程序逻辑就直接拦截svc调用的方法。

虽然无法复现 后续还是通读了下LLeaves的博客,基本思想学了下