#逆向-复现SUAPP
😒5ea1
hook
ida里看到本地库里有load函数,猜测有加载,所以尝试直接从内存中hook本地库下来
一开始写脚本始终hook不掉,崩溃闪退,估计有反hook措施,看看logcat

在ida里找找看下面那个检测到hook的字符串,能直接索引到一个函数里
实现了一个基于inlinehook的检测

它从 /system/lib64/libc.so 文件中读取 signal 函数最开始的 8 个字节,这代表了函数原始的、未 Hook 的机器码。这个值被存到了 ptr 变量里(通过 fseek + fread)。
通过 dlsym 获取 signal 函数在进程内存中的实际地址,并读取这个内存地址最开始的 8 个字节,这代表了函数当前在内存中的机器码(可能已经被 Hook 修改了)。这个值被存到了 v16 变量里(通过 *v14)。
所以我们需要获取signal中的最开始8字节,然后通过hook这里的fread,使得v19的判定必定通过
需要实时检测是否载入了这个库,载入了才能进行hook
// 文件名: fixed_suapp_hook.js
const TARGET_LIB = "libsuapp.so";
const FRIDA_READ_SIZE = 8; // fread 判断大小
const PATCH_BYTES = [0x50, 0x00, 0x00, 0x58, 0x00, 0x02, 0x1F, 0xD6]; // signal patch
const DUMP_DIR = "/sdcard/Download/";
// fread hook - 检测是否读取 signal 并 patch 数据
function hookFread() {
const freadAddr = Module.findExportByName("libc.so", "fread");
if (!freadAddr) {
console.error("[-] fread not found");
return;
}
console.log("[+] Hooking fread at", freadAddr);
Interceptor.attach(freadAddr, {
onEnter(args) {
this.buf = args[0];
this.size = args[1].toInt32();
this.count = args[2].toInt32();
},
onLeave(retval) {
if (this.size === 1 && this.count === FRIDA_READ_SIZE) {
console.log("[*] Intercepted fread for signal check, patching buffer at", this.buf);
Memory.writeByteArray(this.buf, PATCH_BYTES);
retval.replace(FRIDA_READ_SIZE);
}
}
});
}
// Dump libsuapp.so 到本地
function dump_so(moduleName) {
try {
const m = Process.getModuleByName(moduleName);
const fileName = `${DUMP_DIR}${m.name}_${m.base}_${m.size}.so`;
console.log("[+] Dumping module:", m.name);
console.log(" Base:", m.base, "Size:", m.size, "Path:", m.path);
Memory.protect(m.base, m.size, 'r--');
const dumpBuf = m.base.readByteArray(m.size);
const f = new File(fileName, "wb");
f.write(dumpBuf);
f.flush();
f.close();
console.log("[+] Dumped to:", fileName);
} catch (e) {
console.error("[-] Dump failed:", e);
}
}
// 监控动态库加载:android_dlopen_ext
function monitorLibraryLoad() {
const dlopen_ext = Module.findExportByName(null, "android_dlopen_ext");
if (!dlopen_ext) {
console.error("[-] android_dlopen_ext not found");
return;
}
console.log("[*] Monitoring android_dlopen_ext");
Interceptor.attach(dlopen_ext, {
onEnter(args) {
const path = args[0].readCString();
this.isTarget = path.includes("suapp");
if (this.isTarget) {
console.log("[+] Detected suapp loading:", path);
}
},
onLeave() {
if (this.isTarget) {
console.log("[*] suapp loaded. Hooking + dumping...");
hookFread();
dump_so(TARGET_LIB);
}
}
});
}
// 初始化
setImmediate(() => {
console.log("[*] Frida script initialized");
monitorLibraryLoad();
});
linker
JNI_OnLoad 函数通常负责注册 native 方法,通常这个注册是通过调用 RegisterNatives 方法来完成的
先用FindClass找MainActivity类,然后再调用RegisterNatives注册一个函数列表
RegisterNatives函数原型
jint RegisterNatives(JNIEnv *env, jclass clazz, const JNINativeMethod *methods, jint nMethods);
- env 是 JNI 环境。
- clazz 是你要注册 native 方法的 Java 类。
- methods 是包含所有 native 方法的数组,通常是一个 JNINativeMethod 类型的结构体数组,每个元素代表一个 native 方法。
- nMethods 是数组的大小。
v22 = ((__n128 (__fastcall *)(_QWORD, __int64, char **, __int64))*(_QWORD *)(*(_QWORD *)v28[0] + 1720LL))(
v28[0],
v14,
off_4D080,
2);
所以这里应该进去看off_4D080,发现指向了start函数
索引到start函数中,发现了一个关键函数:sub_225D4

代码中调用了5次sub_225D4,借助log信息不难看出,这可能是一个用于加载外部函数的linker——根据Got you
和libc.so
可以猜测这是从libc.so中获取一个检测hook的函数,而Hello from native code!
则可能是读取了本地的库
索引到sub_225D4,分析没错
你提供的这个函数 sub_225D4 是一个典型的 ELF 动态加载器/解密加载器的一部分。
🧠 总体功能概述:
打开一个文件(很可能是 .so 或 ELF 结构的资源),将其中一部分 mmap 到内存,执行多步骤验证与解密逻辑,然后执行一些后续函数。
这通常用于加载加壳的 native 模块、隐藏代码段解密执行或用于反调试检测流程的模块加载阶段。
v5 = __open_2(a2, 0);
//打开a2指向的地址的文件
if ( (fstat(v5, (struct stat *)v15) & 0x80000000) == 0 )
//验证文件正常打开
{
v6 = v16;
if ( v16 >= 37361 )
//验证文件大小(37361通常是一个加壳 ELF 的有效最小长度)
{
v7 = sysconf(40);
if ( v7 != -1 )
{
v8 = -(int)v7 & 0x91F0LL;
v9 = 37360 - v8;
v10 = v6 - 37360;
v11 = v6 - v8;
v12 = (char *)mmap(0, v11, 3, 2, v5, v8);
//对文件偏移进行页对齐(sysconf(40) 得到页大小);
//将文件的前若干部分映射到内存中;
if ( v12 != (char *)-1LL )
{
v13 = v12;
*(_QWORD *)(a1 + 192) = &v12[v9];
if ( sub_21EE4(a1, a2, v5, 37360, v10) && (sub_22124(a1) & 1) != 0 && !mprotect(*(void **)(a1 + 184), v10, 7) )
//经典的校验判断链
//sub_21EE4(...): 可能是 校验或解密函数;
//sub_22124(...) & 1: 校验标志位,确保结构体被正确处理
//mprotect(..., 7): 设置可执行权限,用于执行解密后代码。
{
sub_2140C(*(_QWORD *)(a1 + 208));
sub_21950(*(_QWORD *)(a1 + 208));
sub_21E64(*(_QWORD *)(a1 + 208));
}
munmap(v13, v11);
}
}
}
}
查看sub_21EE4的解密逻辑
bool sub_21EE4(...) {
// 1. 参数保存 + 异或解密
v9.n128_u64[0] = 0x3C3C3C3C3C3C3C3CLL;
v9.n128_u64[1] = 0x3C3C3C3C3C3C3C3CLL;
v10 = veorq_s8((int8x16_t)xmmword_4D2D0, v9);
xmmword_4D2D0 = (__int128)v10;
xmmword_4D2E0 = (__int128)veorq_s8((int8x16_t)xmmword_4D2E0, v9);
v11 = xmmword_4D2E0;
xmmword_4D2F0 = (__int128)veorq_s8((int8x16_t)xmmword_4D2F0, v9);
xmmword_4D300 = (__int128)veorq_s8((int8x16_t)xmmword_4D300, v9);
// 2. 判断是否满足某种特征条件
// 3. 如果特征匹配(v14 == 0),则:
// 3.1 mmap64 映射目标文件的一段内容
// 3.2 打 log,确认映射大小
// 3.3 将一段 504 字节的数据拷贝到 mmap 的内存中
// - 要么 memcpy 整体写入
// - 要么通过循环写入(说明地址重叠)
// 3.4 将写入后的地址记录到结构体中
// 4. 返回判断结果(v14 == 0)
}
veorq_s8 是 ARM NEON 的 vector XOR 指令,用于对 128-bit(16字节)的向量按字节进行异或
所以这段代码是用一系列数据覆盖了原有的main中的一部分内容