a_bogus纯算(V1.0.1.19-fix.01)逆向全流程:从日志插桩到算法复现
1. 逆向分析a_bogus算法的完整技术流程
最近在研究某平台的接口加密时,遇到了一个叫a_bogus的参数。这个参数看起来像是一串随机字符,但实际上是通过复杂算法生成的。经过一周的逆向分析,我终于搞清楚了它的生成逻辑。下面就把这个完整的逆向过程分享给大家,希望能帮助到有类似需求的开发者。
这个a_bogus参数在V1.0.1.19-fix.01版本中的生成过程相当复杂,涉及到多层加密和转换。整个过程可以分为几个关键步骤:环境准备、日志插桩、数据收集、日志分析和算法复现。我会按照这个顺序详细讲解每个环节的具体操作。
2. 环境准备与日志插桩
2.1 开发环境搭建
首先需要准备好开发环境,我使用的是以下工具组合:
- 带有DevTools的Edge浏览器
- VS Code用于查看和分析日志
- PyCharm用于编写处理脚本
在实际操作中,我发现直接使用浏览器控制台输出的日志量太大,很容易导致浏览器卡死。于是写了一个日志收集和下载的工具函数,这个函数可以将日志分块保存到本地。
// 将对象转为字符串便于存储 get_obj_str = function(one_obj) { try { return JSON.stringify(one_obj, function(k, v) { if (v == window) return 'window'; else if (typeof v == 'function') return `function ${v.name}`; else if (v && v[Symbol.toStringTag]) return `Symbol ${v[Symbol.toStringTag]}`; else return v; }) } catch (e) {}; } // 添加日志并自动分块下载 add_json_str = function(param_name, param) { var add_log = function(log_str) { var save_log = function(textContent, file_name) { const blob = new Blob([textContent], { type: 'text/plain' }); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.style.display = 'none'; a.href = url; a.download = file_name; document.body.appendChild(a); a.click(); setTimeout(() => { document.body.removeChild(a); window.URL.revokeObjectURL(url); }, 0); } window.my_code += log_str; if (window.my_code.length >= 1024 * 1024 * 5) { // 每5MB分块 save_log(window.my_code, `logs${window.my_offset}.txt`) window.my_offset += 1; window.my_code = ""; } } add_log(param_name + " = " + get_obj_str(param) + "\n\n"); };2.2 定位关键函数
通过分析网络请求,我发现a_bogus参数是在bdms模块中生成的。具体定位方法是:
- 打开开发者工具,找到包含a_bogus的请求
- 在Fetch/XHR筛选器中找到对应的数据包
- 查看发起程序的调用栈,发现a_bogus在bdms模块中生成
多次实验后发现,bdms的入口是5704堆栈,出口是index堆栈。a_bogus就是在入口和出口之间生成的。虽然入口和出口有时会变化,但分析方法是一样的。
2.3 关键函数插桩
找到入口后,我们需要在关键函数内部插入日志点。a_bogus的生成主要在d函数内部,所以重点在这里插桩。插桩的技巧是:
- 首先在运算操作符加赋值的地方插桩
- 观察d函数,发现使用了v和s两个内存栈
- 设置条件断点,记录关键变量的值
// 在d函数内部设置的条件断点示例 if (t < 2) if (0 === t) { var r = o[a++]; p -= r; var e = v.slice(p + 1, p + r + 1) , n = v[p--] , d = v[p--]; if ("function" != typeof n) return f = 3, void (l = new TypeError(typeof n + " is not a function")); var y = V.get(n); if (y) h.push([o, i, u, s, c, a, f, l]), g(y[0], d, e, y[1]); else { var m = n.apply(d, e); v[++p] = m // 插桩记录apply调用结果 add_json_str("v_apply",v),add_json_str("s_apply",s),add_json_str(`${get_obj_str(n)}.apply(${get_obj_str(d)}, ${get_obj_str(e)})`,m),false } }3. 日志分析与算法推导
3.1 日志预处理
收集到的日志文件通常很大,我这次收集的日志超过了1GB。处理这种大文件,VS Code表现很不错。为了减少日志体积,可以进行以下优化:
- 替换重复的长字符串为短变量名
- 删除不必要的中间数据
- 合并多个日志文件
我写了一个Python脚本来处理日志合并和替换:
import os def replace_log(tup_list, log_file_path): data_lines = [] with open(log_file_path, 'r', encoding='utf-8', errors='replace') as file: data_lines = [line.strip() for line in file.readlines()] for i in range(len(data_lines)): if data_lines[i]: for old_str, new_str in tup_list: data_lines[i] = data_lines[i].replace(old_str, new_str) for old_str, new_str in tup_list: data_lines.append(f'{new_str}={old_str}') os.remove(log_file_path) with open(log_file_path, 'w', encoding='utf-8') as file: file.write('\n'.join(data_lines)) # 替换用户代理字符串等长文本 replace_list = [ ('"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.0.0 Safari/537.36 Edg/133.0.0.0"', 'ua') ] replace_log(replace_list, '/path/log.txt')3.2 逆向分析a_bogus生成逻辑
分析日志要从a_bogus完全生成的位置开始,然后倒推它的生成过程。我发现a_bogus的生成主要分为几个阶段:
- 基础字符串拼接
- 多层加密转换
- 最终编码处理
具体来说,a_bogus是通过以下步骤生成的:
// a_bogus生成的核心逻辑 function get_raw_ab(lm_get_ab_n, key_str) { var s = "", bw = 0; for (var i = 0; i < lm_get_ab_n.length; i += 3) { var cl = 16; var tcz = 0; var sof = 16515072; for (var j = i; j < i + 3; j++) { if (j < lm_get_ab_n.length) { var tlcy = lm_get_ab_n.charCodeAt(j) & 255; tcz = tcz | (tlcy << cl); cl -= 8; } else { bw += 1; // 计算补位 } } for (var h = 18; h >= (6 * bw); h -= 6) { var tsz = tcz & sof; s += key_str[tsz >> h]; sof = sof / 64; } s += '='.repeat(bw); } return s; }这个函数的核心是将输入字符串每3个字符一组进行处理,通过位运算和查表生成最终的a_bogus值。其中key_str是一个特殊的编码表:
info_dic = { "s4": "Dkdpgh2ZmsQB80/MfvV36XI1R45-WUAlEixNLwoqYTOPuzKFjJnry79HbGcaStCe" }3.3 关键算法解析
通过分析日志,我发现a_bogus的生成用到了类似Base64的编码方式,但有几点关键区别:
- 使用自定义的编码表而非标准Base64表
- 输入字符串先经过多层转换
- 包含特殊的补位逻辑
具体编码过程如下:
- 将输入字符串按3字节分组
- 每组24位拆分为4个6位片段
- 每个6位片段作为索引查自定义编码表
- 根据剩余字节数添加补位字符
逆向这个算法的难点在于理解中间的转换过程。我通过日志发现,在进入编码函数前,原始输入已经经过了多次转换:
原始输入 -> 加密转换1 -> 加密转换2 -> 最终编码 -> a_bogus4. 算法复现与验证
4.1 复现加密流程
基于日志分析,我逐步复现了整个加密流程。下面是关键步骤的代码实现:
// 生成中间字符串 function get_lm_g_ab(lm_g_lm_n) { var fixed_sz256_n = [194, 249, 255, 165, 114, 67, 251, 187, 174, 231, 164, 237, 124, 235, 68, 83, 206, 79, 142, 167, 30, 77, 0, 93, 118, 29, 32, 161, 2, 171, 243, 179, 42, 170, 223, 119, 98, 222, 219, 57, 245, 135, 197, 13, 186, 202, 88, 184, 214, 12, 76, 185, 116, 74, 54, 53, 104, 208, 158, 163, 82, 173, 253, 240, 172, 63, 191, 207, 25, 15, 201, 203, 215, 236, 183, 233, 145, 127, 72, 6, 16, 10, 228, 35, 232, 159, 66, 168, 108, 71, 217, 75, 33, 155, 112, 128, 36, 24, 138, 50, 211, 23, 107, 14, 247, 137, 175, 242, 234, 157, 199, 49, 139, 85, 81, 17, 180, 86, 120, 78, 51, 205, 169, 148, 181, 3, 94, 106, 252, 220, 150, 47, 151, 84, 212, 18, 149, 182, 100, 123, 121, 156, 154, 152, 126, 204, 60, 133, 132, 248, 7, 91, 58, 59, 20, 97, 113, 117, 131, 46, 250, 224, 21, 73, 146, 31, 193, 69, 140, 125, 9, 39, 89, 5, 65, 141, 218, 80, 1, 70, 64, 166, 87, 189, 55, 147, 22, 26, 143, 61, 144, 99, 92, 44, 129, 130, 227, 103, 90, 192, 198, 244, 136, 101, 246, 153, 56, 38, 4, 178, 221, 162, 134, 37, 111, 28, 216, 96, 102, 210, 254, 196, 195, 230, 241, 62, 11, 122, 52, 40, 41, 229, 226, 225, 48, 45, 160, 105, 8, 115, 34, 43, 209, 95, 239, 190, 188, 109, 27, 19, 176, 213, 200, 238, 177, 110]; var z = 0; var st = ""; for (var i = 0; i < lm_g_lm_n.length; i++) { var a = (i + 1) % 256; var c = fixed_sz256_n[a]; z = (z + c) % 256; var e = fixed_sz256_n[z]; fixed_sz256_n[a] = e; fixed_sz256_n[z] = c; var g = (e + c) % 256; var h = lm_g_lm_n.charCodeAt(i); var j = fixed_sz256_n[g]; var k = h ^ j; var l = String.fromCharCode(k); st += l; } return st; }4.2 完整算法实现
将各个步骤组合起来,就得到了完整的a_bogus生成算法:
function generate_a_bogus(dpf, ua) { // 1. 初始化变量和编码表 var info_dic = { "s4": "Dkdpgh2ZmsQB80/MfvV36XI1R45-WUAlEixNLwoqYTOPuzKFjJnry79HbGcaStCe" }; // 2. 生成随机数种子 Math.random = function() { return 0.5 }; // 固定随机数便于调试 // 3. 多层加密转换 var lm_get_lm = generate_lm(dpf, ua); var lm_get_ab_tail = get_lm_g_ab(lm_get_lm); var lm_get_ab_head4 = generate_head4(); var lm_get_ab = lm_get_ab_head4 + lm_get_ab_tail; // 4. 最终编码 var a_bogus = get_raw_ab(lm_get_ab, info_dic.s4); return a_bogus; }4.3 验证算法准确性
为了验证算法的正确性,我对比了生成的a_bogus和实际请求中的值:
// 测试用例 var test_ab = generate_a_bogus(dpf, ua); console.log(test_ab === "O7UfhzU7dNmfCdMbuKpByXrUzhVMrPSyaiTobOMTSDTGT1tbUSN2EcGaJooLiiLyRbB0ie3H0ETMGDVcKsUkZK9pKmZDuPsS5G2nVg0L0qwXYzhsLqRiCh0Fww0C8RtL-5cRi1WRIssx1EAl9qIDABIaH5Pq-O8pRrZVp/YycDcWpBgTVx/CenzWfwj=");5. 关键技术与难点解析
5.1 自定义编码算法
a_bogus使用的编码算法与Base64类似但有重要区别:
- 编码表完全不同
- 输入预处理更复杂
- 包含额外的加密步骤
核心编码函数的关键部分:
for (var h = 18; h >= (6 * bw); h -= 6) { var tsz = tcz & sof; s += key_str[tsz >> h]; sof = sof / 64; }这段代码将24位数据分成4个6位片段,然后查表编码。sof初始值为16515072(0xFC0000),每次右移6位。
5.2 多层加密转换
在进入最终编码前,原始数据经过了多层转换:
- 用户代理(UA)字符串加密
- 请求参数(dpf)加密
- 时间戳混合运算
- 随机数生成
这些转换使得直接猜测a_bogus的生成逻辑变得非常困难。
5.3 动态密钥数组
算法中使用了一个256字节的固定数组作为加密密钥:
var fixed_sz256_n = [194, 249, 255, 165, ... , 177, 110];这个数组虽然固定,但它的生成过程相当复杂,通过多步数学运算和位操作得到。
6. 实际应用中的注意事项
6.1 时间戳的影响
在逆向过程中,我发现a_bogus的生成与时间戳密切相关。即使其他参数完全相同,不同时间生成的a_bogus也会不同。这增加了逆向的难度,也说明服务端可能在校验时间有效性。
6.2 随机数的处理
算法中使用了Math.random()生成随机数,但在实际请求中,这些随机数被固定了。这意味着:
- 服务端可能不校验随机性
- 可以使用固定值代替随机数
- 但随机数的存在增加了逆向难度
6.3 参数更新机制
在测试中发现,即使使用完全相同的参数,生成的a_bogus在一段时间后也会失效。这表明:
- 服务端可能定期更换加密参数
- 需要动态更新算法中的某些固定值
- 完全逆向的算法可能需要定期维护
7. 完整实现代码
以下是完整的a_bogus生成算法实现,包含了所有关键步骤:
function generate_full_a_bogus(dpf, ua) { // 1. 初始化编码表 const info_dic = { "s0": "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=", "s1": "Dkdpgh4ZKsQB80/Mfvw36XI1R25+WUAlEi7NLboqYTOPuzmFjJnryx9HVGcaStCe=", "s2": "Dkdpgh4ZKsQB80/Mfvw36XI1R25-WUAlEi7NLboqYTOPuzmFjJnryx9HVGcaStCe=", "s3": "ckdp1h4ZKsUB80/Mfvw36XIgR25+WQAlEi7NLboqYTOPuzmFjJnryx9HVGDaStCe", "s4": "Dkdpgh2ZmsQB80/MfvV36XI1R45-WUAlEixNLwoqYTOPuzKFjJnry79HbGcaStCe" }; // 2. 固定随机数便于调试 Math.random = function() { return 0.5 }; // 3. 生成加密所需的中间数据 const t1 = Date.now(); const t2 = t1 - 1 + Math.floor(Math.random() * 3); const t3 = t1 + Math.floor(Math.random() * 12) + 4; const t4 = t1 + Math.floor(Math.random() * 900) + 100; // 4. 多层加密转换 const lm_get_EP = generate_lm_g_EP(ua); const EP = get_raw_ab(lm_get_EP, info_dic.s3); const eEP = enc_sum(EP); const eedp = enc_sum(enc_sum(dpf + 'dhzx')); // 5. 生成最终a_bogus const szenc_o95 = generate_szenc_o95(t1, t2, t3, t4, eedp, eEP); const szenc_tail = get_szenc_tail(szenc_o95); const szenc = generate_szenc_head8().concat(szenc_tail); const lm_get_lm = get_list_str(szenc); const lm_get_ab_tail = get_lm_g_ab(lm_get_lm); const lm_get_ab = generate_lm_g_ab_head4() + lm_get_ab_tail; const a_bogus = get_raw_ab(lm_get_ab, info_dic.s4); return a_bogus; }8. 逆向经验总结
通过这次对a_bogus算法的完整逆向,我总结了几点重要经验:
- 系统性分析:从结果倒推,逐步分析每个中间步骤
- 日志优化:合理处理日志,减少干扰信息
- 模块化验证:将复杂算法拆分为多个小模块分别验证
- 动态调试:结合断点调试和日志分析,理解执行流程
- 耐心细致:复杂算法逆向需要极大的耐心和细心
这个案例也展示了现代Web应用如何通过多层加密和混淆来保护接口安全。作为开发者,理解这些机制不仅能帮助我们解决爬虫问题,也能提升自身的安全开发能力。
最后需要强调的是,本文的技术内容仅供学习研究之用。在实际应用中,请遵守相关法律法规和网站的使用条款。逆向工程应该以学习和提升技术为目的,而不是用于非法用途。
