三亩地 三亩地SAN MU DI · CODE DIARY
ARTICLE DETAIL

日记详情

真实记录编程学习的某一天,欢迎挑你感兴趣的翻一翻。

移动应用内购安全机制逆向分析:从协议交互到本地校验的攻防实践

移动应用内购安全机制逆向分析:从协议交互到本地校验的攻防实践

1. 项目概述:当“免费”游戏遇上“付费”体验

在移动应用生态里,我们经常会遇到一些设计精巧、玩法有趣的单机小游戏。它们往往通过广告或一次性买断来盈利,但偶尔,你也会碰到那种将核心体验藏在付费墙后面的作品——比如一些跑酷游戏,角色、皮肤、关键道具甚至复活机会都需要真金白银的内购。作为一名对技术原理和“可能性”充满好奇的开发者或爱好者,你可能会想:这些内购机制究竟是如何实现的?如果我只是想在自己的设备上体验完整内容,有没有一种技术层面的探索路径?这就是“逆向某跑酷小游戏内购”这个标题背后所指向的核心领域:移动应用安全分析与协议交互逻辑的探究。

请注意,这里的“逆向”绝非鼓励盗版或侵害开发者权益。它更像是一种“解谜”过程,旨在理解应用如何与服务器通信、如何验证购买状态、本地数据如何存储校验。这种探究本身是安全研究、漏洞挖掘、自动化测试乃至理解软件架构的常见方法。很多资深移动开发工程师和安全研究员都具备这样的技能,用以评估自己产品的安全性或进行竞品分析。本次,我们就以一个虚构的、名为“极速闪避”的2D跑酷小游戏为例,拆解其内购逻辑可能的技术实现,并探讨在技术研究范畴内,理解其流程的通用思路与方法。这完全是一个用于学习交流的技术沙盘推演。

2. 核心思路与技术选型:从黑盒到白盒的探索路径

面对一个已编译打包的安卓APK文件,它对我们来说最初就是一个“黑盒”。我们的目标是理解其内购流程,这通常涉及几个关键环节:支付SDK调用、购买凭证的生成与验证、解锁状态的本地持久化以及可能的服务器端校验。技术选型上,我们需要一套组合工具来应对不同层面的分析。

2.1 静态分析与动态调试双管齐下

静态分析好比“阅读程序的蓝图”,在不运行程序的情况下,通过反编译、反汇编等手段查看代码逻辑。对于安卓应用,核心工具是JADX-GUIGDA。它们能将APK中的DEX字节码文件反编译成可读性较高的Java或Kotlin伪代码。通过搜索与内购相关的关键词,如“purchase”、“billing”、“IAB”、“支付”、“success”等,我们可以快速定位到支付相关的代码模块。对于使用了代码混淆的应用(这是商业应用的标配),反编译后的类名、方法名可能变成a、b、c这样的无意义字符,这时就需要结合字符串搜索和上下文逻辑分析,考验的是耐心和模式识别能力。

动态调试则是“在程序运行时观察其行为”,更为直观。我们需要让应用在受控的环境中运行,并能够实时监控其函数调用、网络请求、文件读写和内存数据。这里首推Frida,这是一个动态插桩工具框架,通过注入JavaScript脚本到目标进程,可以Hook(挂钩)任何函数,拦截和修改参数、返回值。对于内购分析,我们可以Hook谷歌支付库(com.android.billingclient)的关键方法,或者Hook应用自己定义的支付状态处理函数,直接看到购买成功的返回值是如何被处理的。另一个常用工具是Xposed框架,它通过替换系统文件实现全局Hook,但需要设备Root,且模块编写相对复杂,Frida在灵活性和即时性上通常更胜一筹。

2.2 网络抓包:洞察客户端与服务器的对话

绝大多数内购,尤其是涉及解锁永久内容或验证购买凭证的,都离不开网络通信。即使是一款单机游戏,也可能在启动时或购买发生时向开发者的服务器发送请求进行验证。因此,网络抓包是至关重要的一环。

mitmproxyCharles这类中间人代理工具是标准选择。它们需要在电脑上运行,并将手机的网络流量代理到电脑上,从而解密和查看HTTPS请求(需要先在手机安装代理工具的CA证书)。通过抓包,我们可以清晰地看到:

  1. 购买发起请求:应用向服务器发送了哪些信息?商品ID、用户标识、设备信息是什么格式?
  2. 支付平台回调:在用户完成应用商店(如Google Play)的支付流程后,支付平台会回调哪个URL?回调的凭证(Purchase Token)是什么?
  3. 凭证验证请求:应用是否将支付凭证发送到自家服务器进行二次验证?服务器返回的成功报文结构是怎样的?
  4. 解锁信息下发:验证成功后,服务器是否会下发一个“解锁令牌”或直接更新用户数据?本地如何保存这个状态?

分析这些网络请求和响应,能够让我们从协议层面彻底理解内购的完整闭环。有时候,漏洞就出现在服务器验证逻辑的不严谨上,例如仅验证了支付凭证的格式而未向支付平台官方接口进行真实性核验,或者解锁状态完全由客户端本地一个可修改的字段控制。

2.3 本地数据与文件分析

内购解锁的最终结果,必然会在客户端本地留下痕迹。否则应用重启后就会丢失购买状态。我们需要检查:

  • SharedPreferences: Android应用常用的轻量级键值对存储。使用adb shell命令或Root Explorer(需Root)可以查看/data/data/[应用包名]/shared_prefs/目录下的XML文件,里面可能存储着isPremiumUser=trueunlockedSkins=[...]这样的字段。
  • 本地数据库:SQLite数据库文件,可能存储更复杂的购买记录和物品信息。
  • 文件标记:应用可能在私有目录或SD卡特定位置创建一个文件(如.unlock)作为标记。
  • 代码逻辑标志位:在内存中,一个布尔类型的静态变量可能控制着某项功能是否可用。

通过静态分析找到读写这些数据的关键方法,再结合Frida进行Hook和修改,是验证我们猜想的最直接方式。

注意:所有分析应仅限于你自己拥有合法使用权的应用副本,并在完全隔离的测试环境(如模拟器或专属测试机)中进行。任何试图绕过正版验证、用于盗版或损害开发者利益的行为都是不道德且可能违法的。本技术讨论仅面向安全研究、教育及授权测试目的。

3. 实操流程拆解:以“极速闪避”为例的沙盘推演

假设“极速闪避”是一款使用Unity引擎开发,接入了Google Play Billing Library (v5.0+) 的跑酷游戏。它提供三种内购:去除广告(永久)、解锁所有角色(永久)、金币礼包(消耗品)。我们将一步步拆解分析过程。

3.1 环境准备与目标应用设置

工欲善其事,必先利其器。我们需要搭建一个分析环境。

  1. 测试设备:推荐使用Android模拟器,如Android Studio自带的AVD或Genymotion。模拟器易于重置、快照,且通常自带Root权限(方便访问应用数据)。如果使用真机,请确保是一台专门用于测试的、不包含个人敏感数据的设备,并可能需要解锁Bootloader和刷入Magisk获取Root权限。
  2. 核心工具安装
    • JADX-GUI: 从GitHub releases页面下载,用于静态反编译APK。
    • Frida: 在电脑端安装Python包 (pip install frida-tools)。在模拟器或已Root的真机上,下载对应架构的frida-server并运行。
    • mitmproxy: 通过pip安装,用于抓包。同时准备好其CA证书。
    • adb (Android Debug Bridge): 包含在Android SDK中,用于连接设备、推送文件、执行shell命令。
  3. 目标应用:从合法渠道获取“极速闪避”的APK安装包。可以自己用开发工具编译一个测试版本,或者使用应用商店的正式版(用于分析学习)。将APK安装到测试设备上。

3.2 静态分析定位关键代码

将APK文件拖入JADX-GUI。反编译完成后,首先进行全局搜索。

  • 搜索支付相关字符串:在“导航”面板选择“搜索文本”,输入“purchase”、“sku”、“billing”、“IAP”、“success”。这能快速找到可能包含支付逻辑的代码文件。
  • 定位入口点:Unity游戏通常有一个继承自UnityPlayerActivity的主Activity。在反编译代码中搜索“UnityPlayerActivity”可以找到入口类。内购初始化往往在这里或一个单独的Manager类中完成。
  • 分析商品配置:搜索“productId”、“skuDetails”,可能会找到一个硬编码的商品ID列表,例如com.company.speeddodge.remove_ads
  • 查找购买回调:搜索“onPurchasesUpdated”或“PurchaseCallback”,这是Google Billing库的核心回调接口,所有购买结果都在这里处理。

假设我们通过搜索“onPurchasesUpdated”,定位到一个名为a的类(混淆后)。查看其方法,发现一个关键方法b(Purchase purchase),它接收一个Purchase对象。在这个方法里,我们看到了类似如下的伪代码逻辑:

void b(Purchase purchase) { String sku = purchase.getSku(); if (sku.equals("remove_ads")) { // 调用一个本地方法,保存状态 c.a().b(true); // 猜测c是某个管理类,b方法用于设置去广告标志 // 向服务器验证购买凭证 d.a().a(purchase.getPurchaseToken(), sku); } else if (sku.equals("unlock_all_chars")) {...} }

这段代码告诉我们两件事:1. 购买成功后,会立即调用一个本地方法更新状态;2. 会异步向服务器 (d.a()) 发送凭证进行验证。这是我们后续动态调试和Hook的重点。

3.3 动态调试与Hook验证

静态分析给了我们地图,动态调试则是亲自走一遍。我们编写一个Frida脚本,来Hook上面找到的关键方法。

首先,确保frida-server已在设备上运行,并且电脑可以通过adb devices看到设备。然后编写一个JavaScript脚本,比如hook_purchase.js

Java.perform(function() { // 先找到我们关心的类。由于混淆,类名可能是'a',包名需要从反编译信息中获取,假设为‘com.speeddodge.game’ var targetClass = Java.use("com.speeddodge.game.a"); // Hook 那个处理购买的方法 ‘b’ targetClass.b.overload('com.android.billingclient.api.Purchase').implementation = function(purchase) { console.log("[*] Purchase processing method called!"); // 打印商品ID var sku = purchase.getSku(); console.log(" SKU: " + sku); // 打印购买凭证,这是关键! var token = purchase.getPurchaseToken(); console.log(" Purchase Token: " + token); // 打印原始订单信息 var originalJson = purchase.getOriginalJson(); console.log(" Original JSON: " + originalJson); // 继续执行原方法 var result = this.b(purchase); console.log("[*] Original method executed."); return result; }; // 同时Hook那个本地保存状态的方法,假设在类c中 var stateClass = Java.use("com.speeddodge.game.c"); var instance = stateClass.a(); // 获取单例 // Hook它的b方法(设置去广告状态) stateClass.b.overload('boolean').implementation = function(isRemoved) { console.log("[*] Setting ad removal flag to: " + isRemoved); // 我们可以在这里修改参数,比如强制设为true // isRemoved = true; return this.b(isRemoved); }; });

通过命令frida -U -f com.speeddodge.game -l hook_purchase.js --no-pause启动应用并注入脚本。然后在游戏中触发一次内购(在测试环境下,Google Play允许配置测试帐号,购买不会实际扣款)。观察Frida控制台的输出,我们就能亲眼看到购买发生时传递的具体数据,以及本地状态是如何被设置的。这证实了我们的静态分析结果,并拿到了关键的Purchase Token

3.4 网络抓包分析验证流程

启动mitmproxy,在设备上设置好代理并安装CA证书。清空mitmproxy的流量记录,然后在游戏中再次尝试购买(或直接启动游戏,因为它可能会在后台验证历史购买)。

在mitmproxy的交互界面中,我们会看到一系列HTTP/HTTPS请求。重点关注:

  • 域名:请求发送到哪个服务器?是api.speeddodge.com还是validation.google.comiap.googleapis.com
  • 路径:类似/api/v1/validate_purchase/verifyReceipt的路径非常可疑。
  • 请求体:通常是一个JSON,包含我们从Frida脚本中获取的purchaseTokenskupackageName(应用包名)等。
  • 响应体:服务器返回什么?一个简单的{“status”: “valid”},还是一个包含用户权益信息的复杂JSON,如{“unlocked_items”: [“char_hero”, “skin_gold”], “ads_removed”: true}

通过分析,我们可能发现“极速闪避”的验证流程是:客户端将Google Play返回的Purchase Token商品ID发给自己的游戏服务器api.speeddodge.com/verify,游戏服务器再拿着这个Token去Google的服务器https://androidpublisher.googleapis.com进行官方验证,根据Google返回的结果,再决定是否给客户端下发解锁信息。

3.5 本地状态持久化机制剖析

最后,我们探究应用如何记住“你已经购买”。通过adb shell连接到设备(或使用模拟器的终端)。

adb shell su # 获取root权限 cd /data/data/com.speeddodge.game/ find . -name "*.xml" -o -name "*.db" # 查找可能的存储文件 cat shared_prefs/IAPStore.xml # 假设存在这个文件

我们可能会发现一个XML文件,里面存储着:

<?xml version='1.0' encoding='utf-8' standalone='yes' ?> <map> <boolean name="com.speeddodge.game.remove_ads" value="true" /> <string name="com.speeddodge.game.unlock_all_chars_token">eyJhbGciOiJ...(很长的加密字符串)</string> </map>

这表明,去广告状态用一个简单的布尔值存储,而解锁角色可能依赖于一个从服务器下发的加密令牌(token)。这个令牌可能在每次启动游戏时被发送到服务器验证,或者其本身包含加密的过期时间和信息,由客户端本地解密校验。

4. 技术原理深度解析:内购安全机制的攻防逻辑

理解了实操步骤,我们更需要明白这些步骤背后的“为什么”。现代应用内购,尤其是Google Play和Apple App Store的生态内,是一套设计精密的信任链。

4.1 基于票据(Token)的验证链

这是最核心的安全模型。其流程可以概括为:

  1. 客户端发起购买:应用通过Billing SDK向Google Play服务器发起购买请求。
  2. 商店处理支付:Google Play处理支付流程,成功后生成一个唯一的、密码学签名的“购买票据”(Purchase Token),连同订单详情(JSON)返回给客户端。
  3. 客户端本地处理:应用收到票据,首先在本地标记购买成功(如更新UI),但这不是最终授权
  4. 服务器端二次验证(关键):客户端将这张“票据”(Purchase Token)和应用包名、商品ID一起,发送给游戏开发者自己的服务器
  5. 服务器向官方核验:开发者服务器使用其后台的API密钥,调用Google Play Developer API的purchases.products.verify接口,提交这张“票据”。
  6. 官方返回核验结果:Google服务器验证票据的真实性、是否被使用过、商品是否匹配,并将结果返回给开发者服务器。
  7. 最终授权与下发:开发者服务器确认票据有效后,才在自己的数据库中将该用户标记为已购买,并可能生成一个游戏内令牌(Game Token)下发给客户端,用于解锁内容。

这个链条的强度在于,最终的决定权在开发者服务器手中。客户端本地的一切状态都可以被篡改,但服务器不认可就无效。即使黑客伪造了本地数据或拦截了网络请求,只要他无法获得开发者的API密钥去通过Google的官方验证,或者无法伪造一个能被Google验证通过的票据,就无法获得服务器下发的有效游戏令牌。

4.2 本地校验的弱点与混淆加固

许多小游戏或早期应用为了简化架构,省略了服务器验证环节,仅依靠客户端本地校验。这带来了严重的安全风险:

  • SharedPreferences直接修改:如上例中的布尔值,通过Root后直接修改XML文件即可破解。
  • 代码逻辑Patch:通过反编译,找到判断购买状态的代码位置(如if (isPremium()) { ... }),使用修改工具(如ARM Assembler知识手动修改smali代码,或使用Frida实时Hook将函数返回值永远改为true)来绕过检查。
  • 内存修改:使用游戏修改器(如GameGuardian)在运行时搜索并修改控制解锁状态的变量值。

为了增加逆向难度,开发者会采用代码混淆(ProGuard/R8)、字符串加密、核心逻辑用C/C++编写(Native层)、添加反调试检测等手段。例如,将商品ID“remove_ads”在代码中加密存储,运行时解密;将关键的验证函数放在Native库(.so文件)中,这需要逆向工程师具备ARM汇编知识才能进一步分析。

4.3 网络协议层面的安全考量

即使有服务器验证,网络传输过程也可能成为突破口,因此需要:

  • 使用HTTPS:防止中间人轻易窥探和篡改数据。
  • 请求签名:客户端发出的验证请求,除了包含票据,还应包含一个基于请求内容和客户端唯一标识生成的签名,服务器端验签以防止请求被重放或篡改。
  • 游戏令牌的时效性与绑定:下发给客户端的游戏令牌不应是永久的,可以设置过期时间,或与设备ID、用户ID绑定,防止令牌被分享到其他设备滥用。

5. 常见问题与排查技巧实录

在实际的逆向分析过程中,你会遇到各种各样的问题。下面记录了一些典型场景和解决思路。

5.1 静态分析篇:面对混淆的代码

  • 问题:反编译后的代码全是a、b、c、d这样的类名和方法名,完全无法阅读。
  • 技巧1 - 字符串搜索定位:即使代码混淆,程序中的硬编码字符串(如日志信息、URL、错误提示)往往是不混淆或简单加密的。搜索“Purchase failed”、“验证失败”、“http://”等字符串,可以找到关键代码的附近区域。
  • 技巧2 - 调用关系分析:在JADX中,对某个混淆的方法名右键,选择“查找用例”,可以看到哪些地方调用了它。通过分析调用上下文,可以推断其功能。例如,一个被多个地方调用的方法a(String str),如果调用前都先获取了Purchase对象,那它很可能就是处理购买凭证的方法。
  • 技巧3 - 关注资源ID:布局文件、字符串资源、图片资源的ID(如0x7f0d012c)在混淆后的代码中会以常量的形式出现。在JADX中点击这些常量,可以跳转到对应的资源定义(如一个按钮的文本是“购买成功”),从而帮助理解所在代码块的功能。

5.2 动态调试篇:Frida Hook失败

  • 问题:Frida脚本注入成功,但预期的Hook点没有打印日志。
  • 排查1 - 类名/方法签名错误:混淆后的类名可能包含$等特殊字符,需要转义。使用Java.availableJava.enumerateLoadedClasses()先列出所有已加载的类,确认目标类是否已被加载,以及其完整名称。
  • 排查2 - 时机问题:脚本注入时,要Hook的类可能还未被加载。使用setImmediate或监听类加载事件Java.enumerateClassLoaders()Java.ClassFactory.loader来确保在类加载后再执行Hook。
  • 排查3 - 重载(Overload)不匹配:一个方法可能有多个重载版本(参数不同)。使用.overload()指定确切的参数类型。如果不知道,可以尝试targetClass.method.implementation = ...来Hook所有重载,或者用.overload()匹配所有可能类型。
  • 问题:应用检测到Frida或调试环境而崩溃。
  • 技巧:使用Frida的隐身模式,或者使用-f参数在应用启动时第一时间注入,赶在反调试检测启动之前。也可以尝试Hook常见的反调试检测函数(如android.os.Debug.isDebuggerConnected())使其返回false。

5.3 网络抓包篇:HTTPS抓包失败或应用禁用代理

  • 问题:mitmproxy无法解密HTTPS流量,或应用在设置代理后无法联网(证书锁定)。
  • 排查1 - 证书安装与信任:确保mitmproxy的CA证书已正确安装到设备的系统信任证书库(而不仅仅是用户证书库)。在Android 7+上,这通常需要Root后将证书移动到系统目录。
  • 排查2 - 证书锁定(SSL Pinning):应用内置了服务器的公钥或证书,只信任它,而不信任系统证书库。这会直接导致mitmproxy的证书不被信任,连接中断。
  • 解决方案
    • 使用Frida绕过:编写脚本Hook应用使用的网络库(如OkHttp的CertificatePinner类,或底层的SSLContext相关方法),使其跳过证书验证。网上有大量现成的脚本。
    • 使用JustTrustMe模块:如果设备安装了Xposed框架,可以安装“JustTrustMe”模块来全局禁用证书锁定。
    • 修改APK:反编译APK,找到网络库配置或证书校验的代码,将其NOP掉(改为空操作),然后重新打包签名。这需要一定的smali修改能力。

5.4 本地数据篇:数据被加密或校验

  • 问题:SharedPreferences或数据库文件中的关键数据是乱码或加密字符串。
  • 技巧:在静态分析中,搜索加密相关关键词,如“AES”、“DES”、“Cipher”、“encrypt”、“decrypt”。找到加解密工具类。然后使用Frida Hook这些加解密方法,在数据被写入或读取时,打印出明文和密钥。例如,HookCipher.doFinal()方法,就能捕获到进出该方法的原始数据。

6. 从分析到理解:构建更安全的内购系统

经过这样一番深入的逆向分析,我们从一个“攻击者”的视角,完整地审视了一个内购系统的脆弱点。这对于开发者而言,价值巨大。如果你是一名开发者,可以从这次“虚拟攻防”中获得以下加固思路:

  1. 强制服务器端验证:绝不信任客户端。所有购买凭证必须发送到自己的服务器,由服务器向官方商店(Google/Apple)进行验证。这是最重要的原则。
  2. 实现非对称验证:客户端与服务器之间的通信,使用非对称加密签名。服务器为每个客户端生成唯一的密钥对,公钥下发给客户端用于签名请求,服务器用私钥验签。这能有效防止请求伪造和重放攻击。
  3. 关键逻辑放在Native层:将商品ID比对、购买状态校验等核心逻辑用C++实现并编译成Native库(.so)。这能极大增加静态分析和动态Hook的难度。
  4. 实施代码混淆与加固:使用专业的混淆工具(如ProGuard, R8)和商业加固方案(如腾讯乐固、阿里聚安全),对代码进行控制流扁平化、字符串加密、虚拟化保护等,提高逆向工程的门槛。
  5. 增加环境安全检测:在应用启动和支付关键流程中,检测设备是否Root、是否安装了Frida/Xposed、是否处于调试状态、是否存在多开环境等。一旦发现高风险环境,可以限制功能或触发风控。
  6. 设计灵活的令牌系统:不要简单地在客户端存储一个布尔值。使用由服务器签发的、有时效性且与设备/账号绑定的加密令牌(JWT是一种选择)。客户端需要定期或在使用特权功能时,向服务器更新/验证此令牌。

逆向工程如同一把双刃剑。用于非法目的,它侵害创作者利益;用于安全研究与学习,它则是提升软件质量、理解系统架构的宝贵工具。对“极速闪避”这类小游戏内购的拆解之旅,本质上是一次对移动应用安全机制、客户端-服务器信任模型和软件保护技术的深度实践。它教会我们的,远不止如何绕过某个检查点,而是如何以更全面、更审慎的视角去设计和评估一个软件系统的安全性。在技术道路上,保持好奇心与探索精神的同时,坚守法律与道德的边界,才能行稳致远。

← 返回列表