一、2019年下半年,因为工作需要,有大量增值税发票需要查询真伪,而且是每张必查,当时还不太懂爬虫原理,就用“C#+大漠插件+全球鹰验证码识别"写了个类似于按键精灵那样的桌面应用程序,批量查询发票,用了近三个月,效果很满意。后来由于工作变动,不再需要查询,就没有去维护,就这样放弃了;
二、2021年底,有同事问我能不能做个批量查询工具,顺口就应承下来了。
1、花了近一个月的时间,学习《编辑原理》,开始感觉太抽象,就反复听,反复看书,整一个月,一有空,就听视频、看教材,似乎明白了一点点原理,但离理解还差太远。
推荐视频:国防科技大学-编译原理(国家级精品课)高清流畅_哔哩哔哩_bilibilihttps://www.bilibili.com/video/BV11t411V74n?p=2&spm_id_from=pageDriver
2、花了近半个月的时间,学习babel插件。
推荐学习资料:Babel 插件通关秘籍 - zxg_神说要有光 - 掘金小册 (juejin.cn)
JS逆向:AST还原极验混淆JS实战 (qq.com)
3、稍微懂了一点点编辑原理的皮毛,再加上了解了一点babel插件的用法,就不知天高地厚地开始了国税增值税发票查询平台的逆向过程。虽然结果还满意,但过程确实很艰辛,个中曲折就不说了。
第一步:过无限debugger似乎不难,无需多说;
第二步:拿到所有js源码文件。过了debugger,拿到js源码应该也不难
第三步:因为无法调试,只能先还原JS源码,用babel插件还原
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')
var newAst = null;
let jscode = fs.readFileSync(__dirname + '/source/90a1c.js', {
encoding: 'utf-8',
})
let ast = parser.parse(jscode);
hexUnicodeToString(ast); //将所有十六进制编码与Unicode编码转为正常字符
addArrFuncToMemory(ast); //将数组、公用函数添加到内存中
funcToStr(ast); //解密函数实现
var totalObj = {}; //定义一个空对象,用于存放字符串花指令对象
ast = generatorObj(ast); //将字符串花指令对象 写入到totalObj 对象中
changeJunkCode(ast); //遍历花指令对象中的值 ,如果是字符串,则不变,如果不是,则 //在 totalObj 中对象中找到相应的值进行替换,直至为字符串
ast = generatorObj(ast);
changeLastJunkCode(ast); //将对象引用中的字符串花指令转换为对象的字符串
ast = generatorObj(ast);
changeJunkFuncCode(ast); //遍历花指令对象中的值 ,如果是字符串,则不变,如果不是,则在 totalObj 中对象中找到相应的函数进行替换,直至为字符串
ast = generatorObj(ast);
changeLastJunkFuncCode(ast); //将对象引用中的函数花指令转换为对象的字符串
//removeJunkCode(ast); //移除花指令
//changeObjectAccessMode(ast); //对象['属性'] 改为对象.属性
//switchFlat(ast); // 处理switch混淆
let { code } = generator(ast, opts = { jsescOption: { "minimal": true } });
fs.writeFile(__dirname + '/result/new_90a1c.js', code, (err) => { })
//将所有十六进制编码与Unicode编码转为正常字符
function hexUnicodeToString(ast) {
traverse(ast, {
StringLiteral(path) {
var curNode = path.node
delete curNode.extra
},
NumericLiteral(path) {
var curNode = path.node
delete curNode.extra
},
RegExpLiteral(path) {
var curNode = path.node
delete curNode.extra
}
})
}
//将数组、公用函数添加到内存中
function addArrFuncToMemory(ast) {
newAst = parser.parse('');
newAst.program.body.push(ast.program.body[0]);
newAst.program.body.push(ast.program.body[1]);
newAst.program.body.push(ast.program.body[2]);
//newAst.program.body.push(ast.program.body[3]);
// 把这四部分的代码转为字符串,由于存在格式化检测,需要指定选项,来压缩代码
let stringArrFunc = generator(newAst, { compact: true }).code;
// 将字符串形式的代码执行,这样就可以在 nodejs 中运行调用数组和函数了
global.eval(stringArrFunc);
}
//解密函数实现
function funcToStr(ast) {
traverse(ast, {
VariableDeclarator(path) {
if (t.isFunctionExpression(path.node.init)) {
let funcName = path.node.id.name;
if (funcName !== "_0x4a4a") { return; }
let binding = path.scope.getBinding(funcName);
if (binding && binding.referencePaths) {
let referencePaths = binding.referencePaths;
referencePaths.map(p => {
if (t.isCallExpression(p.parentPath)) {
let str = eval(p.parentPath.toString());
p.parentPath.replaceWith(t.stringLiteral(str));
}
})
}
}
}
})
}
//将字符串花指令对象 写入到totalObj 对象中
function generatorObj(ast) {
traverse(ast, {
VariableDeclarator(path) {
//init 节点为 ObjectExpression 的时候,就是需要处理的对象
if (t.isObjectExpression(path.node.init)) {
//取出对象名
let objName = path.node.id.name;
//以对象名作为属性名在 totalObj 中创建对象
objName && (totalObj[objName] = totalObj[objName] || {});
//解析对象中的每一个属性,加入到新建的对象中去,注意属性值依然是 Node 类型
totalObj[objName] && path.node.init.properties.map(function (p) {
totalObj[objName][p.key.value] = p.value;
});
};
}
});
return ast;
}
//遍历花指令对象中的值 ,如果是字符串,则不变,如果不是,则在 totalObj 中对象中找到相应的值进行替换,直至为字符串
function changeJunkCode(ast) {
traverse(ast, {
VariableDeclarator(path) {
if (t.isObjectExpression(path.node.init)) {
path.node.init.properties.map(function (p) {
let realNode = findRealValue(p.value);
realNode && (p.value = realNode);
});
};
}
});
}
//一个递归转换函数
function findRealValue(node) {
if (t.isMemberExpression(node)) {
let objName = node.object.name;
let propName = node.property.value;
if (totalObj[objName][propName]) {
return findRealValue(totalObj[objName][propName]);
} else {
return false;
}
} else {
return node;
}
}
//将对象引用中的字符串花指令转换为对象的字符串
function changeLastJunkCode(ast) {
traverse(ast, {
MemberExpression(path) {
let objName = path.node.object.name;
let propName = path.node.property.value;
totalObj[objName] && t.isStringLiteral(totalObj[objName][propName]) &&
path.replaceWith(totalObj[objName][propName]);
}
});
}
部分还原后的JS代码
$(document)["ready"](function () {
$("#fpdm")["blur"](function () {
if ("eyHee" !== "fRCTG") {
retrycount = 0;
} else {
var _0x3d92c0 = new _0x439101();
var _0x89b16f = _0x3d92c0["getTime"]();
return _0x89b16f;
}
});
});
function yzmTime(_0x3dd72d) {
if (yzmWait == 0) {
$("#yzm_unuse_img")["hide"]();
$("#yzm_img")["show"]();
yzmWait = 60;
} else {
if (yzmWait == 2) {
if ("dgdfx" !== "FOacK") {
$("#yzm_unuse_img")["show"]();
$("#yzm_img")["hide"]();
} else {
_0x36629d("24小时内验证码请求太频繁,请稍后再试!", "警告");
_0x1eb024("#yzm_img")["hide"]();
}
}
yzmWait--;
setTimeout(function () {
if ("FUDMu" === "hzdbb") {
if (_0x1bd127 == 9) {
_0x2ed1ec("系统繁忙,请稍后重试!", "提示");
} else {
_0x1f2455 = _0x3d3c5d + 1;
_0x3f88c4();
}
} else {
yzmTime(_0x3dd72d);
}
}, 1000);
}
}
第四步: 挂上fiddler的AutoResponder ,就可以自由调试前端代码了
第五步:找到关键代码处理(验证码参数来源、查验提交参数的来源)
第六步:把 js代码扣出来,形成一个独立的js文件,
获取验证码参数中的 key9 flwq39 注意callback参数后面的时间戳,这里有个坑,处理不好,查询不了几张发票就让你出错
获取发票信息参数中的key9 flwq39
做到这一步,基本上就可以拿到发票信息了,剩下的,就是对结果进行判断了,主要是key1的值进行判断
这里还有个小坑要踩一踩,原以为到这里就算结束了,事实上不是,发票信息结果里面还有玄机,仔细一看,购买方名称与销售方名称时不时要颠倒顺序,原因就在下面这段代码里。
最后的成品:
我是编程菜鸟,刚学了一点皮毛,就在这里舞文弄墨,实是贻笑大方。第一次写这样的文章,见笔了。