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

张开发
2026/4/10 9:49:53 15 分钟阅读

分享文章

一次尝试某某会APP签名算法逆向追踪:从抓包到SO层
最近在逆向某某会App的登录接口抓包发现请求头里有个签名看起来挺有意思决定完整追踪一下它的生成过程。下面是我一步步的记录。1 burp包上的证据痕迹 标出疑似加密authorization OAuth api_signxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx (标出)疑似加密 找看到这一长串第一反应是这八成是个签名算法。那就顺着这个字段往下追吧。2 反编译提取1关键词搜索OAuth api_sign (疑似的加密)用Jadx打开APK直接搜api_sign看看能不能定位到关键代码。3 文本凑格式 找架构相似 找出关键函数apiProccessModel4.apiSign str;if (str ! null) {apiProccessModel4.request.addHeader(Authorization, OAuth api_sign str);}可见str就是那个疑似加密运气不错一下就找到了。这里有个apiSign被赋值为str然后塞进Header里。那这个str就是我们要找的加密值。4strb.b() 带号的直接定义找str对应的意思前面有 string str 一大堆过程定义但是后面有str b.b(context2, e10, apiProccessModel3.tokenSecret, apiProccessModel3.url);直接表达往上翻了一下前面一堆初始化代码但真正的赋值在这里str是从一个b.b()方法来的。传了context、参数、tokenSecret和url进去。5 看returnpublic static String b(Context context, TreeMapString, 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()里。6 return .apiSign(context, treeMap, str) 实际函数调用private static String a(Context context, TreeMapString, 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()继续跟。7 看returnpublic static String apiSign(Context context, TreeMapString, String treeMap, String str) throws Exception {if (context null) {context VCSPCommonsConfig.getContext();}return VCSPSecurityConfig.getMapParamsSign(context, treeMap, str, false);apiSign()里又调了getMapParamsSign()这层套一层的有点耐心慢慢跟。8 继续看return 代码比较长 拉下一些看public static String getMapParamsSign(Context context, TreeMapString, String treeMap, String str, boolean z10) {String str2 null;if (treeMap null) {return null;}boolean z11 false;SetMap.EntryString, String entrySet treeMap.entrySet();if (entrySet ! null) {IteratorMap.EntryString, String it entrySet.iterator();while (true) {if (it null || !it.hasNext()) {break;}Map.EntryString, 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()看来关键还在后面。9 继续看returnpublic static String getSignHash(Context context, MapString, 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()看起来有点东西。10 重点private static String gs(Context context, MapString, 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。11 果然找到了public static String gs(Context context, MapString, 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, MapString, 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去搜 带lib12 打开ida 放入libkeyinfo文件 因为是jni开发 所以直接搜java_找到对应的函数打开 按F5把汇编转C导入jni.h文件13 右键点击a1函数 -- convert to struct -- JNIEnv 然后hide codesIDA里定位到对应的native函数F5转成伪代码再把第一个参数转成JNIEnv结构体就能看懂逻辑了。13 右键点击a1函数 -- convert to struct -- JNIEnv 然后hide codesIDA里定位到对应的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结果和静态分析完全一致。14 调用链全貌流程图到这里整个追踪路径就清晰了。我画了个流程图方便一眼看清全局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层定位 → 算法还原。15 最后分析下来是HMAC-SHA256密钥硬编码在SO文件的.rodata段里。在IDA里跟进gsNav函数看到调用了OpenSSL的HMAC_Init_ex、HMAC_Update、HMAC_Final系列函数参数传递中有一个固定的buffer指向.rodata段。提取出来一看就是硬编码的密钥。16 深度延伸这个算法的安全性评价与可能的绕过思路安全性评价算法本身HMAC-SHA256是安全的目前没有有效碰撞攻击实现层面密钥硬编码是典型的安全缺陷。一旦so文件被提取密钥就暴露了攻击者可以本地伪造任意签名防御建议密钥应存放在更安全的位置如服务端下发动态令牌白盒加密方案TEE/安全环境存储可能的绕过思路仅用于防御视角思考直接提取密钥从.rodata段拿到密钥本地计算签名Hook HMAC函数用Frida hook HMAC_Final直接拿到计算结果整体替换SO把so文件整个替换成自己的版本返回任意签名动态调试篡改在gsNav返回前修改返回值防御方的对抗思路加反调试ptrace、线程检查对关键函数做混淆/虚拟机保护运行时校验so完整性与服务端配合做二次校验如签名时间戳随机数17 方法论总结这次的经验下次怎么用这次追踪的过程其实可以抽象成一个通用的签名算法逆向框架阶段 操作 关键点抓包定位 找到可疑字段 重点关注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层定位到算法。虽然绕了一点但每一步都有迹可循。

更多文章