好友
阅读权限 30
听众
最后登录 1970-1-1
本文章中所有内容仅供学习交流使用,不用于其他任何目的,不提供完整代码,抓包内容、敏感网址、数据接口等均已做脱敏处理,严禁用于商业用途和非法用途,否则由此产生的一切后果均与作者无关.本文章未经许可禁止转载,禁止任何修改后二次传播,擅自使用本文讲解的技术而导致的任何意外,作者均不负责
一、样本信息
这个 app 的某梆企业壳和之前遇到的版本的不一样,他不是线程杀死的特征。
我们直接开启一下 frida 来测试他的一个情况,直接做一个 dlopen 来测试一下
function hook_dlopen() {
Interceptor.attach(Module.findExportByName(null, "android_dlopen_ext"), {
onEnter: function (args) {
var pathptr = args[0];
if (pathptr !== undefined && pathptr != null) {
var path = ptr(pathptr).readCString();
console.log(path)
}
},
onLeave: function (retval) {
}
});
libDexHelper.so
}
可以看到他已经加载了壳特征的 so,libDexHelper.so,然后被杀死,看起来像是跳转到垃圾地址。这个样本难度不大,我跌跌撞撞搞了三套方案,都可以实现脱壳
第一套方案
ai 黑盒
// @FileName :脱壳机.js
// @Time :2025-12-14 16:10
// @AuThor :Buluo
// =========================================================================
// ⚙️ 配置区域 (Configuration)
// =========================================================================
const CONFIG = {
target_so: "libDexHelper.so", // 目标壳 SO 名字
dump_dir: "/data/data/com.lptiyu.tanke/cache/", // Dump 落地目录
sys_openat: 56 // AArch64 openat syscall number
};
// =========================================================================
// 🛠️ 基础工具模块 (Native Utils)
// =========================================================================
const NativeUtils = (() => {
const libc = Process.getModuleByName("libc.so");
const api = {
open: new NativeFunction(libc.getExportByName("open"), 'int', ['pointer', 'int', 'int']),
read: new NativeFunction(libc.getExportByName("read"), 'int', ['int', 'pointer', 'int']),
write: new NativeFunction(libc.getExportByName("write"), 'int', ['int', 'pointer', 'int']),
close: new NativeFunction(libc.getExportByName("close"), 'int', ['int']),
malloc: new NativeFunction(libc.getExportByName("malloc"), 'pointer', ['int']),
free: new NativeFunction(libc.getExportByName("free"), 'void', ['pointer'])
};
const O_RDONLY = 0, O_WRONLY = 1, O_CREAT = 64, O_TRUNC = 512;
return {
// 创建文件并写入内容
writeFile: (path, content) => {
const pathPtr = Memory.allocUtf8String(path);
const fd = api.open(pathPtr, O_WRONLY | O_CREAT | O_TRUNC, 493); // 0755
if (fd < 0) return false;
const contentPtr = Memory.allocUtf8String(content);
api.write(fd, contentPtr, content.length);
api.close(fd);
return true;
},
// 复制文件并过滤特定行 (核心逻辑)
copyAndFilter: (srcPath, dstPath, filterKeywords) => {
const srcPtr = Memory.allocUtf8String(srcPath);
const dstPtr = Memory.allocUtf8String(dstPath);
const fdIn = api.open(srcPtr, O_RDONLY, 0);
if (fdIn < 0) return false;
const fdOut = api.open(dstPtr, O_WRONLY | O_CREAT | O_TRUNC, 493);
if (fdOut < 0) { api.close(fdIn); return false; }
const bufSize = 4096;
const buf = api.malloc(bufSize);
let pending = "";
while (true) {
const n = api.read(fdIn, buf, bufSize);
if (n <= 0) break;
const chunk = buf.readUtf8String(n);
const lines = (pending + chunk).split('\n');
for (let i = 0; i < lines.length - 1; i++) {
const line = lines[i];
let blocked = false;
for (let k of filterKeywords) {
if (line.indexOf(k) !== -1) { blocked = true; break; }
}
if (!blocked) {
const ptr = Memory.allocUtf8String(line + "\n");
api.write(fdOut, ptr, line.length + 1);
}
}
pending = lines[lines.length - 1];
}
// 处理最后一行
if (pending.length > 0) {
let blocked = false;
for (let k of filterKeywords) { if (pending.indexOf(k) !== -1) blocked = true; }
if (!blocked) {
const ptr = Memory.allocUtf8String(pending + "\n");
api.write(fdOut, ptr, pending.length + 1);
}
}
api.free(buf);
api.close(fdIn);
api.close(fdOut);
return true;
}
};
})();
// =========================================================================
// 🎭 幻境构建模块 (Environment Mirage)
// =========================================================================
const MirageEnv = (() => {
let fakeStatusPtr = null;
let fakeMapsPtr = null;
return {
prepare: () => {
console.log("Building Native Mirage...");
// 1. 伪造 Status (TracerPid: 0)
const statusPath = CONFIG.dump_dir + "native_status";
const statusContent =
`Name:\t${Process.currProcName}\nState:\tS (sleeping)\nTgid:\t${Process.id}\n` +
`Ngid:\t0\nPid:\t${Process.id}\nPPid:\t1\nTracerPid:\t0\n` + // <--- 关键欺骗
`Uid:\t10000\t10000\t10000\t10000\nGid:\t10000\t10000\t10000\t10000\n`;
if (NativeUtils.writeFile(statusPath, statusContent)) {
fakeStatusPtr = Memory.allocUtf8String(statusPath);
console.log(` [+] Fake Status ready: ${statusPath}`);
}
// 2. 伪造 Maps (过滤 Frida)
const mapsPath = CONFIG.dump_dir + "native_maps";
if (NativeUtils.copyAndFilter("/proc/self/maps", mapsPath, ["frida", "gum-js", "re.frida"])) {
fakeMapsPtr = Memory.allocUtf8String(mapsPath);
console.log(` [+] Fake Maps ready: ${mapsPath}`);
}
},
getStatusPtr: () => fakeStatusPtr,
getMapsPtr: () => fakeMapsPtr
};
})();
// =========================================================================
// 🛡️ 防御模块 (Anti-Suicide)
// =========================================================================
const AntiSuicide = (() => {
return {
activate: () => {
const libc = Process.getModuleByName("libc.so");
const funcs = ['raise', 'kill', 'tgkill', 'exit', '_exit'];
funcs.forEach(name => {
const addr = libc.findExportByName(name);
if (addr) {
Interceptor.replace(addr, new NativeCallback((...args) => {
const sig = args[args.length - 1];
if (name.includes('exit')) {
console.warn(`[🛡️] BLOCKED ${name}! Sleeping thread.`);
Thread.sleep(1000000); return 0;
}
if (sig === 5) { // SIGTRAP
// console.warn(`[🛡️] BLOCKED ${name}(SIGTRAP)`);
return 0;
}
return new NativeFunction(addr, 'int', args.map(()=>'int'))(...args);
}, 'int', Array(name === 'raise' || name.includes('exit') ? 1 : (name === 'kill' ? 2 : 3)).fill('int')));
}
});
console.log("[+] Anti-Suicide Shield activated.");
}
};
})();
// =========================================================================
// 🕵️♂️ 抓取模块 (DEX Dumper)
// =========================================================================
const DexDumper = (() => {
// 自动保存 DEX 到文件
const saveDex = (base, size) => {
try {
const magic = base.readCString(4);
if (magic !== "dex\n") return; // 二次校验
const path = `${CONFIG.dump_dir}dumped_${base}.dex`;
const f = new File(path, "wb");
f.write(base.readByteArray(size));
f.flush();
f.close();
console.log(`[🎉] DEX CAPTURED! Path: ${path} (Size: ${size})`);
} catch (e) { console.error("[-] Save failed: " + e); }
};
return {
install: () => {
// 扫描 libart.so 和 libdexfile.so
const modules = Process.enumerateModules().filter(m => m.name.includes("libart") || m.name.includes("libdexfile"));
let count = 0;
modules.forEach(mod => {
mod.enumerateSymbols().forEach(sym => {
// 模糊匹配 Open 函数 (OpenCommon, OpenMemory, Open)
if (sym.name.includes("DexFile") && sym.name.includes("Open") && !sym.name.includes("std")) {
try {
Interceptor.attach(sym.address, {
onEnter: function(args) {
// 盲猜前两个参数,寻找 DEX 头 (dex\n035)
for (let i = 0; i < 2; i++) {
try {
if (args[i].readU32() === 0x0a786564) { // 'dex\n'
const size = args[i+1].toInt32();
// 过滤过小的文件
// if (size > 2048) saveDex(args[i], size);
}
} catch(e) {}
}
}
});
count++;
} catch(e) {}
}
});
});
console.log(`[+] DEX Dumper installed on ${count} targets.`);
}
};
})();
// =========================================================================
// 🚀 核心逻辑 (Syscall Interceptor)
// =========================================================================
function setup_bypass() {
const module = Process.findModuleByName(CONFIG.target_so);
if (!module) return;
const jni_onload = module.findExportByName("JNI_OnLoad");
console.log(`\nTarget Base: ${module.base}`);
console.log("Strategy: Syscall Redirection (Stalker)");
Interceptor.attach(jni_onload, {
onEnter: function() {
this.tid = Process.getCurrentThreadId();
console.log(`[+] JNI_OnLoad entered. Stalker attached to thread ${this.tid}`);
Stalker.follow(this.tid, {
events: { call: false, ret: false, exec: false, block: false, compile: false },
transform: function(iterator) {
let instruction = iterator.next();
do {
// 拦截 svc #0 指令
if (instruction.mnemonic === 'svc') {
iterator.putCallout(function(context) {
const sysNum = context.x8.toInt32();
// openat (56)
if (sysNum === CONFIG.sys_openat) {
const ptrPath = context.x1; // x1 是路径参数
try {
const path = ptrPath.readCString();
// 重定向 Status
if (path && path.includes("/status")) {
const fake = MirageEnv.getStatusPtr();
if (fake) context.x1 = fake;
}
// 重定向 Maps
if (path && path.includes("/maps")) {
const fake = MirageEnv.getMapsPtr();
if (fake) context.x1 = fake;
}
} catch(e) {}
}
});
}
iterator.keep();
} while ((instruction = iterator.next()) !== null);
}
});
},
onLeave: function(retval) {
console.log(`[✅] JNI_OnLoad Completed! Ret: ${retval}`);
Stalker.unfollow(this.tid);
}
});
}
// =========================================================================
// 🏁 主入口 (Main)
// =========================================================================
function main() {
AntiSuicide.activate();
MirageEnv.prepare();
DexDumper.install();
const dlopen = Module.findExportByName(null, "android_dlopen_ext");
if (dlopen) {
Interceptor.attach(dlopen, {
onEnter: function(args) {
const pathPtr = args[0];
if (pathPtr) {
const path = pathPtr.readCString();
if (path && path.includes(CONFIG.target_so)) {
this.found = true;
}
}
},
onLeave: function() {
if (this.found) setup_bypass();
}
});
console.log("Waiting for target SO to load...");
}
}
setImmediate(main);
这套方案使用 Gemini 黑盒,有报错就喂给他,不停的进行对抗,然后实现了过检测以及脱壳,但是这个仍然存在问题。但是也可以看出来一些常见的监测点,包括对 maps 、status 利用 frida 进行重定向,包括出现的 svc 指令特征去进行 hook,这些监测点也比较通用,在其他 app 也能遇到和用上。最后将dex 成功 dump 出来
第二套方案
魔改 frida-service +端口转发,这是在无意间摸索出来的一套方案
[JavaScript] 纯文本查看 复制代码
./aaabbb -l 0.0.0.0:1314adb forward tcp:1314 tcp:1314
frida -H 127.0.0.1:1314 -f com.lptiyu.tanke -l .\bdyp.js
使用的小佳大佬的魔改 aaabbb,做一个端口转发到 1314 端口,然后可以来试一下看看效果。
我们在启动之后,发现一个很有意思的现象libDexHelper.so 的特征壳已经加载成功了,进程还是被杀死了,看着就很像之前那一篇文章的梆梆,我们直接打印 clone 的线程,然后杀掉线程 .
[JavaScript] 纯文本查看 复制代码
function hook_clone() {
var clone = Module.findExportByName('libc.so', 'clone');
Interceptor.attach(clone, {
onEnter: function (args) {
// 只有当 args[3] 不为 NULL 时,才说明上层确实把 “线程控制块指针” 传进来了
if (args[3] != 0) {
// 真正的用户线程函数地址
var addr = args[3].add(96).readPointer() // 读取指针
var so_name = Process.findModuleByAddress(addr).name;
// 获取该 so 在进程里的基址
var so_base = Module.getBaseAddress(so_name);
// 获取相对于 so_base 的偏移
var offset = (addr - so_base);
console.log("===============>", so_name, addr, ptr(offset));
}
},
onLeave: function (retval) {
}
});
}
function hook_dlopen(so_name) {
Interceptor.attach(Module.findExportByName(null, "android_dlopen_ext"), {
onEnter: function (args) {
var pathptr = args[0];
if (pathptr !== undefined && pathptr != null) {
var path = ptr(pathptr).readCString();
if (path.indexOf(so_name) !== -1) {
this.BookReader4Android = true
}
}
},
onLeave: function (retval) {
if (this.BookReader4Android) {
let base = Module.findBaseAddress(so_name);
patch_func_nop(base.add(0x561d0));
patch_func_nop(base.add(0x52cc0));
patch_func_nop(base.add(0x5ded4));
patch_func_nop(base.add(0x5e410));
patch_func_nop(base.add(0x5b9f4));
patch_func_nop(base.add(0x69470));
patch_func_nop(base.add(0x5729c));
}
}
});
}
这样就成功过掉了检测{:1_886:}
第三套方案
使用原生的 frida,定位加密大致位置,直接 trace 看在哪里被杀死的,定位加密位置 可以看这张的一个思路吧
还是老件套,因为是在 libDexHelper.so 被杀死的,我们看他有没有完整的加载再去定位
libDexHelper.so 是加载完成的
那么检测点可能是在 jni 函数里面,看 jni_onload 加载情况
function hook_dlopen(so_name) {
Interceptor.attach(Module.findExportByName(null, "android_dlopen_ext"), {
onEnter: function (args) {
var pathptr = args[0];
if (pathptr !== undefined && pathptr != null) {
var path = ptr(pathptr).readCString();
if (path.indexOf(so_name) !== -1) {
this.BookReader4Android = true
}
}
},
onLeave: function (retval) {
if (this.BookReader4Android) {
console.log(so_name,"加载成功!!!")
const jni_onload =Process.findModuleByName(so_name).findExportByName("JNI_OnLoad")
Interceptor.attach(jni_onload, {
onLeave: function (retval) {
console.log("JNI_OnLoad加载完成")
}
})
}
}
});
}
说明检测是在 jni 函数中实现的逻辑,既然能 trace 出来,并被杀死,就一定有类似 svc 0 的相关操作
function hook_dlopen(so_name) {
Interceptor.attach(Module.findExportByName(null, "android_dlopen_ext"), {
onEnter: function (args) {
var pathptr = args[0];
if (pathptr !== undefined && pathptr != null) {
var path = ptr(pathptr).readCString();
if (path.indexOf(so_name) !== -1) {
this.BookReader4Android = true
}
}
},
onLeave: function (retval) {
if (this.BookReader4Android) {
console.log(so_name, "加载成功!!!")
const jni_onload = Process.findModuleByName(so_name).findExportByName("JNI_OnLoad")
Interceptor.attach(jni_onload, {
onEnter: function (args) {
const thread_id = Process.getCurrentThreadId()
Stalker.follow(thread_id, {
transform: function (iterator) {
while (true) {
let instruction = iterator.next();
if (instruction === null) {
break
}
iterator.keep()
}
}
});
},
onLeave: function (retval) {
console.log("JNI_OnLoad加载完成")
}
})
}
}
});
}
加入 trace 代码,确认没有对 app 启动流程造成影响,才能继续分析
同样的闪退,这样的话就直接写 trace 代码逻辑看看输出情况
function hook_dlopen(so_name) {
Interceptor.attach(Module.findExportByName(null, "android_dlopen_ext"), {
onEnter: function (args) {
var pathptr = args[0];
if (pathptr !== undefined && pathptr != null) {
var path = ptr(pathptr).readCString();
if (path.indexOf(so_name) !== -1) {
this.BookReader4Android = true
}
}
},
onLeave: function (retval) {
if (this.BookReader4Android) {
const targetModule = Process.findModuleByName(so_name);
const jni_onload = targetModule.findExportByName("JNI_OnLoad");
const modBase = targetModule.base;
const modSize = targetModule.size;
const modEnd = modBase.add(modSize);
const modName = targetModule.name;
Interceptor.attach(jni_onload, {
onEnter: function (args) {
this.tid = Process.getCurrentThreadId();
Stalker.follow(this.tid, {
transform: function (iterator) {
let instruction = iterator.next();
if (instruction === null) return;
let offsetInBlock = 0;
do {
const curAddr = instruction.address;
if (curAddr.compare(modBase) >= 0 && curAddr.compare(modEnd) < 0) {
const offset = curAddr.sub(modBase);
if (offsetInBlock === 0) {
console.log(`[transform] start: ${curAddr} name:${modName} offset: ${offset} base: ${modBase}`);
}
console.log("\t" + curAddr + " <+" + offsetInBlock + ">: " + instruction.toString());
offsetInBlock += instruction.size;
}
iterator.keep();
instruction = iterator.next();
} while (instruction !== null);
}
});
},
onLeave: function (retval) {
console.log("JNI_OnLoad加载完成");
// Stalker.unfollow(this.tid);
}
});
}
}
});
}
直接梭哈
日志会放在最后附件里面
结合日志可以看到他是在0x31ec0 偏移地址最终死掉,提到了 x12 寄存器,我们在 ida 去看看这个地址的情况, 需要注意的是,这个 so 文件也是混淆了,需要先 dump so 再去分析
其实这里就能看到很多检测点了,直接 nop 掉这个判断,让他不走使我们报错的位置
[JavaScript] 纯文本查看 复制代码
var targetAddr = modBase.add(0x31CA0)Memory.patchCode(targetAddr, 4, function (code) {
var cw = new Arm64Writer(code, {pc: targetAddr});
cw.putNop();
cw.flush();
});
这时候就会发现,已经成功过掉了检测
总结
这个案例还是比较简单,可能最开始没有摸到思路,ai操作太过于繁琐,但是很多检测点都有涉及,不失为一种思路,还是要有自己的分析思考过程。 {:1_932:}总的来说还是学校的app分析起来香,趁着还没毕业好好分析一波。还得继续学习,还是太菜了 {:1_932:}
trace1.zip
(107.32 KB, 下载次数: 3)
免费评分
查看全部评分