Android逆向实战:Frida动态Hook绕过广告SDK与签名校验

发布时间:2026/7/3 21:09:35
Android逆向实战:Frida动态Hook绕过广告SDK与签名校验 1. 项目概述与核心挑战在Android应用逆向分析这条路上走得久了你会发现很多商业应用为了保护自身利益和用户体验会设置越来越复杂的防御机制。其中广告SDK的强制加载和动态签名校验是两道非常典型且棘手的“关卡”。前者直接影响用户的使用体验和应用的纯净度后者则是开发者为了防止应用被篡改、二次打包而设置的核心防线。我最近在分析一个集成了AdMob广告SDK并且采用了运行时签名校验的应用时就遇到了这个组合难题。表面上看应用运行正常但一旦尝试通过常规的AndroidManifest.xml修改或资源替换来屏蔽广告应用要么直接崩溃要么广告依然“顽强”地显示出来。这背后就是广告SDK的深度集成校验与动态签名校验在协同工作。本次实战我将带你一步步拆解这个案例分享如何定位关键校验点、使用Frida进行动态Hook绕过、以及处理So层加固校验的进阶技巧。无论你是想学习更深层的逆向思路还是在实际项目中遇到了类似困扰相信这篇详尽的复盘都能给你带来直接的帮助。2. 逆向环境与目标应用分析2.1 环境与工具链准备工欲善其事必先利其器。一个稳定、高效的逆向环境是成功的第一步。我的主力环境是一台x86_64架构的Ubuntu 22.04虚拟机当然Windows 10/11配合WSL2也是绝佳选择。核心工具链如下反编译与静态分析Jadx-GUI这是首选的Java反编译器它的图形化界面和强大的代码搜索、跳转功能能极大提升静态分析的效率。我通常使用其GitHub仓库发布的最新版本。Apktool用于解包APK获取AndroidManifest.xml、resources.arsc及dex文件。这是修改资源、进行重打包的必经之路。命令很简单apktool d target.apk -o output_dir。Bytecode Viewer或Fernflower作为Jadx的补充有时不同反编译器对混淆代码的呈现略有差异交叉查看能帮助理解。动态调试与运行时干预Frida本次实战的绝对核心。它是一个动态代码插桩工具允许你向目标进程注入自己的JavaScript代码来Hook函数、修改内存、调用方法等。我们需要在电脑上安装Frida客户端pip install frida-tools并在目标Android设备实体机或模拟器上运行对应架构的Frida-server。ADB (Android Debug Bridge)连接设备和电脑的桥梁用于安装应用、推送文件、端口转发等。确保adb devices能正确列出你的设备。一台已Root的Android手机或模拟器这是运行Frida-server和进行深度Hook的前提。我推荐使用官方Android Studio自带的模拟器AVD并选择带有“Google Play”标志的系统镜像因为它自带Root权限可通过adb root命令获取兼容性最好。网上很多教程让你刷Magisk但对于逆向调试模拟器是更干净、可快速重置的选择。目标应用初步侦察 拿到目标APK后不要急于扔进Jadx。先用keytool或apksigner检查其签名信息了解其签名算法和证书哈希。然后使用apktool解包快速浏览AndroidManifest.xml重点关注application标签下的android:name自定义Application类、meta-data标签可能配置了广告App ID或校验密钥以及所有service、receiver和provider广告SDK和校验逻辑可能藏身于此。最后用Jadx打开APK进行全局搜索。2.2 目标应用防御机制初探将目标APK载入Jadx后我首先进行了几轮关键词搜索广告相关搜索“AdMob”、“GoogleAd”、“ads”、“banner”、“interstitial”、“rewarded”。很快我发现了com.google.android.gms.ads包下的类被大量引用确认了AdMob SDK的存在。此外应用自身还有一个AdManager类负责统一控制广告的加载、显示与隐藏。签名校验相关搜索“signature”、“package”、“getPackageManager”、“PackageInfo”。静态分析发现了多处context.getPackageManager().getPackageInfo(...).signatures的调用。但这只是静态校验更关键的是动态校验——即应用在运行时可能从服务器获取一个预期的签名哈希与当前应用的签名进行比对。这种校验逻辑可能被混淆且触发时机不定。Native层线索在Jadx中搜索“System.loadLibrary”或“native”关键字发现应用加载了一个名为securitycheck的本地库.so文件。这强烈暗示核心的、高强度的校验逻辑可能放在So层用C/C实现逆向难度更大。初步分析结论是这是一个“Java层动态签名校验 So层加固校验 广告SDK深度集成”的复合型防御案例。简单的修改AndroidManifest.xml或替换广告ID的方法很可能失效因为应用在启动或执行关键功能前会进行多重验证。3. 广告SDK绕过策略深度解析3.1 广告加载流程与Hook点定位广告的展示并非无迹可寻。以AdMob为例其展示广告的核心最终都会调用到com.google.android.gms.ads.AdView的loadAd方法或是InterstitialAd的show方法。我们的目标不是阻止这些方法的调用可能导致空指针异常而是让它们“安静地失败”或者返回一个无害的空广告。在Jadx中我聚焦于应用自有的AdManager类。它有一个关键方法public void loadBannerAd(Context context, String adUnitId)。在这个方法内部它创建了AdView实例设置了AdUnit ID然后调用了adView.loadAd(new AdRequest.Builder().build())。注意直接HookAdView.loadAd()有时并不够因为广告SDK可能有异步回调或状态监听。更稳妥的方法是找到广告请求生成的源头或响应处理的环节。通过跟踪代码我发现应用在收到广告后会调用一个onAdLoaded()回调方法。我的策略是Hook这个回调方法使其永远不会被成功触发或者在被触发时执行一个空操作同时阻止真正的广告视图被添加到界面布局中。3.2 Frida Hook脚本编写与实践我编写了以下Frida JavaScript脚本hook_ads.jsJava.perform(function () { console.log([*] 开始Hook广告相关类...); // 场景1Hook应用自有的AdManager的loadBannerAd方法使其什么都不做 var AdManager Java.use(com.example.app.AdManager); if (AdManager) { AdManager.loadBannerAd.implementation function (context, adUnitId) { console.log([] 拦截 AdManager.loadBannerAd()广告单元ID: adUnitId); // 直接返回不执行父类方法广告加载流程被中断 // 注意这可能导致调用方期待一个AdView对象需根据实际情况调整 // 更安全的做法是创建一个空的AdView返回但将其可见性设为GONE try { var fakeAdView this.mBannerAdView; // 假设有这个字段 if (fakeAdView) { fakeAdView.setVisibility(Java.use(android.view.View).GONE.value); } } catch (e) { console.log([-] 处理fakeAdView时出错: e); } // 不调用原方法广告请求根本不会发出 }; console.log([] AdManager.loadBannerAd Hook 成功); } // 场景2Hook AdMob的AdView.loadAd方法传入一个空的AdRequest var AdView Java.use(com.google.android.gms.ads.AdView); if (AdView) { AdView.loadAd.implementation function (adRequest) { console.log([] 拦截 AdView.loadAd()); // 可以选择调用原方法但传入一个无效或空的请求使广告请求失败 // 但更好的方法是不让广告视图被添加到任何父布局 var currentActivity Java.use(android.app.ActivityThread).currentActivity(); if (currentActivity) { // 查找可能是广告的View并移除 // 这是一个更激进但有效的方法需要适配具体UI结构 } // 这里我们选择直接返回不加载广告 // this.loadAd(adRequest); // 注释掉不执行原逻辑 }; console.log([] AdView.loadAd Hook 成功); } // 场景3Hook广告加载成功回调使其失效 var AdListener Java.use(com.google.android.gms.ads.AdListener); if (AdListener) { AdListener.onAdLoaded.implementation function () { console.log([] 拦截 onAdLoaded() 回调); // 不执行任何操作广告加载成功的信号不会被上层应用感知 // 也可以在这里触发一个假的 onAdFailedToLoad 回调 // this.onAdFailedToLoad(Java.use(com.google.android.gms.ads.LoadAdError).$new()); }; console.log([] AdListener.onAdLoaded Hook 成功); } });实操心得Hook的时机脚本需要在广告加载代码执行之前注入。最稳妥的方式是在应用启动的早期例如HookApplication.attachBaseContext()或Application.onCreate()方法时就执行我们的广告Hook逻辑。错误处理Frida脚本中的try-catch非常重要。目标应用可能经过混淆类名或方法名不准确或者在不同版本中有所变化。良好的错误处理可以避免脚本因单个Hook失败而整体崩溃。多线程考虑广告加载和回调可能发生在非UI线程。Frida的Java.perform确保了代码在Java VM线程中执行但你的Hook逻辑本身应尽量简单、原子化避免复杂的同步操作。使用命令frida -U -f com.example.targetapp -l hook_ads.js --no-pause启动应用并注入脚本。观察日志当广告加载被触发时你应该能看到拦截成功的提示并且界面上对应的广告位应该保持空白或消失。4. 动态签名校验的定位与绕过4.1 签名校验原理与常见位置Android应用的签名信息存储在PackageInfo.signatures数组中。动态签名校验的流程通常是获取当前运行应用的签名context.getPackageManager().getPackageInfo(...).signatures。对签名进行哈希计算通常是MD5或SHA1。将计算出的哈希值与一个“合法”值进行比对。这个“合法”值可能硬编码在代码或资源文件中相对容易找到和修改。从网络服务器动态获取需要拦截网络请求。隐藏在So库中通过JNI调用返回难度较大。校验代码可能出现在Application.onCreate()应用一启动就检查。主Activity.onCreate()用户看到界面之前检查。某个关键业务逻辑的入口处例如支付页面、核心功能调用前。定时任务或广播接收器中定期或不定期检查。4.2 使用Frida主动调用与内存修改进行绕过我们的绕过思路是让签名校验方法始终返回“真”通过。首先需要在Jadx中定位到具体的校验方法。搜索“signature”找到类似checkSignature(Context context)或isValidApp()的方法。假设我们找到了一个类SecurityUtil其中有一个方法public static boolean verifySignature(Context ctx)。绕过方案一Hook并修改返回值这是最直接的方法。如果校验逻辑集中在某一个方法里。Java.perform(function () { console.log([*] 寻找签名校验方法...); var SecurityUtil Java.use(com.example.app.util.SecurityUtil); if (SecurityUtil) { SecurityUtil.verifySignature.implementation function (ctx) { console.log([] 拦截 verifySignature()强制返回 true); // 可以选择性地打印原始返回值了解其正常逻辑 // var originalResult this.verifySignature(ctx); // console.log(原始校验结果: originalResult); return true; // 强制通过校验 }; console.log([] SecurityUtil.verifySignature Hook 成功); } else { // 如果类名被混淆尝试枚举所有类查找方法特征 Java.enumerateLoadedClasses({ onMatch: function (className) { if (className.includes(util) || className.toLowerCase().includes(sign)) { console.log([?] 发现潜在类: className); try { var clazz Java.use(className); var methods clazz.class.getDeclaredMethods(); for (var i 0; i methods.length; i) { if (methods[i].getName().toLowerCase().includes(verify) || methods[i].getName().toLowerCase().includes(check)) { console.log([!] 尝试Hook方法: className . methods[i].getName()); // 这里需要根据方法签名动态Hook较为复杂通常静态分析更可靠 } } } catch (e) { /* 忽略无法使用的类 */ } } }, onComplete: function () { console.log([*] 类枚举完成); } }); } });绕过方案二篡改获取到的签名信息如果校验逻辑是分散的或者我们需要更底层的绕过可以直接HookPackageManager.getPackageInfo方法返回一个我们构造的、带有“合法”签名的PackageInfo对象。Java.perform(function () { var PackageManager Java.use(android.app.ApplicationPackageManager); var PackageInfo Java.use(android.content.pm.PackageInfo); var Signature Java.use(android.content.pm.Signature); PackageManager.getPackageInfo.overload(java.lang.String, int).implementation function (packageName, flags) { var originalResult this.getPackageInfo(packageName, flags); console.log([*] 拦截 getPackageInfo for: packageName); if (packageName.equals(com.example.targetapp)) { console.log([] 目标应用包名匹配尝试篡改签名信息); // 创建一个伪造的签名对象这里需要填入正确的合法签名字节 // 通常你需要从原版APK中提取或从网络响应/So库中获取合法的签名哈希然后反向构造。 // 这是一个复杂步骤此处仅演示思路。 // var fakeSignature Signature.$new(fakeSignatureData); // originalResult.signatures [fakeSignature]; } return originalResult; }; });重要提示方案二非常复杂因为需要构造合法的签名对象。在实际操作中更常见的做法是结合方案一修改校验结果和静态Patch直接修改校验方法的Smali代码使其永远返回0x1。对于So层的校验则需要使用Frida的Interceptor去Hook Native函数。4.3 应对So层Native签名校验当签名校验逻辑在libsecuritycheck.so中时我们需要使用Frida的Native Hook能力。定位Native函数使用readelf -s libsecuritycheck.so或objdump -T libsecuritycheck.so查看导出函数。更常见的是Java层通过native boolean nativeCheckSignature(String param)这样的声明来调用那么函数名可能是Java_com_example_app_SecurityUtil_nativeCheckSignature。使用Frida Interceptor Hook Native函数Java.perform(function () { // 首先确保So库已加载 var libName libsecuritycheck.so; var module Process.getModuleByName(libName); if (module) { console.log([] 找到模块: libName 基址: module.base); // 假设我们通过逆向So知道了校验函数的偏移地址或符号 // 方法A通过导出函数名Hook如果有 var checkFuncAddr Module.findExportByName(libName, native_check_signature); // 方法B通过偏移地址Hook更常见 // var checkFuncAddr module.base.add(0x1234); if (checkFuncAddr) { Interceptor.attach(checkFuncAddr, { onEnter: function (args) { console.log([] Native签名校验函数被调用参数: , args[0], args[1]); // 可以在这里打印或修改传入的参数 }, onLeave: function (retval) { console.log([] Native函数原始返回值: , retval); // 将返回值修改为1表示成功 retval.replace(ptr(0x1)); console.log([] 已将返回值修改为 1); } }); console.log([] Native层签名校验Hook成功); } else { console.log([-] 未找到指定的Native函数地址); } } else { console.log([-] 未加载模块: libName); } });踩坑记录So层函数Hook对函数签名的把握要求极高参数类型、调用约定。一个错误的参数读取可能导致进程崩溃。务必使用IDA Pro或Ghidra等工具对So库进行初步的静态分析确定函数原型后再进行Hook。5. 整合绕过与稳定性测试5.1 编写综合Hook脚本将广告绕过和签名校验绕过的逻辑整合到一个脚本中并合理安排Hook顺序。通常签名校验的Hook需要最早执行确保应用在后续任何逻辑包括广告初始化执行前就已经处于“校验通过”的状态。// comprehensive_hook.js Java.perform(function () { console.log(); console.log([*] 注入综合绕过脚本); console.log(); // 第一阶段绕过签名校验 (优先级最高) // ... (插入上述签名校验Hook代码) ... // 短暂延迟确保校验逻辑已生效非必需但更稳妥 setTimeout(function() { Java.perform(function () { // 第二阶段绕过广告SDK // ... (插入上述广告Hook代码) ... console.log([*] 所有Hook点设置完毕。); }); }, 500); // 延迟500毫秒 });5.2 测试与问题排查启动测试使用frida -U -f com.example.targetapp -l comprehensive_hook.js --no-pause启动应用。观察控制台输出确认所有预期的Hook点都成功拦截。功能遍历手动操作应用进入每一个包含广告的页面触发每一个可能调用签名校验的功能如登录、支付、解锁高级功能。观察应用是否出现崩溃、广告是否依然出现、功能是否受限。日志分析密切关注Frida控制台和logcat输出。任何崩溃都会产生堆栈跟踪这是定位问题的最重要线索。常见的崩溃原因包括Hook了错误的方法签名参数数量或类型不匹配。在Hook方法中进行了不安全的操作如在非UI线程操作UI。Native Hook时访问了无效的内存地址。稳定性验证让应用在后台运行一段时间或反复切换前后台检查是否有定时触发的校验逻辑导致后续崩溃。5.3 常见问题与解决方案速查表问题现象可能原因排查与解决方案注入后应用秒退Frida-server版本与客户端不匹配目标应用有反调试/反Frida检测。1. 确保Frida-server与frida-tools版本兼容。2. 检查logcat崩溃日志。3. 尝试使用frida -U --no-pause -f com.example.app先不注入脚本看应用能否正常启动。若不能说明有基础的反调试。需先绕过反调试如Hookptrace、fopen等函数。广告依然显示Hook点不正确或时机不对广告由WebView或第三方组件加载。1. 确认Hook的类和方法名完全正确注意混淆。2. 尝试更早注入脚本HookApplication初始化。3. 检查是否还有其他广告SDK如穿山甲、腾讯广点通。4. 尝试HookWebView.loadUrl拦截广告请求URL。签名校验绕过后其他功能异常校验方法有多处只绕过了一处返回值修改影响了其他依赖逻辑。1. 全局搜索所有调用verifySignature的地方确保全部Hook。2. 不要简单返回true可以尝试先调用原方法获取结果仅当结果为false时改为true避免影响正常流程。Native Hook导致崩溃函数地址错误参数读写越界调用约定错误。1. 使用Module.enumerateExports()再次确认函数地址。2. 在onEnter中仅打印参数地址不进行深度读取。3. 详细分析So文件确定函数确切的参数类型和个数。Frida脚本执行一段时间后失效应用可能动态加载了新的Dex或So覆盖了原有代码。1. 监听ClassLoader在新类加载时重新应用Hook。2. 对于So可以Hookdlopen函数在目标库加载时立即执行Native Hook代码。6. 进阶对抗加固与混淆在实际的高强度对抗中你可能会遇到以下情况代码整体加固Dex被加密运行时解密。Jadx打开后代码量极少。这时需要动态脱壳在内存中dump出解密后的Dex。可以使用Frida脚本在ClassLoader加载类时或者dexFile相关函数被调用时进行dump。字符串加密所有关键的类名、方法名、URL、密钥都是加密的运行时解密。你需要找到通用的解密函数然后用Frida Hook它批量解密并打印出原文从而还原出可读的代码逻辑。反Hook与反调试应用会检测Frida、Xposed等框架的存在检测调试端口或使用ptrace自身防止附加。绕过这些需要更底层的知识例如修改Frida的默认端口和特征。Hookfopen、read等函数阻止应用读取/proc/self/status等文件来检测TracerPid。使用inline-hook技术绕过基于ptrace的检测。这些属于更高级的逆向工程范畴每一步都需要对Android系统底层有深入的理解。本次实战聚焦于相对常见的广告SDK和动态签名校验掌握了这些基础且核心的Hook技巧就为应对更复杂的挑战打下了坚实的基础。记住逆向是一个不断学习和迭代的过程每一个应用都是一次新的谜题而工具和思路是你的钥匙。