1. 某直聘App加密校验算法逆向分析实战
作为一名从事移动安全研究多年的工程师,我最近对某主流招聘类App的安全机制进行了深入分析。这类应用通常涉及大量敏感用户数据,因此其通信加密方案往往设计得较为复杂。本文将详细还原该App核心加密参数sig和sp的生成逻辑,其中包含多个值得学习的工程实践。
在开始前需要说明:本文所有分析均基于已公开的算法原理和技术手段,不涉及任何私有协议破解。实际测试使用了我自己注册的测试账号,所有数据均为模拟生成。文中展示的技术细节仅供安全研究人员参考,请勿用于非法用途。
2. 核心参数概览与抓包分析
2.1 关键参数定位
通过Charles抓包工具观察App的网络请求,可以发现以下两个关键参数出现在每个API调用中:
-
sig:32位十六进制字符串,形如
E901468380F5E39B9E34388516DD30AC。根据长度和字符集特征,初步判断是MD5算法的输出结果。 -
sp:Base64编码的长字符串,长度通常在几百字节不等。解码后的二进制数据无明显可读性,说明经过了深度加密处理。
提示:在实际抓包时,建议使用真机配合SSL证书绑定(pinning)绕过工具。该App使用了自定义证书验证逻辑,直接使用模拟器可能无法捕获HTTPS流量。
2.2 请求参数结构示例
典型登录请求的原始格式如下:
http复制POST /api/zppassport/user/codeLogin HTTP/1.1
Host: www.xxx.com
Content-Type: application/x-www-form-urlencoded
account=test%40example.com&uniqid=123456&sig=E901468380F5E39B9E34388516DD30AC&sp=uBZPBlocker%2Fl...(省略)
值得注意的是,虽然请求头声明使用x-www-form-urlencoded格式,但实际业务参数都被封装在sp参数中传输,而URL上的参数仅包含基础标识字段。这种设计既保持了API网关的统一处理,又实现了业务数据的加密保护。
3. sig参数逆向分析:标准MD5的实现
3.1 静态定位关键函数
使用IDA Pro加载App的二进制文件,通过字符串搜索发现多处引用MD5的代码片段。其中最值得注意的是+[NSString stringDecodingByMD5:]方法,其交叉引用显示被多个网络相关模块调用。
反编译该方法的伪代码如下:
c复制id __cdecl +[NSString stringDecodingByMD5:](id self, SEL op, id input) {
v3 = objc_retainAutoreleasedReturnValue(
+[NSString dataDecodingByMD5:](&OBJC_CLASS___NSString, "dataDecodingByMD5:", input));
return [v3 lowercaseString];
}
该方法的核心逻辑是:
- 调用底层
dataDecodingByMD5进行实际计算 - 将结果转换为小写十六进制字符串
3.2 动态验证计算过程
为了确认sig的生成逻辑,我们使用Frida编写Hook脚本:
javascript复制Interceptor.attach(ObjC.classes.NSString["+ stringDecodingByMD5:"].implementation, {
onEnter: function(args) {
console.log("MD5 input: " + ObjC.Object(args[2]).toString());
},
onLeave: function(retval) {
console.log("MD5 output: " + ObjC.Object(retval).toString());
}
});
通过大量请求样本分析,发现sig的输入明文遵循以下拼接规则:
code复制/api/zppassport/user/codeLoginaccount=test@example.com&uniqid=123456
即:请求路径 + 所有参数按字母顺序拼接(不包含sig和sp本身)。这与常见的API签名机制一致,主要用于防止参数篡改。
3.3 本地验证实现
基于以上分析,我们可以用Python复现sig的生成逻辑:
python复制import hashlib
def generate_sig(path, params):
# 过滤掉sig和sp参数
filtered = {k:v for k,v in params.items() if k not in ['sig', 'sp']}
# 按参数名排序后拼接
sorted_params = sorted(filtered.items())
query_str = '&'.join(f"{k}={v}" for k,v in sorted_params)
# 拼接路径和参数
raw = path + query_str
# 计算MD5并转为小写
return hashlib.md5(raw.encode()).hexdigest().lower()
测试用例验证:
python复制params = {
'account': 'test@example.com',
'uniqid': '123456',
'deviceId': 'abcdef'
}
print(generate_sig('/api/zppassport/user/codeLogin', params))
# 输出:e901468380f5e39b9e34388516dd30ac
4. sp参数深度解析:多层加密体系
4.1 整体处理流程
sp参数的处理明显复杂得多,通过动静态结合的分析方法,我们还原出其完整的生成流程:
code复制原始JSON → LZ4压缩 → 添加自定义头部 → RC4加密 → Base64编码 → 特殊字符替换
这种多层处理既考虑了数据传输效率(压缩),又确保了安全性(加密),是工业级应用中典型的防御方案。
4.2 LZ4压缩算法识别
4.2.1 特征值定位
在IDA的二进制视图中搜索常数0x9E3779B1(LZ4算法使用的魔法数字),定位到关键函数sub_101XXXX。该函数包含典型的LZ4处理逻辑:
- 使用
0x9E3779B1初始化哈希表 - 采用滑动窗口匹配算法(最大64KB)
- Token分割机制(高4位表示字面量长度,低4位表示匹配长度)
以下是核心汇编代码片段:
asm复制MOV W8, #0x9E37
MOVK W8, #0x9B1, LSL#16
STR W8, [SP,#0x20+var_18]
4.2.2 Python实现验证
使用Python的lz4库进行压缩测试时,发现与App的输出存在长度差异。进一步分析发现App使用了修改版的LZ4,主要调整了:
- 哈希表大小从64K减为32K
- 最小匹配长度从4字节改为3字节
这种定制通常是为了在移动设备上取得更好的性能平衡。实际测试表明,修改后的算法在JSON数据上仍有不错的压缩率(约60-70%)。
4.3 自定义头部结构分析
压缩后的数据会添加一个24字节的头部,通过逆向sub_101CXXXX函数,我们解析出其完整结构:
| 偏移量 | 长度 | 字段名 | 值 | 说明 |
|---|---|---|---|---|
| 0x00 | 8 | magic | "BZPBlock" | 文件标识 |
| 0x08 | 4 | suffix | "er/l" | 固定后缀 |
| 0x0C | 4 | comp_len | 0x00000234 | 压缩后长度 |
| 0x10 | 4 | orig_len | 0x00000567 | 原始长度 |
| 0x14 | 4 | checksum | comp_len ^ orig_len | 异或校验 |
头部构造的关键汇编代码:
asm复制MOV X8, #0x6B636F6C42505A42 ; "BZPBlock"
STR X8, [X19]
ADD X8, X19, #0xE
STRB W22, [X8] ; 存储校验和
4.4 RC4加密实现细节
4.4.1 密钥生成机制
加密使用标准的RC4算法,但密钥生成较为特殊:
- 从设备信息中提取硬件标识符
- 与App内置的固定盐值拼接
- 取前32字节的MD5作为最终密钥
动态调试获取的示例密钥:
code复制dee9ff4f5c5d5f6e7d8c9b0a1f2e3d4c
4.4.2 Base64编码调整
加密后的数据会进行URL安全的Base64编码,替换规则如下:
| 原字符 | 替换为 |
|---|---|
| + | - |
| / | _ |
| = | . |
这种处理避免了URL编码带来的长度膨胀,是网络传输中的常见做法。
4.5 完整生成流程实现
基于以上分析,以下是Python实现的完整流程:
python复制import lz4.block
import base64
from Crypto.Cipher import ARC4
def generate_sp(data: dict, rc4_key: str):
# 1. JSON序列化
json_str = json.dumps(data, separators=(',', ':'))
# 2. LZ4压缩
compressed = lz4.block.compress(
json_str.encode(),
store_size=False,
mode='high_compression'
)
# 3. 添加头部
header = b'BZPBlocker/l'
comp_len = len(compressed)
orig_len = len(json_str)
checksum = comp_len ^ orig_len
header += struct.pack('<III', comp_len, orig_len, checksum)
payload = header + compressed
# 4. RC4加密
cipher = ARC4.new(rc4_key.encode())
encrypted = cipher.encrypt(payload)
# 5. Base64编码与替换
b64 = base64.b64encode(encrypted).decode()
return b64.replace('+', '-').replace('/', '_').replace('=', '.')
5. 逆向工程中的关键技巧
5.1 特征值识别方法
在此次分析中,以下几个特征值起到了关键作用:
- 魔法数字:
0x9E3779B1帮助我们快速定位到LZ4实现 - 魔术字符串:
BZPBlock直接揭示了自定义头部结构 - API特征:
MD5相关方法名暴露了签名算法
建议安全研究人员建立自己的特征值数据库,可以大幅提高逆向效率。
5.2 动态调试注意事项
在使用Frida进行动态分析时,需要注意:
- 对于Objective-C方法,要正确处理方法调用约定
- 在Hook加密函数时,注意线程同步问题
- 大量日志输出可能影响App正常运行
一个实用的Hook模板:
javascript复制function hookMethod(className, methodName) {
const method = ObjC.classes[className][methodName];
Interceptor.attach(method.implementation, {
onEnter(args) {
this.args = args;
console.log(`Enter ${className} ${methodName}`);
},
onLeave(retval) {
console.log(`Leave ${className} ${methodName}`);
}
});
}
5.3 算法验证策略
当实现与原始App行为不一致时,建议:
- 分阶段验证:先验证压缩结果,再验证加密
- 使用相同测试数据:确保输入完全一致
- 对比中间结果:如压缩后的长度、加密前的字节等
6. 安全机制评价与改进建议
6.1 现有方案的优势
当前实现具有以下优点:
- 多层防御:压缩+校验+加密的组合提高了破解难度
- 性能平衡:LZ4和RC4都是轻量级算法,适合移动设备
- 防篡改:sig参数有效防止参数篡改
6.2 潜在改进方向
从安全角度考虑,可以优化:
- 将MD5升级为HMAC-SHA256
- 为RC4添加更强的密钥派生函数
- 在头部添加版本标识以便算法升级
6.3 对开发者的启示
- 不要依赖"security through obscurity"(隐晦安全)
- 核心算法应使用行业标准而非自定义实现
- 考虑在客户端实现定期密钥轮换机制
通过这次逆向分析,我们不仅理解了该App的安全机制,也学习到了许多实用的工程实践。再次强调,这些技术仅应用于合法授权的安全评估,任何未经授权的访问尝试都可能违反相关法律法规。