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