
1. 项目背景与目标拆解1.1 这次逆向分析到底在分析什么拿到一台装有咖啡品牌App的安卓设备我的第一反应并不是直接拖进JADX里看代码而是先想清楚一件事这次逆向分析的目标是什么。因为目标不同技术路径会差出十万八千里。单说“咖啡App”市面上这类应用的功能高度集中会员注册与登录、积分商城、门店定位、点单支付、优惠券核销。这类业务型App在技术上通常走的是“标准安卓工程 服务端接口”的路线客户端本身没有太复杂的逻辑真正的核心全部藏在服务端的接口协议里。所以对一个咖啡App做逆向分析最值得投入精力的方向其实是接口层搞明白它的签名参数是怎么生成的、请求头里有哪些字段参与了服务端校验、加密逻辑是在Java层还是SO层。另外一种情况是你想搞清楚它是否安全存储了用户隐私或者它的支付流程是否有漏洞。这个方向就更偏向数据流与权限分析关注的是App读写了哪些本地文件、是否明文保存用户手机号、SharedPreferences里是否存在token硬编码。我在开始前把目标明确为三点第一确认App是否做了加固处理第二还原出核心接口的签名生成逻辑第三把整体通信链路上可能的隐患梳理出来。这个定位既符合安全研究的技术价值也足够落地不至于陷入无休止的加解密迷宫里。1.2 为什么选择咖啡App作为分析对象道理很简单咖啡App属于中低频使用的工具类应用不像社交或电商类App那样拥有超高强度的风控体系。它的逆向分析难度适中非常适合梳理一套完整的分析流程。你可以在这个项目里练到APK静态分析、抓包、脱壳、动态调试、算法还原这些核心技能又不会被顶级商业加固方案摁在地上摩擦。更关键的是咖啡App的接口签名逻辑五花八门。有些团队直接用MD5把参数拼接后加盐做签名有些则引入AES-GCM做整体加密还有的在HTTPS之上自定义一套协议层。不同实现方式的破解思路完全不同练一遍基本上等于把这些主流套路都过了一遍。对我自己来说这也是一个很好的素材库以后遇到同类业务型App时能第一时间判断出它的技术栈大概是什么水平。另一个现实原因在于咖啡App的业务链路足够长。从一个陌生设备登录到完成一笔下单中间至少经过十几个接口每个接口都可能附带不同的签名字段和加密策略。这条路完整走一遍之后你对整个移动端通信协议设计方法的理解会提升一个档次而不只是停留在“会用frida hook函数”这个层面。1.3 法律边界与合规底线逆向分析本身是中性技术但用在别人生产的商业App上必须严格限定在安全研究、学习交流的范畴内。我在整个分析过程中只对接口协议的技术特性做验证全程不涉及破解会员权益、绕过支付、盗取用户数据等任何违规行为。有一点我想特别强调就算你把签名算法完全还原出来了也不要去尝试恶意调用服务端接口更不要批量抓取用户数据。别让技术热情把自己带进灰色地带。如果你所在的公司需要对竞品做技术调研也一定要走正式法务流程确认授权范围后再动手。这篇文章里展示的所有技术和思路都应当只用于你拥有合法分析权限的App或者用来加固你自己开发的App。把逆向当成理解攻防逻辑的手段而不是牟利的工具这既是对行业负责也是对自己负责。2. 环境准备与信息收集2.1 APK体量与基础信息检查我拿到目标App后做的第一件事是先用aapt检查APK的基本信息包括包名、版本号、权限声明和targetSdkVersion。这一步看起来不起眼但信息量非常大。aapt dump badging coffee.apk重点看三列package、sdkVersion、targetSdkVersion。如果targetSdkVersion低于26说明App没有完全适配分区存储后续动态调试时路径选择的自由度会更高如果权限列表里出现了READ_PRIVILEGED_PHONE_STATE或QUERY_ALL_PACKAGES这种敏感权限就要单独标记出来后面动态分析时优先追踪。紧接着用APK查壳类的工具过一遍确认它是否有加固痕迹。常用的是pkid或者直接用jadx打开后观察是否存在明确的入口壳类。如果App用了360加固、腾讯乐固、爱加密这类方案在Manifest里的Application类通常会被替换成壳的载体类比如com.stub.StubApp或者com.tencent.StubShell.TxAppEntry。看到这些类名的时候基本可以断定这是一个加固过的App后续不能用普通的静态分析直接看业务代码。我当时查看的结果是这个咖啡App使用了爱加密的免费版加固。免费版意味着脱壳难度相对较低因为部分加固函数仍然保留在Java层未全部下沉到SO中。这个信息直接决定了下一步的技术选择。2.2 工具链选型与搭配逻辑很多人一开始就纠结用JADX还是GDA用Charles还是Burp Suite其实工具选型没有绝对正确答案关键取决于目标App的加固状态和通信协议强度。我在这个项目里采用的是“基础三件套加动态补充”的组合。静态分析使用JADX结合GDA。JADX的优点是反编译速度快、代码结构清晰对普通APK的还原度非常高但当遇到混淆代码时JADX处理嵌套泛型和重载方法的可读性会比较差这时候GDA的手动修复能力和交叉引用功能反而更好用。两者的定位不是二选一而是配合使用。抓包工具我这里选择的是Charles配合Fiddler双开。Charles的HTTPS解析界面更直观断点修改请求方便Fiddler的Composer模式和脚本自动化更顺手。实测下来这个咖啡App没有做代理检测Charles可以稳定抓到全部请求不需要额外处理绕过反代理的逻辑。动态分析工具是frida和Xposed的组合。frida用来动态hook、实时查看函数调用栈和返回值速度快、脚本灵活Xposed具体用LSPosed更适合做持久化模块。比如某次需要持续篡改签名结果观察服务端行为用Xposed模块写一个hook脚本挂在App上比每次启动都重新加载frida脚本要稳定得多。2.3 签名校验与重打包预判在正式反编译之前还需要判断一下这个App有没有做签名校验这关系到后续脱壳和重打包是否可行。最直接的办法是拿原APK做一次签名然后换一个签名再安装看App是否闪退或出现异常提示。# 删除原签名 keytool -printcert -jarfile coffee.apk # 使用新的keystore重签名 apksigner sign --ks my.keystore --out coffee_re.apk coffee.apk如果重打包后的App运行正常就说明它没有做签名校验反向于脱壳后的重打包分析要省很多力气。如果直接闪退大概率是用PackageManager的getPackageInfo对比了签名信息那就需要在脱壳后动态hook掉这个校验方法。我实测的咖啡App没有这层防护省掉了一个麻烦步骤。但值得注意的是没有签名校验不代表没有其他完整性校验后面脱壳后看到校验逻辑时再做处理。3. 抓包与请求链路分析3.1 代理配置与证书信任问题处理给安卓App配代理这件事看起来简单实际上暗坑很多。第一步是让手机与电脑处在同一局域网然后修改WiFi代理指向电脑IP与端口。这个方法对大多数App有效但有个前提App没有禁用系统代理。如果遇到那种不走系统代理的App就要用iptables配合redsocks把流量强制转发到代理端口。Charles配置好之后屏幕上会出现大量https乱码这是因为安卓App默认不信任用户安装的CA证书。解决方式有两种一种是直接把Charles的证书安装到系统证书目录另一种是把App的networkSecurityConfig拉出来看检查它是否只信任系统证书。如果networkSecurityConfig配置了trust-anchors只包含系统证书你需要将证书文件命名为hash.0放入/system/etc/security/cacerts/目录。这要求设备已经有root权限。我当时用的是一台Pixel模拟器直接adb root后push进去重启生效。还有个小细节每次配置完证书后要确认Charles的SSL Proxying设置里已勾选了目标域名。否则即使证书装好了Charles也不会主动解密HTTPS流量只会显示一堆隧道连接。3.2 拦到请求后的第一轮分析等到咖啡App的请求开始出现在Charles面板上时第一轮分析的核心不是去读每个接口的语义而是先给全部请求做一次“体检”。我会关注三个层面接口路径、请求头字段、POST请求的body体特征。这个咖啡App的登录接口是/api/v1/user/loginPOST请求的body里除了账号密码还有一个显眼的长字符串sign目测由32位十六进制字符组成疑似MD5。请求头里则有固定的nonce字段每次请求都不同且带有一个timestamp。这几乎是业务型App的通用套路时间戳防重放 随机数混淆 参数签名防篡改。服务端校验的核心就是这三个字段的组合。假如签名算法是简单地把所有参数按字典序拼接后做MD5那破解难度会非常低但如果引入AES或者RSA就要另想办法。我先把登录接口的请求整体复制下来包括请求头、请求体、时间戳和nonce然后用一个python脚本把不同时间点、不同nonce下的请求做对比找出哪些字段会变、哪些字段固定不变。这一步能快速缩小签名算法的参与范围后期动态hook时也能有的放矢。3.3 未加固部分的福利网络层仍有Java代码可读这个咖啡App虽然用了爱加密的加固但我发现一个有趣的细节它的网络层代码并没有全部下沉到SO库中。像okhttp3.Interceptor、Retrofit的Service接口这些都还保留在Java层只是被R8混淆过。这意味着即使不脱壳直接拿JADX打开加固壳也有可能看到业务代码的调用关系。因为加固方案有时只会加密真正的Application入口和部分核心类剩余的资源文件和动态代理生成的代码仍然是可读的。这种情况在免费版加固方案里非常常见爱加密的免费版并不会把所有类都加密只保护特定的ContentProvider和ShellApplication。顺着这个线索我在JADX里直接搜索了sign相关的gradle混淆映射虽然看不到完整的方法名但能搜到大量a.b.c风格的混淆类。再配合一些交叉引用很快定位到网络拦截器所在的类。节省了不少先脱壳再分析的精力。3.4 代理抓包失败的两种典型场景抓包过程中最容易遇到两类问题很多人会误判为反爬策略其实是自己配置不对。第一种现象App在开启代理后直接无法联网。检查之后发现这个咖啡App虽然没有禁用系统代理但在某些模块里调用了NetworkCapabilities来判断网络状态如果代理改变了网络体验就会被判定为“当前网络不可用”直接拒绝发起请求。解决思路是对该系统调用的返回值进行hook把它改成“移动网络在线”即可。第二种现象代理能连上但请求全是CONNECT隧道无法解密内容。查了一圈发现是Android 7.0以上版本的networkSecurityConfig默认不信任用户证书。如果不想折腾系统证书其实还可以用frida直接hookSSLContext相关的trustManager方法把证书校验逻辑强制放行。这两种问题在后续逆向中大概率还会遇到提前把底层原理摸透后面分析效率才能提上来。4. 脱壳与代码还原实操4.1 对爱加密免费版脱壳的整体思路对于有着“爱加密”加固特征的APK有一个常见的误区拿一个脱壳工具就开始跑等跑完发现dump出来的dex文件乱七八糟代码里全是垃圾指令。这不是工具问题而是对加固方案缺少基本了解。爱加密免费版的执行流程大致是这样壳的Application入口先被系统加载这个入口会负责解密真正的业务dex文件然后动态加载进当前进程。所以脱壳的核心思路就是在壳解密完真实dex、但还没把它扔进内存运行前把内存中的dex镜像完整保存下来。实现上一般有两种路径一种是修改系统源码来dump另一种是使用frida直接attach找到DexClassLoader的加载点。我用的方案是frida的dex_dump脚本配合DexClassLoader的loadClass断点来定位dex文件在内存中的地址。这个方案虽然简单但对免费版的爱加密已经足够了。唯一需要注意的是运行时会持续产生大量dex镜像有些是垃圾数据需要根据文件头dex\n035来过滤。4.2 脱壳后的代码清洗与整体分析脱壳完成后会得到一堆dex文件不能直接拿过来就当成最终源码。要用frida-dexdump自带的合并工具先把多个dex合并然后导入JADX进行一次整体反编译。如果反编译后的代码里仍然出现大量无法解析的类说明加固的类还没有被完整dump下来需要重新调整hook时机挂到ClassLoader.loadClass触发前的一瞬间。整个咖啡App在脱壳后的代码量大约在几百个类不算大。静态分析时我主要做两件事第一找入口Activity和Application的onCreate逻辑确认脱壳是否完整第二把网络层相关的package标记出来逐个看它的签名生成代码是否存在。我在JADX里搜索关键词MessageDigest、SecretKeySpec、Mac以及sign很快就锁定了三处疑似签名生成代码。一处是用MD5做参数签名一处是用HmacSHA256做请求头签名还有一处是AES加密用于敏感字段传输。一个咖啡App用三层加密老实说有点防御过剩但对逆向分析来说反而更有料了。4.3 脱壳后重打包与动态测试脱壳的一个直接收益是你可以在不修改官方App的前提下把脱壳后的代码重新打包、换签名、装上设备进行后续动态调试。这样最大的好处是调试时可以看到所有的logcat输出还能直接以root权限访问App的私有目录。重打包的时候要注意脱壳后的dex如果数量多得用smali/baksmali重新组装。我先用apktool d把脱壳后的APK解包替换其中的dex文件再重新构建和签名。但apktool可能无法自动处理爱加密壳本身的校验所以重新打包后要先确认壳入口是否被正确移除或者替换为空壳入口否则App可能启动后直接crash。我用一个简单的方法绕过这个问题把脱壳后的dex放进去后同时把原有壳的StubApp类替换成一个自定义的空壳类入口只调用原始的attachBaseContext不带任何解密逻辑。这样App启动时可以直接加载真实的业务dex。重打包后测试了几轮基本功能都能跑通说明脱壳和重打包成功。此时已经把App从“加固模式”解锁为“可调试模式”接下来动态分析会顺畅很多。5. 签名算法还原与动态调试5.1 从静态代码中定位sign生成逻辑脱壳后的代码中搜索到的MessageDigest一般会有多处分发原代码可能是经过混淆的但逻辑链路仍然清晰。最终的签名生成大概率集中在某个工具类里它先读取请求参数将参数按照特定规则排序再拼上固定的盐值最后计算MD5。我在JADX里定位到一个静态方法其代码大致如下public static String getSign(String path, String body, String nonce, long timestamp) { String raw path body nonce timestamp SALT; return md5(raw); }再看SALT的定义发现它不是硬编码在代码里而是通过BuildConfig读取的。这就有点意思了——说明签名算法的盐值来源于构建配置不同渠道包的盐值可能不同。如果更新版本时盐值变化旧签名就会全部失效这种设计明显是为了防止签名算法被逆向后直接复用。我需要进一步追踪BuildConfig里的SALT值。好消息是R8混淆不会破坏BuildConfig字段直接搜索这个字段即可拿到字符串明文。但也有可能字符串被拆分拼接这种情况下只能通过动态hook来获取完整的盐值。5.2 动态hook签名函数来验证猜想静态分析只能得到“代码长什么样”要想确认它真的在运行时被调用动态hook是必经之路。我用frida写了一段脚本hook住签名方法打印入参和返回值Java.perform(function() { var clz Java.use(com.coffee.app.util.SignUtil); clz.getSign.implementation function(a, b, c, d) { var ret this.getSign(a, b, c, d); console.log(Path a); console.log(Body b); console.log(Nonce c); console.log(Timestamp d); console.log(Sign ret); return ret; }; });启动App随便触发一个登录请求frida的输出面板立刻打印出了完整的参数组合和签名结果。和我静态分析拿到的那串代码完全对得上。这一步的验证非常关键否则很可能你静态分析出来的代码在运行时根本不会被调用那就是白忙一场。这个咖啡App的签名逻辑验证结果所有参与签名的参数按固定顺序拼接加上固定盐值后整体MD5。虽然拼装顺序没有特别反人类但盐值的存在使得外部无法直接伪造请求。5.3 遇到反调试与完整性校验后调整策略动态hook过程中我还遇到过一次反调试。具体表现为用frida attach模式启动时App直接弹“检测到调试环境”并闪退。排查后确认App在启动阶段的Application.attachBaseContext里检测了TracerPid和Debug.isDebuggerConnected。处理办法是给App套一个frida-server的隐型启动模式或者直接用early instrumentation模式在attachBaseContext执行前注入代码。我用的是frida的-f参数配合--runtimev8模式在App启动的极早期阶段就绕过检测。关键是要在壳的attachBaseContext方法执行前完成hook时机非常重要。另外一个典型坑是脱壳重打包后App运行时会对自身dex文件的哈希值做校验一旦发现被修改就会触发自毁。解决思路是hook住校验方法将返回结果强行置为合法值。动手之前先确认这个校验是在Java层还是SO层如果在SO层处理难度要大得多。5.4 完整还原sign算法的最后一公里拿到完整的参数组合、盐值和加密方式之后最后一步是把验证过的逻辑迁移到本地脚本中实现离线签名。import hashlib, time, random, string def generate_sign(path, body, nonce, timestamp): SALT coffee_secret_salt_2023 raw path body nonce str(timestamp) SALT return hashlib.md5(raw.encode()).hexdigest() nonce .join(random.choices(string.ascii_letters string.digits, k16)) timestamp int(time.time()) path /api/v1/user/login body {account:test,password:123456} sign generate_sign(path, body, nonce, timestamp) print(sign)运行脚本生成一串签名然后和Charles里抓到的真实签名对比格式一致、结果可控。这已经足够证明签名算法被完全还原了。不过这只是一个起点。服务端通常还会校验nonce的时效性和唯一性意味着就算你能算出合法签名也不能无限重放。真正的安全边界要看服务端那边的处理逻辑而不只是客户端签名有没有被破解。6. 常见问题与防坑指南6.1 抓包时Charles显示乱码的排查流程这个问题我在分析过程中至少遇到过三次。现象是Charles能截获HTTPS请求但Response内容是一堆看不懂的字符。排查顺序是第一确认手机上是否已正确安装Charles证书第二确认Charles的SSL Proxying设置里勾选了目标域名第三检查App是否设置了networkSecurityConfig只信任系统证书第四确认系统时间和证书时间是否匹配。如果以上都没问题还有可能是不小心抓到了HTTP/2的流量Charles在某些版本下对HTTP/2解密的兼容性不佳。可以在Proxy Settings里关闭HTTP/2支持再试。排查抓包问题时先记录当前App版本、安卓版本、代理方式和证书安装位置这四个参数直接决定抓包方案的可行性。磨刀不误砍柴工及时把这些信息记录下来后面的动态调试效率会高很多。6.2 脱壳产物是空文件或者不完整时怎么处理脱壳反编译出的dex文件大小很小用JADX打开全是垃圾类这表明dump时机不对。以爱加密的机制来说真实dex的加载是一个延时操作有时候会等到某个Activity启动后才加载出来而不是Application刚创建就完成。解决方案是在frida脚本里主动轮询进程的ClassLoader找到加载了目标包名类的ClassLoader后再执行dex dump。把dump的触发点从“时间驱动”变成“事件驱动”成功率会高非常多。6.3 动态hook时如何避免日志刷屏与干扰咖啡App里埋了很多日志点hook所有方法时会发现控制台输出几千行日志真正的签名信息反而被淹没。优化方法是细分入口只hook特定类或特定方法并在脚本里加入filter只打印非空返回值和非空参数的那几条记录。另外一个更优雅的方案是hook日志类比如android.util.Log或App自定义的LogUtils在输出层拦截并渲染重点信息。避免刷屏之后Frida的稳定性和控制台可读性都会提升不少。6.4 加固App的Application入口被篡改后如何定位真实入口脱壳后重打包时如果把壳的Stub入口换掉需要确认业务代码真正以哪个Application作为入口。很多加固App会把业务Application通过ContentProvider机制加载所以反编译后要留意Manifest里注册的ContentProvider以及它的onCreate方法。我在这个咖啡App里发现它的真实Application入口被转移到了原始的CoffeeApp类中但在Manifest的android:name里注册的却是壳的StubApp。所以查找真实入口时不能只看Manifest还要在脱壳后的代码里搜索attachBaseContext和onCreate的完整实现再确认类的依赖关系。6.5 常见问题速查表一句话版本现象可能原因排查手法抓包全是CONNECT隧道证书未安装或系统证书未信任把证书装入系统证书目录动态hook无输出加固App尚未完整加载业务dex切换为early instrumentation脱壳dex为空或太小dump时机不对改用ClassLoader事件驱动重打包后闪退壳校验或签名不一致hook完整性校验或保留原壳接口签名始终不一致参数拼接顺序或盐值不对动态hook打印完整入参这几种场景在逆向工程里几乎是必然遇到的把排查顺序记在脑子里至少能帮你省掉两个小时的试错时间。7. 本次分析的总结与心得整个咖啡App逆向分析走完最大的感受是移动端安全的攻防重点从来不在客户端本身而在于服务端是否信任了不该信任的客户端。一个可以自由脱壳、重打包、篡改请求的App如果服务端没有配套的风控策略那么无论客户端做多少层加密都是形同虚设。在分析过程中我对爱加密免费版加固的理解也更深了一层。免费版保住的只是静态层面的代码可见性一旦进入动态层detached进程、内存dump、frida hook等手段有太多孔隙可以利用。商业App在安全设计上如果想真正提升门槛至少要把签名校验下沉到SO层并引入动态防护机制否则只做静态加固其实挡不住有决心的人。这次项目也验证了一个思路对称加密和哈希算法本身没有绝对安全安全取决于密钥管理和服务端验证策略。这个咖啡App的盐值放在BuildConfig里虽然不算裸奔但对一个资深逆向者来说基本上属于半公开的信息。密钥如果在服务端下发或者每次会话动态生成破解难度会高两个级别。如果你也想拿一款App做逆向练手我的建议是不要从社交类、金融类这种风控做到极致的App开始找一款中低频使用的业务型App按这篇文章的思路走一遍你会对整个移动安全技术栈有一个非常扎实的理解。等这条路走顺了再往SO层逆向、unidbg模拟执行这些更深的坑里跳也不迟。