1. 项目背景与核心挑战
最近在分析某出行平台客户端通信协议时,遇到了一个名为"wsgsig"和"secdd-challenge"的安全校验机制。这类签名算法通常用于防止API接口被恶意调用,是移动端安全防护体系中的重要组成部分。作为从事逆向分析多年的工程师,我决定深入剖析这套机制的实现原理和破解思路。
这个签名机制的核心在于,客户端每次发起网络请求时,都会生成一个动态变化的加密字符串(即wsgsig),服务端通过验证该签名的有效性来判断请求是否合法。而secdd-challenge则是服务端下发的一种挑战机制,用于增强交互过程的安全性。这两个参数共同构成了该平台API调用的"通行证"。
2. 技术原理深度解析
2.1 wsgsig签名生成机制
通过动态调试和代码分析,发现wsgsig的生成主要包含以下几个关键步骤:
-
参数收集阶段:
- 收集请求URL的path部分
- 获取当前时间戳(精确到秒)
- 提取设备唯一标识符
- 收集请求参数并按照特定规则排序
-
字符串拼接阶段:
所有参数按照key=value的形式用&连接,形成一个待签名字符串。例如:code复制device_id=123×tamp=1688888888&path=/api/v3/userinfo -
加密处理阶段:
使用HMAC-SHA256算法对拼接后的字符串进行加密,密钥是一个硬编码在so文件中的固定字符串。加密后的二进制数据再经过Base64编码,最终生成wsgsig值。
关键发现:在最新版本中,密钥不再是简单的静态字符串,而是通过运行时动态解密获得,这增加了逆向难度。
2.2 secdd-challenge响应机制
secdd-challenge是服务端下发的动态校验机制,其工作流程如下:
- 客户端首次请求时,服务端返回一个包含
secdd-challenge字段的响应 - 客户端需要解析该字段,提取其中的算法标识和参数
- 根据算法要求执行特定计算(如哈希、位移运算等)
- 将计算结果附加到后续请求的header中
常见的challenge算法包括:
- 简单的字符串反转
- 基于时间戳的异或运算
- 多段字符串拼接后取MD5
3. 逆向分析实战过程
3.1 定位关键代码位置
使用Frida进行动态注入,通过拦截网络请求库的调用,最终定位到签名生成的核心函数位于libsecurity.so中,函数伪代码如下:
c复制void generate_wsgsig(
char *result,
const char *path,
const char *query_params,
int64_t timestamp
) {
char buffer[1024];
// 参数排序和拼接
sort_and_concat_params(buffer, path, query_params);
// 获取动态密钥
char *key = get_dynamic_key();
// HMAC-SHA256计算
unsigned char hmac[32];
HMAC(EVP_sha256(), key, strlen(key),
(unsigned char*)buffer, strlen(buffer),
hmac, NULL);
// Base64编码
base64_encode(hmac, 32, result);
}
3.2 动态密钥获取分析
新版本中密钥不再硬编码,而是通过以下方式动态获取:
- 从assets加载加密的密钥数据
- 使用运行时生成的AES密钥解密
- 解密密钥的生成与设备指纹相关
通过hookget_dynamic_key函数,可以dump出内存中的真实密钥。测试发现同一版本app在不同设备上运行时,密钥确实会发生变化。
3.3 challenge算法破解
针对secdd-challenge,主要采用以下分析方法:
- 收集大量含challenge的请求-响应对
- 通过差分分析找出输入输出之间的关系
- 使用Frida重放算法验证猜测
例如发现一种典型算法实现:
javascript复制function solveChallenge(challenge) {
const parts = challenge.split('|');
const str = parts[0] + parts[2].substr(0, 4);
return md5(str).substr(8, 16);
}
4. 完整请求构造方案
基于上述分析,构建合法请求需要以下步骤:
-
初始化阶段:
- 获取设备指纹信息(IMEI、Android ID等)
- 加载并解密动态密钥
-
请求构造阶段:
python复制def build_request(url, params): # 1. 处理基础参数 timestamp = int(time.time()) sorted_params = sort_params(params) # 2. 生成wsgsig to_sign = f"path={url.path}&{urlencode(sorted_params)}" hmac = hmac_sha256(secret_key, to_sign) wsgsig = base64.b64encode(hmac).decode() # 3. 添加headers headers = { "x-wsgsig": wsgsig, "x-timestamp": str(timestamp), "user-agent": "DidiApp/5.8.1" } # 4. 处理可能的challenge if last_challenge: solution = solve_challenge(last_challenge) headers["x-secdd-solution"] = solution return Request(url, params, headers) -
挑战处理阶段:
python复制def solve_challenge(challenge): algorithm, *args = challenge.split(':') if algorithm == "v1": return md5(args[0] + args[2][:4])[8:24] elif algorithm == "v2": return xor_rotate(args[0], int(args[1])) ...
5. 常见问题与解决方案
5.1 签名无效(403错误)
可能原因及解决方案:
- 时间不同步:确保设备时间与服务器时间误差在30秒内
- 参数顺序错误:严格按照字母序排列参数
- 密钥版本不匹配:检查so文件是否更新导致密钥生成逻辑变化
5.2 challenge验证失败
典型表现:
- 请求返回412状态码
- 响应中包含新的challenge
解决方法:
- 确认算法识别正确
- 检查字符串处理细节(如substr位置、编码格式等)
- 使用真实设备生成的数据作为参考
5.3 设备指纹被封禁
预防措施:
- 避免高频使用相同设备指纹
- 模拟真实设备的指纹生成模式
- 定期更新设备信息数据库
6. 进阶技巧与优化建议
-
性能优化:
- 预计算常用参数的哈希值
- 缓存解密后的密钥
- 使用C++扩展提高加密计算速度
-
反检测策略:
- 随机化请求时间间隔
- 模拟真实客户端的请求头顺序
- 添加合理的"噪声"参数
-
长期维护方案:
- 建立自动化特征检测系统
- 监控接口返回的变化
- 维护算法版本与app版本的映射关系
在实际项目中,我们发现这套安全机制大约每3-4个月会有一次较大更新,主要变化集中在:
- 密钥生成逻辑的修改
- 新增challenge算法类型
- 签名参数列表的调整
保持对so文件的定期逆向分析,是长期稳定运行的关键。建议每次app大版本更新后,立即进行以下检查:
- 对比新旧版so文件的导出函数
- 跟踪密钥相关函数的调用链
- 收集新版challenge样本进行分析