乐于分享
好东西不私藏

一次尝试某APP签名算法逆向追踪:从抓包到SO层

一次尝试某APP签名算法逆向追踪:从抓包到SO层

最近在逆向某App的登录接口,抓包发现请求头里有个签名,看起来挺有意思,决定完整追踪一下它的生成过程,下面是我一步步的记录。

第一步:burp包上的证据痕迹 标出疑似加密

authorization OAuth api_sign=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx

xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx (标出)疑似加密 找

看到这一长串,第一反应是:这八成是个签名算法。那就顺着这个字段往下追吧。

第二步:反编译提取1关键词搜索

OAuth api_sign= (疑似的加密)

用Jadx打开APK,直接搜”api_sign”,看看能不能定位到关键代码。

第三步:文本凑格式 找架构相似 找出关键函数

apiProccessModel4.apiSign = str;if (str != null) {apiProccessModel4.request.addHeader(“Authorization”, “OAuth api_sign=” + str);}

可见str就是那个疑似加密

运气不错,一下就找到了。这里有个apiSign被赋值为str,然后塞进Header里。那这个str就是我们要找的加密值。

第四步:

 str=b.b() 带=号的直接定义

找str对应的意思

前面有 string str =”

一大堆过程定义

但是后面有str = b.b(context2, e10, apiProccessModel3.tokenSecret, apiProccessModel3.url);

直接表达

往上翻了一下,前面一堆初始化代码,但真正的赋值在这里:str是从一个b.b()方法来的。传了context、参数、tokenSecret和url进去。

第五步:看return

public static String b(Context context, TreeMap<String, String> treeMap, String str, String str2) {if (treeMap != null && TextUtils.isEmpty(treeMap.get(ApiConfig.SKEY))) {treeMap.put(ApiConfig.SKEY, f(context, new String[0]));}return a(context, treeMap, str);

点进b.b()看一眼,它调了a()方法,看来真正的逻辑在a()里。

第六步:return + .apiSign(context, treeMap, str) 实际函数调用

private static String a(Context context, TreeMap<String, String> treeMap, String str) {try {if (VCSPCommonsConfig.getContext() == null) {VCSPCommonsConfig.setContext(context);}String apiSign = VCSPSecurityBasicService.apiSign(context, treeMap, str);if (!TextUtils.isEmpty(apiSign)) {return apiSign;

a()方法里调了VCSPSecurityBasicService.apiSign(),继续跟。

第七步:看return

public static String apiSign(Context context, TreeMap<String, String> treeMap, String str) throws Exception {if (context == null) {context = VCSPCommonsConfig.getContext();}return VCSPSecurityConfig.getMapParamsSign(context, treeMap, str, false);

apiSign()里又调了getMapParamsSign(),这层套一层的,有点耐心慢慢跟。

第八步:继续看return 代码比较长 拉下一些看

public static String getMapParamsSign(Context context, TreeMap<StringString> treeMap, String str, boolean z10) {String str2 = null;if (treeMap == null) {return null;}boolean z11 = false;Set<Map.Entry<StringString>> entrySet = treeMap.entrySet();if (entrySet != null) {Iterator<Map.Entry<StringString>> it = entrySet.iterator();while (true) {if (it == null || !it.hasNext()) {break;}Map.Entry<StringString> next = it.next();if (next != null && next.getKey() != null && ApiConfig.USER_TOKEN.equals(next.getKey()) && !TextUtils.isEmpty(next.getValue())) {z11 = true;break;}}}if (z11) {if (TextUtils.isEmpty(str)) {str = VCSPCommonsConfig.getTokenSecret();}str2 = str;}return getSignHash(context, treeMap, str2, z10);

getMapParamsSign()代码有点长,但最后return的是getSignHash(),看来关键还在后面。

第九步:继续看return

public static String getSignHash(Context context, Map<StringString> map, String str, boolean z10) {try {return gs(context.getApplicationContext(), map, str, z10);catch (Throwable th2) {VCSPMyLog.error(clazz, th2);return "error! params invalid";}}

getSignHash()里调了gs(),这个gs()看起来有点东西。

第十步:重点!!!

private static String gs(Context context, Map<StringString> map, String str, boolean z10) {try {if (clazz == null || object == null) {synchronized (lock) {initInstance();}}if (gsMethod == null) {gsMethod = clazz.getMethod("gs"Context.classMap.classString.classBoolean.TYPE);}return (String) gsMethod.invoke(object, context, map, str, Boolean.valueOf(z10));catch (Exception e10) {e10.printStackTrace();return "Exception gs: " + e10.getMessage();catch (Throwable th2) {th2.printStackTrace();return "Throwable gs: " + th2.getMessage();}}private static void initInstance() {if (clazz == null || object == null) {try {int i10 = KeyInfo.f69594a;clazz = KeyInfo.class;object = KeyInfo.class.newInstance();catch (Exception e10) {e10.printStackTrace();}}}

哇塞,这里居然是反射调用!initInstance()里把clazz赋值为KeyInfo.class,然后通过反射调用gs()。难怪前面一直找不到具体实现,原来是藏在这里了。

尝试了getMethod(“gs”, Context.class, Map.class, String.class, Boolean.TYPE); 但是内部自带函数

尝试了invoke(object, context, map, str, Boolean.valueOf(z10)); 但是内部自带函数

可以看到 initInstance()函数在上面被调用 找 initInstance()函数定义位置在下面 gs来自clazz 都殊途同归到看下面 clazz = KeyInfo.class;

就是说gs找不到直接定义表达 但是可以推断来自KeyInfo

反射绕了一圈,最后还是指向KeyInfo类。那好,直接去看KeyInfo。

第十一步:果然找到了

public static String gs(Context context, Map<StringString> map, String str, boolean z10) {try {try {return gsNav(context, map, str, z10);catch (Throwable th2) {return "KI gs: " + th2.getMessage();}catch (Throwable unused) {SoLoader.load(context, LibName);return gsNav(context, map, str, z10);}}private static native String gsNav(Context context, Map<StringString> map, String str, boolean z10);

可以看到最后到native层了 JNI开发 那去把apk改成zip 去文件夹里面找so文件拿ida看。

看到native关键字就懂了——算法在SO层。gs()里调了gsNav(),这个gsNav()是native方法,得去SO里找了。

public class KeyInfo {private static final String LibName = “keyinfo”;

文件名这里 到ida去搜 带lib

第十二步:打开ida 放入”libkeyinfo”文件 因为是jni开发 所以直接搜java_

找到对应的函数打开 按F5把汇编转C,导入jni.h文件

第十三步:右键点击a1函数 –> convert to struct –> JNIEnv 然后hide codes

IDA里定位到对应的native函数,F5转成伪代码,再把第一个参数转成JNIEnv结构体,就能看懂逻辑了。

右键点击a1函数 –> convert to struct –> JNIEnv 然后hide codes

IDA里定位到对应的native函数,F5转成伪代码,看到核心逻辑:

这段代码调了两次j_getByteHash(就是HMAC-SHA256),中间有一次字符串拼接,最后返回第二次hash的结果。

为了验证,用Frida hook一下:

var addr = Module.findExportByName("libkeyinfo.so""getByteHash");console.log(addr);Interceptor.attach(addr, {onEnterfunction(args) {this.x1 = args[2];this.x2 = args[3];},onLeavefunction(retval) {console.log("--------------------");console.log(Memory.readCString(this.x1));console.log(Memory.readCString(this.x2));console.log(Memory.readCString(retval));}});

运行后每次请求都会打印出输入数据、密钥和hash结果,和静态分析完全一致。

第十四步:调用链全貌流程图

到这里整个追踪路径就清晰了。我画了个流程图,方便一眼看清全局:

text抓包发现 api_sign搜索 "api_sign" 定位到 apiProccessModel4.apiSign = strstr = b.b()b.b() → a()a() → VCSPSecurityBasicService.apiSign()apiSign() → VCSPSecurityConfig.getMapParamsSign()getMapParamsSign() → getSignHash()getSignHash() → gs()gs() 反射调用 → KeyInfo.gs()KeyInfo.gs() → gsNav() (native)libkeyinfo.so → 算法实现

这个图的好处是:以后再遇到类似问题,你可以直接套这个分析框架——抓包定位 → Java层追踪 → 识别反射/动态加载 → SO层定位 → 算法还原。

第十五步:最后分析下来是HMAC-SHA256,密钥硬编码在SO文件的.rodata段里。

在IDA里跟进gsNav函数,看到调用了OpenSSL的HMAC_Init_ex、HMAC_Update、HMAC_Final系列函数,参数传递中有一个固定的buffer,指向.rodata段。提取出来一看,就是硬编码的密钥。

第十六步:深度延伸:这个算法的安全性评价与可能的绕过思路

安全性评价:

算法本身:HMAC-SHA256是安全的,目前没有有效碰撞攻击

实现层面:密钥硬编码是典型的安全缺陷。一旦so文件被提取,密钥就暴露了,攻击者可以本地伪造任意签名

防御建议:密钥应存放在更安全的位置,如:

服务端下发(动态令牌)

白盒加密方案

TEE/安全环境存储

可能的绕过思路(仅用于防御视角思考):

直接提取密钥:从.rodata段拿到密钥,本地计算签名

Hook HMAC函数:用Frida hook HMAC_Final,直接拿到计算结果

整体替换SO:把so文件整个替换成自己的版本,返回任意签名

动态调试篡改:在gsNav返回前修改返回值

防御方的对抗思路:

加反调试(ptrace、线程检查)

对关键函数做混淆/虚拟机保护

运行时校验so完整性

与服务端配合做二次校验(如签名+时间戳+随机数)

第十七步:方法论总结:这次的经验下次怎么用

这次追踪的过程,其实可以抽象成一个通用的签名算法逆向框架:

阶段 操作 关键点

  1. 抓包定位 找到可疑字段 重点关注Authorization、sign、token等

  2. 静态搜索 反编译搜关键词 搜字段名、赋值语句、类名

  3. 调用链追踪 从赋值点往上追 注意反射、动态加载、JNI

  4. 反射识别 找到Class.forName或getMethod 反射是常见混淆手段,看到就警觉

  5. JNI定位 找到native方法和so名字 loadLibrary是关键线索

  6. SO分析 IDA打开,定位JNI函数 先搜Java_包名类名,再F5

  7. 算法还原 识别密码学函数调用 HMAC、AES、RSA家族函数特征明显这个框架以后可以复用:

换个App,同样的套路

换个算法(AES/RSA),流程一样

遇到其他混淆(Obfuscator、DexGuard),先剥壳再套这个框架

整个追踪过程到此结束。从抓包开始,一路追到Java层,再通过反射找到KeyInfo类,最后进SO层定位到算法。虽然绕了一点,但每一步都有迹可循。

合规提示本分析仅用于安全技术研究,所有数据均已脱敏,请勿用于非法用途。在进行类似分析时,请确保拥有合法授权,遵守《网络安全法》《数据安全法》等相关法律法规。

看雪ID:FinSectech

https://bbs.kanxue.com/user-home-1070028.htm

*本文为看雪论坛优秀文章,由 FinSectech原创,转载请注明来自看雪社区

# 往期推荐

安卓逆向基础知识之frida Hook

2025 强网杯和强网拟态部分题解

在逆向分析方面-unidbg真的适合 MCP 吗?

AI静态分析,内核模块隐藏 Frida 特征,绕过linker私有结构遍历崩溃链

某安全so库深度解析

球分享

球点赞

球在看

点击阅读原文查看更多