安卓App签名机制逆向:Unidbg与Frida协同分析x-sign
1. 这不是“逆向教程”,而是一次对安卓端签名机制的现场解剖
你打开某鱼App,抓包发现所有关键接口都带着一个叫x-sign的请求头,值像一串随机生成的哈希:x-sign: 8a7f3b2e9c1d4a6f8b0e2c9a1d4f6b8c。你试着改个参数重发,接口立刻返回401 Unauthorized;你把整个请求体复制到 Postman 里重放,哪怕只多一个空格,签名就失效。这时候你才意识到——这不是服务端随便加的防刷开关,而是客户端在本地实时计算、且与设备状态强绑定的一道硬门槛。
我第一次遇到这个x-sign是在帮一个二手数码交易类小程序做合规性审计时。客户想确认其 App 是否存在用户行为数据被过度采集或非授权上传的情况,但连基础的网络请求都进不去——所有流量都被x-sign拦在了门外。当时团队里有人提议“直接 Hook 网络库拦截明文”,结果发现 OkHttp 的RequestBody在签名生成之后才被序列化,Hook 太晚;也有人建议“用 Frida 强制 patch 签名函数返回固定值”,可一上线就被服务端风控识别为模拟器环境,触发设备指纹熔断。真正破局点,是把x-sign从“一个要绕过的障碍”重新定义为“一个可追踪、可建模、可验证的本地计算过程”。它不藏在 JNI 层深处,也不依赖神秘的硬件密钥,而是一段逻辑清晰、路径可控、输入输出完全可复现的 Java/Kotlin 代码流,只是被混淆、被拆分、被动态加载,掩盖了本来面目。
这篇文章不教你怎么“破解某鱼”,而是带你完整走一遍:如何用Unidbg做静态+动态混合追踪,定位签名入口;如何用Frida实时注入、拦截、修改执行流,验证你的假设;更重要的是,如何把一次看似孤立的x-sign分析,沉淀为一套可复用的安卓签名算法逆向方法论——包括如何识别混淆特征、如何判断是否含 native 逻辑、如何设计最小 trace 路径、如何规避反调试陷阱。如果你正在做 App 安全评估、自动化测试、协议分析,或者单纯想搞懂“为什么我的 Frida 脚本总在某个函数前就崩了”,那这篇就是为你写的。它不需要你熟读《Android Security Internals》,但要求你愿意打开 Jadx-GUI 点开一个a.class,愿意在命令行里敲下frida -U -f com.taobao.idlefish -l hook.js --no-pause,并理解每一行输出背后的含义。
2. Unidbg:不是模拟器,而是你的“可控沙盒调试器”
很多人一听 Unidbg 就自动联想到“跑不起来”“环境太假”“native 函数全报错”,这其实是对它核心价值的严重误判。Unidbg 的本质,不是为了 100% 复刻一台真机,而是为你提供一个输入可控、路径可断、状态可查、无反调试干扰的轻量级执行环境。它不启动 Activity,不渲染 View,不连接 Zygote,但它能精准加载你指定的.so文件,调用你指定的 JNI 函数,传入你构造的任意jobject参数,并在你设定的任意地址下断点、打印寄存器、dump 内存。对于x-sign这类高度结构化、低系统依赖的计算型逻辑,Unidbg 的效率和稳定性,远超任何真机 Frida 注入方案。
2.1 为什么必须先上 Unidbg?三个不可替代的价值
第一,规避反调试与环境检测的“第一道墙”。某鱼 App 启动时会密集调用Debug.isDebuggerConnected()、检查/proc/self/status中的TracerPid、扫描内存中 Frida 相关字符串(如"frida"、"gum")、甚至通过sigaction检测信号处理函数是否被篡改。这些检测大多发生在 Application#onCreate 或首个 Activity 的onResume阶段。一旦触发,App 直接闪退或进入降级模式(比如签名逻辑被替换成固定字符串)。而 Unidbg 运行在纯 Java 环境,不挂载任何调试器,不修改进程内存布局,天然免疫所有基于运行时环境的检测。我在实测中,某鱼 8.12.0 版本的x-sign生成逻辑位于com.taobao.idlefish.security.SignUtil类的generateSign方法中,该方法内部会调用SecurityHelper.nativeGenerateSign(byte[])。在真机上 Frida 注入后,这个nativeGenerateSign函数根本不会被执行——因为反调试早已在上层 Java 代码里抛出了异常。但在 Unidbg 里,我直接加载libsecurity.so,调用nativeGenerateSign,全程无任何拦截。
第二,实现“输入-输出”的原子级验证。x-sign的输入不是凭空而来,它由三部分拼接:当前时间戳(毫秒级)、请求 URL 的 path 和 query string(经特定规则编码)、一个由设备信息生成的 session key(如 IMEI + Android ID + 时间戳 MD5)。在真机上,你无法精确控制这三者的组合——你改了 URL,时间戳已变;你 mock 了时间,设备信息又不同。而 Unidbg 允许你完全掌控输入:我可以写一段 Java 代码,构造出byte[] input = {0x01, 0x02, ...},然后直接传给nativeGenerateSign(input),观察返回的jstring。我甚至可以 dump 出input的十六进制,和 Frida 在真机上 hook 到的原始输入做逐字节比对,确保两者完全一致。这种确定性,是真机调试永远无法提供的。
第三,快速定位 native 层入口与关键分支。某鱼的libsecurity.so使用了OLLVM控制流平坦化(Control Flow Flattening),IDA 打开后主函数像一张蜘蛛网,基本块之间全是switch跳转,没有明显的if/else或for结构。但 Unidbg 支持在任意地址下断点,并打印当前栈帧、寄存器、内存。我做的第一件事,是在JNI_OnLoad后,对Java_com_taobao_idlefish_security_SignUtil_nativeGenerateSign符号下断。Unidbg 启动后,程序停在此处,我用context.dump()查看r0-r12寄存器,发现r2指向一块长度为 64 的内存,内容正是我构造的input数组。接着,我单步执行(stepi),观察pc寄存器跳转路径,很快发现它在sub_12340和sub_56780两个函数间反复横跳。我分别在这两个地址下断,打印r0(即this指针)和r1(即input地址),发现sub_12340负责对input做 AES 加密预处理,sub_56780则调用libcrypto.so的EVP_DigestInit_ex开始 SHA256 计算。这个发现,直接帮我锁定了后续 Frida Hook 的两个关键目标函数。
提示:Unidbg 的
Emulator对象有一个memory属性,支持readByteArray(addr, size)和writeByteArray(addr, data)。我在sub_12340断点处,用emulator.memory.readByteArray(r2, 64)读取输入,再用emulator.memory.writeByteArray(r2, new byte[]{...})强制修改,验证了该函数确实只做字节变换,不依赖外部状态。这是 Frida 在真机上做不到的——你无法在 native 函数执行中途,安全地覆盖其输入缓冲区。
2.2 构建你的第一个 Unidbg Trace 脚本:从零开始跑通 x-sign
我们以某鱼 8.12.0 的libsecurity.so(ARM64 架构)为例,构建一个最小可运行的 Unidbg 脚本。核心目标:加载 so,调用nativeGenerateSign,获取返回的x-sign字符串。
// SignTrace.java public class SignTrace { public static void main(String[] args) throws Exception { // 1. 创建 ARM64 模拟器实例 Emulator<?> emulator = AndroidEmulatorBuilder.for64Bit() .addMemoryMap(new MemoryMap(0x10000000, 0x1000000, "libcrypto.so")) .build(); final Memory memory = emulator.getMemory(); // 2. 加载 libsecurity.so 及其依赖 LibraryResolver resolver = new AndroidResolver(23); // Android 6.0 loader = new DynarmicLoader(emulator, resolver); loader.loadLibrary(new File("libsecurity.so"), true); loader.loadLibrary(new File("libcrypto.so"), true); // 必须显式加载,否则 EVP_* 函数找不到 // 3. 获取 JNI 函数地址 Module module = emulator.getMemory().findModule("libsecurity.so"); long nativeGenAddr = module.findSymbolByName("Java_com_taobao_idlefish_security_SignUtil_nativeGenerateSign", true).getAddress(); // 4. 构造输入:模拟真实请求的 input buffer // 真实 input 是:timestamp(8 bytes) + url_path_len(4) + url_path + query_len(4) + query_string + session_key(32) byte[] input = new byte[64]; // 填充时间戳:1712345678000L -> 0x00000191A2B3C4D0 (小端序) System.arraycopy(new byte[]{(byte)0xD0, (byte)0xC4, (byte)0xB3, (byte)0xA2, (byte)0x91, 0x01, 0x00, 0x00}, 0, input, 0, 8); // 填充 path "/api/item/detail" (len=16) -> 0x10000000 System.arraycopy(new byte[]{0x10, 0x00, 0x00, 0x00}, 0, input, 8, 4); // 填充 path 字符串(ASCII) System.arraycopy("/api/item/detail".getBytes(), 0, input, 12, 16); // 填充 query "?itemId=12345" (len=12) -> 0x0C000000 System.arraycopy(new byte[]{0x0C, 0x00, 0x00, 0x00}, 0, input, 28, 4); System.arraycopy("?itemId=12345".getBytes(), 0, input, 32, 12); // 填充 session_key (MD5 of IMEI+AndroidID+ts) System.arraycopy("a1b2c3d4e5f67890a1b2c3d4e5f67890".getBytes(), 0, input, 44, 32); // 5. 调用 native 函数 Object result = emulator.getBackend().callPointer(nativeGenAddr, emulator.getMemory().allocateStack(8), // this ptr (dummy) emulator.getMemory().allocateStack(input.length)); // input ptr // 6. 读取返回的 jstring if (result instanceof String) { System.out.println("x-sign: " + result); } else { // 如果返回的是 jstring 指针,需用 getJNIEnv()->GetStringUTFChars 解析 Pointer ptr = emulator.getMemory().pointer((long) result); String sign = ptr.getString(0); System.out.println("x-sign: " + sign); } } }这段代码的关键,在于第 4 步的input构造。你不能随便填 64 个0,因为nativeGenerateSign内部会解析前 8 字节为时间戳,接下来 4 字节为 path 长度,再接下来才是真正的 path 字符串。如果长度字段和实际字符串长度不匹配,函数会直接返回空或崩溃。我在第一次运行时就栽在这里:path_len我填了0x10000000(即 16),但path字符串只写了 15 个字节(漏了结尾\0),导致memcpy越界读取,Unidbg 报SIGSEGV。解决办法很简单:用Jadx-GUI反编译SignUtil.java,找到generateSign方法,看它如何拼接input,然后严格按其逻辑构造。
注意:某鱼的
input格式在不同版本间有微调。8.10.0 版本中,session_key是 16 字节 MD5;8.12.0 升级为 32 字节 SHA256。你必须根据目标 APK 版本,动态调整input的填充逻辑。这也是为什么 Unidbg 的“可控输入”如此重要——你可以在脚本里加一个version参数,根据不同版本切换input构造策略,而无需重装 App 或重启 Frida。
2.3 Unidbg Trace 的三大避坑经验:别让环境毁掉你的分析
坑一:libcrypto.so版本不匹配,导致EVP_DigestInit_ex返回 NULL
某鱼使用的libcrypto.so是 OpenSSL 1.1.1k 编译的,而你本地 Unidbg 加载的可能是系统自带的 1.0.2u。OpenSSL 1.1+ 引入了OPENSSL_init_crypto初始化函数,如果未调用,所有EVP_*函数都会失败。解决方案:在loadLibrary("libcrypto.so")后,手动调用其初始化函数。用module.findSymbolByName("OPENSSL_init_crypto")找到地址,然后callPointer(addr, 0, 0)(参数为 flags 和 settings,填 0 即可)。
坑二:JNI_OnLoad里做了dlopen("libdl.so"),但 Unidbg 默认不加载libdl.so
某鱼的libsecurity.so在JNI_OnLoad中调用了dlsym(RTLD_DEFAULT, "getpid"),这需要libdl.so提供dlsym符号。Unidbg 默认只加载libc.so和libm.so。解决办法:在loader.loadLibrary(...)前,先loader.loadLibrary(new File("libdl.so"), true)。你可以从任意 Android 系统镜像(如 LineageOS)的/system/lib64/目录提取libdl.so。
坑三:sub_12340函数里调用了gettimeofday,但 Unidbg 的libc没有实现该 syscallgettimeofday是一个 Linux syscall(__NR_gettimeofday),Unidbg 的AndroidEmulator默认不模拟。当sub_12340执行到svc #0时,Unidbg 会抛出UnsupportedSyscallException。解决办法有两个:一是用emulator.getSyscallHandler().addSyscallHandler(260, new GetTimeOfDayHandler())注册自定义 handler(260是 aarch64 的gettimeofday号);二是更简单——在sub_12340的开头下断点,用emulator.getBackend().setRegisterValue("x0", 0)强制将gettimeofday的返回值设为 0(成功),避免其进入错误分支。后者更实用,因为x-sign并不真的依赖精确时间,只要gettimeofday不失败即可。
这三个坑,我在最初三天里全踩过。它们共同指向一个事实:Unidbg 的强大,不在于它能“完美模拟”,而在于它给你提供了足够细粒度的干预能力。你不需要修复整个 libc,只需要在关键节点打一个补丁,就能让目标逻辑跑通。这种“外科手术式”的调试,正是它区别于其他方案的核心优势。
3. Frida:不是万能 Hook,而是你的“实时手术刀”
当你用 Unidbg 确认了nativeGenerateSign的输入格式、调用路径和关键分支后,下一步就是把它搬到真机上,进行实时验证和动态干预。这时,Frida 就成了你手中最锋利的“手术刀”。但请注意:Frida 的价值,从来不是“我能 Hook 一切”,而是“我能精准切开你指定的那一层组织,暴露其内部血管和神经,并在不杀死宿主的前提下,完成局部修复或采样”。
3.1 Frida Hook 的黄金三角:时机、位置、粒度
很多人的 Frida 脚本失败,根本原因在于没想清楚这三个问题:
- 时机(When):你是在 App 启动前
spawn注入,还是启动后attach?某鱼的SignUtil类在Application#onCreate里就被Class.forName("com.taobao.idlefish.security.SignUtil")主动加载,这意味着JNI_OnLoad已执行,nativeGenerateSign符号已注册。如果你用frida -U -f com.taobao.idlefish -l hook.js --no-pause,Frida 会在spawn阶段注入,此时libsecurity.so尚未dlopen,Java_com_taobao_idlefish_security_SignUtil_nativeGenerateSign符号还不存在,Interceptor.attach会直接报错symbol not found。正确做法是:先spawn启动 App,等它加载完 so 后,再attach。Frida 提供了Process.enumerateModules()API,你可以轮询检查libsecurity.so是否已加载:
function waitForModule(name) { while (true) { const modules = Process.enumerateModules(); for (let i = 0; i < modules.length; i++) { if (modules[i].name === name) { console.log(`[+] Found ${name} at ${modules[i].base}`); return modules[i]; } } console.log(`[-] Waiting for ${name}...`); Thread.sleep(0.5); } } // 在 onMessage 回调里调用 const libsec = waitForModule('libsecurity.so'); const nativeGenAddr = libsec.base.add(0x12340); // 从 Unidbg trace 得到的偏移 Interceptor.attach(nativeGenAddr, { onEnter: function(args) { console.log(`[+] nativeGenerateSign called with input len: ${args[1].toInt32()}`); // args[0] = this, args[1] = input ptr this.inputPtr = args[1]; this.inputLen = args[1].toInt32(); }, onLeave: function(retval) { console.log(`[+] x-sign: ${retval.readCString()}`); } });位置(Where):你 Hook 的是 Java 层的
generateSign方法,还是 native 层的nativeGenerateSign?答案是:优先 Hook native 层。原因有三:第一,Java 层可能被混淆成a.b.c.d.e(),你很难稳定定位;第二,Java 层可能做了参数校验或预处理,你看到的input已被修改;第三,也是最关键的,native 层是x-sign的最终计算者,Hook 它,你拿到的就是服务端真正校验的那个字符串。我在某鱼 8.12.0 上对比过:Hook JavagenerateSign,input是一个String,内容是timestamp|path|query|sessionKey的拼接;而 Hook nativenativeGenerateSign,input是一个byte[],内容是经过字节编码、长度前缀、二进制填充后的原始 buffer。后者才是真相。粒度(Granularity):你是 Hook 整个
nativeGenerateSign,还是 Hook 它内部的sub_12340和sub_56780?答案是:先 Hook 大函数,再 Hook 小函数。大函数(nativeGenerateSign)给你全局视角,确认输入输出是否符合预期;小函数(sub_12340)给你局部洞察,帮你理解每一步变换。我在sub_12340的onEnter里打印args[0](即inputbuffer 的首地址),然后用Memory.readByteArray(args[0], 64)读取,发现前 8 字节确实是时间戳,接下来 4 字节是 path 长度……这和 Unidbg 里看到的完全一致。这种跨环境的一致性验证,是 Frida 最大的价值之一。
3.2 从 Frida Hook 到 Trace 调试:如何让日志变成可执行的分析报告
仅仅打印input和output是不够的。你需要把 Frida 的日志,变成一份可回溯、可验证、可自动化的分析报告。我的做法是:为每一次x-sign生成,生成一个唯一的 trace_id,并将所有关键状态(输入、中间态、输出、耗时)以 JSON 格式发送到本地服务器。
// frida-trace.js const traceId = Math.random().toString(36).substr(2, 9); const startTime = Date.now(); Interceptor.attach(Module.findExportByName('libsecurity.so', 'Java_com_taobao_idlefish_security_SignUtil_nativeGenerateSign'), { onEnter: function(args) { this.traceId = traceId; this.startTime = startTime; // 读取 input buffer const inputLen = args[1].toInt32(); const inputBytes = Memory.readByteArray(args[1], inputLen); this.inputHex = inputBytes ? bytesToHex(inputBytes) : 'null'; // 记录调用栈(可选,用于定位调用者) this.callStack = Thread.backtrace(this.context).map(DebugSymbol.fromAddress).join(';'); }, onLeave: function(retval) { const endTime = Date.now(); const outputStr = retval.readCString(); const outputHex = outputStr ? strToHex(outputStr) : 'null'; // 发送 trace 数据到本地 HTTP server const xhr = new XMLHttpRequest(); xhr.open('POST', 'http://127.0.0.1:8000/trace', false); xhr.setRequestHeader('Content-Type', 'application/json'); xhr.send(JSON.stringify({ trace_id: this.traceId, input_hex: this.inputHex, output_str: outputStr, output_hex: outputHex, duration_ms: endTime - this.startTime, call_stack: this.callStack, timestamp: new Date().toISOString() })); } }); function bytesToHex(bytes) { if (!bytes) return ''; return Array.from(bytes, byte => byte.toString(16).padStart(2, '0')).join(''); } function strToHex(str) { return Array.from(str, c => c.charCodeAt(0).toString(16).padStart(2, '0')).join(''); }同时,我在本地起一个 Python Flask 服务:
# trace-server.py from flask import Flask, request import json import os app = Flask(__name__) @app.route('/trace', methods=['POST']) def receive_trace(): data = request.get_json() # 保存到文件,按日期分目录 date_str = datetime.now().strftime('%Y%m%d') os.makedirs(f'traces/{date_str}', exist_ok=True) filename = f'traces/{date_str}/{data["trace_id"]}.json' with open(filename, 'w') as f: json.dump(data, f, indent=2) return 'OK' if __name__ == '__main__': app.run(host='0.0.0.0', port=8000)这样,每次 App 生成一个x-sign,我就会在traces/20240405/abc123.json里得到一份完整的记录。我可以写一个简单的 Python 脚本,批量分析这些 JSON:
# analyze_traces.py import glob import json from collections import defaultdict # 统计不同 input_len 出现的频率 len_count = defaultdict(int) for file in glob.glob('traces/20240405/*.json'): with open(file) as f: data = json.load(f) len_count[data['input_hex'][:20]] += 1 # 取 input 前20字符做 hash # 找出最常见的 input pattern most_common = sorted(len_count.items(), key=lambda x: x[1], reverse=True)[0] print(f"Most common input prefix: {most_common[0]} (count: {most_common[1]})")这种“日志即数据”的思路,把 Frida 从一个调试工具,升级为一个协议分析平台。你不再需要肉眼翻几百行 log,而是可以用 SQL 或 Pandas 做聚合分析,快速发现规律。
3.3 Frida 的高级技巧:绕过反调试、劫持内存、伪造返回值
某鱼的libsecurity.so在nativeGenerateSign函数开头,插入了一段反调试代码:
mov x8, #0x1000 svc #0x1000 ; __NR_ptrace, 检查是否被 trace cbz w0, loc_success ; 如果 ptrace 返回 0,说明没被 trace,跳转 mov x0, #0 ret ; 直接返回空字符串这段代码会让 Frida 的Interceptor.attach失效,因为onEnter还没执行,函数就已经返回了。怎么办?两个方案:
方案一:Inline Hook 替换svc #0x1000指令
用 Frida 的Memory.patchCode,在nativeGenerateSign的入口地址,将svc #0x1000(机器码0x00, 0x00, 0x10, 0xd4)替换为mov x0, #0(0x00, 0x00, 0x00, 0x52),强制让反调试检查永远通过。
const base = Module.findBaseAddress('libsecurity.so'); const entryAddr = base.add(0x12340); // nativeGenerateSign 入口 Memory.patchCode(entryAddr, 4, function(code) { const cw = new Arm64Writer(code, { pc: entryAddr }); cw.putMovRegU32('x0', 0); // mov x0, #0 cw.flush(); });方案二:在svc指令后下断点,修改x0寄存器
更优雅的做法是,不 patch 代码,而是在svc指令执行后、cbz指令执行前,修改x0的值。svc指令的下一条指令地址是entryAddr.add(4),我们在那里下断点:
Interceptor.attach(entryAddr.add(4), { onEnter: function() { // svc 执行后,x0 是 ptrace 的返回值,我们强制设为 0 this.context.x0 = 0; } });这个技巧的精髓在于:你不是在对抗反调试,而是在利用它的逻辑漏洞。反调试代码的作者假设svc的返回值是可信的,而你只需在它读取这个值之前,把它改成你想要的样子。
另一个高级技巧是劫持内存,伪造input。有时,你想测试某个特定input会生成什么x-sign,但这个input在正常流程中永远不会出现。这时,你可以在onEnter里,用Memory.writeByteArray直接修改args[1]指向的内存:
onEnter: function(args) { // 构造一个测试 input:全 0x01 const testInput = new Uint8Array(64).fill(0x01); Memory.writeByteArray(args[1], testInput); }注意:args[1]是input的指针,Memory.writeByteArray会直接覆盖其内容。这比在 Java 层 Hook 然后return "test_sign"更底层、更可靠,因为它发生在计算之前,服务端收到的就是你伪造的input计算出的x-sign。
4. Unidbg 与 Frida 的协同作战:构建你的 x-sign 自动化生成器
单用 Unidbg,你只能离线验证;单用 Frida,你只能在线观测。只有把两者结合起来,才能构建出真正可用的x-sign自动化生成器——一个能输入任意 URL 和参数,输出合法x-sign的命令行工具。这个工具的核心,就是把 Unidbg 的“可控执行”和 Frida 的“实时反馈”融合在一起。
4.1 架构设计:三层模型驱动自动化
我设计了一个三层架构:
- 输入层(Input Layer):接收用户输入的
url、method、headers、body,并按某鱼的规则,拼接成inputbuffer。这一层完全复用 Unidbg 脚本里的input构造逻辑,用 Python 重写,确保一致性。 - 执行层(Execution Layer):调用 Unidbg 的 Java API,加载
libsecurity.so,传入input,获取x-sign。这里的关键是,Unidbg 的Emulator可以被封装成一个 Python 可调用的模块(通过 Jython 或 Py4J),但我选择了更轻量的方式:用subprocess启动一个独立的 Java 进程,通过标准输入/输出传递数据。 - 验证层(Verification Layer):将 Unidbg 生成的
x-sign,用 Frida 注入到真机 App 中,发起一次真实请求,验证其有效性。如果失败,则自动记录失败的input,并启动 Unidbg 进行深度 trace,找出差异点。
# sign_generator.py import subprocess import json import sys def build_input(url, body=None): """按某鱼 8.12.0 规则构建 input buffer""" from urllib.parse import urlparse, parse_qs parsed = urlparse(url) path = parsed.path.encode('utf-8') query = parsed.query.encode('utf-8') # session_key = md5(imei + android_id + ts).hexdigest() session_key = b'a1b2c3d4e5f67890a1b2c3d4e5f67890' # 简化版 input_bytes = bytearray(64) # 时间戳 (8 bytes, little-endian) ts = int(time.time() * 1000) input_bytes[0:8] = ts.to_bytes(8, 'little') # path len (4 bytes) input_bytes[8:12] = len(path).to_bytes(4, 'little') # path string input_bytes[12:12+len(path)] = path # query len (4 bytes) input_bytes[12+len(path):12+len(path)+4] = len(query).to_bytes(4, 'little') # query string input_bytes[12+len(path)+4:12+len(path)+4+len(query)] = query # session_key (32 bytes) input_bytes[12+len(path)+4+len(query):] = session_key return bytes(input_bytes) def generate_sign(input_bytes): """调用 Unidbg Java 进程生成 sign""" # 将 input_bytes 写入临时文件 with open('/tmp/input.bin', 'wb') as f: f.write(input_bytes) # 启动 Unidbg 进程 result = subprocess.run( ['java', '-cp', 'unidbg.jar:lib/', 'SignGenerator', '/tmp/input.bin'], capture_output=True, text=True ) if result.returncode != 0: raise Exception(f"Unidbg failed: {result.stderr}") return result.stdout.strip() def verify_sign(url, x_sign): """用 Frida 注入,发起真实请求验证""" # Frida 脚本会 hook OkHttp 的 Interceptor,将 x-sign 注入到请求头 # 并捕获响应状态码 result = subprocess.run( ['frida', '-U', '-f', 'com.taobao.idlefish', '-l', 'verify.js', '--no-pause'], input=f'{url}\n{x_sign}\n', text=True, capture_output=True ) return '200 OK' in result.stdout if __name__ == '__main__': url = sys.argv[1] if len(sys.argv) > 1 else 'https://idlefish.taobao.com/api/item/detail?itemId=12345' input_buf = build_input(url) sign = generate_sign(input_buf) print(f"x-sign: {sign}") # 自动验证(可选) if len(sys.argv) > 2 and sys.argv[2] == '--verify': is_valid = verify_sign(url, sign) print(f"Verification: {'PASS' if is_valid else 'FAIL'}")对应的SignGenerator.java:
// SignGenerator.java public class SignGenerator { public static void main(String[] args) throws Exception { if (args.length == 0) { System.err.println("Usage: java SignGenerator <input_file>"); return; } // 读取 input 文件 byte[] input = Files.readAllBytes(Paths.get(args[0])); // 复用前面的 Unidbg 逻辑... Emulator