When to use / when not to use
- Use: Electron packaged desktop applications——
resources/app.asarstructure, main/renderer process JS logic recovery, native.nodemodules, key/communication logic extraction - Use: V8 bytecode
.jscfiles (compiled artifacts of bytenode-like tools) identification and boundary determination - Use: dynamic debugging——main process
--inspect, renderer process--remote-debugging-port(CDP)、ELECTRON_RUN_AS_NODEreusing the runtime in Node mode - Use: anti-debugging countermeasures——devtools / inspect detection identification and bypass (linkage [[re-evasion]])
- Don't use: pure browser extensions (go to [[re-browser-ext]])
- Don't use: obfuscated JS/scripts outside the Electron context (go to [[re-script-deob]] / [[re-deobfuscate]])
- Don't use:
.jscappearing in Cocos game asset scenarios (that is Cocos engine bytecode, go to [[re-game]]; this skill only covers Electron's V8 bytecode) - Don't use: when only general binary initial reconnaissance is needed ([[re-binary-core]] general foundation; this skill only supplements Electron packaging layer semantics)
- Note:dynamic execution defaults to sandbox ([[re-analyze/platform-tips]] highest principle)——running the application, connecting to CDP, loading
.jscare all handled as dynamic, static unpacking may skip the sandbox
Tool preparation
Verify all tools before use. Static unpacking may skip the sandbox; running the application / dynamic debugging defaults to sandbox + network isolation ([[re-analyze/platform-tips]] highest principle). Electron applications are mostly cross-platform packaged, the toolchain is mainly the npm ecosystem and consistent across OSes.
Node.js + npm —— tool foundation (asar / CDP client / bytenode / fuses)
- Linux: distribution package
apt install nodejs npm(Debian/Ubuntu repository versions are rather old, for V8 version-locked scenarios use NodeSource or nvm to install a newer version); macOS:brew install node; Windows: official installer orchoco install nodejs; cross-platform: nvm (nvm install <version>) to switch Node versions as needed - Verify:
node --version && npm --version - nvm: official install script (GitHub nvm-sh/nvm); for Windows use nvm-windows or the official installer
@electron/asar —— asar archive unpacking (the currently maintained official package)
npm install -g @electron/asaror install-freenpx @electron/asar ...(npx auto-downloads on first use; the old package nameasaris deprecated but the CLI is still compatible and usable)- Verify:
npx @electron/asar --help(should list thepack / list / extract-file / extractsubcommands) - Usage:
npx @electron/asar extract app.asar out/(extract the whole package),npx @electron/asar list app.asar(list directory),npx @electron/asar extract-file app.asar path/to/file(single file) - Tool-free direct read: the asar header embeds a JSON directory (
{"files":{...}}, each file containssize/offset/integrity), the JSON string starts at offset 16 (the first 12 bytes are pickle nested size fields, offset 12 is the header string length, all uint32 LE)——strings app.asar | grep '"files"'or a small scriptJSON.parseto locate, no tools needed
strings / grep —— text clues
- Linux: bundled with binutils (
apt install binutilsetc.); macOS: bundled (orbrew install binutils); Windows: within WSL or Sysinternals strings - Verification:
strings --version
chrome-remote-interface — CDP client (main tool for dynamic debugging)
- In-project
npm install chrome-remote-interface(official npm package, Chrome DevTools Protocol client) - Verification:
node -e "require('chrome-remote-interface')"no error means installed; for actual connectivity see operation step 6 - Alternative: pure curl + Node built-in ws, no package installation needed (
curl http://127.0.0.1:<port>/json/listgetwebSocketDebuggerUrlthen connect WebSocket and send JSON-RPC)
bytenode — .jsc compilation/loading comprehension tool (for verification and comparison)
npm install -g bytenodeor in-project installation- Verification:
npx bytenode --help(-ccompile,-e/--electroncompile for Electron,-ep/--electron-pathspecify Electron executable) - Purpose: recompile comparison sample with target version Node/Electron
.jscmagic number and loadability; itself is not a decompiler (see pitfall 1) - Electron 42+ main process scenario (
electronMain) has no corresponding CLI parameter, must use API call form:require('bytenode').compileFile({ electronMain: true, filename: 'main.js' })(echoing theelectronMainmode of pitfall 1)
@electron/fuses — runtime capability switch inspection/toggling
npm install -g @electron/fuses(official package)- Verification:
npx @electron/fuses read --app <应用可执行文件或 .app 路径>—outputs each fuse status (RunAsNode / EnableNodeCliInspectArguments / EnableEmbeddedAsarIntegrityValidation / OnlyLoadAppFromAsar etc.) - Toggle:
npx @electron/fuses write --app <路径> RunAsNode=off EnableNodeCliInspectArguments=off(read and assess before writing; toggling modifies the binary, breaks signatures, macOS requires re-signing) - Tool-free inspection: strings search for fuse sentinel string
dL7pKGdnNz796PbbjQWNKmHXBZaB9tsX, followed by fuse version, length and wire bytes (0x30(0)=disabled,0x31(1)=enabled,0x72(r)=removed)
General-purpose decompiler — native .node modules
- Ghidra (official release package, requires JDK; some distribution repositories have
apt install ghidra/pacman -S ghidra;macOSbrew install --cask ghidra; Windows official zip; verificationanalyzeHeadless -help) - IDA: commercial version; Freeware version architecture support scope subject to official page
.nodeis essentially an ELF/Mach-O/PE dynamic library, before importing into decompiler first usefile/readelf -hto confirm architecture (linked with [[re-format-elf]] / [[re-format-macho]] / [[re-format-pe]])
(Optional) xvfb-run — run GUI applications in Linux headless environment
- Linux:
apt install xvfb/dnf install xvfb/pacman -S xvfb - Verification:
xvfb-run -a node --version; when no display server,xvfb-run -a electron .run target - macOS/Windows have native display, no need for
Operation steps
Execute in order, archive each step's results; dynamic execution defaults to sandbox.
-
Identify Electron and version:
ls resources/ # 打包应用应有 app.asar(+ app.asar.unpacked/) strings <可执行文件> | grep -i -E 'electron|chrome' | head- Judgment signals:
resources/app.asarstructure, multiple processes in process list (main process + GPU/renderer child processes), Electron/Chromium version strings in binary - Version confirmation: packaged application
--versionoften has no output—reliable way is to use CDP in dynamic phase to executeprocess.versions(all fields electron/chrome/v8/node/modules); statically can only rely on binary strings version strings as auxiliary - Electron → Chromium/Node correspondence check official electron-timelines release table (only lists Chromium/Node, no V8 column); V8 version can only be obtained at runtime
process.versions.v8get
- Judgment signals:
-
asar unpacking:
npx @electron/asar list app.asar # 先看目录结构 npx @electron/asar extract app.asar out/ # 整包解出 npx @electron/asar extract-file app.asar package.json # 单文件快速取- Tool-free: JSON directory starts at offset 16,
strings app.asar | grep '"files"'or concatenate the scripts by size/offset to reconstruct all files app.asar.unpacked/The directory contains native modules that were not packed into the archive; they are on disk as-is, so analyze them directly- Before modifying the package, first
npx @electron/fuses read --app <可执行文件>checkEnableEmbeddedAsarIntegrityValidation/OnlyLoadAppFromAsar—replacing app.asar while integrity verification is enabled will cause startup failure (see Pitfall 2)
- Tool-free: JSON directory starts at offset 16,
-
Main process entry and logic:
- After unpacking, read
out/package.jsoninmainfield → main process entry JS (defaultmain.js) - The main process is in the Node environment: the require chain,
app/BrowserWindow/ipcMaincalls form the business skeleton; IPC handlers (ipcMain.handle/on) are the hub of main/renderer communication, so enumerate all of them first - The main process logic can be reused by
ELECTRON_RUN_AS_NODE=1 <可执行文件> script.jsin a pure Node manner (same V8/Node ABI, see the boundary in Pitfall 3)—parts that do not depend on the electron API can be run offline to observe behavior - When the main process is packaged as
.jsc:ELECTRON_RUN_AS_NODEunderrequire('bytenode')loads .jsc (the version must match, see Pitfall 1)
- After unpacking, read
-
Renderer process JS recovery:
- Renderer code is generally bundled under
out/some subdirectory (webpack/browserify/rollup bundle, often obfuscated and minified into one line) - Recovery order: determine the entry HTML (BrowserWindow's loadFile/loadURL arguments) → find the bundle referenced by
<script>→ beautify (js-beautify-like) → locate strings (strings/xxd) → for the obfuscation layer use [[re-script-deob]] / [[re-deobfuscate]] - Extraction of renderer process keys/logic proceeds with string references (hardcoded keys, API domains, characteristic constants) as anchors
.jscWhen the file appears, jump to the boundary handling in Pitfall 1 (do not parse it as JS text)
- Renderer code is generally bundled under
-
Native module
.node:- Locate:
app.asar.unpacked/**/*.node(and the .node pointed to by the require path after unpacking) - Identify:
file xxx.node(ELF/Mach-O/PE + architecture),readelf -h(Linux);.nodethat is, a dynamic library, go through [[re-format-elf]] / [[re-format-macho]] / [[re-format-pe]] first → [[re-ghidra]] / [[re-ida]] / [[re-radare2]] decompilation, and for C++ semantics go through [[re-cpp-abi]] - NODE_MODULE_VERSION(
process.versions.modules) corresponds one-to-one with the Electron version; loading a .node with the wrong version will report an ABI error—this can be used to confirm the target Electron version - Exported functions are the N-API/V8 interface layer; cross-locate business functions from the export table + call sites on the JS side (linked with [[re-imports]])
- Locate:
-
Dynamic CDP debugging:
# 主进程(Node/V8 Inspector,默认端口 9229) <可执行文件> --inspect=9229 <可执行文件> --inspect-brk=9229 # 启动即暂停,等调试器 # 渲染进程(Chromium CDP,默认端口 9222) <可执行文件> --remote-debugging-port=9222- Before starting, first
npx @electron/fuses read --app <可执行文件>:EnableNodeCliInspectArgumentswhen it is Disabled,--inspect/--inspect-brkis ignored (SIGUSR1 also no longer opens the inspector)—in this case you can only use renderer process CDP or [[re-frida]] injection - Renderer process connection procedure:
Use chrome-remote-interface to connect tocurl -s http://127.0.0.1:9222/json/list # 列 targets(title/url/webSocketDebuggerUrl)webSocketDebuggerUrl,Runtime.evaluateexecute arbitrary JS,Console.enablecollect logs,Page.captureScreenshottake screenshots; connect to the main process in the same way via--inspectport - The port is subject to the actual response of
/json/list; do not guess a fixed port (the application may occupy/modify it); the target will not appear until the window is created, so trigger it in the UI first and then enumerate - Single-instance lock (
app.requestSingleInstanceLock()common) causes a second launch to exit immediately—use--user-data-dir=<新目录>or terminate the existing instance first - In browser pages, connecting to the CDP WebSocket is subject to Origin checking (Chrome 111+): add
--remote-allow-origins=*; Node clients have no Origin header and are unaffected - Runtime information forensics:
Runtime.evaluateexecuteprocess.versions(version),process.env(environment variables),require.cache(loaded modules) — more reliable than static strings - For Linux environments without a display, use
xvfb-run -ato wrap it before starting
- Before starting, first
-
Anti-debugging countermeasures and data extraction:
- Common forms of devtools/inspect detection: check for the inspect parameter in
process.argv/process.execArgv, DevTools open detection (window sizeouterWidth - innerWidth, console timing),debuggerstatements,process.versionsexception validation - Locate detection points → patch to skip (after modifying the bundle, repack asar; note pitfall 2 integrity) or runtime hook ([[re-frida]]), at the CDP layer
Debugger.enablebypass first before - Injection surface assessment:
contextIsolation(default true since Electron 12),sandbox(default true since Electron 20),nodeIntegration(default false) determines what preload/renderer processes can require, CDPRuntime.evaluatemust choose the correct execution context (contextId of main world vs isolated world) - Data extraction linkage: runtime memory/keys [[re-memdump]], hardcoded keys [[re-crypto-keys]], encrypted data recovery [[re-crypto-decrypt]], system calls/file behavior [[re-tracing]]
- Behavior observation:
ELECTRON_ENABLE_LOGGING=1or--enable-loggingopen Chromium logs, diagnose failures first
- Common forms of devtools/inspect detection: check for the inspect parameter in
Cross-domain joint
- [[re-managed]]: managed code gateway path (re-managed → this skill), Electron's JS layer is orchestrated by it
- [[re-script-deob]] / [[re-deobfuscate]]: renderer process obfuscated JS restoration
- [[re-evasion]]: devtools/inspect detection countermeasure methodology (this skill provides target features, bypass strategies are linked)
- [[re-format-elf]] / [[re-format-macho]] / [[re-format-pe]]: native
.nodemodule format parsing prerequisite - [[re-cpp-abi]]: native module C++ semantics (RTTI/vtable/STL) recovery
- [[re-imports]]: .node export table and library fingerprint, Electron version confirmation aid
- [[re-ghidra]] / [[re-ida]] / [[re-radare2]]: native module decompilation
- [[re-frida]]: dynamic injection alternative channel for fuse-disabled inspect scenarios
- [[re-memdump]] / [[re-crypto-keys]] / [[re-crypto-decrypt]]: runtime key and encrypted data extraction
- [[re-tracing]]: dynamic behavior observation (file/network/system calls)
- [[re-variant]]: multi-version comparison (patch localization, logic differences)
- [[re-game]]: Cocos scenario
.jsc(JS engine bytecode) and Electron.jsc(V8 bytecode) distinction - [[re-sandbox]]: mandatory prerequisite for all dynamic execution ([[re-analyze/platform-tips]] default sandbox principle)
- Companion: [[re-patching]] (bundle byte patch modification), [[re-triage]] (preliminary inspection prerequisite)
Common pitfalls and traps
.jscis a V8 bytecode snapshot, not JS text: symptom — after decoding.jscAfter the file, grep/beautify/parse are all ineffective; the content is unreadable binary. Reason — tools like bytenode compile JS into V8 bytecode and remove the source code (Function.prototype.toString()is replaced with stub code, which is one of the identifying characteristics). Boundary — the V8 bytecode format is undocumented and changes with the V8 version; there is currently no public decompiler tool;node --print-bytecode(available in official Node builds) can only print the bytecode mnemonics compiled during the "running" process, not restore the source code. Countermeasure — first confirm the sample's.jsccompilation environment (magic number/loader hints,process.versions.v8comparison). If it can load, run behavioral analysis (strace/network/memory). If source code semantics are needed, look for an uncompiled .js copy or bundle map. Version lock —.jscmust be compiled by Node or Electron with the same V8 version and the same platform/architecture in order to load; loading across versions crashes directly. Electron 42+ (V8 ≥ 14.8) main process loading of.jscrequires compilation with a real Electron main process (bytenodeelectronMainmode);ELECTRON_RUN_AS_NODEcompiled that way will crash on load (read-only snapshot verification mismatch) — report uncertain boundaries truthfully; do not speculate about source code content- Path obfuscation and integrity verification inside asar: Symptom — the extracted file paths do not match the require logic, or the app exits immediately after modifying app.asar. Reason — asar supports arbitrary filenames (including
../encoding variants), andEnableEmbeddedAsarIntegrityValidationwhen the fuse is enabled, asar contents are verified,OnlyLoadAppFromAsarwhen enabled, non-asar loading is rejected. Countermeasure — restore according to the size/offset of the JSON directory rather than path semantics; before repacking, first@electron/fuses read. When verification is enabled, switch to runtime hooks (frida) or dynamic analysis; do not hard-modify the archive ELECTRON_RUN_AS_NODEbehavior difference: Symptom — after setting the environment variable, the application does not run as Node as expected, or the main process.jsccrashes on load. Reason — in this mode Electron only provides a Node runtime (noelectronAPI、require('electron')abnormal behavior), andRunAsNodewhen the fuse is Disabled, that environment variable is completely ignored (along withprocess.fork()becoming ineffective). Countermeasure — first read fuses to confirm the RunAsNode status; this mode is only used to rerun logic that does not depend on the electron API. GUI/window code must go through normal startup- contextIsolation / sandbox determine the injection surface: Symptom — require reports an error in the preload script, or CDP evaluate cannot access page variables. Reason — since Electron 12, contextIsolation defaults to true (the renderer process and preload context are isolated); since Electron 20, sandbox defaults to true (preload has no full Node capability); nodeIntegration defaults to false. Countermeasure — CDP
Runtime.evaluatefirst enumerate the execution contexts (Runtime.enablethen check contextId), distinguish the main world from the isolated world before evaluating; design the injection/read surface according to the actual configuration of these three switches - fuses change runtime capabilities (debug entry points may be entirely disabled): Symptom —
--inspect、--inspect-brk、ELECTRON_RUN_AS_NODEare all ineffective; the application behaves as if it did not receive the arguments. Reason —EnableNodeCliInspectArguments/RunAsNodethe fuse is flipped off. Countermeasure —@electron/fuses readconfirm first; writing a fuse modifies the binary and breaks the signature (on macOS it must be re-signed; when unsigned, use the resetAdHocDarwinSignature option). This is the last resort for modifying packaged artifacts; prefer switching to frida / renderer process CDP channels - Single-instance lock and uncertain CDP port: Symptom — a second launch with arguments exits directly, /json/list cannot connect. Reason —
requestSingleInstanceLock()Make the second instance exit immediately; the CDP port may be occupied or changed by the application; countermeasure—use an independent--user-data-dirto bypass the single-instance lock; the port is subject to/json/listactual response; when the window is not created, the target is not exposed; trigger the UI first, then enumerate