吾爱破解 - 52pojie.cn

 找回密码
 注册[Register]

QQ登录

只需一步,快速开始

查看: 1557|回复: 17
收起左侧

[Android 原创] 小白借助 AI 复盘某班课鉴权链路分析

[复制链接]
mxyoor 发表于 2026-3-14 14:24
本帖最后由 mxyoor 于 2026-3-14 14:35 编辑

小白借助 AI 复盘某班课鉴权链路分析

提示:本文仅供参考学习,请勿用于非法用途!如有侵权联系我删除,为了保障他人权益,所以文中数据都是残缺或修改过的,更多的是思路分析。

写在前面

先说明一下,我是刚注册不久的小白,这篇帖子就是一次过程分享。

最近用到了某班课,想着就尝试用ai逆向一下他的签到流程

材料准备

  • iOS 侧 HAR(看接口分工和头字段)
  • Android 包(静态 + 动态)
  • Frida / JADX / IDA 等常规工具
  • AI 协作:gpt5.4、gpt5.3codex、claude4.6

分析过程

1) 先用 HAR 把接口分工看清

由于常用设备为ios,所以先在 iOS 上抓了两份 HAR,不急着猜签名,先确认接口职责。

签到提交接口:

/checkin

同时还能看到前置查询接口:

/xxx/{class_id}/checkins/current
/xxx/{class_id}/checkins?pageIndex=1

这一步让我先确定了“查”和“提”是分开的,checkin_id 也更可能来自前置结果而不是本地凭空生成。

2) 再看头字段,先分出两套风格

/checkin 这组头:

/ccs/{class_id}/checkins/current
/ccs/{class_id}/checkins?pageIndex=1

看到这里,其实思路已经出来一点了:

  • App 不是直接无脑提 /checkin
  • 它会先去查当前签到状态
  • checkin_id 应该不是本地乱造的,而是要从返回结果里拿

所以我自己把 HAR 这一步理解成:

先把“谁负责查、谁负责提”分清楚。


提头字段

接口路径看明白以后,最重要的就是header里面的鉴权头

1. /checkin 这条老接口的头

X-mssvc-access-id
X-mssvc-signature
X-mssvc-sec-ts
X-device-code
X-app-id
X-app-version
X-dpr
X-app-machine
X-app-system-version
Date
User-Agent

/xxx/... 这组头:

X-access-id
X-signature
X-client-app-id
X-client-version
Date
User-Agent

看到这里我就先立了一个假设:

这两类接口大概率不是同一个签名模板。

后面的动态分析基本验证了这个方向。

这里其实还有一层关键推理:

  • 从头字段里反复出现 X-signature / X-mssvc-signature
  • 并且它们和 Dateaccess-id 这类鉴权相关字段一起出现
  • 结合常见 API 鉴权模式,优先怀疑是 HMAC 类签名

所以我的路线不是“直接开 Hook”,而是先做这个判断链:

HAR 头字段
-> 怀疑存在签名机制
-> 推测是 HMAC 类算法
-> 优先 Hook javax.crypto.Mac

3) 安卓动态:从“猜”切到“看运行时”

前期静态分析能拿到一些字符串和类名,但要完整串起“谁组串、谁签名、谁发请求”会比较卡。

所以我改了路线:让 App 在运行时自己把链路吐出来。实际排错后,普通 frida-server 附加在启动早期稳定性比较一般,后面切到 Zygisk Gadget 的等待注入模式,Hook 成功率明显提升。

我这里保留完整脚本,方便复现和对照(脚本不删)。

在 AI 协助下整理出的 frida_capture_all.js 如下:


'use strict';

// ======== 工具函数 ========

function bytesToHex(byteArray) {
    if (!byteArray) return '(null)';
    var hex = '';
    for (var i = 0; i < byteArray.length; i++) {
        var b = byteArray[i] & 0xff;
        hex += ('0' + b.toString(16)).slice(-2);
    }
    return hex;
}

function bytesToUtf8(byteArray) {
    if (!byteArray) return '(null)';
    try {
        var result = '';
        for (var i = 0; i < Math.min(byteArray.length, 4096); i++) {
            var b = byteArray[i] & 0xff;
            if (b >= 0x20 && b < 0x7f) {
                result += String.fromCharCode(b);
            } else if (b === 0x0a) {
                result += '\\n';
            } else if (b === 0x0d) {
                result += '\\r';
            } else {
                result += '\\x' + ('0' + b.toString(16)).slice(-2);
            }
        }
        return result;
    } catch (e) {
        return '(decode error)';
    }
}

var captureCount = 0;

// 用 send() 确保数据被 Python 端接收并写入日志
function log(text) {
    send(text);
}

function logSection(title, data) {
    captureCount++;
    var lines = [];
    lines.push('╔══════════════════════════════════════════════');
    lines.push('║ [#' + captureCount + '] ' + title);
    lines.push('╠══════════════════════════════════════════════');
    for (var key in data) {
        if (data.hasOwnProperty(key)) {
            var val = data[key];
            if (typeof val === 'string' && val.indexOf('\n') >= 0) {
                lines.push('║ ' + key + ':');
                val.split('\n').forEach(function(line) {
                    lines.push('║   ' + line);
                });
            } else {
                lines.push('║ ' + key + ': ' + val);
            }
        }
    }
    lines.push('╚══════════════════════════════════════════════');
    send(lines.join('\n'));
}

// ======== 全局 Mac key 跟踪 (修复 this 属性丢失) ========
var macKeyMap = {};

function getMacId(macObj) {
    try {
        return '' + Java.use('java.lang.System').identityHashCode(macObj);
    } catch (e) {
        return 'unknown';
    }
}

// ======== MessageDigest update 数据缓存 ========
var mdUpdateMap = {};

function getMdId(mdObj) {
    try {
        return '' + Java.use('java.lang.System').identityHashCode(mdObj);
    } catch (e) {
        return 'unknown';
    }
}

// ======== Android Hooks ========

Java.perform(function () {
    log('================================================');
    log(' TargetApp 全量捕获脚本 v3');
    log(' 修复: 日志保存 + HMAC key追踪 + URLConnection捕获');
    log('================================================');

    // ===== 1. javax.crypto.Mac (HMAC签名捕获) =====
    try {
        var Mac = Java.use('javax.crypto.Mac');

        Mac.init.overload('java.security.Key').implementation = function (key) {
            try {
                var keyBytes = key.getEncoded();
                var keyHex = bytesToHex(keyBytes);
                var keyStr = bytesToUtf8(keyBytes);
                var macId = getMacId(this);
                macKeyMap[macId] = { hex: keyHex, utf8: keyStr };
                log('[Mac.init] ★ Key (hex): ' + keyHex);
                log('[Mac.init] ★ Key (UTF8): ' + keyStr);
                log('[Mac.init]   Key length: ' + keyBytes.length + ' bytes');
                log('[Mac.init]   Algorithm: ' + this.getAlgorithm());
            } catch (e) {
                log('[Mac.init] error: ' + e);
            }
            return this.init(key);
        };

        try {
            Mac.init.overload('java.security.Key', 'java.security.spec.AlgorithmParameterSpec').implementation = function (key, params) {
                try {
                    var keyBytes = key.getEncoded();
                    var macId = getMacId(this);
                    macKeyMap[macId] = { hex: bytesToHex(keyBytes), utf8: bytesToUtf8(keyBytes) };
                    log('[Mac.init2] ★ Key: ' + bytesToHex(keyBytes));
                } catch (e) {}
                return this.init(key, params);
            };
        } catch (e) {}

        Mac.doFinal.overload('[B').implementation = function (data) {
            var result = this.doFinal(data);
            var macId = getMacId(this);
            var keyInfo = macKeyMap[macId] || { hex: '(unknown)', utf8: '(unknown)' };
            var info = {};
            info['Algorithm'] = this.getAlgorithm();
            info['Key (hex)'] = keyInfo.hex;
            info['Key (UTF8)'] = keyInfo.utf8;
            var dataKey = 'Data (' + data.length + ' bytes)';
            info[dataKey] = bytesToUtf8(data);
            info['Data (hex)'] = bytesToHex(data);
            info['MAC output (hex)'] = bytesToHex(result);
            info['Stacktrace'] = Java.use('android.util.Log').getStackTraceString(
                Java.use('java.lang.Exception').$new());
            logSection('HMAC 签名', info);
            return result;
        };

        Mac.doFinal.overload().implementation = function () {
            var result = this.doFinal();
            var macId = getMacId(this);
            var keyInfo = macKeyMap[macId] || { hex: '(unknown)', utf8: '(unknown)' };
            logSection('HMAC 签名 (update+final)', {
                'Algorithm': this.getAlgorithm(),
                'Key (hex)': keyInfo.hex,
                'Key (UTF8)': keyInfo.utf8,
                'MAC output (hex)': bytesToHex(result)
            });
            return result;
        };

        try {
            Mac.update.overload('[B').implementation = function (data) {
                log('[Mac.update] Data (' + data.length + ' bytes): ' + bytesToUtf8(data));
                return this.update(data);
            };
        } catch (e) {}

        log('[+] Hooked javax.crypto.Mac');
    } catch (e) {
        log('[-] Mac hook failed: ' + e);
    }

    // ===== 2. MessageDigest + update() hook =====
    try {
        var MessageDigest = Java.use('java.security.MessageDigest');

        // Hook update() 追踪增量数据
        try {
            MessageDigest.update.overload('[B').implementation = function (data) {
                var mdId = getMdId(this);
                if (!mdUpdateMap[mdId]) mdUpdateMap[mdId] = [];
                var chunk = bytesToUtf8(data);
                mdUpdateMap[mdId].push(chunk);
                if (data.length < 512) {
                    log('[MD.update] ' + this.getAlgorithm() + ' (' + data.length + 'B): ' + chunk);
                }
                return this.update(data);
            };
        } catch (e) {}

        try {
            MessageDigest.update.overload('[B', 'int', 'int').implementation = function (data, off, len) {
                var mdId = getMdId(this);
                if (!mdUpdateMap[mdId]) mdUpdateMap[mdId] = [];
                var sub = [];
                for (var i = 0; i < len; i++) sub.push(data[off + i]);
                var chunk = bytesToUtf8(sub);
                mdUpdateMap[mdId].push(chunk);
                if (len < 512) {
                    log('[MD.update] ' + this.getAlgorithm() + ' (' + len + 'B): ' + chunk);
                }
                return this.update(data, off, len);
            };
        } catch (e) {}

        MessageDigest.digest.overload('[B').implementation = function (data) {
            var result = this.digest(data);
            var algo = this.getAlgorithm();
            if (data.length < 4096) {
                var mdInfo = {};
                mdInfo['Algorithm'] = algo;
                var dataKey = 'Data (' + data.length + ' bytes)';
                mdInfo[dataKey] = bytesToUtf8(data);
                mdInfo['Digest (hex)'] = bytesToHex(result);
                logSection('MessageDigest', mdInfo);
            }
            return result;
        };

        MessageDigest.digest.overload().implementation = function () {
            var result = this.digest();
            var mdId = getMdId(this);
            var updateData = mdUpdateMap[mdId] || [];
            var info = {
                'Algorithm': this.getAlgorithm(),
                'Digest (hex)': bytesToHex(result)
            };
            if (updateData.length > 0) {
                info['Update data (拼接)'] = updateData.join('');
            }
            logSection('MessageDigest (final)', info);
            delete mdUpdateMap[mdId];
            return result;
        };

        log('[+] Hooked MessageDigest (含 update)');
    } catch (e) {
        log('[-] MessageDigest hook failed: ' + e);
    }

    // ===== 3. HttpURLConnection 完整捕获 =====
    try {
        var HttpURLConnection = Java.use('java.net.HttpURLConnection');
        HttpURLConnection.setRequestProperty.implementation = function (key, value) {
            if (key && (key.toLowerCase().indexOf('x-') === 0 ||
                key.toLowerCase() === 'date' ||
                key.toLowerCase() === 'content-type' ||
                key.toLowerCase() === 'authorization')) {
                log('[URLConn Header] ' + key + ': ' + value);
            }
            return this.setRequestProperty(key, value);
        };
        log('[+] Hooked HttpURLConnection.setRequestProperty');
    } catch (e) {
        log('[-] HttpURLConnection header hook failed: ' + e);
    }

    try {
        var URL = Java.use('java.net.URL');
        URL.openConnection.overload().implementation = function () {
            var conn = this.openConnection();
            var urlStr = this.toString();
            // 替换了特征域名过滤,请按需修改关键字
            if (urlStr.indexOf('targetdomain') >= 0 || urlStr.indexOf('targetkey') >= 0) {
                log('[URLConn Open] ' + urlStr);
            }
            return conn;
        };
        log('[+] Hooked java.net.URL.openConnection');
    } catch (e) {}

    // Hook getInputStream 捕获响应体
    try {
        var HttpsURLConnection = Java.use('javax.net.ssl.HttpsURLConnection');
        HttpsURLConnection.getInputStream.implementation = function () {
            var stream = this.getInputStream();
            try {
                var url = this.getURL().toString();
                // 替换了特征域名过滤
                if (url.indexOf('targetdomain') >= 0 || url.indexOf('targetkey') >= 0) {
                    var code = this.getResponseCode();
                    var respHeaders = '';
                    var headerFields = this.getHeaderFields();
                    var iter = headerFields.entrySet().iterator();
                    while (iter.hasNext()) {
                        var entry = iter.next();
                        var hKey = entry.getKey();
                        var hVal = entry.getValue();
                        if (hKey !== null) {
                            respHeaders += hKey + ': ' + hVal.toString() + '\n';
                        }
                    }

                    // 读取全部响应体
                    var ByteArrayOutputStream = Java.use('java.io.ByteArrayOutputStream');
                    var ByteArrayInputStream = Java.use('java.io.ByteArrayInputStream');
                    var baos = ByteArrayOutputStream.$new();
                    var buf = Java.array('byte', new Array(4096).fill(0));
                    var n;
                    while ((n = stream.read(buf)) !== -1) {
                        baos.write(buf, 0, n);
                    }
                    stream.close();

                    var bodyBytes = baos.toByteArray();
                    var bodyStr = Java.use('java.lang.String').$new(bodyBytes, 'UTF-8');

                    logSection('HTTP 响应 ★', {
                        'URL': url,
                        'Status': '' + code,
                        'Headers': respHeaders,
                        'Body': bodyStr
                    });

                    // 返回新的 InputStream 让 app 继续正常读取
                    return ByteArrayInputStream.$new(bodyBytes);
                }
            } catch (e) {
                log('[URLConn getInputStream] error: ' + e);
            }
            return stream;
        };
        log('[+] Hooked HttpsURLConnection.getInputStream (响应体捕获)');
    } catch (e) {
        log('[-] HttpsURLConnection.getInputStream hook failed: ' + e);
    }

    // Hook getOutputStream 记录请求体 URL
    try {
        var HttpsURLConn2 = Java.use('javax.net.ssl.HttpsURLConnection');
        HttpsURLConn2.getOutputStream.implementation = function () {
            var os = this.getOutputStream();
            try {
                var url = this.getURL().toString();
                if (url.indexOf('targetdomain') >= 0 || url.indexOf('targetkey') >= 0) {
                    log('[URLConn getOutputStream] URL: ' + url);
                }
            } catch (e) {}
            return os;
        };
        log('[+] Hooked HttpsURLConnection.getOutputStream');
    } catch (e) {
        log('[-] getOutputStream hook failed: ' + e);
    }

    // ===== 4. OkHttp 请求(安全模式,不读 body 避免消耗) =====
    try {
        var RequestBuilder = Java.use('okhttp3.Request$Builder');

        RequestBuilder.addHeader.implementation = function (name, value) {
            log('[OkHttp +Header] ' + name + ': ' + value);
            return this.addHeader(name, value);
        };

        RequestBuilder.header.implementation = function (name, value) {
            log('[OkHttp =Header] ' + name + ': ' + value);
            return this.header(name, value);
        };

        try {
            RequestBuilder.url.overload('java.lang.String').implementation = function (url) {
                if (url.indexOf('targetkey') >= 0) {
                    log('[OkHttp URL] ' + url);
                }
                return this.url(url);
            };
        } catch (e) {}

        log('[+] Hooked OkHttp Request.Builder (安全模式)');
    } catch (e) {
        log('[-] OkHttp Request.Builder hook failed: ' + e);
    }

    // OkHttp Interceptor
    try {
        var RealInterceptorChain = Java.use('okhttp3.internal.http.RealInterceptorChain');
        RealInterceptorChain.proceed.overload('okhttp3.Request').implementation = function (request) {
            var url = request.url().toString();
            if (url.indexOf('targetkey') >= 0) {
                var method = request.method();
                var headers = request.headers();
                var headerStr = '';
                for (var i = 0; i < headers.size(); i++) {
                    headerStr += headers.name(i) + ': ' + headers.value(i) + '\n';
                }
                var reqBodyStr = '';
                var reqBody = request.body();
                if (reqBody) {
                    try {
                        var FormBody = Java.use('okhttp3.FormBody');
                        if (FormBody.class.isInstance(reqBody)) {
                            var fb = Java.cast(reqBody, FormBody);
                            var parts = [];
                            for (var j = 0; j < fb.size(); j++) {
                                parts.push(fb.encodedName(j) + '=' + fb.encodedValue(j));
                            }
                            reqBodyStr = parts.join('&');
                        }
                    } catch (e) {
                        reqBodyStr = '(非FormBody)';
                    }
                }

                logSection('OkHttp 请求', {
                    'URL': url,
                    'Method': method,
                    'Headers': headerStr,
                    'Body': reqBodyStr || '(empty)'
                });

                var response = this.proceed(request);

                try {
                    var respHeaders = response.headers();
                    var respHeaderStr = '';
                    for (var k = 0; k < respHeaders.size(); k++) {
                        respHeaderStr += respHeaders.name(k) + ': ' + respHeaders.value(k) + '\n';
                    }
                    var source = response.body().source();
                    source.request(Java.use('java.lang.Long').MAX_VALUE.value);
                    var respBuffer = source.getBuffer().clone();
                    var respBodyStr = respBuffer.readUtf8();

                    logSection('OkHttp 响应', {
                        'URL': url,
                        'Status': response.code() + ' ' + response.message(),
                        'Headers': respHeaderStr,
                        'Body': respBodyStr
                    });
                } catch (e) {
                    logSection('OkHttp 响应 (读取失败)', {
                        'URL': url,
                        'Error': e.toString()
                    });
                }

                return response;
            }
            return this.proceed(request);
        };
        log('[+] Hooked OkHttp RealInterceptorChain');
    } catch (e) {
        log('[-] RealInterceptorChain hook failed: ' + e);
    }

    // ===== 5. SharedPreferences 读写 =====
    try {
        var SharedPrefsImpl = Java.use('android.app.SharedPreferencesImpl');
        var Editor = Java.use('android.app.SharedPreferencesImpl$EditorImpl');

        Editor.putString.implementation = function (key, value) {
            if (key && (key.toLowerCase().indexOf('token') >= 0 ||
                key.toLowerCase().indexOf('key') >= 0 ||
                key.toLowerCase().indexOf('secret') >= 0 ||
                key.toLowerCase().indexOf('sign') >= 0 ||
                key.toLowerCase().indexOf('access') >= 0 ||
                key.toLowerCase().indexOf('user') >= 0 ||
                key.toLowerCase().indexOf('pwd') >= 0 ||
                key.toLowerCase().indexOf('session') >= 0 ||
                key.toLowerCase().indexOf('login') >= 0 ||
                key.toLowerCase().indexOf('id') >= 0)) {
                logSection('SharedPrefs 写入 ★', {
                    'Key': key,
                    'Value': value ? value.toString() : '(null)'
                });
            }
            return this.putString(key, value);
        };

        SharedPrefsImpl.getString.implementation = function (key, defValue) {
            var result = this.getString(key, defValue);
            if (key && (key.toLowerCase().indexOf('token') >= 0 ||
                key.toLowerCase().indexOf('key') >= 0 ||
                key.toLowerCase().indexOf('secret') >= 0 ||
                key.toLowerCase().indexOf('sign') >= 0 ||
                key.toLowerCase().indexOf('access') >= 0)) {
                logSection('SharedPrefs 读取', {
                    'Key': key,
                    'Value': result ? result.toString() : '(null)'
                });
            }
            return result;
        };
        log('[+] Hooked SharedPreferences');
    } catch (e) {
        log('[-] SharedPreferences hook failed: ' + e);
    }

    // ===== 6. 延迟 hook 解壳后的类 =====
    hookDecryptedClasses();

});

function hookDecryptedClasses() {
    setTimeout(function () {
        Java.perform(function () {
            log(' 延迟 hook (10s) - 尝试加载解壳后的类...');

            // 替换了目标 App 的特有包名和类名
            var classesToTry = [
                'com.targetapp.network.TargetClient',
                'com.targetapp.network.coreapi.CoreApiClient',
                'com.targetapp.network.coreapi.signbody.BaseRequestSignBody',
                'com.targetapp.network.mssvc.MssvcClient',
                'com.targetapp.network.mssvc.MssvcApiClient'
            ];

            classesToTry.forEach(function (clsName) {
                try {
                    var cls = Java.use(clsName);
                    log('[+] Found: ' + clsName);
                    var methods = cls.class.getDeclaredMethods();
                    for (var i = 0; i < methods.length; i++) {
                        var m = methods[i];
                        var mName = m.getName();
                        if (mName.indexOf('sign') >= 0 || mName.indexOf('Sign') >= 0 ||
                            mName.indexOf('token') >= 0 || mName.indexOf('key') >= 0 ||
                            mName.indexOf('header') >= 0 || mName.indexOf('request') >= 0 ||
                            mName.indexOf('init') >= 0 || mName.indexOf('secret') >= 0) {
                            log('  ★ ' + m.toString());
                        }
                    }
                } catch (e) {}
            });

            Java.enumerateLoadedClasses({
                onMatch: function (className) {
                    // 替换了包名关键字过滤
                    if (className.indexOf('targetapp') >= 0 &&
                        (className.indexOf('sign') >= 0 || className.indexOf('Sign') >= 0 ||
                         className.indexOf('token') >= 0 || className.indexOf('Token') >= 0 ||
                         className.indexOf('Client') >= 0 || className.indexOf('Secret') >= 0)) {
                        log('[Loaded] ' + className);
                    }
                },
                onComplete: function () {
                    log(' 类枚举完成');
                }
            });
        });
    }, 10000);

    setTimeout(function () {
        Java.perform(function () {
            log(' 二次延迟 hook (20s)...');

            try {
                var BaseSign = Java.use('com.targetapp.network.coreapi.signbody.BaseRequestSignBody');
                var baseMethods = BaseSign.class.getDeclaredMethods();
                log('[BaseRequestSignBody] 方法列表:');
                for (var i = 0; i < baseMethods.length; i++) {
                    log('  ' + baseMethods[i].toString());
                }
                var fields = BaseSign.class.getDeclaredFields();
                log('[BaseRequestSignBody] 字段列表:');
                for (var j = 0; j < fields.length; j++) {
                    log('  ' + fields[j].toString());
                }
            } catch (e) {
                log('[-] BaseRequestSignBody not loaded: ' + e.message);
            }

            try {
                var CoreClient = Java.use('com.targetapp.network.coreapi.CoreApiClient');
                var coreMethods = CoreClient.class.getDeclaredMethods();
                log('[CoreApiClient] 方法列表:');
                for (var i = 0; i < coreMethods.length; i++) {
                    log('  ' + coreMethods[i].toString());
                }
            } catch (e) {
                log('[-] CoreApiClient not loaded: ' + e.message);
            }

            Java.enumerateLoadedClasses({
                onMatch: function (className) {
                    if (className.indexOf('targetapp') >= 0 &&
                        (className.indexOf('sign') >= 0 || className.indexOf('Sign') >= 0)) {
                        log('[Loaded 20s] ' + className);
                        try {
                            var cls = Java.use(className);
                            var methods = cls.class.getDeclaredMethods();
                            for (var i = 0; i < methods.length; i++) {
                                log('  → ' + methods[i].toString());
                            }
                        } catch (e) {}
                    }
                },
                onComplete: function () {}
            });
        });
    }, 20000);
}

脚本的第一版思路很直接,就是先把下面这些点都挂上:

Java.perform(function () {
    hookMac();
    hookMessageDigest();
    hookHttpURLConnection();
    hookOkHttpBuilder();
    hookSharedPreferences();
    hookInterestingUrls(['/checkin', '/xxx/.../checkins/current']);
});

这一段其实就已经够说明思路了:

  • Mac 看签名输入输出
  • MessageDigest 看摘要参与情况
  • HttpURLConnection / OkHttp 看头和 URL
  • SharedPreferences 看本地取值

因为前面从 HAR 看头字段时,我已经知道有一类头很像签名结果,所以脚本里最先重点观察的就是 javax.crypto.Mac

function hookMac() {
    var Mac = Java.use('javax.crypto.Mac');

    Mac.init.overload('java.security.Key').implementation = function (key) {
        log('[Mac.init] algorithm=' + this.getAlgorithm());
        return this.init(key);
    };

    Mac.update.overload('[B').implementation = function (data) {
        log('[Mac.update] data=' + bytesToUtf8(data));
        return this.update(data);
    };

    Mac.doFinal.overload().implementation = function () {
        var result = this.doFinal();
        log('[Mac.doFinal] out=' + bytesToHex(result));
        return result;
    };
}

我当时就是靠这段,先看到了班课侧 /xxx/... 那组请求的签名串输入格式。

也正因为这里先有了运行时证据,后面我才敢判断:

/xxx/.../checkin 不是一套完全相同的签名风格。

4) 再去盯 URL 和调用栈

只看 Mac 还不够,因为你只能知道“哪里在算”,但不一定知道“算的是哪条链”。

所以后面我又把 URL 构造点也挂上了。

这里也直接贴正文保留的关键思路片段:

function hookInterestingUrls(targets) {
    var URL = Java.use('java.net.URL');
    URL.$init.overload('java.lang.String').implementation = function (url) {
        for (var i = 0; i < targets.length; i++) {
            if (url.indexOf(targets[i]) >= 0) {
                log('[URL.<init>] ' + url);
                log(Java.use('android.util.Log').getStackTraceString(
                    Java.use('java.lang.Exception').$new()
                ));
            }
        }
        return this.$init(url);
    };
}

我自己后来回头看,真正把分析往前推了一步的,不是“搜到了路径”,而是:

  • URL 出现了
  • 调用栈也跟着出来了
  • 这样才能知道后面该追哪个运行时网络类

5) 关键日志

1) Api_A 登录请求的签名链
[#986] MessageDigest (final)
Algorithm: MD5
Digest (hex): 208e8b8脱敏253993

[Mac.init] ★ Key (UTF8): <KEY_REDACTED>
[Mac.init]   Key length: xx bytes
[Mac.init]   Algorithm: HmacSHA1
[Mac.update] Data: POST|/xxx/account-login|MTANDROID|Tue, 10 Mar 2026 14:11:27 GMT||208e8b8脱敏993
[#987] HMAC 签名 (update+final)
MAC output (hex): fabe112dc543a74d脱敏cd02d345d57
[URLConn Open]/xxx/account-login

这段日志说明了三件事:

  • 先对请求体做 MD5
  • 再把 MD5 结果拼进签名原串
  • 最终使用 HmacSHA1
2) 签到查询链(/checkins + /current)
[Mac.init] ★ Key (UTF8): <KEY_REDACTED>
[Mac.init]   Key length: xx bytes
[Mac.init]   Algorithm: HmacSHA1
[Mac.update] Data: GET|/xxx/<CC_ID>/checkins|<ACCESS_ID>|Tue, 10 Mar 2026 14:13:14 GMT|order=DESC&pageIndex=1&pageSize=100&sortBy=OPENTIME|
[#1201] HMAC 签名 (update+final)
MAC output (hex): edcf4458脱敏db029e
[URLConn Open] /xxx/<CC_ID>/checkins?order=DESC&pageIndex=1&pageSize=100&sortBy=OPENTIME

[Mac.update] Data: GET|/xxx/<CC_ID>/checkins/current|<ACCESS_ID>|Tue, 10 Mar 2026 14:13:17 GMT||
[#1208] HMAC 签名 (update+final)
MAC output (hex): 2b98f脱敏0ea24f5
[URLConn Open]/xxx/<CC_ID>/checkins/current

这段能直接看出常规接口的组串模板:

  • METHOD|PATH|ACCESS_ID|DATE|QUERY|
  • 没有 query 时,QUERY 为空,但结尾分隔符 | 仍保留
3) Api_B 链路(以某消息列表接口为样本)
[#1185] MessageDigest (final)
Algorithm: MD5
Digest (hex): 6c07f5脱敏7353

[Mac.init] ★ Key (UTF8): 脱敏
[Mac.init]   Key length: xx bytes
[Mac.init]   Algorithm: HmacSHA1
[Mac.update] Data: /xxxxx/index.php/ccmsg/list_unread|<ACCESS_ID>|Tue, 10 Mar 2026 14:12:58 GMT|6C07F脱敏07353
[#1186] HMAC 签名 (update+final)
MAC output (hex): 4434da38脱敏86221d02b45
[URLConn Open] /xxxxx/index.php/ccmsg/list_unread

这一条和 常规接口 的差异也很明显:

  • 组串第一段是完整 URL,不是 path
  • MD5 参与后会转成大写十六进制再拼接

6) 还原签名生成逻辑

为了避免“只看一条日志就下结论”,我实际是按下面这条线做交叉验证:

  1. 先定算法:看 Mac.initAlgorithm,先确认主流程是不是 HmacSHA1
  2. 再定原串:看同一调用链里的 Mac.update Data,把字段顺序完整抄下来。
  3. 定摘要参与方式:看 MessageDigest (final),确认是否先做 MD5、是否大小写转换后再入串。
  4. 对齐请求语义:把 Mac.update 里的每段内容和 URL/Query/Date/Header 一一对应,确认 path/full_urlaccess_iddatequery/body 的位置关系。
  5. 多样本回放:同一接口抓多次、同一时段抓不同接口,验证“固定槽位不变、变量槽位随请求变化”。
  6. 反证排错:排除同名但语义不同的字段,避免把无关值误当鉴权参数。

签名生成逻辑(Java层)

下面这部分是基于 Java 层日志还原出的签名流程。可以看出,签名并不是单一函数的固定模板,而是“使用同一套 HmacSHA1,但按不同接口族使用不同组串规则”。

1) 常规业务接口

输入:
    method      (GET/POST/PATCH...)
    path        (如 /xxx/<CC_ID>/checkins/current)
    access_id   (X-access-id)
    date        (Date 头)
    query       (URL query,无则空串)
    key         (运行时捕获)

1. 组串:method + "|" + path + "|" + access_id + "|" + date + "|" + query + "|"
2. UTF-8 编码组串
3. HMAC-SHA1(key, bytes)
4. 输出 20 字节摘要,转 40 位小写十六进制
5. 作为签名值写入 X-signature

2) 登录接口

输入:
    body_json_compact
    app_id = MTANDROID
    date
    key = <KEY_REDACTED>

1. body_json_compact -> MD5 -> body_md5_lower
2. 组串:"POST|/xxxxxxxx/account-login|" + app_id + "|" + date + "||" + body_md5_lower
3. UTF-8 编码组串
4. HMAC-SHA1(key, bytes)
5. 输出 20 字节摘要,转 40 位小写十六进制

3) 消息类服务接口

输入:
    full_url    (完整 URL)
    access_id   (X-xxxxx-access-id)
    date        (Date 头)
    body_or_param
    key

1. body_or_param -> MD5 -> body_md5_lower
2. body_md5_lower 转大写 -> body_md5_upper
3. 组串:full_url + "|" + access_id + "|" + date + "|" + body_md5_upper
4. UTF-8 编码组串
5. HMAC-SHA1(key, bytes)
6. 输出 20 字节摘要,转 40 位小写十六进制
7. 作为 X-xxxxx-signature

4) 关于 access_id

这一项单独说结论:access_id 来自登录接口响应,不是本地每次现算。

证据链(Java 层):

  1. 登录响应解析时,直接从返回值提取 accessId。  
  2. 登录成功后,该值会写入会话并持久化到本地缓存,作为后续请求的鉴权上下文。  
  3. 发起请求时,该值被放进特定的 Header 头中。  
  4. HAR 可见这个头在同一会话内基本稳定,真正频繁变化的是 path/query/date/body,因此签名值会随请求内容变化。

写在最后

现在的 AI 的迭代速度真的太快了。很多以前要花很久才能摸到门槛的事情,现在通过和模型来回协作,能更快从“看不懂”走到“有证据地说清楚”。

当然,AI 不是替代思考的魔法棒,它更像一个放大器:你问得越具体、验证越扎实,得到的结果就越可靠。对我这种新手来说,这种变化很鼓舞人,也让我更愿意继续把每一次分析过程认真记录下来。

第一次发逆向分析帖,如有错误欢迎指正。

最后
本文仅用于技术研究与学习交流,请勿用于任何商业用途。

免费评分

参与人数 6威望 +1 吾爱币 +25 热心值 +4 收起 理由
qtfreet00 + 1 + 20 + 1 感谢发布原创作品,吾爱破解论坛因你更精彩!
wyouyou888 + 1 欢迎分析讨论交流,吾爱破解论坛有你更精彩!
jaffa + 1 谢谢@Thanks!
longer008 + 1 谢谢@Thanks!
hehehero + 2 + 1 感谢发布原创作品,吾爱破解论坛因你更精彩!
ytfh1131 + 1 + 1 谢谢@Thanks!

查看全部评分

发帖前要善用论坛搜索功能,那里可能会有你要找的答案或者已经有人发布过相同内容了,请勿重复发帖。

za3380885 发表于 2026-3-15 08:31
感谢分享
shiren3 发表于 2026-3-15 12:37
头像被屏蔽
qq894030745 发表于 2026-3-15 15:51
2012026329 发表于 2026-3-15 21:43
感谢分享
za3380885 发表于 2026-3-16 08:18
感谢分享
yobues 发表于 2026-3-16 08:25
感谢分享,对我很有借鉴意义
haidemili 发表于 2026-3-16 08:38
感谢分享!!谢谢楼主大大
OiSteveniO 发表于 2026-3-16 15:41

感谢分享!!!!!
WWWHELP123 发表于 2026-3-17 07:42
感谢分享
您需要登录后才可以回帖 登录 | 注册[Register]

本版积分规则

返回列表

RSS订阅|小黑屋|处罚记录|联系我们|吾爱破解 - 52pojie.cn ( 京ICP备16042023号 | 京公网安备 11010502030087号 )

GMT+8, 2026-4-29 11:55

Powered by Discuz!

Copyright © 2001-2020, Tencent Cloud.

快速回复 返回顶部 返回列表