BabyAnti 详解
重要:这里有个更正从WMCTF2023下载的附件应该出错了应该是BabyAnti1.0的附件,后面我出flag和发现没碰到mincore的问题才发现
java层发现了一些反调试的东西,并且在lib下发现了anticheat.so
libapp.so直接搜索3000

用blutter分析下
1
| python3 blutter.py path/to/app/lib/arm64-v8a out_dir
|
然后ida导入脚本add_names,就能恢复符号了,但是问题在于不能建立交叉引用

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 程序里,常见情况是:
这里代码直接拿到了字符串地址,所以 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
|
而不是:
这里前面的:
就很像字符串对象前缀、长度编码、标记位,或者 snapshot 序列化格式的一部分。后面的:
也说明它未必以 \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 代码中经常看到这种指令:
这里:
也就是:
所以:
就是 Object Pool 的一个 slot。
obj.txt 是 snapshot 里的所有 Dart 对象的反序列化结果。
换句话说:
1
| obj.txt = Dart heap object graph
|
IDA 看到的是:
但 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)
|
所以现在已经可以确认:
"GetFlag" 是一个合法 overlay 名字
- 它在
main.dart 的 overlay map 中被注册了
- 当
OverlayManager.add("GetFlag") 被调用时,最终会构造 GetFlag widget
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();
|

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, 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 毫秒轮询一次内存,看看模块加载了没有。实际发生的事情是这样的:
- 游戏启动。
- 刚好在一个 500ms 的等待空隙里,游戏加载了
libanticheat.so。
- Dart 引擎立马调用了
is_device_injected,此时你的 Hook 还没来得及打上去,于是反作弊函数返回了 1 (True)。
- Dart 引擎收到 True,立刻往屏幕上扔了一个
CheatDetectedHint 弹窗。
- 几毫秒后,你的 Frida 脚本醒了,把反作弊给 Hook 掉了,也把
libapp.so 的分数 3000 改成了 0。
- 紧接着游戏引擎更新画面(此时分数为 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, 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 会返回:
如果有映射:shellcode 再读取该页内容
然后搜索frida-agent
这套动态生成的 Shellcode,通过一个循环,暴力枚举从 0x00000000 到 0xFFFFFFFF 的所有内存页,不断地用 svc 触发 mincore。一旦发现某个内存页有效,就直接去读取那块内存里的数据,暴力搜索 Frida 的特征码(比如 frida-agent)。这是一种完全不依赖文件系统、极难被常规手段拦截的内存扫描方案。
其实就是很多壳会这样做:
mmap 一块内存
- 写入机器码
mprotect 改为可执行
- 跳转执行
例如:
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;
function killAntiCheat() { if (anticheat_hooked) return; var anticheat_mod = Process.findModuleByName("libanticheat.so"); if (anticheat_mod) { console.log("[+] 发现 libanticheat.so (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) {} });
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; } }
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 = 0x31E292; const patchAddr = libapp.add(patch_offset);
try { Memory.protect(patchAddr, 7, 'rwx'); patchAddr.writeByteArray([0x48, 0x81, 0xFA, 0x00, 0x00, 0x00, 0x00]); console.log("[+] 已将 0x" + patch_offset.toString(16) + " 处的比较条件从 5000 改为 0!"); } catch(e) { console.error("[-] Patch 失败: " + e); } }
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的博客,基本思想学了下