资讯详情

SO文件逆向实战:从定位3DES算法到提取密钥IV的完整流程

📅 2026/10/19 14:56:04 | 华诺云谱 👁 阅读
SO文件逆向实战:从定位3DES算法到提取密钥IV的完整流程
做移动安全这行的兄弟应该都碰到过这种场景抓包抓到了明文和密文结果顺着调用点翻遍了整个DEX文件都找不到加密函数在哪。更有意思的是明明在Java代码里看到了一个native方法叫encryptData跟进一看里面只剩下一句System.loadLibrary和一行native调用真正的实现全在so文件里。这周我刚好在做一个老客户端的协议兼容项目需要把so里的3DES加密逻辑完整复现出来整个过程走完觉得思路和方法值得写一篇讲透从工具准备、静态定位、动态验证到密钥IV的提取一条线串下来给准备入坑逆向或者正在挠头的朋友一份能直接照抄的实操手册。这个需求核心就一句话你从Java层只看到一堆byte[]进、一堆byte[]出中间那层黑盒到底做了什么必须把so掰开揉碎看清楚。下面我尽量按我实际操作的顺序来讲你跟着走一遍之后遇到类似的native层加密也能有个明确的套路。1. 为什么这类加密需求最后都落在SO文件里1.1 实战场景来源我平时接到这类分析任务来源基本是下面几种。第一种是做接口兼容某个老系统还在用3DES做报文加密现在需要在新客户端里复现同一套加密流程但老客户端没有开源只能去逆向老版本里的so把算法参数一项项对齐。第二种是安全评估需要确认客户端是否硬编码了密钥、是否存在弱密钥、加解密逻辑是否可以被轻易替换这种时候你光看Java层是不行的因为开发早就把关键逻辑下沉了。第三种是协议分析抓包发现请求体是一段Base64字符串想搞清楚服务端返回的到底是什么必须把客户端本地的解密流程跑通才能继续往下做后续测试。这些需求里十次有八次落点都在so文件。原因不复杂很多客户端团队为了提升破解成本会把加密算法整体下沉到native层Java层只留一个JNI壳子。从Java层看你只能看到一个native方法传进去一堆byte[]返回来的还是一堆byte[]至于里面是3DES、AES还是别的什么单看Java代码根本判断不了。这时候需要清醒一点分析so文件不是把里面每一行汇编都读懂而是先搞清楚三个问题。第一这里跑的是什么算法第二密钥、IV、填充方式是什么第三输入输出之间经历了什么变换。这三个问题解决了你就可以用任意语言写一个等价实现跑通数据需求就完成了。1.2 为什么偏偏是3DES可能有人会觉得现在新项目都用AES了怎么还有人在折腾3DES。实际业务里3DES的存量非常大尤其是一些银行、支付、政务类的老系统十几年前定下的报文规范现在还在跑新客户端要去兼容这些老系统就得老老实实照着重做一遍3DES。3DES的本质是把DES跑了三轮用三个不同或者部分相同的8字节密钥把明文依次做一次加密、一次解密、再一次加密也就是常说的EDE结构。DES自己的密钥只有56位有效长度单用已经不安全了但跑三轮之后密钥长度翻到112位或者168位在当年足够应付绝大多数场景所以大量老协议都选了它。分析3DES有个天然优势就是算法特征非常明显比AES好认得多。AES的S盒、列混合矩阵虽然也有固定特征但实现方式灵活有的查表有的位运算编译出来千奇百怪。而DES的核心结构是Feistel网络加固定的S盒常量只要so里带了标准S盒你在二进制里用一串特征字节就能把它搜出来。第三节我会专门讲这一步。2. 动手前准备工具箱与样本探测2.1 工具清单与选型理由先把我常用的工具列出来你可以按自己的习惯替换但作用是一样的。工具用途我为什么选它IDA Pro静态反汇编、F5伪代码对ARM的支持最成熟分析so首选Ghidra免费替代方案没有ID授权时用功能不弱Frida动态hook、运行时函数监控不用重启进程脚本灵活社区文档多jadx-gui反编译APK/DEX快速定位Java层的JNI调用点010 Editor查看so二进制结构看rodata段、字符串、导入表比较方便真机或模拟器运行环境建议用带root的真机兼容性最好IDA和Frida是绝对的主力。IDA负责把so变成可读的代码Frida负责在运行时把so的实际行为验证一遍。这两者配合能对付绝大多数不含商业加固的so文件。如果团队没有ID授权Ghidra完全可以替代静态分析这一环只是交互效率和插件生态稍微差一点。2.2 从APK到SO的基本探测实际操作的第一步永远是先拿到APK。如果是某个受委托测试的App直接adb install之后把安装包从设备里拖出来即可如果是市场下载的注意选与目标版本一致的包因为不同版本的so实现可能有差异版本错位会导致整个分析结论不作数。拿到APK先别急着开IDA我习惯先用jadx-gui打开直接搜索native方法名。比如你在Java层看到某个方法标记为nativejadx会高亮显示记下完整包名、类名、方法签名这是后面动态hook的重要索引。同时看一下上层调用者搞清楚传入参数到底是String还是byte[]参数个数是多少这决定了你在native层看待数据的方式。接下来用file命令看so的架构file lib/arm64-v8a/libcrypto_enc.so如果输出是ELF 64-bit LSB shared object, ARM aarch64那就用64位分析如果是32位ARM就用32位模式。这个信息也决定了你Frida脚本里指针的宽度。然后再用readelf看一眼动态符号表readelf -Ws lib/arm64-v8a/libcrypto_enc.so | grep -i -E des|encrypt如果so没做strip导出表里一般能看到带des或者encrypt字样的符号直接点进IDA看就行。如果被strip过导出表干干净净那就得靠内存特征搜索也就是下一节的内容。3. 静态分析在汇编里定位3DES并提取密钥IV3.1 先用IDA锁住导出函数把so拖进IDA选对处理器类型之后第一步看Exports窗口。我遇到过的函数命名五花八门标准JNI导出函数名是Java_包名_类名_方法名这种一眼就能看出对应关系有些是开发自己起的名字比如des_encrypt、EncryptData还有更过分的全部叫func_1C03F0这种基本可以判断被strip过或者做了符号清理。拿到函数名后双击进入用F5看伪代码。需要提醒的是F5对O3优化过的代码还原度一般但足够让你判断这个函数是否跟加密相关。判断标准是看代码里的运算模式如果有大量移位、异或、查表整体代码密度很高循环结构多那基本就是块密码的实现。我记得有一次遇到一个叫util_encrypt的函数F5出来一堆指针操作一开始以为只是个普通封装。但注意到里面频繁出现一个基址加上固定偏移的加载往这个偏移处一看正是一组16轮轮密钥用到的置换表。所以我的经验是看到查表密集的函数不要放过跟着它引用的常量区走往往会有大发现因为这个世界上没有几个函数会平白无故去查一张512字节的表。3.2 认出3DES靠算法常量特征识别3DES最简单的方法是先认出它内部的DES核心。3DES不是独立算法它只是DES的复合所以只要so里出现了DES的标志性S盒常量基本就抓到了3DES的尾巴。DES有两个标志性静态数据表一个是16轮轮密钥排列表另一个是8个S盒每个S盒是4行16列的常量矩阵。S盒整体是512字节特征极其明显。比如标准DES S1盒第一行的前几个字节是0x0E 0x04 0x0D 0x01 0x02 0x0F 0x0B 0x08这个特征全世界通用。用IDA的二进制搜索直接搜这一串字节命中的地方大概率就是DES/3DES的查表循环所在。再看函数结构辅助确认。DES核心是Feistel结构每轮把32位数据分成左右两半右半进f函数做扩展、异或、查表、置换结果跟左半异或然后交换标准实现是16轮。你如果看到循环体里大量进行32位的异或伴随查表和位排列循环次数是16那就高度怀疑是DES系列。需要注意嵌入式环境下有些魔改版只做8轮或者12轮但S盒特征还是会暴露身份。区分3DES和DES也很简单3DES要处理24字节密钥。在伪代码里如果发现有一个buffer被分成三个8字节块分别参与密钥扩展或者密钥长度被限定为24那就是3DES。再进一步加密是K1加密、K2解密、K3加密叫EDE解密反过来是K3解密、K2加密、K1解密。这个方向决定了你自己写等价实现时密钥块的排列顺序错了结果全乱哪怕算法和参数都对也白搭。3.3 密钥和IV的三种藏法认出算法之后核心任务变成找密钥和IV。这两样东西在so里的藏法主要有三种我按出现频率排个序。第一种是直接硬编码在.rodata段或者.data段。用IDA跳转到函数引用的常量地址把数据区转成字节或者字符串查看经常能看到一组确定性字节就是密钥。要特别留意密钥不一定是可打印字符很多是0x01、0x23这种冷门字节字符串窗口里可能显示成乱码或者空你必须右键转成Hex确认。只看字符串窗口不知道要漏掉多少关键数据。第二种是动态拼接。有些so会把密钥拆成几段运行时先初始化一个数组再经过几次异或、加减运算拼出最终密钥。这种最费时间因为你要在伪代码里追踪每一处写入。我的建议是先找到最终传给DES密钥扩展函数的那个buffer地址然后回溯它在内存里经历了哪些操作。实际操作中我通常用Frida在运行时直接把这个buffer dump出来一步到位比在汇编里绕来绕去快得多。第三种是Java层传入。这种反而好办密钥不在so里而是从Java层通过JNI传进去的你只需要搞清楚参数顺序在动态阶段打印出来即可。IV的藏法跟密钥类似最常见的是放在固定偏移的常量区或者干脆置零某些老系统还会用固定字符串当IV比如“12345678”这种我在真实项目里见到过不止一次。静态阶段建议把确认到的算法模式、密钥、IV、填充方式、字符编码全部记到笔记里。到了动态阶段你只需要用这段参数跑一组对照测试能够用自己写的Python脚本加密出和App一样的结果逻辑就算彻底搞懂了。4. 动态验证用Frida把静态猜测坐实4.1 Frida的挂载与函数定位静态分析给出的判断说到底还是推测真正的确认必须靠动态。推荐Frida的理由很直接不需要重启App不用改so也不用重打包附加进程之后直接在内存里hook目标函数遇到加固过的so也能硬上。附加流程分三步走。第一步先枚举目标模块拿到so的加载基址。下面这段是标准开头var moduleName libcrypto_enc.so; var base Module.findBaseAddress(moduleName); console.log(base: base);第二步定位函数地址。如果so保留了导出符号直接用Module.findExportByName如果没有符号就遍历导出表把名字里带encrypt或者des的符号打出来var exports Module.enumerateExports(moduleName); exports.forEach(function(exp) { if (exp.name.toLowerCase().indexOf(encrypt) 0) { console.log(exp.name exp.address); } });第三步是Interceptor.attach。hook native函数时我建议同时打印参数和返回值字节数据用hexdump输出一眼就能看出是不是Base64出现过。如果你静态分析已经定位到了内部关键函数可以直接用基址加偏移的方式hook这种写法对混淆so更稳定因为不依赖符号名。4.2 参数与返回值的还原JNI函数参数有个固定约定前两个参数永远是JNIEnv*和jobject静态方法就是jclass从第三个参数开始才是业务入参。所以你在hook时记住args[2]才是真正的数据。如果入参是byte[]你想直接在native层把它读出来可以用下面这种简化脚本理解思路Interceptor.attach(encryptFunc, { onEnter: function(args) { var env args[0]; var jdata args[2]; var ByteArray Java.use(byte[]); var data Java.array(byte, Java.cast(jdata, ByteArray)); console.log(input hex: hexdump(ptr(data), { length: data.length })); } });不过这里有个细节真实使用中jobject转Java对象要做本地引用包装直接用Java.cast未必每次都灵。我个人的习惯是两层hook并用Java层负责把输入输出转换成能看的Hex或者Base64native层负责打印关键指针、偏移和长度再把指向的内存区域整体dump出来。两层数据能对上说明静态分析的结论没有错。还有一个快速验证的小技巧如果你怀疑某个so函数是加密函数先不管参数格式直接在入口打印前几个参数指针的值再用Process.readByteArray把指针指向的内存前64字节读出来。看到的数据里如果出现明确的字节对齐和长度感大概率就是密文或者密钥buffer。这个办法虽然原始但特别适合刚上手、没有现成类型信息的时候。4.3 调用链与栈回溯分析到这一步算法和参数基本清楚了但往往还差最后一环这个加密函数到底在什么时机被调用是谁在调用它。这个信息对理解整个业务逻辑至关重要也经常能帮你发现多个功能点共用同一个加密函数的情况。用Frida做栈回溯是最直接的方式。定位到你感兴趣的函数地址后在onEnter里打印调用栈var f Module.findBaseAddress(moduleName); Interceptor.attach(f.add(0x3C8C), { onEnter: function(args) { console.log(Thread.backtrace(this.context, Backtracer.ACCURATE) .map(DebugSymbol.fromAddress).join(\n)); } });注意这里的0x3C8C是函数相对基址的偏移处理带混淆的so时比符号名更可靠。栈回溯打出来之后你就能看到是哪一层的JNI封装进入了这个地址再对照jadx里反编译出来的Java调用关系一条完整的数据流就浮出来了。我在某个受委托测试的App上遇到过一种情况登录接口把时间戳、设备ID、随机数拼在一起做3DES加密填充方式是PKCS7密钥看着是24字节但实际只有前16字节参与运算后8字节全是0。这种“弱密钥”问题静态分析非常难发现因为代码里确实加载了一个24字节的数组但动态验证时你dump出参与密钥扩展的实际buffer才发现后半段全是0。所以我的结论是静态分析负责缩小范围动态验证负责一锤定音。5. 避坑指南常见问题与实操经验5.1 加固和混淆是最大干扰现在越来越多的so不是裸奔的。商业加固会在native层做指令抽取、控制流扁平化甚至虚拟化你从IDA里看到的可能不是原始指令而是一堆解释器分派逻辑。这种情况下静态分析基本缴械只能靠动态手段硬上比如运行时内存dump、Unidbg模拟执行或者干脆用Frida在Java层hook拿到足够的输入输出对用已知明文反推参数。我的建议是不要一上来就死磕汇编。先用Java层hook收集几十组输入输出再在本地用Python写几套候选配置比如3DES/EDE/不同填充跑一遍看哪一套能对上。很多时候根本不用跟每条汇编数据对上了结论就锁定了。这个思路反直觉但确实高效因为在真实加固样本里硬啃虚拟化指令时间成本是不可控的。5.2 3DES与单纯DES、加密模式的识别陷阱新手最容易翻车的地方是把3DES当DES或者反过来。判断标准其实很硬核看密钥长度。DES只处理8字节密钥3DES处理24字节。如果代码里同一个密钥扩展逻辑被执行了三次或者传入的密钥buffer长度是24那就是3DES。我把常见参数组合整理成了一张速查表方便你做对照实验时快速定位项目常见取值影响算法结构DES / 3DES-EDE / 3DES-EEE密钥块排列顺序不同输出完全不同密钥长度8字节 / 24字节长度错了数据直接对不上分组模式ECB / CBC / CFBCBC还需要IVECB不需要填充方式PKCS7 / ZeroPadding / NoPadding解密末尾会残留0x00或乱码输出编码Hex / Base64 / 原始字节抓包看到的东西和so内部字节经常不一致模式识别这一块最容易踩的是ECB和CBC的混淆。ECB模式同一明文块加密结果相同CBC模式每个块都跟前一个块产生关联必须要有IV。如果so里的加密函数只有两个参数入参是明文和密钥没有IV参数那我第一反应就是ECB如果有三个参数第三个多半是IV。5.3 编码坑Hex、Base64与乱码从我的实操经验看so内部加密完的原始字节通常有三种出口直接返回byte[]、转成Hex字符串、转成Base64字符串。抓包看到什么格式不代表so内部就是什么格式。最容易被坑的场景是你在Frida里把返回的jbyteArray直接打印出来看到一堆乱码就以为解密失败。其实那很可能只是因为你没有做Hex编码把原始字节直接按字符打印了。我现在养成一个习惯写了个通用的dump函数把每段字节统一转成Hex和ASCII两列输出长度超过64字节就截断显示。这样一来密钥、密文、IV在日志里都一目了然不用来回猜。另外一个隐藏坑是字符编码。从Java层传入的String在native层可能是modified UTF-8也可能是GBK老编码你用错误的编码去解释字符串再拿去做密钥派生结果必然对不上。遇到这种情况先确认字符串到底怎么编码的再看密钥是否就是字符串本身或者是否经过MD5/SHA1这类摘要处理。5.4 实操心得与合规边界最后聊几点个人经验。第一分析顺序永远是从Java到native先确认输入输出再进入so不要上来就一头扎进汇编。第二静态分析用伪代码判断算法类型很有效但不要迷信伪代码里的变量名和注释被编译器优化掉的细节只有动态才能真正确认。第三Frida脚本从简单开始能打印目标函数参数和返回值就能解决90%的问题剩下10%用栈回溯和内存dump补齐。第四点也是最重要的一点拿到样本之后先确认授权范围只在你拥有或者被明确授权测试的应用上做分析。逆向分析本身是中性的技术能力用在学习、合规审计、协议兼容这些正向上它就是有用的工具脱离授权去做损害他人的事情那是另一回事也是这一行最基本的底线。希望大家拿到一手的分析经验之后都能把它用在正确的地方。
📝

华诺云谱内容团队

资深建站顾问 · 行业研究员

10年+企业数字化服务经验,专注智能建站、SEO优化与品牌营销,持续输出建站技巧、行业洞察与营销干货,已帮助5000+企业实现数字化增长。

你可能需要的服务

订阅华诺云谱资讯周报

每周一封,精选建站技巧、SEO与营销干货,直达邮箱。已有 8,000+ 企业主订阅,助你少走弯路。

↑