【逆向】Android App soul api-sign算法分析

〇·免责声明

本篇文章仅用于学习交流,切勿用于违法犯罪,如使用者违法相关法律与本文作者无关!

一·背景

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
由于 这几个类引用的错综复杂 加上混淆的比较严重 关键地方 请求参数的处理 并没有彻底搞清楚。
以上分析 有 其他见解的朋友 可以私信我 或 此帖留言讨论

  • 2
    点赞
  • 11
    收藏
    觉得还不错? 一键收藏
  • 20
    评论
评论 20
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值