当前位置: 首页 > news >正文

Android Anti-Frida 三大核心检测机制深度解析与稳定绕过

1. 这不是“绕过”而是理解对抗逻辑的起点Frida 是移动端逆向与动态插桩领域里最锋利的一把瑞士军刀——轻量、灵活、实时、可编程。但凡做过 Android 应用加固分析、协议抓取或 SDK 行为审计的人几乎都经历过这样一个瞬间刚注入 Frida 脚本目标 App 立刻闪退、弹出“环境异常”提示甚至直接进入假死状态。这时候控制台里只有一行模糊的日志Failed to initialize Frida agent或Detected Frida server。很多人第一反应是去搜“Anti-Frida 绕过脚本”复制粘贴、改个包名、重试三次失败后转头去问“为什么这个脚本不 work”。但问题从来不在脚本本身而在于我们跳过了最关键的一步没有把 Anti-Frida 当作一个可被解构的检测系统而是当成一道不可逾越的墙。这正是本篇要破除的第一个认知误区。所谓“绕过”本质是“响应式适配”——就像医生不会一上来就开药而是先做血常规、CT、病理切片确认病灶位置、细胞类型、耐药基因突变位点再决定用单抗、小分子抑制剂还是联合疗法。Anti-Frida 检测同样如此它由多个异构检测模块组成有的查进程名有的扫内存特征有的钩子openat系统调用有的甚至用 JNI 层主动调用ptrace(PT_DENY_ATTACH)反调试。它们不是铁板一块而是分层布防、有主有次、存在检测盲区与响应延迟的工程实现。我过去三年在金融类 App、IoT 设备 SDK、游戏热更新框架的逆向支持中累计分析过 47 个商用加固方案含腾讯云御安全、360 加固保、网易易盾、梆梆安全、顶象设备指纹等发现其中82% 的 Anti-Frida 实现存在至少 1 个可稳定利用的检测时序窗口或逻辑断点而真正需要“魔改 Frida 源码级对抗”的案例不到 5%。所以本文不提供“一键 bypass”的黑盒脚本而是带你亲手拆解三类最典型、最高频、最容易被误判为“已失效”的检测机制进程名/命令行参数检测、内存特征扫描、以及 ptrace 自反调试校验。每一种都会从原理出发展示真实 APK 中的 smali 片段或 so 符号调用链给出 Frida 脚本的逐行注释实现并附上验证方法与失败回溯路径。你将看到为什么Process.myPid()返回的 PID 在某些检测中反而成了“自证清白”的关键为什么Memory.scanSync扫出来的\x00\x00\x00\x00地址段其实是 Frida 注入后留下的“胎记”为什么ptrace(PT_DENY_ATTACH, 0, 0, 0)被调用两次第二次才是真正的“死刑判决”。这不是教你怎么“骗过”App而是教你如何像它的开发者一样思考他们想拦住什么依据什么判断这个判断在什么条件下会失效当你能回答这三个问题你就已经站在了对抗链路的上游。2. 进程名与命令行参数最原始却最顽固的防线几乎所有 Android 应用在启动时都会执行一段初始化逻辑其中就包括对当前进程运行环境的快速快照采集。而“进程名是否为frida或frida-server”、“/proc/self/cmdline中是否包含--enable-jit或-l参数”就是这类检测中最基础、成本最低、也最容易被忽视的一环。它不像内存扫描那样需要 JNI 层配合也不依赖复杂的系统调用 Hook只需要几行 Java 代码就能完成// 示例某金融类 App 的 AntiFridaHelper.java 片段 public static boolean isFridaProcess() { try { String processName ActivityThread.currentApplication().getPackageName(); if (processName.contains(frida) || processName.contains(re.frida)) { return true; } // 读取 cmdline BufferedReader reader new BufferedReader( new FileReader(/proc/ android.os.Process.myPid() /cmdline) ); String cmdline reader.readLine(); reader.close(); if (cmdline ! null (cmdline.contains(frida) || cmdline.contains(--enable-jit) || cmdline.contains(-l))) { return true; } } catch (Exception e) { Log.e(AntiFrida, cmdline check failed, e); } return false; }这段代码看似简单但它背后藏着两个关键设计意图第一它不依赖任何外部库或 so 文件纯 Java 实现启动即检无延迟第二它检查的是“进程启动时的原始上下文”而非运行时动态状态因此无法通过 Frida 的Java.perform延迟注入来规避。很多初学者会尝试用frida -U -f com.xxx.app --no-pause -l script.js启动以为加了--no-pause就能躲过检测。实则不然——--no-pause只是让 Frida 不自动暂停主线程但/proc/self/cmdline文件内容在zygotefork 出该进程时就已经写死frida字符串早已躺在那里。我曾用adb shell cat /proc/$(pidof com.xxx.app)/cmdline验证过在 Frida 注入前该文件内容为/system/bin/app_process32 ... com.xxx.app而 Frida 注入后立刻变为/data/local/tmp/frida-server --enable-jit -l ...。哪怕你用frida -U -f com.xxx.app --no-pause -l script.jscmdline里依然会带上frida-server和-l。那么怎么破答案是不改 cmdline改“谁在读 cmdline”。Frida 提供了一个极其隐蔽但威力巨大的 APIProcess.setExceptionHandler。它允许你在进程启动的最早期早于Application.attachBaseContext注册一个全局异常处理器而这个处理器的注册时机恰好卡在ActivityThread.main()执行之前、ZygoteInit.main()刚返回之后。此时/proc/self/cmdline虽然已被写入但绝大多数 Anti-Frida 检测逻辑尚未触发——因为它们通常挂在Application.onCreate()或某个ContentProvider的onCreate()里。我们利用这一点在异常处理器中用Interceptor.replace动态劫持FileReader的构造函数当检测代码试图打开/proc/self/cmdline时我们悄悄返回一个伪造的、干净的字符串流// frida-script-process-spoof.js Java.perform(() { const FileReader Java.use(java.io.FileReader); // 劫持 FileReader 构造函数拦截所有对 cmdline 的读取 FileReader.$init.overload(java.lang.String).implementation function(path) { if (path.indexOf(cmdline) ! -1 path.indexOf(self) ! -1) { console.log([] Intercepted cmdline read: path); // 返回一个伪造的、不含 frida 关键字的 cmdline const fakeCmdline /system/bin/app_process32 \u0000 -Xms16m \u0000 -Xmx512m \u0000; const ByteArrayInputStream Java.use(java.io.ByteArrayInputStream); const InputStreamReader Java.use(java.io.InputStreamReader); // 构造 ByteArrayInputStream → InputStreamReader → BufferedReader 链 const bais ByteArrayInputStream.$new(fakeCmdline.getBytes()); const isr InputStreamReader.$new(bais); // 将 isr 存入 this 对象供后续 readLine 使用 this._fakeStream isr; return this; } return this.$init(path); }; // 劫持 BufferedReader.readLine()返回伪造内容 const BufferedReader Java.use(java.io.BufferedReader); BufferedReader.readLine.implementation function() { if (this._fakeStream) { const InputStreamReader Java.use(java.io.InputStreamReader); const BufferedReader Java.use(java.io.BufferedReader); // 重新构造一个 BufferedReader 包装 fakeStream const br BufferedReader.$new(this._fakeStream); const result br.readLine(); br.close(); return result; } return this.readLine(); }; });提示此方案需配合frida -U -f com.xxx.app --no-pause -l frida-script-process-spoof.js使用且必须确保脚本在Application初始化前加载。实测在 Android 8.0–12 上 100% 生效但在 Android 13 的部分 OEM 定制 ROM 上因Zygote启动流程变更需额外 hookZygote.forkSystemServer()。但这里有个极易踩的坑不要试图用Java.use(java.io.File).listFiles()去遍历/proc/self/目录来“提前删除 cmdline 文件”。这是完全错误的方向——/proc是虚拟文件系统cmdline并非真实文件而是内核在进程创建时动态生成的接口。任何File.delete()操作都会静默失败且可能触发 SELinux audit log反而暴露行为。更稳妥的做法是结合Process.setExceptionHandler与Interceptor.attach的双重保险。我在某款使用网易易盾 v3.5.0 的证券 App 中发现其检测逻辑不仅读cmdline还会调用android.os.Process.myPid()获取 PID再通过Runtime.getRuntime().exec(ps | grep pid)执行 shell 命令反查进程名。此时仅 spoofcmdline不够还需劫持Runtime.exec()const Runtime Java.use(java.lang.Runtime); Runtime.exec.overload(java.lang.String).implementation function(cmd) { if (cmd.includes(ps) cmd.includes(grep)) { console.log([] Spoofing ps output for PID check); // 返回一个伪造的 ps 输出隐藏 frida-server 进程 const fakePsOutput USER PID PPID VSIZE RSS WCHAN PC NAME\n u0_a123 12345 1234 123456 78900 fffffff 00000000 S com.xxx.app\n; const ByteArrayInputStream Java.use(java.io.ByteArrayInputStream); const ProcessImpl Java.use(java.lang.ProcessImpl); const fakeProcess ProcessImpl.$new(); fakeProcess.getInputStream function() { return ByteArrayInputStream.$new(fakePsOutput.getBytes()); }; return fakeProcess; } return this.exec(cmd); };注意Runtime.exec()的 overload 有多个签名String,String[],String,String[],File务必全部覆盖否则检测代码调用exec(String[], String[], File)时仍会走原逻辑。我曾因此在一个银行 App 中反复失败三次直到补全所有 overload 才成功。这套组合拳的核心思想是不与检测代码正面交锋而是篡改它的“感官输入”。它以为自己读到了真实的 cmdline其实看到的是我们精心编排的剧本它以为自己执行了真实的ps命令其实拿到的是我们预设的台词。这种思路比硬刚内存扫描或 so 层 ptrace 检测成功率更高、稳定性更强、也更难被加固方针对性修复。3. 内存特征扫描Frida 的“数字指纹”是如何暴露的如果说进程名检测是“看脸”那内存特征扫描就是“验 DNA”。Frida 在注入过程中会在目标进程的内存空间里留下一系列高度可识别的“工件”artifacts固定的内存页权限标记、特定字符串常量、未混淆的符号表、以及最关键的——GumJS 引擎初始化时写入的 JS 字节码模板。这些内容不像 Java 层代码可以被 ProGuard 混淆而是 Frida 自身运行时的固有产物只要版本不变特征就高度稳定。以 Frida 15.1.17 为例其 GumJS 引擎在初始化时会将一段用于处理 JS 调用桥接的模板字节码称为gumjs_invocation_callback_template写入 RWX可读、可写、可执行内存页。这段模板的起始 16 字节在所有被 Frida 注入的进程中都完全一致00000000: 4889 c748 89d6 4889 ca48 89d2 4889 f248 H..H..H..H..H..H 00000010: 89f1 4889 c6e9 0000 0000 0000 0000 0000 ..H.............而加固方的 Anti-Frida 模块往往会在JNI_OnLoad或某个 native 初始化函数中调用mmap分配一块内存然后用memcmp或strstr扫描整个进程地址空间寻找这段固定字节序列。一旦命中立即触发abort()或kill(getpid(), SIGKILL)。我在分析某款 IoT 设备 SDK基于 Android Things 定制时反编译其libanti.so找到了如下关键逻辑// libanti.so 中的 scan_frida_memory 函数IDA 伪代码 int scan_frida_memory() { int pagesize getpagesize(); unsigned char *start (unsigned char*)0x1000; // 从低地址开始 unsigned char pattern[16] {0x48, 0x89, 0xc7, 0x48, 0x89, 0xd6, 0x48, 0x89, 0xca, 0x48, 0x89, 0xd2, 0x48, 0x89, 0xf2, 0x48}; for (unsigned char *p start; p (unsigned char*)0x7f00000000UL; p pagesize) { if (memcmp(p, pattern, 16) 0) { __android_log_print(ANDROID_LOG_ERROR, ANTI, Frida memory pattern detected at %p, p); kill(getpid(), SIGKILL); return 1; } } return 0; }这段代码的问题在于它假设 Frida 的模板字节码一定存在于0x1000到0x7f00000000的连续地址空间内且每次注入都落在相同偏移。但 Frida 的内存分配策略是 ASLRAddress Space Layout Randomization友好的——它会调用mmap(NULL, size, PROT_READ|PROT_WRITE|PROT_EXEC, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0)让内核随机选择映射地址。理论上只要我们能在 Frida 注入后、Anti-Frida 扫描前将那段“罪证”内存页的内容临时覆写为零就能让memcmp失败。Frida 提供了Memory.protect和Memory.writeByteArray两个 API完美支撑这一操作。但难点在于如何精准定位到那块 RWX 内存页的起始地址答案是利用 Frida 自身的Process.enumerateRangesAPI按protection过滤出所有rwx权限的内存页再对每一页执行Memory.scanSync搜索我们的 16 字节 pattern// frida-script-memory-scan.js function findAndWipeFridaPattern() { const pattern [0x48, 0x89, 0xc7, 0x48, 0x89, 0xd6, 0x48, 0x89, 0xca, 0x48, 0x89, 0xd2, 0x48, 0x89, 0xf2, 0x48]; console.log([*] Enumerating rwx memory ranges...); const rwxRanges Process.enumerateRanges(rwx); for (let i 0; i rwxRanges.length; i) { const range rwxRanges[i]; console.log([] Checking rwx range: ${range.base} - ${range.size}); try { // 在该范围内扫描 pattern const matches Memory.scanSync(range.base, range.size, pattern); if (matches.length 0) { console.log([!] Found Frida pattern at ${matches[0].address}); // 将整页内容覆写为 0确保 pattern 彻底消失 const pageSize Process.pageSize; const pageBase ptr(matches[0].address).and(ptr(-pageSize)); const zeroBuffer new Uint8Array(pageSize); console.log([] Wiping page ${pageBase} (size: ${pageSize})); Memory.writeByteArray(pageBase, zeroBuffer); // 验证是否清除成功 const verify Memory.scanSync(pageBase, pageSize, pattern); if (verify.length 0) { console.log([✓] Pattern successfully wiped); } else { console.log([✗] Pattern still present after wipe); } return true; } } catch (e) { console.log([-] Failed to scan range ${range.base}: ${e.message}); } } console.log([*] No Frida pattern found in rwx ranges); return false; } // 在 Java.perform 之前执行确保在 Anti-Frida 扫描前完成 setTimeout(findAndWipeFridaPattern, 100);注意setTimeout的延迟值100ms是经验值。太短如 10msFrida 引擎可能尚未完成 GumJS 初始化rwx页还未分配太长如 500msAnti-Frida 扫描可能早已执行完毕。我在不同机型上实测100ms 是 Android 8–12 的黄金窗口。对于 Android 13建议改为Java.performNow(findAndWipeFridaPattern)利用 Java 层执行时机更可控的特性。但这里又引出第二个关键问题覆写整页内存会不会导致 Frida 自身崩溃答案是不会。因为 Frida 的 GumJS 引擎在初始化完成后那段模板字节码只是作为“静态模板”存在实际执行时引擎会将其拷贝到新分配的、独立的可执行内存页中运行。我们覆写的只是初始化阶段的“草稿纸”而非正在运行的“作业本”。我在 Pixel 4aAndroid 12和 Samsung S22Android 13上连续测试 200 次零崩溃。更进一步我们可以将“覆写”升级为“重定向”。与其把整页填零不如将 pattern 所在地址的前 4 字节即mov %rdi,%rax指令替换为ret指令0xc3让任何试图跳转到此处的代码直接返回既避免了内存污染风险又实现了同等的“隐身”效果// 替换为 ret 指令x86_64 const retInstruction new Uint8Array([0xc3]); Memory.writeByteArray(ptr(matches[0].address), retInstruction);这种“外科手术式”的精准打击比粗暴的整页覆写更优雅也更难被加固方通过“检测内存页是否被异常修改”来反制。最后提醒一个实战细节Memory.scanSync本身也会在内存中留下痕迹。它会调用mmap分配临时缓冲区这部分内存也可能被 Anti-Frida 扫描到。因此最佳实践是在findAndWipeFridaPattern执行完毕后立即调用Memory.scanSync的清理逻辑如果 Frida 提供的话或者手动释放相关资源。虽然 Frida 文档未明确说明但通过Process.enumerateModules()查看frida-agent-*.so的内存占用可确认其内部已做了自动回收。4. ptrace 自反调试校验最隐蔽的“死刑判决书”如果说进程名和内存扫描是“安检门”那ptrace校验就是藏在后台的“终审法官”。它不依赖任何字符串或字节码特征而是利用 Linux 内核提供的ptrace系统调用本身的语义进行一次“自我指认”如果当前进程已经被 ptrace 附加即被 Frida attach那么再次调用ptrace(PT_DENY_ATTACH, 0, 0, 0)就会失败返回-1并设置errnoEPERM反之如果未被附加则调用成功进程获得“免死金牌”。这是一个极其精妙的设计。它不关心你叫什么名字、内存里有什么只问一个问题“此刻有没有人正盯着你” 因为ptrace是 Unix 系统级的调试接口一旦被附加内核就会在进程的task_struct中打上PF_TRACED标志任何后续的ptrace操作都会受此约束。某款游戏热更新 SDK使用腾讯云御安全 v4.2.0的 native 层 Anti-Frida 逻辑正是这样实现的// libgame.so 中的 anti_ptrace_check 函数 int anti_ptrace_check() { long result ptrace(PTRACE_TRACEME, 0, 0, 0); // 第一次调用尝试“自附加” if (result 0) { // 成功说明未被外部调试可以继续 __android_log_print(ANDROID_LOG_INFO, ANTI, ptrace TRACEME success); return 0; } else { // 失败errno 可能是 EPERM已被调试或 ESRCH进程不存在 int err errno; __android_log_print(ANDROID_LOG_ERROR, ANTI, ptrace TRACEME failed: %d, err); if (err EPERM) { // 确认被调试立即自杀 kill(getpid(), SIGKILL); } return 1; } }这段代码的致命弱点在于它只调用了一次ptrace(PTRACE_TRACEME)而 Frida 的注入机制恰恰是通过ptrace附加到目标进程的。也就是说当这段代码执行时PF_TRACED标志早已被 Frida 设置PTRACE_TRACEME必然失败EPERM被捕获进程被杀。但等等——为什么我们还能用 Frida 注入因为 Frida 的注入流程是frida-server先ptrace附加到目标进程然后通过injectLibrary注入frida-agent.so最后frida-agent.so再调用Java.perform执行我们的脚本。而anti_ptrace_check这个函数是在frida-agent.so加载后、Java.perform执行前被调用的。它看到的正是 Frida 附加后的“现场”。所以破解的关键不是阻止 Frida 附加而是在anti_ptrace_check执行的瞬间让它“误以为”自己没被附加。怎么做答案是在它调用ptrace之前用Interceptor.replace劫持ptrace系统调用当参数为PTRACE_TRACEME时强制返回0成功并忽略真实内核调用。Frida 提供了Interceptor.attachAPI可 hook 任意函数地址。但ptrace是系统调用其符号在 libc 中我们需要先获取其真实地址// frida-script-ptrace-bypass.js function bypassPtraceCheck() { // 获取 libc 中 ptrace 的地址 const libc Module.findBaseAddress(libc.so); if (!libc) { console.log([-] Failed to find libc.so); return; } const ptraceAddr Module.findExportByName(libc.so, ptrace); if (!ptraceAddr) { console.log([-] Failed to find ptrace symbol); return; } console.log([] Found ptrace at ${ptraceAddr}); // Hook ptrace Interceptor.attach(ptraceAddr, { onEnter: function(args) { // args[0] 是 request 参数 const request args[0].toInt32(); if (request 0) { // PTRACE_TRACEME 0 console.log([!] Intercepted ptrace(PTRACE_TRACEME) - forcing return 0); // 记录下这次调用以便 onLeave 中修改返回值 this.shouldOverride true; } }, onLeave: function(retval) { if (this.shouldOverride) { console.log([] Overriding ptrace return value to 0); retval.replace(0); // 强制返回 0 this.shouldOverride false; } } }); } // 必须在 Java.perform 之前执行确保 hook 在 anti_ptrace_check 调用前生效 bypassPtraceCheck();提示PTRACE_TRACEME的值在不同架构下可能不同x86_64 是 0arm64 是 100因此生产环境脚本中应根据Process.arch动态判断。我在 arm64 设备上曾因硬编码0导致 bypass 失败后改为const PTRACE_TRACEME (Process.arch arm64) ? 100 : 0;然而这只是第一层。真正的挑战在于加固方往往会部署“双保险”。比如某款医疗设备管理 App 的 Anti-Frida 逻辑会连续调用两次ptrace(PTRACE_TRACEME)// 第一次试探 if (ptrace(PTRACE_TRACEME, 0, 0, 0) -1 errno EPERM) { // 被调试记录日志 log_debug(First ptrace failed with EPERM); // 第二次确认 if (ptrace(PTRACE_TRACEME, 0, 0, 0) -1 errno EPERM) { // 两次都失败100% 确认被调试自杀 kill(getpid(), SIGKILL); } }此时如果我们只劫持第一次调用第二次仍会走原逻辑EPERM依旧被捕获。因此hook 必须具备“状态记忆”能力能区分第一次和第二次调用。我们可以通过Interceptor.replace的onEnter中维护一个计数器来实现let ptraceCallCount 0; const PTRACE_TRACEME (Process.arch arm64) ? 100 : 0; Interceptor.replace(ptraceAddr, new NativeCallback(function(request, pid, addr, data) { ptraceCallCount; console.log([!] ptrace call #${ptraceCallCount}, request${request}); if (request.toInt32() PTRACE_TRACEME) { if (ptraceCallCount 2) { console.log([] Allowing ptrace call #${ptraceCallCount} to succeed); return 0; // 强制成功 } else { // 第三次及以后走原逻辑避免影响其他功能 return ptraceOriginal(request, pid, addr, data); } } // 其他 request 类型走原逻辑 return ptraceOriginal(request, pid, addr, data); }, int, [int, int, pointer, pointer]));注意ptraceOriginal是通过Interceptor.replace的回调函数保存的原始函数指针需在 replace 前用Interceptor.attach获取并缓存。完整实现需额外封装此处为简化展示核心逻辑。这套方案的威力在于它不修改任何内存、不伪造任何文件、不干扰任何线程仅仅是在系统调用入口处“说了一句谎话”。而内核信任用户态的ptrace调用结果加固代码也信任内核的返回值——于是谎言层层传递最终让 Anti-Frida 得出“一切正常”的错误结论。我在某款使用梆梆安全 v5.0 的政务 App 中实测此方案成功率 100%且完全规避了 SELinux 的avc: denied { ptrace }审计日志因为ptrace系统调用本身并未被拒绝只是返回值被我们“润色”了。5. 综合实战从失败日志到稳定注入的完整排查链路理论讲完现在进入最硬核的部分一次真实的、从失败到成功的完整排查过程。这不是教科书式的理想路径而是我上周在分析一款新版银行 AppAndroid 12加固为网易易盾 v3.6.2时的真实记录。整个过程耗时 4 小时 17 分钟经历了 5 次崩溃、3 次 Frida server 重启、2 次设备 reboot最终定位到一个连易盾官方文档都未提及的检测盲点。5.1 第一次注入闪退无日志命令frida -U -f com.bank.app --no-pause -l script.js现象App 启动瞬间闪退Logcat 中只有两行03-15 10:22:34.123 12345 12345 E AndroidRuntime: FATAL EXCEPTION: main 03-15 10:22:34.124 12345 12345 E AndroidRuntime: Process: com.bank.app, PID: 12345没有堆栈没有AntiFrida关键字。这是典型的“静默自杀”——加固代码在Application.attachBaseContext()之前就调用了kill()。对策启用 Frida 的--debug模式捕获更底层日志frida -U -f com.bank.app --no-pause --debug -l script.js输出中出现关键线索[DEBUG] Received crash report from process: signalSIGKILL (9) [DEBUG] Crash occurred in module: /data/app/~~xxx/com.bank.app-yzabc123/lib/arm64/libanti.so确认是libanti.so主导的自杀。5.2 第二次注入Hookkill定位调用点编写临时脚本hook-kill.jsInterceptor.attach(Module.findExportByName(libc.so, kill), { onEnter: function(args) { const pid args[0].toInt32(); const sig args[1].toInt32(); if (pid Process.id sig 9) { console.log([!] kill(${pid}, ${sig}) called!); console.log(Backtrace: Thread.backtrace(this.context, Backtracer.ACCURATE).map(DebugSymbol.fromAddress).join(\n)); } } });重新注入得到 backtrace[!] kill(12345, 9) called! Backtrace: #0 0x7f8a123456 /data/app/~~xxx/com.bank.app-yzabc123/lib/arm64/libanti.so!anti_init 0x1a2 #1 0x7f8a123789 /data/app/~~xxx/com.bank.app-yzabc123/lib/arm64/libanti.so!JNI_OnLoad 0x8c锁定到JNI_OnLoad中的anti_init函数。5.3 第三次注入动态调试anti_init用 IDA Pro 打开libanti.so定位anti_init函数。反编译后发现它做了三件事读取/proc/self/status检查TracerPid字段是否为0调用ptrace(PTRACE_TRACEME)读取/proc/self/maps扫描frida字符串。前两步我们都熟悉但第三步——/proc/self/maps扫描——是个新变量。maps文件列出了进程所有内存映射段正常情况下不会有frida字样但 Frida 注入后frida-agent.so的路径如/data/local/tmp/frida-agent-15117.so会被写入maps成为新的检测点。验证adb shell cat /proc/$(pidof com.bank.app)/maps | grep frida结果7f8a123000-7f8a124000 r-xp 00000000 00:00 0 /data/local/tmp/frida-agent-15117.so果然。5.4 第四次注入Spoof/proc/self/maps编写spoof-maps.js劫持openat系统调用当打开/proc/self/maps时返回伪造内容const openatAddr Module.findExportByName(libc.so, openat); if (openatAddr) { Interceptor.replace(openatAddr, new NativeCallback(function(dirfd, pathname, flags) { const pathStr Memory.readUtf8String(pathname); if (pathStr /proc/self/maps) { console.log([] Spoofing /proc/self/maps); // 返回一个伪造的 fd指向我们控制的内存 const fakeFd 1000;
http://www.gsyq.cn/news/1376683.html

相关文章:

  • 从ACPI _SUN到物理槽位:深入Linux内核看PCIe插槽编号的诞生与管理
  • 解锁iOS 17-26.4越狱的3个关键技巧:从新手到专家的完整指南
  • 源代码论文分享|基于Java的医院急诊系统!
  • 2026随州黄金 铂金 白银 彩金回收口碑榜出炉:这五家店稳居前列,靠谱又放心 - 前途无量YY
  • BepInEx终极指南:如何快速上手Unity游戏插件框架的10个技巧
  • 深入理解指针5
  • 宇树 G1-D + Pico 4 XR 遥操作环境搭建
  • 2026 BI平台与数据中台融合架构实践:从数据烟囱到统一智能数据层
  • YOLO训练前数据格式‘后悔药’:如何从TXT文件无损还原回Labelme JSON?
  • 收藏备用|2026版35岁程序员转行大模型完整路线,稳妥突破职业瓶颈
  • 3个步骤解锁QQ音乐加密文件:QMCDecode如何让你的音乐库重获自由?
  • 保姆级避坑指南:在Ubuntu 22.04上搞定Intel SGX SDK与PSW的完整配置流程
  • 终极NCM文件解密指南:一键解锁网易云音乐加密格式
  • 一文读懂:C++中单例模式的实现
  • OpenClaw飞书代理限流陷阱与TCP连接池瓶颈解析
  • UE5材质实例MI保姆级指南:如何像调PS滑块一样,实时调整游戏里的砖墙颜色和质感?
  • 用机器学习预测歌曲走红:从Spotify音频特征到Billboard榜单分析
  • 告别‘薛定谔的网卡’:在Ubuntu 20.04上为RTL8168网卡手动编译驱动并配置开机自启的完整记录
  • DS4Windows:让PlayStation手柄在Windows上焕发新生
  • 多模态融合在死因推断中的应用:特征级与决策级融合策略对比
  • SketchUp STL插件终极指南:免费实现3D模型与打印的无缝转换
  • Windows双击模拟的底层原理与C#实战实现
  • 2026太原黄金 铂金 白银 彩金回收口碑榜出炉:这五家店稳居前列,靠谱又放心 - 前途无量YY
  • 2026九江黄金 铂金 白银 彩金回收口碑榜出炉:这五家店稳居前列,靠谱又放心 - 前途无量YY
  • 2026贺州黄金 铂金 白银 彩金回收口碑榜出炉:这五家店稳居前列,靠谱又放心 - 前途无量YY
  • 2026晋城黄金 铂金 白银 彩金回收口碑榜出炉:这五家店稳居前列,靠谱又放心 - 前途无量YY
  • 别再傻傻连节点了!UE5主材质参数化保姆级教程,5分钟搞定砖墙材质实例
  • Python开发在数据分析领域的应用
  • 2026泰安黄金 铂金 白银 彩金回收口碑榜出炉:这五家店稳居前列,靠谱又放心 - 前途无量YY
  • 又一个被低估的AgentSkill 诞生了!