某航空同盾AST解混淆和初步分析
提示:仅供学习交流,不得用做商业交易,如有侵权请及时联系作者删除
混淆分析
首先将目标代码下载下来,在本地进行格式化(方便分析),可以看到整体代码格式很工整,一个自执行方法,传入了一个大串字符串
将代码展开,可以看到首先就是一些花指令,包含了简单的运算(+,-,*,/)等等,然后就是一个自执行方法,传入一个函数,接着运行这个函数。
那么就可以知道传入的这个函数就是正儿八经的整体逻辑了,展开后发现有一层经典的OB混淆,字符串都调用了Oooo0这个数组,获取其中的Index位置的字符串。
解密函数定位
从上分析得知,字符串混淆根据Oooo0这个数组得出,那么只需要拿到这个数组就可以反解成正常的字符串,根据代码可以看出 Oooo0 是由 ooQGOO(Oooo0)返回生成的,那么传入的参数就是第一个自执行方法传入的长串字符串了
AST解混淆
根据上诉可得,只需要拿到ooQGOO函数,以及长串字符串即可成功解密混淆
直接编写AST代码尝试进行解混淆
const fs = require('fs');
const babel = require('@babel/core');
const parser = require('@babel/parser');
const traverse = require('@babel/traverse').default;
const generate = require('@babel/generator').default;
const targetFilePath = './test/xxx.js'; // 代码源文件
const code = fs.readFileSync(targetFilePath, 'utf-8');
var Oooo0 = "decodeStr"; // 解密用到的长串字符串
// 解密用到的解密函数
function ooQGOO(QQ00O, QOoo0){
return xxx;
}
// 解密后的数组
var decodeArray = ooQGOO(Oooo0);
// AST代码,匹配 MemberExpression 节点,对其进行替换
traverse(ast,
{
MemberExpression(path) {
try {
if (path.node.object.name == "Oooo0" && path.node.property.type == 'NumericLiteral') {
path.replaceWith(babel.types.StringLiteral(decodeArray[path.node.property.value]))
}
} catch (error) {
// 处理可能的错误,例如访问不存在的属性
console.error('处理CallExpression时出错:', error);
}
}
}
);
const output = generate(ast, {}, code);
const outputFilePath = './test/output.js';
fs.writeFileSync(outputFilePath, output.code, 'utf-8');
console.log('解混淆完成,结果已保存到:', outputFilePath);
尝试第一次运行,发现会报错,某个函数不存在,秉承着缺什么补什么的原则,去源码里面找,发现是花指令中的一个,查看全部的花指令发现并不多,这里可以直接全部复制
那么此时代码就变成了下面的样式
const fs = require('fs');
const babel = require('@babel/core');
const parser = require('@babel/parser');
const traverse = require('@babel/traverse').default;
const generate = require('@babel/generator').default;
const targetFilePath = './test/xxx.js'; // 代码源文件
const code = fs.readFileSync(targetFilePath, 'utf-8');
// 花指令
function oQQOO(QQ00O, QOoo0) {
return QQ00O <= QOoo0;
}
function QOQoQ(QQ00O, QOoo0) {
return QQ00O != QOoo0;
}
// xxxxx 记得全部复制,省事
var Oooo0 = "decodeStr"; // 解密用到的长串字符串
// 解密用到的解密函数
function ooQGOO(QQ00O, QOoo0){
return xxx;
}
// 解密后的数组
var decodeArray = ooQGOO(Oooo0);
// AST代码,匹配 MemberExpression 节点,对其进行替换
traverse(ast,
{
MemberExpression(path) {
try {
if (path.node.object.name == "Oooo0" && path.node.property.type == 'NumericLiteral') {
path.replaceWith(babel.types.StringLiteral(decodeArray[path.node.property.value]))
}
} catch (error) {
// 处理可能的错误,例如访问不存在的属性
console.error('处理CallExpression时出错:', error);
}
}
}
);
const output = generate(ast, {}, code);
const outputFilePath = './test/output.js';
fs.writeFileSync(outputFilePath, output.code, 'utf-8');
console.log('解混淆完成,结果已保存到:', outputFilePath);
继续尝试运行,发现报错window不存在,将window对象补上
那么最终代码如下:
const fs = require('fs');
const babel = require('@babel/core');
const parser = require('@babel/parser');
const traverse = require('@babel/traverse').default;
const generate = require('@babel/generator').default;
const targetFilePath = './test/xxx.js'; // 代码源文件
const code = fs.readFileSync(targetFilePath, 'utf-8');
window = globalThis;
// 花指令
function oQQOO(QQ00O, QOoo0) {
return QQ00O <= QOoo0;
}
function QOQoQ(QQ00O, QOoo0) {
return QQ00O != QOoo0;
}
// xxxxx 记得全部复制,省事
var Oooo0 = "decodeStr"; // 解密用到的长串字符串
// 解密用到的解密函数
function ooQGOO(QQ00O, QOoo0){
return xxx;
}
// 解密后的数组
var decodeArray = ooQGOO(Oooo0);
// AST代码,匹配 MemberExpression 节点,对其进行替换
traverse(ast,
{
MemberExpression(path) {
try {
if (path.node.object.name == "Oooo0" && path.node.property.type == 'NumericLiteral') {
path.replaceWith(babel.types.StringLiteral(decodeArray[path.node.property.value]))
}
} catch (error) {
// 处理可能的错误,例如访问不存在的属性
console.error('处理CallExpression时出错:', error);
}
}
}
);
const output = generate(ast, {}, code);
const outputFilePath = './test/output.js';
fs.writeFileSync(outputFilePath, output.code, 'utf-8');
console.log('解混淆完成,结果已保存到:', outputFilePath);
此时已经可以解除字符串混淆了,效果如下:
结语
解完混淆后发现,和上个版本的相比,关键加密代码差距挺大的,上个版本的3DES,hash128,RSA的明显特征这个版本基本搜不到,纯算的话还是需要点时间的,补环境话应该简单点。这里提供部分上个版本的指纹值,可以做下参考