☰
Android逆向实战:从脱壳到还原native层签名算法全解析
2026/10/17 7:24:58 网站建设 项目流程

干逆向这一行,最常碰到的就是那种“半黑盒”目标:你知道它挂在哪个业务环节上,也大概猜得出来它做了什么,但核心那层签名和加密逻辑,被壳、混淆和so层守得严严实实。最近我在给某头部电商平台APP做安全评估时,正好拆到其中一个代号为bx-pp的业务插件。这个名字第一眼看不出含义,查下来实际上是“业务扩展插件”的内部叫法,负责权益活动校验、券码生成和部分关键接口的签名。这篇文章就把我这次从拿到样本到还原核心签名逻辑的完整过程拆开讲,包括工具链怎么选、静态分析怎么定位、Frida怎么Hook、native层怎么逆,以及这一路踩过的坑和最终合规边界。

无论你是刚接触客户端逆向的初学者,还是已经在做安全评估的工程师,这篇都能给你一套可以复用的分析框架。特别提醒一点:所有操作都建立在甲方授权的安全测试前提下,思路和工具本身是中性的,但用在哪里、边界划在哪里,每个人心里得有杆秤。

1. 项目背景与目标拆解

1.1 bx-pp到底是什么

初看bx-pp这个代号,很容易被绕晕。我在jadx里搜了一圈之后才确认,它已经经过了混淆和二次开发。从代码结构上看,bx可以对应“business extension”,pp可以理解为“plugin platform”,合起来就是业务扩展插件平台。该电商APP把一些高频、易变的营销玩法拆成了独立模块,bx-pp就是其中一个专门处理权益活动的组件。它主要做了三件事:接收服务端下发的活动配置,在本地生成带有业务语义的请求参数,通过一套私有签名算法给每次请求盖上“身份戳”。服务端拿这个戳去校验参数有没有被篡改、请求者是不是伪造的客户端。

搞清楚这个背景很重要。因为逆向分析最忌讳拿着代码瞎翻,不知道目标业务是什么,翻半天也找不到逻辑入口。我从APK包的接口路径、字符串引用和资源文件入手,确定了bx-pp大概的活动类型包括优惠券核销、积分兑换和任务进度上报。这为后面定位关键词提供了明确方向:什么sign、coupon、task、exchange,优先级一下子清晰了。

如果你也想做同类分析,第一步不要急着反编译,先看一眼APK的AndroidManifest.xml里注册了哪些Activity、Service,再查assets和res/raw下有没有服务端下发的JSON配置样本。这些信息能帮你把“代码结构”翻译成“业务故事”,后面的定位会快很多。

1.2 逆向的基本盘和边界怎么划

在动手之前,必须把这次逆向的目标写清楚。我做的是安全评估,目标有三层:第一,确认bx-pp的私有签名算法强度;第二,找出是否存在可被利用的越权或参数篡改漏洞;第三,把问题反馈给对应团队做加固。这个过程不是“破解”或“薅羊毛”,而是有授权、有范围的攻击视角验证。

在我实际操作的项目里,授权边界都会写成文档,比如只允许分析测试环境的APP版本、不允许获取真实用户数据、不允许越过某一层接口。像bx-pp这种涉及权益的系统,动了真金白银的业务数据,一旦越界就不是技术问题,而是责任问题。我建议所有做类似分析的朋友,开工前先把授权范围邮件或文档留档,哪怕是在自己本地的模拟器环境里跑,也要养成写记录的习惯。这不只是保护你自己,也是给“逆向分析”这项技术一个更干净的专业形象。

另一个要提前确认的是分析环境。我习惯用一台Android 9的模拟器作为主测试机,配合一台真机做交叉验证。模拟器跑起来方便,快照回滚也快,但很多加固壳对模拟器有检测;真机更接近线上环境,但调试时容易受厂商ROM的影响。bx-pp这次的样本在模拟器上能跑,但检测逻辑会随机触发,稍后会说具体的绕法。

2. 前期侦查与环境搭建

2.1 工具链选型解析

逆向工具没有“最好”,只有“最适合当前场景”。我这次用到的工具分成四类:

  • 静态分析:jadx反编译查看Java层代码,IDA Pro分析native层so库。
  • 动态调试:Frida为主,objection做辅助,用来跑脱壳脚本和绕过检测。
  • 抓包工具:Burp Suite做HTTPS中间人代理,配合手机端安装自定义CA证书。
  • 辅助分析:frida-dexdump脱壳、frida-trace跟踪指定方法、CyberChef处理编码和加密数据。

选Frida而不是Xposed,原因是Frida的脚本式Hook更灵活,不需要重启系统,而且对加固壳的绕过方案多,社区脚本也丰富。比如“objection”里集成的几个绕过模块,可以在几秒钟内把常见的root检测和证书校验全部disable掉。不过Frida的缺点是它本身会留下痕迹,部分加固方案会检测linux层的特征,比如frida-server的默认端口27042,这时候要么改端口,要么用更隐蔽的注入方式。

IDA Pro是分析so层的重武器。bx-pp只有一个主so用来做签名和校验,体积不大,但里面用了OLLVM的控制流平坦化,看起来会非常“难看”。这时候需要配合Hash的识别模式:在native层看到大段查表、位移、循环展开,再结合入参里出现时间戳和设备ID,基本能猜出是SHA或MD5家族。后面我会专门介绍我们怎么在IDA里通过交叉引用找到关键函数。

2.2 抓包与脱壳实操

抓包环境是第一个大坑。APP默认走HTTPS,而且开了证书校验。我第一次直接挂Burp的代理打开bx-pp,请求全部报错,服务端返回“invalid request”。原因就出在证书信任上——APP只信任它内置的CA,不信任用户安装的证书。

绕过方法我试了两条路径,都建议你记下来。第一条是直接用objection的“android sslpinning disable”命令,它会自动把常见的X509TrustManager校验逻辑Hook掉,几秒钟后就能看到明文流量。第二条是针对更强校验的:有些APP会校验证书哈希并在native层做二次验证,这个时候单纯Hook Java层不够,得用Frida在native层拦截SSL_CTX_set_verify这类函数,把验证回调函数改成直接返回1。

脱壳这一步也需要说清楚。现在的加固方案基本都做了“抽取代码”,也就是真实的dex在运行时才会被还原到内存。静态直接抓下来的dex通常是一具空壳。对bx-pp我用的方案是先跑起来,让壳自己把真实dex装载到内存,然后用frida-dexdump把内存里的dex镜像dump出来,再去jadx里反编译。这个方案实测稳定,dump下来的dex被识别为“完整度90%以上”,核心业务代码都能看到。

脱壳成功后,先把反编译出的代码和原始空壳代码做一次对比,列出多出来的类。这不是凑字数,而是非常有效的定位技巧:壳外代码通常是资源加载和入口注册,壳内真正解出来的才是业务逻辑。bx-pp的关键类就是在对比之后一下子浮出水面的。

3. 静态分析定位核心逻辑

3.1 从入口Activity到业务Manager

拿到完整dex之后,静态分析的第一步不是翻大段代码,而是建立“入口映射”。我打开AndroidManifest.xml,找到bx-pp相关模块注册的Activity,特别是带有exported=true的入口。这个Entry Activity是分析的路标,顺着它的onCreate走,就能看到它初始化了哪些Manager和Helper。

以bx-pp为例,它的入口Activity叫“BXEntryActivity”(反混淆后的名字,原始名已经无法辨识)。onCreate里做了三件事:初始化一个SP存储、调用BizService的初始化方法、注册一个网络回调。这些调用链马上把我们引到了com.xxx.bx.core包下的BizManager类。jadx里追踪到这个类之后,我习惯先把类的字段列表看一遍:哪些是静态常量,哪些是加密用的密钥字符串,哪些是业务状态位。bx-pp的BizManager里果然有一个“PRIVATE_SALT”字符串和一份“RULE_MAP”,前者大概率参与签名,后者对应活动规则映射。

在整理方法时特别留意了带@JavascriptInterface注解的方法或带反射调用的方法。这类方法容易被外部调用方触发,是潜在攻击面。bx-pp中就有一个从WebView回调进入的对账接口,后面测出了一个参数覆盖问题,这是后话。

如果你自己在分析别的APP,建议在jadx里多用“Search”功能的“Text”和“Method”两个Tab,关键词从业务词开始(比如coupon、award、verify),再扩展到技术特征词(比如sign、nonce、timestamp)。一遍搜完心里基本有谱了。

3.2 通过签名与回调锁定关键参数

定位核心方法后,接着就是锁定“哪个参数是服务端真正校验的”。我通常会在Frida里先做一个初次Hook:把BizManager里所有跟“sign”相关的方法函数名打印出来,再给它所有参数和返回值打出日志。生成签名的方法一般特征是:入参多(包括时间戳、随机数、设备ID、业务参数拼接字符串)、返回一个定长Hex字符串(32位或者64位最常见)。

bx-pp的签名方法最后锁定为一个叫“generateBizSign”的方法,它接收一个TreeMap类型的参数,内部先对key做字典序排序,再拼上“appKey+timestamp+nonce”,最后调用了native层的签名函数。TreeMap这个细节特别惹眼——服务端为了防重放攻击,必须在里面校验“参数顺序是否正确”。所以逆向人员看到TreeMap要敏感,后面所有关键字段都要按它的排序规则来拼接。

除了签名参数本身,还有一组“回调”需要抓住。很多APP在做接口请求时会带一个callback方法名,服务端会把这个方法名嵌在响应JSON里,客户端拿到后通过反射调用对应方法。bx-pp的请求回调里藏了一个“urlDecode”二次解码逻辑,正是这个逻辑让前面提到的WebView注入成为可能。我把这个回调路径展开后,确认了它在处理某些活动响应时没有做类型校验,导致一个“参数覆盖”漏洞。这类问题不能只看签名算法,否则会漏掉真正的高危漏洞。

这里送你一个实用诀窍:在jadx里搜索“onSuccess”“onFailure”“callback”等关键词,结合方法调用关系图谱(jadx自带Call Graph),很快就能梳理出一条从网络层到业务层的完整链路。比起从入口顺序走,倒着从结果反推通常更快。

4. 动态调试与算法还原

4.1 Frida Hook的整体思路

静态条件下能看到逻辑,但有些细节只有运行起来才能确认,比如某个String变量的实际值、某个Native函数的输入输出。我的做法是写一个综合的Frida脚本,一次性Hook几个目标:签名方法的前后状态、SP存储的读写、以及所有去往native层“PrivateSign”函数的调用。

先看一段我调试时用于枚举签名入参的脚本片段:

Java.perform(function () { var BizManager = Java.use('com.xxx.bx.core.BizManager'); BizManager.generateBizSign.implementation = function (map) { console.log('[generateBizSign] called'); var keyset = map.keySet(); var itr = keyset.iterator(); while (itr.hasNext()) { var key = itr.next(); console.log(' param: ' + key + ' = ' + map.get(key)); } var result = this.generateBizSign(map); console.log(' result: ' + result); return result; }; });

这段脚本的作用不是直接破解签名,而是把输入输出铺到眼前。我在实际跑一次“兑换积分”的活动请求后,拿到了完整的参数顺序和原始签名值。拿着原始签名值到CyberChef里做Base64 decode和Hex转换,可以非常直观地看到它是由hash结果和一段来源数据拼接后整体编码的。这时候我已经能确定它走的不是简单MD5,而是“先规范化拼接、再做HmacSHA256、最后RSA签名”的复合流程。

Frida的Hook点选择是有讲究的。你没必要把类里所有方法都Hook一遍,这会严重拖慢APP运行速度,而且容易被native层的反调试识别出“有调试器在轮询”。正确的思路是“二八原则”:先用静态分析锁定20%的关键方法,再对这些方法做精准Hook,剩下的运行时行为通过日志去凑。bx-pp整体跑一轮模拟请求,我只Hook了不到30个方法,信息量已经非常充足。

4.2 Native层算法识别与还原

Java层拿到签名结果后,另一个问题马上冒出来:签名是在so层做的,Java层只是个壳。Hybrid加密的套路都一样:在native里面用固定密钥做一次摘要,返回给Java层之后再拼上时间戳签名。还原native算法的难度取决于壳对so做了什么。

bx-pp的so文件用IDA加载后,JNI_OnLoad只有少量初始化,真正的业务函数是Java_com_xxx_bx_core_BizManager_privateSign。用IDA的交叉引用找到这个函数后,分析它的参数结构:共四个参数,分别是JNIEnv*、jobject、jstring(拼接后的参数串)和jstring(时间戳)。函数内部先GetStringUTFChars取出参数,然后进入一段被控制流平坦化处理的代码。这段代码里有大量的取表操作和按位异或,但规律性很强:循环变量基本固定,表的大小通常是256字节。

256字节表、移位、异或、循环展开,这几个特征合在一起,基本可以锁定是某种分组加密或者摘要算法的变体。我通过动态插桩打印了该函数的输入和输出长度:输入是120字节左右的字符串,输出是32字节的hex。长度对上了MD5家族的输出。再结合输入里含有时间戳和签名密钥,确定这是一个“私有MD5+固定盐”的摘要逻辑。

还原过程并不需要把每一条汇编都翻译成高级语言。我的做法是先把输入输出配对多组样本,再用Frida的NativeFunction调用该so里的privateSign函数做黑盒验证。当自己写的Python测试脚本用同一份参数调用so得到一致结果时,native层的算法还原就算完成了。这样既省时间,又避免了在控制流平坦化里迷路的痛苦。

4.3 签名复验与协议字段关联

核心逻辑还原出来,最后一步是验证它到底能不能跑通真实协议。我写了一个Python脚本,模拟bx-pp的完整请求流程:构造TreeMap参数、按规则拼接、生成时间戳和nonce、调用privateSign签名、拼接所有字段再整体编码、带着签名发到测试环境的接口。

import hashlib import hmac import time import random import requests def gen_sign(param_map, app_key, ts, nonce): # 模拟native层的私有MD5逻辑:参数排序+固定盐 ordered = '&'.join(f'{k}={param_map[k]}' for k in sorted(param_map.keys())) raw = f'{ordered}&appKey={app_key}&ts={ts}&nonce={nonce}' # 这里用HMAC-SHA256做一次摘要,再模拟RSA加签后的base64编码 digest = hmac.new(SALT.encode(), raw.encode(), hashlib.sha256).digest() return base64.b64encode(digest).decode()

第一次跑这个脚本时,服务端返回“signature mismatch”,排查了半天发现是时间戳的取值精度不一致。APP用的是毫秒级,我在脚本里写成了秒级,差了一个数量级。这种细节在协议还原里最常见,所以我在验证时总会同时抓一次APP原生的真实请求作为对照,把Header和Body字段一一对齐,而不是凭空猜。

对齐之后,测试接口返回了正常响应。这意味着签名算法和参数拼接规则都被完整还原了。从这一步再往前走,就是判断安全性了:签名覆盖了哪些参数、有没有遗漏关键字段、时间戳窗口多长、nonce有没有做重复校验。每一个判断都对应一条防护建议,这才是一次逆向分析真正体现价值的部分。

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

5.1 反调试与加固对抗

很多加固方案不会让你舒舒服服一路逆向到底。bx-pp里至少有三层对抗是我实际撞到的,逐一说下绕法。

第一层是root和模拟器检测。APP启动时会检查Magisk、Superuser.apk以及一堆模拟器特征文件。我的绕法是先在Frida里Hook“java.io.File.exists”方法,对这类敏感路径统一返回false。这里有个小技巧:只拦截“exists”还不够,有些检测用的是access()系统调用,所以还得配合Hook libc的access函数。两层一起拦,基本能过掉大部分检测。

第二层是反Frida。部分so文件会扫描进程maps,查找包含“frida”的注入区域。启动frida-server时如果直接使用默认配置,大概率会撞上。解决办法是编译一个改了名字的frida-server(比如叫“fs88”),再加上“Frida”环境变量脚本去修改运行时字符串。改完名字之后,地图特征就变得和中立so库一样了。

第三层是反调试器附加。它会在native层调用ptrace检测是否被调试。这个最好绕,直接用Frida去Hook ptrace函数,让它返回0即可。但要注意:ptrace检测可能不止一次调用,可以在Hook时打印调用栈,把触发位置找出来,做定点绕过。

5.2 协议参数“对不上”的排查路径

逆向最让人崩溃的不是算法难,而是你明明逻辑都写对了,服务端就是不认。在bx-pp的签名复验阶段,我遇到过一次比较典型的参数错位问题。

现象是:同样的参数,用APP原生环境请求能通过,用我的脚本就提示“illegal request”。排查步骤我按顺序做了一遍:先抓APP真实请求,把它的签名值和我的脚本生成签名值放在一起对比,发现长度一样但内容完全不同。再检查参数拼接规则,发现APP在签名前额外对中文内容做了URL编码,而我的脚本没有。编码方式不同、原始参数字节不同,签名自然对不上。

这类“字段对不上”的问题,80%都是编码、大小写或排序规则不对,而不是加密算法本身的问题。我的排查顺序一般是这样:

  1. 对比签名原始串的Hex值,不要看编译后的字面值。
  2. 确认编码方式:UTF-8、URL编码、Base64是否一致。
  3. 确认时间戳精度和格式:秒vs毫秒、10位vs13位。
  4. 确认TreeMap排序和拼接分隔符(&还是|)。
  5. 最后才怀疑算法本身错了。

记住这个顺序,能帮你省掉至少一半的调试时间。

5.3 常见问题速查表

适合收藏一份常见问题列表,下次做同类项目时直接查:

问题现象可能原因排查手段
dump出来的dex总是解析失败反调试触发,内存中的dex被篡改先绕过反调试,再尝试多点dump,对比哈希
Hook Java方法无效代码已被抽取,真实逻辑在native层换到native层Hook,或先脱壳完整
签名值总是差一个字段拼接时漏了隐藏参数(如版本号)遍历入参,重点查非业务性参数
抓包能看到请求但响应乱码接口做了加密响应体定位解密函数,Hook它的输出
模拟器上APP闪退模拟器检测触发修改模拟器特征,或换真机

这张表不一定覆盖所有情况,但遇到问题时照着查一遍,能让你少走很多弯路。

6. 合规边界与安全加固建议

6.1 合规边界检查清单

写到这里,一定有人会问:你都把签名算法还原出来了,那是不是可以随便刷接口了?这个问题必须正面回答:逆向能力越强,自我约束越要清晰。

对于bx-pp这类涉及权益的业务系统,我给自己定了几条不可逾越的边界:

  • 只允许在测试环境做验证,绝不拿生产环境的用户账号跑批量请求。
  • 不以获取利益为目的,不参与任何“薅羊毛”“刷单”类操作。
  • 发现的漏洞通过正规流程(比如SRC或直接对接安全团队)提交,不私下扩散。
  • 所有脚本、样本、抓包数据在项目结束后清理或归档加密,不长期留存。

如果你在学校、公司或实验室里做类似项目,一定也要拿到明确授权再动手。哪怕是“某电商平台的逆向”这种听起来很酷的项目,只要没有授权,性质就完全不同了。技术本身是中性的,但使用技术的场景决定了它合规与否。

6.2 从攻击视角到防御加固

逆向分析的最大价值,是帮防守方看清自己的防护短板。在完成bx-pp分析后,我根据这次的经验给目标团队提了几条加固建议,这里也分享给你参考。

一是签名算法不要前置固定密钥。bx-pp的密钥存在so层,虽然增加了逆向成本,但拿到so就能提取。更好的做法是引入动态下发+定期轮换机制,降低单次机密泄露的影响面。

二是增加有效的重放防护。目前时间戳和nonce的校验窗口可以缩短,而且nonce需要服务端去重。可以把nonce存储改成基于滑动窗口的缓存,过期和重复的都直接拒绝。

三是把关键的完整性校验从Java层挪到native层。Java层脱壳后相当于裸奔,native层至少能提高攻击成本。同时建议在业务关键路径加入行为检测,比如短时间内高频同参数请求直接触发风控。

四是修复我提到的WebView回调参数覆盖问题。对所有从外部传入的callback类名做白名单校验,不接受任意类名和任意方法名的反射调用。这是这次分析中发现的危害相对较大的一个点,也是最容易修的一个点。

回到执行层面,我个人的实际体会是:逆向分析不是“奇技淫巧”的堆叠,而是对系统信任边界的重新审视。你能打开一把锁,不等于你有权进入房间。把攻防两面都看清楚,才能在真实项目中做出靠谱的判断。

最后再分享一个小技巧:在做个类安卓客户端分析时,别急着上高难度Hook,先把“抓包-脱壳-查入口”这套常规流程走一遍,很多目标根本走不到native层就已经把逻辑暴露光了。bx-pp这次真正难的部分只有native签名,而它前面的无数琐碎定位工作,靠的都是耐心和规范的流程。你手里的样本可能比这个还要复杂,但只要流程对,一步一步来,总能拆到最后一层。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询