root 环境下,com.xxx.finance(某ID的 bank App) 打开就白屏卡死随后闪退。简单记录下逆向分析过程: 梆梆加固查了什么、查到之后会做什么、怎么一层层把它压住让 App 运行起来,最后怎么把加密的业务 dex 从内存里 dump 出来。
root环境一启动就白屏,几十秒后退回桌面。logcat -b crash 抓崩溃现场,:
Cause 那行写着 "null pointer dereference"。普通野指针崩溃,pc 会落在某个真实的 .so 里,回溯能还原几帧调用栈。这次的崩溃:pc 落在 0x1f4(= 500,比任何合法映射都小得多),控制流明显被劫持跳飞了;backtrace 只有两帧还全空,是 lr 被清 0、栈根本回溯不出来;fault addr 又正好等于 pc,说明它直接执行到了这个非法低地址。这大概率是加固壳主动自毁,sp=0 是加固壳主动清的;回溯打空是反分析的结果。低地址具体是 0x1f4 还是 0x61c、0x79c,不同自毁点会变。
这次案例中,梆梆防护由三块组成:
libDexHelper.so 里明文串带 classes.dve、art::DexFile::OpenMemory、XZ-compressed data is corrupt、assets/meta-data/rsa.sig 这些脱壳器专属特征。AndroidManifest 的 Application/入口 Activity 指向 com.byb.splash.activity.SplashActivity,包名 com.bnc.finance,业务类却在 com.byb.*。
把 APK 里的 classes.dex 拿出来,纯静态数一遍 class_def:3358 个,只有 androidx/*、com.google.*、com.alibaba.*、okhttp3 这些框架和库,com/byb、com/bnc 的业务类一个都没有。
也就是说框架层是明文的,梆梆只把 App 自研的 com.byb.* 抽出来加密进了 classes.dve。这对分析很关键:在 classes.dex 里翻不到业务逻辑,得脱壳才拿得到。反过来说,这也是选择性加固给分析者留的口子,想抓框架层的 hook 点,根本不用脱壳,明文就能逆;只有要动业务自身逻辑时才非脱不可。
先看它怎么在进程里落脚。libDexHelper 的初始化分三级:
那张 base64 表里,ActivityThread/LoadedApk/mApplication/mProviderMap 正是反射接管 App 的 Application/ContentProvider 创建要用的字段,梆梆靠反射把自己嵌进启动流程,在真 Application 跑起来前先接管。顺便,这也是第二套串混淆:框架反射名用 base64 藏,检测串则用 XOR(0xAC)(见检测面那节)。

sub_1D3F4 是真正的总编排,解密 dex、加载 dex、反篡改自毁全在这一个 29.8KB 的函数里(它调解密器 sub_355BC、调 OpenMemory 桥 sub_FBA8,自己又引用 ptrace ×3、内嵌自毁序列)。dex 落地的完整流水线:
sub_FBA8 我叫它"OpenMemory 桥":它不 link ART,而是 dlsym 未导出的 art::DexFile::OpenMemory,按 ART 版本挑不同签名分支(so 里躺着好几个 OpenMemory 的 mangled 名),最后拿假 location 串 Anonymous-DexFile 把内存 dex 送进 ART。
解密算法(sub_355BC)是一大段内联 SIMD、不调任何标准 crypto 库(细节见上表)。诚实边界:本文靠运行时 dump 拿明文 dex(见脱壳那节),没有把这段 SIMD 密码 + key 派生逆到能离线解密,想纯静态完整脱壳,那段还得单独啃,不在本文范围。
这条链末尾还有一个反脱壳机关:ART 拿到的是 native heap 缓冲、就地(in-place)用,不 copy 到标准 [anon:dalvik-DEX data] 区。为什么这让常规脱壳工具集体失灵,留到脱壳那节。
要绕过一只壳,得先知道它查什么。但在定位任何一个检测点之前,得先过两道反静态门槛。
第一道,.text 里撒垃圾字节。 capstone 线性反汇编撞到非法指令就停,一次 pass 只覆盖到第一段。直接扫 /proc/self/maps 的引用点会得 0 个,不是没有,是反汇编撞上第一颗垃圾字节就断了。改成"每 4 字节 try 一条、失败就跳过"的容错扫描,引用点就变成 10 个。
第二道,检测串 XOR(key=0xAC) 加密。 明文串(MAGISK/ptrace/crc32)在 .rodata 里 strings 得出来,但代码对它们零直接 adrp+add 引用,真正用的是加密 blob,运行时才解。解密循环在 cmdline 检查那段(0x23f94 附近)实锤,一次解 16 字节:

所以对梆梆这种壳,strings | grep -i magisk 出来的东西八成是诱饵。要找的是解密循环 + 加密 blob组合。
sub_12FA4 是个 ptrace 反调试薄壳,但它把检测和处置合在了一处:

反篡改线程判定环境有问题后,不需要另调什么"自杀函数",直接拿请求码 5 调这个薄壳就地引爆:清 SP、清 LR、BR 到低地址 0x61C,一次 pc 落在 0x61C、回溯打空的 SIGSEGV。魔数 0xB6A2 也在这儿露了个头(自毁点那节会讲为什么它是个陷阱、不能拿来当自毁判据)。请求码不是 5 的正常调用,则转到下面那条经指针表间接调 ptrace 的路(见本节最后)。动态里还能看到它 open("/proc/self/cmdline")、open("/proc/self/exe"),确认自己是以预期包名在跑,防重打包、防壳外调试。
/proc/self/maps(@0xdcc15)在这份 so 里被引用了 10 处。核心是 sub_AF28C,它干的事比"扫 maps"多一层:

一是遍历全部映射,比对有没有注入库、异常段;二是解析出 /system/lib64/libc.so 的基址,mprotect 一段成 RWX,把结果缓存在 qword_133338。这个缓存后面供给 syscall 指针表(本节最后)。动态确证:fopen("/proc/self/maps") 反复出现,初始化窗口里就 ×3。
光扫自己不够,它还挨个翻别的进程:
这两条在启动初始化那一小段窗口里不一定跑满,但静态引用摆在那,是常驻能力。
顺着导入表往下摸,还有三条也接了线,跟 ptrace 一样走 0x10fXXX 指针表间接调(getmntent@0x10fb18、dl_iterate_phdr@0x10f670、sendto@0x10f818),不是死代码:


前面几处都提到"经指针表调"。这是梆梆反 hook 的底座,也最难绕过:关键 syscall(ptrace 这些)不 bl libc 的 PLT,而是:

表里的项是运行时用前面 sub_AF28C 拿到的 libc 基址填进去的。调用点看不到任何符号名,PLT 桩和 inline hook 都落不下去。这条我动态反证过:hook 了 libc 的 ptrace 导出,进程从启动到自毁全程零命中,它根本不碰 libc 的 ptrace。反过来这也说明,对付它得在更底层动手(seccomp 拦 syscall 入口),这是后面绕过思路的来源。
这些静态也在,但动态跑一遍更全:
检测手法五花八门,处置却收口,全汇到同一个自毁原语。下面拆它。
检测面那节反调试那张图里,前面已经在 sub_12FA4 见过自毁模板了(a2==5 那条)。这里把它当"自毁"正面分析:字节指纹、系统性。伪代码如下:

if (a2 == 5) JUMPOUT(0x61C);,0x61C 是个小于 0x1000 的低地址,落在第 0 页,正常没映射,跳过去就是 SIGSEGV。反汇编看得更透,同款自毁在 sub_567E0 里是这么一串:

拆开就是:清 SP、清 LR、BR 到低地址。对照先判性质那节的 tombstone 完全吻合,BR 到 0x1f4/0x61c 给出 pc,MOV SP,X0(X0=0)给出 sp=0,MOV X30,X0 给出 lr=0。达到的就是既崩、又让工具还原不出栈的效果。
这套序列有个字节级不变量,可以当指纹:
三条连着的 D2800000 9100001F AA0003FE 紧跟一条 BR Xn,正常编译代码里不会出现(谁会先把 X0 清零、再拿它同时盖掉 SP 和 LR),是很可靠的定位锚。它也不是孤例,反篡改主调度器 sub_1D3F4(约 29.8KB,内部引用 ptrace 三次)里,同款序列逐字节一致:

自毁序列的编码会随 build 变,这点尤其要注意。最典型的是清 SP/LR 这两条,梆梆有两种写法,一种直接拿零寄存器 mov sp, xzr(0x910003ff)/mov lr, xzr(0xaa1f03fe),另一种经 X0 中转 mov sp, x0(0x9100001f)/mov x30, x0(0xaa0003fe),这份样本走的是后一种,两种字节完全不同。所以拿一份现成的自毁签名 scanner 来扫,很可能一条都命中不了;偏移就更别提了,不同 build 同一偏移指向完全不同的函数,照搬只会认错地方。判自毁点只能对着手上这份 so、锚整段序列重新逆一遍。
把两种清 SP/LR 编码都纳入识别后再扫,全 .text 里 486 个 br xN,只命中 5 个自杀点,sub_12FA4/sub_1304C/sub_1D3F4(×2)/sub_567E0,零误伤。
同一个加固厂、不同 App、不同壳版本,检测点和自毁编码都会变,别人的偏移和签名只能当参考,手上这份 so 一定得自己重新逆一遍。另外,上面序列里那个 MOVK #0xB6A2 看着像自毁标记,其实是梆梆遍布全 so(60+ 处)的通用魔数哨兵,很多普通业务代码也用它:
![0xB6A2 的一处普通用法:MOV 之后紧跟 BL 普通调用,和自毁的"BR 到低地址"完全两码事,判自毁必须锚整段序列,不能只认魔数] (upload/tmp/1085268_NF8F435TUE5PKYX.webp)
拿 0xB6A2 单独当判据,会大面积误伤。
有了自毁点,最直觉的绕法是"把进程冻住再 dump"。但直接 kill -STOP 冻住 main,300 毫秒内 main 就被 SIGKILL。冻住的进程自己跑不了代码,那必然是外部进程在反制。查一下进程结构:
main fork 出两个看门狗子进程盯着它,main 一被冻/被改,看门狗就 SIGKILL 它。顺序反过来就行:
三进程全 State: T (stopped)、main 存活。有序 SIGSTOP 破多进程守护,是对付多进程自守的通用手法。
把自毁的 BR 源头 NOP 掉之后,梆梆会换一种自毁方式;每压一层,它就升一级:

冰与火的战歌:Windows内核攻防实战高级班!从零到实战,融合AI与Windows内核攻防全技术栈,打造具备自动化能力的内核开发高手。
最后于 16小时前
被星野安全编辑
,原因: