注:本文章仅供学习使用,请勿用做其他商业用途,如有侵权,请联系本人
提示:以下是本篇文章正文内容,下面案例可供参考
一、什么是AST
技术 ?
AST
(Abstract Syntax Tree),译为抽象语法树,是编译原理中的一个概念,为源代码的抽象语法结构的树状表示,树上的每个节点都表示源代码中的一种结构,这种数据结构可以类别为一个大的JSON
对象。通过 AST
技术,我们面对的就不再是各种符号混杂空格而成的文本字符串,而是一个严谨规范的树形结构,我们可以通过对 AST
树节点的一系列操作,借助机器高效且精准地修改代码。
AST 的用途很广,IDE 的语法高亮、代码检查、格式化、压缩、转译等,都需要先将代码转化成 AST 再进行后续的操作,ES5 和 ES6 语法差异,为了向后兼容,在实际应用中需要进行语法的转换,也会用到 AST。AST 并不是为了逆向而生,但做逆向学会了 AST,在解混淆时可以如鱼得水。
通过 AST 解析网站:https://astexplorer.net/,左侧为我们要输入的 JavaScript 代码,右侧为 AST 树状结构
1、AST对爬虫工程师有什么意义?
对于爬虫工程师来说,它并不能帮你找到加密参数的具体位置,但是可以使用它把混淆的代码解混淆后替换到浏览器里,方便找加密参数的生成逻辑.
2、在线解混淆工具
3、解混淆--> babel库的使用
安装 npm install @babel/core --save-dev
Babel 是一个 JavaScript 编译器,也可以说是一个解析库,Babel 内置了很多分析 JavaScript 代码的方法,我们可以利用 Babel 将 js 代码转换成 AST 语法树,然后增删改查等操作之后,再转换成 JavaScript 代码。Babel 包含的各种功能包、API、各方法可选参数等,都非常多,在实际使用过程中,应当多查询官方文档 网址:Document
在做逆向解混淆中,主要用到了 Babel 的以下几个功能包
- @babel/core:Babel 编译器本身,提供了 babel 的编译 API;
- @babel/parser:将 JavaScript 代码解析成 AST 语法树;
- @babel/traverse:遍历、修改 AST 语法树的各个节点;
- @babel/generator:将 AST 还原成 JavaScript 代码;
- @babel/types:判断、验证节点的类型、构建新 AST 节点等。
常见的混淆还原:
const generator = require('@babel/generator').default //将 AST 还原成 JavaScript 代码
const parser = require("@babel/parser"); // 编译成语法树
const traverse = require("@babel/traverse"); //对语法树进行操作
const types = require("@babel/types"); //判断、验证节点的类型、构建新 AST 节点等
var fs =require("fs"); //读取文件
js = fs.readFileSync('mfw.js',{encoding:'utf-8'})
let ast = parser.parse(js);
function writeFile(code) {
console.log("Write start\\n");
fs.writeFile(file_out, code, function (err) {
if (err) {
return console.error(err);
}
});
console.log("Write finish\\n");
}
visitor1={
"Program"(path){
var body =path.get('body.0');
var node =body.node;
var args=node.expression.argument;
if(args==undefined)return;
var params=args.callee.params;
var paramsvalue=args.arguments;
var name,valuelist;
for(var i=0;i<params.length;i++){
name=params[i].name;
valuelist=paramsvalue[i].elements;
body.traverse({
MemberExpression(_path){
var _node=_path.node;
var _name=_node.object.name;
if(!types.isNumericLiteral(_node.property))return;
var _value=_node.property.value;
if(name==_name){
if(valuelist[_value]==undefined)return;
if(valuelist[_value].value==undefined)return;
rvalue=valuelist[_value].value;
switch(typeof rvalue){
case "string":
_path.replaceWith(types.StringLiteral(rvalue));
break;
case "number":
_path.replaceWith(types.NumericLiteral(rvalue));
break;
}
}
}
});
}
}
}
const visitor2={
VariableDeclarator(path)
{
const {id,init}=path.node;
if(!types.isLiteral(init))return;
const binding=path.scope.getBinding(id.name);
if(binding.constantViolations.length===0)
{
for(const refer_path of binding.referencePaths)
{
refer_path.replaceInline(init);
}
//path.remove();
}
}
}
replaceliteral=function(path,value){
switch(typeof value){
case 'boolean':
path.replaceWith(types.booleanLiteral(value));
break;
case 'number':
path.replaceWith(types.NumericLiteral(value));
break;
case 'string':
path.replaceWith(types.stringLiteral(value));
break;
default:
break;
}
}
const visitor3={
"UnaryExpression|BinaryExpression|CallExpression|ConditionalExpression":{
enter:function(path){
const{value}=path.evaluate();
replaceliteral(path,value);
}
}
}
const visitor4={
"FunctionDeclaration"(path){
let {id}=path.node;
let code=path.toString();
if(code.indexOf("try")!=-1 ||code.indexOf("random")!=-1||code.indexOf("Date")!=-1){
return;
}
eval(code);
let scope =path.scope;
const binding = path.scope.parent.getBinding(id.name);
let isdel=false;
if(!binding || binding.constantViolations.length>0){
return;
}
for(const refer_path of binding.referencePaths)
{
let call_express=refer_path.findParent(p=>p.isCallExpression());
let arguments=call_express.get('arguments');
let args=[];
arguments.forEach(arg=>{args.push(arg.isLiteral())});
if(args.length ===0 || args.indexOf("false")!=-1){
continue;
}
try{
let value= eval(call_express.toString());
if(value==undefined)return;
switch(typeof value){
case "string":
call_express.replaceWith(types.StringLiteral(value));
isdel=true;
break;
case "number":
call_express.replaceWith(types.NumericLiteral(value));
isdel=true;
break;
}
}catch(e){
}
}
if(isdel){
//path.remove();
}
}
}
const visitor5={
"StringLiteral|NumericLiteral"(path){
delete path.node.extra;
}
}
const visitor6={
"CallExpression"(path){
var node =path.node;
var code=path.toString();
var value;
if(!node.arguments.length>0)return;
if(!types.isLiteral(node.arguments[0]))return;
if(code.indexOf("Time")!=-1)return;
try{
value=eval("value="+code);
}catch(e){
}
if(value==undefined)return;
switch(typeof value){
case "string":
path.replaceWith(types.StringLiteral(value));
break;
case "number":
path.replaceWith(types.NumericLiteral(value));
break;
case "boolean":
path.replaceWith(types.BooleanLiteral(value));
break;
}
}
}
traverse.default(ast,visitor1);
traverse.default(ast,visitor2);
traverse.default(ast,visitor3);
traverse.default(ast,visitor4);
traverse.default(ast,visitor5);
traverse.default(ast,visitor6);
const {code} = generator(ast,opts = {"comments":false},js);
fs.writeFile('mfw_decode.js', code, (err)=>{});
switch-case 反控制流平坦化混淆还原:
// const _0x34e16a = '3,4,0,5,1,2'['split'](',');
// let _0x2eff02 = 0x0;
// while (!![]) {
// switch (_0x34e16a[_0x2eff02++]) {
// case'0':
// let _0x38cb15 = _0x4588f1 + _0x470e97;
// continue;
// case'1':
// let _0x1e0e5e = _0x37b9f3[_0x50cee0(0x2e0, 0x2e8, 0x2e1, 0x2e4)];
// continue;
// case'2':
// let _0x35d732 = [_0x388d4b(-0x134, -0x134, -0x139, -0x138)](_0x38cb15 >> _0x4588f1);
// continue;
// case'3':
// let _0x4588f1 = 0x1;
// continue;
// case'4':
// let _0x470e97 = 0x2;
// continue;
// case'5':
// let _0x37b9f3 = 0x5 || _0x38cb15;
// continue;
// }
// break;
// }
const parser = require("@babel/parser");
const generate = require("@babel/generator").default
const traverse = require("@babel/traverse").default
const types = require("@babel/types")
const fs = require("fs");
const code = fs.readFileSync("code.js", {encoding: "utf-8"});
const ast = parser.parse(code)
const visitor = {
WhileStatement(path) {
// switch 节点
let switchNode = path.node.body.body[0];
// switch 语句内的控制流数组名,本例中是 _0x34e16a
let arrayName = switchNode.discriminant.object.name;
// 获得所有 while 前面的兄弟节点,本例中获取到的是声明两个变量的节点,即 const _0x34e16a 和 let _0x2eff02
let prevSiblings = path.getAllPrevSiblings();
// 定义缓存控制流数组
let array = []
// forEach 方法遍历所有节点
prevSiblings.forEach(pervNode => {
let {id, init} = pervNode.node.declarations[0];
// 如果节点 id.name 与 switch 语句内的控制流数组名相同
if (arrayName === id.name) {
// 获取节点整个表达式的参数、分割方法、分隔符
let object = init.callee.object.value;
let property = init.callee.property.value;
let argument = init.arguments[0].value;
// 模拟执行 '3,4,0,5,1,2'['split'](',') 语句
array = object[property](argument)
// 也可以直接取参数进行分割,方法不通用,比如分隔符换成 | 就不行了
// array = init.callee.object.value.split(',');
}
// 前面的兄弟节点就可以删除了
pervNode.remove();
});
// 储存正确顺序的控制流语句
let replace = [];
// 遍历控制流数组,按正确顺序取 case 内容
array.forEach(index => {
let consequent = switchNode.cases[index].consequent;
// 如果最后一个节点是 continue 语句,则删除 ContinueStatement 节点
if (types.isContinueStatement(consequent[consequent.length - 1])) {
consequent.pop();
}
// concat 方法拼接多个数组,即正确顺序的 case 内容
replace = replace.concat(consequent);
}
);
// 替换整个 while 节点,两种方法都可以
path.replaceWithMultiple(replace);
// path.replaceInline(replace);
}
}
traverse(ast, visitor)
const result = generate(ast)
console.log(result.code)
删除冗余代码混淆还原:
const parser = require("@babel/parser");
const generate = require("@babel/generator").default
const traverse = require("@babel/traverse").default
const types = require('@babel/types');
const code = `
const example = function () {
let a;
if (false) {
a = 1;
} else {
if (1) {
a = 2;
}
else {
a = 3;
}
}
return a;
};
`
const ast = parser.parse(code)
const visitor = {
enter(path) {
if (types.isBooleanLiteral(path.node.test) || types.isNumericLiteral(path.node.test)) {
if (path.node.test.value) {
path.replaceInline(path.node.consequent.body);
} else {
if (path.node.alternate) {
path.replaceInline(path.node.alternate.body);
} else {
path.remove()
}
}
}
}
}
traverse(ast, visitor)
const result = generate(ast)
console.log(result.code)
删除未使用变量混淆还原:
const parser = require("@babel/parser");
const generate = require("@babel/generator").default
const traverse = require("@babel/traverse").default
const code = `
const a = 1;
const b = a * 2;
const c = 2;
const d = b + 1;
const e = 3;
console.log(d)
`
const ast = parser.parse(code)
const visitor = {
VariableDeclarator(path){
const binding = path.scope.getBinding(path.node.id.name);
// 如标识符被修改过,则不能进行删除动作。
if (!binding || binding.constantViolations.length > 0) {
return;
}
// 未被引用
if (!binding.referenced) {
path.remove();
}
// 被引用次数为0
// if (binding.references === 0) {
// path.remove();
// }
// 长度为0,变量没有被引用过
// if (binding.referencePaths.length === 0) {
// path.remove();
// }
}
}
traverse(ast, visitor)
const result = generate(ast)
console.log(result.code)
二、案例
网站地址 :Imh0dHBzOi8vd3d3Lm5mdGNuLmNvbS9oNS8jLyI=
接口分析
根据接口不难发现,头部【nftcnApiNonce、Nftcnapiappsecret、Nftcnapinonce、Nftcnapisignature】就是我们需要破解的参数;
全局搜索"Nftcnapisignature",很快就发现参数加密的位置,jS文件被混淆了。
解混淆代码:
nftcnapitimestamp:时间戳
nftcnapinonce:随机数
function _() {
for (var t = '0123456789', n = "", i = 0; i < 21; i++) {
n += t['charAt'](Math['floor'](Math.random() * t.length));
}
return n;
}
nftcnApiAppSecret:(nftcnApiAppId+nftcnApiNonce)转大写
//nftcnApiAppSecret
_0xd4f37a = (0x0, _0xa7ae8['default'])(_0x3a9b25 + _0xaa94dc)[_0x474b47(0x58e)](),
var _0x51900b = function(_0x29df97) {
return function(_0x2e1e5d) {
var _0x1a5093 = a0_0x2631;
return new _0x308cec(!0x0)[_0x1a5093(0x1e14)](_0x2e1e5d)[_0x29df97]();
}
;
}
nftcnApiSignature: 加密算法SHA256withRSA
RSA加密参数:
'marketTabId=37&nftcnApiAppId=nftcn-web-h5&nftcnApiAppSecret=7840F2FF0A5DC96227DDA8D662706A63&nftcnApiNonce=442366595145494719079&nftcnApiTimestamp=1689738060972&pageNum=1&pageSize=10'
至于算法,你可以尝试还原混淆,把代码抠出来,篇幅比较长,这里我就演示啦~
_0x1d3ac4 = _0x52e66c(_0x57eec2),
function _0x52e66c(_0x468382) {
var _0x467755 = _0x37795e
, _0x2aa469 = new _0x4c8849[(_0x467755(0x343))]['RSAKey']()
, _0x546ab1 = _0x467755(0x480);
_0x2aa469 = _0x4c8849['default'][_0x467755(0x9c4)][_0x467755(0x79f)](_0x546ab1);
var _0x2dbdb3 = new _0x4c8849[(_0x467755(0x343))][(_0x467755(0x1e3))][(_0x467755(0x5f8))]['Signature']({
'alg': _0x467755(0x79c)
});
_0x2dbdb3[_0x467755(0xb36)](_0x2aa469),
_0x2dbdb3[_0x467755(0x800)](_0x468382);
var _0x18b063 = _0x4c8849[_0x467755(0x343)][_0x467755(0x373)](_0x2dbdb3[_0x467755(0x67b)]())
, _0x1b934b = _0x18b063;
return _0x1b934b;
}
运行结果:
总结
案例网站涉及加密算法比较多,JS有又被混淆,是一个可以练练手的网站,小伙伴们一起来探讨一下吧