前阵子接了个某金融类App的逆向分析需求,目标应用带了企业级加固壳,常规的jadx打开一片乱码,frida附加直接崩溃。折腾了几天,最终把 wll-kgsa 和 signature 这两个核心参数的生成逻辑完整逆了出来。这个过程中踩了不少坑,也把整个分析链路重新梳理了一遍,从壳的识别、脱壳方案选型,到Java层定位、so层算法还原,每一步都有值得记录的细节,今天整理出来分享给同样在做App逆向分析的朋友。
这篇内容既适合刚入门的逆向新手当作实战路线图,也适合已经做过一些逆向但第一次碰到企业级壳的朋友参考。我会把完整的分析思路、工具链选型、关键代码定位方法和排查技巧都讲清楚,尤其是几个“卡壳卡到怀疑人生”的点,会重点说。
1. 壳的基本概念与分类:先搞清楚对手是谁
1.1 壳的核心作用与常见分类方式
壳在App逆向分析里是个绕不开的话题。简单说,壳是加固厂商提供的一套“保护层”,在App运行时先把真正的DEX、SO等核心代码加密或隐藏起来,等程序启动后再在内存中还原。逆向人员直接拿到的安装包,里面根本不是完整的可分析代码,必须先把这层“保护罩”拆掉,才能继续后面的分析。
从我实际遇到的案例来看,安卓端的壳大概分这么几个层级:
-
DEX整体加固:最基础的方案。DEX文件整体加密放在assets目录或so文件里,运行时在native层解密后加载。特征是在jadx里能看到壳的Application入口类,原代码全部不可见。这类壳脱壳最容易,用frida-dexdump这类工具基本都能搞定。
-
DEX抽取加固:在整体加固基础上,把DEX中的方法体代码(CodeItem)在安装包里抹掉,运行时由壳的native层按需从服务器解密填充回来。特征是反编译后能看到方法签名和字段,但方法内部全是空实现或直接throw异常。
-
VMP加固:最棘手的一层。把关键方法或整个DEX用虚拟化指令集替代,原有的Dalvik字节码被转换成一堆自定义指令,由壳内置的虚拟机解释执行。这种壳静态分析基本无效,只能靠动态调试和脚本辅助硬啃。
-
SO加固:针对native层的保护。常见手段包括so加密、so抽取、系统API的inline hook对抗、调试器检测等。银行类App经常是SO加密+DEX抽取+VMP多层叠加。
不同壳的应对策略差别很大,动手之前先确认目标是什么类型的壳,能少走很多弯路。
1.2 快速识别壳类型的实操方法
想知道目标App用的是哪家的壳,有两个最快的办法:
一是直接看特征包。用jadx打开apk,看Application开头是哪个类名,或者看assets目录和lib目录下so的名称。壳厂商的SDK名称普遍有辨识度,比如你会在assets里看到一堆.dat、.bin文件,或者lib/下看到libexec.so、libshell.so这类命名,基本就能锁定时厂商。
二是用现成工具。libcheck检测一下so库,或者用frida跑一遍壳检测脚本。也可以直接看/data/app/包名/lib/arm64/目录下的文件列表,加固过的App几乎都会多出一些奇怪的so文件。
以我这次遇到的某壳为例,脱壳前用jadx打开,所有Activity都是空壳,源码全部指向壳的类,AndroidManifest.xml里的Application被替换成了壳的入口,assets下面有明显的加密文件。综合判断是DEX抽取配合SO级整体保护的方案,对应策略就很清晰了——先用脱壳机把DEX抽出来,再针对关键方法做动态分析。
1.3 壳与逆向分析的对立与博弈
壳的对抗升级一直存在。早期FDex2这类脱壳工具可以暴力dump dex文件,后来壳厂商加了反调试和内存保护。我遇到过的几个新版本壳,已经会用“运行时校验DEX完整性”的方式,一旦发现DEX被dump过就主动退出,甚至直接封号。这轮分析的App就是这种情况,frida刚attach上去还没执行脚本,进程直接自杀。后面我会讲我绕过这个检测的方法。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 逆向环境搭建与工具链选型
2.1 基础环境准备:模拟器、真机还是云机
做App逆向先解决一个问题:目标App跑在什么环境里。模拟器方便但容易触发环境检测,真机稳定但需要处理root和调试环境,云手机适合批量操作但成本偏高。我建议优先用真机加root的方案,避障最少,兼容性也最好。
具体配置建议:
- Android版本:Android 10到13都行,太新的版本可能自带限制,太老的版本兼容性差。我用的是Android 12的一台Pixel,稳定性还可以。
- Root方案:Magisk刷入,配合
zygisk模块来隐藏root痕迹。金融类App大多做了root检测,纯靠root后直接跑大概率被拒。 - 调试工具:Frida 16.x版本配合frida-server,版本务必要和PC的python frida一致,不然会一直握手失败。
- 抓包环境:Charles或Burp Suite,用来看接口请求和参数结构。遇到证书校验,配合
JustTrustMe或frida的SSL pinning绕过脚本处理。
如果是已经部署了RASP(运行时应用自保护)的App,真机都要小心处理。目标App的反root能力我在测试手机上被打回好多次,后来是先装Magisk再刷Shamiko隐藏模块,最后才顺利跑起来。
2.2 脱壳工具的实际比较与选型
脱壳工具选哪个,直接决定你要多花多少时间。市面常见的几类我叫得上名的:
| 工具名称 | 适用场景 | 优缺点 | 实际体验 |
|---|---|---|---|
| frida-dexdump | DEX整体加固、部分抽取加固 | 轻量、简单,一条命令出结果 | 对老版本壳效果很好,新壳基本失效 |
| BlackDex | 免root脱壳 | 支持很多厂商的壳 | 但遇到so加固或多层壳会漏掉方法 |
| frida-unpack | 抽取加固的专用脱壳 | 能还原被抽掉的方法体 | 对VMP和极限抽取支持不理想 |
| YouTuP | Android逆向脱壳平台 | 支持VMP、抽取等高强度壳 | 需要上传,合规性好很多 |
这次遇到的目标是抽取+VMP混合加固,我先尝试frida-dexdump,dump出来的DEX用jadx打开能看到完整类结构但没有方法体,明显是抽取加固定位。后来改用BlackDex在内存中先让壳主动解密了部分方法,再用frida-unpack在运行时把方法体从内存里找回来,才勉强把关键应用层逻辑拼出来。
2.3 Java层与native层分析工具搭配
脱壳只是第一步,真正分析wll-kgsa和signature这两个参数,需要静态和动态配合着来。
静态分析我主要用jadx-gui加GDA,jadx负责看反编译后的Java代码,GDA看纯smali和so反汇编更顺手。关键方法在jadx里明确后,再去IDA Pro里看对应的native函数。IDA的F5确实给力,但so被壳保护后,需要先dump出解密后的so再分析,这又是一个大工程。
动态分析用Frida为主,objection辅助。objection在内存搜索类名、方法名和hook常用方法时非常方便,比如用android hooking list classes快速定位启用的类,用android hooking search classes搜包含特定字符串的类。不过在目标App开了反调试的情况下,objection一启动就会被杀掉,需要借助frida脚本做反反调试,再继续。
3. wll-kgsa参数逆向:从抓包到Java层定位
3.1 抓包分析与参数外观判断
拿到目标App后,第一步照例是抓包。开Fiddler或Charles,禁掉应用代理检测,安装系统证书后开始操作App。这里有个细节:金融类App一般会有SSL Pinning,直接用系统代理抓不到包。我一般是先objection android sslpinning disable,不行再写frida脚本hookTrustManager和OkHttp3的证书校验逻辑来绕过。
抓包结果很清晰,请求头里有两个非常显眼的参数:
code复制wll-kgsa: 1f8b8a2b4c1d0e1e2c7f4603a5b5f6e6d
signature: 9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08
wll-kgsa这个参数名非常有个性,一看就是业务方自定义的拼接参数。参数值是32位的十六进制字符串(去掉最后的填充后其实能算),看起来像MD5或者某种HASH计算的结果。signature则是64位的十六进制,这个长度和SHA256正好对上。
这两个参数出现在同一个请求头里,合理推测是服务端做接口签名校验用的。wll-kgsa可能是某个“关键词+算法”缩写的参数,signature则是把整个请求体拼上时间戳和密钥后算出来的摘要值。
3.2 Java层搜索与hook定位:找到生成入口
如果目标App没有对DEX做过深的抽取或VMP加固,jadx全局搜索wll-kgsa就能直接搜到生成代码。但这次搜索没有任何结果——参数不是普通字符串常量,而是在native层拼接或其他地方动态生成的。
我用Frida的运行时搜索来定位。核心思路是两个方向:
- Java层字符串搜索:先hook
java.lang.String的构造函数,在App运行过程中对所有构造出来的字符串做过滤,搜到包含wll-kgsa或signature的就直接打印调用栈。 - native层字符串引用搜索:这个更关键。先让App运行起来,在内存中全局搜这两个字节序列,如果so里硬编码了这两个参数名,很快就能定位到具体so和偏移地址。
用Frida的Memory.scan在App进程里搜wll-kgsa十六进制和字符串,最终在libProtectClass.so里找到了引用。这就意味着参数名本身不是Java层传上去的,而是native层直接拼好再返回给Java层用的。
为了确认,再扒一下调用关系。用frida-trace跟踪Request.Builder的addHeader函数和OkHttp3的HttpUrl构造链,成功捕获到Java层拿到的完整请求头和header名称来源,顺着调用栈往上翻,看到native层调用的一个方法叫native_generateHeader。
3.3 定位native函数与JNI调用链
既然确认是native生成,JNI函数名就成了突破口。用jadx反编译后搜System.loadLibrary,找到目标so名称,然后看哪些方法带有native关键字。objection在Android下执行:
code复制android hooking list activities
android hooking list services
这些不太对症,用frida-trace -i "native_generateHeader"反而来得更快。加入hook后,每次调用native_generateHeader都会打印参数和返回值,虽然返回值是经过处理的,但能清晰看到调用时机和传入的字符串。
再去IDA里打开对应的so,用JNI_OnLoad里注册的函数表反查,native_generateHeader在导出表里就有对应实现。F5反编译后能看到它内部其实调用了wll_kgsa_generate和sign_generate两个函数,参数分别是请求体、时间戳和密钥相关字符串,返回值拼进header返回。
这里被壳做了UDP之类的校验吗?其实没有,这个so只是DEX加壳后的“附带品”,真正的核心加密函数在另一个so里。但这个发现让分析路径清晰了:wll-kgsa和signature的算法都在native层,Java层只是转发。
4. signature参数逆向:算法还原与密钥提取
4.1 静态分析关键函数:从汇编到伪代码
按F5的反编译结果顺着函数名找下去,发现sign_generate内部逻辑比较复杂,涉及几个固定字符串和动态计算。先看函数开头,有几个明显特征:调用了一些malloc、memcpy、memset来拼接数据,然后调用一个哈希函数,最后转成十六进制字符串。
对比sign_generate的汇编代码和Java层日志,能确定传入参数分为三块:时间戳(13位的毫秒级时间戳)、请求体中的关键值(wll-kgsa?不,是一组业务字段拼接)、以及一段固定盐值。
最终signature的计算过程可以还原为:
- 取请求体的部分字段,按字母序排序后拼接。
- 拼接上时间戳和固定的盐值salt。
- 对该字符串做SHA-256哈希(也可能用了HMAC-SHA256)。
- 将哈希结果转成十六进制小写字符串,作为
signature的值。
wll-kgsa则是另一个独立计算:大概是base64(前若干字节的MD5结果) + 固定混淆词,最后转小写。实际抓包里的32位十六进制值也就是MD5的长度,很吻合。
4.2 动态验证与关键盐值提取
静态还原的算法顺序不到最后一个字节都别急着信。我在IDA里虽然看到了哈希调用,但具体选了EVP_sha256()还是EVP_md5(),只靠静态看不保险。直接用Frida来验证:
javascript复制Interceptor.attach(Module.findExportByName("libsign.so", "sign_generate"), {
onEnter: function(args) {
console.log("sign_generate called");
console.log("arg0:", hexdump(args[0]));
console.log("arg1:", hexdump(args[1]));
console.log("arg2:", args[2]); // length
},
onLeave: function(retval) {
console.log("retval:", retval);
}
});
这里顺带补一个细节:如果so用了自写哈希而不是标准Openssl,那findExportByName可能找不到符号,需要用Module.enumerateSymbols遍历或直接Memory.scan搜特征字节定位函数地址。
动态跑一次请求,确认sign_generate的三个入参分别是:
- 排序后的业务字段拼接串
- 13位时间戳
- 盐值字符串
利用Frida打印完整指令流或跟踪哈希函数入口参数,能看到盐值的具体内容是一个硬编码在so里的字符串。这个盐值本身并不随机,但在每次请求时会带上时间戳,所以若不修改内容重放,服务端也能验出时间窗问题。
4.3 一个比较隐蔽的坑:签名校验的“双保险”
顺着静态分析,我发现服务端校验signature时还会再叠一层业务层的签名校验,它不仅在请求头上校验,还在请求正文某个字段里再放一遍签名。这就是匹配最新热词中invalid signature detected的场景——如果你只改了请求头里的签名,但正文里的那个同步签名字段没改,服务端照样返回签名校验失败。
这在接口分析时要特别注意:对于大多数金融或企业类App,signature这类安全参数往往不是加一次就完事,而是请求体和请求头各算一遍。逆向时至少要同时跟踪两个地方,才不会漏掉完整性校验逻辑。
4.4 wll-kgsa参数名含义的不完全考证
关于wll-kgsa这个命名,由于没有官方文档,只能猜测。从拼写音节来看,“wll”可能是某个业务系统的缩写,“kg”可能是”关键字“或”kgram“的缩写,“sa”可能是”Signature Algorithm“之类的意思。也有可能是某个团队内部的命名规范。有点类似“某个参数叫token还是叫access_token”,核心是背后的算法和密钥,参数名叫什么对复现逻辑不影响。
不过提个建议:后续遇到这种命名不清晰的未知参数,除了搜代码,也要看服务端有没有返回错误提示的字段,错误提示里往往会带出参数含义。这次目标App在签名失败时有条“invalid signature detected”的error message,一眼就知道是校验不过,省了不少力气。
5. 常见问题与排查技巧实录
5.1 “invalid signature detected”这类报错的通用排查思路
逆向过程中,调试时改了一处参数导致签名验证失败是最常见的。如果你在尝试请求时遇到了invalid signature detected或者signature check failed之类的错误,先别急着怀疑算法还原不对,按这个顺序排查:
- 参数顺序:签名拼接串的字段顺序是不是服务端规定的?很多App会要求按参数名ASCII码排序后再拼接。
- 时间戳:用的是毫秒还是秒?本地时间与服务器时间偏差是否过大?
- 盐值salt:是否用了最新的salt版本?有些App的salt是动态下发的,不是写死在App里的。
- 双重签名:请求头和请求体是不是都要带?只改一处会导致失败。
- 字符编码:拼接串里有没有特殊字符转义问题?比如空格变成
+或者%20,签名结果就完全不同。
这些坑我一个不落地踩了一遍。上次用其它App做练习时,最后发现是”秒和毫秒“的问题,服务端要毫秒级时间戳,本地脚本用了秒级,调试了整整一个晚上。
5.2 壳导致的反调试与反Frida问题
这轮分析里最折磨人的是反Frida检测。目标App会在启动时检测frida-server的运行状态,常规做法是frida-server改名、用随机端口启动,但这些老办法在新版壳面前基本没用。后来我用的是frida-server + Magisk模块的方式,把frida的相关特征隐藏掉,再用zipalign重打包,才勉强跑起来。
当然,如果你的测试机是已经root过的Pixel,也可以开Magisk DenyList,把目标App加进隐藏名单,然后再启动frida-server试试。有的壳会检测/proc/self/maps里有没有frida相关的映射,这个我没深究,但大方向是:先让App认为自己是干净的,再谈后续分析。
5.3 脱壳完整性不够时的补救方法
脱壳阶段我用BlackDex跑出来的DEX有类但方法体缺失,这个问题很常见。补救思路是多dump几次,或者在运行过程中把目标方法的CodeItem从内存中扣出来。
有次实战里,我直接在frida脚本里遍历所有已加载的类,找到目标方法后调用Method.DeclaringClass和dex.dump把当前加载的DEX完整导出来。这个方法对抽取加固的壳有时很有效,因为壳在方法被调用时已经把真正的Method实现填回内存了,抓准时机dump就能拿到完整代码。
5.4 系统盘更换导致的Secure Boot类报错误导入
顺带说一个调试时的信息差问题:我在排查过程中搜到过一个报错invalid signature detected check secure boot policy,第一反应以为是App校验失败,后来发现这根本是系统盘的引导相关报错,跟Android App没有关系。在Windows机器上更换系统盘或者开启安全启动策略时,也会出现类似的英文提示。
这个提醒很重要:逆向分析中,很多人容易把目标App的报错和其他系统层的报错混在一起。一定要确认报错究竟来自客户端、服务端还是中间设备,不然就是南辕北辙。特别是当搜索到的内容明显和secure boot、系统盘有关时,就直接跳过,别浪费时间。
5.5 经验技巧速查表
| 场景 | 推荐做法 | 避免做法 |
|---|---|---|
| 反调试检测 | Magisk+Shamiko隐藏root与frida | 直接root后裸奔真机调试 |
| SSL Pinning | frida脚本hook TrustManager 或 objection sslpinning disable | 只装系统证书不绕过校验 |
| DEX脱壳 | frida-dexdump/BckDex先dump,确认方法体完整性 | dump一次就以为万事大吉 |
| native函数定位 | frida-trace + IDA 交叉查看JNI注册表 | 盲目搜索全so字符串 |
| 签名参数分析 | 同时检查请求头和请求体,避免双重签名遗漏 | 只改一个地方就重放请求 |
| 报错判断 | 确认报错来自服务端还是本地系统 | 直接把系统报错当App报错排查 |
6. 实操心得与进一步扩展建议
6.1 一次完整逆向的项目节奏感
这类有壳有签名的逆向项目,最大的难点不是某一个环节特别难,而是环节太多、链条太长,中间任何一步掉链子都会前功尽弃。我建议按照“外壳 → 反调试 → 抓包 → 静态定位 → 动态确认 → 还原算法”这个顺序推进,每完成一步就把产物保存好,防止App更新导致前功尽弃。
以这次的wll-kgsa和signature为例,如果你只是临时调接口,其实拿到算法和盐值就够了;但如果要做更加自动化的批量请求工具,还有两个点要关注:
- 密钥的存储与保护:盐值在App里通常是硬编码字符串,但也有一些App会把密钥藏在服务器下发的配置里,或通过加解密后再传给客户端,这种情况只靠脱壳看不了全貌,需要抓服务器返回的配置来进一步分析。
- 加密方案的版本兼容:App升级后算法可能不变但盐值会变,或者干脆换一个签名方案上线。所以做签名复现的时候,给参数生成逻辑留一个可配置的“版本号”字段,后面省心很多。
6.2 合规与个人实践建议
App逆向分析这些年一直是安全研究的热门方向,但一定要把握好边界。我这篇内容里涉及的所有操作,都是在合规授权或自研App测试环境下完成的。如果你拿到别人的App,想对这个逻辑做类似的逆向分析,请先确认是否有明确的授权范围,或者参考相关法律法规与行业准则。逆向技术本身是中性的,关键看怎么用。
从我个人的实践感受来说,每次逆向分析项目最大的收获不是那个参数本身,而是分析方法论的打磨。比如“先在Java层快速定位入口,再顺着JNI到native层做交叉验证”这个思路,在任何有签名校验的App里几乎都能复用,尤其是处理那些加壳加签名校验的金融类、办公类App,熟练之后效率会高很多。
6.3 给新手的三个小建议
- 用hook代替裸看:很多时候一行
frida代码胜过在IDA里看半小时汇编。先动态抓取参数,再回过去静态验证,效率极高。 - 保留现场:每次抓包和分析的请求都保存成文件,方便回头对照参数变化,也方便写自动化脚本时做回归测试。
- 不要怕卡壳:逆向过程中卡住一两天太正常了。换工具、换思路、换环境,甚至先放着睡一觉再来,往往就是差一个很小但又很容易忽视的细节。
希望这篇关于wll-kgsa和signature参数逆向的分析过程,能给你在壳对抗和签名还原方面带来一些灵感。后面如果你们对某一步的具体操作感兴趣,比如某壳脱壳的具体步骤、Frida绕过反调试的脚本写法,我可以再单独写一篇拆开讲。
