1. 项目概述当App的“门卫”盯上了libc.so在Android逆向分析的世界里我们常常会遇到各种“门卫”——也就是应用内置的安全检测机制。它们负责检查应用的完整性防止被调试、被注入或者被篡改。其中对关键系统库libc.so进行CRC循环冗余校验校验是一种相当常见且有效的防护手段。简单来说App会在启动或运行的关键节点计算当前设备中libc.so文件的CRC值并与一个预设的“正确”值进行比较。一旦发现不匹配就判定环境异常轻则功能受限重则直接闪退。这个机制之所以有效是因为像Frida这样的动态插桩工具其核心工作原理就是向目标进程注入一个Agent代理。这个Agent在运行时往往会修改进程的内存布局或加载额外的库这就有可能间接影响到libc.so在内存中的状态或者改变其被加载的依赖关系从而导致计算出的CRC值与原始值产生偏差。于是你的Frida刚刚附加上去App就“自杀”了逆向工作还没开始就宣告结束。我最近在分析一个金融类App时就撞上了这堵墙。常规的绕过签名校验、反调试的方法都试过了App依然能在Frida附着的瞬间精准“自爆”。通过日志分析和动态跟踪最终把问题定位到了它对libc.so的CRC检测上。今天要分享的就是如何用Frida这把“万能钥匙”巧妙地骗过这个“门卫”让我们的分析工具能够安稳地运行起来。这个方法不仅适用于本例对于任何采用类似CRC校验逻辑的Native层C/C层防护都有很高的参考价值。2. 核心原理与对抗思路拆解2.1 CRC校验在Android安全中的角色CRC校验本质上是一种哈希函数用于检测原始数据在传输或存储后是否发生变化。在Android安全领域开发者将其用于保护核心的共享库文件如libc.so。libc.so是C语言标准库在Android上的实现提供了最基础的系统调用封装、内存管理、字符串操作等函数。几乎所有的Android应用无论是Java还是Native开发都依赖于它。攻击者如果能够篡改libc.so或者劫持其内部的函数例如open、read、strcmp等就能实现非常底层的Hook从而绕过大量基于Java层的安全检测。因此保护libc.so的完整性就成为了应用安全特别是Native层安全的一道重要防线。应用的检测逻辑通常如下定位文件在运行时通过dlopen、/proc/self/maps或直接文件路径等方式确定libc.so在内存中的加载基址或其在文件系统中的路径。计算校验值读取libc.so文件或其在内存中的特定段如.text代码段使用CRC32算法计算其校验和。比对与裁决将计算出的CRC值与一个硬编码在程序中的、或从服务器下发的“合法”值进行比对。如果不一致则触发反制逻辑如调用exit()或abort()。2.2 Frida的介入如何触发检测Frida的工作模式决定了它很容易触发这类检测进程内存布局改变Frida注入的frida-agent.so会作为一个新的动态库被加载到目标进程的内存空间。这会改变/proc/self/maps中记录的库列表和内存布局。有些检测代码会遍历maps检查是否有未知的库如包含“frida”字样的库被加载。链接与依赖关系虽然Frida Agent不直接链接libc.so但它的加载过程可能涉及动态链接器的活动在某些极端情况下可能被间接感知。Hook行为本身更高级的检测会监控关键函数如dlopen、dlsym是否被非法调用或篡改。Frida的Interceptor.attach就是通过修改函数入口处的指令来实现的这种对代码段的修改本身就是CRC校验要防范的。2.3 我们的绕过策略偷梁换柱与检测逻辑硬碰硬比如尝试去修复CRC值通常比较复杂因为你需要精确知道对方计算的是哪一段内存、用的哪种CRC变种、以及正确的值是什么。一个更巧妙且通用的思路是拦截Hook负责执行CRC比对的函数。我们不需要改变libc.so本身也不需要知道正确的CRC值是多少。我们只需要让这个比对函数“说谎”永远返回“比对成功”的结果即可。具体到实现上我们分为两个层面Java层拦截如果检测逻辑是用Java/Kotlin编写的并且调用了JNI函数来计算CRC我们可以直接Hook这个Java方法让其返回true或预期的值。Native层拦截这是更常见的情况。检测逻辑位于.so库文件中。我们需要定位关键函数使用逆向工具如IDA Pro, Ghidra或动态分析Frida的Stalker或Trace找到执行CRC计算和比对的函数。通常函数名可能包含check、verify、crc、integrity等关键词。Hook关键点使用Frida的Interceptor来Hook这个函数。我们的目标不是阻止它执行而是在它即将返回结果时篡改返回值比如将表示失败的0改为成功的1或者直接替换整个函数体让其不做任何计算直接返回成功。本次实战我们面对的就是一个在Native层一个名为libsecurity.so的库中实现的、对libc.so进行CRC校验的案例。3. 环境准备与工具链配置3.1 基础环境搭建工欲善其事必先利其器。一个稳定、干净的分析环境是成功的第一步。测试设备推荐使用一台Root后的Android真机。模拟器如雷电模拟器虽然方便但许多加固和检测方案会针对模拟器环境进行特殊处理增加不确定性。真机更能反映真实对抗环境。Frida环境PC端通过pip安装pip install frida-tools。安装后使用frida --version和frida-ps --version验证。Android端下载与PC端Frida版本对应的frida-server。例如PC端是16.1.4则需下载frida-server-16.1.4-android-arm64.xz根据手机CPU架构选择。解压后通过adb推送到手机并赋予执行权限adb push frida-server /data/local/tmp/ adb shell chmod 755 /data/local/tmp/frida-server adb shell /data/local/tmp/frida-server 版本对应务必确保frida-tools与frida-server的主版本号一致否则可能出现连接失败或协议错误。逆向分析工具IDA Pro 或 Ghidra 用于静态分析.so文件理解其校验逻辑。Jadx-GUI 用于快速浏览Java代码寻找可能的入口点。3.2 目标App分析与关键函数定位在编写Frida脚本之前我们必须先找到要Hook的目标。初步侦察将目标APK文件拖入Jadx全局搜索关键词如“crc”、“libc”、“check”、“verify”、“integrity”。重点关注System.loadLibrary加载的库名本例中我们发现了libsecurity.so。静态分析.so用IDA Pro打开libsecurity.so可以从APK的lib/目录解压或从运行中的进程/data/app/.../lib/目录dump。在导出函数Exports和字符串Strings视图中搜索“crc”、“libc”。很快我们发现了一个名为Java_com_example_app_SecurityHelper_checkLibcIntegrity的JNI函数以及内部调用的一个关键函数native_check_libc_crc。动态验证为了确认我们的猜想写一个简单的Frida脚本枚举libsecurity.so的所有导出函数并尝试Hook那个看起来最可疑的native_check_libc_crc打印其参数和返回值。// 侦察脚本 setTimeout(function() { Module.enumerateExportsSync(libsecurity.so).forEach(function(exp) { console.log(exp.name at exp.address.toString()); }); }, 0);运行后我们确认了native_check_libc_crc的存在并且通过附加进程发现每次App启动时它都会被调用返回一个整数0或1返回0后App立刻崩溃。注意很多加固方案会混淆函数名和字符串。如果找不到明显的关键词就需要结合动态跟踪。使用Frida的Stalker跟踪线程执行流或者用Interceptor.attach去Hookdlopen、dlsym观察在触发崩溃前哪些自定义函数被加载和调用这是一个需要耐心的过程。4. Frida脚本实战逆向与Hook经过分析我们确定了攻击点libsecurity.so中的native_check_libc_crc函数。它的逻辑是计算libc.so的CRC32值与内置值比较一致则返回1不一致则返回0。我们的目标就是让它永远返回1。4.1 脚本结构与核心API下面是一个完整、可直接使用的Frida脚本包含了详细的注释。// frida_bypass_libc_crc.js // 作者一个被CRC检测折磨过的逆向工程师 // 功能Hook libsecurity.so 中的CRC校验函数强制返回成功。 Java.perform(function () { console.log([*] 脚本已加载开始寻找目标...); // 方案一直接Hook Native函数最直接有效 var libsecurity Module.findBaseAddress(libsecurity.so); if (libsecurity) { console.log([] libsecurity.so 基址: libsecurity); // 关键找到目标函数的偏移量或符号 // 这里假设我们通过静态分析知道 native_check_libc_crc 的偏移是 0x1234 // 或者如果符号未剥离可以直接用 Module.findExportByName var checkFuncAddr Module.findExportByName(libsecurity.so, native_check_libc_crc); if (!checkFuncAddr) { // 如果符号被剥离使用偏移量。这个偏移量需要从IDA中获取。 // 例如IDA中显示函数地址为 0x76B81234模块基址为 0x76B80000则偏移为 0x1234 var offset 0x1234; // 请替换为实际偏移量 checkFuncAddr libsecurity.add(offset); console.log([!] 未找到导出符号使用偏移量计算地址: checkFuncAddr); } else { console.log([] 找到函数符号地址: checkFuncAddr); } // 使用 Interceptor 进行 Hook Interceptor.attach(checkFuncAddr, { onEnter: function (args) { console.log([*] native_check_libc_crc 被调用); // 可以在这里打印参数辅助分析 // console.log(参数0: args[0]); // console.log(参数1: args[1]); }, onLeave: function (retval) { console.log([*] 函数原始返回值: retval); // 核心操作将返回值替换为 1 (成功) // 根据函数签名返回值可能是32位整数。使用 .replace() 进行修改。 var newRetval ptr(1); // 将返回值指针指向数值1 retval.replace(newRetval); console.log([] 已将返回值篡改为: 1 (强制成功)); } }); console.log([] Native 层 Hook 安装完成); } else { console.log([-] 未找到 libsecurity.so可能库名不同或尚未加载。); } // 方案二Hook Java层的JNI调用备用方案 // 如果Native函数难以定位可以尝试从Java层入手。 var SecurityHelper Java.use(com.example.app.SecurityHelper); if (SecurityHelper) { // 假设调用Native方法的Java函数叫 doNativeCheck() SecurityHelper.doNativeCheck.implementation function () { console.log([*] Java层检测函数被调用直接返回true); return true; // 直接返回成功阻止其调用真正的Native函数 // 注意如果Native函数有副作用如初始化其他东西直接返回可能有问题。 }; console.log([] Java 层 Hook 安装完成); } console.log([*] 所有Hook点部署完毕CRC检测已失效。); });4.2 脚本关键点解析与避坑指南基址与偏移Module.findBaseAddress获取的是模块在目标进程内存中的实际加载基址每次运行可能不同。Module.findExportByName是最理想的方式但如果符号被剥离stripped就需要借助静态分析工具获取函数在模块内的相对偏移量RVA然后加上基址得到绝对地址。Interceptor.attach的onLeave这是篡改返回值的黄金时机。retval是一个NativePointer对象指向函数返回值所在的内存。使用retval.replace(ptr(newValue))可以修改它。务必注意函数返回类型如果是int就替换为ptr(1)如果是bool可能也是1表示真。返回值类型如果函数返回的是结构体或字符串指针修改会复杂得多。本例中通过逆向分析看函数调用上下文或反编译代码确认了返回的是简单的int0失败1成功。执行时机脚本需要在目标函数被调用之前注入。因此通常使用frida -U -f com.example.app -l script.js --no-pause在App启动时立即注入或者先启动App再用frida -U -n com.example.app -l script.js附加。实操心得在实际操作中经常遇到函数符号被彻底剥离的情况。这时动态定位就变得至关重要。一个技巧是先让App运行起来在触发崩溃的瞬间用frida-trace快速跟踪所有库的调用frida-trace -U -n App Name -i *check* -i *crc*。这能帮你快速缩小目标函数的范围。5. 进阶对抗应对反Frida检测你以为Hook了CRC检测就高枕无忧了吗道高一尺魔高一丈。很多安全方案会集成反Frida检测。它们会主动探测Frida的存在如果发现同样会触发退出。我们的绕过脚本本身就可能成为被检测的目标。因此一个完整的绕过方案必须是立体的。5.1 常见反Frida手段及绕过检测方法原理Frida绕过思路端口扫描检测默认的Frida Server端口27042是否开放。1.更改端口启动frida-server时指定非默认端口./frida-server -l 0.0.0.0:8080脚本连接时使用frida.get_device_manager().add_remote_device(192.168.x.x:8080)。2.Hook网络相关函数如connect、bind当发现目标端口是27042时返回错误或重定向。进程/模块枚举遍历/proc/self/maps或/proc/self/task/.../maps查找包含“frida”、“gum-js”、“gumpp”等关键词的模块枚举进程查找frida-server。Hookfopen、read、fgets等文件读取函数当路径包含/proc/self/maps或读取内容包含frida关键词时返回伪造的、干净的内容。特征字符串检查在内存中搜索Frida Agent特有的字符串如“LIBFRIDA”、“GumJS”等。修改Frida Agent的二进制文件将这些特征字符串替换为无意义的字符。或者Hook内存扫描函数如strstr、memmem过滤掉相关结果。D-Bus接口探测Frida使用D-Bus进行通信检测特定的D-Bus服务名是否存在。修改Frida的D-Bus服务名或者Hook D-Bus相关函数隐藏服务。线程名检测Frida会创建一些具有特定名称如“gmain”、“gdbus”的线程。Hookpthread_create或prctl(PR_SET_NAME)修改线程名。5.2 增强版脚本集成反反Frida我们在主脚本的基础上可以增加一个反反Frida模块。以下是一个针对“端口扫描”和“maps检测”的增强示例// 反反Frida模块 function antiAntiFrida() { // 1. 绕过 /proc/self/maps 检测 var fopen Module.findExportByName(null, fopen); if (fopen) { Interceptor.attach(fopen, { onEnter: function (args) { this.path args[0].readCString(); if (this.path this.path.indexOf(/proc/self/maps) ! -1) { console.log([!] 检测到 maps 文件读取: this.path); // 这里可以记录但更高级的做法是后续Hook read来过滤内容 } } }); } // 2. 过滤 maps 文件内容 (Hook read) var read Module.findExportByName(null, read); if (read) { Interceptor.attach(read, { onEnter: function (args) { this.fd args[0]; this.buf args[1]; this.count args[2].toInt32(); }, onLeave: function (retval) { // 如果读取成功且是我们关心的文件描述符需要结合fopen Hook来跟踪fd // 这里简化处理假设我们已经知道目标fd或者遍历所有read调用 // 更稳健的做法是维护一个fd到path的映射 var numBytes retval.toInt32(); if (numBytes 0) { // 读取内容 var content this.buf.readByteArray(numBytes); var contentStr ; for (var i 0; i numBytes; i) { contentStr String.fromCharCode(content[i]); } // 检查并过滤frida关键词 if (contentStr.indexOf(frida) ! -1 || contentStr.indexOf(gum-js) ! -1 || contentStr.indexOf(libfrida) ! -1) { console.log([] 检测到maps中包含Frida特征尝试过滤...); // 注意直接修改内存中的内容非常危险且复杂容易崩溃。 // 更常见的做法是阻止检测函数得到真实结果而非修改这里。 // 此处仅作演示实际应用需结合具体检测代码逻辑。 } } } }); } // 3. 绕过基于线程名的检测 (Hook prctl) var prctl Module.findExportByName(null, prctl); if (prctl) { Interceptor.attach(prctl, { onEnter: function (args) { var option args[0].toInt32(); // PR_SET_NAME 15 if (option 15) { var threadNamePtr args[2]; if (threadNamePtr) { var oldName threadNamePtr.readCString(); // 如果线程名是Frida相关的可以偷偷改掉 if (oldName (oldName.indexOf(gmain) ! -1 || oldName.indexOf(gdbus) ! -1)) { console.log([!] 尝试修改Frida线程名: oldName); // 注意直接修改字符串常量可能无效或崩溃。更好的方法是提前Hook创建线程的地方。 } } } } }); } console.log([*] 反反Frida模块加载完成。); } // 在主函数中调用 Java.perform(function () { antiAntiFrida(); // 先部署反反Frida // ... 然后部署之前的主Hook逻辑 (CRC绕过) bypassCRCCheck(); });重要警告反反Frida是一个深度对抗的过程上述代码仅为思路演示。粗暴地Hook系统调用和修改内存极易导致进程不稳定崩溃或死锁。在实际操作中务必先精准定位App具体使用了哪种检测方法然后进行针对性的、最小化的Hook并充分测试。6. 问题排查与实战心得即使脚本写得再完美在实际操作中也会遇到各种问题。下面是我在多次实战中总结的常见问题与解决方案。6.1 常见问题速查表问题现象可能原因排查步骤与解决方案TypeError: cannot read property add of nullModule.findBaseAddress返回null意味着目标模块尚未加载。1. 检查模块名称拼写是否正确区分大小写。2. 在Java.perform内使用setTimeout延迟执行Hook代码等待模块加载setTimeout(function(){...}, 1000)。3. 监听模块加载事件Module.load。Error: access violation accessing 0x...Hook的函数地址不正确或者函数原型参数/返回值判断错误导致访问非法内存。1. 双重检查函数地址。使用frida-trace或console.log(hexdump(addr))确认地址是否指向有效的代码段。2. 使用NativeFunction原型来辅助定义确保调用约定正确。Hook成功但App仍然崩溃1. 检测点不止一个你只绕过了一处。2. 检测逻辑在Hook生效前就已执行完毕。3. 你的Hook操作本身引发了新的崩溃如参数处理错误。1. 扩大搜索范围寻找其他可能的校验函数。2. 尝试更早地注入脚本使用-f在App启动时注入。3. 简化Hook脚本onEnter和onLeave中只做最简单的返回值替换排除其他干扰。Frida无法附加进程1.frida-server未运行或版本不匹配。2. App有强大的反调试/反附加保护如ptrace保护。3. SELinux策略限制。1. 检查adb shell ps脚本注入后系统卡顿或死机Hook了过于底层或频繁调用的函数如open、read导致性能瓶颈或递归死锁。1. 避免Hook高频函数。如果必须Hook在onEnter中尽快判断是否为目标调用不是则立即返回。2. 使用Interceptor.replace完全替换函数而不是attach性能更好。6.2 个人实战心得静态分析与动态分析结合不要只依赖一种方法。静态分析IDA给你地图动态分析Frida给你实时的导航。先用静态分析找到可疑函数再用Frida去动态验证它的行为参数、返回值、调用栈。由外而内逐步深入先尝试从Java层Hook如果不行再深入JNI最后才是纯Native层的函数。Java层Hook通常更简单、稳定。保持脚本的简洁与可调试性在开发阶段多使用console.log()打印关键信息地址、参数、返回值。一旦脚本稳定再考虑移除日志以减少痕迹。理解原理胜过复制脚本本文提供的脚本是一个模板但每个App的校验逻辑都可能不同。关键是理解“定位关键函数”和“篡改返回值”这两个核心思想。你需要根据实际情况调整目标函数名、偏移量和返回值修改逻辑。测试环境隔离在进行此类对抗时最好使用专门的测试机或模拟器。避免在主用手机或装有敏感信息的设备上操作以防App存在更激进的反制措施导致设备异常。绕过libc.so的CRC检测只是Android Native层安全对抗的一个缩影。它考验的是逆向工程师对Linux/Android底层机制的理解、对逆向工具的熟练运用以及耐心和细心。当你成功绕过检测看到Frida顺利附着并能开始自由探索App内部逻辑时那种成就感无疑是巨大的。希望这篇详实的实战记录能为你打开一扇门让你在Android逆向的道路上走得更远。记住工具是死的思路是活的。