距离上一次的某乎jsvmp也过了好一段时间,现在也从2.0的版本升级到了3.0的版本
自然的,算法也就发生了一些改变。最明显最直接可见的变化就是长度边长了,并且相同的入参,输出结果并不相同。那么这篇文章就在原来2.0的基础上【JS逆向系列】某乎x96参数与jsvmp初体验,来分析一下3.0版本变难了多少,算法又要怎么还原出来。
至于参数如何查找这篇文章就跳过了,相关内容可以查看前一篇,这篇从【__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随机数的返回
Math.random = function(){
return 0.50
};
输入这段代码后再执行加密函数,此时就发现结果都是一样的了
那么此时就得到了一组样本,当随机数恒定返回0.5时。入参【a63da42088bd8d635961ede065daeb51】的正确结果为【t=V/NpKQqHpejG8nmTuCzIrXW+JszxwLVVyuy+8S0ak=pe1N4BRA6Qxz+LDn+Xyj】,那么接下在就真正可以开始补环境了。
首先安装依赖库
npm install jsdom
npm install canvas
然后在头部加上jsdom的代码
const{
JSDOM}=require("jsdom");
const dom=new JSDOM("Hello world
");
window=dom.window;
Math.random = function(){
return 0.50
};
结尾加上测试代码
console.log(D('a63da42088bd8d635961ede065daeb51'));
console.log('t=V/NpKQqHpejG8nmTuCzIrXW+JszxwLVVyuy+8S0ak=pe1N4BRA6Qxz+LDn+Xyj');
开始测试运行
提示缺少【document】,那么就加上这个定义
document=window.document;
继续运行,后面还有类似的报错,继续补全。
最后头部为
const{
JSDOM}=require("jsdom");
const dom=new JSDOM("Hello world
");
window=dom.window;
document=window.document;
navigator=window.navigator;
location=window.location;
history=window.history;
screen=window.screen;
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[property]);
return target[property]
}
});
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[property]);
return target[property]
}
});
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[property]);
return target[property]
}
});
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[property]);
return target[property]
}
});
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[property]);
return target[property]
}
});
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[property]);
return target[property]
}
});
同时,整个大逻辑被一个try代码块包裹着
那么如果报错的话,我们也看不到,不方便补环境,所以去掉try代码块,只保留try里面的内容。
可以看到读取了不少属性,最后运行到【获取属性get document Symbol(Symbol.toStringTag) string】这一步就退出了,那么看看这一步的结果是不是和网页不一样
确实是不一样的结果,所以这里就需要hook掉toString方法
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 '[object HTMLDocument]';
}
return _temp;
};
再次运行后,日志内容比之前更加长了,说明补的内容有效了,同时得到的加密结果也不一样了
这里最后是location对象出现问题,那么在jsdom上面,就需要补上url链接,那么就会自动补全location对象,开头部分的代码就修改为
const{
JSDOM}=require("jsdom");
const dom=new JSDOM("Hello world
",{
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和网页返回的不一样,继续补上
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 '[object HTMLDocument]';
}else if(this.constructor.name === 'CanvasRenderingContext2D'){
return '[object CanvasRenderingContext2D]'
}
return _temp;
};
又继续往下跑了,这次是检测了window下的_resourceLoader,浏览器上是undefined,但是node上返回对象。还有后面的_sessionHistory,一起补上。
const{
JSDOM}=require("jsdom");
const dom=new JSDOM("Hello world
",{
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未定义,和之前一样处理
const{
JSDOM}=require("jsdom");
const dom=new JSDOM("Hello world
",{
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一下看看
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;
};
果然是,那么继续补上
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() { [native code] }'
}
return _temp;
};
漂亮,终于得到一样的结果,那么这里补环境就完成了,总结一下我们补了什么
const{
JSDOM}=require("jsdom");
const dom=new JSDOM("Hello world
",{
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[property]);
return target[property]
}
});
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[property]);
return target[property]
}
});
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[property]);
return target[property]
}
});
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[property]);
return target[property]
}
});
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[property]);
return target[property]
}
});
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[property]);
return target[property]
}
});
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 '[object HTMLDocument]';
}else if(this.constructor.name === 'CanvasRenderingContext2D'){
return '[object CanvasRenderingContext2D]'
}
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() { [native code] }'
}
return _temp;
};
当需要运行的时候,可以把代码部分的代码注释掉,因为这部分只是方便我们查看以及补环境,不影响最终的结果
在修改字节码之前,要么需要详细分析字节码的逻辑,又或者反汇编字节码到类似js代码的方式。再来看能不能通过修改字节码的方案来绕过环境检测。
例如之前2.0部分的代码,是先进行环境检测,检测完成后才进行真正的加密,所以才可以修改字节码,使得它跳过了环境检测的部分,直接开始核心的加密函数。如果3.0也是沿用之前的逻辑,先进行了检测再加密,那么这种方案就是可行的。
但是3.0没有办法直接进行反汇编,因为相对于2.0的代码来说,增加了控制流的代码,那么最好是先尝试还原了控制流,再做后续处理。
(首先是 按照前面说的去掉try代码块)
首先肯定是处理反调试,3.0也是有时间检测,但是时间检测被放到了jsvmp内部了,不好直接干掉,那么就把初始化里面关于时间的都干掉
// 删除时间参数
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[0].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[i];
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了。
let cases_list = [27, 34, 41, 48, 101, 117, 124, 147, 258, 283, 380, 400, 449, 459, 468, 469, 473, 479, 481, 485, 491, 496, 506];
traverse(ast, {
SwitchCase(path){
if(path.node.test){
if(cases_list.includes(path.node.test.value)){
path.remove()
}
}
}
});
这时删除了多个case后,依然可以得到正确结果。
继续往后调试,会发现一些控制流的分支是虚假分支,也就是在运行的时候,是恒真或者恒假的分支,这种最好是可以将它还原掉,方便后面真实分支的case合并。
分析发现,例如在case 331中存在赋值的【this.M[21] = 8;】,这里就可以把这个值记录下来,其他任何没有出现赋值的,都是null了
let cases_dict = {
};
// 数组虚假分析