JS逆向解密教程4:深入某吧发帖接口Acs-Token解密全过程

JS逆向解密教程4:深入某吧发帖接口Acs-Token解密全过程

在前几期的JS逆向教程中,我们已经成功解决了某吧发帖接口/f/commit/post/add请求体中的三个关键加密参数:jtmouse_pwd_BSK。今天,我们将攻克最后一个堡垒——请求头中的Acs-Token参数。这个参数是某吧发帖接口的重要组成部分,掌握了它的生成方式,就能实现完整的自动化发帖功能。

本文目录

  1. 数据抓包:定位Acs-Token请求头
  2. 全局搜索:追踪getAcsSign方法
  3. 代码分析:理解getAcsSign逻辑结构
  4. 深度追踪:从getSign到gs方法
  5. 控制台验证:确认加密入口
  6. 代码扣取:获取acs-2027.js文件
  7. 环境补全:创建env.js环境文件
  8. 本地测试:验证加密结果
  9. 常见问题与解决方案
  10. 获取完整课件
1. 数据抓包:定位Acs-Token请求头

首先打开浏览器开发者工具(F12),在Network面板中勾选”Preserve log”。在某吧中任意回复一条内容,观察抓包结果。在发送的请求中,我们重点关注请求头(Headers)部分。

通过仔细查看,可以发现目标接口的请求头中包含关键的Acs-Token参数,其值是一长串看似随机的加密字符串,格式通常为P1_时间戳_时间戳_随机字符串...。这正是我们需要进行 **Acs-Token解密** 分析和逆向的目标。

在请求头中定位Acs-Token参数
图1: 在请求头中定位Acs-Token参数

定位到参数后,下一步是在源代码中搜索其生成位置。在Sources面板使用Ctrl+Shift+F进行全局搜索,关键词为Acs-Token

搜索结果中,我们找到了关键代码段:

t.ackSdk = t.requireInstance("common/widget/AcsSdk"),
            t.ackSdk.getAcsSign(function(e) {
                $.ajax({
                    type: "get",
                    url: "/home/get/panel",
                    data: a,
                    dataType: "json",
                    headers: {
                        "Acs-Token": e // 这里给请求头赋值
                    }
                })

从代码中可以清晰看到,Acs-Token的值e是由getAcsSign方法通过回调函数返回的。这为我们解密整个 某吧发帖接口 的加密流程指明了分析方向。

全局搜索Acs-Token定位关键代码
图2: 全局搜索Acs-Token定位关键代码
3. 代码分析:理解getAcsSign逻辑结构

接下来,我们全局搜索getAcsSign,寻找其具体实现。在众多搜索结果中,我们找到了定义该函数的地方:

 getAcsSign: function(s) {
            if (window.paris_2027) {
                var a = !1
                  , i = setTimeout(function() {
                    a = !0,
                    $.stats.track("get_acs_sign", "acs_sign_fail_603", "pb_common", "view"),
                    s("tb_603")
                }, 5e3);
                try {
                    window.paris_2027.getAcsInstance(function(t, c) {
                        if (!a) {
                            if (t) {
                                var e = t.code || 600;
                                return clearTimeout(i),
                                void s(e)
                            }
                            if (!c || !c.getSign)
                                return clearTimeout(i),
                                c ? $.stats.track("get_acs_sign", "acs_sign_fail_606", "pb_common", "view") : $.stats.track("get_acs_sign", "acs_sign_fail_604", "pb_common", "view"),
                                void s("tb_604");
                            var o = !1
                              , _ = setTimeout(function() {
                                o = !0,
                                $.stats.track("get_acs_sign", "acs_sign_fail_605", "pb_common", "view"),
                                s("tb_605")
                            }, 5e3);
                            c.getSign(function(t, c) {
                                if (clearTimeout(i),
                                clearTimeout(_),
                                !a && !o) {
                                    if (t) {
                                        var e = t.code || 600;
                                        return void s(e)
                                    }
                                    s(c) // 最终通过这里返回加密结果
                                }
                            })
                        }
                    })
                } catch (t) {
                    $.stats.track("get_acs_sign", "acs_sign_fail_602", "pb_common", "view", {
                        errMsg: t.message
                    }),
                    s("tb_602")
                }
            } else
                $.stats.track("get_acs_sign", "acs_sign_fail_601", "pb_common", "view"),
                s("tb_601")
        } 

分析这段代码,我们发现:

  • 函数首先检查window.paris_2027对象是否存在
  • 通过window.paris_2027.getAcsInstance获取实例
  • 最终通过c.getSign方法生成签名,并通过回调函数s(c)返回结果
  • 包含了丰富的超时处理和错误追踪机制
4. 深度追踪:从getSign到gs方法

继续深入分析,我们需要找到getSign方法的实现。通过断点调试和堆栈跟踪,我们找到了getSign函数:

getSign = function(t, e) {
            var r = this.monitor
              , n = f()
              , o = function(e, o) {
                var i = f() - n;
                e ? r.error(g, i, e) : r.success(g, i),
                t(e, o)
            };
            this.loadController.getTarget((function(t, r) {
                if (t)
                    return o(t);
                try {
                    r.gs((function(t, e) {   // 这里是关键加密调用 
                        o(e, t)
                    }
                    ), e)
                } catch (n) {
                    n = s.create(553, n),
                    o(n)
                }
            }
            ))
        }

在这个函数中,我们发现了最关键的调用:r.gs()。这个gs方法就是最终完成 请求头加密 、生成Acs-Token值的核心加密函数。通过单步调试进入gs方法内部,我们发现其中包含了复杂的加密运算和逻辑,这表明我们已经到达了加密算法的核心层。

图3: 在控制台验证gs函数生成结果
5. 控制台验证:确认加密入口

在确认r.gs是加密入口后,我们需要验证这个函数确实能生成正确的Acs-Token。我们在浏览器控制台中执行以下测试代码:

// 在控制台中验证gs函数 r.gs((function(t, e) { console.log("生成的Acs-Token:", t); }), e);

执行后,控制台输出类似这样的结果:

P1_1758528006625_1758531152558_g84BgtFBvvtb..............

这个结果与我们在请求头中看到的Acs-Token格式完全一致,确认了r.gs就是我们要找的加密函数入口。

在控制台验证gs函数生成Acs-Token解密值
图4: 在控制台验证gs函数生成结果
6. 代码扣取:获取acs-2027.js文件

确认加密入口后,接下来就是 JS逆向 的常规操作——扣取相关代码。通过分析,我们发现gs方法所在的文件是acs-2027.js。这正是我们在系列 JS逆向教程 中,继 数据抓包实战 后最关键的代码分析阶段。我们在Sources面板中找到这个文件,并将其完整内容复制保存。


acs-2027.js:

// 拷贝acs-2027.js 完整内容  放在这上面
下面是增加的代码
function ASC() {
    var e, result;
    ACS_2027.gs((function(t, e) {
        result =  t;
    }), e);
    return result; // 将ACS_2027.gs函数返回的结果返回,以便外部变量x能接收到
};

module.exports = {  导出
	ASC
};

这个文件通常包含大量的加密函数、辅助方法和逻辑判断,是实现Acs-Token生成的核心文件。

7. 环境补全:创建env.js环境文件

由于浏览器环境与Node.js环境存在差异,我们需要创建一个环境补全文件env.js,来模拟浏览器中的各种对象和方法。
env.js:

var catvm = {};
//框架运行内存
catvm.memory = {
	config:{print:false,proxy:false},
};//默认关闭打印
//主要保护主要的伪造函数
;;
(() => {
	"use strict";
	const $toString = Function.toString;
	const myFunction_toString_symbol = Symbol('('.concat('', ')_', (Math.random() + '').toString(36)));
	const myToString = function() {
		return typeof this == 'function' && this[myFunction_toString_symbol] || $toString.call(this)
	};

	function set_native(func, key, value) {
		Object.defineProperty(func, key, {
			enumerable: false,
			configurable: true,
			writable: true,
			value: value,
		})
	};
	delete Function.prototype['toString']; //删除原型链上的toString
	set_native(Function.prototype, 'toString', myToString); //换成自定义个getter方法
	set_native(Function.prototype.toString, myFunction_toString_symbol,
	'function toString() { [native code] }'); //套个娃 保护一下我们定义的toString 否则暴漏
	catvm.safefunction = (func) => {
		set_native(func, myFunction_toString_symbol, `function ${myFunction_toString_symbol,func.name || ''}(){ [native code] }`);
	}; //导出函数到globalThis

}).call(this);

//框架日志功能

catvm.print = {}
catvm.memory.print = [];
catvm.print.log = function()
{
	if(catvm.config.print)
	{
		
	}
}

catvm.print.getall = function()
{
	
}
//框架代理功能

catvm.proxy = function(o)
{
	if(catvm.memory.config.proxy == false){return o};
	return new Proxy(o,
	{
		get: function(target, property, receiver) {
		
		console.log("方法:", "get  ", "对象:", target.constructor.name  ,"  属性:", property,  "  属性值类型:",target[property]);
		return target[property];
		},
		
		set: function(target, property, value, receiver) {
			console.log("方法:", "set  ", "对象:", target.constructor.name  ,"  属性:", property,"  属性值类型:",target[property]);
		
		      return Reflect.set(...arguments);
		    }
	});
}


var EventTarget = function EventTarget(){};
catvm.safefunction(EventTarget);

Object.defineProperties(EventTarget.prototype,{
    [Symbol.toStringTag]:
    {   
        value: 'EventTarget',
        configurable: true,
      
    }
    
});

EventTarget.prototype.addEventListener = function addEventListener(){};
catvm.safefunction(EventTarget.prototype.addEventListener);

EventTarget.prototype.dispatchEvent = function dispatchEvent(){};
catvm.safefunction(EventTarget.prototype.dispatchEvent);

EventTarget.prototype.removeEventListener = function removeEventListener(){};
catvm.safefunction(EventTarget.prototype.removeEventListener);

EventTarget= catvm.proxy(EventTarget)

var WindowProperties = function WindowProperties(){};
catvm.safefunction(WindowProperties);

Object.defineProperties(WindowProperties.prototype,{
    [Symbol.toStringTag]:
    {   
        value: 'WindowProperties',
        configurable: true,
      
    }
    
});
WindowProperties.prototype.__proto__ = EventTarget.prototype;



window = this

var Window = function Window(){
    throw new TypeError('Illegal constructor')
};catvm.safefunction(Window);

Object.defineProperties(Window.prototype,{
    [Symbol.toStringTag]:
    {   
        value: 'Window',
        configurable: true,
      
    }
    
});
Window.prototype.PERSISTENT = 1;
Window.prototype.TEMPORARY = 0;


Window.prototype.__proto__ = WindowProperties.prototype;

window.setTimeout = function setTimeout(){debugger;};catvm.safefunction(window.setTimeout);

window.clearTimeout= function clearTimeout(){debugger;};catvm.safefunction(window.clearTimeout);

window.setInterval= function setInterval(){debugger;};catvm.safefunction(window.setInterval);

window.clearInterval= function clearInterval(){debugger;};catvm.safefunction(window.clearInterval);
// window.ACS_2027 = function ACS_2027(){};
// Window.$BSB_2027 = function ACS_2027(){};

window.__proto__ = Window.prototype;
window = catvm.proxy(window)
Window = catvm.proxy(Window)
var Location = function Location(){
    throw new TypeError('Illegal constructor')
};catvm.safefunction(Location);

Object.defineProperties(Location.prototype,{
    [Symbol.toStringTag]:
    {   
        value: 'Location',
        configurable: true,
      
    }
    
});

location = {}
location.__proto__ = Location.prototype;
location= catvm.proxy(location)
var Navigator = function Navigator(){
    throw new TypeError('Illegal constructor')
};catvm.safefunction(Navigator);

Object.defineProperties(Navigator.prototype,{
    [Symbol.toStringTag]:
    {   
        value: 'Navigator',
        configurable: true,
      
    }
    
});



navigator = {}
navigator.__proto__ = Navigator.prototype;

for(var prototype_ in Navigator.prototype)
{
    navigator[prototype_] = Navigator.prototype[prototype_];
    Navigator.prototype.__defineGetter__(prototype_,function(){
        throw new TypeError('Illegal constructor');
    });
}
Navigator.prototype.userAgent = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36';


navigator= catvm.proxy(navigator)
var History = function History(){
    throw new TypeError('Illegal constructor')
};catvm.safefunction(History);

Object.defineProperties(History.prototype,{
    [Symbol.toStringTag]:
    {   
        value: 'History',
        configurable: true,
      
    }
    
});

history = {}
history.__proto__ = History.prototype;
history= catvm.proxy(history)
var Document = function Document(){
    throw new TypeError('Illegal constructor')
};catvm.safefunction(Document);

Object.defineProperties(Document.prototype,{
    [Symbol.toStringTag]:
    {   
        value: 'Document',
        configurable: true,
      
    }
    
});

document = {}
document.__proto__ = Document.prototype;
document.cookie = 'JNsEsNswwuqNCCYMF4SA7aEHgSKuBxg+ZXH8DpJkSFjPm+bApq4gEq36MEvsYSTIed1RA9tXFWJ8+v+ysPkvw4ZjzqN/azefW'
document= catvm.proxy(document)
var Screen = function Screen(){
    throw new TypeError('Illegal constructor')
};catvm.safefunction(Screen);

Object.defineProperties(Screen.prototype,{
    [Symbol.toStringTag]:
    {   
        value: 'Screen',
        configurable: true,
      
    }
    
});

screen = {}
screen.__proto__ = Screen.prototype;
screen= catvm.proxy(screen)
debugger;

env.js 环境补全代码主要完成了以下工作:

  1. 创建全局框架对象:初始化了 catvm 对象,用于管理框架的内存、日志和代理配置。
  2. 保护核心函数:重写了 Function.prototype.toString 方法,使其返回 [native code],防止原生函数被检测出差异。
  3. 模拟浏览器事件目标:构造了 EventTarget 及其原型链上的方法(如 addEventListener),为后续的 Window 等对象提供基础。
  4. 构建完整的 Window 对象:按照浏览器规范,模拟了 WindowProperties -> Window 的原型链,并将 window 对象指向此链。
  5. 注入关键环境变量:创建并模拟了 locationnavigatordocumentscreenhistory 等浏览器环境必备的核心对象。
  6. 实现定时器函数:模拟了 setTimeoutsetInterval 及其清除方法,尽管目前实现为空函数。
  7. 启用对象代理:通过 catvm.proxy 功能,为所有模拟的对象添加了 get 和 set 陷阱,便于调试和监控属性访问。
  8. 设置关键属性:为模拟对象添加了正确的 Symbol.toStringTag 属性,并为 navigator 设置了真实的 userAgent 字符串。

环境补全是JS逆向中最关键的步骤之一,需要根据运行时的具体错误信息不断调整和完善环境模拟。

8. 本地测试:验证加密结果

现在我们可以创建测试文件,在本地环境中验证加密结果。创建test.js文件:

// test.js
const { VM } = require('vm2');
const fs = require('fs');

function getAcsToken() {
    try {
        // 1. 读取两个文件的内容
        const envContent = fs.readFileSync('./env.js', 'utf8');
        const acsContent = fs.readFileSync('./acs-2027.js', 'utf8');

        // 2. 创建虚拟机沙箱
        const vm = new VM({
            sandbox: {
                // 提供一些Node.js基础对象
                console: console,
                require: require,
                module: { exports: {} },
                // 其他需要的变量...
            }
        });

        // 3. 先在把env.js浏览器环境代码和acs-2027.js加密代码合并
       const combinedContent = envContent + acsContent;
        // 4. 再执行 
       const result = vm.run(combinedContent);
        let token = '';
        if (result && result.ASC) {
            for(let i = 1; i < 5; i++) {
                token = result.ASC();
                console.log(`第${i}次尝试生成Token:`, token ? '成功' : '失败');
                if (token) break;
            }
        }

        return token;

    } catch (error) {
        console.error('❌ 生成Acs-Token时发生错误:');
        console.error(error.message);
        return null;
    }
}

// 测试
console.log('开始生成Acs-Token...');
const token = getAcsToken();

if (token) {
    console.log('✅ 生成成功:');
    console.log(token);
} else {
    console.log('❌ 生成失败');
}

test.js 代码分析总结:

1. 为什么需要调用虚拟机(VM2)?

  • 环境隔离与安全env.js 代码会修改全局对象(如 Function.prototype.toString)和原型链。在Node.js主线程中直接运行会污染全局环境,可能导致其他模块崩溃。虚拟机提供了一个沙箱环境,将这些操作隔离起来。
  • 模拟浏览器全局对象acs-2027.js 加密代码依赖于 windowdocument 等浏览器特有的全局对象。Node.js默认没有这些对象,虚拟机沙箱是我们模拟这些对象的最佳场所。
  • 避免冲突:防止我们模拟的全局变量与Node.js内置模块或第三方库的变量发生命名冲突。

2. 为什么需要安装 vm2 模块?

  • 非内置模块:Node.js 原生的 vm 模块功能较弱,安全性差。vm2 是一个第三方库,提供了更强壮、更安全的沙箱环境。
  • 安全性vm2 专门设计用于执行不可信的代码,能有效防止沙箱内的代码逃逸并访问主机系统,比原生 vm 模块安全得多。
  • 功能增强:它提供了更灵活的配置选项(如 sandbox)和更好的调试支持。

总结:使用 vm2 虚拟机是为了安全、隔离地模拟出一个浏览器运行环境,使得为浏览器编写的加密代码能够在Node.js端顺利运行,同时不破坏主程序的安全性。

运行node test.js,如果一切配置正确,应该能够输出与浏览器中格式一致的Acs-Token

本地运行测试验证加密结果
图5: 本地运行测试验证加密结果
9. 常见问题与解决方案

在实际操作过程中,你可能会遇到以下问题,这里提供详细的解决方案:

  • Q1: 运行时报错 Error: Cannot find module 'vm2'
    • 原因分析:这是因为你的项目目录中没有安装 vm2 这个第三方模块。
    • 解决方案
      1. 在终端中,进入你的项目根目录(即包含 test.js 文件的目录)。
      2. 执行安装命令:npm install vm2
      3. 等待安装完成后,再次运行 node test.js 即可。
  • Q2: 执行后生成的 Acs-Token 值为空(null 或 undefined)
    • 原因分析:最常见的原因是环境补全不充分。加密代码在运行时检测到了与浏览器环境的差异(例如缺少某个特定属性或函数)。
    • 解决方案
      1. 开启调试:在 env.js 文件开头,将 catvm.memory.config 中的 proxy: false 改为 proxy: true。这会在控制台打印出所有被访问的对象属性,帮助你定位缺失了什么。
      2. 查看完整错误:确保 test.js 中的 try-catch 块能打印出完整的错误堆栈(error.stack),而不仅仅是消息。
      3. 依葫芦画瓢:根据错误信息,在 env.js 中补全相应的浏览器对象和API。
  • Q3: 报错 XXX is not a constructor 或 Illegal constructor
    • 原因分析:这是因为加密代码尝试通过 new 关键字来创建某个浏览器对象(如 new EventTarget()),但我们在 env.js 中模拟的构造函数被直接调用时会抛出错误。
    • 解决方案:这是正常现象,说明环境模拟正在起作用。无需解决此错误,我们的目的是让加密代码在调用 ASC() 方法时能绕过这些检测并成功返回结果。
  • Q4: 如何模拟更真实的浏览器环境?
    • 解决方案
      1. 补全Cookie:在 env.js 的 document 对象中,设置更真实的 cookie 字符串。
      2. 完善Navigator:补全 navigator 对象的其他属性,如 platformlanguagehardwareConcurrency 等。
      3. 模拟屏幕:为 screen 对象添加 widthheightavailWidthavailHeight 等属性。
  • Q5: 虚拟机(VM2)运行性能有问题
10. 获取完整课件

关注公众号:孤狼网络科技,回复:acs-token,获取完整代码课件,包括已经扣取好的acs-2027.jsenv.jstest.js文件。

总结
通过本JS逆向教程,我们成功把某吧发帖接口请求头加密参数Acs-Token解密。整个过程分析了某吧Acs-Token参数的生成机制,从数据抓包到代码分析,从全局搜索到深度追踪,最终实现了本地化生成。至此,我们已经解决了某吧发帖接口的所有加密参数,为实现完整的自动化发帖功能打下了坚实基础。

本文由林石工作室提供技术支持,转载请注明出处。

Comments

No comments yet. Why don’t you start the discussion?

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注