0x00 前言 最近在分析某企业级加固(爱加密)样本时,发现只要开启 LSPosed 并 Hook 该应用,App 启动后会立即白屏或闪退。通过 Logcat 抓取到了异常日志,顺藤摸瓜发现了一种利用 LoadedApk 核心机制进行环境检测的“骚操作”。
测试环境 本分析基于实机测试,确保复现的稳定性。
设备 A : 魅族 21 (Meizu 21) / Android 14 / Flyme OS
设备 B : Google Pixel 7 / Android 13 / Stock ROM
Root 环境 : KernelSU / Magisk
LSPosed 版本 :
Official v1.9.2
Modded v1.9.3_mod
0x01 异常日志分析 在白屏崩溃现场,我捕获到了如下关键堆栈信息:
2025-12-22 16:56:07.496 E Unable to instantiate appComponentFactory
java.lang.ClassNotFoundException: Didn't find class "androidx.core.app.CoreComponentFactory" ...
at dalvik.system.BaseDexClassLoader.findClass(BaseDexClassLoader.java:259)
...
at android.app.LoadedApk.createAppFactory(LoadedApk.java:279)
at android.app.LoadedApk.createOrUpdateClassLoaderLocked(LoadedApk.java:1050)
at java.lang.reflect.Method.invoke(Native Method)
at org.lsposed.lspd.nativebridge.HookBridge.invokeOriginalMethod(Native Method) <-- 关键点1
at J.callback(Unknown Source:194) <-- 关键点2
at LSPHooker_.createOrUpdateClassLoaderLocked(Unknown Source:11) <-- 关键点3
at android.app.LoadedApk.getClassLoader(LoadedApk.java:1137)
...
at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:977) 分析:
崩溃原因 : ClassNotFoundException ,看起来是系统找不到类。
堆栈脏了 :在 LoadedApk.createOrUpdateClassLoaderLocked 和调用者之间,夹杂了 LSPHooker_ 、 J.callback 和 HookBridge 。
结论 :这是典型的 LSPosed的 Hook 调用链。
0x02 原理溯源: 为什么这里会有 LSPosed 的堆栈?查阅 LSPosed 源码 可知,为了实现模块注入,LSPosed 必须 Hook LoadedApk.createOrUpdateClassLoaderLocked 。
源码坐标 : core/src/main/java/org/lsposed/lspd/core/Startup.java
public class Startup {
private static void startBootstrapHook(boolean isSystem) {
Utils.logD("startBootstrapHook starts: isSystem = " + isSystem);
LSPosedHelper.hookMethod(CrashDumpHooker.class, Thread.class, "dispatchUncaughtException", Throwable.class);
<-- 关键点
LSPosedHelper.hookMethod(LoadedApkCreateCLHooker.class, LoadedApk.class, "createOrUpdateClassLoaderLocked", List.class);
LSPosedHelper.hookAllMethods(AttachHooker.class, ActivityThread.class, "attach");
} 壳的策略: App 刚启动时(Application 甚至 attachBaseContext 之前),壳代码开始运行。此时壳利用反射主动调用这个系统方法,并故意构造导致崩溃的条件(例如修改 appComponentFactory 为不存在的类,或者传入导致空指针的参数)。
0x03 完美复现:幽灵对象 (Ghost Instance) 为了验证这个猜想,我尝试复现该检测逻辑。直接反射调用该方法可能因为系统健壮性而不崩溃。
最稳妥的复现方式是使用 Unsafe 创建一个 全空的“幽灵对象” 。因为字段全是 null ,调用该对象的方法必然触发 NullPointerException ,且 crash 发生在原方法内部, 完美保留 Hook 框架的调用栈 。
检测代码 (Java):
import android.app.Application;
import android.content.Context;
import android.content.pm.ApplicationInfo;
import android.util.Log;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.List;
public class LSPosedTrap {
private static final String TAG = "AntiHook";
/**
* 核心检测逻辑
*/
public static void detect(Context context) {
Log.d(TAG, " 启动幽灵对象检测 (Ghost Instance Detection)...");
try {
// 1. 获取 Unsafe 类
Class<?> unsafeClass = Class.forName("sun.misc.Unsafe");
Field theUnsafeField = unsafeClass.getDeclaredField("theUnsafe");
theUnsafeField.setAccessible(true);
Object unsafe = theUnsafeField.get(null);
// 2. 获取 LoadedApk 类
Class<?> loadedApkClass = Class.forName("android.app.LoadedApk");
// 3. 【核心】使用 Unsafe 分配一个“全空”的 LoadedApk 实例
// 这个实例没有经过构造函数,所有字段(mPackageName, mClassLoader 等)都是 null
Method allocateInstance = unsafeClass.getMethod("allocateInstance", Class.class);
Object ghostLoadedApk = allocateInstance.invoke(unsafe, loadedApkClass);
Log.d(TAG, " 幽灵 LoadedApk 实例创建成功: " + ghostLoadedApk);
// 4. 获取目标方法
Method targetMethod = loadedApkClass.getDeclaredMethod("createOrUpdateClassLoaderLocked", List.class);
targetMethod.setAccessible(true);
// 5. 【引爆】调用方法
// 因为 ghostLoadedApk 内部全是 null,原方法一执行就会产生空指针异常
// 但如果 LSPosed Hook 了,它的 Bridge 会在异常抛出前的堆栈里
Log.d(TAG, "⚡️ 正在调用 Hook 点,等待崩溃...");
targetMethod.invoke(ghostLoadedApk, (List) null);
// 如果走到这里没崩,说明 LSPosed 甚至拦截了 NPE(极少见)或者方法没被 Hook 且没执行内部逻辑
Log.e(TAG, "❌ 异常未触发!无法获取堆栈。");
} catch (Exception e) {
// 6. 捕获异常,剥离出真实的堆栈
Throwable cause = e;
// 如果是反射调用的异常,剥开一层
if (e instanceof java.lang.reflect.InvocationTargetException) {
cause = e.getCause();
}
checkStackTrace(cause);
}
}
private static void checkStackTrace(Throwable e) {
if (e == null) return;
Log.d(TAG, " 捕获到预期崩溃 (" + e.getClass().getSimpleName() + "),正在扫描堆栈...");
StackTraceElement[] elements = e.getStackTrace();
boolean found = false;
for (StackTraceElement element : elements) {
String className = element.getClassName();
String method = element.getMethodName();
String fullLine = className + "." + method;
// 打印堆栈供调试
Log.v(TAG, "Stack: " + fullLine);
// LSPosed / Xposed / SandHook 特征
if (className.contains("org.lsposed") ||
className.contains("de.robv.android.xposed") ||
className.contains("LSPHooker") ||
className.contains("com.elder.xposed") || // EdXposed
className.contains("HookBridge") ||
className.contains("SandHook")) {
Log.e(TAG, "???????????? 发现 LSPosed 痕迹! ????????????");
Log.e(TAG, " 证据: " + fullLine);
found = true;
}
}
if (!found) {
Log.d(TAG, "✅ 堆栈看似干净 (或者 LSPosed 隐藏得极深)");
}
}
} 0x04 总结与后话 这段能够稳定复现的“幽灵对象”代码,其实是我和 AI 对战了 N 个回合,尝试了各种姿势(从最初的 Hook 自身、到构造空参数、再到最后的 Unsafe)才最终搞定的。
不得不说, 爱加密这招是真的骚 。它完全跳出了传统思维,直接利用系统 API 的必经之路和 Java 异常机制来做“自爆卡车”,成本极低但杀伤力极大。
写这篇文章的初衷,是因为在网上搜了一圈,发现大家面对爱加密这种企业壳,基本都是掏出 Frida 硬刚(各种 Unpacker 脚本)。Frida 虽好,但不太适合长期稳定运行。其实只要把检测原理扒干净了, 用 LSPosed 照样能优雅地过掉 。
至于具体怎么过?
原理都在这了, 懂的都懂,应该不用我多说了吧? 大家自己动手,丰衣足食。
传播安全知识、拓宽行业人脉——看雪讲师团队等你加入!