尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

从抓包到So层逆向:Android协议签名校验破解全流程实战

从抓包到So层逆向:Android协议签名校验破解全流程实战 上周我接到一个挺典型的任务内部代号TK的Android客户端某个列表接口突然对签名校验变得极其严格连我们自己的测试环境都调不通。抓包工具一连上RequestBody是一坨看不出规则的二进制几个关键Header也不是明文服务端一直报签名错误。这个现象说明问题已经不在常规的HTTP层而是App在发出请求前把参数、时间戳、设备信息等一系列内容送进了native层做了处理。想把这套协议理清楚绕不开So层逆向。这篇文章想把这一整套流程从头到尾讲清楚怎么抓包、怎么从网络包反推加密入口、怎么定位到so文件、怎么在静态和动态两个维度上把关键函数拆明白以及这一路上会遇到哪些奇葩问题。适合三类人一是做端上安全自查的客户端开发二是刚上手移动端逆向的分析师三是对App协议感兴趣、想搞懂加密原理的测试同学。还是那句话安全研究要做在自己能负责的范围内以下所有案例用的都是脱敏后的内部测试样本。1. 抓包到最后为啥会演变成逆向So层一个真实排查场景1.1 症状很明显抓包只能看到一串看不懂的密文先还原一下当时的现场。TK这个App平时所有接口都是正常的HTTPS明文Charles可以很顺畅地看到JSON数据。但从某个版本开始列表接口的响应体变成了乱码请求体也出现了一段类似tksign2f5c9d1a...的32位hex串而且每次都不同。我最初以为是服务端改了协议直接把抓到的包复制出来重放结果全部被拒。这时候才意识到App在发包之前已经在native层对请求内容做了处理。这里有个容易混淆的点抓包工具能解开HTTPS不等于能解开业务数据。证书校验那层是“传输安全”native层加密是“数据安全”两层是独立的事。如果App只是普通HTTPS你装了证书就能看到明文一旦请求体是密文说明业务层自己加了密。这个判断标准很关键因为它决定了后面你是继续分析协议格式还是直接进入代码逆向。1.2 协议分析真正要回答的三个问题协议分析听起来很玄落到实际就三件事参数是怎么来的、密钥藏在哪、签名是怎么算的。参数是怎么来的每个请求携带的timestamp、随机数、设备编号从哪里生成是否有固定顺序。密钥藏在哪是Java层硬编码字符串还是写进了so文件或者是通过某种算法动态派生。签名是怎么算的用MD5、HMAC、RSA还是自研算法盐是什么拼接顺序是什么。这三点任何一点落在native层就必须打开so文件去逆向。以TK这个案例来说前两步很快定位到Java层但第三步死活找不到算法入口最后发现签名逻辑被放到了libtk_core.so里面。这就是“抓包协议分析”和“So层逆向”的连接点抓包负责告诉你结果逆向负责告诉你结果是怎么来的。1.3 这些信号出现基本就要往So层走了并不是所有抓包难题都需要逆向So层但下面几个信号一旦出现基本跑不掉反编译Java代码后发现关键类里全是System.loadLibrary和native方法业务逻辑被大量转移到JNI。请求体或者响应体不是标准JSON、Protobuf而是自定义二进制格式且Java层找不到对应的编码逻辑。服务端对请求做强签名校验而且对缺少某个Header的请求直接返回401或者200但数据为空。证书固定的校验逻辑不在TrustManager里而是在native层直接对比证书二进制。反过来如果App只是纯Java层加密那用jadx反编译就能搞定不需要碰So层如果服务端本身开放了调试开关那更可以直接跳过。判断“要不要逆向So层”的标准很简单Java层看不到关键逻辑或者看到的逻辑是空壳那就去so里找。2. 搭一套能稳定复现的抓包环境证书、流量中继、SSL Pinning2.1 工具分工Charles、mitmproxy、Wireshark各管一段移动端抓包的工具其实不少但每类的定位不同我自己的分工是这样工具适用层面主要用途注意点CharlesHTTP/HTTPS日常看请求、改包重放、观察Header配置灵活但自动化能力弱mitmproxyHTTP/HTTPS跑Python脚本批量改流量、录制回放命令行门槛需要会写脚本WiresharkTCP/IP层看底层连接、分析非HTTP流量看不了HTTPS明文需要先导出密钥这个案例里我主要用Charles因为TK的列表接口是标准HTTPSCharles能快速看到Host、路径、Header方便第一时间判断是协议问题还是证书问题。等需要自动化重放和批量验证签名的时候再切到mitmproxy写脚本。Wireshark只用在一种场景App完全不走HTTP监听端口、直接建立裸TCP连接的时候它会帮你发现“数据压根没进入预期通道”。2.2 手机流量进入抓包机从安装证书到系统信任手机端抓包的第一步是把目标手机的流量引到电脑上这个动作在很多文章里被一笔带过但实际踩坑无数。具体操作是先让手机和电脑连同一个WiFi电脑上启动Charles后开启SSL监听端口默认一般是8888。然后在手机WiFi设置里把HTTP监听服务器指向电脑的IP和端口。注意这里要指定电脑在局域网内的实际IP别用127.0.0.1。配置成功后抓包工具里会立刻出现来自手机的新连接。流量通了以后手机会弹窗要求安装CA证书。Android端直接下载安装即可但这里有个大坑Android 7.0以上默认不信任用户CA证书只能信任系统证书。如果你用的是高版本Android测试机装完证书后发现HTTPS包还是显示unknown十有八九是证书没有被系统信任。解决办法是把证书转成系统证书步骤大概是# 1. 用openssl计算哈希值作为文件名 openssl x509 -inform PEM -subject_hash_old -in charles.pem | head -1 # 2. 假设输出是 8d8f4f0e重命名证书文件 mv charles.pem 8d8f4f0e.0 # 3. 推送到系统证书目录并设置权限 adb root adb remount adb push 8d8f4f0e.0 /system/etc/security/cacerts/ adb shell chmod 644 /system/etc/security/cacerts/8d8f4f0e.0做完这一步去手机设置里的“信任的凭据”看能不能找到Charles的证书能找到再回去抓包。这里最容易忽略的是部分深度定制的系统可能禁止直接写入系统分区这时候可以用Magisk模块的方式导入或者干脆换一台能解锁的测试机别在系统权限上耗太久。2.3 SSL Pinning怎么治objection和frida脚本起步证书装好、流量也进来了但App如果做了SSL Pinning你会发现HTTPS握手直接被客户端掐断抓包工具里报连接重置手机端弹SSLHandshakeException。所谓SSL Pinning就是App把服务器证书或者公钥指纹内嵌到客户端里只有内嵌的证书才能通过校验抓包工具用的那个证书不在白名单里自然连不上。对付这种情况最快速的办法是用objection关掉证书锁定objection -g com.tk.example explore android sslpinning disableobjection对常见的OkHttp、WebView场景很管用但它本质上是hook住Java层的信任逻辑。如果App把证书校验放在了native层objection就会失效。TK这个案例就是这样Java层关闭证书锁定后请求还是失败说明校验逻辑已经下沉到so文件里了。这时候才真正进入So层逆向的地界要么去分析native层证书校验函数要么去分析整个协议生成流程。从“抓包”到“逆向So层”的转折点就在这里不是你想不想的问题而是常规手段已经全部失效。3. 从抓到的网络包反推代码入口定位协议处理逻辑3.1 用jadx反编译先把Java层调用链画出来逆向的第一步不是直接打开IDA看汇编而是先回到Java层把入口找到。我用jadx打开TK的APK然后按关键字搜索sign、encrypt、secret、token、native这些词很快定位到一个叫NativeSign的类public class NativeSign { static { System.loadLibrary(tk_core); } public static native String sign(String data, long timestamp); }到这里就很明确了native方法叫sign参数是业务字符串和时间戳返回一个String。下一步要找出谁调用了它于是在jadx里搜索NativeSign.sign顺着调用链看到了一个统一的请求构建器所有请求发出去之前都会先调用这个方法把结果塞进Headertksign。这是最理想的定位流程先反编译Java层把调用链画出来确认native方法的位置和类型再进入so层找具体实现。如果Java层找不到说明调用是动态反射或者方法被混淆了那就需要借助Frida运行时枚举Java方法看看哪些类在调用native方法。总之Java层的调用链是整个逆向的地图地图没画清楚就钻进去看汇编大概率会迷路。3.2 定位so文件与JNI函数入口静态导出符号要会看找到native方法后开始解剖so文件。先看APK里面的lib目录一般有arm64-v8a、armeabi-v7a等多个目录优先看arm64-v8a现代设备基本都是64位。把libtk_core.so拖出来后第一件事不是直接上反编译器而是先看符号表readelf -s libtk_core.so | grep -i sign nm -D libtk_core.so | grep Java_如果so里导出的是Java_com_tk_example_NativeSign_sign这种名字说明JNI函数是静态绑定直接按函数名定位就行。还有一种情况是动态注册Java层方法名叫sign但so里的导出符号里面压根找不到Java_com_tk_example_NativeSign_sign这种情况就需要去反汇编JNI_OnLoad函数在代码里找到RegisterNatives调用第三个参数就是JNINativeMethod数组里面会写明Java方法名、签名和对应的native函数指针。TK这个so是比较常规的动态注册所以打开JNI_OnLoad后很快看到了一个gMethods数组里面有sign、getDeviceId、getTkToken三个函数。这里有个习惯很重要不要只看你想看的那一个函数最好把整个gMethods数组都列出来。很多时候算法是组合的单个函数看不出全貌比如sign算法内部可能会去调用getDeviceId你只分析sign一个函数就会卡住。3.3 静态逆向工具选型与基本分析顺序so文件的反编译工具我主要用三样IDA、Ghidra、readelf。三者分工不同IDA交互式反汇编体验好F5插件一键生成伪代码查交叉引用特别方便。缺点是收费但能力确实强。Ghidra完全免费开源反编译质量在大多数场景下已经足够支持直接分析ELF文件界面稍显笨重但功能不输IDA。readelf/nm/objdump命令行三件套用来快速看符号、依赖、段信息属于进门前必做的检查。我的分析顺序很少变先用readelf看文件属性确认架构、动态符号、依赖库再用Ghidra载入找到JNI_OnLoad或者目标导出函数用反编译视图把伪代码拉出来最后在伪代码里搜索字符串常量像密钥、盐值这些东西通常是字符串形式存在的。整个过程中交叉引用是最要紧的看到一个可疑函数按一下XRef看看谁调用了它就能顺藤摸瓜找到算法入口。4. 把一个So层签名函数完整啃下来静态、动态、模拟三件套4.1 实战从一段32位hex签名还原出HMAC-SHA256现在进入TK案例的核心部分。前面已经知道sign是一个native函数参数是业务字符串data和时间戳timestamp返回32位hex字符串。用Ghidra反汇编后伪代码里出现了几个关键特征调用了HMAC_CTX_new、HMAC_Init_ex、HMAC_Update、HMAC_Final这一组OpenSSL函数。HMAC_Init_ex的第三个参数传入了一个固定字符串比如a1b2c3d4e5。最后有一段循环把unsigned char数组逐个格式化成%02x十六进制字符串。到这基本可以断定签名算法是HMAC-SHA256(data timestamp, 固定盐值)再取十六进制编码后的前32位作为最终结果。至于为什么是前32位大概率是服务端为了缩短Header长度人为截断。验证方式也很简单直接用OpenSSL命令本地重算一遍echo -n business_data1699999999 | openssl dgst -sha256 -hmac a1b2c3d4e5 -hex拿这个结果跟抓包工具里看到的tksign做对比如果一致说明算法还原没有任何问题。这个案例里第一次比对就完全匹配整个链路走通。这里有个经验优先怀疑HMAC和哈希算法因为在签名场景里它们极其常见而且OpenSSL的调用特征非常明显比自研加密算法好认得多。4.2 用Frida动态插桩把运行时证据补回来静态分析能还原逻辑但有些变量的实际值在反编译后看不出来需要动态验证。TK这个案例里盐值虽然能从静态代码里翻出来但我想确认运行时传入的data到底拼接了什么内容于是用Frida动态hook了一把。先部署frida-server到测试设备上Windows下就是先跑adb push frida-server /data/local/tmp/然后执行adb shell chmod 755 /data/local/tmp/frida-server再启动。启动后用Frida脚本hook Java层方法把参数和返回值全部打印出来Java.perform(function () { var clazz Java.use(com.tk.example.NativeSign); clazz.sign.implementation function (data, timestamp) { var result this.sign(data, timestamp); console.log(sign called: data data , ts timestamp result); return result; }; });跑起来之后控制台里能看到每一次请求的入参和返回值配合抓包工具抓到的Header就能确认这确实是协议里用的那个签名。如果静态分析跟动态结果对不上优先怀疑静态分析漏了某些数据变换比如传入的data并不是直接拼接而是先做了Base64或者URL编码。再进一步如果native函数特别复杂可以直接hook so层的函数入口。先拿到so的加载基址再根据Ghidra里的函数偏移地址用Interceptor.attach下断点读取参数寄存器和返回值。这种方式比Java层hook更底层能拿到native真正处理过的内容。4.3 没有真机也能跑Unidbg模拟执行流程有些情况下测试设备并不在手边或者设备被反调试检测盯得很紧这时候我建议用Unidbg在电脑上模拟执行so文件。Unidbg是一个Java框架它可以在JVM里模拟Android的so运行环境不需要真机、不需要root直接加载so文件并调用里面的函数。用Unidbg跑TK的libtk_core.so大致流程是这样的public class TkSignTest { public static void main(String[] args) { AndroidEmulator emulator AndroidEmulatorBuilder.for64Bit().build(); Memory memory emulator.getMemory(); memory.setLibraryResolver(new AndroidResolver(23)); VM vm emulator.createDalvikVM(); DalvikModule dm vm.loadLibrary(tk_core, false); dm.callJNI_OnLoad(emulator); DvmClass NativeSign vm.resolveClass(com/tk/example/NativeSign); DvmObject? signResult NativeSign.callStaticJniMethodObject(emulator, sign(Ljava/lang/String;J)Ljava/lang/String;, new StringObject(vm, business_data), 1699999999L); System.out.println(sign result: signResult.getValue()); } }这个方案适合在没有真机时快速验证算法也适合后续做大批量参数测试。不过Unidbg对so的依赖很敏感如果so里依赖了其他库要在加载的时候把依赖so一并配置好。常见报错是找不到liblog.so或者libc.so这时可以用Unidbg自带的虚拟库或者自己实现对应的函数。TK这个so依赖比较少加载还算顺利但我也遇到过依赖十几个系统库的so那就要做好跟Unidbg源码打交道的心理准备。4.4 遇到加壳、混淆、反调试时的处理思路如果TK这种so是加了壳的前面说的静态分析会因为代码被抽取而基本失效。判断方法很简单用Ghidra打开so发现.text段很小导出函数指向的区域是空数据或者汇编特别短很可能就是抽取壳。对付壳的思路是“先跑起来再抓内存”。用Frida的DexDump或者SoDump脚本在进程运行起来后从内存里把真实的so文件dump出来然后再拿dump出来的文件去做静态分析。对反调试通常的做法是先用Frida列出进程里所有加载的模块看有没有frida-agent被检测如果被检测到就把frida-server换一个端口运行或者用frida-gadget集成到APK里再启动。遇到混淆指令时我的建议是不要逐条抠汇编而是先看JNI函数的调用边界从函数入口到函数返回中间调用了哪些其他函数重点关注加密库、字符串处理、内存拷贝这些特征明显的函数。很多时候你根本不需要看懂每一行汇编只要确认函数内部出现了HMAC、SHA或者AES的调用序列算法轮廓就已经出来了。5. 高频问题排查速查表直接照表操作5.1 抓包环节最容易栽的五个坑抓包阶段的问题最烦人因为很多时候你以为自己配置对了结果白忙半天。我把高频问题整理成一个速查表症状可能原因排查与处理抓包工具里看不到目标App任何请求App没走系统HTTP监听直接用Socket建连用Wireshark抓网卡流量看是不是TCP直连手机装了证书仍提示unknownAndroid 7不信任用户证书把证书放进系统证书目录或者用Magisk模块导入证书已经系统信任HTTPS还是握手失败App做了SSL Pinning且校验不在Java层先用objection关掉Java层校验再看是否还需要逆向native能连接但返回的报文是乱码/二进制业务层做了加密不是传输层问题放弃直接读包转而去分析native层加密函数同一个包重放总被拒签名带时间戳或随机数重放无效继续分析签名的生成规则而不是试图绕过校验抓包阶段最重要的一件事是先把“传输层问题”和“业务层问题”分开。传输层问题包括证书信任、SSL Pinning这类问题通常有几个固定的解决方案业务层问题则是App自己加密的数据抓包工具帮不了太多。5.2 逆向环节最容易栽的四个坑逆向So层过程里的坑更多而且每个坑都可能导致前功尽弃。同样整理成表症状可能原因排查与处理Frida attach后进程立刻闪退目标App有反调试检测换端口运行frida-server或者用gadget模式注入Hook某个符号时找不到函数地址so被strip过或者做了动态注册去JNI_OnLoad里找RegisterNatives的注册表数组静态分析看见一堆空代码段so被抽取壳加密先让程序跑起来Frida dump内存中的so文件调用so里的函数一直崩溃缺少依赖库或JNI参数类型不对用Unidbg时补齐依赖库逐参数核对类型签名逆向这条路最大的问题是信息不完整很多报错并不会直接告诉你哪一步出了问题。我的排查顺序是从底层往上层先确认so文件能被正常加载确认JNI函数入口指向正确确认参数类型匹配最后才是分析算法逻辑。跳过任何一步直接看算法都可能被误导几个小时。6. 最后分享几条实操习惯6.1 保持样本脱敏和版本记录的习惯这次TK项目做到一半我就发现一个很现实的问题每次抓到的包内容都带真实业务字段时间一长根本分不清哪个包对应哪个App版本。后来我强制自己做了两件事所有抓包样本统一脱敏把有用户标识的字段替换成固定测试值同时把包名、App版本、so文件的MD5记录下来放在同一个分析目录里。这个习惯在团队协作里尤其重要否则你排查到一半别人给你一个“昨天抓的包”你连它属于哪个版本都不知道整个分析链条就断了。6.2 把Frida脚本当作工程维护别随手敲很多人用Frida就是在命令行里临时敲几行用完就丢。这在小实验里没问题但TK这种涉及多函数、多阶段调用的项目脚本很快就变得不可维护。我后来把所有hook脚本按功能分类放在一个Git仓库里比如ssl_unpin.js、trace_sign.js、dump_module.js每个脚本固定用Python起服务统一管理参数。这样换个App分析时大部分脚本改个类名就能复用效率提升很明显。6.3 先还原算法再说绕不绕过还有一点是我个人的原则做协议分析的目的是把算法还原清楚而不是只追求“能跳过校验”。这个项目最后我不仅知道了TK的签名算法是HMAC-SHA256加固定盐值截断还顺手把请求协议的整体结构整理成了文档。这样不管是客户端联调、服务端排障还是后续新版本协议变更都有据可查。逆向So层不是目的把协议梳理清楚才是目的。
返回列表