目录:
☆ 背景介绍
☆ 确认用Electron开发
☆ Electron桌面应用基本框架
1) package.json
2) hello.js
3) preload.js
4) index.html
5) renderer.js
6) 小结
☆ 预编译版Electron桌面应用基本框架
☆ 逆向分析Electron桌面应用
1) 处理app.asar
2) 定位主入口
3) 调试Electron桌面应用
3.1) 本地开发者工具
3.2) devTools:false
3.3) 远程开发者工具
3.4) 从头调试renderer.js
3.5) 从头调试preload.js
4) IpcMan (Electron IPC Hook)
5) HTTPS抓包
☆ jsc
1) v8字节码
2) bytenode模块
3) 检查jsc版本信息
3.1) ELECTRON_RUN_AS_NODE环境变量
3.2) v8-version-analyzer
3.3) 压缩版jsc
4) 反汇编jsc
4.1) v8dasm (受限)
4.2) 增强d8 (受限)
5) 反编译jsc
5.1) 反编译jsc的Ghidra插件 (未测)
5.2) View8 (受限)
5.2.1) 反编译jsc效果展示
☆ 编译v8源码
1) Visual Studio 2022 社区版组件选择
2) Git
3) Chrome Tools
4) 下载指定版本v8源码
5) 编译v8源码
————————————————————————–
☆ 背景介绍
Node.js在桌面端的使用越来越多。不少正经或恶意软件使用js编程,但不在WEB服务
端或浏览器中用,而是借助Node.js引擎的Native能力在Windows桌面端用。除了编程
语言是js外,得到的程序功能与传统PE相当。
更进一步,Electron桌面应用相当于”Node.js+Chrome”,让许多原来的WEB前端程序
员得以开发伪Native程序。现在软硬件条件好,最终用户分辨不出程序差异,也不在
乎差异,该有的功能有了就成。
本文记录Windows平台Electron逆向工程中的某些技术点。
☆ 确认用Electron开发
假设安装程序是some_install.exe,安装目录是:
C:\Program Files\some\
尝试在安装目录找这些文件或目录:
LICENSE.electron.txt
LICENSES.chromium.html
resources\app\
resources\app\package.json
resources\app.asar
resources\app.asar.unacked\
找到其中某些项,即可推断目标用Electron开发,尤其当app.asar存在时。
可不安装some_install.exe,直接7-Zip查看some_install.exe:
some_install.exe\app.7z\resources\…
传统PE逆向那套对Electron桌面应用行不通,这次更像是WEB前端逆向。
☆ Electron桌面应用基本框架
逆向之前需了解一些正向开发知识点。可从此处入门:
————————————————————————–
Building your First App
https://www.electronjs.org/docs/latest/tutorial/tutorial-first-app
————————————————————————–
建议遍历Electron官方文档,没有正向知识,盲目逆向不可取。
假设有个hello应用,常见有5个文件:
————————————————————————–
Electron\hello\
package.json // 用于确定入口js,比如hello.js
hello.js // Node.js
// main process
// 加载preload.js
// 加载index.html
preload.js // Chrome
// renderer process
// renderer isolated world
index.html // Chrome
// renderer process
// renderer main world
// 加载renderer.js
renderer.js // Chrome
// renderer process
// renderer main world
————————————————————————–
1) package.json
Electron引擎先找package.json,此文件名是预置的、固定的,其内容形如:
————————————————————————–
{
…
“main”: “hello.js”,
…
“devDependencies”: {
“electron”: “^33.2.1”
}
}
————————————————————————–
主要是main字段,指明hello应用的主入口文件,此处为hello.js。
2) hello.js
此文件名任意,只需出现在package.json的main字段中,其内容形如:
————————————————————————–
let { app, BrowserWindow, ipcMain }
= require( ‘electron/main’ );
let path = require( ‘node:path’ );
let createWindow = () => {
let win = new BrowserWindow({
width: 800,
height: 600,
show: true,
webPreferences: {
sandbox: true,
contextIsolation: true,
nodeIntegration: false,
preload: path.join( __dirname, ‘preload.js’ )
}
});
win.loadFile( ‘index.html’ );
};
app.whenReady().then( () => {
ipcMain.handle( … );
ipcMain.on( ‘…’, ( event, value ) => {
…
});
createWindow();
app.on( ‘activate’, () => {
if ( BrowserWindow.getAllWindows().length === 0 ) {
createWindow();
}
});
});
app.on( ‘window-all-closed’, () => {
if ( process.platform !== ‘darwin’ ) {
app.quit();
}
});
————————————————————————–
hello.js对应”main process”,在全功能Node.js环境中执行,可执行Native操作,
比如调用PING.EXE探活。
hello.js可创建多个BrowserWindow,每个BrowserWindow对应一个GUI。每个
BrowserWindow会创建独立的”renderer process”,通过loadFile或loadURL在其中加
载index.html。”renderer process”进程空间相当于Chrome环境,作为对比,”main
process”进程空间相当于Node.js环境。
3) preload.js
hello.js创建BrowserWindow时,创建参数中可指定preload.js,此文件名任意。
preload.js不在”main process”进程空间中,它被加载到”renderer process”进程空
间。preload.js是”renderer process”进程空间最早加载的内容,比index.html还要
早,顾名思义。
preload.js虽然在”renderer process”进程空间中,但相比index.html,preload.js
被赋予Node.js环境特权,可调用所有Node.js API,同时可调用WEB API,比如操作
DOM。作为对比,hello.js没法操作DOM,index.html没法调用Node.js API。
preload.js一般负责在”main process”与各个”renderer process”之间建立IPC通道,
为”renderer process”提供封装过的Native特权能力,等等。
preload.js对应”renderer isolated world”。
4) index.html
此文件名任意,其内容形如:
————————————————————————–
<!DOCTYPE html>
<html lang=”en”>
<head>
<meta charset=”UTF-8″ />
<!– https://developer.mozilla.org/en-US/docs/Web/HTTP/CSP –>
<meta
http-equiv=”Content-Security-Policy”
content=”default-src ‘self’; script-src ‘self'”
/>
<meta
http-equiv=”X-Content-Security-Policy”
content=”default-src ‘self’; script-src ‘self'”
/>
<title>Hello World</title>
</head>
<body>
<h1>Hello World</h1>
<!– You can also require other files to run in this process –>
<script src=”./renderer.js”></script>
</body>
</html>
————————————————————————–
index.html加载到”renderer process”进程空间,随它加载的各种js也在”renderer
process”进程空间。index.html对应”renderer main world”。处理index.html就当
成在Chrome中处理index.html。
5) renderer.js
index.html中加载renderer.js,此文件名任意。理论上不必单独出现renderer.js,
js代码可置于index.html中。
renderer.js在”renderer main world”中,处理renderer.js就当成在Chrome中处理
renderer.js。
6) 小结
Electron桌面应用相当于”Node.js+Chrome”。”main process”即Node.js,”renderer
process”即Chrome。hello.js对应”main process”,在Node.js进程空间执行。
preload.js、index.html、renderer.js对应”renderer process”,在Chrome进程空
间执行。preload.js对应”renderer isolated world”,index.html、renderer.js对
应”renderer main world”。preload.js由hello.js创建BrowserWindow时注入到
Chrome进程空间。
这些概念有助于调试Electron桌面应用。未严谨使用术语,为便于理解,作粗浅类比。
☆ 预编译版Electron桌面应用基本框架
参看
————————————————————————–
Application Packaging
https://www.electronjs.org/docs/latest/tutorial/application-distribution
(use an asar archive to replace the app folder)
Electron’s prebuilt binaries
https://github.com/electron/electron/releases
https://github.com/electron/electron/releases/download/v33.3.0/electron-v33.3.0-win32-x64.zip
————————————————————————–
最省事的Electron桌面应用打包方案是利用官方预编译版Electron。下载Electron预
编译二进制包,将hello目录复制成”resources\app\”目录,双击上层electron.exe,
执行hello。
————————————————————————–
Electron\electron-v33.3.0-win32-x64\
electron.exe
resources\
app\
package.json
hello.js
preload.js
index.html
renderer.js
————————————————————————–
可用工具将app目录打包成app.asar文件,形成如下文件布局:
————————————————————————–
Electron\electron-v33.3.0-win32-x64\
electron.exe
resources\
app.asar
————————————————————————–
这种更常见。
app目录兼容性高,package.json可以是带BOM的UTF-8格式。app.asar兼容性差,
package.json若有BOM(EF BB BF),执行时可能报错。app目录与app.asar文件同时存
在时,app.asar优先级高。
electron.exe改名成some.exe也行,许多Windows平台Electron桌面应用这样干,它
们真正开发的部分就是app目录或app.asar中的那些内容。
从基本框架看出,这些Electron桌面应用真正干活时相当于Chrome,主体功能在WEB
服务端。但它们另有不足为外人道的Native能力,”main process”相当于Node.js,
能干的坏事特别多。
☆ 逆向分析Electron桌面应用
1) 处理app.asar
假设some应用中含有app.asar,这个名字应该是不能改的,Electron引擎固定找
app.asar文件或app目录。
asar是一种打包格式,相当于zip或tar之类的。有现成工具解包、打包asar文件;假
设已有Node.js环境,可在其中安装此工具:
npm install –engine-strict @electron/asar
npm list
npm uninstall –engine-strict @electron/asar
npm安装过程可能被寡妇王修理,可指定代理参数:
–proxy http://ip:port –https-proxy http://ip:port
假设PATH环境变量已就位,确认工具可用:
asar -h
asar -V
此工具需要node出现在PATH中。
以some应用为例:
cd /d C:\Program Files\some\resources\
asar extract app.asar app // 解包
asar pack app app.asar // 打包
若app.asar所在目录存在名为「app.asar.unpacked」的子目录,extract时必须确保
二者位于同一目录。上例不存在此问题,考虑别种情形,将app.asar复制到其他目录
再处理,为了extract,必须同时复制「app.asar.unpacked」子目录到其他目录,否
则extract可能报错。
2) 定位主入口
从app.asar解包生成app目录,查看”app\package.json”,形如:
————————————————————————–
{
…
“main”: “./index.js”,
“dependencies”: {
…
“electron-updater”: …,
…
}
}
————————————————————————–
其中main字段对应主程序,此处即
app\index.js
未对抗逆向工程时,index.js就像hello.js那样。现在Electron桌面应用基本都对抗
逆向工程,比如将js半编译成jsc,js混淆,等等。
3) 调试Electron桌面应用
3.1) 本地开发者工具
Electron桌面应用的”renderer process”可用F12(本地开发者工具)调试。发布者不
会傻到自动提供F12界面,若index.js未用jsc技术,甭管混淆与否,总能找到”new
BrowserWindow”所在,获得返回值win后,增加如下代码:
win.webContents.openDevTools(…);
最理想情况下,将看到F12界面。index.js很可能有对抗措施,检测到F12界面,自动
关闭或其他干扰操作,比如:
win.webContents.on( ‘devtools-opened’, … );
对抗与反对抗需要”case by case”,没有放之四海而皆准的方案,此处不谈。
3.2) devTools:false
创建BrowserWindow时,webPreferences中devTools设为false,将全局禁用本地开发
者工具,后面如何挣扎,都得不到本地F12界面。
若index.js未用jsc技术,可修改devTools为true,或删除此参数;再在获取win后调
用openDevTools();屏蔽可能存在的反F12代码。
不只是devTools参数会影响本地F12界面是否出现,其他参数也可能影响。正确逆向
方式,先调查创建BrowserWindow所用参数,包括但不限于devTools参数,再决定后
续动作。
3.3) 远程开发者工具
chcp 65001 (为了看UTF-8汉字)
“C:\Program Files\some\some.exe” –inspect-brk-node=9229 –remote-debugging-port=9222
–inspect-brk-node在Node.js环境(简称)中开一个远程调试服务,等待调试客户端
接入。调试”main process”需要这个。
–inspect-brk-node比–inspect、–inspect-brk断得更早。
–remote-debugging-port在Chrome环境(简称)中开一个远程调试服务,等待调试客
户端接入。调试”renderer process”需要这个。
–inspect系列用于调试”main process”,–remote-debugging-port用于调试
“renderer process”,二者勿用同一端口。示例中各个端口号是官方文档中的默认值,
有助于”chrome://inspect”轮询发现它们。理论上可以修改,但修改的话,就需要其
他配套修改,我觉得没必要修改默认值,略过。
打开Chrome或Edge,访问:
chrome://inspect (首选)
edge://inspect
http://localhost:9222/ (有时不可用,换上一种)
http://127.0.0.1:9222/
假设是默认情况,”chrome://inspect”会轮询127.0.0.1的9229、9222/TCP口。
首先轮询9229成功,页面中”Remote Target/localhost:9229″下方出现相应项,即
“main process”对应项。点击其上的”inspect”,打开远程开发者工具。
若index.js首部有debugger(可手工增加),远程调试时将命中并断下,可完整调试
“main process”。作为对比,本地F12界面只能调试”renderer process”,无法调试
“main process”。
“renderer process”创建后,”chrome://inspect”轮询9222成功,页面中”Remote
Target/localhost:9222″下方出现一些项,包括各个”renderer process”对应项。
这些项分两大类,分别形如:
DevTools devtools://devtools/bundled/devtools_app.html?remoteBase=https://chrome-devtools-frontend.appspot.co…
some file:///C:/Program%20Files/some/resources/app.asar/index.html
这两个总是成对出现,有多少个”renderer process”,这两个就有多少对。不理
DevTools所在行,关注some所在行,点击其上的”inspect”,打开新的远程开发者工
具。远程F12不但能调试”main process”,也能调试”renderer process”,只不过分
属不同界面,互不干扰。
只调试”renderer process”的话,还可在Chrome中访问:
http://localhost:9222/
此操作相比”chrome://inspect”没有优势,有时不可用,不推荐。
openDevTools()打开的本地开发者工具,与”chrome://inspect”打开的远程开发者工
具可同时存在,调试”renderer process”时,二者会同步刷新,不会引发混乱。
本地调试有可能漏过一些debugger断点,远程调试命中的断点更多,建议始终远程调
试。
前述介绍以Chrome的英文GUI为例,中文GUI自行脑中翻译后寻找对应操作。
3.4) 从头调试renderer.js
有时即使renderer.js首部有debugger,调试时未断下来。如需从头调试renderer.js,
可修改index.html,用alert卡住,延迟加载renderer.js。
另一种方案是,修改renderer.js,用setTimeout()延迟执行renderer.js的主代码逻
辑,在主代码逻辑首部放置debugger。
3.5) 从头调试preload.js
许多正经开发人员问过这个问题,回答到位的很少见,此处给个实践过的方案。
首先,尽早打开开发者工具,无所谓本地、远程F12。其次,提前修改preload.js,
用setTimeout()延迟执行preload.js的主代码逻辑,在主代码逻辑首部放置debugger。
现实中,preload.js很可能使用jsc技术,上法不可行。
4) IpcMan (Electron IPC Hook)
参看
————————————————————————–
Inter-Process Communication
https://www.electronjs.org/docs/latest/tutorial/ipc
Chromium IPC Sniffer
https://github.com/tomer8007/chromium-ipc-sniffer
IpcMan (Electron IPC Hook)
https://github.com/bakyrd/ipcman
https://github.com/bakyrd/ipcman/releases
https://github.com/bakyrd/ipcman/releases/download/v0.1.3/ipcman-devtools-v0.1.3.zip
————————————————————————–
假设在GUI中点击”下载”,GUI本身在”renderer process”中,它可能通过IPC让”main
process”进行实际的HTTPS下载。some应用就是这样,index.html的F12 Network面板
看不到相应尺寸的数据,下载动作发生在”main process”中。
“main process”、”renderer process”之间的IPC通信很可能包含有助于理解代码逻
辑的明文数据,逆向工程中应设法监控二者之间的IPC通信。
起初想用”Chromium IPC Sniffer”,但执行下列命令时崩溃:
chromeipc.exe –update-interfaces-info
后来找到IpcMan项目。
按如下形式组织相关文件:
————————————————————————–
Electron\electron-v33.3.0-win32-x64\
electron.exe
resources\
app\
package.json
hello.js // 需修改
preload.js
index.html
renderer.js
ipcman.js // 源自IpcMan
build\ // 源自IpcMan
————————————————————————–
修改hello.js,在最前部增加
require( ‘./ipcman.js’ ).ipcManDevtools( {} );
原版ipcman.js中有一句
Object.assign({port:9009},e)
表示侦听”0.0.0.0:9009/TCP”,改成
Object.assign({host:’127.0.0.1′,port:9999},e)
启动应用,用浏览器访问
http://127.0.0.1:9999
这是IpcMan的界面,可看到Electron IPC通信,不算太细节,但有点用。
IpcMan源码被处理过,js代码都在一行,大量单字母变量名,等等,不知出于什么考
虑?故未研究过IpcMan源码及技术原理。
只在app目录实测过IpcMan,未在app.asar中实测过,不清楚后者IpcMan是否可用。
只在hello应用中实测过IpcMan。some应用涉及jsc,未测IpcMan是否依然可行。我自
己用Hook、ES6 JS Proxy之类技术监控some应用的IPC通信,即使涉及jsc,仍成功,
想必IpcMan也行。有兴趣者自测jsc情形IpcMan是否可用。
理论上,监控IPC通信,发现敏感明文时,debugger命中,调用栈回溯,定位相关代
码。即使jsc没法改,IPC另一侧的js却可以Patch。比如有些Electron桌面应用很垃
圾,强制升级,模态对话框,无法取消,只能升级。后来靠监控IPC通信发现了js的
Patch点。
5) HTTPS抓包
开发者工具Network面板可以HTTPS抓包,另一种补充手段是为Electron桌面应用设置
HTTP(S)代理,说不定有mitmproxy二次开发需求呢。
Electron框架支持HTTP(S)代理
“C:\Program Files\some\some.exe” –proxy-server=ip:port
或许还能用HttpAnalyzer抓some.exe,自行尝试。
☆ jsc
1) v8字节码
参看
————————————————————————–
Understanding V8’s Bytecode – Franziska Hinkelmann [2017-08-16]
https://www.fhinkel.rocks/posts/Understanding-V8-s-Bytecode
https://medium.com/dailyjs/understanding-v8s-bytecode-317d46c94775
(女程序员)
Code caching
https://v8.dev/blog/code-caching
————————————————————————–
v8引擎是Google家的js引擎,Chrome、Node.js、Electron都用v8引擎处理js。
v8处理js时有名为cached_data中间形态,cached_data内含v8字节码及其他配套数据。
v8引擎有相关API可执行js,也可执行cached_data,执行后者效率更高,省却了js的
解析过程。
jsc是cached_data序列化后保存到硬盘上的形式。js是明文源码,jsc是二进制数据。
2) bytenode模块
参看
————————————————————————–
A minimalist bytecode compiler for Node.js
https://github.com/bytenode/bytenode
How to Compile Node.js Code Using Bytenode – Osama Abbas [2018-11-08]
https://medium.com/hackernoon/how-to-compile-node-js-code-using-bytenode-11dcba856fa9
————————————————————————–
Node.js有现成的bytenode模块,封装相关技术原理,方便地从js生成jsc,抛却js直
接加载执行jsc。
以hello应用为例,假设使用jsc技术:
————————————————————————–
Electron\hello\
package.json // 指向hello_loader.js,不再指向hello.js
hello_loader.js // main process
// 加载bytenode模块
// 加载hello.jsc
hello.jsc // main process
// 经bytenode模块从hello.js生成hello.jsc
// 项目中不再保有hello.js,已删除
preload.js
index.html
renderer.js
————————————————————————–
package.json形如:
————————————————————————–
{
…
“main”: “hello_loader.js”,
…
“devDependencies”: {
“electron”: “^33.2.1”
}
}
————————————————————————–
hello_loader.js形如:
————————————————————————–
require( ‘bytenode’ );
require( ‘./hello.jsc’ );
————————————————————————–
发布hello应用时,确保bytenode模块出现在模块搜索路径上,参看:
————————————————————————–
Loading from node_modules folders
https://nodejs.org/docs/latest/api/modules.html#loading-from-node_modules-folders
Loading from the global folders
https://nodejs.org/docs/latest/api/modules.html#loading-from-the-global-folders
https://nodejs.org/docs/latest/api/modules.html#modulepaths
How to configure NODE_PATH for the Electron instance – [2020-03-08]
https://github.com/twolfson/karma-electron/issues/44
(NODE_PATH does not propagate into the Electron instance)
————————————————————————–
bytenode模块很常用,但不是非用不可。some应用的index.js形如:
————————————————————————–
require( ‘private_loader.js’ );
require( ‘./index.jsc’ );
————————————————————————–
private_loader.js地位相当于bytenode模块,用vm.Script()等自实现jsc处理。
index.jsc相当于hello.jsc。逆向时碰上jsc,不要假设必有bytenode模块。
3) 检查jsc版本信息
jsc涉及C++数据结构序列化/反序列化,此过程与v8版本强相关。这点很好理解,假
设高版本某结构定义发生变化,jsc对应高版本,低版本v8引擎反序列化高版本jsc时,
仍按低版本结构定义反序列化二进制数据,必错。
jsc文件头偏移4处的4字节保存v8版本的哈希值,v8引擎加载jsc时会检查其中的版本
哈希,不匹配时会报错。
逆向jsc时需了解生成jsc所用v8版本。分两种情况,一种有加载jsc的应用程序,另
一种只有jsc没有应用程序,比如别人只发给你jsc,比如云端提供jsc反编译服务。
3.1) ELECTRON_RUN_AS_NODE环境变量
以some应用为例
————————————————————————–
C:\Program Files\some\
some.exe
resources\
app.asar
————————————————————————–
some.exe其实就是electron.exe改名,app.asar中含有jsc,想知道jsc版本信息。这
种最简单,让some.exe自己报告内置v8引擎的版本即可,不考虑魔改情形。
set ELECTRON_RUN_AS_NODE=1
“C:\Program Files\some\some.exe” -p “process.versions” // 所有组件版本
“C:\Program Files\some\some.exe” -p “process.versions.v8” // 只报告v8版本
必须先设置ELECTRON_RUN_AS_NODE环境变量,让some.exe假冒成node.exe执行。这种
假冒程度极其有限,some.exe不能真当成node.exe用。
3.2) v8-version-analyzer
参看
————————————————————————–
NV-Crack
https://github.com/xcf-t/nv-crack
(A quick way to get the v8/Node.js version of a v8-bytecode file)
v8-version-analyzer
https://github.com/j4k0xb/v8-version-analyzer
https://nodejs.org/dist/index.json (版本信息)
https://releases.electronjs.org/releases.json (版本信息)
(A quick way to get the v8/Node.js/Electron version of a v8-bytecode file, typically produced by bytenode)
————————————————————————–
v8-version-analyzer的思路是,遍历几个包含版本信息的json,提前为每个v8版本
生成哈希值,将版本明文及哈希均保存到版本表中;检查jsc时,从偏移4处取4字节,
在版本表中查找版本哈希,匹配则显示版本明文;其实就是字典破解法。
cd /d X:\path\v8-version-analyzer
python -m http.server -b 127.0.0.1 8888
http://127.0.0.1:8888/
上传
any.jsc
匹配则输出版本明文、哈希,包括v8版本、可能的Node.js、Electron版本,等等。
某版v8引擎改动过jsc版本哈希生成算法,上述工具不直接适用于Node.js 22.0.0及
之后版本,因其只用了旧版哈希生成算法。
old参加运算的顺序是:
patch build minor major
new参加运算的顺序是:
major minor build patch
就是倒了个。可修改v8-version-analyzer,生成版本表时,old、new各来一份即可。
3.3) 压缩版jsc
“bytenode –compress”用了brotli压缩算法,此时生成压缩版jsc,没有任何头部信
息,无法判断是不是有效jsc,只能尝试解压,若成功,再hexdump查看。解压数据偏
移2开始的2字节是”de c0″,表示这是原始jsc。偏移4开始的4字节是版本哈希。
v8引擎只处理原始jsc,不处理压缩版jsc,后者是bytenode模块的增强。bytenode模
块自动处理压缩版jsc,内部细节被封装,模块使用者无需关心jsc压缩与否。
v8-version-analyzer只处理原始jsc,不处理压缩版jsc。如遇后者,需手动解压jsc,
再用v8-version-analyzer。将来反汇编、反编译jsc,都要先检查jsc压缩与否。
some应用自实现的private_loader.js,不支持压缩版jsc。
brotli压缩算法在WEB开发领域逐渐替代gzip,据说压缩率更高。010 Editor没有
brotli的模板,brotli不像gzip有头尾信息,只能尝试brotli解压,不出错就算成功。
4) 反汇编jsc
4.1) v8dasm (受限)
参看
————————————————————————–
Disassembling V8 Bytecode
https://github.com/noelex/v8dasm
(make some changes to the source code to print disassembly after code cache deserialization)
https://github.com/v8/v8/tree/13.0.245.20
https://github.com/v8/v8/blob/13.0.245.20/src/snapshot/code-serializer.cc
https://github.com/v8/v8/blob/13.0.245.20/src/snapshot/deserializer.cc
————————————————————————–
v8引擎已含有jsc反汇编代码,只是未导出。v8dasm项目修改v8引擎源码,在jsc反序
列化代码中插入jsc反汇编代码,自编译v8引擎库;编写v8dasm.cpp,链接v8引擎库,
触发jsc反序列化代码,输出jsc反汇编代码。
v8dasm与v8版本强相关,跨版本反汇编能否成功,拼运气。
v8引擎的jsc反序列化过程有所谓的SanityCheck,一是防止跨版本反序列化,二是检
查序列化数据是否被破坏。正经使用v8引擎时,这些检查相当必要。逆向工程时,可
Patch掉这些检查,扩大相关工具的兼容性。比如code-serializer.cc中
SerializedCodeData::FromCachedData、CodeSerializer::Deserialize等函数,就
涉及SanityCheck。
jsc反序列化、反汇编的表述并不严谨,只为行文简便,其实是cached_data反序列化、
BytecodeArray反汇编。
4.2) 增强d8 (受限)
参看
————————————————————————–
Using d8
https://v8.dev/docs/d8
https://github.com/v8/v8/blob/13.0.245.20/src/d8/d8.cc
某知笔记服务端docker镜像授权分析 – 半块西瓜皮 [2021-06-29]
https://guage.cool/wiz-license.html
(适配v8_6.2.x)
————————————————————————–
有种不同于v8dasm的jsc反汇编策略。v8源码自带名为d8的项目,d8源码中可调用v8
引擎未导出的内部API。下载特定版本v8源码,修改d8.cc,调用内部API反汇编
BytecodeArray,自编译v8源码。内部API随v8版本而不同,需阅读相应版本v8源码以
增强d8。
增强d8来反汇编jsc,理论上比v8dasm好,因为autoninja编译v8源码时,d8方案重新
编译、链接的obj更少,编译时间更短,便于开发测试。
编译得到的d8.exe与v8版本强相关,与v8dasm情形类似。
5) 反编译jsc
5.1) 反编译jsc的Ghidra插件 (未测)
参看
————————————————————————–
ghidra_nodejs
https://github.com/PositiveTechnologies/ghidra_nodejs
(parse disassemble and decompile jsc binaries)
(适配v8_6.2.x)
How we bypassed bytenode and decompiled Node.js bytecode in Ghidra – Sergey Fedonin [2021-05-13]
https://swarm.ptsecurity.com/how-we-bypassed-bytenode-and-decompiled-node-js-bytecode-in-ghidra/
Decompiling Node.js in Ghidra – Vladimir Kononovich [2021-05-20]
https://swarm.ptsecurity.com/decompiling-node-js-in-ghidra/
————————————————————————–
ghidra_nodejs是款反汇编、反编译jsc的Ghidra插件,但只有理论价值,极难复现。
ghidra_nodejs未用v8引擎,自实现反序列化过程。假设jsc对应高版本v8,序列化/
反序列化所用数据结构已变化,ghidra_nodejs按低版本结构定义反序列化二进制数
据,必错。不同v8版本,但较为接近,jsc所涉及数据结构未发生重大变化,
ghidra_nodejs有可能成功。
ghidra_nodejs插件涉及两种适配,一是前面说的v8版本适配,二是重新编译插件源
码适配当前Ghidra引擎版本。
原插件适配v8_6.2.x,理论上可修改ghidra_nodejs源码,自行适配some应用所用v8
版本,难度太大,未实施。
5.2) View8 (受限)
参看
————————————————————————–
Exploring Compiled V8 JavaScript Usage in Malware – [2024-07-08]
https://research.checkpoint.com/2024/exploring-compiled-v8-javascript-usage-in-malware/
View8 – Decompiles serialized V8 objects back into high-level readable code
https://github.com/suleram/View8
(View8 utilizes a patched compiled V8 binary)
————————————————————————–
View8项目用VersionDetector.exe获取jsc版本信息。该exe未开源,我未逆向,合理
推测其原理同v8-version-analyzer项目。
parse_v8cache.py中get_version()返回jsc版本信息,并据此调用预编译的某款exe,
姑且称之为verdasm.exe。给view8.py指定verdasm.exe,将不依赖get_version()返
回值,换句话说,此时get_version()可返回任意字符串,无需调用
VersionDetector.exe。
verdasm.exe未开源,应该是v8dasm变种;前者对v8引擎的Patch不同于后者,但原理
类似。
假设反编译some.jsc,用verdasm.exe生成some.jsc.asm
verdasm.exe some.jsc > some.jsc.asm
这步最重要,大多数时候都失败在这步,反序列化抛异常,原因是v8版本强相关。注
意,Patch掉v8引擎版本相关的检查,大多数时候并不能真正解决jsc反序列化失败的
问题。
运气好的话,可从some.jsc生成some.jsc.asm。sfi_file_parser.py对asm进行文本
解析,生成某种更具可读性的输出,相当于反编译jsc。
translate_table.py中有各种字节码的解析处理。假设遇上这种错:
Operator xxx was not found in table
大概率需要修改translate_table.py。
常规用法有:
python3 X:\path\View8\view8.py some.jsc.asm some.jsc.decomp -d
python3 X:\path\View8\view8.py some.jsc some.jsc.decomp -p X:\path\verdasm.exe
python3 X:\path\View8\view8.py some.jsc some.jsc.decomp_a -p X:\path\verdasm.exe -e v8_opcode decompiled
python3 X:\path\View8\view8.py some.jsc some.jsc.decomp_b -p X:\path\verdasm.exe -e v8_opcode translated decompiled
若已有some.jsc.asm,反编译时不需要verdasm.exe,-d参数指明这种情形。最常见
的用法是上述第二行,view8.py内部调用verdasm.exe生成asm,再反编译。上述第三、
四行是给高级用户用的,输出中含有更丰富的中间信息,便于开发、调试、排错。若
未指定-p参数,就会调用VersionDetector.exe,根据获取的jsc版本信息去固定路径
调用View8项目提供的verdasm.exe完成jsc反汇编。View8项目预提供了三个版本的
verdasm.exe,其他版本只能自己设法搞出适配的verdasm.exe。
5.2.1) 反编译jsc效果展示
示例对应v8_13.0.245.20
some.js
some.jsc.asm
some.jsc.decomp
————————————————————————–
function bar () {
console.log( “main -> foo() -> bar()” );
}
function foo () {
bar();
}
foo();
————————————————————————–
Start SharedFunctionInfo
Parameter count 1
Register count 3
Frame size 24
0000017300040064 @ 0 : 13 00 LdaConstant [0]
0000017300040066 @ 2 : c9 Star1
0000017300040067 @ 3 : 19 fe f7 Mov <closure>, r2
000001730004006A @ 6 : 68 66 01 f8 02 CallRuntime [DeclareGlobals], r1-r2
000001730004006F @ 11 : 21 01 00 LdaGlobal [1], [0]
0000017300040072 @ 14 : c9 Star1
0000017300040073 @ 15 : 64 f8 02 CallUndefinedReceiver0 r1, [2]
0000017300040076 @ 18 : ca Star0
0000017300040077 @ 19 : af Return
0x01730004003c: [SharedFunctionInfo] in idk
Constant pool (size = 2)
0000017300040079: [TrustedFixedArray]
– map: 0x0292000005e5 <Map(TRUSTED_FIXED_ARRAY_TYPE)>
– length: 2
0: 0x0292001931fd <FixedArray[4]>
Start FixedArray
00000292001931FD: [FixedArray] in OldSpace
– map: 0x0292000005bd <Map(FIXED_ARRAY_TYPE)>
– length: 4
0: 0x029200193215 <SharedFunctionInfo bar>
Start SharedFunctionInfo
Parameter count 1
Register count 3
Frame size 24
00000173000400B0 @ 0 : 21 00 00 LdaGlobal [0], [0]
00000173000400B3 @ 3 : c9 Star1
00000173000400B4 @ 4 : 2f f8 01 02 GetNamedProperty r1, [1], [2]
00000173000400B8 @ 8 : ca Star0
00000173000400B9 @ 9 : 13 02 LdaConstant [2]
00000173000400BB @ 11 : c8 Star2
00000173000400BC @ 12 : 61 f9 f8 f7 04 CallProperty1 r0, r1, r2, [4]
00000173000400C1 @ 17 : 0e LdaUndefined
00000173000400C2 @ 18 : af Return
0x017300040088: [SharedFunctionInfo] in idk
Constant pool (size = 3)
00000173000400C5: [TrustedFixedArray]
– map: 0x0292000005e5 <Map(TRUSTED_FIXED_ARRAY_TYPE)>
– length: 3
0: 0x0292000045f5 <String[7]: #console>
1: 0x029200024771 <String[3]: #log>
2: 0x02920019324d <String[22]: #main -> foo() -> bar()>
Handler Table (size = 0)
Source Position Table (size = 0)
End SharedFunctionInfo
1: 0
2: 0x029200193311 <SharedFunctionInfo foo>
Start SharedFunctionInfo
Parameter count 1
Register count 1
Frame size 8
0000017300040100 @ 0 : 21 00 00 LdaGlobal [0], [0]
0000017300040103 @ 3 : ca Star0
0000017300040104 @ 4 : 64 f9 02 CallUndefinedReceiver0 r0, [2]
0000017300040107 @ 7 : 0e LdaUndefined
0000017300040108 @ 8 : af Return
0x0173000400d8: [SharedFunctionInfo] in idk
Constant pool (size = 1)
000001730004010D: [TrustedFixedArray]
– map: 0x0292000005e5 <Map(TRUSTED_FIXED_ARRAY_TYPE)>
– length: 1
0: 0x029200193295 <String[3]: #bar>
Handler Table (size = 0)
Source Position Table (size = 0)
End SharedFunctionInfo
3: 1
End FixedArray
1: 0x02920019336d <String[3]: #foo>
Handler Table (size = 0)
Source Position Table (size = 0)
End SharedFunctionInfo
————————————————————————–
function func_start_0x01730004003c()
{
ACCU = DeclareGlobals([func_bar_0x017300040088, 0, func_foo_0x0173000400d8, 1], <closure>)
r0 = “foo”()
return “foo”()
}
function func_foo_0x0173000400d8()
{
ACCU = “bar”()
return undefined
}
function func_bar_0x017300040088()
{
ACCU = “console”[“log”](“main -> foo() -> bar()”)
return undefined
}
————————————————————————–
或可用Babel处理some.jsc.decomp,得到更易读的js伪码,未实施。
View8项目理论上可反编译jsc,实际受制于能否反汇编jsc,后者v8版本强相关。即
使v8版本相同,jsc反序列化与v8引擎编译选项强相关。实践中,反汇编、反编译第
三方生成的jsc,能否成功,拼运气,实际意义受限。
我在逆向第三方Electron桌面应用时,未从View8项目受益,因为从未成功过,很挫
败。
☆ 编译v8源码
参看
————————————————————————–
Checking out the V8 source code
https://v8.dev/docs/source-code
Building V8 from source
https://v8.dev/docs/build
Building V8: the raw, manual workflow
https://v8.dev/docs/build-gn#manual
Building Chrome V8 on Windows – [2023]
https://gist.github.com/jhalon/5cbaab99dccadbf8e783921358020159
————————————————————————–
众多jsc逆向实验需要编译v8源码,这不是一件愉快的事,比较复杂。除了原生v8源
码,有时需根据Node.js、Electron版本移植一些Patch。jsc反序列化除了与v8版本
强相关,与v8编译选项也强相关,同一份v8源码不同编译选项得到的v8引擎,处理对
方生成的jsc,也可能失败,不是”启用指针压缩”这种过于明显的编译选项差异。v8
编译选项会影响snapshot_blob.bin、常量字符串池等,jsc序列化/反序列化与它们
相关。
编译v8源码很耗CPU,性能不好的PC,编译一次耗时两三个小时。
1) Visual Studio 2022 社区版组件选择
参看
————————————————————————–
https://chromium.googlesource.com/chromium/src/+/master/docs/windows_build_instructions.md#Setting-up-Windows
Visual Studio 2022 社区版
https://visualstudio.microsoft.com/downloads/
————————————————————————–
从微软网站下载安装VS时速度只有2KB,小钻风指出可能是DNS问题,将DNS切换成阿
里的223.5.5.5,下载速度达到4MB。
我选了这些组件:
————————————————————————–
Desktop Development with C++
Python Development
C++ ATL for Latest v143 Build Tools (x86 & x64)
C++ MFC for Latest v143 Build Tools (x86 & x64)
C++ Clang Compiler for Windows
C++ Clang tools for Windows (x64/x86)
C++ CMake tools for Windows
Windows 11 SDK
Debugging Tools For Windows (appwiz.cpl里改SDK)
————————————————————————–
2) Git
参看
————————————————————————–
https://chromium.googlesource.com/chromium/src/+/main/docs/windows_build_instructions.md#Install-git
https://git-scm.com/downloads/win
https://github.com/git-for-windows/git/releases/download/v2.47.1.windows.1/PortableGit-2.47.1-64-bit.7z.exe
————————————————————————–
本来depot_tools.zip自带git,但最近更新depot_tools时提示:
WARNING:root:depot_tools will soon stop bundling Git for Windows.
To prepare for this change, please install Git directly. See
https://chromium.googlesource.com/chromium/src/+/main/docs/windows_build_instructions.md#Install-git
所以干脆自己装git,展开到:
X:\dev\git\
3) Chrome Tools
————————————————————————–
https://storage.googleapis.com/chrome-infra/depot_tools.zip
————————————————————————–
展开depot_tools.zip到
X:\dev\depot_tools\
set Path=X:\dev\git\bin;X:\dev\depot_tools;%Path%
set DEPOT_TOOLS_WIN_TOOLCHAIN=0
set vs2022_install=C:\Program Files\Microsoft Visual Studio\2022\Community
set https_proxy=socks5://…
可用SOCKS5代理,不是非得用HTTP(S)代理
更新depot_tools
cd /d X:\dev\depot_tools
gclient (可能需挂代理)
我忘了执行gclient之前是否调整过如下参数:
git config –global core.autocrlf false
git config –global core.filemode false
git config –global core.fscache true
git config –global core.preloadindex true
gclient
更新结束后检查一下
where python3 (不要用 where python)
确保如下路径先出现
X:\dev\depot_tools\python3.bat
4) 下载指定版本v8源码
以v8_13.0.245.20为例
编辑
X:\dev\boto.cfg
————————————————————————–
[Boto]
#
# no http://
#
proxy = <ip>
proxy_port= <port>
————————————————————————–
set Path=X:\dev\git\bin;X:\dev\depot_tools;%Path%
set DEPOT_TOOLS_WIN_TOOLCHAIN=0
set vs2022_install=C:\Program Files\Microsoft Visual Studio\2022\Community
set NO_AUTH_BOTO_CONFIG=X:\dev\boto.cfg
set https_proxy=http://…
挂代理下载v8源码,在某地区有众所周知的理由
cd /d X:\dev\v8_13.0.245.20
fetch v8
cd /d X:\dev\v8_13.0.245.20\v8
git checkout 13.0.245.20
gclient sync -D (不指定-D也可以,我这是洁癖)
5) 编译v8源码
cd /d X:\dev\v8_13.0.245.20\v8
python3 tools\dev\gm.py x64.debug
python3 tools\dev\gm.py x64.release
也可手工分解执行
mkdir out\x64.release
gn args out\x64.release (调整编译选项)
autoninja -C out\x64.release (开始编译)
autoninja -C out\x64.release d8