背景
目标 app: com.everysing.lysn (Lysn),使用了韩国 Lockin Company 的商业保护方案 LIAPP。
版本号: 1.6.10
设备环境: Pixel 4 XL,strongR-frida 16.5.2。
现象:
- 直接打开 app:
![image]()
- Frida spawn 启动:检测到 frida,进程被终止
![image]()
第一步:定位检测库
用 frida hook android_dlopen_ext 打印加载的 so:
1 | Interceptor.attach(Module.findExportByName(null, "android_dlopen_ext"), { |
输出:
1 | libstats_jni.so |
加载 libdxxcuxd.so 后进程立即终止,说明检测逻辑在这个 so 中。
查看 AndroidManifest.xml,Application 类是 com.liapp.x——这是 LIAPP 的壳。它在 attachBaseContext 中调用 System.loadLibrary("dxxcuxd") 加载 native 检测库。
第二步:找到初始化入口
现在的壳和风控组件都喜欢把检测逻辑放在 ELF 初始化函数中。ELF 初始化有三个入口,执行顺序固定:
1 | DT_INIT → 最早(单个函数) |
用 IDA 打开 libdxxcuxd.so,查看 .init_array 段:
只有 DT_INIT_ARRAY 类型的函数,没有 DT_INIT 类型的。如果有 DT_INIT 类型的,那么我们优先看 DT_INIT 类型的,因为它的执行时间早于 DT_INIT_ARRAY 类型的函数。且两者都优先于 JNI_OnLoad 执行。
有两个初始化函数(已重命名):
| 地址 | 函数 | 作用 |
|---|---|---|
| 0xA2838 → 0xFBE0 | init_arr1 | 主初始化,包含所有检测逻辑 |
| 0xA2840 → 0x10898 | init_arr2 | 简单变量清零,无检测 |

第三步:分析 init_arr1
直接用ida mcp让ai分析写了注释
init_arr1(原名 sub_FBE0)是 LIAPP 的主初始化函数,内部按顺序做了以下事情:
检测 _progname
_progname 是 libc 导出的全局变量,保存当前进程名。LIAPP 计算它的 CRC32 存入全局变量,用于后续完整性校验。
检测 environ
遍历环境变量,检查是否包含:
.sandbox— 沙箱环境(VirtualApp、双开助手等会注入)/data/data/com.lbe.parallel— 平行空间(双开框架)
很多虚拟环境会修改LD_PRELOAD、CLASSPATH、TMPDIR等环境变量,或添加SANDBOX标记,因此environ是反虚拟化的重要检测点。
sub_294E0:代码段完整性校验(CRC32)

1 | sub_294E0(a1): |
这个函数在多处被调用,意味着 如果我们直接 patch .text 段的代码,后续 CRC32 校验会发现篡改并杀进程。 这是一个重要约束。
sub_164A8:检测 BlackDex
检查 _progname 是否包含 blackdex(脱壳工具),是则杀进程。
sub_3DDB4:核心 Frida 检测

这是最关键的函数,包含 maps 扫描和线程名检测:
1 | sub_3DDB4(a1): |
sub_1D25C:maps 黑名单

这个函数检查 /proc/self/maps 的每一行是否包含以下关键词:
1 | /data/local/tmp ← frida server 常见路径 |
返回 0 表示安全,-1 表示发现风险。
sub_15FD0:后台检测线程
启动两个 pthread 持续检测:
sub_16378— 延迟后调用sub_160E8杀进程sub_182F0— 循环发送kill(pid, SIGALRM)
sub_160E8:终极杀进程
多重保险机制:
1 | kill(getpid(), 9); // SIGKILL 自己 |
用 fork() 子进程杀父进程是为了绕过父进程中的 kill hook——子进程虽然继承了内存,但 frida 的 Interceptor hook 可能不生效。
LIAPP 的 syscall 包装
LIAPP 不直接调用 libc 的 openat/read,而是通过 dlsym 获取函数指针,可能指向直接 syscall 的包装:
1 | // sub_1265C (openat 包装) |
这意味着 hook libc 的 read/openat 不一定能拦截 LIAPP 的文件读取。
第四步:绕过策略选择
方案 A:hook call_constructors + patch 代码(有坑)
思路:hook linker 的 call_constructors,在 .init_array 执行前 patch 检测函数。
1 | # 找到 call_constructors 偏移 |
1 | Interceptor.attach(Process.findModuleByName('linker64').base.add(0x51120), { |
问题:
sub_294E0会做 CRC32 完整性校验,patch .text 段会被检测到sub_3DDB4内部有初始化逻辑(注册信号处理器、设置标志位),不能整个跳过- 各种条件分支和标志位依赖关系复杂,容易遗漏
call_constructors偏移因设备/Android 版本不同,通用性差
方案 B:让检测正常执行,但让它的武器全部失效(最终方案)
思路:不阻止检测代码运行,而是让它的每一个”杀进程手段”都无效化。
核心观察:LIAPP 检测到 frida 后的唯一动作就是杀进程。 如果杀不死,检测就等于没用。
同时,由于 LIAPP 可能用 syscall 直接读取 /proc/task/*/comm(绕过 libc hook),我们无法保证在 read 层面过滤掉 frida 线程名。但这没关系——让它检测到也无所谓,反正杀不死。
第五步:最终绕过方案
时序分析
1 | frida spawn app |
四层防护
| 层 | 时机 | 作用 |
|---|---|---|
| 1 | 脚本加载时 | replace kill/fork/exit/signal,杀进程操作全部失效 |
| 2 | 脚本加载时 | 过滤 /proc/task/*/comm(对走 libc 的路径有效) |
| 3 | dlopen 返回后 | patch 后台检测线程入口为 RET |
| 4 | Java VM 就绪后 | 拦截 System.exit / Process.killProcess |
为什么不能 patch sub_3DDB4(核心 Frida 检测)?
sub_3DDB4 除了检测逻辑,还有初始化操作(注册信号处理器、设置 dword_A5D9C 标志位等)。后续 LIAPP 的 JNI 方法(字符串解密、DEX 加载)依赖这些初始化数据。直接 patch 为 RET 会导致:
1 | signal 11 (SIGSEGV), code 2 (SEGV_ACCERR) |
另外,sub_294E0 的 CRC32 校验也会发现 .text 段被修改。
最终脚本
1 | // 绕过 com.everysing.lysn 的 LIAPP (libdxxcuxd.so) frida/root 检测 |

从打开 IDA 到 app 正常启动,Claude 承担了大部分分析和编码工作,连这篇文章都是AI帮忙总结的,只能感叹 AI 还是太强大了。

