〇·免责声明
本篇文章仅用于学习交流,切勿用于违法犯罪,如使用者违法相关法律与本文作者无关!
一·背景
soul 这款app 还是有点意思的,作为上帝视角的程序员来说 怎么能不尝试开启金手指模式。
二·反编译
本篇文章主要是对api-sign算法的分析,反编译步骤请参考网络其他文章。
三·算法分析
请求接口:https://api.soulapp.cn/v3/post/praise?postId=294348164
请求头:
api-sign:853F2B036105103D5AA7EB43AF94EDC4A0B88F31
os: android
X-Auth-Token: zVbDmdFooc7Ejn9xJer6puAEm8/xtZ51
api-sign-version: v4
device-id:AAAAAAAAAAAAAA
request-nonce:8051d3f7b4944ea380eb8984890b0d67
Connection: close
app-id: 10000003
app-version: 3.0.17
app-time:1540286088314
host: api.soulapp.cn
User-Agent: [WIFI;PAFM00;Android;27;1080*2340;100021;zh_CN]
请求头有误 会提示如下错误消息:
{
"code": 9000006,
"message": "网络错误,联系官方微信Soulandu解决吧!",
"data": null,
"success": false
}
当前分析版本:cn.soulapp.android_3.0.18_18101801
三个 classes.dex 我们主要用到 classes.dex 和 classes3.dex
classes.dex package cn.soulapp.android.api.b 里 有 方法主要用于构造请求头:
private static void b(final aa$a aa$a, final aa aa) {
final SoulApp b = SoulApp.b();
//当前时间戳 cn.soulapp.android.api.a.a(); 可以忽略
final long n = System.currentTimeMillis() - cn.soulapp.android.api.a.a();
// 随机生产UUID 并删除 UUID中的-
final String replaceAll = UUID.randomUUID().toString().replaceAll("-", "");
//无用 暂理解为 记录日志
j.b("requestNonce" + replaceAll, new Object[0]);
//api-sign 生成算法
aa$a.b("api-sign", cn.soulapp.android.api.b.c.a(aa, replaceAll, n));
//手机系统标识
aa$a.b("os", "android");
final String f = cn.soulapp.android.c.f();
//token
if (!TextUtils.isEmpty((CharSequence)f)) {
aa$a.b("X-Auth-Token", f);
}
//api-sign 维护了一个算法的版本 当前算法版本是v4
aa$a.b("api-sign-version", "v4");
//设备号
aa$a.b("device-id", UTDevice.getUtdid((Context)b));
//UUID
aa$a.b("request-nonce", replaceAll);
aa$a.b("Connection", "close");
aa$a.b("app-id", "10000003");
aa$a.b("app-version", "3.0.18");
// 当前时间戳 ,请求接口会对时间做校验 如果 和当前时间不符 会返回错误
aa$a.b("app-time", String.valueOf(n));
while (true) {
try {
if (!TextUtils.isEmpty((CharSequence)cn.soulapp.android.api.a.a(aa.a().i()))) {
aa$a.b("host", cn.soulapp.android.api.a.a(aa.a().i()));
}
aa$a.b("User-Agent", cn.soulapp.android.c.a("[" + l.b((Context)b) + ";" + Build.MODEL + ";Android;" + Build$VERSION.SDK_INT + ";" + cn.soulapp.lib.basic.utils.u.c() + "*" + cn.soulapp.lib.basic.utils.u.d() + ";" + k.a(h.a((Context)b)) + ";" + Locale.getDefault().toString() + ";" + q.a((Context)b) + "]"));
aa$a.b("app-info", "[" + q.d() + "]");
}
catch (Exception ex) {
continue;
}
break;
}
}
然后 分析 cn.soulapp.android.api.b.c.a(aa, replaceAll, n) 生成 sign的算法 代码在 package cn.soulapp.android.api.b;下
public class c
{ //格式化当前时间戳
private static String a(final long n) {
return e.a(n, "Asia/Shanghai", "yyyyMMddHHmm");
}
public static String a(aa a, final String s, final long n) {
//拼接调用加密算法前的参数
final StringBuilder sb = new StringBuilder();
// a.a().a() 部分 是 package okhttp3; 里的HttpUrl 类 return new URL(this.t); 获取当前请求地址
// if (this.query != null) {this.file = this.path + "?" + this.query; } 有一个方法是这么写的 ,path+?+请求参数
所以这里的Path 理解为接口的这部分:https://api.soulapp.cn/v3/post/praise
sb.append(a.a().a().getPath());
final HashMap<Object, CharSequence> hashMap = new HashMap<Object, CharSequence>();
final ArrayList<String> list = new ArrayList<String>();
int i = 0;
try {
//这里略复杂 下面详细说一下 ,刚开始我理解为 只是 请求参数的拼接 后来仔细看代码 可能对 请求参数做了复杂处理
//可以确认这里是参数出掉等于号后的拼接
while (i < ((s)a.d()).a()) {
final s s2 = (s)a.d();
final String a2 = s2.a(i);
hashMap.put(a2, s2.c(i));
list.add(a2);
++i;
}
final String[] array = list.toArray(new String[0]);
//排序算法 理解为主要是请求参数 需要按先后顺序传参 如:https://api.soulapp.cn/v3/post/praise?b=2&a=1
则这里的 array 会调整拼接顺序为 a=1b=2
Arrays.sort(array, String.CASE_INSENSITIVE_ORDER);
if (hashMap.size() != 0) {
for (final String s3 : array) {
if (!ca.a(hashMap.get(s3))) {
//如果 参数 是中文的话 进行utf-8编码
sb.append(s3).append(URLDecoder.decode(hashMap.get(s3), "Utf-8"));
}
}
}
}
//出异常的处理方式
catch (Exception ex) {
a = (aa)a.a();
if (((HttpUrl)a).q() > 0) {
int k = 0;
while (k < ((HttpUrl)a).q()) {
final String a3 = ((HttpUrl)a).a(k);
final String b = ((HttpUrl)a).b(k);
Label_0307: {
if (ca.a(a3) || ca.a(b)) {
break Label_0307;
}
try {
sb.append(a3).append(URLDecoder.decode(b.replaceAll("%(?![0-9a-fA-F]{2})", "%25"), "utf-8"));
++k;
}
catch (Exception ex2) {
try {
sb.append(a3).append(b);
}
catch (Exception a3) {}
}
}
}
}
}
//设备号
sb.append(UTDevice.getUtdid((Context)SoulApp.b()));
//appid
sb.append("10000003");
//写死的一个key
sb.append(SoulApp.b().a().getAuthKey());
//时间格式化
sb.append(a(n));
sb.append(s);
//版本号 替换掉.
sb.append("3.0.18".replaceAll("\\.", ""));
j.a((Object)("genSign = :" + sb.toString()));
//拼接好 参数 进行 SHA1 加密
return f.b(sb.toString()).toUpperCase();
}
}
然后说一下 请求参数 处理的部分
int i = 0;
try {
while (i < ((s)a.d()).a()) {
final s s2 = (s)a.d();
final String a2 = s2.a(i);
hashMap.put(a2, s2.c(i));
list.add(a2);
++i;
}
1`(s)a.d()).a() 返回的是当前请求参数 数量
2· final s s2 = (s)a.d(); 返回的是 一个 s 它继承 ab 抽象类
3·s 的代码在 classes3.dex package okhttp3; s.class
这个类里维护了一个 参数列表
this.b = okhttp3.internal.c.a(list);
this.c = okhttp3.internal.c.a(list2);
参数列表的数据是这么来的:
this.a.add(HttpUrl.a(s, " \"':;<=>@[]^`{}|/\\?#&!$(),~", true, false, true, true));
this.b.add(HttpUrl.a(s2, " \"':;<=>@[]^`{}|/\\?#&!$(),~", true, false, true, true));
我们看一下 HttpUrl.a()方法做了什么,代码在classes.dex package okhttp3;
static String a(String s, final int n, final int n2, final String s2, final boolean b, final boolean b2, final boolean b3, final boolean b4) {
int codePoint;
for (int i = n; i < n2; i += Character.charCount(codePoint)) {
codePoint = s.codePointAt(i);
if (codePoint < 32 || codePoint == 127 || (codePoint >= 128 && b4) || s2.indexOf(codePoint) != -1 || (codePoint == 37 && (!b || (b2 && !a(s, i, n2)))) || (codePoint == 43 && b3)) {
final c c = new c();
c.a(s, n, i);
a(c, s, i, n2, s2, b, b2, b3, b4);
s = c.s();
return s;
}
}
s = s.substring(n, n2);
return s;
}
c.a(s, n, i); 方法在 package okio; c.class
由于 这几个类引用的错综复杂 加上混淆的比较严重 关键地方 请求参数的处理 并没有彻底搞清楚。
以上分析 有 其他见解的朋友 可以私信我 或 此帖留言讨论