找回密码
立即注册
搜索
热搜: Java Python Linux Go
发回帖 发新帖

4845

积分

0

好友

663

主题
发表于 7 天前 | 查看: 40| 回复: 0

最近在分析一个App的登录接口,抓包时发现请求头里带有一个签名参数,格式大致如下:

Authorization: OAuth api_sign=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx

看到这一长串疑似哈希值的字符串,第一反应是这很可能是一个用于验证请求完整性的签名。整个逆向分析过程,就是从定位这个 api_sign 字段开始,一步步深入到底层SO库的逻辑。下面是我的完整追踪记录。

第一步:从抓包定位到Java层入口

首先用Jadx打开目标APK,直接搜索关键词 "api_sign",这是最直接的定位方式。

Java代码中定位api_sign赋值

运气不错,很快找到了关键代码片段:

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

很明显,变量 str 就是我们最终要找的签名值。接下来的目标就是找到 str 的生成逻辑。

第二步:追溯调用链

继续向上追溯 str 的来源,发现它由一个名为 b.b() 的静态方法返回。

追溯str的来源

关键赋值语句是:

str = b.b(context2, e10, apiProccessModel3.tokenSecret, apiProccessModel3.url);

现在需要进入 b.b() 方法一探究竟。

第三步:深入核心方法

点开 b.b() 方法,发现它内部又调用了另一个方法 a()

b.b()方法内部逻辑

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);
}

继续跟进 a() 方法:

a()方法调用安全服务

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;
        }
    ...
}

可以看到,核心计算转移到了 VCSPSecurityBasicService.apiSign() 方法。继续追踪:

apiSign方法逻辑

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);
}

方法链继续延伸,调用到了 VCSPSecurityConfig.getMapParamsSign()。这个方法代码较长,但核心是最后调用了 getSignHash()

getMapParamsSign方法部分逻辑

public static String getMapParamsSign(Context context, TreeMap<String, String> treeMap, String str, boolean z10) {
    // ... 参数校验和预处理逻辑
    return getSignHash(context, treeMap, str2, z10);
}

第四步:识别反射与JNI调用

进入 getSignHash() 方法,发现它调用了 gs() 方法。

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";
    }
}

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();
    }
}

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 方法实现在 KeyInfo 类中。这种使用反射进行逆向工程的方法是混淆和隐藏核心逻辑的常见手段。

第五步:定位到Native层

查看 KeyInfo 类,果然找到了一个同名的 gs 方法,并且它最终调用了一个 native 方法。

KeyInfo类中的native方法

public class KeyInfo {
    private static final String LibName = "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 关键字和 libkeyinfo 库名,就明确了算法最终实现在 SO 层。这是典型的 JNI开发模式,核心安全逻辑下沉到 Native 代码以增加逆向难度。

第六步:静态分析SO文件

将 APK 解压,在 lib 目录下找到 libkeyinfo.so 文件,使用 IDA Pro 打开进行分析。

  1. 定位JNI函数:在 IDA 中搜索 Java_ 前缀,可以快速定位到 Java_com_xxx_KeyInfo_gsNav 函数(具体包名已脱敏)。
  2. 反编译与结构体转换:按 F5 将汇编代码转换为伪 C 代码。为了更清晰地理解 JNI 函数调用,需要将第一个参数(JNIEnv)转换为正确的结构体:在参数上右键 -> Convert to struct -> 选择 JNIEnv

分析伪代码后,找到了核心计算逻辑。其中一段关键代码调用了名为 j_getByteHash 的函数两次,疑似进行哈希计算。

SO层核心算法伪代码

伪代码片段显示:

v55 = j_getByteHash(a1, a2, v30, v16, v80, 256);
if ( v55 && (v56 = v55, v57 = strcpy(v79, dest), strcat(v57, v56), memset(v80, 0, sizeof(v80)), v58 = strlen(v79), (v59 = j_getByteHash(a1, a2, v79, v58, v80, 256)) != 0) ) {
    v53 = a1->functions->NewStringUTF(a1, v59);
}

根据函数特征(两次哈希、拼接操作)和后续分析,可以判断 j_getByteHash 极可能是 HMAC-SHA256 的实现。

第七步:动态验证算法

为了验证静态分析的结论,使用 Frida 进行动态 Hook,直接拦截 getByteHash 函数的输入和输出。

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));
    }
});

运行脚本后,每次发起请求,Frida 都会打印出传入 getByteHash 的数据、密钥以及计算出的哈希值。对比抓包得到的 api_sign,确认了该函数就是签名的最终生成点。

第八步:总结调用链与算法

至此,完整的逆向路径已经清晰。我将整个调用链整理如下,这构成了一个通用的 Android App 签名逆向分析框架:

抓包发现 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 → HMAC-SHA256 算法实现

最终算法:分析确认,该签名算法为 HMAC-SHA256。密钥被硬编码在 SO 文件的 .rodata 数据段中,通过逆向工具可以提取。

延伸思考:安全性与对抗

从安全研究的角度来看,这次分析暴露出一些常见问题:

  1. 密钥硬编码:将密钥固化在客户端是高风险行为。一旦 SO 文件被逆向提取,签名即可被伪造。更好的做法是使用动态密钥、白盒加密或依托于 TEE 安全环境。
  2. 混淆强度:虽然使用了反射和 JNI 增加分析难度,但整体调用链依然清晰可循。更高级的混淆(如控制流扁平化、虚拟机保护)或运行时完整性校验能进一步提升对抗能力。
  3. 防御视角:作为开发者,除了加固客户端,更应在服务端实施二次校验机制,例如结合时间戳、随机数(Nonce)和请求频率限制,即使签名被破解,也能有效阻止重放攻击。

这次从抓包到 SO 层的完整追踪,不仅还原了一个具体的签名算法,更展示了一套可复用的逆向工程分析方法论。无论是分析其他签名算法,还是研究更复杂的加密协议,其核心思路——从外到内、从动态到静态、从高层语言到底层实现——都是相通的。


合规提示:本技术分析仅用于学习交流与安全研究,旨在提升防御能力。所有分析过程均在合法授权或本地测试环境下进行,涉及的应用程序信息均已脱敏。请严格遵守《网络安全法》等相关法律法规,切勿将技术用于非法用途。




上一篇:CIO制定IT战略规划时需警惕的三个常见误区
下一篇:Claude Code源代码因npm发布失误泄露,Anthropic面临严重安全事件
您需要登录后才可以回帖 登录 | 立即注册

手机版|小黑屋|网站地图|云栈社区 ( 苏ICP备2022046150号-2 )

GMT+8, 2026-4-9 16:26 , Processed in 0.579456 second(s), 42 queries , Gzip On.

Powered by Discuz! X3.5

© 2025-2026 云栈社区.

快速回复 返回顶部 返回列表