https://fasterthanli.me/articles/cracking-electron-apps-open
Home Articles Series
[ ]
Feedback
* Log in
Cracking Electron apps open
Jul 3, 2023 8 minute read electron * rust * asar [logo-round]
fasterthanlime
Contents
* Windows Installer
* Windows, no installer
* macOS - Universal
* Linux .deb
* Linux .rpm
* Linux AppImage
* Linux snap
* Google Chrome OS
* Does this work for any Electron app?
* Figma
* Discord
* Conclusion
I use the draw.io desktop app to make diagrams for my website. I run
it on an actual desktop, like Windows or macOS, but the asset
pipeline that converts .drawio files, to .pdf, to .svg, and then to
.svg again (but smaller) runs on Linux.
So I have a Rust program somewhere that opens headless chromium, and
loads just the HTML/JS/CSS part of draw.io I need to render my
diagrams, and then use Chromium's "print to PDF" functionality to
save a PDF.
This is pretty much exactly what the official draw.io docker image
does btw.
Wait, why do we need Chromium again?
Because SVG doesn't have rich text formatting, so draw.io kinda...
sorta.. there's HTML markup in there, and we need a browser to lay it
all out. (I suppose we could write a custom renderer, but then it
wouldn't look the way it does in the editor so shrug).
Anyway - over time, draw.io updates, and my converter program breaks!
When that happens, I need to grab a recent build of draw.io desktop
and rip it apart to find the files I want!
Draw.io desktop releases in a few different formats: Windows
Installer, Windows No Installer, macOS Universal, Linux deb, Linux
snap, Linux AppImage, Linux rpm, and Google Chrome OS.
Let's see how we can crack open all of these!
(crack as in walnut, not as in jail time).
Windows Installer
You'll want 7-zip: on Ubuntu you can install p7zip-full.
We get a 100M .exe file:
Shell session
$ ls -lhA
total 100M
-rw-rw-r-- 1 amos amos 100M Jul 2 20:49 draw.io-21.4.0-windows-installer.exe
Will 7-zip be able to list what's inside?
Shell session
$ 7z l draw.io-21.4.0-windows-installer.exe
7-Zip [64] 16.02 : Copyright (c) 1999-2016 Igor Pavlov : 2016-05-21
p7zip Version 16.02 (locale=en_US.UTF-8,Utf16=on,HugeFiles=on,64 bits,16 CPUs AMD Ryzen 9 5950X 16-Core Processor (A20F10),ASM,AES-NI)
Scanning the drive for archives:
1 file, 104056664 bytes (100 MiB)
Listing archive: draw.io-21.4.0-windows-installer.exe
--
Path = draw.io-21.4.0-windows-installer.exe
Type = PE
Physical Size = 104056664
CPU = x86
Characteristics = Executable 32-bit NoRelocs NoLineNums NoLocalSyms
Created = 2018-12-16 00:26:14
Headers Size = 1024
Checksum = 104056718
Image Size = 2211840
Section Alignment = 4096
File Alignment = 512
Code Size = 26624
Initialized Data Size = 473088
Uninitialized Data Size = 16384
Linker Version = 6.0
OS Version = 4.0
Image Version = 6.0
Subsystem Version = 4.0
Subsystem = Windows GUI
DLL Characteristics = Relocated NX-Compatible NoSEH TerminalServerAware
Stack Reserve = 1048576
Stack Commit = 4096
Heap Reserve = 1048576
Heap Commit = 4096
Image Base = 4194304
Comment = FileVersion: 21.4.0.0
FileVersion: 21.4.0
ProductVersion: 21.4.0.0
ProductVersion: 21.4.0
CompanyName: JGraph
FileDescription: draw.io desktop
LegalCopyright: Copyright 2017-2019 draw.io
ProductName: draw.io
----
Path = [0]
Size = 103883624
Packed Size = 103883624
Virtual Size = 103883624
Offset = 153088
--
Path = [0]
Type = Nsis
Physical Size = 103883624
Method = Deflate
Solid = -
Headers Size = 161498
Embedded Stub Size = 0
SubType = NSIS-3 Unicode BadCmd=11
Date Time Attr Size Compressed Name
------------------- ----- ------------ ------------ ------------------------
..... 6931 $PLUGINSDIR/System.dll
..... 45608 $PLUGINSDIR/StdUtils.dll
2019-06-17 21:21:30 ..... 1899 $PLUGINSDIR/modern-wizard.bmp
..... 4684 $PLUGINSDIR/nsDialogs.dll
..... 8018 $PLUGINSDIR/UAC.dll
..... 2027 $PLUGINSDIR/nsProcess.dll
..... 3299 $PLUGINSDIR/nsExec.dll
2023-06-14 23:42:16 ..... 103376259 103376259 $PLUGINSDIR/app-64.7z
..... 242382 $PLUGINSDIR/nsis7z.dll
2023-06-14 23:42:18 ..... 152955 $R0/Uninstall draw.io.exe
..... 1080 $PLUGINSDIR/WinShell.dll
------------------- ----- ------------ ------------ ------------------------
2023-06-14 23:42:18 103376259 103845142 11 files
Of course! Looks like it's using NSIS, the Nullsoft Scriptable
Install System, and 7-zip knows how to extract that:
Shell session
$ 7z x draw.io-21.4.0-windows-installer.exe
(cut)
SubType = NSIS-3 Unicode BadCmd=11
Everything is Ok
Files: 11
Size: 104395691
Compressed: 104056664
The actual installed app data lies in a nested .7z archive:
Shell session
$ cd '$PLUGINSDIR'
$ 7z x app-64.7z
(cut)
Everything is Ok
Folders: 2
Files: 74
Size: 433816630
Compressed: 103376259
Did we find what we were looking for?
Shell session
$ find . -name '*.html'
./LICENSES.chromium.html
Mhh, not quite, that's just a Chromium license (expected for an
Electron app).
What we want is in the .asar, which is Electron's own archive format
Shell session
$ find . -name '*.asar'
./resources/app.asar
Let's install the CLI:
Shell session
$ sudo npm install -g @electron/asar
added 14 packages in 13s
1 package is looking for funding
run `npm fund` for details
(Note: I'm sudo-ing out of laziness here, if you just want to install
npm packages globally, you can just change the permissions of the
global prefix, or change the global prefix altogether, see this
article)
Now let's see what's hidden in there:
Shell session
$ hash -r
# (we just added a command to $PATH - tab-completion won't have it until we
# open a new shell or call `hash -r` manually)
$ asar l resources/app.asar | grep -v node_modules | grep -E '[.](html|css)$'
/drawio/teams.html
/drawio/src/main/webapp/clear.html
/drawio/src/main/webapp/dropbox.html
/drawio/src/main/webapp/export-fonts.css
/drawio/src/main/webapp/export3.html
/drawio/src/main/webapp/github.html
/drawio/src/main/webapp/gitlab.html
/drawio/src/main/webapp/index.html
/drawio/src/main/webapp/onedrive3.html
/drawio/src/main/webapp/open.html
/drawio/src/main/webapp/teams.html
/drawio/src/main/webapp/vsdxImporter.html
/drawio/src/main/webapp/styles/atlas.css
/drawio/src/main/webapp/styles/dark.css
/drawio/src/main/webapp/styles/grapheditor.css
/drawio/src/main/webapp/mxgraph/css/common.css
/drawio/src/main/webapp/mxgraph/css/explorer.css
Okay yeah that's what I need!
Shell session
$ asar e ./resources/app.asar ./app-unpacked
(no output)
$ du -sh ./app-unpacked
204M ./app-unpacked
Game's over, we can grab anything we want from there.
Again, this isn't "hacking" - draw.io desktop is open source in the
first place, and they don't make even the slightest attempt at hiding
what's in there.
It is interesting though, that we're able to extract the entire
contents of that Windows app from Linux. Similar tricks are employed
in, like, "wine install scripts" for GOG games (since their Windows
games only provide installers).
Let's look at some of the other installation formats, though!
Windows, no installer
Let's see:
Shell session
$ ls -lhA
total 100M
-rw-rw-r-- 1 amos amos 100M Jul 2 21:34 draw.io-21.4.0-windows-no-installer.exe
The file command-line utility is good at sussing out what's actually
in a... file:
Shell session
$ file draw.io-21.4.0-windows-no-installer.exe
draw.io-21.4.0-windows-no-installer.exe: PE32 executable (GUI) Intel 80386, for MS Windows, Nullsoft Installer self-extracting archive, 5 sections
Here too, 7z x does the job:
Shell session
$ 7z x draw.io-21.4.0-windows-no-installer.exe
Everything is Ok
Files: 4
Size: 103925123
Compressed: 103841200
$ ll
total 100M
drwx------ 2 amos amos 4.0K Jul 2 21:36 '$PLUGINSDIR'
-rw-rw-r-- 1 amos amos 100M Jul 2 21:34 draw.io-21.4.0-windows-no-installer.exe
$ ls '$PLUGINSDIR/'
app-64.7z nsis7z.dll StdUtils.dll System.dll
You know the rest.
(P.S: draw.io folks: a self-extracting NSIS archive is not really "no
installer". An actual archive file would be that. Windows 11 is
getting native support for 7z archives soon.
macOS - Universal
Shell session
$ ls -lhA
total 218M
-rw-rw-r-- 1 amos amos 218M Jul 2 21:40 draw.io-universal-21.4.0.dmg
Annoyingly the macOS build is a DMG, which is a mountable image
format. This is not an issue if you're on macOS, of course, but say
you're trying to extract this from Linux, your options are limited.
Haha, just kidding!
Shell session
$ 7z x draw.io-universal-21.4.0.dmg
(cut)
$ find . -name '*.asar'
./draw.io 21.4.0-universal/draw.io.app/Contents/Resources/app.asar
$ asar list "$(find . -name '*.asar')" | grep -F index.html
/drawio/src/main/webapp/index.html
Are you starting to see a pattern here?
Linux .deb
Debian package files are just GNU ar archives (just like static
libraries!)
So we can list the contents and extract it with ar (provided by GNU
binutils):
Shell session
$ ar t drawio-amd64-21.4.0.deb
debian-binary
control.tar.gz
data.tar.xz
Or llvm-ar, provided by LLVM:
Shell session
$ llvm-ar t drawio-amd64-21.4.0.deb
debian-binary
control.tar.gz
data.tar.xz
Or 7-zip! Again! (Which is handy if you're doing this from Windows,
for example):
Shell session
$ 7z x drawio-amd64-21.4.0.deb
(cut)
$ ls -lhA
total 206M
-rw-rw-r-- 1 amos amos 3.3K Jul 2 21:45 control.tar.gz
-rw-rw-r-- 1 amos amos 103M Jul 2 21:45 data.tar.xz
-rw-rw-r-- 1 amos amos 4 Jul 2 21:45 debian-binary
-rw-rw-r-- 1 amos amos 103M Jul 2 21:45 drawio-amd64-21.4.0.deb
What we're looking for is in data.tar.xz: we can extract with 7-zip
too, although annoyingly it first extracts data.tar, and then you can
find the .asar in there:
Shell session
$ 7z l data.tar | grep -E '[.]asar$'
2023-06-14 23:33:19 ..... 194079121 194079232 ./opt/drawio/resources/app.asar
Alternatively, you can use tar to do this, which.. should work on
Linux, macOS and even Windows 11 nowadays!
Shell session
# list files
$ tar wtf data.tar.xz | grep -E '[.]asar$'
./opt/drawio/resources/app.asar
# extract only the .asar
$ tar pfx data.tar.xz ./opt/drawio/resources/app.asar
amos@sonic /tmp/winnoins
# bingo!
$ asar list ./opt/drawio/resources/app.asar | grep -F 'index.html'
/drawio/src/main/webapp/index.html
Linux .rpm
Extracting with 7z gives us a .cpio file:
Shell session
$ 7z x drawio-x86_64-21.4.0.rpm
(cut)
ls -lhA
total 520M
-rw-rw-r-- 1 amos amos 418M Jul 2 23:02 draw.io-21.4.0-1.x86_64.cpio
-rw-rw-r-- 1 amos amos 103M Jul 2 23:02 drawio-x86_64-21.4.0.rpm
Extracting again gives us what we want!
Shell session
$ 7z l draw.io-21.4.0-1.x86_64.cpio | grep -E '[.]asar$'
2023-06-14 23:36:02 ..... 194079121 194079121 ./opt/drawio/resources/app.asar
Ok yeah I'm definitely seeing a pattern now.
Linux AppImage
Shortest one yet:
Shell session
$ 7z l drawio-x86_64-21.4.0.AppImage | grep -E '[.]asar$'
2023-06-14 23:33:19 ..... 194079121 67062337 resources/app.asar
Linux snap
This one links to the snap store.
If you use their API, at https://search.apps.ubuntu.com/api/v1/
package/drawio, you'll notice that...
JSON
{
"aliases": null,
"anon_download_url": "https://api.snapcraft.io/api/v1/snaps/download/84JReQ8pcNGJyAbT0gSDiW7OpDkrdaXp_180.snap",
"apps": [
"drawio"
],
"architecture": [
"amd64"
],
"base": "core18",
"binary_filesize": 144482304,
"channel": "stable",
"common_ids": [],
"confinement": "strict",
"contact": "mailto:support@diagrams.net",
"content": "application",
"date_published": "2019-08-07T15:46:03.060636Z",
"deltas": [],
"description": "draw.io desktop",
"developer_id": "odtkI5xnXQ5ok0IrzhKceRjC7sEztjsf",
"developer_name": "draw.io",
"developer_validation": "verified",
"download_sha3_384": "183eb28b82d7c43faa1b186f7df06cbe10cb268af76a7b3e11ad25e8daf44ba3a4a64f071fd3774ccfc795c67b7037d4",
"download_sha512": "ea337e84e09135445e75e1430b8daeb857277e63918575ddfebbd6cfb2b6979ebf09cf35100a253527fd2fa20e898f63f4120df9d19bba839bf6fba5ef2222ce",
"download_url": "https://api.snapcraft.io/api/v1/snaps/download/84JReQ8pcNGJyAbT0gSDiW7OpDkrdaXp_180.snap",
"epoch": "0",
"gated_snap_ids": [],
"icon_url": "https://dashboard.snapcraft.io/site_media/appmedia/2019/08/android-chrome-512x512.png",
"last_updated": "2023-06-27T13:36:29.785729+00:00",
"license": "Apache-2.0",
"links": {
"contact": [
"mailto:support@diagrams.net"
],
"website": [
"https://www.diagrams.net"
]
},
"name": "drawio.jgraph",
"origin": "jgraph",
"package_name": "drawio",
"prices": {},
"private": false,
"publisher": "draw.io",
"ratings_average": 0,
"release": [
"16"
],
"revision": 180,
"screenshot_urls": [
"https://dashboard.snapcraft.io/site_media/appmedia/2019/08/screenshot.png"
],
"snap_id": "84JReQ8pcNGJyAbT0gSDiW7OpDkrdaXp",
"summary": "draw.io",
"support_url": "",
"title": "draw.io",
"version": "21.5.1",
"website": "https://www.diagrams.net"
}
There's an anon_download_url field.
Shell session
$ curl --silent --location --remote-name https://api.snapcraft.io/api/v1/snaps/download/84JReQ8pcNGJyAbT0gSDiW7OpDkrdaXp_180.snap
(no output)
$ ls -lhA
total 138M
-rw-rw-r-- 1 amos amos 138M Jul 2 23:10 84JReQ8pcNGJyAbT0gSDiW7OpDkrdaXp_180.snap
$ file 84JReQ8pcNGJyAbT0gSDiW7OpDkrdaXp_180.snap
84JReQ8pcNGJyAbT0gSDiW7OpDkrdaXp_180.snap: Squashfs filesystem, little endian, version 4.0, xz compressed, 144481911 bytes, 133 inodes, blocksize: 131072 bytes, created: Tue Jun 27 13:36:19 2023
Oh, a squashfs filesystem, xz-compressed!
Gee, I wonder if-
Shell session
$ 7z l 84JReQ8pcNGJyAbT0gSDiW7OpDkrdaXp_180.snap | grep -E '[.]asar$'
2023-06-27 15:35:17 ..... 194485747 62628240 resources/app.asar
..I guess it does!
Google Chrome OS
This time it links to the chrome web store.
Adapting some code from the crxviewer extension, we can build a valid
download URL by pasting the following code onto the JS console in a
browser tab open to the relevant chrome web store page:
JavaScript code
(function() {
let extensionID = new URL(location.href).pathname.split("/")[4];
console.log(`Downloading extension ${extensionID}`);
let x = `id%3D${extensionID}%26uc`;
let url = `https://clients2.google.com/service/update2/crx?response=redirect&prod=chromiumcrx` +
`&prodchannel=unknown&prodversion=9999.0.9999.0&acceptformat=crx2,crx3&x=${x}`;
return console.log(url);
}())
Let's see what we got!
Shell session
$ ls -lhA
total 39152
-rw-r--r--@ 1 amos wheel 19M Jul 3 14:32 extension_21_2_7_0.crx
This is much smaller than any of the other ones.
Well yes, because this one doesn't actually include a full copy of
Chromium and V8. It's not actually an Electron app, just the HTML/JS.
Also, like a lot of other file formats (JAR Java Archives, ODF
OpenDocument Format, MSIX Windows installers) it's actually just "a
zip file with extra bytes at the beginning":
$ unzip -l extension_21_2_7_0.crx | head
Archive: extension_21_2_7_0.crx
warning [extension_21_2_7_0.crx]: 1320 extra bytes at beginning or within zipfile
(attempting to process anyway)
Length Date Time Name
--------- ---------- ----- ----
0 05-03-2023 12:45 images/
0 05-03-2023 12:45 img/
0 05-03-2023 12:45 img/clipart/
0 05-03-2023 12:45 img/computers/
0 05-03-2023 12:45 img/finance/
0 05-03-2023 12:45 img/lib/
0 05-03-2023 12:45 img/lib/active_directory/
So, you guessed it, we can extract it with 7-zip.
No .asar archive here though! We can get at the meat of the app
directly:
Shell session
$ 7z l extension_21_2_7_0.crx | grep .html
2022-06-11 16:07:56 ..... 694 401 index.html
.asar archives only serve Electron apps because, well, some older
versions of Windows had limits on path length for some APIs, so some
tools struggled to install/uninstall apps.
Also in general, Windows is much slower than Linux/macOS at "lots of
small files" (see this in-depth explanation), so having a single file
that transparently operates like a folder from Electron's perspective
(yes really) nets you some nice speedups.
Does this work for any Electron app?
Figma
Figma for macOS is a .dmg that's.. about two megabytes?
Shell session
$ ls -lhA
total 3608
-rw-r--r--@ 1 amos wheel 1.8M Jul 3 15:18 Figma.dmg
Extracting it with 7-zip gives us "HFS+ Private Data" folders and
files:
Shell session
$ tree -ah
[ 128] .
+-- [ 288] Figma
| +-- [ 15K] .DS_Store
| +-- [ 64] .HFS+ Private Directory Data\015
| +-- [914K] .VolumeIcon.icns
| +-- [ 96] .background
| | +-- [ 49K] dmg-background.tiff
| +-- [ 13] Applications
| +-- [ 96] Figma.app
| | +-- [ 256] Contents
| | +-- [1.6K] CodeResources
| | +-- [1.8K] Info.plist
| | +-- [ 96] MacOS
| | | +-- [238K] DynamicUniversalApp
| | +-- [ 8] PkgInfo
| | +-- [ 192] Resources
| | | +-- [ 128] App.nib
| | | | +-- [9.5K] keyedobjects-101300.nib
| | | | +-- [ 12K] keyedobjects.nib
| | | +-- [ 96] en.lproj
| | | | +-- [3.2K] Localizable.strings
| | | +-- [914K] icon.icns
| | | +-- [ 96] ja.lproj
| | | +-- [3.6K] Localizable.strings
| | +-- [ 96] _CodeSignature
| | +-- [3.6K] CodeResources
| +-- [ 64] [HFS+ Private Data]
+-- [1.8M] Figma.dmg
13 directories, 15 files
And the .app itself can't be opened:
$ 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}}}
Why? Because 7-zip didn't preserve permissions:
Shell session
$ cat ./Figma.app/Contents/Info.plist | grep -i cfbundlename -A 1
CFBundleName
DynamicUniversalApp
$ stat ./Figma.app/Contents/MacOS/DynamicUniversalApp
16777231 1453815 -rw-r--r-- 1 amos wheel 0 244000 "Jun 27 11:21:39 2023" "Jun 27 11:21:39 2023" "Jul 3 15:23:28 2023" "Jun 27 11:21:39 2023" 4096 480 0 ./Figma.app/Contents/MacOS/DynamicUniversalApp
We can fix that with chmod +x - after that, running it complains that
it's not in the /Application folder.
Of course that 2-megabyte bundle isn't the actual app. Running it
complains that it must be in /Applications to work correctly, and
that's because of App Translocation (thanks to Skip R. and Kahanis on
Mastodon for clarifications there).
What if you don't want to run that though? Or if you haven't sneakily
switched to macOS like I just did, and running the application is
straight up not an option?
Well, the download URLs are in the Info.plist (see property list):
$ cat ./Figma.app/Contents/Info.plist | grep -i https -B 1
aarch64
https://desktop.figma.com/mac-arm/Figma.zip
x86_64
https://desktop.figma.com/mac/Figma.zip
Personally I think it's hilarious, that they have you download a 2MB
downloader that will then download the right build for your
architecture.
But it also saves everyone bandwidth (as opposed to distributing an
actual universal binary), so I'll allow it.
Anyway:
Shell session
$ 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
How interesting!
Shell session
$ 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
And, well, I've got good news and bad news:
Shell session
$ asar ef app.asar main.js
$ js-beautify main.js | grep '[.]node"'
var K = hl.app.isPackaged ? require("./bindings.node") : require("../build/Release/bindings.node"),
Ei = kt(() => hl.app.isPackaged ? require("./desktop_rust.node") : require("../build/Release/desktop_rust.node")),
That command comes from the js-beautify npm package.
The good news is apparently they ship some Rust? (And I'm assuming
some C++, as everyone said they did?) and it's used for some parts of
the app:
Shell session
$ js-beautify main.js | grep -E '(K[.]|Ei)' | pbcopy
Ei = kt(() => hl.app.isPackaged ? require("./desktop_rust.node") : require("../build/Release/desktop_rust.node")),
let t = Ei(),
return Ei().getFontPreview(t, e, r, i, n)
process.platform === "darwin" && K.customizeWindowButtons(t)
if (process.platform === "darwin") return K.launchApp(t, e.foreground)
return process.platform !== "darwin" ? null : K.getAppPathForProtocol(t) || null
if (process.platform === "darwin") return K.getBundleVersion(t);
if (process.platform === "win32") return K.getExecutableVersion(t);
return K.removeBundleDirectory(t)
return process.platform !== "darwin" ? !1 : K.isP3ColorSpaceCapable()
return process.platform !== "darwin" ? null : JSON.parse(K.getActiveNSScreens(t.getNativeWindowHandle()))
process.platform === "win32" ? K.forceFocusWindow(t.getNativeWindowHandle()) : t.focus()
if (process.platform === "win32") return K.removeAgentRegistryLoginItem()
return process.platform !== "win32" ? !1 : K.isSystemDarkMode()
return K.getCurrentKeyboardLayout()
return K.getWindowScreenshot(t.getNativeWindowHandle())
if (process.platform === "darwin") return K.makePanel(t.getNativeWindowHandle())
if (process.platform === "darwin") return K.positionPanel(t, e)
if (process.platform === "darwin") return K.showPanel()
if (process.platform === "darwin") return K.hidePanel()
return process.platform !== "darwin" ? !1 : K.getPanelVisibility()
if (process.platform === "darwin") return K.destroyPanel()
return K.getWindowUnderCursor()
return K.CheckSpelling(t, e)
return K.SetDictionary(t)
return K.AddWord(t, e)
return t.map(r => K.IgnoreWord(r, e)).every(r => r)
return K.GetCorrectionsForMisspelling(t, e)
return K.GetAvailableDictionaries()
if (process.platform === "darwin") return K.setMenuShortcuts(t)
return process.platform !== "darwin" ? !1 : K.getOSNotificationsEnabled()
return Ei().talonInitialize(t, e, r)
Ei().talonOpenStream(t, e)
Ei().talonSinkStream(t, e, r)
Ei().talonCloseStream(t, e)
for (let i = 0; i < e; i++) K.triggerHaptic(t, e, r), i !== e - 1 && await new Promise(n => setTimeout(n, r))
Mostly it feels like they're complementing Electron's APIs. How do we
know it's still actually electron?
Shell session
$ 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
That's how.
There's a ton of interesting symbols in desktop_rust.node (you can
run nm on it) btw: harfbuzz and freetype stuff, which I expected
since the JavaScript code above calls getFontPreview:
Shell session
$ nm -C desktop_rust.node | grep ' _ft_' | head -5
0000000000144b42 s _ft_adobe_glyph_list
0000000000029fcc t _ft_alloc
0000000000029fe8 t _ft_ansi_stream_close
000000000002a014 t _ft_ansi_stream_io
0000000000029fe0 t _ft_free
$ nm -C desktop_rust.node | grep ' _hb_' | head -5
00000000000e3e4c t _hb_shapers_get()
0000000000086b10 t _hb_options_init()
00000000000dbf7c t _hb_ot_shape_normalize(hb_ot_shape_plan_t const*, hb_buffer_t*, hb_font_t*)
000000000013972c t _hb_ot_shape_normalize(hb_ot_shape_plan_t const*, hb_buffer_t*, hb_font_t*) (.cold.1)
00000000000db7f0 t _hb_ot_shape_fallback_kern(hb_ot_shape_plan_t const*, hb_font_t*, hb_buffer_t*)
...but also cxxbridge stuff! Which is interesting.
Shell session
nm -C desktop_rust.node | grep 'cxxbridge' | head
0000000000029164 t generate_svg(rust::cxxbridge1::String, float, rust::cxxbridge1::String, rust::cxxbridge1::String, rust::cxxbridge1::String)
000000000002866c t get_font(rust::cxxbridge1::String)
0000000000138108 t get_font(rust::cxxbridge1::String) (.cold.1)
00000000000280fc t rust::cxxbridge1::Vec::reserve_total(unsigned long)
00000000000280f0 t rust::cxxbridge1::Vec::drop()
0000000000028100 t rust::cxxbridge1::Vec::set_len(unsigned long)
0000000000029d90 t rust::cxxbridge1::Vec::Vec(rust::cxxbridge1::Vec const&)
00000000000280cc t rust::cxxbridge1::Vec::Vec()
0000000000028134 t rust::cxxbridge1::Vec::reserve_total(unsigned long)
0000000000028128 t rust::cxxbridge1::Vec::drop()
So I guess this is the "Rust+C++" bundle, and bindings.node is maybe
just Objective-C/Swift for macOS-specific platform stuff.
Disassembling these is left as an exercise to the reader.
Discord
How about one last for the road?
Shell session
$ 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
Alright, this is promising!
But here too, this app is in fact, just an installer/updater:
Shell session
$ 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,
(rg is ripgrep)
This could just be a self-updater, but let's keep digging:
Shell session
$ js-beautify app/app_bootstrap/splash/index.js | grep 'discord.com' -B 10
const ee = [{
value: "deb",
label: "Ubuntu (deb)"
}, {
value: "tar.gz",
label: "Linux (tar.gz)"
}, {
value: "nope",
label: "I'll figure it out"
}],
te = `https://discord.com/api/download/${DiscordSplash.getReleaseChannel()}?platform=linux&format=`,
$ cat build_info.json
{
"releaseChannel": "stable",
"version": "0.0.275"
}
Ah, there we go! This lets us build this URL:
Shell session
$ 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
What's in there? Turns out, it's the same as what's in the macOS app.
This is just Discord doing their best at supporting Linux, on which
it's really hard to distribute anything properly, let alone a
self-updating app.
There is, however, a second update system, from what I can see,
that's able to download individual components:
Shell session
$ cat bootstrap/manifest.json
{
"discord_desktop_core": 0,
"discord_erlpack": 0,
"discord_spellcheck": 0,
"discord_utils": 0,
"discord_voice": 0
}
We've got this in moduleUpdater.js:
JavaScript code
remoteBaseURL = `${endpoint}/modules/${buildInfo.releaseChannel}`;
And also this:
JavaScript code
const url = `${remoteBaseURL}/${encodeURIComponent(getRemoteModuleName(queuedModule.name))}/${encodeURIComponent(queuedModule.version)}`;
(Note: getRemoteModuleName just adds .x64 if we're running 64-bit
Windows. I'm assuming Windows is the last OS Discord supports
non-64-bit versions for, because.. gamers.. and Windows 7.. or
something).
After hitting a few 404, I ended up searching for updates.discord.com
and found this error message on Reddit:
[2023-04-07 17:44:23.790094 -04:00] ERROR [updater_client]: Failed 7: Other(
Reqwest(
reqwest::Error {
kind: Request,
url: Url { scheme: "https", cannot_be_a_base: false, username: "", password: None,
host: Some(Domain("updates.discord.com")), port: None,
path: "/distributions/app/manifests/latest",
query: Some("install_id=b92b8922-c623-4a40-8965-8f6752288cb4&channel=stable&platform=win&arch=x86"),
fragment: None },
source: hyper::Error(Connect, ConnectError("dns error",
Os { code: 11003, kind: Uncategorized, message: "A non-recoverable error occurred during a database lookup." })) }))
Which is interesting! First of all, it means Discord also ships Rust
in its Electron app, and second of all it means we can grab a full
manifest easily:
Shell session
$ curl "https://updates.discord.com/distributions/app/manifests/latest?channel=stable&platform=win&arch=x86" -o my-manifest.json
I used the fx interactive JSON viewer tool to find the proper jq
query:
$ jq -C .modules.discord_desktop_core.full my-manifest.json
{
"host_version": [
1,
0,
9014
],
"module_version": 1,
"package_sha256": "62a8bb668df50930e514b7910ba236e259a0bfe7cd97d8e131541317229faeaa",
"url": "https://dl.discordapp.net/distro/app/stable/win/x86/1.0.9014/discord_desktop_core/1/full.distro"
}
So what is that .distro file, actually?
Shell session
$ file full.distro
full.distro: OpenPGP Public Key
Ah, mh.
Searching for "discord full.distro" turns up a GitHub repository
named harmonicord/packageDiscordAsar and harmonyClient/packageAsar -
these are both 404-ing, but similar searches turn up OpenAsar, which,
if we look at their sources a bit..
javacript
body = Buffer.concat(body);
body = zlib.brotliDecompressSync(body);
fs.writeFileSync('client.tar', body);
Ah. It's just brotli.
Shell session
$ 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
Quick mnemonic:
* tar wtf: what the fuck is inside of there?
* tar pfx: please fucking extract this
And inside of there... we have 2500+ files:
Shell session
$ find . | wc -l
2613
And it's the actual app!
Shell session
$ rg -i 'devtools' index.js
125: const enableDevtoolsSetting = global.appSettings.get('DANGEROUS_ENABLE_DEVTOOLS_ONLY_ENABLE_IF_YOU_KNOW_WHAT_YOURE_DOING', false);
126: const enableDevtools = buildInfo.releaseChannel === 'stable' ? enableDevtoolsSetting : true;
130: Menu.setApplicationMenu(createApplicationMenu(enableDevtools));
$ rg -i 'Window' index.js
8:exports.setMainWindowVisible = setMainWindowVisible;
12: BrowserWindow
117: const windowNative = require('./discord_native/browser/window');
123: global.mainWindowId = Constants.DEFAULT_MAIN_WINDOW_ID;
135: getWindow: getPopoutWindowByKey
136: } = require('./popoutWindows');
138: windowNative.injectGetWindow(key => {
139: return getPopoutWindowByKey(key) || BrowserWindow.fromId(mainScreen.getMainWindowId());
147:function setMainWindowVisible(visible) {
148: mainScreen.setMainWindowVisible(visible);
Conclusion
It's usually pretty easy to get at an app's source code, at least for
Electron apps. Most of the time it's not even minified (and they
waste a lot of space in node_modules).
You can learn a bunch of things that way! Sometimes even find
vulnerabilities, which some companies will throw insultingly low
bounties at on HackerOne or something.
Anyway. I'll now go and try to remember why I was doing that in the
first place. Have fun!
Discuss this article on /r/fasterthanlime
If you liked what you saw, please support my work!
Github logo Donate on GitHub Patreon logo Donate on Patreon
Looking for the homepage?
Another article: Profiling linkers
Mastodon YouTube Patreon GitHub Sponsors RSS
About Legal Notice Privacy Policy Terms and Conditions