用Frida-RPC实现安卓逆向:演唱会数据自动化获取实践

发布时间:2026/10/2 4:31:23
用Frida-RPC实现安卓逆向:演唱会数据自动化获取实践 说实话第一次把目标锁定在“安卓逆向 演唱会数据”这两个词上的时候我脑子里蹦出来的第一个念头是这东西能做成自动化吗很多人一提逆向就想到破解、脱壳、分析so文件一听就觉得门槛特别高。但真正把这些流程跑下来之后我发现最难的其实不是逆向本身而是怎么把逆向的结果变成一条稳定、可复用的数据通道。这篇文章就围绕我用Frida-RPC实现演唱会数据自动获取的完整过程来写核心关键词是安卓逆向、Frida-RPC、自动化。目标是让有安卓基础、但对Frida桥接调用还比较陌生的朋友看完之后能自己动手跑通一条从Hook到RPC再到数据的流水线。我选择的目标应用是票务类APP大家应该都熟就是那个每逢热门演唱会就“抢不到票”的大麦。我要拿的是演唱会列表数据场次、城市、演出名称、票价区间、状态这些公开信息。为什么不直接抓包因为票务APP的接口做了一层又一层防护直接拿到加密数据也解不开为什么不是简单Hook一下就算完因为只看不导出数据留在App进程里没法给后续的数据分析、定时巡检用。Frida-RPC正好解决这两个问题让App自己把数据解密、自己把列表组装好然后通过RPC通道把结果交到我手里。这篇不会讲怎么绕过支付、怎么薅羊毛那些事情碰了就是给自己找麻烦。我只会把技术链路讲清楚App端怎么Hook、RPC接口怎么设计、Python端怎么调用、数据怎么做清洗。咱们走的是技术研究加效率工具的路子合规这条线我从头到尾都会守着。1. 为什么选Frida-RPC这条路1.1 直接抓包不行吗——先把痛点列清楚很多人一听到“获取App数据”第一反应就是用Charles或者Fiddler抓包把请求地址复制出来自己在代码里复现一遍请求。这个思路在普通App上没毛病我以前也这么干。但到了大麦这种体量的票务App上直接抓包会遇到三座大山第一HTTPS证书校验。App内部做了证书固定就算你给手机装了用户证书客户端照样不认。你要么用Frida去Hook掉证书校验逻辑要么就只能在模拟器里装Xposed模块去绕过。这条路不是走不通而是每次App更新你都得重新适配一次维护成本非常高。第二请求参数是加密的。就算你拿到了接口地址body里的关键参数也不是明文。有的是把参数做了MD5加盐有的是走了自定义加密算法你光看着加密后的字符串根本不知道后端校验了什么。想逆推加密算法票务App的so文件做了加固字符串也不给你明着放逆向成本直接拉满。第三返回数据不一定是明文。有些接口返回的JSON里关键字段是加密过的需要客户端拿本地密钥解密后再展示到界面上。也就是说就算你把响应体原封不动搬过来得到的也是一堆没法用的密文。所以我当时的判断是与其在外围跟加密算法死磕不如让App自己把活干了。数据在界面上已经显示出来了说明App内部一定有一段代码完成了从密文到明文的转换。我要做的就是找到那个已经处理完数据的对象把里面的值掏出来。这就是Frida最擅长的事情。1.2 为什么是Frida而不是Xposed或者Magisk模块Xposed我早些年也用过功能很强但有两个让我很头疼的问题第一装Xposed框架需要重启设备调试一次重启一次来回折腾非常浪费时间第二Xposed的Hook代码是写死在模块里的要改逻辑就得改模块、重新打包、重装迭代效率太低。Frida完全换了一个思路。它把JavaScript引擎注入到目标进程里Hook代码用JS写改完逻辑保存一下重新运行一次脚本就生效了连App都不用重启。调试的时候你能在命令行里实时看到输出能动态地把对象内容打印出来这种感觉跟Xposed那种“装好模块再祈祷它能跑”的体验完全不是一个量级。还有一个特别实用的点Frida是跨平台的。电脑上装一个frida-tools手机端跑一个frida-server两边通过USB连起来就行。Hook逻辑写在JS文件里控制逻辑写在Python里两边用一套接口对接。这样一来我的数据脚本可以很自然地嵌进自动化测试框架比如GitLab CI/CD里那种定时任务——晚上跑一次数据入库第二天早上直接看报表全程不用人管。1.3 用RPC而不是简单Hook的理由如果只是临时看一眼数据那直接写Hook脚本打console.log就够了。但我的目标是自动化是要让外部程序主动来取数据那就得换一种思路由App进程提供一个“服务”外部通过USB通道来调用。Frida的RPC机制干的就是这件事。你在JS脚本里用rpc.exports把函数暴露出去Python端就能像调用本地函数一样直接调用App进程内的Java方法。听起来很玄乎其实你可以这样理解普通Hook像是你站在窗外往厨房里看能看见厨师在做什么菜但你吃不到RPC是你在厨房墙上开了一个小窗口厨师把做好的菜直接从这个窗口递出来给你。窗口开好了以后你随时想吃什么喊一声就行。从自动化角度来看这个能力是决定性的。因为可以把获取数据的动作拆成一个个可复用的函数需要的时候就调一下不需要人一直盯着脚本执行。App在里面帮我把数据准备好我在外面收割结果整个过程是双向的、动态的而不是一次性的静态Hook。2. 项目前置准备与整体方案设计2.1 环境清单设备、工具和版本在动手写代码之前先把环境整理好。我用的是一台小米手机作为测试机系统是Android 9已经解锁了Bootloader并刷入了Magisk。真机的兼容性比模拟器好很多因为模拟器的硬件特征和真实手机差别较大有些App会做模拟器检测。如果你手头没有可折腾的旧手机用云手机也行但稳定性会差一些实测下来真机最省心。Frida版本我用了15.2.18这个版本比较稳定往上的一些新版本在Android低版本上反而容易出问题。电脑端需要装Python 3.8以上frida-tools直接用pip安装就好。工具版本要求用途测试真机Android 8~10均可运行目标App承载Frida注入frida-server与电脑端frida版本一致手机端注入服务核心组件frida-tools15.x电脑端命令行工具与Python绑定Python3.8编写调用RPC的控制脚本ADB最新版手机与电脑通信的基础通道抓包工具HttpCanary手机端辅助确认接口位置不是主力方案安装frida-server的时候有个坑必须保证手机端的frida-server版本和电脑端的frida-tools版本一致否则连接的时候会报“version mismatch”。这个错很好排查但它就是爱在关键时候冒出来打断思路所以建议一开始就固定版本。2.2 整体流程从APK到结构化数据我的完整流程分六步走通之后就可以形成一个自动化的闭环手机连接电脑启动frida-server用frida -U -f cn.damai -l hook.js命令启动App并注入Hook脚本。这一步的作用是让Frida先于App业务代码运行保证不会漏掉早期注册的对象。手动在App里刷一遍目标页面比如进入某个城市的演唱会列表页触发数据的加载。Hook脚本实时观察内存中的数据结构定位承载数据的对象和字段。把验证过的代码封装成RPC函数挂到rpc.exports上。Python端通过frida.get_device_manager()连接USB设备调用RPC函数拿数据。对拿到的JSON做清洗和结构转换存入本地文件或者数据库。整套流程看着不复杂但每一步都有很多细节尤其是第三步——定位关键对象直接决定后面所有工作能不能顺利推进。2.3 先给自己定三条不能碰的红线写逆向相关的代码我始终提醒自己要有边界。这个项目我只做数据获取和展示下面三条红线从头到尾没碰过第一条不碰支付和账号体系。获取演唱会数据只需要浏览公开页面不需要登录账号更不需要处理余额、优惠券之类的东西。凡是涉及支付逻辑的类和方法看到了也直接跳过。第二条不做签名校验绕过。我全程没有去替换签名、Hook掉完整性校验也没有对App做任何暴力破解。一个合格的测试机会检测到篡改后处于异常状态但我的目标本来就不是绕过它。第三条不对接口做高频恶意请求。Frida-RPC的调用频率和正常用户浏览时的数据加载频率保持一致不会去压接口、刷验证码或者做任何影响服务端稳定的事情。技术是用来做效率工具的不是用来搞破坏的。这些问题想清楚之后后面的编码就不会走偏。3. 核心实战从Hook定位到RPC导出3.1 定位目标函数三步找到下刀口定位是整个项目里最花时间的一步。我总结下来的方法可以分成三步第一步从UI层面反推。打开演唱会列表页看页面上的数据长什么样。我需要的字段基本都能在界面上看到演唱会名称、艺人、城市、时间、票价区间、销售状态。那么在App内部一定会有一个对象把这些字段都装起来并且大概率是一个列表。我只需要在Frida里找到这个Adapter或者数据源对象。第二步用枚举法缩小范围。Frida有一个很实用的API叫Java.choose它可以在Java堆里按类名搜索所有存活对象。我可以先把可能跟演唱会相关的类名粗略扫一遍比如类名里带Concert、ShowItem、ProjectItem的打印出每个对象的toString结果看看哪个对象里的数据正好和我屏幕上看到的一致。第三步确认并深入。找到目标对象之后用反射把它的字段全部打出来看每个字段的类型和名字。这一步是纯体力活但也是最能让人感到“原来如此”的时刻——你会亲眼看到App在内存里已经把数据准备得清清楚楚我们只是临门一脚把它导出来。下面是我当初定位时写的一个简单脚本框架作用是枚举与演唱会条目相关的对象并打印toString内容// hook_find.js Java.perform(function () { var targetClasses [ cn.damai.commonbusiness.seatbiz.view.model.BasePriceInfo, cn.damai.ticklet.ui.detail.bean.TicketShowItem, cn.damai.projectview.bean.ProjectItem ]; targetClasses.forEach(function (clsName) { try { Java.choose(clsName, { onMatch: function (instance) { console.log([found] clsName - instance.toString()); }, onComplete: function () { console.log([done] clsName); } }); } catch (e) { // 类不存在或者加载失败直接忽略 } }); });这段代码要配合App已经打开列表页的状态来跑因为对象没被创建的话Java.choose是搜不到东西的。我第一次跑的时候等了两分钟什么都没输出后来才发现是手机端的frida-server挂了重新启动之后马上就有数据。3.2 从验证Hook到写RPC接口定位到目标对象之后先不要急着写RPC。先把数据打印出来确认字段名和我们理解的一致这个阶段我叫它“验证Hook”。验证通过之后再把脚本改造成RPC导出的模式。RPC的语法很直接在JS脚本里用rpc.exports定义一个导出对象每个属性就是一个可被外部调用的函数。下面的代码是我当时改造成RPC接口的一个简化版本演示了如何从页面中抓取列表数据并返回给外部// hook_rpc.js Java.perform(function () { var ProjectItem Java.use(cn.damai.projectview.bean.ProjectItem); var ArrayList Java.use(java.util.ArrayList); function getConcertList() { var result []; Java.choose(cn.damai.projectview.bean.ProjectItem, { onMatch: function (instance) { var item { name: , city: , showTime: , priceRange: }; try { item.name instance.getName(); } catch (e) {} try { item.city instance.getCityName(); } catch (e) {} try { item.showTime instance.getShowTime(); } catch (e) {} try { item.priceRange instance.getPriceStr(); } catch (e) {} result.push(item); }, onComplete: function () {} }); return result; } rpc.exports { getConcertList: getConcertList }; });这段代码的意图是在App内存里找到所有ProjectItem对象把关键字段取出来装进一个数组返回。实际运行时你会发现App滚动列表只会加载当前屏幕周围的数据所以拿到的条数取决于列表当前位置。想要拿全量数据要么配合手动滚动要么在RPC函数里去调用App内部的数据加载方法。3.3 给RPC接口做参数化改造演唱会数据跟城市强相关光有一个getConcertList肯定不够用。我后面把它改造成了支持参数的形式外部传入城市ID和页码函数先去找到对应的列表数据源再调用App内部的分页加载逻辑最终返回指定页码的数据。改造的关键点在于RPC函数运行在App进程里而参数是从Python端传进来的。在有限的生命周期里目标对象可能还没有准备好所以代码里要做空值保护。还有一个比较隐蔽的问题Frida的RPC调用是从JS线程发起的如果直接在RPC函数里去更新UI或者触发网络请求有可能会遇到线程安全或者UI线程限制的问题。我的处理方式是RPC函数只读内存中已有的数据不触发网络请求网络请求通过手动在App里滑动列表来触发。这样写起来简单也足够稳定。为了减少重复代码我把对象转JSON的逻辑抽成了一个统一的方法不同数据源传不同字段名就行。3.4 Python端调用RPCJS端准备完毕接下来就是Python控制端的活了。连接Frida并调用RPC函数的代码非常简单核心逻辑就几行import frida import time device frida.get_usb_device() session device.attach(cn.damai) script session.create_script(open(hook_rpc.js, encodingutf-8).read()) script.load() # 调用JS中暴露的RPC函数 data script.exports_sync.get_concert_list() print(type(data), data)这里有一个容易被坑的地方script.exports和script.exports_sync的区别。前者是异步的返回值是一个Future得自己处理await后者是同步的直接阻塞到JS端函数执行完返回结果。对于数据获取这种严格依赖返回值的场景用exports_sync省心很多。另一个坑是设备连接状态。手机息屏、USB线松动、frida-server崩溃、App被系统回收任何一个问题都会让Python端报连接错误。我后来封了一层重试逻辑发现连接断了就等待3秒重新attach实测可以解决大部分偶发问题。代码大致长这样def get_concer_list_with_retry(max_retry3): for attempt in range(max_retry): try: device frida.get_usb_device(timeout5) session device.attach(cn.damai) script session.create_script(open(hook_rpc.js, encodingutf-8).read()) script.load() return script.exports_sync.get_concert_list() except Exception as e: print(f尝试第{attempt 1}次失败{e}) time.sleep(3) return None加上重试逻辑之后整个脚本的健壮性明显提升不再需要人盯在旁边处理偶发故障了。4. 数据清洗与结构化输出4.1 演唱会数据的常见字段结构从RPC接口返回的数据是JSON格式但结构比较乱有嵌套对象、null值、还有字段缺失的情况。为了做数据分析和定时巡检我会把它清洗成一张规范的二维表。结合大麦演唱会列表页常见的信息整理之后的字段大概长这样字段名类型说明示例project_idstring项目唯一ID381728namestring演出名称“某某巡回演唱会·北京站”artiststring艺人/团体“某某乐队”citystring举办城市“北京”venuestring场馆“国家体育场”show_timestring演出时间“2025-07-12 周六 19:30”price_rangestring票价区间“380-1680元”status_textstring销售状态“预售”/“热卖中”/“已售罄”update_timestring数据抓取时间“2025-04-06 23:10:33”update_time这个字段是我自己加的非常重要。定时巡检任务跑完之后如果没有这个时间戳下游根本不知道这份数据是什么时候抓的遇到数据过期的问题会很难排查。4.2 清洗逻辑与入库实践JSON原始数据里经常出现这种情况某个字段值为None某个字段明明存在但内容是空字符串还有个别字段在JSON key里根本没出现。我的清洗规则是所有字段都转成字符串空值统一填时间字段做格式规范化。简单来说就是保证下游读数据时永远不需要再做空值判断。清洗完的数据我直接写入SQLite。用SQLite的原因很简单单文件、零配置、Python内置支持完全够用。建立一张concerts表字段就跟上面表格一样主键是project_id加update_time的组合这样每天跑一次巡检同一场演唱会会有多条带时间戳的记录方便追溯状态变化。import sqlite3 import json import datetime def clean_rows(raw_rows): clean [] for row in raw_rows: item {} item[project_id] str(row.get(projectId) or ) item[name] str(row.get(name) or ) item[artist] str(row.get(artistName) or ) item[city] str(row.get(cityName) or ) item[venue] str(row.get(venueName) or ) item[show_time] str(row.get(showTime) or ) item[price_range] str(row.get(priceStr) or ) item[status_text] str(row.get(statusText) or ) item[update_time] datetime.datetime.now().strftime(%Y-%m-%d %H:%M:%S) clean.append(item) return clean def save_to_db(rows, db_pathdamai_concerts.db): conn sqlite3.connect(db_path) c conn.cursor() c.execute( CREATE TABLE IF NOT EXISTS concerts ( project_id TEXT, name TEXT, artist TEXT, city TEXT, venue TEXT, show_time TEXT, price_range TEXT, status_text TEXT, update_time TEXT, PRIMARY KEY (project_id, update_time) ) ) c.executemany( INSERT OR REPLACE INTO concerts VALUES (?,?,?,?,?,?,?,?,?), [(r[project_id], r[name], r[artist], r[city], r[venue], r[show_time], r[price_range], r[status_text], r[update_time]) for r in rows] ) conn.commit() conn.close()用这套逻辑跑了一段时间之后我发现一个有趣的变化某场演唱会的状态从“预售”变成了“热卖中”然后又变成了“已售罄”。这就是带时间戳的好处你能从历史记录里看到状态变化的过程而不是只看到当下的一瞬间。5. 常见问题与排查技巧实录5.1 我踩过的五个高频坑整个项目做下来有五个坑出现频率最高每次都要花时间排查。我把它们整理成一张表格希望能帮你少走弯路问题现象根本原因解决方案Frida连接报unable to connect to remote frida-serverUSB连接断开或frida-server未启动确认adb devices有设备重新执行adb shell /data/local/tmp/frida-server 注入后脚本不输出任何日志目标类未加载先手动打开对应页面触发数据加载后再执行HookRPC调用一直阻塞不返回exports_sync调用时JS端出现异常在JS函数里加try/catch返回友好错误信息拿到的列表数据条数很少只有当前屏幕附近的对象存活配合手动滑动列表或多触发几次分页加载App启动时崩溃frida-server版本与frida-tools不一致统一版本重新推送frida-server其中第四个坑最有迷惑性。我第一次跑通RPC接口时特别兴奋结果返回的数据只有5条当时还以为是字段名取错了。后来仔细想了一下才明白Java.choose只能在堆里找存活对象列表滚到哪堆里的对象就到哪。屏幕外被回收的对象你自然拿不到。所以要么手动慢慢滚要么用代码触发App内部的列表加载逻辑让数据源把所有数据都拉进来。5.2 提升稳定性的几个操作细节稳定性是自动化脚本能否落地的关键分享三个我实操后觉得很值得的细节。第一个新增脚本启动时先做一个“暖场”操作。App冷启动之后列表页一般不会立刻加载数据RPC调用会扑空。我的做法是启动App后先等3秒然后模拟一次滑动操作再等2秒最后才开始调RPC。这套“等–滑–等”节奏虽然粗暴但实测效果很好。第二个给手机开启“不锁定屏幕”模式。手机息屏之后USB通信和App运行状态都可能受影响Frida连接会变得不稳定。我在开发者选项里把锁屏方式改成了“无”然后保证充电线一直插着这样脚本跑到半夜也不会掉线。第三个隔离环境。这台测试机除了用来跑数据获取脚本之外不装任何多余的应用也不作为日常手机使用。App的缓存、通知、弹窗这些干扰因素能减少就减少。干净的环境能让问题定位快很多尤其是当脚本偶发失败时你不会怀疑是哪里的通知弹窗挡住了界面。5.3 关于工具能力边界的反思Frida-RPC这套方案强不强很强。它能直接钻进App进程里把内存中整理好的数据搬出来效率比模拟点击、OCR识别高一个量级。但我也清楚地知道这套方案的边界它依赖App内部的代码结构一旦App改了类名、改了实现方式脚本就要跟着适配。它不是一劳永逸的方案而是需要持续维护的工具。从另一个角度看Frida-RPC只是安卓逆向工具箱里的一把好用的扳手它解决的是效率和自动化问题不是所有数据问题。如果访问的频率不高、数据量不大直接从公开接口或者网页端抓取也许是更轻量的选择。技术选型永远是场景说了算不是工具说了算。写在最后的个人体会这个项目做完之后我对“自动化获取数据”这件事有了更具体的理解。以前总以为自动化就是写个爬虫、跑个定时任务但真正落到安卓App这个场景时你会发现最大的障碍不是“怎么写代码”而是“怎么获得干净的数据”。加密、加固、风控每一层都在提醒你数据不是免费的想拿就要付出对应的技术成本。Frida-RPC给我的感觉是它提供了一个非常优雅的交互模型App在它的世界里准备好材料我在我的世界里发出请求两边通过一条USB线对话。这个过程中我没有对App做任何破坏性的修改没有绕过签名校验也没有触碰支付和账号体系我只用了一个调试工具读取了它本来就展示在界面上的公开信息。如果你也想在自己的安卓逆向项目里尝试Frida-RPC我的建议是别一上来就啃大而全的框架先拿一个简单App练手比如一个新闻客户端、一个工具类App定位一个列表数据源跑通一个RPC函数。等到你对Java.choose、rpc.exports、exports_sync这套链路有了体感之后再回头处理复杂App你会发现那些加密、混淆、加固都没有想象中那么可怕。真正的门槛不在工具在于你愿不愿意一层一层往下看。