本帖最后由 buluo533 于 2025-7-29 10:35 编辑
本文章中所有内容仅供学习交流使用,不用于其他任何目的,不提供完整代码,抓包内容、敏感网址、数据接口等均已做脱敏处理,严禁用于商业用途和非法用途,否则由此产生的一切后果均与作者无关.本文章未经许可禁止转载,禁止任何修改后二次传播,擅自使用本文讲解的技术而导致的任何意外,作者均不负责
某酒店登录系统滑块,难度适中,食用地址:
aHR0cHM6Ly9ob3RlbC5vY3l1YW4uY29tL2xvZ2lu
密码学参考文章:
RSA密钥格式解析&转换------aHR0cHM6Ly9tcC53ZWl4aW4ucXEuY29tL3MvcHcycEFEYU1QaWJpTlJvQ3dhOUlzZz9zY2VuZT0x
加密的诗篇:RSA与Padding技术的艺术融合------aHR0cHM6Ly9tcC53ZWl4aW4ucXEuY29tL3MvaU0xS1lldy13R1VORXBwWk1YTG5vdw==
【密码学】分组密码模式------aHR0cHM6Ly9tcC53ZWl4aW4ucXEuY29tL3MvNzZjNXNyWVZrakdIS203MmN4TU1jUQ==
分析crypto-js当中AES的实现----aHR0cHM6Ly9tcC53ZWl4aW4ucXEuY29tL3MvRlBjUzlvRUQ5UHFDOV9nLXpmTEhqUQ==
本文主要是对滑块的思路进行分析讲解,分析ast还原以及滑块加密逻辑
滑块图片
一、流程分析
完成一次完整的滑块滑动过程,确定需要解决的加密参数
流程接口
按照整体流程,我们先从两个前置接口开始分析
前置载荷
第一前置返回值
目前看这里主要就是返回一些请求信息,对当前状态的信息存储,载荷中user_id参数疑是uuid,可以借助python的uuid库调用实现。接下来看看第二个请求和第一个请求的关系
第二次请求载荷
在这里我们发现challenge参数和第一个请求返回的一致的,说明第一次请求返回值会继续在第二次接口中使用。
第二前置接口返回值
这里的返回值就有很多参数信息,有背景图,缺口图,还有一个大数组,还有一个很重要的关键词publicKey熟悉的人一看就会猜加密会用到RSA(不了解的可以参考密码学文章)。这样发现前置接口没有过多的需要加密的点。
我们继续看看大数组的用处
图片切块
查看图片内容,发现和之前滑块一样被切片,但是这个直接返回了切片的顺序,只需要找到前端还原的算法就可以复现。
轨迹加密逻辑
这里我们发现了三个点,challenge参数值与前置接口的值保持一致,需要处理的值是collectData和key,在我写文章的时候发现,如果滑块长期静置不去动会有新接口更新challenge参数。这样就是我们整个要处理的部分
重置接口
重置接口返回值
重置接口的载荷也是用过期的challenge参数去换新的challenge参数,这也可能是一个风控点,可以注意一下
二、ast还原混淆代码逻辑分析
我ast写的很丑陋,能用就行,大佬轻喷。
我们其实在接口返回的过程中已经看到一个动态加载的js代码,猜测应该是加密逻辑的部分,经过测试没有问题,确实是,但是在核心代码部分在虚拟机中执行(非常恶心),不能进行替换(可能是我太菜了,大佬们教教我)
所以ast解混淆后更多用于本地对照,方便分析流程。
1、分析混淆逻辑
ast解混淆是90%的思路+10%的api,思路分析是最重要的部分(蔡老板讲的)。先把全部代码扒拉下来,
编码混淆
首先可以看到的就是编码的混淆,这个用ast处理就很简单了.。然后发现他有很多赋值操作,都是同一个a0_0x4000,跟过去看看。
解密函数
大数组
发现是一个很明显的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代码分析.js", encoding = "utf-8")
let ast = parse(jsc_code)
let init_ast = parse(jsc_code)
traverse(init_ast, {
Program(path) {
path.node.body = path.node.body.slice(0, 2);
path.stop();
}
});
eval(generator(init_ast, {minified: true}).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;
}
}
});
let decode_name = []
traverse(ast, {
VariableDeclarator(path) {
const {id, init} = path.node;
if (init && (init.type === 'Identifier' || init.type === 'FunctionDeclaration') && init.name === 'a0_0x4000') {
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, "a0_0x4000")
let result = eval(eval_path)
path.replaceWith(types.valueToNode(result))
}
}
});
let code = generator(ast).code;
fs.writeFileSync("./output.js", code)
首先就是我手动换了一下大数组和解密函数位置,给他们置顶,给他们存进内存方便调用函数,在我们分析过程中发现所有的赋值操作都是var表达式,我们提取特征将赋值的变量名存进列表,方便替换,后面我们找函数调用,将已经符合列表函数名的函数替换成解密函数,这样就可以愉快的替换实现解混淆,同时删除多余的代码(专用性的代码,不同环境要优化,也可以借助引用关系进行处理)
解混淆后
这样就有了比较清晰的代码,方便我们进行代码的分析
三、加密逻辑分析
断点起点
直接滑动验证码,下断点开始跟栈。
结合分析
闭包作用域
当我们跟栈到这里的时候就发现了问题_0x5b5fa5包含了我们需要的参数,在网上就没有了,我们再查看作用域,这时候发现他是在闭包里面,在这里我们查看不方便,直接回到解混淆后的代码,因为我们很多特征改变了,所以相互之间对照的时候尽量选择不变的如变量名来进行检索对照分析。
函数
搜索发现_0x5b5fa5是sendCollect函数调用的传参。我们检索一下sendCollect函数调用
第一个调用
第二个调用
第三次调用
我们一共发现了三次关键性的调用函数,第一个看起来像是轨迹校验,第二个像是轨迹生成,第三个我们发现了关键的加密参数名称,非常的明显,我们在源码中再找到第三次的位置,进行断点调试。
定位逻辑
一定要找清晰明确的对照点来定位函数位置。
加密逻辑断点
我们看到我们传参是这个_0x22ef7b,所以我们断点下在这里,先来确定他是不是我们需要的加密参数来源,然后分析参数的产生,很明显找到了。我们结合解混淆后的逻辑,将断点往上放,看看每一个加密点产生位置。
明显加密特征
在这两个函数调用中我们看到了很明显的一个加密特征,RSA,以及他加密内容的来源。
加密1
断点下载这里之后我们直接调用看看,发现很明显的一个加密点,我们直接在解混淆后代码中给他拿出来。
_0x45d877 = _0x520183["bufferBuilder"](window, _0x12cc77["map"](function (_0x24e1fa) {
var _0x50da05 = _0x24e1fa[0]["toFixed"](2);
var _0x32fd74 = _0x24e1fa[1]["toFixed"](2);
return ''["concat"](_0x50da05, ",")["concat"](_0x32fd74, ",")["concat"](_0x24e1fa[2]);
}));
根据代码的执行逻辑,后面的一个函数是会先执行的,我们看一下他的一个返回结果。
轨迹
这里很明显是一个轨迹,这个就有点像我们找的第二个函数调用的接口信息,x轴,y轴,时间,忘记了可以上去看看,这是我们伪造轨迹的一个核心。这也说明加密函数在前面的一个逻辑中,我们切换断点,直接进去看看,同时对照本地代码。
加密点1
对照代码1
_0x3d13af["prototype"]["bufferBuilder"] = function (_0x343746, _0x496ffc) {
var _0x19d55d = [];
_0x19d55d = _0x19d55d["concat"]((0, _0x137d59["intToByteList"])(1));
var _0x5c0cee = [];
_0x5c0cee = _0x5c0cee["concat"](this["makeUserBrowerInfo"](_0x343746));
_0x5c0cee = _0x5c0cee["concat"](this["make"](_0x496ffc));
_0x19d55d = _0x19d55d["concat"]((0, _0x137d59["intToByteList"])(Math["min"](this["counts"], this["maxlen"])));
var _0x106a70 = (0, _0x51bcee["buf"])((0, _0x137d59["byteList2Uint8Array"])(_0x5c0cee));
_0x19d55d = _0x19d55d["concat"]((0, _0x137d59["intToByteList"])(_0x106a70, 32));
_0x19d55d = _0x19d55d["concat"](_0x5c0cee);
var _0x32ed1e = (0, _0x137d59["bytesToBase64"])((0, _0x137d59["byteList2Uint8Array"])(_0x19d55d));
return _0x32ed1e;
};
非常清晰的一个逻辑,同时我们也发现很多陌生的函数调用,我们跟进去看看makeUserBrowerInfo
_0x3d13af["prototype"]["makeUserBrowerInfo"] = function (_0x5b1e3c) {
var _0x14c818 = [];
var _0x3876a5 = (0, _0x4d913a["getBrowerUserAgent"])();
_0x14c818 = this["listComposite"](_0x14c818, 2, _0x3876a5);
var _0x197689 = (0, _0x4d913a["getWindowSize"])();
_0x14c818 = this["listComposite"](_0x14c818, 2, _0x197689);
var _0x5d19e4 = (0, _0x4d913a["getUrl"])();
_0x14c818 = this["listComposite"](_0x14c818, 2, _0x5d19e4);
var _0x528bde = (0, _0x4d913a["getIsF12"])();
_0x14c818 = this["listComposite"](_0x14c818, 2, _0x528bde);
var _0x78cf86 = (0, _0x4d913a["getIsHeadless"])();
_0x14c818 = this["listComposite"](_0x14c818, 2, _0x78cf86);
var _0x26fd0f = Date["now"]()["toString"]();
_0x14c818 = this["listComposite"](_0x14c818, 2, _0x26fd0f);
this["counts"] = this["counts"] + 6;
return _0x14c818;
};
这里大概是一个指纹校验的地方,UserAgent,WindowSize,Url等等,很明显的一些指纹特征,同时的listComposite函数对指纹的处理
_0x3d13af["prototype"]["listComposite"] = function (_0x32abb7, _0xb17fae, _0x18fc6b) {
if (_0xb17fae === void 0) {
_0xb17fae = 2;
}
var _0x567937 = _0x32abb7;
if (_0x18fc6b) {
var _0x1219c9 = Math["min"](_0x18fc6b["split"]('')["length"], this["maxlen"]);
var _0x24d279 = (0, _0x137d59["intToByteList"])(1);
var _0x5ab793 = (0, _0x137d59["intToByteList"])(_0xb17fae === 1 ? 8 + 3 : _0x1219c9 + 3);
var _0x1feae4 = (0, _0x137d59["intToByteList"])(_0xb17fae);
var _0x4e28f7 = _0xb17fae === 1 ? (0, _0x137d59["intToByteList"])(_0x18fc6b) : (0, _0x137d59["stringToBytes"])(_0x18fc6b["substring"](0, _0x1219c9));
_0x567937 = _0x567937["concat"](_0x280ffe(_0x280ffe(_0x280ffe(_0x280ffe([], _0x24d279, !![]), _0x5ab793, !![]), _0x1feae4, !![]), _0x4e28f7, !![]));
} else {
var _0x24d279 = (0, _0x137d59["intToByteList"])(0);
var _0x5ab793 = (0, _0x137d59["intToByteList"])(4);
var _0x1feae4 = (0, _0x137d59["intToByteList"])(1);
var _0x4e28f7 = (0, _0x137d59["intToByteList"])(0);
_0x567937 = _0x567937["concat"](_0x280ffe(_0x280ffe(_0x280ffe(_0x280ffe([], _0x24d279, !![]), _0x5ab793, !![]), _0x1feae4, !![]), _0x4e28f7, !![]));
}
return _0x567937;
};
接下来是_0x51818f 生成的逻辑
加密2
我们还是直接跟进去同时对照
var _0x41c5ba = function (_0x3c0113, _0x2a4c49) {
var _0xf87a91 = _0xf3a3a7["getKey"](16);
var _0x439b22 = _0xf3a3a7["encode"](_0x3c0113, _0xf87a91);
var _0x4e92fb = _0x313169["publicKeyEncrypt"](_0xf87a91, _0x2a4c49);
var _0x5b3f6a = "verify_" + new Date()["getTime"]();
var _0x1aeac9 = {};
_0x1aeac9["collectData"] = _0x439b22;
_0x1aeac9["key"] = _0x4e92fb;
_0x1aeac9["callback"] = _0x5b3f6a;
return _0x1aeac9;
};
加密算法
我们发现到了这里collectData,key都有了非常清晰的出处,我们再跟进 _0xf3a3a7["getKey"]和_0xf3a3a7["encode"]详细
加密逻辑核心
发现这几个加密点都在一起,核心算法也可以拿出来分析一下。
function getKey (_0x4a3e32) {
var _0x544c9f = ["0", "1", "2", "3", "4", "5", "6", "7", "8", "9", "A", "B", "C", "D", "E", "F", "G", "H", "I", "J", "K", "L", "M", "N", "O", "P", "Q", "R", "S", "T", "U", "V", "W", "X", "Y", "Z"];
var _0x33f64e = '';
for (var _0x17dc91 = 0; _0x17dc91 < _0x4a3e32; _0x17dc91++) {
var _0x1eb9d4 = Math["ceil"](Math["random"]() * 35);
_0x33f64e += _0x544c9f[_0x1eb9d4];
}
return _0x33f64e;
}
function encode(_0x3865e0, _0x5753eb) {
var _0x470bc1 = _0x1e4ffe["default"]["parse"](_0x5753eb);
var _0x2ea94c = this["getKey"](16);
var _0x110f3e = _0x1e4ffe["default"]["parse"](_0x2ea94c);
var _0x246aad = {};
_0x246aad["mode"] = _0x27349f["default"];
_0x246aad["padding"] = _0x331f1e["default"];
_0x246aad["iv"] = _0x110f3e;
var _0x1211ee = _0x180b02["default"]["encrypt"](_0x3865e0, _0x470bc1, _0x246aad)["toString"]();
return _0x53fa0c["default"]["stringify"](_0x110f3e["concat"](_0x53fa0c["default"]["parse"](_0x1211ee)));
}
};
var _0x313169 = {
"publicKeyEncrypt": function (_0x1b9ff2, _0x347537) {
var _0xb15ad3 = "-----BEGIN PUBLIC KEY-----PUBLIC_KEY-----END PUBLIC KEY-----";
var _0x2fc45d = new _0x1c1460["default"]();
var _0xf5d796 = _0xb15ad3["replace"]("PUBLIC_KEY", _0x347537);
_0x2fc45d["setPublicKey"](_0xf5d796);
return _0x2fc45d["encrypt"](_0x1b9ff2);
}
};
四、总结
其实到这里不论是扣代码,补环境,纯算都有比较明确的思路,技术没有优劣高低,能解决问题就是好的,主要给大佬们提供思路{:1_932:} ,就不用我的代码污染大佬们了{:1_932:} |