Windows installers and the Electron

The draw.io desktop app ships diagrams as .drawio files that eventually become PDFs and SVGs in my asset pipeline. When draw.io updates and my converter breaks, I need a fresh desktop build to dig through. draw.io publishes releases in several formats, and it turns out most of them can be opened on Linux with the right tools.

The Windows installer is a 100 MB .exe. 7-zip lists its contents and extracts the NSIS package, revealing a nested .7z archive containing the actual app data. Inside that is the Chromium license — expected for an Electron app — and the real payload, the .asar archive, which Electron uses as its own archive format.

Install the asar CLI globally and you can list the contents:

The Chromium license is just a side effect of Electron; the app code lives inside .asar, which handles all the HTML, JavaScript, and CSS that draw.io needs to render diagrams. Once the archive is opened, the entire app payload is yours to extract — draw.io is open source, so none of this is "hacking" in any meaningful sense.

The Windows no-installer build is a self-extracting NSIS archive. The file utility identifies it correctly, and 7z x extracts it the same way as the installer version.

Other desktop formats

The macOS build is a DMG, which is normally a mountable image. On Linux, 7-zip handles that too, treating it like any other container. Linux .deb files are just GNU ar archives; ar from binutils or LLVM, or 7-zip, can list and extract them. The real payload is data.tar.xz, which 7-zip expands to a data.tar and then to the app directory.

The Linux .rpm format follows a similar path: 7-zip extracts it to a .cpio file, and a second extraction yields the app contents. The AppImage version extracts directly with 7-zip.

The snap package links to the Snap Store API. The JSON response includes an anon_download_url field that points to an xz-compressed squashfs filesystem, which 7-zip can open and navigate just like any other archive.

Chrome OS and the zip trick

The Chrome OS build links to the Chrome Web Store and is much smaller than the others because it ships only the HTML/JS, not a full Chromium binary. Downloading it relies on a URL built from a script run in the browser console while viewing the store page. That download is a .crx file, which is really a zip file with extra header bytes — 7-zip recognizes it and extracts the contents directly. There is no .asar here; the app files are laid bare in the archived folder.

The reason Electron apps use .asar at all comes down to older Windows path-length limits and the OS's poor handling of many small files. A single archive that looks like a folder from Electron's perspective gives speedups on Windows, but the format is unnecessary for a pure web payload that ships in a ZIP-like container.

Every format draw.io distributes — Windows installer, no-installer build, macOS DMG, Linux deb/rpm/AppImage/snap, and the Chrome OS web store package — can be cracked open with the same basic toolkit: 7-zip plus, where needed, ar or the asar CLI. None of these require platform-specific tools, just a familiarity with the common container formats.

Beyond the boilerplate: Figma and Discord

The generic extraction recipe doesn't hold for every Electron app. Figma's macOS build is a case in point: the .dmg weighs in at roughly two megabytes, and digging into it with 7-zip surfaces HFS+ Private Data folders instead of a straightforward app bundle.

$ ls -lhA total 3608 -rw-r--r--@ 1 amos wheel 1.8M Jul 3 15:18 Figma.dmg

That reveals a .app that won't launch as-is:

$ open Figma.app The application cannot be opened for an unexpected reason, error=Error Domain=RBSRequestErrorDomain Code=5 "Launch failed." UserInfo={NSLocalizedFailureReason=Launch failed., NSUnderlyingError=0x6000005f2610 {Error Domain=NSPOSIXErrorDomain Code=111 "Unknown error: 111" UserInfo={NSLocalizedDescription=Launchd job spawn failed}}}

The culprit is missing file permissions — 7-zip doesn't preserve them. A quick chmod +x gets you past that, but the next error is more interesting: the app insists it must live in /Applications. That's App Translocation at work, a macOS security measure that runs quarantined apps from a randomized read-only path.

If running the app isn't an option, the Info.plist hands you the actual download URLs:

$ cat ./Figma.app/Contents/Info.plist | grep -i https -B 1 <key>aarch64</key> <string>https://desktop.figma.com/mac-arm/Figma.zip</string> <key>x86_64</key> <string>https://desktop.figma.com/mac/Figma.zip</string>

So Figma ships a tiny architecture-aware downloader rather than a universal binary. It's a clever bandwidth saver, even if it feels odd. The real payload turns out to be far more interesting:

$ curl -sLO https://desktop.figma.com/mac-arm/Figma.zip $ unzip -l ./Figma.zip | grep -F '.asar' 1658107 06-27-2023 11:13 Figma.app/Contents/Resources/app.asar 0 06-27-2023 11:14 Figma.app/Contents/Resources/app.asar.unpacked/ 0 06-27-2023 11:13 Figma.app/Contents/Resources/app.asar.unpacked/node_modules/ 0 06-27-2023 11:14 Figma.app/Contents/Resources/app.asar.unpacked/node_modules/fsevents/ 55872 06-27-2023 11:14 Figma.app/Contents/Resources/app.asar.unpacked/node_modules/fsevents/fsevents.node 173696 06-27-2023 11:14 Figma.app/Contents/Resources/app.asar.unpacked/bindings.node 2249984 06-27-2023 11:14 Figma.app/Contents/Resources/app.asar.unpacked/desktop_rust.node
$ asar list app.asar | grep .js | grep -v node_modules /build.json /i18n/ja.json /js /js/desktop_shell.js /main.js /package-lock.json /package.json /shell_app_binding_renderer.js /tray_binding_renderer.js /web_app_binding_renderer.js

That formatting command comes from the js-beautify package, and the bundle includes native components. Figma ships some Rust (and likely C++) to complement Electron's APIs. So how do we confirm it's still Electron underneath?

$ ls -lhA Figma.app/Contents/Frameworks total 0 drwxr-xr-x@ 7 amos wheel 224B Jun 27 11:13 Electron Framework.framework drwxr-xr-x@ 3 amos wheel 96B Jun 27 11:13 Figma Helper (GPU).app drwxr-xr-x@ 3 amos wheel 96B Jun 27 11:13 Figma Helper (Plugin).app drwxr-xr-x@ 3 amos wheel 96B Jun 27 11:13 Figma Helper (Renderer).app drwxr-xr-x@ 3 amos wheel 96B Jun 27 11:13 Figma Helper.app drwxr-xr-x@ 5 amos wheel 160B Jun 27 11:13 Mantle.framework drwxr-xr-x@ 5 amos wheel 160B Jun 27 11:13 ReactiveObjC.framework drwxr-xr-x@ 5 amos wheel 160B Jun 27 11:13 Squirrel.framework

The desktop_rust.node module is a treasure trove of symbols. Running nm on it reveals harfbuzz and freetype routines, which aligns with the JavaScript calling getFontPreview. But there's also cxxbridge symbols, suggesting a Rust+C++ bridge, while bindings.node probably wraps Objective-C/Swift for macOS-specific platform code. Disassembly is left as an exercise for the reader.

Discord's updater maze

Discord's macOS download is equally deceptive:

$ ls -lhA Discord.dmg -rw-r--r--@ 1 amos wheel 158M Jul 3 16:11 Discord.dmg $ 7z l Discord.dmg | grep 'asar' 2023-04-26 23:30:26 ..... 4816906 4820992 Discord/Discord.app/Contents/Resources/app.asar

That too is just an installer/updater:

$ rg 'NEW_UPDATE_ENDPOINT' index.js 23: NEW_UPDATE_ENDPOINT 26: if (!updater.tryInitUpdater(buildInfo, NEW_UPDATE_ENDPOINT)) { appUpdater.js 27: if ((0, _updater.tryInitUpdater)(_buildInfo.default, _Constants.NEW_UPDATE_ENDPOINT)) { Constants.js 25:const NEW_UPDATE_ENDPOINT = settings.get('NEW_UPDATE_ENDPOINT') || 'https://updates.discord.com/'; 34: NEW_UPDATE_ENDPOINT,

Keep probing and you hit a URL template that resolves to the real application payload:

$ curl -sL -o discord.tar.gz "https://discord.com/api/download/stable?platform=linux&format=tar.gz" $ ls -lhA total 197880 -rw-r--r--@ 1 amos wheel 88M Jul 3 16:36 discord.tar.gz

It's the same content as the macOS app — Discord doing its best on Linux, where self-updating distribution is a headache. But there's a second update mechanism that fetches individual components:

$ cat bootstrap/manifest.json { "discord_desktop_core": 0, "discord_erlpack": 0, "discord_spellcheck": 0, "discord_utils": 0, "discord_voice": 0 }

In moduleUpdater.js, you find the logic:

remoteBaseURL = `${endpoint}/modules/${buildInfo.releaseChannel}`;

Note that getRemoteModuleName appends .x64 on 64-bit Windows — presumably the last platform where Discord still supports non-64-bit builds.

Hunting through 404s eventually surfaces a Reddit error mentioning updates.discord.com. That error is revealing twice over: first, it proves Discord also embeds Rust in its Electron app; second, it exposes a full manifest endpoint:

$ curl "https://updates.discord.com/distributions/app/manifests/latest?channel=stable&platform=win&arch=x86" -o my-manifest.json

That points to a .distro file. The name is opaque until you check OpenAsar — a third-party Discord client replacement — whose sources reveal the trick:

body = Buffer.concat(body); body = zlib.brotliDecompressSync(body); fs.writeFileSync('client.tar', body);

The .distro file is just brotli compressed.

$ brotli -d full.distro -o full.distro.tar (cut) $ tar wtf full.distro.tar delta_manifest.json files/core.asar files/index.js files/package.json
Cool bear

Handy mnemonic for the tar flags involved:

  • tar wtf: what the fuck is inside of there?
  • tar pfx: please fucking extract this

Once decompressed, you're looking at 2500+ files:

$ find . | wc -l 2613

Finally, the actual app source.

The takeaway

For Electron apps, source code is usually trivial to obtain. Often it isn't even minified, and node_modules is shipped in full. That accessibility makes it easy to learn how an app works — and occasionally to find vulnerabilities, which some companies respond to with insultingly low bounties on HackerOne or similar platforms.