irpas技术客

极验滑块验证码破解与研究(一):AST还原混淆JS_帯泪的鱼

irpas 1907

极验滑块验证码破解与研究(一):AST还原混淆JS 声明一、环境安装1. node安装1.1. node下载1.2. 配置环境变量1.3. node安装检测1.4. pycharm配置node环境 2. babel库安装2.1. babel库安装命令2.2. babel库安装检测 二、AST还原混淆JS1. 模块导入2. 复制还原需要用到的js源码3. AST还原函数详解3.1. replace_unicode3.2. replace_unicode, replace_name_array, replace_Dvn3.3. replace_ForStatement3.4. delete_func 4. AST还原流程4.1. AST还原整体流程4.2. 极验不同js或不同版本还原方式 三、结语

声明

原创文章,请勿转载!

本文内容仅限于安全研究,不公开具体源码。维护网络安全,人人有责。

本文关联文章超链接:

极验滑块验证码破解与研究(一):AST还原混淆JS极验滑块验证码破解与研究(二):缺口图片还原极验滑块验证码破解与研究(三):滑块缺口识别极验滑块验证码破解与研究(四):滑块轨迹构造极验滑块验证码破解与研究(五):请求分析及加密参数破解 一、环境安装 1. node安装 1.1. node下载

点击此处 node官方下载链接 下载安装包,并安装

1.2. 配置环境变量 1.3. node安装检测 node --version # v14.16.0 1.4. pycharm配置node环境

2. babel库安装 2.1. babel库安装命令 npm install @babel/core --save-dev 2.2. babel库安装检测 node # 进入node环境 require("@babel/parser") # 引入babel库

二、AST还原混淆JS

温馨提示:极验js都采用相同的混淆方式。后期文章用到的 fullpage.9.0.7.js ,click.3.0.2.js 等都用此方式还原 不懂AST语法树的小伙伴可以问问度娘哦 本文以 slide.7.8.4.js 文件为例,在线AST语法树转换工具

aHR0cHM6Ly9zdGF0aWMuZ2VldGVzdC5jb20vc3RhdGljL2pzL3NsaWRlLjcuOC40Lmpz 1. 模块导入

导入还原混淆JS需要用到的功能块

const parser = require("@babel/parser"); const traverse = require("@babel/traverse").default; const t = require("@babel/types"); const generator = require("@babel/generator").default; const fs = require("fs"); 2. 复制还原需要用到的js源码

博主太懒了,小伙伴自己复制吧。(源代码太长了,减少篇幅…) 此部分代码为 slide.7.8.4.js 文件中的函数,一模一样的复制下来就行,还原时会用到。

AaWgt.Bq_ = function () {}(); AaWgt.CyZ = function () {}(); AaWgt.Dvn = function () {}; AaWgt.EeS = function () {}; function AaWgt() {} 3. AST还原函数详解 3.1. replace_unicode

js代码中有很多字符为的二进制、Unicode编码,此函数将代码中的其他字符编码转换为utf-8编码

// 删除节点中的extra属性(二进制、Unicode等编码 -> utf-8) function replace_unicode(path) { let node = path.node; if (node.extra === undefined) return; delete node.extra; }

还原前 还原后

3.2. replace_unicode, replace_name_array, replace_Dvn

代码中大量使用类似 var aVMl = AXzPo.DVn 的表达式,AXzPo.DVn实际为一个大数组,下面函数将其替换为具体字符

// 定义一个全局变量,存放待替换变量名 let name_array = []; function get_name_array(path) { let {kind, declarations} = path.node if (kind !== 'var' || declarations.length !== 3 || declarations[0].init === null || declarations[0].init.property === undefined) return; if (declarations[0].init.property.name !== "Dvn") return; // 获取待替换节点变量名 let name1 = declarations[0].id.name // 获取待输出变量名 let name2 = declarations[2].id.name // 将变量名存入数组 name_array.push(name1, name2) // 删除下一个节点 path.getNextSibling().remove() // 删除下一个节点 path.getNextSibling().remove() // 删除path节点 path.remove() } function replace_name_array(path) { let {callee, arguments} = path.node if (callee === undefined || callee.name === undefined) return; // 不在name_array中的节点不做替换操作 if (name_array.indexOf(callee.name) === -1) return; // 调用AaWgt.Dvn函数获取结果 let value = AaWgt.Dvn(arguments[0].value); // 创建节点并替换结果 let string_node = t.stringLiteral(value) path.replaceWith(string_node) } function replace_Dvn(path) { let {arguments, callee} = path.node // 解析arguments参数 if (arguments.length !== 1) return; if (arguments[0].type !== 'NumericLiteral') return; // 解析callee if (callee.type !== 'MemberExpression') return; let {object, property} = callee; if (object.type !== 'Identifier' || property.type !== 'Identifier') return; if (property.name === 'Dvn') { // 计算值 let value = AaWgt.Dvn(arguments[0].value); // 创建节点并替换 let string_node = t.stringLiteral(value) path.replaceWith(string_node) } }

还原前 还原后

3.3. replace_ForStatement

js代码中大量switch-case结构,不利于代码逻辑解析,需要控制流平坦化

// 控制流平坦化 function replace_ForStatement(path) { var node = path.node; // 获取上一个节点,也就是VariableDeclaration var PrevSibling = path.getPrevSibling(); // 判断上个节点的各个属性,防止报错 if (PrevSibling.container === undefined || PrevSibling.container[0].declarations === undefined || PrevSibling.container[0].declarations[0].init === null || PrevSibling.container[0].declarations[0].init.object === undefined || PrevSibling.container[0].declarations[0].init.object.object === undefined) return; if (PrevSibling.container[0].declarations[0].init.object.object.callee.property.name !== 'EeS') return; // SwitchStatement节点 var body = node.body.body; // 判断当前节点的body[0]属性和body[0].discriminant是否存在 if (!t.isSwitchStatement(body[0])) return; if (!t.isIdentifier(body[0].discriminant)) return; // 获取控制流的初始值 var argNode = PrevSibling.container[0].declarations[0].init; var init_arg_f = argNode.object.property.value; var init_arg_s = argNode.property.value; var init_arg = AaWgt.EeS()[init_arg_f][init_arg_s]; // 提取for节点中的if判断参数的value作为判断参数 var break_arg_f = node.test.right.object.property.value; var break_arg_s = node.test.right.property.value; var break_arg = AaWgt.EeS()[break_arg_f][break_arg_s]; // 提取switch下所有的case var case_list = body[0].cases; var resultBody = []; // 遍历全部的case for (var i = 0; i < case_list.length; i++) { for (; init_arg != break_arg;) { // 提取并计算case后的条件判断的值 var case_arg_f = case_list[i].test.object.property.value; var case_arg_s = case_list[i].test.property.value; var case_init = AaWgt.EeS()[case_arg_f][case_arg_s]; if (init_arg == case_init) { //当前case下的所有节点 var targetBody = case_list[i].consequent; // 删除break节点,和break节点的上一个节点的一些无用代码 if (t.isBreakStatement(targetBody[targetBody.length - 1]) && t.isExpressionStatement(targetBody[targetBody.length - 2]) && targetBody[targetBody.length - 2].expression.right.object.object.callee.object.name == "AaWgt") { // 提取break节点的上一个节点AJgjJ.EMf()后面的两个索引值 var change_arg_f = targetBody[targetBody.length - 2].expression.right.object.property.value; var change_arg_s = targetBody[targetBody.length - 2].expression.right.property.value; // 修改控制流的初始值 init_arg = AaWgt.EeS()[change_arg_f][change_arg_s]; targetBody.pop(); // 删除break targetBody.pop(); // 删除break节点的上一个节点 } //删除break else if (t.isBreakStatement(targetBody[targetBody.length - 1])) { targetBody.pop(); } resultBody = resultBody.concat(targetBody); break; } else { break; } } } //替换for节点,多个节点替换一个节点用replaceWithMultiple path.replaceWithMultiple(resultBody); //删除上一个节点 PrevSibling.remove(); }

还原前 还原后

3.4. delete_func

还原过程结束啦,是时候删除部分无关函数了。

// 删除无关函数 function delete_func(path) { let {expression} = path.node if (expression === undefined || expression.left === undefined || expression.left.property === undefined) return; if (expression.left.property.name === 'Bq_' || expression.left.property.name === 'Dvn' || expression.left.property.name === 'CyZ' || expression.left.property.name === 'EeS' ) { path.remove() } } 4. AST还原流程 4.1. AST还原整体流程 // 需要解码的文件位置 let encode_file = "slide.7.8.4.js" // 解码后的文件位置 let decode_file = "ast_slide.7.8.4.js_init.js" // 读取需要解码的js文件, 注意文件编码为utf-8格式 let jscode = fs.readFileSync(encode_file, {encoding: "utf-8"}); // 将js代码修转成AST语法树 let ast = parser.parse(jscode); // AST结构修改逻辑 const visitor = { StringLiteral: { enter: [replace_unicode] }, VariableDeclaration: { enter: [get_name_array] }, CallExpression: { enter: [replace_name_array, replace_Dvn] }, ForStatement: { enter: [replace_ForStatement] }, ExpressionStatement: { enter: [delete_func] }, } // 遍历语法树节点,调用修改函数 traverse(ast, visitor); // 将ast转成js代码,{jsescOption: {"minimal": true}} unicode -> 中文 let {code} = generator(ast, opts = {jsescOption: {"minimal": true}}); // 将js代码保存到文件 fs.writeFile(decode_file, code, (err) => { }); 4.2. 极验不同js或不同版本还原方式

本系列文章使用的js版本:

fullpage.9.0.7.js:https://static.geetest.com/static/js/fullpage.9.0.7.js slide.7.8.4.js:https://static.geetest.com/static/js/slide.7.8.6.js click.3.0.2.js(后期出点选系列文章会用到):https://static.geetest.com/static/js/click.3.0.2.js

小伙伴们可以发现,极验fullpage.9.0.x.js、slide.7.8.x.js、click.3.0.x.js均使用相同的混淆方式。所以,本文章的AST还原方式可以用于当前最新版本js的还原。下面介绍一个案例,将适用slide.7.8.4.js还原的AST代码替换成适用fullpage.9.0.7.js的

第一步:将fullpage.9.0.7.js中部分源码替换到AST文件中。

第二步:使用编译器的替换功能,将AST代码中的函数名称全部替换。

第三步:最后,记得将需要解码的文件修改成fullpage.9.0.7.js

// 需要解码的文件位置 let encode_file = "fullpage.9.0.7.js" // 解码后的文件位置 let decode_file = "ast_fullpage.9.0.7.js_init.js" 三、结语

友情链接:极验滑块验证码破解与研究(二):缺口图片还原

本期文章结束啦,如果对您有帮助,记得收藏加关注哦,后期文章会持续更新 ~~~


1.本站遵循行业规范,任何转载的稿件都会明确标注作者和来源;2.本站的原创文章,会注明原创字样,如未注明都非原创,如有侵权请联系删除!;3.作者投稿可能会经我们编辑修改或补充;4.本站不提供任何储存功能只提供收集或者投稿人的网盘链接。

标签: #node安装11 #node下载12 #配置环境变量13 #node安装检测14 #babel库安装21