吾爱破解 - 52pojie.cn

 找回密码
 注册[Register]

QQ登录

只需一步,快速开始

查看: 364|回复: 2
上一主题 下一主题
收起左侧

[Android 原创] APP数据采集踩坑实录:从被ban到稳定跑通的4种方案

  [复制链接]
跳转到指定楼层
楼主
xiybhk 发表于 2026-4-8 13:07 回帖奖励
本文仅用于逆向学习,请勿用于非法用途,如有侵权请联系删除。

背景

最近接了个数据采集的需求,目标是某个国内生活服务类APP的详情页数据。网页端做了严格的访问控制,大部分内容都藏在APP里。一开始觉得不难,抓个包、模拟请求就完了。结果前前后后折腾了两周,踩了一堆坑,方案换了四轮才跑通。

写这篇记录主要是因为,网上关于Frida在模拟器上的版本兼容问题几乎没有系统性的资料,我自己走了很多弯路。希望后来人少踩一些。

整个过程中用到了AI工具(Claude Code)辅助分析反编译代码和编写自动化脚本,效率确实提升不少,后面会顺带提到。




方案一:UA伪装直接请求

第一个想法很朴素。抓包的时候发现,同一个页面URL,换不同的User-Agent请求,服务端返回的内容差距很大。普通浏览器UA直接提示"请使用APP查看",但特定客户端的UA可以拿到完整数据。

代码写起来很快,requests + BeautifulSoup解析HTML,半天搞定。
headers = {
    "User-Agent": "Mozilla/5.0 ... (特定客户端标识)"
}
resp = requests.get(target_url, headers=headers)
soup = BeautifulSoup(resp.text, "html.parser")

跑了一批测试数据,问题来了:大概只有不到一成的页面能正常返回内容,剩下的全是频率限制的拦截页。服务端对这类请求做了计数,超过阈值直接返回空。

试过换IP、加延时、分布式请求,都没用。限制是绑定在内容ID上的,不是绑IP。也就是说一个页面被看过几次之后,谁来请求都拿不到了。

放弃,当备用方案都嫌勉强。




方案二:抓包复现API签名

既然网页端走不通,那就直接走APP的API。上mitmproxy挂代理,很快就把API请求结构摸清了。

APP用的是POST请求,参数都在form-data里。返回的JSON结构也很规整,数据质量比网页版好得多。

但问题在签名。

APP请求里带了四个签名字段,缺一个服务端就返回签名校验失败。用jadx反编译APK,追了一下签名逻辑,发现它分两层:

第一层是一个自研的native方法,在 .so 文件里实现。Java层只有一个入口方法,参数进去,签名字段出来,中间是黑盒。

第二层更狠,用了一个商业安全SDK。这个SDK内部是一套字节码虚拟机,把核心逻辑编译成自定义指令集运行。IDA打开.so一看,全是虚拟机dispatch循环,硬逆的话工作量巨大。

两层签名都跟请求参数绑定,改任何一个参数签名就失效。Python纯算复现?想了想放弃了,至少现阶段不现实。
请求参数 → 第一层签名(native .so) → 生成字段A、B
         → 第二层签名(商业SDK VM) → 生成字段C、D
         → 带上A/B/C/D发请求

签名机制过于复杂,纯Python复现成本太高,放弃。




方案三:Frida Hook签名函数

既然自己算不出来,那就让APP帮我算。思路是:Frida注入APP进程,直接调用APP里的签名函数,把参数传进去,签名拿出来。我只需要在Python里构造请求参数,签名的活交给APP自己干。

模拟器选的MuMu 12(Android 12, x86_64)。Frida方案如果跑通,模拟器就只是个"签名服务器",不需要任何UI交互,后台挂着就行。

三天噩梦

第一天装好环境,写了个简单的Frida脚本试水:
Java.perform(function() {
    var targetClass = Java.use('com.xxx.libs.http.NativeSign');
    console.log("Hook成功");
});

frida-server启动正常,attach进程也没报错,但 Java.perform 直接卡死,回调函数永远不执行。

查了一下,Java.available 返回 false。意思是Frida连Java虚拟机都找不到。

接下来三天我试了所有能想到的办法:

尝试结果
spawn模式启动一样卡死
attach到已运行的进程一样卡死
重命名frida-server二进制无效,不是检测问题
换端口启动frida-server无效
APK注入Frida GadgetGadget注入成功但Java Bridge同样不可用
换雷电模拟器 Android 9同样的问题
怀疑是安全SDK检测Frida测试了其他无保护APP,同样的问题


换了两个模拟器、试了Gadget模式,都是同一个症状:Java.available = false。这时候我开始怀疑不是APP的问题,是Frida本身在x86_64模拟器上有bug。

转折点

搜了一圈资料,在52pojie上找到一篇帖子(chenchenchen777发的,研究MuMu模拟器下ARM so加载的),里面提到了一句:Frida 16.0.11在MuMu上可以正常Java.perform。

当时我用的是Frida 17.9.1,最新版。

抱着试试看的心态降级:
pip install frida==16.0.11 frida-tools==12.3.0

模拟器端也换成对应的frida-server-16.0.11-android-x86_64。

重新attach,Java.available 返回 true。

三天的问题,换个版本号就解决了。

后来去翻了Frida的GitHub Issues,找到了 #3692,确认是17.x的回归bug,在x86_64模拟器上Java Bridge初始化有问题。不是某个特定版本的问题,是整个17.x分支都有。

跑通签名

Frida能用之后,剩下的就顺了。

Hook目标类,用message模式暴露签名函数给Python端调用:
'use strict';

var SignClass = null;
var HashMapClass = null;

Java.perform(function() {
    try {
        SignClass = Java.use('com.xxx.libs.http.NativeSign');
        HashMapClass = Java.use('java.util.HashMap');
        send({type: 'ready'});
    } catch(e) {
        send({type: 'error', msg: 'Hook failed: ' + e});
    }
});

function toJavaMap(obj) {
    var map = HashMapClass.$new();
    for (var key in obj) {
        if (obj.hasOwnProperty(key)) {
            map.put(key, String(obj[key]));
        }
    }
    return map;
}

recv('sign', function onSign(message) {
    Java.perform(function() {
        try {
            var params = toJavaMap(message.params);
            var gets = toJavaMap(message.gets);
            SignClass.INSTANCE.value.signStep1(params, gets, message.api);
            SignClass.INSTANCE.value.signStep2(params, gets);
            send({type: 'signed', params: fromJavaMap(params)});
        } catch(e) {
            send({type: 'sign_error', msg: String(e)});
        }
    });
    recv('sign', onSign);
});

Python端构造请求参数,通过Frida message机制发给模拟器里的APP签名,拿到签名字段后直接调API:
class FridaSigner:
    def sign(self, params: dict, gets: dict, api: str) -> dict:
        self._result = None
        self._event.clear()
        self._script.post({
            "type": "sign",
            "params": params,
            "gets": gets,
            "api": api
        })
        self._event.wait(timeout=10)
        if self._result is None:
            raise Exception("Sign timeout")
        return self._result["params"]

# 使用
signed = signer.sign(params, gets, api_name)
resp = requests.post(api_url, headers=headers, data=signed, timeout=30)
data = json.loads(resp.content.decode(errors="replace"))

实测签名速度很快,整个流程大约2秒一条数据。模拟器CPU占用也不高,毕竟只是调了个函数。




方案四:ADB自动化 + mitmproxy(备用)

在Frida调通之前,我还搞了一套备用方案。思路是:既然我不会签名,那就让APP自己去请求,我在中间截获响应就行。
ADB模拟点击 → APP打开详情页 → APP自己签名发API请求 → mitmproxy截获响应 → 保存数据

用ADB发触摸事件模拟用户操作,mitmproxy跑一个addon脚本过滤目标API:
class ResponseCapture:
    def response(self, flow):
        if "target_api_endpoint" in flow.request.url:
            data = json.loads(flow.response.content.decode(errors="replace"))
            save_to_file(data)

这套方案确实能用,我拿它采了50多条数据验证可行性。但痛点也很明显:

速度慢,大约6秒一条,因为要等APP渲染页面。不稳定,偶尔APP会弹广告弹窗把流程打断。列表滚动到底部时要处理加载更多的逻辑。返回上一页偶尔会出现导航栈错乱。

作为Frida跑不通时的保底还行,但不适合大批量采集。




最终架构

Frida调通之后,整个系统长这样:
┌──────────────────────────────────────┐
│  Python 主控脚本                      │
│  构造请求参数 / 配额管理 / 重试 / 输出  │
└──────────────┬───────────────────────┘
               │ Frida RPC (message模式)
┌──────────────▼───────────────────────┐
│  MuMu模拟器(后台,不需要操作UI)       │
│  frida-server-16.0.11                │
│  APP进程保持运行,仅提供签名服务        │
└──────────────────────────────────────┘

工程化方面做了一些事情,简单说几个:

错误处理用了三级分类。API返回的各种错误码分成"可重试"、"永久失败"和"配额耗尽"三种。可重试的做指数退避(2的n次方加随机抖动),配额耗尽的切换账号,永久失败的记录日志跳过。
class RetryableError(Exception): pass
class PermanentError(Exception): pass
class QuotaExhausted(Exception): pass

def classify_error(code, msg):
    if code in QUOTA_CODES:
        return QuotaExhausted()
    if code in RETRYABLE_CODES:
        return RetryableError()
    return PermanentError()

进度保存用了原子写入。先写临时文件再rename,防止中途crash导致进度文件损坏。我中间因为没做好这个,程序crash了一次丢了几十条数据的进度,从此老老实实写原子保存。

去重是内存集合加磁盘文件双写。启动时加载历史ID到内存set里,运行中每处理一条就追加写入文件。重启后能无缝续传。

还做了个预过滤。先调列表API拿到候选ID和摘要信息,根据摘要字段过滤掉空数据(大约占四分之一),只对有效的ID去请求详情。这个优化省了不少配额。




四种方案对比

维度UA伪装Python签名Frida HookADB自动化
可行性不到一成可用不可行可行可行
速度快但成功率低-~2秒/条~6秒/条
稳定性-
外部依赖-Frida+模拟器ADB+mitmproxy+模拟器
开发成本半天放弃了1天(不算踩坑)2天





踩坑总结

Frida版本是第一优先级排查项。在x86_64模拟器上用Frida,先试16.0.11。不要从最新版开始。17.x的Java Bridge在模拟器上有已知的回归bug(#3692)。这一条能帮你省三天。

"签名服务器"模式比UI自动化好太多。一开始我觉得ADB方案更直观,后来发现Frida Hook的优势是碾压级的。模拟器只当签名服务器用,不需要管UI状态、不需要处理弹窗、不需要等页面加载。纯函数调用,干净利落。

两层签名不一定要两层都自己算。如果第一层是标准算法(HMAC之类的),可以考虑纯Python复现第一层就行。第二层如果是商业SDK的字节码VM,直接放弃硬逆,用Frida调。把精力花在值得花的地方。

工程化的东西不能偷懒。进度保存、去重、错误分类、配额管理,代码量不大但不能没有。长时间无人值守运行,没有这些东西迟早翻车。

论坛帖子有时候比官方文档管用。Frida版本的问题,官方文档不会告诉你。是咱52pojie上一篇模拟器SO加载分析的帖子给了我关键线索。社区的经验类文章真的很有价值,这也是我写这篇的原因。




后续

目前方案依赖模拟器运行。如果要规模化,模拟器是个瓶颈。后面打算看看Unidbg,试试能不能把.so加载到纯Java环境里直接调用签名函数,去掉模拟器依赖。第一层签名如果是标准算法,也许可以纯算还原。

有进展再写。




遇到类似Frida版本问题的兄弟可以楼下交流。对签名方案有经验的大佬也欢迎指点一下Unidbg的路线。

免费评分

参与人数 4吾爱币 +4 热心值 +2 收起 理由
Issacclark1 + 1 谢谢@Thanks!
helian147 + 1 + 1 热心回复!
丶七年 + 1 感谢发布原创作品,吾爱破解论坛因你更精彩!
15126819695 + 1 + 1 app逆向很不错的思路 谢谢分享

查看全部评分

发帖前要善用论坛搜索功能,那里可能会有你要找的答案或者已经有人发布过相同内容了,请勿重复发帖。

沙发
NSP 发表于 2026-4-9 18:28
本帖最后由 NSP 于 2026-4-9 18:34 编辑

你太牛了
3#
fzlte0 发表于 2026-4-9 20:55
方案三:Frida Hook签名函数

既然自己算不出来,那就让APP帮我算。思路是:Frida注入APP进程,直接调用APP里的签名函数,把参数传进去,签名拿出来。我只需要在Python里构造请求参数,签名的活交给APP自己干。

模拟器选的MuMu 12(Android 12, x86_64)。Frida方案如果跑通,模拟器就只是个"签名服务器",不需要任何UI交互,后台挂着就行。

好方法,点个赞。
您需要登录后才可以回帖 登录 | 注册[Register]

本版积分规则

返回列表

RSS订阅|小黑屋|处罚记录|联系我们|吾爱破解 - 52pojie.cn ( 京ICP备16042023号 | 京公网安备 11010502030087号 )

GMT+8, 2026-4-10 07:01

Powered by Discuz!

Copyright © 2001-2020, Tencent Cloud.

快速回复 返回顶部 返回列表