Electron逆向工程入门
文章介绍了基于Electron的桌面应用逆向工程方法,涵盖基本框架分析、预编译版处理、调试技巧、IPC监控、jsc技术及v8源码编译等内容。 2025-2-20 05:53:44 Author: blog.nsfocus.net(查看原文) 阅读量:57 收藏

阅读: 2

目录:

    ☆ 背景介绍

    ☆ 确认用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


文章来源: https://blog.nsfocus.net/electron/
如有侵权请联系:admin#unsafe.sh