前言

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

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

某校园跑软件

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

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

分析思路

{x} 抓包,看请求是否有签名,没有就去第4步
{x} 检测安装包是否有壳,无壳就去第4步
{x} 脱壳
{x} 分析代码
{x} 伪造请求
{x} 等待正义的审判 ::(惊哭)

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

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位的)

{message type="warning" content="顺便记下包名com.tanma.unirun"/}

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

单击目标应用,开始脱壳

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

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

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

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

搜索字符串 sign

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

只看目标应用的包

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

看包名:

{x} entities 实体包,排除
{x} enums 枚举包,排除
{x} 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;
        }
    }
}

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

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

最后修改:2021 年 11 月 30 日
如果觉得我的文章对你有用,请随意赞赏