标题: Electron逆向工程入门 创建: 2025-02-18 12:28 修改: 2025-02-20 16:49 链接: https://scz.617.cn/web/202502181228.txt -------------------------------------------------------------------------- 目录: ☆ 背景介绍 ☆ 确认用Electron开发 ☆ Electron桌面应用基本框架 1) package.json 2) hello.js 3) preload.js 4) index.html 5) renderer.js 6) 小结 ☆ 预编译版Electron桌面应用基本框架 ☆ 逆向分析Electron桌面应用 1) 处理app.asar 1.1) 支持asar格式的7-Zip插件 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 此文件名任意,其内容形如: -------------------------------------------------------------------------- Hello World

Hello World

-------------------------------------------------------------------------- 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可能报错。 1.1) 支持asar格式的7-Zip插件 参看 -------------------------------------------------------------------------- Asar7z https://www.tc4shell.com/en/7zip/asar/ https://www.tc4shell.com/binary/Asar.zip -------------------------------------------------------------------------- Asar7z是个7-Zip插件,放到 C:\Program Files\7-Zip\Formats\Asar.64.dll 之后7-Zip可以解包、打包app.asar,比"@electron/asar"方便。 简单测试,用Asar7z插件解包、打包app.asar,无需app.asar.unpacked目录在场。 暂未用第三方Electron桌面应用测试Asar7z插件新打包app.asar的可用性。 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引擎版本相关的检查及各种SanityCheck,大多数时候并不能真正解决 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 , 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 - length: 2 0: 0x0292001931fd Start FixedArray 00000292001931FD: [FixedArray] in OldSpace - map: 0x0292000005bd - length: 4 0: 0x029200193215 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 - length: 3 0: 0x0292000045f5 1: 0x029200024771 2: 0x02920019324d foo() -> bar()> Handler Table (size = 0) Source Position Table (size = 0) End SharedFunctionInfo 1: 0 2: 0x029200193311 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 - length: 1 0: 0x029200193295 Handler Table (size = 0) Source Position Table (size = 0) End SharedFunctionInfo 3: 1 End FixedArray 1: 0x02920019336d 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], ) 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 = proxy_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