小c惠生活 APP 请求签名算法逆向分析记录

这次主要分析的是小c惠生活 APP 的接口请求签名。目标不是改 APP,也不是做功能,只是单纯学习它的请求头动态参数是怎么生成的。抓包时发现,请求里有几个比较关键的动态 Header:
X-Ashe
X-Nami
X-Garen这几个字段每次请求都会变化,所以一开始直接重放请求会失败。
一、关键动态字段经过抓包和 Hook 分析,最终确认:
X-Garen = 当前时间戳,毫秒级
X-Nami = 随机字符串
X-Ashe = 请求签名其中真正核心的是 X-Ashe。X-Ashe 依赖三个东西:
servername
methodname
时间戳
随机字符串
二、签名原始数据APP 请求里会带两个业务字段:
servername
methodname例如某个接口可能类似:
servername = SilkwormFusion
methodname = FusionService.GetFeedPromotions签名前会先把它们拼接成:
servername + “.” + methodname然后统一转成小写。例如:
SilkwormFusion.FusionService.GetFeedPromotions会变成:
silkwormfusion.fusionservice.getfeedpromotions
三、X-Ashe 签名算法最终还原出来的算法大概是:
lowerCase = (servername + “.” + methodname).toLowerCase()

step1 = MD5(lowerCase)

X-Ashe = MD5(step1 + timestamp + randomStr)也就是:
X-Ashe = MD5(
MD5((servername + “.” + methodname).toLowerCase())
+ X-Garen
+ X-Nami
)其中:
X-Garen = timestamp
X-Nami = randomStr
四、伪代码表示用伪代码表示就是:
String method = (servername + “.” + methodname).toLowerCase(Locale.ROOT);

String firstMd5 = md5(method);

String timestamp = String.valueOf(System.currentTimeMillis());

String randomStr = randomStr();

String xAshe = md5(firstMd5 + timestamp + randomStr);

headers.put(“X-Ashe”, xAshe);
headers.put(“X-Nami”, randomStr);
headers.put(“X-Garen”, timestamp);
五、Python 版本复现Python 里可以这样表示:
import time
import hashlib
import random
import string

def md5(s: str) -> str:
return hashlib.md5(s.encode(“utf-8”)).hexdigest()

def random_str(length: int = 17) -> str:
chars = string.ascii_letters + string.digits
return “”.join(random.choice(chars) for _ in range(length))

def make_sign(servername: str, methodname: str):
timestamp = str(int(time.time() * 1000))
nami = random_str()

method = f”{servername}.{methodname}”.lower()

first_md5 = md5(method)

ashe = md5(first_md5 + timestamp + nami)

return {
“X-Ashe”: ashe,
“X-Nami”: nami,
“X-Garen”: timestamp,
}

headers = make_sign(
“SilkwormFusion”,
“FusionService.GetFeedPromotions”
)

print(headers)
六、随机字符串规则X-Nami 是随机字符串,但这里还有一个细节。未登录状态和登录状态下,随机字符串生成方式不完全一样。1. 未登录状态未登录时,大概是生成一个 UUID,然后去掉 -,再截取指定长度。类似:
UUID 去掉横杠后,取前 17 位伪代码:
String randomStr = UUID.randomUUID()
.toString()
.replace(“-“, “”)
.substring(0, 17);
2. 登录状态登录状态下,会先生成一个随机串,然后把用户相关 ID 插入到随机串中间。大概逻辑是:
生成 16 位随机字符串
取用户 silk_id
把 silk_id 插入到随机串中间
得到最终 randomStr伪代码类似:
String base = random16();
String silkId = getSilkId();

int index = (base.length() – silkId.length()) / 2;

String randomStr =
base.substring(0, index)
+ silkId
+ base.substring(index);也就是说,登录后的 X-Nami 不是纯随机,它里面混入了用户 ID 相关信息。
七、完整算法总结整体签名流程可以总结为:
1. 取 servername 和 methodname
2. 拼接成 servername.methodname
3. 转小写
4. 对拼接结果做第一次 MD5
5. 获取当前毫秒时间戳,作为 X-Garen
6. 生成随机字符串,作为 X-Nami
7. 拼接:第一次 MD5 + 时间戳 + 随机字符串
8. 对拼接结果再做一次 MD5,得到 X-Ashe最终公式:
X-Garen = 当前毫秒时间戳

X-Nami = randomStr

X-Ashe = MD5(
MD5(
lower(servername + “.” + methodname)
)
+ X-Garen
+ X-Nami
)
八、逆向定位过程这几个字段一开始只能通过抓包看到结果:
X-Ashe
X-Nami
X-Garen但是抓包看不到生成过程。所以后面主要 Hook 了请求构造链路,包括:
OkHttp Request
Headers.Builder.add
RequestBody
Ktor 请求封装
请求拦截器
时间戳生成
随机字符串生成
MD5 方法最后定位到签名是在 Java/Kotlin 层生成的,不是在 native 层最终计算的。中途也看过 so 层,比如 libkeygen.so,但是主签名逻辑不在那里。
九、结论这个 APP 的请求签名核心不复杂,主要是双层 MD5:
第一次 MD5:对 servername.methodname 小写后加密
第二次 MD5:第一次结果 + 时间戳 + 随机串真正麻烦的是定位过程。因为请求最后发出去之前,中间经过了多层封装:
业务请求对象
请求参数封装
公共 Header 添加
动态 Header 生成
OkHttp / Ktor 网络层所以单纯抓包只能看到结果,想还原算法还是要顺着请求构造流程往上追。这次的重点收获是:
不要只盯着 URL
要重点看 servername、methodname 和动态 Header最终还原出来的核心公式就是:
X-Ashe = MD5(MD5(lower(servername + “.” + methodname)) + X-Garen + X-Nami)

© 版权声明
THE END
喜欢就支持一下吧
点赞6 分享
评论 抢沙发

请登录后发表评论

    暂无评论内容