文章总结: 文档详细记录逆向分析某App签名算法的完整过程:从抓包发现api_sign字段开始,通过反编译定位Java层调用链,利用反射机制追踪至SO层的native方法,最终通过IDA静态分析确认采用HMAC-SHA256算法且密钥硬编码在.rodata段。关键发现包括算法实现存在密钥暴露风险,提出采用服务端动态令牌、白盒加密或TEE存储等加固方案,并探讨了通过提取密钥或Hook进行防御测试的思路。 综合评分: 86 文章分类: 逆向分析,移动安全,漏洞分析,二进制安全,安全开发
一次尝试某APP签名算法逆向追踪:从抓包到SO层
FinSectech FinSectech
看雪学苑
2026年4月1日 17:59 上海
最近在逆向某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
点进b.b()看一眼,它调了a()方法,看来真正的逻辑在a()里。
第六步:return + .apiSign(context, treeMap, str) 实际函数调用
private static String a(Context context, TreeMap
a()方法里调了VCSPSecurityBasicService.apiSign(),继续跟。
第七步:看return
public static String apiSign(Context context, TreeMap
apiSign()里又调了getMapParamsSign(),这层套一层的,有点耐心慢慢跟。
第八步:继续看return 代码比较长 拉下一些看
public static String getMapParamsSign(Context context, TreeMap<String, String> treeMap, String str, boolean z10) {String str2 = null;if (treeMap == null) {return null;}boolean z11 = false;Set<Map.Entry<String, String>> entrySet = treeMap.entrySet();if (entrySet != null) {Iterator<Map.Entry<String, String>> it = entrySet.iterator();while (true) {if (it == null || !it.hasNext()) {break;}Map.Entry<String, String> 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<String, String> 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<String, String> map, String str, boolean z10) {try {if (clazz == null || object == null) {synchronized (lock) {initInstance();}}if (gsMethod == null) {gsMethod = clazz.getMethod("gs", Context.class, Map.class, String.class, Boolean.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<String, String> 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<String, String> 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, {onEnter: function(args) {this.x1 = args[2];this.x2 = args[3];},onLeave: function(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 = str↓str = 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完整性
与服务端配合做二次校验(如签名+时间戳+随机数)
第十七步:方法论总结:这次的经验下次怎么用
这次追踪的过程,其实可以抽象成一个通用的签名算法逆向框架:
阶段 操作 关键点
- 抓包定位 找到可疑字段 重点关注Authorization、sign、token等
- 静态搜索 反编译搜关键词 搜字段名、赋值语句、类名
- 调用链追踪 从赋值点往上追 注意反射、动态加载、JNI
- 反射识别 找到Class.forName或getMethod 反射是常见混淆手段,看到就警觉
- JNI定位 找到native方法和so名字 loadLibrary是关键线索
- SO分析 IDA打开,定位JNI函数 先搜Java_包名类名,再F5
- 算法还原 识别密码学函数调用 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库深度解析
球分享
球点赞
球在看
点击阅读原文查看更多
免责声明:
本文所载程序、技术方法仅面向合法合规的安全研究与教学场景,旨在提升网络安全防护能力,具有明确的技术研究属性。
任何单位或个人未经授权,将本文内容用于攻击、破坏等非法用途的,由此引发的全部法律责任、民事赔偿及连带责任,均由行为人独立承担,本站不承担任何连带责任。
本站内容均为技术交流与知识分享目的发布,若存在版权侵权或其他异议,请通过邮件联系处理,具体联系方式可点击页面上方的联系我。
本文转载自:看雪学苑 FinSectech FinSectech《一次尝试某APP签名算法逆向追踪:从抓包到SO层》
版权声明
本站仅做备份收录,仅供研究与教学参考之用。
读者将信息用于其他用途的,全部法律及连带责任由读者自行承担,本站不承担任何责任。











评论