Android逆向实战:从smali修改到去广告与内购破解
1. 项目概述与逆向工程核心思路最近在逆向分析一些经典的单机手游发现《火柴人联盟》v1.14.1这个版本是个非常不错的练手对象。它集成了多种广告SDK和运营商计费接口代码结构清晰逻辑典型非常适合用来系统性地学习Android逆向中的去广告、内购破解以及smali代码修改技巧。这个版本虽然不算新但其中涉及的技术点——签名验证绕过、广告调用链分析、支付逻辑篡改——在当今很多应用里依然通用。通过这个实战案例你不仅能学会如何“干掉”烦人的广告和实现“免费”内购更重要的是能掌握一套逆向分析的通用方法论以后遇到类似的应用就能举一反三。整个逆向过程可以概括为“三板斧”定位、分析、修改。首先我们需要定位到应用中的关键功能点比如哪里检测签名、哪里弹出广告、哪里发起支付。然后分析这些功能点的实现逻辑理解其判断条件和执行流程。最后也是最关键的一步就是通过修改smali字节码改变原有的逻辑实现我们的目标——让签名验证失效、让广告无法展示、让支付永远成功。听起来是不是有点像外科手术没错逆向工程就是给应用做“手术”而smali就是我们的手术刀。接下来我会带你一步步完成这台“手术”并分享我在操作中踩过的坑和总结的技巧。2. 环境准备与工具链选择工欲善其事必先利其器。在开始动手之前我们需要搭建一个稳定、高效的逆向分析环境。这里我推荐一套经过实战检验的工具组合兼顾了功能强大和上手难度。2.1 核心工具介绍与配置1. Android Killer (AK):这是我们的主力“手术台”。AK是一个集反编译、代码编辑、回编译、签名、安装于一体的图形化工具对smali的支持非常友好。它的优势在于能实时将smali代码与Java代码进行对照虽然转换不一定100%准确极大地方便了代码阅读。在AK中我们主要使用它的“工程管理器”来浏览反编译后的资源、清单文件和smali代码用它的“代码编辑器”来修改smali。注意AK对高版本Android SDK的支持可能不佳。建议在分析像《火柴人联盟》v1.14.1这类较老的应用时使用JDK 1.8和对应的Android SDK版本可以避免很多莫名其妙的回编译错误。2. JADX / JEB:这是我们的“X光机”用于将APK或DEX文件反编译成更易读的Java代码。虽然最终修改是在smali层面但通过阅读Java代码来理解程序逻辑要高效得多。我习惯用JADX进行快速的全局搜索和逻辑梳理因为它搜索速度快且免费。当遇到JADX反编译出来逻辑混乱或关键方法缺失时可以辅以JEB进行交叉验证JEB的反编译能力通常更强但速度慢一些。3. ADB Logcat:这是我们的“听诊器”和“监视器”。很多关键线索都藏在日志里。我们需要在真机或模拟器上运行游戏并通过adb logcat命令实时抓取日志。为了精准过滤可以结合grep命令例如adb logcat | grep -i “ad”来专门查看广告相关的日志或者adb logcat | grep -i “pay”来监控支付流程。日志中打印的类名、方法名、Tag字符串都是我们逆向时定位代码的黄金路标。4. 模拟器 (Mumu / 雷电):推荐使用Mumu或雷电模拟器进行测试。它们对游戏的兼容性较好且自带root环境有时需要手动开启方便我们进行一些需要高权限的操作或调试。模拟器的另一个好处是可以轻松截图、录屏方便记录分析过程。5. 文本编辑器 (VS Code / Sublime Text):当我们需要同时查看、对比多个smali文件或者进行复杂的文本搜索替换时一个强大的文本编辑器必不可少。VS Code配合Smali语法高亮插件体验会非常好。2.2 实战环境搭建步骤安装Java环境确保系统已安装JDK 1.8并配置好JAVA_HOME环境变量。这是AK和JADX运行的基础。配置Android SDK下载Android SDK至少包含与目标APK编译版本相近的Platform Tools。将adb命令所在目录加入系统PATH。安装并配置Android Killer下载AK后首次运行需要配置Android SDK路径和JDK路径。建议在设置中将“APKTOOL”版本切换到较新的稳定版如2.6.0以上以提高对某些APK的反编译成功率。准备测试设备在Mumu模拟器中安装原版《火柴人联盟》v1.14.1。打开模拟器的USB调试功能。在命令行输入adb devices确认设备已连接。建立工作目录创建一个清晰的文件夹结构例如/Stickman_Alliance_Reverse/ ├── original.apk # 原始APK ├── decompiled/ # AK反编译后的工程目录 ├── modified/ # 修改后的smali备份 ├── logs/ # 保存的logcat日志 └── output/ # 回编译后生成的APK这套环境搭建好后我们就能以“外科医生”的视角从容地对APK进行解剖了。记住稳定的环境是成功的一半不要在工具问题上耗费过多精力。3. 签名验证的定位与绕过策略任何修改过的APK在重新签名后其签名信息必然与原版不同。因此绕过签名验证是修改APK后能正常运行的第一步。《火柴人联盟》的签名验证逻辑相对直接是我们入门的好例子。3.1 日志分析与关键字符串定位首先我们将原版APK安装到模拟器并运行同时开启adb logcat抓取日志。我们重点关注应用启动时以及进行任何可能触发验证的操作如点击内购时的日志输出。很快我们就能在日志中发现类似“检测到盗版游戏”或“signature”这样的关键字。例如在《火柴人联盟》中我们可能会看到应用打印了“signatures”或调用了名为“getAPPSecretString”的方法。这些字符串就是我们的“线索”。打开Android Killer载入APK等待反编译完成。在AK的“工程搜索”功能中我们直接搜索刚才在日志里看到的字符串“signatures”。搜索结果会定位到所有包含该字符串的smali文件。通常我们会发现一个名为utils.smali或类似名称的文件里面有一个getAPPSecretString方法。3.2 代码回溯与验证逻辑分析找到getAPPSecretString方法后我们需要向上回溯看是哪个类调用了它。在AK中可以右键点击该方法选择“查找引用”。这能帮助我们快速定位到调用链的源头。回溯后发现通常是在主Activity例如com.DBGame.DiabloLOL.DiabloLOL的onCreate方法中初始化了一个名为AddBlackName的类并调用了它的Check()静态方法。这个Check()方法内部很可能就包含了对getAPPSecretString的调用并利用其返回值进行签名比对。我们点开AddBlackName.smali文件找到它的Check()方法。分析其smali代码核心逻辑通常是获取当前应用的签名信息计算其哈希值如MD5然后与内置的一个合法签名哈希值进行比对。如果比对失败则可能弹出盗版提示、强制退出、或者禁用某些功能。3.3 Smali修改实现绕过理解了逻辑修改就很简单了。我们的目标就是让这个验证永远“通过”或者直接让它“失效”。有两种经典的修改思路思路一让验证函数直接返回“正确”的签名值。找到getAPPSecretString方法观察其返回值。如果它原本是返回一个计算后的字符串我们可以直接修改让它返回一个空字符串“”或者更彻底一点直接返回我们预设的“正确”签名哈希值如果能在代码里找到的话。在smali中返回字符串的指令是return-object vx其中vx是存放字符串引用的寄存器。例如原方法可能这样结尾.line 22 .local v4, pkgname:Ljava/lang/String; const-string v1, 这里是计算出的错误签名 return-object v1我们可以修改为.line 22 .local v4, pkgname:Ljava/lang/String; const-string v1, # 返回空字符串或者填入正确的签名MD5 return-object v1思路二让验证检查函数直接“什么都不做”就返回。这是更常用、更彻底的方法。我们找到核心的验证方法例如AddBlackName-Check()将其方法体清空只保留一个return-void指令。原Check()方法可能有很多逻辑.method public static Check()V .locals 6 .prologue ... # 一大堆计算和判断的代码 .line 50 invoke-static {v0}, Lcn/mycompany/addblackname/utils/utils;-getAPPSecretString(Landroid/content/Context;)Ljava/lang/String; move-result-object v1 ... # 后续的比对逻辑 .line 60 return-void .end method我们可以将其修改为.method public static Check()V .locals 0 .prologue # 可以插入一行自定义日志方便调试非必须 # const-string v0, Check() bypassed by B.S. # invoke-static {v0}, Landroid/util/Log;-d(Ljava/lang/String;Ljava/lang/String;)I return-void .end method实操心得在清空方法体时一定要注意寄存器的使用.locals声明。如果原方法使用了6个寄存器.locals 6而你清空后一个都不用需要将.locals 6改为.locals 0否则回编译可能会报错。保险起见可以保留原.locals声明只清空其后的逻辑代码。修改完成后在AK中点击“编译”如果没有错误就会生成一个已签名的dist文件。将其安装到模拟器测试如果游戏能正常启动且不再提示盗版那么签名验证就成功绕过了。4. 广告去除的全面分析与实战修改去广告是提升游戏体验的关键也是逆向中常见的需求。《火柴人联盟》的广告类型比较典型横幅广告Banner、插屏广告Interstitial、开屏广告Splash。我们需要逐一击破。4.1 广告类型识别与调用链追踪再次运行游戏并仔细观察。你会发现在游戏主界面、选择英雄界面底部有横幅广告。点击手机返回键试图退出游戏时会弹出全屏的插屏广告。游戏启动时可能会有短暂的开屏广告今日推荐。我们的任务就是找到控制这些广告显示和隐藏的代码。最有效的方法依然是日志分析法。在游戏运行时触发这些广告同时监控logcat。例如当你点击返回键弹出插屏广告时日志里很可能会出现“----- showInterAD --exit”这样的Tag。这个字符串就是我们的突破口。在AK中全局搜索“showInterAD”很快就能定位到BLHelper.smali类中的一个静态方法showInterAD(Ljava/lang/String;)V。4.2 横幅与插屏广告的去除分析BLHelper.smali中的showInterAD方法其核心逻辑是构造一个Message对象设置其what值为一个特定的常量例如0xd3即十进制的211然后通过Handler发送出去。在主Activity的Handler中会根据这个what值来调用真正的显示广告的方法。因此去除广告的思路就是阻断消息的发送。找到发送消息的那行smali代码sget-object v3, Lcom/DBGame/Common/BLHelper;-sContext:Lcom/DBGame/DiabloLOL/DiabloLOL; invoke-virtual {v3, v2}, Lcom/DBGame/DiabloLOL/DiabloLOL;-sendMessage(Landroid/os/Message;)V我们只需要在这行代码前加上#号将其注释掉或者直接删除这行广告消息就无法发送自然也就不会显示了。# invoke-virtual {v3, v2}, Lcom/DBGame/DiabloLOL/DiabloLOL;-sendMessage(Landroid/os/Message;)V同理对于横幅广告搜索“showBanner”或“HANDLER_SHOWBANNER”找到对应的消息发送代码what值可能是200用同样的方法注释掉即可。注意事项有些广告SDK的初始化也在onCreate中。我们可以顺藤摸瓜在DiabloLOL.smali的onCreate方法里找到类似invoke-static {p0, v7}, Lcn/cmgame/billing/api/game/main/Tool;-adIntervalInit(...)的调用将其注释掉可以从源头阻止广告模块的初始化。但要注意如果游戏逻辑强依赖广告SDK的某个回调直接注释初始化可能导致崩溃所以优先采用注释发送消息的方式更安全。4.3 开屏广告与消息处理器的深度处理开屏广告的去除稍微复杂一点。我们需要找到显示开屏广告的方法通常是Tool.smali类中的adSplashShow。我们的目标是让这个方法返回false表示不显示开屏广告。查看adSplashShow方法的smali代码寻找其返回值。在smali中布尔值false对应整数0true对应1。返回值通过return vx指令传递vx寄存器里存放着返回值。找到方法中设置返回值的逻辑通常是在方法末尾。我们可以在方法开始不久、任何关键逻辑执行之前就强制返回false。.method public static adSplashShow(...)Z # 注意返回值是Z即boolean .locals 7 .prologue # 在方法开头就强制返回false const/4 v0, 0x0 # 将v0寄存器设为0 (false) return v0 # 立即返回 # 后面的所有原有代码都不会执行 ... .end method此外为了确保万无一失我们还可以直接“废掉”处理广告消息的Handler。在DiabloLOL$3.smali一个内部类继承自Handler中找到handleMessage方法。里面有一个switch语句smali中表现为packed-switch或sparse-switch根据msg.what跳转到不同的case。找到处理广告的case例如case 200显示横幅、case 210隐藏横幅、case 211显示插屏。在对应的sswitch标签下将调用实际广告显示方法的代码注释掉。:sswitch_8 # 对应 case 200 iget-object v2, p0, Lcom/DBGame/DiabloLOL/DiabloLOL$3;-this$0:Lcom/DBGame/DiabloLOL/DiabloLOL; # 注释掉调用showBanner的代码 # invoke-static {v2}, Lcom/DBGame/DiabloLOL/DiabloLOL;-access$400(Lcom/DBGame/DiabloLOL/DiabloLOL;)V goto :goto_0通过这种“发送端拦截”和“处理端废弃”的双重保障游戏中的广告就能被彻底清除。5. 内购破解的精细化分析与实现内购破解是逆向中最具“价值”也最需谨慎的部分。我们的目标不是盗取而是通过技术手段理解其计费流程并修改本地验证逻辑使应用认为购买已成功。《火柴人联盟》集成了移动、联通、电信三大运营商的短信计费SMS Pay以及第三方支付如Egame为我们提供了多套样本。5.1 支付入口分析与权限清理首先我们需要找到游戏的支付入口。通常是在点击购买钻石、金币等道具时触发。运行游戏并点击购买同时抓取logcat搜索“支付”、“success”、“fail”、“order”等关键词。在《火柴人联盟》中我们可能会看到“Egame支付成功”、“Unicom支付cancel”、“购买道具[xxx] 成功”等日志。这些字符串是定位支付代码的关键。在AK中搜索这些字符串就能找到对应的支付回调方法。安全第一步清理危险权限。短信计费需要发送短信的权限。为了防止修改版应用在用户不知情的情况下发送短信我们应在修改前就移除这些权限。用AK打开AndroidManifest.xml找到并删除以下权限声明uses-permission android:nameandroid.permission.SEND_SMS / uses-permission android:nameandroid.permission.CALL_PHONE /这体现了逆向工程师的职业道德在实现功能的同时必须确保用户设备的安全。5.2 运营商支付接口的破解游戏会根据用户的SIM卡运营商移动、联通、电信来调用不同的支付接口。我们需要逐一攻破。1. 电信支付Egame搜索“Egame支付成功”定位到EgamePay.smali类中的pay方法。分析其回调逻辑会发现它调用了一个EgamePayListener接口的paySuccess、payFailed、payCancel方法。我们的修改策略是无论支付过程如何都强制走向成功分支。在pay方法内部找到调用paySuccess的地方。通常支付流程是异步的但我们可以在支付请求发起后立即手动调用成功回调。.method public static pay(Landroid/app/Activity;Ljava/util/Map;Lcn/egame/terminal/paysdk/EgamePayListener;)V .locals 1 .prologue ... # 在方法内部支付逻辑开始前直接调用成功回调 # p2 是传入的 EgamePayListener 参数 invoke-interface {p2, p1}, Lcn/egame/terminal/paysdk/EgamePayListener;-paySuccess(Ljava/util/Map;)V # 调用成功后直接返回不再执行后面的真实支付流程 return-void ... .end method这样只要游戏调用电信支付就会立即收到“支付成功”的反馈。2. 联通支付搜索“Unicom支付成功”定位到DiabloLOL$4.smali一个支付回调类中的PayResult方法。这个方法通常会根据一个状态码参数比如arg1进行switch判断跳转到成功、失败、取消等分支。破解方法简单粗暴使用goto指令无条件跳转到成功分支。找到PayResult方法中switch语句对应的pswitch_data_0标签区域在switch判断之前直接goto到成功处理的代码块例如:pswitch_0。.method public PayResult(Ljava/lang/String;IILjava/lang/String;)V .prologue .line 694 # 无条件跳转到支付成功的case goto :pswitch_0 packed-switch p2, :pswitch_data_0 ... :pswitch_0 # 这里是支付成功的处理逻辑 ... return-void .end method3. 移动支付搜索“购买道具[ ] 成功”定位到DiabloLOL$5.smali中的onResult方法。其破解思路与联通支付类似使用goto :pswitch_0跳转到成功分支。有时移动支付会有一个“正在处理请稍后...”的遮罩层如果破解后这个遮罩层一直不消失我们需要分析成功分支的代码将其核心逻辑调用purchaseComplete提取出来直接放到支付函数里并绕过遮罩层的显示和关闭逻辑。5.3 支付接口的统一与伪装为了让破解更完美我们可以修改游戏判断运营商类型的逻辑让它始终使用我们已破解的某个支付接口例如电信的Egame支付。这样就不需要破解所有接口一劳永逸。找到判断SIM卡运营商的方法通常是getSimType()或getOperatorType()。它的返回值决定了iFromPay变量的值0移动1电信2联通。我们只需修改这个方法让它永远返回1电信。.method private getSimType()I .locals 1 .prologue # 方法一开始就直接返回1伪装成电信网络 const/4 v0, 0x1 return v0 .end method或者更彻底一点直接在主Activity的onCreate中找到给iFromPay赋值的地方将其硬编码为1。.line 254 # invoke-direct {p0}, Lcom/DBGame/DiabloLOL/DiabloLOL;-getSimType()I # move-result v4 const/4 v4, 0x1 # 直接赋值而不是调用方法 iput v4, p0, Lcom/DBGame/DiabloLOL/DiabloLOL;-iFromPay:I这样无论用户实际是什么运营商游戏都会走电信Egame的支付流程而我们已经在Egame的pay方法里强制返回成功了。6. 回编译、测试与疑难问题排查所有修改完成后就到了检验成果的时刻。这个过程可能不会一帆风顺但解决问题的过程正是提升技术的最佳途径。6.1 回编译与签名在Android Killer中点击“编译”按钮。AK会自动完成以下步骤将修改后的smali代码重新编译成classes.dex。将新的dex文件与其他资源一起打包成APK。使用配置的签名文件AK自带或自定义对APK进行签名。如果编译成功你会在工程目录下的dist文件夹里找到生成的APK文件。如果编译失败AK会在下方的“日志”窗口输出错误信息。最常见的回编译错误.locals 寄存器数量错误当你清空一个方法体时如果方法开头声明的寄存器数量.locals N与实际使用的数量不符就会报错。确保清空后方法内不再使用任何局部寄存器或者将.locals N改为.locals 0。smali语法错误多删了或少删了指令比如return-void写成了return。仔细检查修改处的smali语法。资源ID冲突如果你修改了AndroidManifest.xml或资源文件有时会引起ID冲突。对于单纯的smali代码修改一般不会遇到此问题。6.2 安装测试与功能验证将新生成的APK安装到模拟器需要先卸载原版。安装后按以下清单进行测试[ ]启动测试应用是否能正常启动无闪退无“盗版”提示。[ ]广告测试进入各个界面主界面、英雄选择、副本底部是否还有横幅广告点击返回键是否还会弹出插屏广告启动时是否有开屏广告[ ]内购测试尝试购买最便宜的道具如少量金币。点击购买后是否直接提示“购买成功”游戏内的钻石/金币数量是否增加是否收到任何运营商的扣费短信切记用小额道具测试[ ]支付接口伪装测试在关于或设置页面如果有查看当前支付渠道或者通过日志判断是否统一走到了我们修改的支付路径如电信。6.3 常见问题与解决方案实录在实际操作中你可能会遇到以下问题问题1游戏启动闪退Logcat报错“ClassNotFoundException”或“MethodNotFoundException”。原因很可能你修改或删除的某个类/方法被游戏的其他部分所依赖。例如你注释了广告SDK的某个初始化方法但游戏在别处调用了该SDK的其他方法。解决不要直接删除或注释整个方法体而是让方法“空转”并安全返回。对于初始化方法保留其调用但清空内部实现对于返回关键值的方法返回一个安全的默认值如null、0、false或空字符串。问题2内购成功后游戏卡在“正在处理请稍后...”的界面。原因支付成功的回调逻辑中可能漏掉了关闭“等待遮罩层”的代码。在移动支付破解中尤其常见。解决仔细分析支付成功的代码分支:pswitch_0。找到其中调用BLHelper.closeShieldLayer()或类似方法的语句。确保你的goto跳转是跳转到包含这行代码的逻辑块末尾或者手动在支付方法里添加关闭遮罩层的调用。问题3修改后游戏功能异常比如无法开始战斗、角色技能失效。原因可能误改了与广告、支付无关的代码。逆向修改必须精准最好一次只修改一个地方测试通过后再进行下一处。解决使用AK的“对比”功能或者用文本对比工具如Beyond Compare对比修改前后的smali文件确认修改范围。如果问题复杂可以备份当前工程然后回退到上一步采用更保守的修改方式。问题4回编译成功但安装时提示“安装包解析错误”。原因APK签名有问题或者APK本身在打包过程中损坏。解决尝试使用AK自带的签名工具重新签名或者换一个签名文件。确保模拟器/手机允许安装来自未知来源的应用。逆向工程是一个需要耐心和细心的过程。每一个成功的修改都建立在对代码逻辑的准确理解之上。当你看到游戏按照你的意愿运行广告消失内购“成功”时那种成就感是无可替代的。更重要的是通过这个完整的实战流程你掌握的是一套通用的Android应用逆向分析方法论这套方法论可以迁移到无数其他应用的分析中。记住技术是用来学习和探索的请务必在合法合规的范围内使用它。