在移动安全研究领域,Unidbg已成为动态分析Native层保护的利器。本文将深入探讨如何解决libshpssdk.so在JNI调用时出现的intno=2异常,通过系统化的环境补全方案,为逆向工程师提供一套可复用的高级技巧。
当我们在Unidbg中加载Shopee的libshpssdk.so并尝试调用关键函数时,通常会遇到以下错误日志:
code复制[16:01:16 210] WARN [com.github.unidbg.linux.ARM64SyscallHandler]
(ARM64SyscallHandler:399) - handleInterrupt intno=2, NR=-127248,
svcNumber=0x10a, PC=unidbg@0xfffe0134,
LR=RX@0x40096dd8[libshpssdk.so]0x96dd8, syscall=null
关键参数解析:
| 参数名称 | 值 | 技术含义 |
|---|---|---|
| intno | 2 | 对应ARM的EXCP_SWI(软件中断) |
| NR | -127248 | 无效的系统调用号(JNI未正确初始化) |
| svcNumber | 0x10a | JNI函数调用标识 |
这种异常的本质是SO文件尝试通过JNI调用Java层功能时,Unidbg未能提供完整的执行环境。与常规的系统调用补全不同,JNI环境补全需要更精细的处理策略。
在ARMv8架构中,异常分为同步异常和异步异常两大类。我们遇到的intno=2属于同步异常中的SWI(Software Interrupt):
code复制异常类型层级:
1. 同步异常
├── SVC(Supervisor Call)
├── HVC(Hypervisor Call)
└── SMC(Secure Monitor Call)
2. 异步异常
├── IRQ
└── FIQ
Unidbg通过Hook异常处理流程来模拟真实设备环境。当JNI调用发生时,执行流程如下:
svc指令触发软件中断在ARM64SyscallHandler.java中,异常分发逻辑如下:
java复制if (intno == ARMEmulator.EXCP_SWI) {
int NR = backend.reg_read(Arm64Const.UC_ARM64_REG_X8).intValue();
long svcNumber = backend.reg_read(Arm64Const.UC_ARM64_REG_X16).longValue();
if (svcNumber != 0) {
// JNI调用处理路径
handleJniCall(emulator, NR, svcNumber);
} else {
// 系统调用处理路径
handleSyscall(emulator, NR);
}
}
当出现NR为无效值且svcNumber非零时,表明JNI调用链出现断裂。这种情况通常源于:
首先建立标准的Unidbg执行环境:
java复制public class ShopeeEmulator extends AbstractJni {
private final AndroidEmulator emulator;
private final VM vm;
private final Module module;
public ShopeeEmulator() {
// 使用Dynarmic加速执行
emulator = AndroidEmulatorBuilder.for64Bit()
.addBackendFactory(new DynarmicFactory(true))
.build();
// 设置Android 9.0环境
Memory memory = emulator.getMemory();
memory.setLibraryResolver(new AndroidResolver(23));
// 加载APK获取包信息
vm = emulator.createDalvikVM(new File("shopee.apk"));
vm.setJni(this);
vm.setVerbose(true);
// 加载目标SO
DalvikModule dm = vm.loadLibrary(
new File("libshpssdk.so"), true);
dm.callJNI_OnLoad(emulator);
module = dm.getModule();
}
}
通过分析堆栈回溯,我们发现异常发生在getApplicationInfo调用链上。修改AbstractJni的默认行为:
java复制@Override
public DvmObject<?> callObjectMethodV(BaseVM vm, DvmObject<?> dvmObject,
String signature, VaList vaList) {
if (signature.contains("getApplicationInfo")) {
// 返回模拟的ApplicationInfo对象
return new ApplicationInfo(vm);
}
return super.callObjectMethodV(vm, dvmObject, signature, vaList);
}
关键补全点:
getApplication()调用链getSharedPreferences()数据存取PackageManager相关查询ApplicationInfo属性填充java复制@Override
public DvmObject<?> getObjectField(BaseVM vm, DvmObject<?> dvmObject,
String signature) {
switch (signature) {
case "android/content/pm/ApplicationInfo->sourceDir:Ljava/lang/String;":
return new StringObject(vm, "/data/app/com.shopee.my/base.apk");
case "android/content/pm/PackageInfo->applicationInfo:Landroid/content/pm/ApplicationInfo;":
return buildFakeAppInfo(vm);
}
return super.getObjectField(vm, dvmObject, signature);
}
private DvmObject<?> buildFakeAppInfo(BaseVM vm) {
ApplicationInfo appInfo = new ApplicationInfo(vm);
// 设置关键字段
appInfo.setFieldInt("flags", 0x400000); // FLAG_DEBUGGABLE
appInfo.setFieldObject("packageName",
new StringObject(vm, "com.shopee.my"));
return appInfo;
}
在vm.setVerbose(true)基础上,增加定制化日志:
java复制vm.setVerbose(new Log() {
@Override
public void debug(String message) {
if (message.contains("JNI")) {
System.out.println("[JNI TRACE] " + message);
}
}
});
通过Unidbg的调试器接口设置关键断点:
python复制# 在IDA Python脚本中
add_bpt(0x40096DD8, 1, BPT_SOFT)
enable_bpt(0x40096DD8, True)
# 配合使用unidbg的attach调试
emulator.attach().addBreakPoint(module.base + 0x96DD8);
对于频繁调用的JNI方法,使用缓存机制:
java复制private Map<String, DvmObject<?>> jniCache = new ConcurrentHashMap<>();
@Override
public DvmObject<?> callStaticObjectMethodV(BaseVM vm, DvmClass dvmClass,
String signature, VaList vaList) {
if (jniCache.containsKey(signature)) {
return jniCache.get(signature);
}
// ...正常处理逻辑
jniCache.put(signature, result);
return result;
}
Shopee的libshpssdk.so采用了多层防护策略:
环境检测:
数据加密:
c复制// 典型的加密函数结构
JNIEXPORT jstring JNICALL Java_com_shopee_shpssdk_SHPSSDK_encrypt(
JNIEnv* env, jobject obj, jbyteArray input) {
// 使用硬件加速的AES加密
if (isDebugged()) {
return (*env)->NewStringUTF(env, "INVALID_DEBUG");
}
return real_encrypt(env, input);
}
反调试措施:
在实际分析过程中,建议采用"最小化补全"原则,只修补触发异常的必要环境,避免过度补全导致引入新的问题。