将NodeJS模块编译为字节码文件,目的,与JS混淆加密相似,也是为了JS代码的安全性,使代码不可阅读。
一、编译模块为字节码
在NodeJS中,与编译一个直接执行的JS文件为字节码不同,如果JS代码是一个模块,有导出函数,是要被其它文件require,那么,它不能直接的调用VM.script编译成bytecode。
例如代码:
exports.hello = function () {
console.log('Hello');
}
有导出函数是hello,用以下代码编译:
//读取文件
var code = fs.readFileSync(filePath, 'utf-8');
//调用v8虚拟机,编译代码
var script = new vm.Script(require('module').wrap(code));
//得到字节码,即bytecode
var bytecode = script.createCachedData();
//写文件,后缀为.bytecode
fs.writeFileSync(filePath.replace(/\.js$/i, '.bytecode'), bytecode);
特殊之处是module模块的warp方法,它会对代码进行包裹,前面的代码,经warp之后,会成为:
(function (exports, require, module, __filename, __dirname) { exports.hello = function () {
console.log('Hello');
}
这是必须遵守的约定,然后才能进行编译。
二、加载并调用字节码模块
编译出字节码模块后,自然是require并调用它,方法如下:
const _module = require('module');
const path = require('path');
_module._extensions['.bytecode'] = function (module, filename) {
//读取bytecode式的模块
var bytecode = fs.readFileSync(filename);
//设置正确的文件头信息
setHeader(bytecode, 'flag_hash', getFlagBuf());
var sourceHash = buf2num(getHeader(bytecode, 'source_hash'));
//申请空间并放入bytecode
const script = new vm.Script('0'.repeat(sourceHash), {
cachedData: bytecode,
});
//bind为输出函数
const wrapperFn = script.runInThisContext();
// 这里的参数列表和之前的 wrapper 函数是一一对应的
wrapperFn.bind(module.exports)(module.exports, require, module, filename, path.dirname(filename));
}
//require字节码模块
const hello = require('./hello.bytecode');
hello.hello();
代码中调用到的几个函数如下:
let _flag_buf;
function getFlagBuf() {
if (!_flag_buf) {
const script = new vm.Script("");
_flag_buf = getHeader(script.createCachedData(), 'flag_hash');
}
return _flag_buf;
}
function getHeader(buffer, type) {
const offset = HeaderOffsetMap[type];
return buffer.slice(offset, offset + 4);
}
function setHeader(buffer, type, vBuffer) {
vBuffer.copy(buffer, HeaderOffsetMap[type]);
}
function buf2num(buf) {
// 注意字节序问题
let ret = 0;
ret |= buf[3] << 24;
ret |= buf[2] << 16;
ret |= buf[1] << 8;
ret |= buf[0];
return ret;
}
重点是bind方法,将函数绑定到某个对象,bind()会创建一个函数,函数体内的this对象的值会被绑定到传入bind()中的第一个参数的值,例如:f.bind(obj),实际上可以理解为obj.f(),
bind(module.exports)则给输出函数进行了绑定。
const hello = require('./hello.bytecode');hello.hello();这段代码的执行效果:
与直接require原始的js文件效果是一致的。
传统的混淆加密,比如JShaman,是把代码变成“乱码”,使代码不能正常阅读理解。而此字节码方式,是把代码变成了非文本模式的二进制格式,于安全的目标而言,两者异曲同工。