本帖最后由 buluo533 于 2025-8-11 18:42 编辑
本文章中所有内容仅供学习交流使用,不用于其他任何目的,不提供完整代码,抓包内容、敏感网址、数据接口等均已做脱敏处理,严禁用于商业用途和非法用途,否则由此产生的一切后果均与作者无关.本文章未经许可禁止转载,禁止任何修改后二次传播,擅自使用本文讲解的技术而导致的任何意外,作者均不负责
某初滑块滑块,难度适中,食用地址(好像更新了代码,更简单了,我会给出复杂一点的版本):
aHR0cHM6Ly9hcHAueWljaHVhcHAuY24vaW5kZXgvaW52aXRlL2luZGV4Lw==
工具站:
https://babel.nodejs.cn/docs/babel-core
本文主要是对滑块的思路进行分析讲解,同样分析简单ob混淆的ast还原以及滑块加密逻辑
滑块情况
一、流程分析
debuggger
开局一个debugger,掏出多年的hook脚本,直接注入过掉(百度一搜就有,或者自己写一个)
let AAA = Function.prototype.constructor
Function.prototype.constructor = function (x) {
console.log(x)
if (X != "debugger") {
return AAA(x)
}
;
return AAA.call(this, x)
}
解决掉debugger我们就可以按照之前的经验来分析,我们需要进行滑动,来看需要处理的请求接口信息
滑动接口
很明显,啥都没有,还反手送我一个debugger,还有一串混淆的代码,很明显,这个js代码是我们要处理的,但是我们没有看到校验的逻辑。我们就可以猜测了,因为之前还有假的滑块,先排除一下,会不会是cookie校验
cookie前
cookie信息
我们对比一下滑动前和滑动后的参数变化,发现有一个异常的参数guard_ret,简单注入hook一下
(function () {
'use strict';
var cookie_cache = document.cookie;
Object.defineProperty(document, 'cookie', {
get: function () {
return cookie_cache;
},
set: function (val) {
console.log('Setting cookie', val);
if (val.indexOf('guard_ret') != -1) {
debugger;
}
var cookie = val.split(";")[0];
var ncookie = cookie.split("=");
var flag = false;
var cache = cookie_cache.split("; ");
cache = cache.map(function (a) {
if (a.split("=")[0] === ncookie[0]) {
flag = true;
return cookie;
}
return a;
})
cookie_cache = cache.join("; ");
if (!flag) {
cookie_cache += cookie + "; ";
}
return cookie_cache;
}
});
})();
cookie堆栈
这里就是成功断住了,我们往上看看堆栈,分析产生
拼接cookie
我们发现这个js文件就是我们返回的那一个js文件,混淆很难看,直接ast一把梭哈
二、ast还原混淆代码逻辑分析
拿到混淆代码先总览一边,看看还原思路。
混淆逻辑
很明显一个编码混淆。
赋值调用
然后发现有很多_0xb0e1函数的一个赋值调用,符合ob混淆的特征,是一个解密函数,然后就是找准大数组和数组移位函数
大数组逻辑
找准了大数组和解密函数,接下来就是开始写ast代码,先搭一个模板,理一下思路
(1)将三要素,大数组,数组移位函数,解密函数,存进内存
(2)编码混淆还原
(3)解密函数调用还原
(4)ob混淆特征大对象还原
一步一步来,找到对应的三要素位置,直接提取出来,存进内存。
语法树
let parse = require("@babel/parser").parse;
let traverse = require("@babel/traverse").default;
let generator = require("@babel/generator").default;
let fs = require("fs");
let types = require("@babel/types");
let jsc_code = fs.readFileSync("./混淆代码.js", encoding = "utf-8")
let ast = parse(jsc_code)
let init_ast = parse(jsc_code)
traverse(init_ast, {
Program: function (path) {
path.node.body = path.node.body.slice(0, 5)
path.stop()
}
})
let test_code = generator(init_ast, {minified: true}).code
eval(test_code)
接下来做一个编码的还原,掏出俩件套
traverse(ast, {
StringLiteral({node}) {
if (node.extra && /\\[ux]/gi.test(node.extra.raw)) {
node.extra = undefined;
}
}
});
traverse(ast, {
NumericLiteral({node}) {
if (node.extra && /^0[obx]/i.test(node.extra.raw)) {
node.extra = undefined;
}
}
});
完成这一步之后我们可以调用解密函数测试一下,有没有存进内存。
解密函数调用
因为我们有很多赋值调用的过程,我们把赋值的函数名给收集起来,然后统一替换成解密函数_0xb0e1,执行替换结果,代码如下。
let decode_name = []
traverse(ast, {
VariableDeclarator(path) {
const {id, init} = path.node;
if (init && (init.type === 'Identifier' || init.type === 'FunctionDeclaration') ) {
let name = id.name
decode_name.push(name)
path.remove()
}
}
});
traverse(ast, {
CallExpression: {
exit: function (path) {
let {node} = path
let func_name = node.callee.name
if (!decode_name.includes(func_name)) {
return
}
if (!node.arguments || node.arguments.length !== 1) {
return;
}
let eval_path = path.toString().replace(func_name, "_0xb0e1")
let result = eval(eval_path)
path.replaceWith(types.valueToNode(result))
}
}
});
这样就可以把所有解密函数调用的部分还原,可以看一下结果,还是非常的清晰
解密函数还原结果
接下来就是处理最难的部分,ob混淆的大数组处理。我的大致思路如下
1、收集所有的符合条件的对象
2、分成两部分处理,一部分是函数和运算符的还原,一部分是字符串的还原
3、通过函数调用和作用域去还原,比较特定,不通用
第一部分是收集所有的符合条件的对象
let obj_name = []
traverse(ast, {
VariableDeclarator: function (path) {
let {parentPath, node, scope} = path
if (!parentPath.isVariableDeclaration()) {
return;
}
let {id, init} = node
if (!types.isObjectExpression(init)) {
return;
}
if (!id.name && id.name === undefined) return;
let name = id.name
obj_name.push(name)
}
})
在最开始写代码建议使用某一个对象去测试,然后去处理函数和运算符的还原
traverse(ast, {
CallExpression: {
exit(path) {
if (path.get("callee.object").node && obj_name.includes(path.get("callee.object").node.name)) {
let property = path.get("callee.property").node.value
let argument_path_array = path.get("arguments")
let name = path.get("callee.object").node.name
path.scope.getBinding(name).scope.path.traverse({
ObjectExpression: function (path_Expression) {
let {node, parentPath} = path_Expression
if (!parentPath.isVariableDeclarator()) {
return
}
if (parentPath.node.id.name !== name) {
return;
}
let properties = node.properties
for (i of properties) {
if (i.key.value && i.key.value === property) {
let value = i.value
switch (value.type) {
case 'FunctionExpression':
let return_path = value.body.body[0].argument
if (value.body.body[0].argument.type === 'BinaryExpression') {
let operator = return_path.operator
let left = argument_path_array[0].node
let right = argument_path_array[1].node
path.replaceInline(types.binaryExpression(operator, left, right))
} else if (value.body.body[0].argument.type === 'CallExpression') {
let func = argument_path_array[0].node
if (!types.isCallExpression(path) && !path.arguments) {
return;
}
let func_arguments = path.node.arguments.slice(1)
path.replaceInline(types.callExpression(func, func_arguments))
}
break
}
}
}
path_Expression.scope.crawl();
}
});
}
}
}
})
第三步是字符串对应的还原
traverse(ast, {
MemberExpression:
function (path) {
if (obj_name.includes(path.get("object.name").node) && path.get('property').isStringLiteral()) {
let _string = path.get('property').node.value
let name = path.get("object.name").node
path.scope.getBinding(name).scope.path.traverse({
ObjectExpression: function (path_Expression) {
let {node, parentPath} = path_Expression
if (!parentPath.isVariableDeclarator()) {
return
}
if (parentPath.node.id.name !== name) {
return;
}
let properties = node.properties
for (i of properties) {
let key_value = i.key.value
let value = i.value
if (key_value === _string) {
switch (value.type) {
case 'StringLiteral':
let real_value = value.value
path.replaceWith(types.valueToNode(real_value))
break
}
}
}
path_Expression.scope.crawl();
}
});
}
}
})
总体还原思路是从函数调用入手,去匹配他的值和对象里的键这样去实现还原。现在更新的加密更加简单(需要处理定时器的逻辑),没有了对象的还原,简单写了一个,仅供参考
traverse(ast, {
CallExpression: {
exit: function (path) {
let {node} = path
let func_name = node.callee.name
if (func_name !== "_0x4de0") {
return;
}
let result = eval(path.toString())
path.replaceWith(types.valueToNode(result))
}
}
});
剩下的就是处理垃圾代码的删除,就可以完成这个的还原。
三、加密逻辑分析
加密逻辑
还原后的代码一目了然,可以看到清晰的逻辑,找一下 _0x2b7a59的一个生成逻辑
const _0x167271 = _0x39eb8c["getBoundingClientRect"](),
_0x4b9064 = _0x255abf['getBoundingClientRect'](),
_0x3f4be5 = _0x167271["left"] - _0x4b9064['left'],
_0x32d3e9 = _0x167271["top"] - _0x4b9064["top"];
x = Math["round"](_0x3f4be5);
y = Math["round"](_0x32d3e9);
var _0x2c1619 = _0x5ce987("guard"),
_0x564781 = _0x2c1619["substr"](0, 8),
_0x1499fd = _0x5cc496(x["toString"]() + 'x' + y["toString"](), _0x564781),
_0x2b7a59 = _0x40fb99(_0x1499fd);
document["cookie"] = "guardret=" + _0x2b7a59;
function _0x5ce987(_0x17f493) {
const _0x5b6ec3 = document['cookie']["split"](';');
let _0x119d4b = null;
for (let _0x4de2de of _0x5b6ec3) {
const [_0x1cc261, _0xeec935] = _0x4de2de["trim"]()['split']('=');
if (_0x1cc261 === _0x17f493) {
_0x119d4b = _0xeec935
}
}
return _0x119d4b;
}
function _0x5cc496(_0x49fb4d, _0x45898e) {
let _0x2f7fee = '';
var _0x45898e = _0x45898e + "zVbhsiCROg";
for (let _0x351fbd = 0; _0x351fbd < _0x49fb4d["length"]; _0x351fbd++) {
const _0x37b77c = _0x49fb4d["charCodeAt"](_0x351fbd) ^ _0x45898e["charCodeAt"](_0x351fbd % _0x45898e["length"]);
_0x2f7fee += String["fromCharCode"](_0x37b77c);
}
return _0x2f7fee;
}
function _0x40fb99(_0x20cad9) {
return btoa(_0x20cad9)
}
比较简单的一个逻辑,用python代码实现一下
def encrypt(data, key):
result = []
key += "zVbhsiCROg"
key_length = len(key)
for i in range(len(data)):
data_char_code = ord(data[i])
key_char_code = ord(key[i % key_length])
xor_result = data_char_code ^ key_char_code
result.append(chr(xor_result))
return ''.join(result)
四、总结
这个站整体偏简单,没想到他竟然更新的更简单了,利用ast可以更快处理,还是用老版本做了一个示例,大佬们可以试试搞一下新版本,一把梭哈,cookie加密原理没有变{:1_932:} 给一个老版本混淆可以拿来练手
混淆代码.zip
(12.46 KB, 下载次数: 4)
|