[逆向]某校园跑软件签名算法分析

浮夸小生。
2021-11-30 / 0 评论 / 489 阅读 / 正在检测是否收录...
温馨提示:
本文最后更新于2021年11月30日,已超过1103天没有更新,若内容或图片失效,请留言反馈。

前言

同学在群中分享某虚拟定位轻松跑步法。

作为旁观者,萌生出模拟请求进行数据伪造来完成任务(老操作了,像骨灰盒自动参加赠楼这种)

某校园跑软件

其实要求还算可以,极限情况:50天、每天1公里5分钟;就是不太好坚持

逆向 某校园跑软件签名算法分析

分析思路

抓包,看请求是否有签名,没有就去第4步
检测安装包是否有壳,无壳就去第4步
脱壳
分析代码
伪造请求
等待正义的审判 表情

一、先抓包(非重点,不过多赘述)

token 理论上是用户标识

appkey 肯定大部分时间为定值

sign MD5签名

逆向 某校园跑软件签名算法分析

二、查壳

360加固,一般 #(cos滑稽)

逆向 某校园跑软件签名算法分析

三、脱壳

这里使用这个工具:

CodingGay/BlackDex: BlackDex is an Android unpack(dexdump) tool, it supports Android 5.0~12 and need not rely to any environment. BlackDex can run on any Android mobile phone or emulator, you can unpack APK File in several seconds. (github.com)

下载32位版本安装包(因为,目标APP是32位的)

逆向 某校园跑软件签名算法分析

单击目标应用,开始脱壳

逆向 某校园跑软件签名算法分析

四、分析目标“源码”位置

打开脱壳后的dex文件所在目录,

使用MT管理器 的 dex编辑器++ 打开

搜索字符串 sign

逆向 某校园跑软件签名算法分析

只看目标应用的包

逆向 某校园跑软件签名算法分析

看包名:

entities 实体包,排除
enums 枚举包,排除
network.settings 网络设置包 且类名”MyInterceptor”与拦截相关, 大概率符合条件

初步查看源码,在请求头中添加sign,肯定是这里了

接下来,使用电脑进行具体签名流程分析了
逆向 某校园跑软件签名算法分析

五、分析代码逻辑

分析请看注释

public final class MyInterceptor implements Interceptor {
    private static final String APPKEY = "389885588s0648fa";
    private static final String APPSECRET = "56E39A1658455588885690425C0FD16055A21676";
    private static final Charset ChartSet_UTF8 = Charset.forName("UTF-8");
    public static final Companion Companion = new Companion((DefaultConstructorMarker) null);
 
    public MyInterceptor() {
    }
 
    static {
    }
 
    public Response intercept(Interceptor.Chain chain) {
        boolean z;
        String str;
        Request.Builder newBuilder;
        Request.Builder addHeader;
        Request.Builder addHeader2;
        Request.Builder newBuilder2;
        Request.Builder addHeader3;
        Request.Builder addHeader4;
        Request.Builder addHeader5;
        String str2;
        String str3;
        boolean z2;
        String str4;
        String str5;
        String str6;
        String str7;
        Set queryParameterNames;
        Interceptor.Chain chain2 = chain;
        Intrinsics.checkParameterIsNotNull(chain2, "chain");
        Request request = chain.request();
        // 注意,TreeSet是有序集合,且默认为正序排列,即{a, b, c, d [,...]}
        TreeSet treeSet = new TreeSet();
        Request request2 = null;
        HttpUrl url = request != null ? request.url() : null;
        if (!(url == null || (queryParameterNames = url.queryParameterNames()) == null)) {
            treeSet.addAll(queryParameterNames);
        }
        // 待签名字符串
        StringBuilder sb = new StringBuilder();
        Iterator it = treeSet.iterator();
        // 开始迭代所有请求参数
        while (true) {
            z = false;
            if (!it.hasNext()) {
                break;
            }
            // str8 为参数名
            String str8 = (String) it.next();
            // 获取参数名对应的值
            List queryParameterValues = url != null ? url.queryParameterValues(str8) : null;
            if (queryParameterValues != null && (true ^ queryParameterValues.isEmpty())) {
                // 参数值非空
                // 取参数值列表第一个
                String str9 = (String) (url != null ? url.queryParameterValues(str8) : null).get(0);
                if (!TextUtils.isEmpty(str9)) {
                    // str8 参数名, str9 参数值
                    // str9非空,追加str8 str9在sb之后
                    sb.append(str8);
                    sb.append(str9);
                }
            }
        }
        // 追加APPKEY
        sb.append("389885588s0648fa");
        // 追加APPSECRET
        sb.append("56E39A1658455588885690425C0FD16055A21676");
        if (request.body() != null) {
            BufferedSink buffer = new Buffer();
            RequestBody body = request.body();
            if (body != null) {
                body.writeTo(buffer);
            }
            Charset charset = ChartSet_UTF8;
            RequestBody body2 = request.body();
            MediaType contentType = body2 != null ? body2.contentType() : null;
            if (contentType != null) {
                // 追加请求体
                sb.append(buffer.readString(contentType.charset(charset)));
            }
        }
        // 同sb
        String sb2 = sb.toString();
        Intrinsics.checkExpressionValueIsNotNull(sb2, "signStr.toString()");
        CharSequence charSequence = sb2;
        if (!(charSequence == null || charSequence.length() == 0)) {
            // 本级语句块替换一些字符为空字符,即删除部分字符,这将导致参与MD5运算的字符串有所差异
            // z2 为是否发生过替换操作
            // 删除空格
            if (StringsKt.contains$default(charSequence, " ", false, 2, (Object) null)) {
                str3 = StringsKt.replace$default(sb2, " ", "", false, 4, (Object) null);
                z2 = true;
            } else {
                str3 = sb2;
                z2 = false;
            }
            // 删除~
            if (StringsKt.contains$default(str3, "~", false, 2, (Object) null)) {
                str4 = StringsKt.replace$default(str3, "~", "", false, 4, (Object) null);
                z2 = true;
            } else {
                str4 = str3;
            }
            // 删除!
            if (StringsKt.contains$default(str4, "!", false, 2, (Object) null)) {
                str5 = StringsKt.replace$default(str4, "!", "", false, 4, (Object) null);
                z2 = true;
            } else {
                str5 = str4;
            }
            // 删除(
            if (StringsKt.contains$default(str5, "(", false, 2, (Object) null)) {
                str6 = StringsKt.replace$default(str5, "(", "", false, 4, (Object) null);
                z2 = true;
            } else {
                str6 = str5;
            }
            // 删除)
            if (StringsKt.contains$default(str6, ")", false, 2, (Object) null)) {
                str7 = StringsKt.replace$default(str6, ")", "", false, 4, (Object) null);
                z2 = true;
            } else {
                str7 = str6;
            }
            // 删除'
            if (StringsKt.contains$default(str7, "'", false, 2, (Object) null)) {
                sb2 = StringsKt.replace$default(str7, "'", "", false, 4, (Object) null);
                z = true;
            } else {
                z = z2;
                sb2 = str7;
            }
            if (z) {
                sb2 = URLEncoder.encode(sb2, "utf-8");
                Intrinsics.checkExpressionValueIsNotNull(sb2, "URLEncoder.encode(dealStr,\"utf-8\")");
            }
        }
        if (z) {
            StringBuilder sb3 = new StringBuilder();
            // 使用替换结果 sb2 计算MD5
            String encodeByMD5 = MD5Digest.Companion.encodeByMD5(sb2);
            if (encodeByMD5 == null) {
                str2 = null;
            } else if (encodeByMD5 != null) {
                // 转换为大写
                str2 = encodeByMD5.toUpperCase();
                Intrinsics.checkExpressionValueIsNotNull(str2, "(this as java.lang.String).toUpperCase()");
            } else {
                throw new TypeCastException("null cannot be cast to non-null type java.lang.String");
            }
            sb3.append(str2);
            // MD5追加编码
            sb3.append("encodeutf8");
            str = sb3.toString();
        } else {
            MD5Digest.Companion companion = MD5Digest.Companion;
            // 没有发生替换,使用sb
            String sb4 = sb.toString();
            Intrinsics.checkExpressionValueIsNotNull(sb4, "signStr.toString()");
            // 使用sb计算MD5
            String encodeByMD52 = companion.encodeByMD5(sb4);
            if (encodeByMD52 == null) {
                str = null;
            } else if (encodeByMD52 != null) {
                // 转换为大写
                str = encodeByMD52.toUpperCase();
                Intrinsics.checkExpressionValueIsNotNull(str, "(this as java.lang.String).toUpperCase()");
            } else {
                throw new TypeCastException("null cannot be cast to non-null type java.lang.String");
            }
        }
        try {
            User user = (User) new PreUtil("sp_name_user").getValue("sp_user",
                    Reflection.getOrCreateKotlinClass(User.class), (Object) null);
            if (user != null) {
                OauthTokenBean oauthToken = user.getOauthToken();
                String token = oauthToken != null ? oauthToken.getToken() : null;
                Request request3 = chain.request();
                if (!(request3 == null || (newBuilder2 = request3.newBuilder()) == null
                        || (addHeader3 = newBuilder2.addHeader("token", token)) == null
                        || (addHeader4 = addHeader3.addHeader("appKey", "389885588s0648fa")) == null
                        || (addHeader5 = addHeader4.addHeader("sign", str)) == null) // 添加签名至头部
                ) {
                    request2 = addHeader5.build();
                }
            } else {
                Request request4 = chain.request();
                if (!(request4 == null || (newBuilder = request4.newBuilder()) == null
                        || (addHeader = newBuilder.addHeader("appKey", "389885588s0648fa")) == null
                        || (addHeader2 = addHeader.addHeader("sign", str)) == null)) {
                    request2 = addHeader2.build();
                }
            }
            Response proceed = chain2.proceed(request2);
            if (proceed == null) {
                Intrinsics.throwNpe();
            }
            return proceed;
        } catch (Exception e) {
            e.printStackTrace();
            Response proceed2 = chain2.proceed(request);
            if (proceed2 == null) {
                Intrinsics.throwNpe();
            }
            return proceed2;
        }
    }
}

六、逻辑总结(经过验证)

将请求参数正序排序
将请求参数按 参数名1参数值1参数名2参数值2.... 拼接
追加appkey, 追加appsecret
追加请求体
删除部分字符
计算MD5

0

评论 (0)

取消