ARTICLE DETAIL

资讯详情

深耕网站建设、视觉设计与SEO优化的一线实战洞察。

App逆向入门实战:新闻详情页接口抓包与签名算法还原

App逆向入门实战:新闻详情页接口抓包与签名算法还原

1. 项目缘起与整体思路拆解

1.1 为什么选一个新闻详情页作为逆向入门靶子

做移动端数据采集的朋友大概都有个共识:网页端能拿到的东西,尽量别碰App。但现实往往不遂人愿——很多内容平台把核心数据往App里一收,网页端只留个摘要或者干脆给你个"请下载客户端查看"。我这次接到的需求就是这种情况:某新闻类应用的详情页正文、发布时间、作者署名、阅读量这些字段,在网页端要么缺失要么被截断,只能从App的接口里想办法。

选新闻详情页作为逆向入门靶子,是我反复权衡后的决定。原因有三:第一,新闻类App的接口结构通常比社交、电商类简单,字段命名直白,返回体多是规整的JSON,不像电商那样层层嵌套还带各种加密签名;第二,详情页的请求参数相对固定,一般就是文章ID加几个设备标识,不像列表页那样带分页游标、推荐算法参数;第三,新闻内容的时效性要求没那么苛刻,允许你慢慢调试,不像抢购类接口那样窗口期只有几秒。对第一次实战的人来说,这三点决定了你踩坑后还有回旋余地。

这里说的"逆向",指的是通过分析App与服务器之间的通信过程,还原出接口的请求构造方式,从而在App之外复现数据获取。它和破解、篡改是两码事——我们只关心"怎么正常地把数据请求出来",不碰任何绕过授权、伪造身份的灰色操作。这个边界一开始就要划清楚,后面所有工作都在这个框架内进行。

1.2 整体方案选型:抓包为主、静态分析为辅

入门阶段最容易犯的错,是一上来就扎进反编译工具里啃smali代码。我见过不少新手,抱着几十万行的反编译结果找了两天,最后连请求地址都没定位到。我的思路是反过来的:先抓包看清通信全貌,再针对性地做静态分析。

抓包的价值在于,它直接告诉你"App发了什么、服务器回了什么",这是最接近真相的一手材料。你拿到一个详情页请求,能看到完整的URL、请求头、请求体、响应体,剩下的工作就是搞清楚这些参数各自怎么来的。大部分参数其实是设备信息、时间戳、版本号这类"看一眼就懂"的东西,真正需要逆向的往往只有一两个签名字段。

静态分析则是在抓包遇到障碍时用的补充手段。比如你发现请求头里有个sign字段,每次请求都不一样,抓包只能看到结果看不到算法,这时候才需要把安装包拆开,找到生成这个字段的代码逻辑。工具上,抓包我用的是系统代理配合证书信任的方式(具体工具名不展开,市面主流抓包工具逻辑相通),静态分析用反编译套件看Java层逻辑,遇到native层再上动态调试。这套组合对新闻类App足够用了。

需要强调的是,整个过程中要严格遵守目标平台的服务条款与相关法律法规,采集行为仅限个人学习研究、且不涉及任何非公开或需授权的内容。这一点不是套话,是决定你这条路能走多远的底线。

1.3 环境准备清单与前置认知

动手之前,把环境搭利索能省掉后面一半的折腾。我列一下自己实际用到的东西,以及每样东西的作用,方便你对照准备。

环节用途关键注意点
抓包工具捕获App与服务器的HTTP/HTTPS通信需在设备上安装并信任其根证书
测试设备运行目标App建议用独立测试机,避免影响日常使用
反编译套件查看App的Java层代码逻辑关注签名、加密相关类
动态调试环境跟踪运行时变量入门阶段可暂缓,抓包卡住再上
接口调试工具手动重放请求、验证参数支持自定义请求头与请求体

前置认知这块,有三件事必须先想明白。第一,HTTPS不是抓不到,是需要证书信任。很多人第一次抓包发现全是CONNECT请求、看不到明文,就以为加密无解,其实是证书没装对或者没被系统信任。第二,App的请求不一定都走同一个通道,有的走标准HTTP库,有的走自研网络层,抓包工具要能覆盖到。第三,参数分"静态"和"动态"两类,静态的抄下来就行,动态的必须搞清楚生成规则,否则重放必失败。把这三条刻在脑子里,后面遇到问题能快速定位到是哪一环出了岔子。

2. 抓包实战:从一片混沌到锁定目标请求

2.1 证书配置这一步,九成人卡在这里

抓包的第一道坎永远是证书。我按步骤说,但重点讲那些文档里不写的坑。

先在测试设备上把抓包工具的根证书下载下来。常规做法是设备连上抓包工具开的代理,然后浏览器访问工具的证书下载页。下载完安装,这一步大部分人能过。真正的坑在"信任"环节——很多系统版本里,用户手动安装的证书默认是不被应用信任的,你得进到证书管理里,找到刚装的那张,手动把它标记为受信任。不同系统版本入口不一样,有的在"安全"里,有的在"加密与凭据"里,找的时候耐心点。

装完证书,打开目标App,随便点进一篇文章。这时候抓包工具里应该开始刷请求了。如果看到的全是加密的隧道请求、没有明文,八成是证书没被这个App信任。这里有个细节:部分App会做证书校验,只信任系统内置证书,不认用户安装的。遇到这种情况,入门阶段可以先换个思路——看看App有没有网页版或者轻量版,接口是否通用;实在不行再考虑更深入的手段,但那已经超出"初学第一次实战"的范畴了,不建议一上来就啃。

提示:证书配置完成后,先拿一个已知的、简单的请求验证一下能不能看到明文,确认通道打通了再去分析目标App,否则你会在"到底是没抓到还是抓到了看不懂"之间反复怀疑人生。

2.2 在请求洪流里精准定位详情页接口

App一启动,请求是成百上千地刷。列表页、推荐位、广告、埋点、配置下发……全混在一起。新手最容易在这里迷失,挨个点开看,看到眼花。我的做法是用操作来缩小范围:先清空抓包记录,然后在App里只做一个动作——点进某篇文章,停住,不再做任何操作。这时候新冒出来的请求,大概率就包含详情页数据。

定位到候选请求后,看响应体。新闻详情页的响应通常有几个特征:包含大段正文文本、有标题字段、有发布时间、有作者信息。你甚至可以直接在响应体里搜文章标题里的几个字,能搜到的基本就是它了。我这次定位到的接口,URL路径里带article/detail字样,请求方式是GET,参数不多,响应是标准JSON,正文放在一个content字段里,还带了publishTime、author、readCount这些字段。到这一步,其实已经成功了一大半。

但别急着高兴。把这条请求的完整信息(URL、所有请求头、所有查询参数)原样复制到接口调试工具里重放一次,看看能不能拿到同样的数据。如果能,说明这个接口没有强校验,直接构造请求就行;如果返回错误或者空数据,说明有参数在起作用,进入下一节的参数分析。

2.3 请求头里的玄机:哪些必须带,哪些可以省

重放的时候,我习惯做一个"减法实验":把请求头一个个删掉,看哪个删了会导致失败。这样能快速区分出"必需头"和"可有可无头"。

实测下来,新闻类App的详情页接口,必需的头通常就这几类:标识客户端身份的(比如版本号、平台标识)、标识设备的(设备ID之类)、以及一个签名字段。像User-Agent、Accept这些,很多时候服务器并不严格校验,删了照样返回数据。但签名字段是硬骨头,删了必挂,改了也必挂,它往往是对请求参数加时间戳加密钥做哈希得来的。

我这次遇到的接口,请求头里有个X-Sign字段,32位十六进制,一看就是MD5或者类似哈希。查询参数里有个ts时间戳,还有个nonce随机串。这三个东西凑一起,基本可以断定签名规则是"参数排序拼接+密钥+哈希"的经典套路。抓包能看到签名结果,但看不到算法,这就必须上静态分析了。

注意:做减法实验时,每次只改一个变量,改完立刻重放验证。一次改多个,失败了你也说不清是哪个导致的。

3. 静态分析:把签名算法从代码里挖出来

3.1 反编译后的第一件事:全局搜关键词

拿到安装包,用反编译套件拆开,得到一堆smali文件和资源。别急着从头读,那是不可能的。第一件事是全局搜索。搜什么?搜你抓包时看到的那些特征字符串——签名字段的头名X-Sign、参数名nonce、ts,或者接口路径里的article/detail。

搜索命中后,顺着引用关系往上找。通常你会落在一个网络请求封装类或者拦截器里,那里就是拼接参数、计算签名的地方。我这次搜X-Sign,直接定位到一个拦截器类,里面有个方法在请求发出前统一给请求头塞签名。点进去看,逻辑很清晰:取出所有查询参数,按key的字典序排序,拼成key1=value1&key2=value2的形式,末尾追加密钥,然后做MD5,转小写,塞进X-Sign头。

这里有个经验:签名逻辑往往集中在一个工具类里,方法名可能叫getSign、buildSign、signParams之类。找到它,整个算法就摊在你面前了。如果搜不到,可能是被混淆了,那就换个思路——搜MD5相关的调用,或者搜时间戳格式化相关的代码,从侧面逼近。

3.2 读懂签名逻辑:排序、拼接、加盐、哈希

把那段逻辑翻译成人话,大概是这么个流程:

  1. 收集本次请求的所有业务参数(不含签名本身),放进一个映射表;
  2. 按参数名的ASCII码升序排序;
  3. 遍历排序后的参数,拼成k=v并用&连接,跳过空值;
  4. 在拼接串末尾追加一个固定的盐值(salt),这个盐通常硬编码在代码里,也可能从配置下发;
  5. 对最终字符串做MD5,输出小写十六进制,即为签名。

我用Python把这个过程复现出来,核心就几行:

import hashlib def build_sign(params: dict, salt: str) -> str: items = sorted((k, v) for k, v in params.items() if v not in (None, "")) raw = "&".join(f"{k}={v}" for k, v in items) + salt return hashlib.md5(raw.encode("utf-8")).hexdigest()

盐值从哪来?就在刚才那个拦截器类里,通常以一个字符串常量出现,或者从某个配置类里读。我这次是在同一个类的静态字段里找到的,一串看起来毫无规律的字母数字组合。把它抄下来,签名就能自己算了。

3.3 时间戳与随机串:动态参数的生成规则

ts是当前毫秒时间戳,这个没什么好说的,int(time.time() * 1000)。nonce是随机串,代码里一般是取UUID去掉横线,或者用随机数生成器拼一个定长字符串。这两个参数本身不参与"保密",它们的作用是让每次请求的签名都不同,防止重放攻击和缓存命中。

这里有个容易忽略的点:时间戳的时区和精度。有的接口要求秒级,有的要求毫秒级,还有的要求和服务器时间偏差不超过一定范围。我这次是毫秒级,且实测偏差在几分钟内都能通过,容错还算宽松。如果你的请求一直返回"签名错误"或"请求过期",先检查时间戳精度对不对,再检查设备时间准不准。

提示:把签名函数封装好之后,先用抓包抓到的那条真实请求做验证——用相同的参数、相同的时间戳、相同的nonce,算出来的签名应该和抓包里的一模一样。对上了,说明算法还原正确;对不上,回去检查排序规则、空值处理、盐值有没有抄错。

4. 完整复现:从单条请求到稳定采集

4.1 构造请求的完整参数清单

把前面所有分析结果汇总,一条详情页请求需要的东西就齐了。我整理成表,方便对照:

参数位置参数名来源是否动态
查询参数articleId从列表页或目标文章获取否
查询参数ts当前毫秒时间戳是
查询参数nonce随机字符串是
查询参数platform固定值,如android否
查询参数appVersion固定值,从安装包信息取否
请求头X-Sign上述参数排序拼接加盐后MD5是
请求头deviceId设备标识,可用固定值测试否

构造请求时,先把静态参数填好,再生成动态参数,最后算签名。顺序不能乱,因为签名依赖前面所有参数。我用接口调试工具手动拼了一次,返回200,正文完整,字段齐全。到这一步,单条请求的复现就算成功了。

4.2 用脚本把流程串起来

手动拼一次是验证,真正要用还得写成脚本。我用Python的requests库,把签名函数、参数构造、请求发送串成一个完整流程:

import time, uuid, requests from sign_util import build_sign # 前面写好的签名函数 SALT = "从代码里抄出来的盐值" BASE_URL = "https://example.com/article/detail" def fetch_article(article_id: str) -> dict: params = { "articleId": article_id, "ts": str(int(time.time() * 1000)), "nonce": uuid.uuid4().hex, "platform": "android", "appVersion": "x.y.z", } headers = { "X-Sign": build_sign(params, SALT), "deviceId": "固定的测试设备标识", "User-Agent": "目标App的UA", } resp = requests.get(BASE_URL, params=params, headers=headers, timeout=10) resp.raise_for_status() return resp.json()

跑通之后,拿几个不同的articleId试,都能正常返回,说明参数构造是稳的。这里我特意把deviceId写成固定值,实测服务器并不校验它和真实设备的绑定关系,省了不少事。如果你的目标接口校验严格,可能需要在首次请求时动态获取设备标识,那就得再往前追一层。

4.3 控制节奏:别让采集变成攻击

脚本能跑通,不代表可以放开跑。新闻类接口对高频请求通常有频率限制,触发后轻则返回空数据,重则临时封禁。我的做法是单线程加随机间隔,每次请求之间sleep 1到3秒的随机值,既不影响采集效率,也不会给服务器造成压力。

import random, time for aid in article_ids: data = fetch_article(aid) process(data) time.sleep(random.uniform(1.0, 3.0))

另外,采集范围要克制。只取你真正需要的字段,别把整个响应体无差别存下来;只采公开可见的内容,别去碰需要登录或付费的部分。这些不是技术问题,是使用边界问题,但恰恰是决定你这项工作是否站得住脚的关键。

5. 踩坑实录与排查速查表

5.1 那些让我卡了半天的典型问题

签名一直对不上。最开始我算出来的签名和抓包的差几位,反复检查算法没发现问题。后来发现是排序规则理解错了——代码里用的是TreeMap,按key的自然顺序排,而我一开始按参数出现顺序拼的。改成字典序排序后立刻对上了。这个坑很典型,排序规则一定要以代码为准,别想当然。

重放返回"设备异常"。有次换了个deviceId重放,接口直接拒绝。回去看代码,发现服务端会拿deviceId和请求头里的另一个标识做关联校验。解决办法是把抓包时那套设备标识原样保留,别自作聪明改。

响应体里正文是空的。抓包看得到正文,脚本请求回来content字段却是空字符串。排查半天,发现是请求头少了一个Accept-Encoding,服务器根据这个头决定返回压缩格式还是明文,缺了它走了另一条分支。补上就好了。这种"看起来无关紧要的头其实影响返回内容"的情况,在减法实验时特别容易漏掉。

时间戳精度搞错。有一版脚本用了秒级时间戳,接口一直返回"请求过期"。改成毫秒级立刻正常。这个错误很低级,但新手真的容易犯,因为不同接口要求不一样,得逐个确认。

5.2 问题排查速查表

现象可能原因排查方向
抓包全是加密隧道证书未信任检查证书是否被系统信任
重放返回签名错误算法还原有误用真实请求反向验证签名函数
重放返回空数据缺少必需请求头逐个补回头做减法实验的逆操作
返回"请求过期"时间戳精度或偏差确认秒/毫秒,校准设备时间
返回"设备异常"设备标识关联校验保留抓包时的完整设备标识
正文为空但状态200编码或压缩头缺失检查Accept-Encoding等头
高频后返回空触发频率限制降频、加随机间隔

5.3 给初学者的几条实在建议

第一,先跑通一条,再谈批量。我见过太多人单条还没验证成功就开始写并发框架,最后问题堆在一起根本没法定位。单条请求从抓包到重放再到脚本复现,每一步都验证通过,再往下走。

第二,把分析过程记下来。哪个参数从哪来、签名算法长什么样、盐值是多少,随手记在一个文档里。逆向过程是探索性的,你今天搞明白了,过一周再看脚本可能就忘了当初为什么这么写。

第三,遇到native层别硬刚。如果签名逻辑不在Java层,而在so库里,入门阶段可以先放一放,看看有没有其他接口或者网页端能迂回拿到数据。不是所有问题都必须正面解决,绕过去也是一种能力。

第四,边界意识比技术更重要。能抓到什么、能采什么、采了怎么用,这三件事心里要有杆秤。技术本身中性,但使用方式决定了性质。我个人的原则是:只碰公开内容、只做低频采集、只用于学习研究,超出这个范围的一律不碰。

这套流程走下来,一个新闻详情页的接口从抓包到稳定复现,前后大概花了我两个晚上。第一个晚上全耗在证书和定位请求上,第二个晚上啃签名算法和调脚本。现在回头看,最难的不是技术本身,而是在一片混沌里保持"先看清全貌再动手"的耐心。很多人卡住不是因为不会,是因为太急,抓到一个请求就埋头分析,结果分析的是个广告接口。先把范围缩小、把目标锁定,后面的路会顺很多。

返回列表