V8 字节码分析,简单写写在前辈们的基础上,又遇到些什么问题(绝对不是我水不出很长的文章的问题)
(如有错漏,敬请指正,因为是在弄完后过了很久才写的)
我的blog:blog.dorimu.cn
拿到一个需要逆向分析的 JS start.js。
目标环境:
Node.js:16.14.0
对应 V8:9.4.146.24-node.20(flag hash ed0ab240)
核心代码如下:
经过搜索资料发现:
这就是V8 cachedData / bytenode 方案。
定位到 bytecode 后,我先上了 View8:
e5cK9s2c8@1M7s2y4Q4x3@1q4Q4x3V1k6Q4x3V1k6Y4K9i4c8Z5N6h3u0Q4x3X3g2U0L8$3#2Q4x3V1k6K6N6h3I4W2M7X3q4E0i4K6u0r3g2X3W2W2N6K6R3`.
然后配套 9.4.146.24.exe 去跑反编译。
发现问题:
输出一点代码后自动崩溃退出。
View8 因 d8 崩溃只导出 23 个外围函数,关键的函数基本丢失。
当时第一反应是项目太久没维护,又去github找别的项目。
又看了 jsc2js:
92fK9s2c8@1M7s2y4Q4x3@1q4Q4x3V1k6Q4x3V1k6Y4K9i4c8Z5N6h3u0Q4x3X3g2U0L8$3#2Q4x3V1k6^5M7i4V1J5x3o6l9$3i4K6u0r3K9Y4y4U0x3X3A6K6
这个仓库新一些,也有 patch + CI 体系。
把 patch 套到 v8 9.4.146.24,结果仍然和第一轮差不多。
这时候基本就炸毛了(先躺一会):
字节码本来就不太好 hook,现成工具又不稳定。
于是查阅相关资料:
dceK9s2c8@1M7s2y4Q4x3@1q4Q4x3V1k6Q4x3V1k6%4N6%4N6Q4x3X3g2S2P5h3&6S2K9$3g2&6j5g2)9J5k6h3y4G2L8g2)9J5c8X3q4J5N6r3W2U0L8r3g2K6i4K6u0r3j5%4c8X3i4K6u0r3j5g2)9J5k6s2q4#2K9h3y4C8i4K6u0V1k6%4g2A6k6r3g2Q4x3X3c8@1L8#2)9J5k6r3c8A6M7$3q4K6M7$3g2E0j5X3I4W2i4K6u0V1N6U0S2Q4x3X3c8T1P5i4c8W2j5$3!0V1k6g2)9J5c8R3`.`.
afcK9s2c8@1M7s2y4Q4x3@1q4Q4x3V1k6Q4x3V1k6J5j5$3g2Q4x3X3g2E0L8$3g2Q4x3V1j5J5x3o6t1#2i4K6u0r3x3o6q4Q4x3V1j5H3y4#2)9J5c8Y4j5^5i4K6u0V1j5Y4W2@1k6h3y4G2k6r3g2Q4x3X3c8V1k6h3y4G2L8i4m8A6L8r3g2J5i4K6u0r3
V8 bytecode 就是 V8 自己序列化的一段内部数据。
想稳定拿结果,必须回到 V8 源码层改输出逻辑。
不同 V8 版本在字节码层差异很大,尤其是 opcode、参数语义、寄存器布局。
先patch,再单独构建 d8:
构建参数:
真男人就要硬刚v8,部分diff我就不贴出来了,把问题和思路贴一下,欸嘿~
CodeSerializer::Deserialize 默认会严检 magic/version/flags/hash/checksum/source hash。
如果任何一项没通过,它会直接 reject 掉这份缓存,返回空对象。
src/snapshot/code-serializer.cc:
src/snapshot/deserializer.cc:
这里就是之前view8打印不出来的主要问题:
BytecodeArray::Disassemble
打常量池
命中 SharedFunctionInfo
SharedFunctionInfoPrint
再次 Disassemble
深度叠加,最终栈爆
src/diagnostics/objects-printer.cc:
src/objects/objects.cc:
src/d8/d8.cc:
src/objects/string.cc:
来来回回折腾了三天 hhhhhhh
写完跑路
const vm = require('vm');
const v8 = require('v8');
const zlib = require('zlib');
const fs = require('fs');
const path = require('path');
const Module = require('module');
v8.setFlagsFromString('--no-lazy');
v8.setFlagsFromString('--no-flush-bytecode');
global.generateScript=function(cachedData, filename) {
cachedData = zlib.brotliDecompressSync(cachedData);
fixBytecode(cachedData);
const length = readSourceHash(cachedData);
let dummyCode = '';
if (length > 1) {
dummyCode = '"' + '\u200b'.repeat(length - 2) + '"';
}
const script = new vm.Script(dummyCode, {
cachedData,
filename
});
if (script.cachedDataRejected) {
throw new Error('');
}
return script;
}
global.compileCode = function(javascriptCode, compress) {
const script = new vm.Script(javascriptCode, {
produceCachedData: true
});
let bytecodeBuffer = (script.createCachedData && script.createCachedData.call) ?
script.createCachedData() :
script.cachedData;
if (compress) bytecodeBuffer = zlib.brotliCompressSync(bytecodeBuffer);
return bytecodeBuffer;
};
global.fixBytecode = function(bytecodeBuffer) {
const dummyBytecode = compileCode('');
dummyBytecode.subarray(12, 16).copy(bytecodeBuffer, 12);
};
global.readSourceHash = function(bytecodeBuffer) {
return bytecodeBuffer.subarray(8, 12).reduce((sum, number, power) => sum += number * Math.pow(256, power), 0);
};
try {
Module._extensions['.jsc'] = function(fileModule, filename) {
const data = fs.readFileSync(filename, 'utf8')
const bytecodeBuffer = Buffer.from(data, 'base64');
const script = generateScript(bytecodeBuffer, filename);
function require(id) {
return fileModule.require(id);
}
require.resolve = function(request, options) {
return Module._resolveFilename(request, fileModule, false, options);
};
if (process.main) {
require.main = process.main;
}
require.extensions = Module._extensions;
require.cache = Module._cache;
const compiledWrapper = script.runInThisContext({
filename: filename,
lineOffset: 0,
columnOffset: 0,
displayErrors: true
});
const dirname = path.dirname(filename);
const args = [
fileModule.exports, require, fileModule, filename, dirname, process, global
];
return compiledWrapper.apply(fileModule.exports, args);
};
} catch (ex) {
console.error('xrequire:' + ex.message);
}
require("${codeScript}")
const vm = require('vm');
const v8 = require('v8');
const zlib = require('zlib');
const fs = require('fs');
const path = require('path');
const Module = require('module');
v8.setFlagsFromString('--no-lazy');
v8.setFlagsFromString('--no-flush-bytecode');
global.generateScript=function(cachedData, filename) {
cachedData = zlib.brotliDecompressSync(cachedData);
fixBytecode(cachedData);
const length = readSourceHash(cachedData);
let dummyCode = '';
if (length > 1) {
dummyCode = '"' + '\u200b'.repeat(length - 2) + '"';
}
const script = new vm.Script(dummyCode, {
cachedData,
filename
});
if (script.cachedDataRejected) {
throw new Error('');
}
return script;
}
global.compileCode = function(javascriptCode, compress) {
const script = new vm.Script(javascriptCode, {
produceCachedData: true
});
let bytecodeBuffer = (script.createCachedData && script.createCachedData.call) ?
script.createCachedData() :
script.cachedData;
if (compress) bytecodeBuffer = zlib.brotliCompressSync(bytecodeBuffer);
return bytecodeBuffer;
};
global.fixBytecode = function(bytecodeBuffer) {
const dummyBytecode = compileCode('');
dummyBytecode.subarray(12, 16).copy(bytecodeBuffer, 12);
};
global.readSourceHash = function(bytecodeBuffer) {
return bytecodeBuffer.subarray(8, 12).reduce((sum, number, power) => sum += number * Math.pow(256, power), 0);
};
try {
Module._extensions['.jsc'] = function(fileModule, filename) {
const data = fs.readFileSync(filename, 'utf8')
const bytecodeBuffer = Buffer.from(data, 'base64');
const script = generateScript(bytecodeBuffer, filename);
function require(id) {
return fileModule.require(id);
}
require.resolve = function(request, options) {
return Module._resolveFilename(request, fileModule, false, options);
};
if (process.main) {
require.main = process.main;
}
require.extensions = Module._extensions;
require.cache = Module._cache;
const compiledWrapper = script.runInThisContext({
filename: filename,
lineOffset: 0,
columnOffset: 0,
displayErrors: true
});
const dirname = path.dirname(filename);
const args = [
fileModule.exports, require, fileModule, filename, dirname, process, global
];
return compiledWrapper.apply(fileModule.exports, args);
};
} catch (ex) {
console.error('xrequire:' + ex.message);
}
require("${codeScript}")
@@ SerializedCodeData::SanityCheck
- SanityCheckResult result = SanityCheckWithoutSource();
- if (result != CHECK_SUCCESS) return result;
- ...
- return CHECK_SUCCESS;
+ return SerializedCodeData::SanityCheckResult::CHECK_SUCCESS;
@@ SerializedCodeData::SanityCheckWithoutSource
- if (this->size_ < kHeaderSize) return INVALID_HEADER;
- uint32_t magic_number = GetMagicNumber();
- if (magic_number != kMagicNumber) return MAGIC_NUMBER_MISMATCH;
- ...
- if (Checksum(ChecksummedContent()) != c) return CHECKSUM_MISMATCH;
- return CHECK_SUCCESS;
+ return SerializedCodeData::SanityCheckResult::CHECK_SUCCESS;
@@ SerializedCodeData::SanityCheck
[培训]Windows内核深度攻防:从Hook技术到Rootkit实战!