【JS逆向系列】某乎x96参数3.0版本与jsvmp进阶
@(【JS逆向系列】某乎x96参数3.0版本与jsvmp进阶)## 前言
距离上一次的某乎jsvmp也过了好一段时间,现在也从2.0的版本升级到了3.0的版本
自然的,算法也就发生了一些改变。最明显最直接可见的变化就是长度边长了,并且相同的入参,输出结果并不相同。那么这篇文章就在原来2.0的基础上[【JS逆向系列】某乎x96参数与jsvmp初体验](https://www.52pojie.cn/thread-1619464-1-1.html),来分析一下3.0版本变难了多少,算法又要怎么还原出来。
## 初看js代码
至于参数如何查找这篇文章就跳过了,相关内容可以查看前一篇,这篇从【__g._encrypt】开始。两个版本的入口是相同的,都是从【__g._encrypt】进入到jsvmp内部代码,入参也都是一个md5结果的16进制字符串。
某乎的jsvmp与其他的略有不同,一般的jsvmp是堆栈式的,而某乎的这个是寄存器式的。也是也之前一样,是有vm的初始化,这次3.0的对象是【l】对象
结构上和之前还是很想的,不过多了不少参数,有几个关键的参数需要注意
|参数| 映射含义 |
|--|--|
| this.c | 通用寄存器 |
| this.s | pc寄存器 |
| this.S | 栈帧 |
| this.i | 数组缓存 |
| this.Q | 跳转标志位 |
| this.G | 操作码数组 |
| this.D | 字符串数组 |
| this.w | 控制流出口 |
| this.g | 异常跳转 |
| this.a | 时间检测参数 |
| this.e | 3字节操作码 |
| this.T | 控制流入口 |
| this.U | 时间检测参数 |
| this.M | 常量虚假指令 |
以上的仅仅是我个人的理解,不一定正确,仅供参考。
## 补环境方案
还是和之前一样,首先试试能不能通过补环境得出相同的结果,首先在网页上拿一组样本。
这里入参是【a63da42088bd8d635961ede065daeb51】结果是【RiO+y9AqW9KuaS+8vShliRMUs8LvryJRSxJinhVvmy+JvR5Xel5Uv5psmxAcilNl】,按照之前的办法,就是补环境使得到相同的结果,但是对于3.0版本就会出现问题。这里发现,相同的入参,多次执行,结果是不一样的。
这就不好办了,那么即使补环境出来的结果,也不知道是不是对的。一般这种情况下,就是计算涉及到的随机数或者时间。而这里就是包含的随机数,所以需要hook随机数的返回
```javascript
Math.random = function(){
return 0.50
};
```
输入这段代码后再执行加密函数,此时就发现结果都是一样的了
那么此时就得到了一组样本,当随机数恒定返回0.5时。入参【a63da42088bd8d635961ede065daeb51】的正确结果为【t=V/NpKQqHpejG8nmTuCzIrXW+JszxwLVVyuy+8S0ak=pe1N4BRA6Qxz+LDn+Xyj】,那么接下在就真正可以开始补环境了。
首先安装依赖库
```bash
npm install jsdom
npm install canvas
```
然后在头部加上jsdom的代码
```javascript
const{JSDOM}=require("jsdom");
const dom=new JSDOM("<!DOCTYPE html><p>Hello world</p>");
window=dom.window;
Math.random = function(){
return 0.50
};
```
结尾加上测试代码
```javascript
console.log(D('a63da42088bd8d635961ede065daeb51'));
console.log('t=V/NpKQqHpejG8nmTuCzIrXW+JszxwLVVyuy+8S0ak=pe1N4BRA6Qxz+LDn+Xyj');
```
开始测试运行
提示缺少【document】,那么就加上这个定义
```javascript
document=window.document;
```
继续运行,后面还有类似的报错,继续补全。
最后头部为
```javascript
const{JSDOM}=require("jsdom");
const dom=new JSDOM("<!DOCTYPE html><p>Hello world</p>");
window=dom.window;
document=window.document;
navigator=window.navigator;
location=window.location;
history=window.history;
screen=window.screen;
Math.random = function(){
return 0.50
};
```
测试可以运行出结果
这个结果和样本明显不一样,说明还缺少了其他环境没有补到。
那么接下来就得对前面的环境变量上代{过}{滤}理,看看还用到了什么属性和方法
```javascript
window = new Proxy(window, {
set(target, property, value, receiver) {
console.log("设置属性set window", property, typeof value);
return Reflect.set(...arguments);
},
get(target, property, receiver) {
console.log("获取属性get window", property, typeof target);
return target
}
});
document = new Proxy(document, {
set(target, property, value, receiver) {
console.log("设置属性set document", property, typeof value);
return Reflect.set(...arguments);
},
get(target, property, receiver) {
console.log("获取属性get document", property, typeof target);
return target
}
});
navigator = new Proxy(navigator, {
set(target, property, value, receiver) {
console.log("设置属性set navigator", property, typeof value);
return Reflect.set(...arguments);
},
get(target, property, receiver) {
console.log("获取属性get navigator", property, typeof target);
return target
}
});
location = new Proxy(location, {
set(target, property, value, receiver) {
console.log("设置属性set location", property, typeof value);
return Reflect.set(...arguments);
},
get(target, property, receiver) {
console.log("获取属性get location", property, typeof target);
return target
}
});
history = new Proxy(history, {
set(target, property, value, receiver) {
console.log("设置属性set history", property, typeof value);
return Reflect.set(...arguments);
},
get(target, property, receiver) {
console.log("获取属性get history", property, typeof target);
return target
}
});
screen = new Proxy(screen, {
set(target, property, value, receiver) {
console.log("设置属性set screen", property, typeof value);
return Reflect.set(...arguments);
},
get(target, property, receiver) {
console.log("获取属性get screen", property, typeof target);
return target
}
});
```
同时,整个大逻辑被一个try代码块包裹着
那么如果报错的话,我们也看不到,不方便补环境,所以去掉try代码块,只保留try里面的内容。
可以看到读取了不少属性,最后运行到【获取属性get document Symbol(Symbol.toStringTag) string】这一步就退出了,那么看看这一步的结果是不是和网页不一样
确实是不一样的结果,所以这里就需要hook掉toString方法
```javascript
var Object_toString = Object.prototype.toString;
Object.prototype.toString = function () {
let _temp = Object_toString.call(this, arguments);
console.log(this);
console.log("Object.prototype.toString: " + _temp);
if(this.constructor.name === 'Document'){
return '';
}
return _temp;
};
```
再次运行后,日志内容比之前更加长了,说明补的内容有效了,同时得到的加密结果也不一样了
这里最后是location对象出现问题,那么在jsdom上面,就需要补上url链接,那么就会自动补全location对象,开头部分的代码就修改为
```javascript
const{JSDOM}=require("jsdom");
const dom=new JSDOM("<!DOCTYPE html><p>Hello world</p>",{url:'https://www.zhihu.com/search'});
window=dom.window;
document=window.document;
navigator=window.navigator;
location=window.location;
history=window.history;
screen=window.screen;
Math.random = function(){
return 0.50
};
```
这里canvas和网页返回的不一样,继续补上
```javascript
var Object_toString = Object.prototype.toString;
Object.prototype.toString = function () {
let _temp = Object_toString.call(this, arguments);
console.log(this);
console.log("Object.prototype.toString: " + _temp);
if(this.constructor.name === 'Document'){
return '';
}else if(this.constructor.name === 'CanvasRenderingContext2D'){
return ''
}
return _temp;
};
```
又继续往下跑了,这次是检测了window下的_resourceLoader,浏览器上是undefined,但是node上返回对象。还有后面的_sessionHistory,一起补上。
```javascript
const{JSDOM}=require("jsdom");
const dom=new JSDOM("<!DOCTYPE html><p>Hello world</p>",{url:'https://www.zhihu.com/search'});
window=dom.window;
document=window.document;
navigator=window.navigator;
location=window.location;
history=window.history;
screen=window.screen;
window._resourceLoader = undefined;
window._sessionHistory = undefined;
Math.random = function(){
return 0.50
};
```
出现alert未定义,和之前一样处理
```javascript
const{JSDOM}=require("jsdom");
const dom=new JSDOM("<!DOCTYPE html><p>Hello world</p>",{url:'https://www.zhihu.com/search'});
window=dom.window;
document=window.document;
navigator=window.navigator;
location=window.location;
history=window.history;
screen=window.screen;
alert=window.alert;
window._resourceLoader = undefined;
window._sessionHistory = undefined;
Math.random = function(){
return 0.50
};
```
结果还是不一样,并且获取了window的原型后就没有了,那么这种情况很有可能检测了原型链和函数或者tostring,那么hook一下看看
```javascript
var Function_toString = Function.prototype.toString;
Function.prototype.toString = function () {
let _temp = Function_toString.call(this, arguments);
console.log(this);
console.log("Function.prototype.toString: " + _temp);
return _temp;
};
```
果然是,那么继续补上
```javascript
var Function_toString = Function.prototype.toString;
Function.prototype.toString = function () {
let _temp = Function_toString.call(this, arguments);
console.log(this);
console.log("Function.prototype.toString: " + _temp);
if(this.name === 'Window'){
return 'function Window() { }'
}
return _temp;
};
```
漂亮,终于得到一样的结果,那么这里补环境就完成了,总结一下我们补了什么
```javascript
const{JSDOM}=require("jsdom");
const dom=new JSDOM("<!DOCTYPE html><p>Hello world</p>",{url:'https://www.zhihu.com/search'});
window=dom.window;
document=window.document;
navigator=window.navigator;
location=window.location;
history=window.history;
screen=window.screen;
alert=window.alert;
window._resourceLoader = undefined;
window._sessionHistory = undefined;
Math.random = function(){
return 0.50
};
window = new Proxy(window, {
set(target, property, value, receiver) {
console.log("设置属性set window", property, typeof value);
return Reflect.set(...arguments);
},
get(target, property, receiver) {
console.log("获取属性get window", property, typeof target);
return target
}
});
document = new Proxy(document, {
set(target, property, value, receiver) {
console.log("设置属性set document", property, typeof value);
return Reflect.set(...arguments);
},
get(target, property, receiver) {
console.log("获取属性get document", property, typeof target);
return target
}
});
navigator = new Proxy(navigator, {
set(target, property, value, receiver) {
console.log("设置属性set navigator", property, typeof value);
return Reflect.set(...arguments);
},
get(target, property, receiver) {
console.log("获取属性get navigator", property, typeof target);
return target
}
});
location = new Proxy(location, {
set(target, property, value, receiver) {
console.log("设置属性set location", property, typeof value);
return Reflect.set(...arguments);
},
get(target, property, receiver) {
console.log("获取属性get location", property, typeof target);
return target
}
});
history = new Proxy(history, {
set(target, property, value, receiver) {
console.log("设置属性set history", property, typeof value);
return Reflect.set(...arguments);
},
get(target, property, receiver) {
console.log("获取属性get history", property, typeof target);
return target
}
});
screen = new Proxy(screen, {
set(target, property, value, receiver) {
console.log("设置属性set screen", property, typeof value);
return Reflect.set(...arguments);
},
get(target, property, receiver) {
console.log("获取属性get screen", property, typeof target);
return target
}
});
var Object_toString = Object.prototype.toString;
Object.prototype.toString = function () {
let _temp = Object_toString.call(this, arguments);
console.log(this);
console.log("Object.prototype.toString: " + _temp);
if(this.constructor.name === 'Document'){
return '';
}else if(this.constructor.name === 'CanvasRenderingContext2D'){
return ''
}
return _temp;
};
var Function_toString = Function.prototype.toString;
Function.prototype.toString = function () {
let _temp = Function_toString.call(this, arguments);
console.log(this);
console.log("Function.prototype.toString: " + _temp);
if(this.name === 'Window'){
return 'function Window() { }'
}
return _temp;
};
```
当需要运行的时候,可以把代码部分的代码注释掉,因为这部分只是方便我们查看以及补环境,不影响最终的结果
## 修改字节码方案(反混淆与反汇编)
在修改字节码之前,要么需要详细分析字节码的逻辑,又或者反汇编字节码到类似js代码的方式。再来看能不能通过修改字节码的方案来绕过环境检测。
例如之前2.0部分的代码,是先进行环境检测,检测完成后才进行真正的加密,所以才可以修改字节码,使得它跳过了环境检测的部分,直接开始核心的加密函数。如果3.0也是沿用之前的逻辑,先进行了检测再加密,那么这种方案就是可行的。
但是3.0没有办法直接进行反汇编,因为相对于2.0的代码来说,增加了控制流的代码,那么最好是先尝试还原了控制流,再做后续处理。
(~~首先是~~ 按照前面说的去掉try代码块)
首先肯定是处理反调试,3.0也是有时间检测,但是时间检测被放到了jsvmp内部了,不好直接干掉,那么就把初始化里面关于时间的都干掉
```javascript
// 删除时间参数
traverse(ast, {
SwitchCase(path){
if(path.node.test){
if(path.node.test.value === 300){
path.node.consequent.splice(0, 1)
}else if(path.node.test.value === 360){
path.node.consequent.splice(0, 1)
}else if(path.node.test.value === 368){
path.node.consequent.expression.right.test = t.booleanLiteral(false)
}
}
},
FunctionDeclaration(path){
if(path.node.id && path.node.id.name === 'l'){
for (let i = path.node.body.body.length - 1; i >= 0; i--) {
let item = path.node.body.body;
if(item.expression.left.property.name === 'a' || item.expression.left.property.name === 'U'){
path.node.body.body.splice(i, 1)
}
}
}
}
});
```
此时再运行,依然可以得到相同的结果,那么就说明这里的时间和2.0是一样,只是用来反调试,与加密逻辑无关。
接下来也不知道怎么入手,那么就来点暴力点的,这么多个case,有没有可能有一些是没有用到的呢?那么在所有的case前面都下一个断点
然后调试运行,当在断点停下的时候,取消断点再运行下去,直到结束。那么下载来还有断点的case,就是不会运行到的case了。
```javascript
let cases_list = ;
traverse(ast, {
SwitchCase(path){
if(path.node.test){
if(cases_list.includes(path.node.test.value)){
path.remove()
}
}
}
});
```
这时删除了多个case后,依然可以得到正确结果。
继续往后调试,会发现一些控制流的分支是虚假分支,也就是在运行的时候,是恒真或者恒假的分支,这种最好是可以将它还原掉,方便后面真实分支的case合并。
分析发现,例如在case 331中存在赋值的【this.M = 8;】,这里就可以把这个值记录下来,其他任何没有出现赋值的,都是null了
```javascript
let cases_dict = {};
// 数组虚假分析
traverse(ast, {
MemberExpression(path){
if(path.node.object.type === 'ThisExpression' && path.node.property.type === 'Identifier' && path.node.property.name === 'M' && path.parent.type === 'MemberExpression'){
if(path.parentPath.parent.type === 'AssignmentExpression' && path.parentPath.parent.right.type === 'NumericLiteral'){
cases_dict = path.parentPath.parent.right.value;
path.parentPath.parentPath.parentPath.remove()
}
}
}
});
traverse(ast, {
MemberExpression(path){
if(path.node.object.type === 'ThisExpression' && path.node.property.type === 'Identifier' && path.node.property.name === 'M' && path.parent.type === 'MemberExpression'){
if(cases_dict.hasOwnProperty(path.parent.property.value)){
path.parentPath.replaceWith(t.numericLiteral(cases_dict))
}else{
path.parentPath.replaceWith(t.numericLiteral(0))
}
}
}
});
```
这样就转变成了字面量
接着例如case 50,下面的this.T实际就是上面的50,像这种也可以还原为字面量
```javascript
// 还原this.T
traverse(ast, {
SwitchCase(path){
if(path.node.test){
if(path.node.consequent.length > 1 && path.node.consequent.expression.right.type === 'BinaryExpression'){
let item = path.node.consequent.expression.right;
if(item.left.type === 'BinaryExpression' && item.left.right.type === 'MemberExpression' && item.left.right.property.type === 'Identifier' && item.left.right.property.name === 'T'){
item.left.right = t.numericLiteral(path.node.test.value);
}
}
}
}
});
```
最后是case 331的地方
这里的【V】变量只出现了一次,所以可以进行手动的处理
接着再将字面量的内容还原一下
前面会发现很多个都指向了352,那么这个352肯定是一个关键的地方
最后还有一些是有【+=】或者【-=】的,这种也可以顺带还原一下
```javascript
traverse(ast, {
SwitchCase(path){
if(path.node.test){
if(path.node.consequent.expression.right.type === 'NumericLiteral'){
if(path.node.consequent.expression.operator === '+='){
path.node.consequent.expression.operator = '=';
path.node.consequent.expression.right.value = path.node.consequent.expression.right.value + path.node.test.value;
}else if(path.node.consequent.expression.operator === '-='){
path.node.consequent.expression.operator = '=';
path.node.consequent.expression.right.value = path.node.test.value - path.node.consequent.expression.right.value;
}
}
}
}
});
```
那么来到这里,ast的部分就算完成了。
先看看这个case 352,它是指向case 300
然后case 300,又指向了case 368,那么如下图
如果把后面的两个节点合并成一个,那么就少了一个节点了,其实相当于把case 368的代码全部放到case 300的下面
换完以后,再次运行一下代码,也能得到相同的结果,那就说明这么修改是没有问题的,没有影响原来的执行流程。
接着就可以开始分析vmp的代码逻辑了。根据之前2.0的经验以及分析,case 331就是解码那段base64字符串到字节码的逻辑
```javascript
let b64_code = 'ABt7CAAUSAAACADfSAAACAD1SAAACAAHSAAAC......';//内容太长省略了
let opcode_list = [];
let text_list = [];
function decrypt_text(item){
let U = 66;
let M = [];
for (let b = 0; b < item.length; b++) {
M.push(String.fromCharCode(24 ^ item.charCodeAt(b) ^ U));
U = 24 ^ item.charCodeAt(b) ^ U;
}
return M.join("")
}
(function () {
let D = atob(b64_code);
let w = D.charCodeAt(0) << 16 | D.charCodeAt(1) << 8 | D.charCodeAt(2);
for (var k = 3; k < w + 3; k += 3) {
opcode_list.push(D.charCodeAt(k) << 16 | D.charCodeAt(k + 1) << 8 | D.charCodeAt(k + 2));
}
for (let V = w + 3; V < D.length;) {
let E = D.charCodeAt(V) << 8 | D.charCodeAt(V + 1);
let T = D.slice(V + 2, V + 2 + E);
text_list.push(decrypt_text(T));
V += E + 2;
}
})();
console.log(opcode_list);
console.log(text_list);
```
可以看到字节码和字符串都解密出来了,接着就从case 352开始入手写代码,相对于2.0,代码逻辑基本没有变化太多,变化的都是因为控制流而影响的
相当于把2.0逻辑的所有函数展平,然后用控制流来执行,把之前的代码改一改,又能用起来,还原了初始化的代码如下
```javascript
(function () {
let local_0 = ;
let local_1 = ;
let local_2 = ;
let local_3 = new Array(32);
let local_23 = ;
let local_24 = ;
let local_25 = new Array(4);
let local_26 = new Array(36);
let local_6 = (local_2 & 255) << 24;
let local_7 = (local_2 & 255) << 16;
let local_8 = (local_2 & 255) << 8;
let local_9 = local_2 & 255;
local_25 = local_6 | local_7 | local_8 | local_9;
local_6 = (local_2 & 255) << 24;
local_7 = (local_2 & 255) << 16;
local_8 = (local_2 & 255) << 8;
local_9 = local_2 & 255;
local_25 = local_6 | local_7 | local_8 | local_9;
local_6 = (local_2 & 255) << 24;
local_7 = (local_2 & 255) << 16;
local_8 = (local_2 & 255) << 8;
local_9 = local_2 & 255;
local_25 = local_6 | local_7 | local_8 | local_9;
local_6 = (local_2 & 255) << 24;
local_7 = (local_2 & 255) << 16;
local_8 = (local_2 & 255) << 8;
local_9 = local_2 & 255;
local_25 = local_6 | local_7 | local_8 | local_9;
local_26 = local_25 ^ local_24;
local_26 = local_25 ^ local_24;
local_26 = local_25 ^ local_24;
local_26 = local_25 ^ local_24;
local_9 = 0;
while (local_9 < 32) {
let local_27 = local_26;
let local_28 = local_26;
let local_29 = local_26;
let local_30 = local_27 ^ local_28 ^ local_29 ^ local_23;
let local_16 = new Array(4);
let local_5 = new Array(4);
local_16 = 255 & local_30 >>> 24;
local_16 = 255 & local_30 >>> 16;
local_16 = 255 & local_30 >>> 8;
local_16 = 255 & local_30;
local_5 = local_0 & 255];
local_5 = local_0 & 255];
local_5 = local_0 & 255];
local_5 = local_0 & 255];
local_6 = (local_5 & 255) << 24;
local_7 = (local_5 & 255) << 16;
local_8 = (local_5 & 255) << 8;
local_9 = local_5 & 255;
let local_17 = local_6 | local_7 | local_8 | local_9;
let local_13 = 32 - 13;
let local_14 = (local_17 & 4294967295) << 13;
let local_15 = local_14 | local_17 >>> local_13;
let local_18 = local_15;
local_13 = 32 - 23;
local_14 = (local_17 & 4294967295) << 23;
local_15 = local_14 | local_17 >>> local_13;
let local_19 = local_15;
let local_20 = local_17 ^ local_18 ^ local_19;
let local_31 = local_20;
local_26 = local_26 ^ local_31;
local_3 = local_26;
local_9 = local_9 + 1;
}
window["__ZH__"]["zse"]["zk"] = local_3;
window["__ZH__"]["zse"]["zb"] = local_0;
window["__ZH__"]["zse"]["zm"] = local_1;
function _0x2068(parameter) {}
__g["_encrypt"] = _0x2068;
})();
```
可以看到这里是初始化了一大堆固定值,然后复制给了【zk】、【zb】、【zm】这三个对象,那么它前面的一大堆计算其实也不用管,因为也没有涉及到时间和随机数,也就是说计算前是定值,那么计算后,也一定是一个定值,那么我们直接用计算后的定值就可以了。最后两句是定义了一个函数,赋值给了【__g】的【_encrypt】属性,这一点和2.0是一模一样的。
那么接着还原一下【_0x2068】内部的逻辑
```javascript
function _0x2068(parameter) {
let local_47 = Date["now"]();
let local_48 = ;
let local_44 = Date["now"]();
if (typeof window == "undefined") {
local_44 = 1;
}
if (!(typeof window == "undefined")) {
if (typeof document == "undefined") {
local_44 = 2;
}
if (!(typeof document == "undefined")) {
if (typeof navigator == "undefined") {
local_44 = 3;
}
if (!(typeof navigator == "undefined")) {
if (typeof location == "undefined") {
local_44 = 4;
}
if (!(typeof location == "undefined")) {
if (typeof history == "undefined") {
local_44 = 5;
}
if (!(typeof history == "undefined")) {
if (typeof screen == "undefined") {
local_44 = 6;
}
if (!(typeof screen == "undefined")) {
if (typeof navigator["userAgent"] == "undefined") {
local_44 = 7;
}
if (!(typeof navigator["userAgent"] == "undefined")) {
if (window["name"] == "nodejs") {
local_44 = 8;
}
if (!(window["name"] == "nodejs")) {
if (document["toString"]()["indexOf"]("HTMLDocument") == -1) {
local_44 = 10;
}
if (!(document["toString"]()["indexOf"]("HTMLDocument") == -1)) {
if (navigator["toString"]()["indexOf"]("Navigator") == -1) {
local_44 = 11;
}
if (!(navigator["toString"]()["indexOf"]("Navigator") == -1)) {
if (location["toString"]()["indexOf"]("http") == -1) {
local_44 = 12;
}
if (!(location["toString"]()["indexOf"]("http") == -1)) {
if (history["toString"]()["indexOf"]("History") == -1) {
local_44 = 13;
}
if (!(history["toString"]()["indexOf"]("History") == -1)) {
if (screen["toString"]()["indexOf"]("Screen") == -1) {
local_44 = 14;
}
if (!(screen["toString"]()["indexOf"]("Screen") == -1)) {
if (navigator["userAgent"]["toLowerCase"]()["indexOf"]("headless") !== -1) {
local_44 = 15;
}
if (!(navigator["userAgent"]["toLowerCase"]()["indexOf"]("headless") !== -1)) {
let local_45 = Object["getOwnPropertyDescriptor"];
if (local_45["toString"]()["indexOf"]("native code") == -1) {
local_44 = 16;
}
if (!(local_45["toString"]()["indexOf"]("native code") == -1)) {
if (typeof document["createElement"] == "undefined") {
local_44 = 17;
}
if (!(typeof document["createElement"] == "undefined")) {
if (document["createElement"]("canvas")["getContext"]("2d")["toString"]()["indexOf"]("Canvas") == -1) {
local_44 = 22;
}
if (!(document["createElement"]("canvas")["getContext"]("2d")["toString"]()["indexOf"]("Canvas") == -1)) {
if (typeof window["buffer"] !== "undefined") {
local_44 = 24;
}
if (!(typeof window["buffer"] !== "undefined")) {
if (typeof window["emit"] !== "undefined") {
local_44 = 25;
}
if (!(typeof window["emit"] !== "undefined")) {
if (typeof window["callPhantom"] !== "undefined") {
local_44 = 26;
}
if (!(typeof window["callPhantom"] !== "undefined")) {
if (typeof window["__phantomas"] !== "undefined") {
local_44 = 27;
}
if (!(typeof window["__phantomas"] !== "undefined")) {
if (typeof window["_phantom"] !== "undefined") {
local_44 = 28;
}
if (!(typeof window["_phantom"] !== "undefined")) {
if (typeof window["WebPage"] !== "undefined") {
local_44 = 29;
}
if (!(typeof window["WebPage"] !== "undefined")) {
if (typeof window["fxdriver_id"] !== "undefined") {
local_44 = 30;
}
if (!(typeof window["fxdriver_id"] !== "undefined")) {
if (typeof window["__fxdriver_unwrapped"] !== "undefined") {
local_44 = 31;
}
if (!(typeof window["__fxdriver_unwrapped"] !== "undefined")) {
if (typeof window["domAutomation"] !== "undefined") {
local_44 = 32;
}
if (!(typeof window["domAutomation"] !== "undefined")) {
if (typeof window["ubot"] !== "undefined") {
local_44 = 33;
}
if (!(typeof window["ubot"] !== "undefined")) {
if (typeof window["CasperError"] !== "undefined") {
local_44 = 34;
}
if (!(typeof window["CasperError"] !== "undefined")) {
if (typeof window["casper"] !== "undefined") {
local_44 = 35;
}
if (!(typeof window["casper"] !== "undefined")) {
if (typeof window["patchRequire"] !== "undefined") {
local_44 = 36;
}
if (!(typeof window["patchRequire"] !== "undefined")) {
if (typeof document["$cdc_asdjflasutopfhvcZLmcfl_"] !== "undefined") {
local_44 = 37;
}
if (!(typeof document["$cdc_asdjflasutopfhvcZLmcfl_"] !== "undefined")) {
if (navigator["webdriver"] == true) {
local_44 = 38;
}
if (!(navigator["webdriver"] == true)) {
if (typeof document["__webdriver_script_fn"] !== "undefined") {
local_44 = 39;
}
if (!(typeof document["__webdriver_script_fn"] !== "undefined")) {
if (typeof window["_resourceLoader"] !== "undefined") {
local_44 = 40;
}
if (!(typeof window["_resourceLoader"] !== "undefined")) {
if (typeof window["_sessionHistory"] !== "undefined") {
local_44 = 41;
}
if (!(typeof window["_sessionHistory"] !== "undefined")) {
if (typeof window["global"] !== "undefined") {
local_44 = 42;
}
if (!(typeof window["global"] !== "undefined")) {
if (typeof Object["getPrototypeOf"](alert) !== "function") {
local_44 = 43;
}
if (!(typeof Object["getPrototypeOf"](alert) !== "function")) {
if (typeof document["getElementById"] == "undefined") {
local_44 = 44;
}
if (!(typeof document["getElementById"] == "undefined")) {
if (typeof Object["getPrototypeOf"](document["getElementById"]) !== "function") {
local_44 = 45;
}
if (!(typeof Object["getPrototypeOf"](document["getElementById"]) !== "function")) {
if (typeof document["getElementsByClassName"] == "undefined") {
local_44 = 46;
}
if (!(typeof document["getElementsByClassName"] == "undefined")) {
if (window["__proto__"]["constructor"]["toString"]()["indexOf"]("") == -1) {
local_44 = 48;
}
if (!(window["__proto__"]["constructor"]["toString"]()["indexOf"]("") == -1)) {
if (typeof window["__nightmare"] !== "undefined") {
local_44 = 49;
}
if (!(typeof window["__nightmare"] !== "undefined")) {
if (new Error()["stack"]["indexOf"]("localhost") !== -1) {
local_44 = 50;
}
if (!(new Error()["stack"]["indexOf"]("localhost") !== -1)) {
if (new Error()["stack"]["indexOf"]("puppeteer") !== -1) {
local_44 = 51;
}
if (!(new Error()["stack"]["indexOf"]("puppeteer") !== -1)) {
if (navigator["userAgent"]["toLowerCase"]()["indexOf"]("phantomjs") !== -1) {
local_44 = 52;
}
if (!(navigator["userAgent"]["toLowerCase"]()["indexOf"]("phantomjs") !== -1)) {
if (navigator["userAgent"]["toLowerCase"]()["indexOf"]("electron") !== -1) {
local_44 = 53;
}
if (!(navigator["userAgent"]["toLowerCase"]()["indexOf"]("electron") !== -1)) {
if (location["href"]["indexOf"]("localhost") !== -1) {
local_44 = 54;
}
if (!(location["href"]["indexOf"]("localhost") !== -1)) {
if (window["spawn"]) {
local_44 = 56;
}
if (!window["spawn"]) {
if (typeof window["_Selenium_IDE_Recorder"] !== "undefined") {
local_44 = 57;
}
if (!(typeof window["_Selenium_IDE_Recorder"] !== "undefined")) {
if (typeof window["_selenium"] !== "undefined") {
local_44 = 58;
}
if (!(typeof window["_selenium"] !== "undefined")) {
if (typeof window["__webdriver_evaluate"] !== "undefined") {
local_44 = 59;
}
if (!(typeof window["__webdriver_evaluate"] !== "undefined")) {
if (typeof window["__selenium_evaluate"] !== "undefined") {
local_44 = 60;
}
if (!(typeof window["__selenium_evaluate"] !== "undefined")) {
if (typeof window["__webdriver_script_function"] !== "undefined") {
local_44 = 61;
}
if (!(typeof window["__webdriver_script_function"] !== "undefined")) {
if (typeof window["__fxdriver_evaluate"] !== "undefined") {
local_44 = 62;
}
if (!(typeof window["__fxdriver_evaluate"] !== "undefined")) {
if (typeof window["__driver_unwrapped"] !== "undefined") {
local_44 = 63;
}
if (!(typeof window["__driver_unwrapped"] !== "undefined")) {
local_44 = 0;
}
}
}
}
}
}
}
}
}
}
}
}
}
}
}
}
}
}
}
}
}
}
}
}
}
}
}
}
}
}
}
}
}
}
}
}
}
}
}
}
}
}
}
}
}
}
}
}
}
}
}
}
}
}
}
let local_49 = local_44;
let local_41 = [];
let local_42 = parameter["length"];
let local_12 = 0;
while (local_12 < local_42) {
let local_43 = parameter["charCodeAt"](local_12);
local_41["push"](local_43 & 255);
local_12 = local_12 + 1;
}
let local_50 = local_41;
let local_51 = Date["now"]() - local_47;
if (local_51 > 10000) {
local_49 = 126;
}
if (!(local_51 > 10000)) {}
let local_52 = Math["floor"](Math["random"]() * 127);
local_50["unshift"](local_49);
local_50["unshift"](local_52);
let local_21 = local_50["length"] % 16;
let local_10 = 16 - local_21;
let local_33 = 0;
while (local_33 < local_10) {
local_50["push"](local_10);
local_33 = local_33 + 1;
}
let local_34 = local_50["slice"](0, 16);
let local_35 = new Array(16);
let local_11 = 0;
while (local_11 < 16) {
local_35 = local_34 ^ local_48 ^ 42;
local_11 = local_11 + 1;
}
let local_36 = __g["r"](local_35);
let local_37 = local_36["slice"]();
let local_38 = local_50["slice"](16, local_50["length"]);
let local_39 = __g["x"](local_38, local_36);
local_37 = local_37["concat"](local_39);
let local_53 = local_37;
let local_54 = local_53["length"] % 3;
if (local_54 == 1) {
local_53["push"]("\0");
local_53["push"]("\0");
}
if (!(local_54 == 1)) {}
if (local_54 == 2) {
local_53["push"]("\0");
}
if (!(local_54 == 2)) {}
let local_55 = "6fpLR" + "qJO8M/c3j" + "nYxFkUV" + "C4ZIG12SiH=5v0mXDazWB" + "Tsuw7QetbKdoPyAl+hN9rgE";
let local_56 = 0;
let local_57 = "";
let local_13 = local_53["length"] - 1;
while (local_13 >= 0) {
let local_58 = 8 * (local_56 % 4);
local_56 = local_56 + 1;
let local_59 = local_53 ^ 58 >>> local_58 & 255;
local_58 = 8 * (local_56 % 4);
local_56 = local_56 + 1;
local_59 = local_59 | (local_53 ^ 58 >>> local_58 & 255) << 8;
local_58 = 8 * (local_56 % 4);
local_56 = local_56 + 1;
local_59 = local_59 | (local_53 ^ 58 >>> local_58 & 255) << 16;
local_57 = local_57 + local_55["charAt"](local_59 & 63);
local_57 = local_57 + local_55["charAt"](local_59 >>> 6 & 63);
local_57 = local_57 + local_55["charAt"](local_59 >>> 12 & 63);
local_57 = local_57 + local_55["charAt"](local_59 >>> 18 & 63);
local_13 = local_13 - 3;
}
return local_57;
}
```
环境检测部分有点长,但是却发现,3.0的逻辑也是先进行环境检测,再进行加密函数的,那么到这里就可以确定,之前说的修改字节码的方法是可以实现的。
修改的核心思路那之前的基本相似,就是当第一次出现跳转指令的时候,把pc寄存器的值修改到第二次跳转的后面
3.0的跳转代码在case 443,第一次出现跳转时,pc寄存器的值为【1284】,这里的this.b就是要转跳的值,在第一次跳转的时候暂时不用管。
接着进入到第二次跳转,此时的pc寄存器位【1288】,并且记录一下this.b为【2066】。这时就可以尝试修改。但是这里有一个逗号表达式,不方便我们修改,那么手动把它搞成一个一个语句。
但是这样还不对,因为环境检测还不是一个正确的值,那么直接设置为检验通过的值
那么加上检测校验正确的值,那么就应该可以了,前面仅仅保留一些必要的代码
```javascript
window=global;
Math.random = function(){
return 0.50
};
```
这时运行发现,结果也是一模一样的,那么说明修改字节码的方案完成。
## 算法还原
那么把vmp反汇编出来了,那么算法还原的难度也就基本没有了,相当于就是把js的逻辑写成其他语言的逻辑
```python
def x_zse_96_b64encode(md5_bytes: bytes):
h = {
"zk": ,
"zb": ,
"zm":
}
def left_shift(x, y):
x, y = ctypes.c_int32(x).value, y % 32
return ctypes.c_int32(x << y).value
def Unsigned_right_shift(x, y):
x, y = ctypes.c_uint32(x).value, y % 32
return ctypes.c_uint32(x >> y).value
def Q(e, t):
return left_shift((4294967295 & e), t) | Unsigned_right_shift(e, 32 - t)
def G(e):
t = list(struct.pack(">i", e))
n = ], h['zb']], h['zb']], h['zb']]]
r = struct.unpack(">i", bytes(n))
return r ^ Q(r, 2) ^ Q(r, 10) ^ Q(r, 18) ^ Q(r, 24)
def g_r(e):
n = list(struct.unpack(">iiii", bytes(e)))
^ G(n ^ n ^ n ^ h['zk'])) for r in range(32)]
return list(struct.pack(">i", n) + struct.pack(">i", n) + struct.pack(">i", n) + struct.pack(">i", n))
def g_x(e, t):
n = []
i = 0
for _ in range(len(e), 0, -16):
o = e
a = ^ t for c in range(16)]
t = g_r(a)
n += t
i += 1
return n
local_48 =
local_50 = bytes() + md5_bytes# 随机数0 是环境检测通过
local_50 = x_zse_96_V3.pad(bytes(local_50))
local_34 = local_50[:16]
local_35 = ^ local_48 ^ 42 for local_11 in range(16)]
local_36 = g_r(local_35)
local_38 = local_50
local_39 = g_x(local_38, local_36)
local_53 = local_36 + local_39
local_55 = "6fpLRqJO8M/c3jnYxFkUVC4ZIG12SiH=5v0mXDazWBTsuw7QetbKdoPyAl+hN9rgE"
local_56 = 0
local_57 = ""
for local_13 in range(len(local_53) - 1, 0, -3):
local_58 = 8 * (local_56 % 4)
local_56 = local_56 + 1
local_59 = local_53 ^ Unsigned_right_shift(58, local_58) & 255
local_58 = 8 * (local_56 % 4)
local_56 = local_56 + 1
local_59 = local_59 | (local_53 ^ Unsigned_right_shift(58, local_58) & 255) << 8
local_58 = 8 * (local_56 % 4)
local_56 = local_56 + 1
local_59 = local_59 | (local_53 ^ Unsigned_right_shift(58, local_58) & 255) << 16
local_57 = local_57 + local_55
local_57 = local_57 + local_55
local_57 = local_57 + local_55
local_57 = local_57 + local_55
return local_57
```
运行一下测试代码
结果也是完全正确的,那么算法还原的方案也完成了。
那么有了编码,会不会也有解码的方法呢?那肯定是有的,因为如果没有,服务器又怎么验证传上来的结果对不对呢。解码方法其实就是编码方法的逆运算。
例如加法和减法互为逆运算,因为一个数字我加上一个数,再减去这个数,还是得到原来的数字。
乘法和除法互为逆运算、异或和自身互为逆运算等等。那么如果要得到解码方法,相当于就是自己写一个逆运算的方法,按照前面的逻辑可以尝试编写
```python
class x_zse_96_V3(object):
local_48 =
local_55 = "6fpLRqJO8M/c3jnYxFkUVC4ZIG12SiH=5v0mXDazWBTsuw7QetbKdoPyAl+hN9rgE"
h = {
"zk": ,
"zb": ,
"zm":
}
@staticmethod
def pad(data_to_pad):
padding_len = 16 - len(data_to_pad) % 16
padding = chr(padding_len).encode() * padding_len
return data_to_pad + padding
@staticmethod
def unpad(padded_data):
padding_len = padded_data[-1]
return padded_data[:-padding_len]
@staticmethod
def left_shift(x, y):
x, y = ctypes.c_int32(x).value, y % 32
return ctypes.c_int32(x << y).value
@staticmethod
def Unsigned_right_shift(x, y):
x, y = ctypes.c_uint32(x).value, y % 32
return ctypes.c_uint32(x >> y).value
@classmethod
def Q(cls, e, t):
return cls.left_shift((4294967295 & e), t) | cls.Unsigned_right_shift(e, 32 - t)
@classmethod
def G(cls, e):
t = list(struct.pack(">i", e))
n = ], cls.h['zb']], cls.h['zb']], cls.h['zb']]]
r = struct.unpack(">i", bytes(n))
return r ^ cls.Q(r, 2) ^ cls.Q(r, 10) ^ cls.Q(r, 18) ^ cls.Q(r, 24)
@classmethod
def g_r(cls, e):
n = list(struct.unpack(">iiii", bytes(e)))
^ cls.G(n ^ n ^ n ^ cls.h['zk'])) for r in range(32)]
return list(struct.pack(">i", n) + struct.pack(">i", n) + struct.pack(">i", n) + struct.pack(">i", n))
@classmethod
def re_g_r(cls, e):
n = * 32 + list(struct.unpack(">iiii", bytes(e)))[::-1]
for r in range(31, -1, -1):
n = cls.G(n ^ n ^ n ^ cls.h['zk']) ^ n
return list(struct.pack(">i", n) + struct.pack(">i", n) + struct.pack(">i", n) + struct.pack(">i", n))
@classmethod
def g_x(cls, e, t):
n = []
i = 0
for _ in range(len(e), 0, -16):
o = e
a = ^ t for c in range(16)]
t = cls.g_r(a)
n += t
i += 1
return n
@classmethod
def re_g_x(cls, e, t):
n = []
i = 0
for _ in range(len(e), 0, -16):
o = e
a = cls.re_g_r(o)
t = ^ t for c in range(16)]
n += t
t = o
i += 1
return n
@classmethod
def b64encode(cls, md5_bytes: bytes, device: int = 0, seed: int = 63) -> str:
local_50 = bytes() + md5_bytes# 随机数0 是环境检测通过
local_50 = cls.pad(bytes(local_50))
local_34 = local_50[:16]
local_35 = ^ cls.local_48 ^ 42 for local_11 in range(16)]
local_36 = cls.g_r(local_35)
local_38 = local_50
local_39 = cls.g_x(local_38, local_36)
local_53 = local_36 + local_39
local_56 = 0
local_57 = ""
for local_13 in range(len(local_53) - 1, 0, -3):
local_58 = 8 * (local_56 % 4)
local_56 = local_56 + 1
local_59 = local_53 ^ cls.Unsigned_right_shift(58, local_58) & 255
local_58 = 8 * (local_56 % 4)
local_56 = local_56 + 1
local_59 = local_59 | (local_53 ^ cls.Unsigned_right_shift(58, local_58) & 255) << 8
local_58 = 8 * (local_56 % 4)
local_56 = local_56 + 1
local_59 = local_59 | (local_53 ^ cls.Unsigned_right_shift(58, local_58) & 255) << 16
local_57 = local_57 + cls.local_55
local_57 = local_57 + cls.local_55
local_57 = local_57 + cls.local_55
local_57 = local_57 + cls.local_55
return local_57
@classmethod
def b64decode(cls, x_zse_96: str) -> dict:
local_56 = 0
local_57 = []
for local_13 in range(0, len(x_zse_96), 4):
local_59 = (cls.local_55.index(x_zse_96) << 18) + (cls.local_55.index(x_zse_96) << 12) + (cls.local_55.index(x_zse_96) << 6) + cls.local_55.index(x_zse_96)
local_58 = 8 * (local_56 % 4)
local_56 = local_56 + 1
local_57.append((local_59 & 255) ^ cls.Unsigned_right_shift(58, local_58))
local_58 = 8 * (local_56 % 4)
local_56 = local_56 + 1
local_57.append(((local_59 >> 8) & 255) ^ cls.Unsigned_right_shift(58, local_58))
local_58 = 8 * (local_56 % 4)
local_56 = local_56 + 1
local_57.append(((local_59 >> 16) & 255) ^ cls.Unsigned_right_shift(58, local_58))
local_36, local_39 = local_57[-16:][::-1], local_57[:-16][::-1]
local_38 = cls.re_g_x(local_39, local_36)
local_35 = cls.re_g_r(local_36)
local_34 = ^ cls.local_48 ^ 42 for local_11 in range(16)]
local_50 = cls.unpad(bytes(local_34 + local_38))
return {
'seed': local_50,
'device': local_50,
'md5_bytes': local_50
}
```
非常好的,得到了相同的结果,说明解码函数没有问题了。
## 后记
这次的分析比2.0的更加详细,其中是因为3.0来说确实是难了一些
1.结果不是完全固定的
2.存在控制流
3.存在两个时间控制的反调试
4.从编码方法中推导出解码方法 hlrlqy 发表于 2022-9-11 16:00
看到了,您使用ast生成代码的方式很高明,比起直接在case中插桩输出伪代码实现类似trace的效果提供了更大 ...
是的,插桩也是一个很好的方法。两种方法可以互补来更加有效的分析 话说这编程是不是只有英文,没任何意义???{:17_1065:}{:17_1065:} 白云点缀的蓝 发表于 2022-9-11 15:29
话说这编程是不是只有英文,没任何意义???
只有英文是什么意思?没太理解 请问反汇编是如何操作的呢? hlrlqy 发表于 2022-9-11 15:40
请问反汇编是如何操作的呢?
可以先看看前面一篇的分析 get感谢楼主分享 漁滒 发表于 2022-9-11 15:42
可以先看看前面一篇的分析
看到了,您使用ast生成代码的方式很高明,比起直接在case中插桩输出伪代码实现类似trace的效果提供了更大的想象空间 分析的好详细,默默学习一个,谢谢楼主,某乎整的好复杂啊 好复杂,真的看不懂(#-.-)