Adventures in Hacking Electron Apps

essentialrandom

Essential Randomness

Posted on March 1, 2019

Adventures in Hacking Electron Apps

Disclaimer: Hacking apps is often against the Terms of Service. This article is purely theoretical and not an endorsement of the practice. Always Hack Responsibly.

Have you ever been extremely annoyed by a certain aspect of an app or website?

If you're anything like me, the answer is probably yes. If you're a lot like me, the answer is "yes, often."

On the web, I can easily solve most grievances thanks to extensions that allow you to inject custom CSS and Javascript on any website. With desktop apps, however, I usually have to live with my pain, hoping the developers will one day see the light and decide to fix the problem (or will finally have the time to prioritize doing so).

Unless the app is an Electron app.

I don't remember how I learned about this possibility—sometimes, there's no stronger driving force than the need to fix bad design.

In this article, I'm going to talk about how I changed Discord's app code to solve one of my (and other's) biggest grievances: its huge minimum window size.

Discord's default minimum window size

Discord's *huge* minimum window size (940px).

Let's be clear, though: this post—just like any worthy software endeavor—is about the journey rather than the solution. So follow along my adventure, see how I figured out the needed steps, and learn something about the command line, electron apps and hacking!

Note: while I use macOS High Sierra, this process can be similarly replicated on other operating systems.

Unraveling Discord's Secrets

Initially, I wanted to learn as much as I could about the Discord application. MacOS has a UI mechanism to explore application contents, which is a good "step zero." Manual exploration only goes so far, though, and soon I turned to the command line.

Step 1: What does Discord's Process Look Like?

To answer my own question, I ran the following command:

ps x | grep Discord
Enter fullscreen mode Exit fullscreen mode

In short, ps lists all the processes running, x includes the ones not attached to a shell (e.g. those started by clicking on application icons), and piping this output (|) to the grep command displays only the ones featuring the string Discord. You can learn more at explainshell.com.

Here's the output, edited for readability:

1927   ??  S      0:00.08 /Applications/Discord.app/Contents/Frameworks/Electron Framework.framework/Resources/crashpad_handler 
    --no-rate-limit --no-upload-gzip 
    --database=/var/folders/sm/4v5p46v175d3x94qp56r37340000gn/T/Discord Crashes 
    --metrics-dir=/var/folders/sm/4v5p46v175d3x94qp56r37340000gn/T/Discord Crashes 
    --url=http://crash.discordapp.com:1127/post 
    --handshake-fd=73
Enter fullscreen mode Exit fullscreen mode
Process #1927
1928   ??  R     34:58.78 /Applications/Discord.app/Contents/Frameworks/Discord Helper.app/Contents/MacOS/Discord Helper 
    --type=renderer --no-sandbox --autoplay-policy=no-user-gesture-required 
    --force-color-profile=srgb --enable-features=SharedArrayBuffer 
    --disable-features=MacV2Sandbox --service-pipe-token=5494336596696404231 
    --lang=en-US 
    --app-path=/Applications/Discord.app/Contents/Resources/app.asar 
    --node-integration=false --webview-tag=false --no-sandbox 
    --preload=/Users/essential_randomness/Library/Application Support/discord/0.0.254/modules/discord_desktop_core/core.asar/app/mainScreenPreload.js 
    --background-color=#2f3136 --num-raster-threads=2 --enable-zero-copy 
    --enable-gpu-memory-buffer-compositor-resources 
    --enable-main-frame-before-activation 
    --service-request-channel-token=5494336596696404231 --renderer-client-id=6
Enter fullscreen mode Exit fullscreen mode
Process #1928

The first process (#1927) seemed to be related to reporting app crashes. I assumed this because of .../crashpad_handler in the app path, and the url flag pointing to http://crash.discordapp.com:1127/post (which is probably the server endpoint crash reports are communicated to).

The second process (#1928) was more promising. In particular I found the value of the app-path variable (/Applications/Discord.app/Contents/Resources/app.asar) worth exploring.

Step 2: Extracting the App Code.

The .asar extension of the file intrigued me. After a quick google search for "asar files", I found a GitHub repo explaining the format:

Asar is a simple extensive archive format, it works like tar that concatenates all files together without compression, while having random access support.

Luckily, the repository contained information on how to install the asar command- line utility (npm install asar) and how to extract archived files from asar files. My next step was both obvious and easy.

Before making any changes, however, I decided to backup the original archive:

# Backup original file in case of emergency
cd /Applications/Discord.app/Contents/Resources/
cp app.asar app_safe_copy.asar

# Extract app.asar to a folder named "unpacked"
asar extract app.asar unpacked/
Enter fullscreen mode Exit fullscreen mode

After cracking this metaphorical treasure chest open, it was time to list (ls) its content!

cd unpacked
ls
# output:
> app_bootstrap common        node_modules  package.json

ls app_bootstrap/
# output:
> Constants.js       bootstrap.js       ...       appSettings.js       ...
Enter fullscreen mode Exit fullscreen mode

The familiar node_modules/ + package.json combination pointed to the archive being an npm package, bundling together a bunch of JavaScript files and other things like images. This was clearly important code! Even better, the code was not compiled or encrypted in any way.

For the first time I thought that (maybe) I could really pull this off!

Step 3: What is This Code?

I explored the JS files by opening them in VsCode to get some insight on the app's structure. This was interesting, but very slow.

To be faster, I decided to bet on a simple assumption: any file controlling the window width would have had to contain the string "width" itself!

Also, I could exclude the node_modules folder from my search because npm packages reserve this directory for external libraries.

# Find all the files containing the string width in the current folder,
# but exclude the ones in the node_modules one.
grep -iRl "width" ./ | grep -v node_modules
# Output:
> .//app_bootstrap/splash/index.js
> .//app_bootstrap/splash/variables.json
> .//app_bootstrap/splashScreen.js
Enter fullscreen mode Exit fullscreen mode

In depth explanation of the command.

This output was disappointing: the files were clearly related to the splash screen, which wasn't what I was looking to change. I tried going up in the top-level Discord folder (/Applications/Discord.app/) and ran the command again, but the output wasn't much different.

It seemed my luck had ran out.

Step 4: Sometimes Stepping Back is a Step Forward

Rather than despair, I decided to go back to process #1928. The preload flag held another interesting path, in a completely different location than the previous one: /Users/essential_randomness/Library/Application Support/discord/0.0.254/modules/discord_desktop_core/core.asar/app/mainScreenPreload.js.

It was time for another adventure!

# Once again, I searched for files containing the string "width".
cd /Users/essential_randomness/Library/Application\ Support/discord/
grep -iRl "width" ./ | grep -v node_modules
# Output
> .//Preferences
> ...
> .//settings.json
> ...
> .//0.0.254/modules/discord_desktop_core/core.asar
Enter fullscreen mode Exit fullscreen mode
This was when I found out you can grep the content of asar files without unpacking them first. Forgetting steps can lead to interesting discoveries.

This search yielded quite a number of files, so I decided to try to narrow it down further. Since I was looking to change the minimum width, I figured any related variable name would be called either minWidth or min_width. After all, we all take code readability seriously, don't we?

grep -iRl "min_width" ./ | grep -v node_modules
# Output
> .//0.0.254/modules/discord_desktop_core/core.asar

grep -iRl "minWidth" ./ | grep -v node_modules
# Output
> .//0.0.254/modules/discord_desktop_core/core.asar
> .//0.0.254/modules/discord_voice/discord_voice.node
Enter fullscreen mode Exit fullscreen mode

core.asar looked really promising! Once again I extracted it and searched for the right file:

cd 0.0.254/modules/discord_desktop_core
cp core.asar core_safe_copy.asar
asar extract core.asar core_unpacked
cd core_unpacked

# Trying min_width first, as the value is likely a constant.
# Constants use, by many code conventions, a capitalized style (i.e. "MIN_WIDTH").
grep -iRl "min_width" ./ | grep -v node_modules
# Output:
> .//app/mainScreen.js
Enter fullscreen mode Exit fullscreen mode

Could .//app/mainScreen.js finally be The One? I immediately opened it, searched for "min_width" and...

const MIN_WIDTH = settings.get('MIN_WIDTH', 940);
const MIN_HEIGHT = settings.get('MIN_HEIGHT', 500);
Enter fullscreen mode Exit fullscreen mode
...JACKPOT!

Step 5: The Moment of Truth

I knew I had to be onto something. With no clue about whether it would work (but tons of faith) I edited the code:


const MIN_WIDTH = settings.get('MIN_WIDTH', 0);
const MIN_HEIGHT = settings.get('MIN_HEIGHT', 0);
Enter fullscreen mode Exit fullscreen mode

Now all I needed to do was repack the changed asar file. Again, I made sure to create a backup of core.asar (cp core.asar core_safe_copy.asar) before going further. Bricking Discord completely was a real possibility here!

With huge trepidation, I ran the final step:

# Remove the original app file and swap it with our edited code, repacked.
rm core.asar
asar pack core_unpacked core.asar
Enter fullscreen mode Exit fullscreen mode

At this point I restarted Discord, hoping the changes would take effect. I placed my cursor at the app border, started dragging and... IT WORKED!

Discord with minimum window size removed

Discord with minimum window size removed

Side Quest: "I Also Like to Live Dangerously"

At this point, I had still one last curiosity. What would have happened if I had messed up while modifying the code?

I re-extracted the asar file, purposely inserted an invalid JavaScript statement, repacked it and tried to run the app. Unsurprisingly, I got an error!

This hammered down the importance of back ups. Since I had wisely created a copy of core.asar, I simply put the original code back in its place and the error was gone.

Conclusion

As a final warning, modifying code this way is likely against most Terms of Service (cue the usual Great Power => Great Responsibility speech).

Another aspect of code hacking to think about is side effects: Discord is not optimized to be displayed at lower sizes and the UI can be wonky. Since Electron apps use Chromium as a fronted, I modified the UI myself through the Developer Tools console (which Discord kindly makes available under "view > developer > developer tools").

Please remember that a very valid reason why developers don't want their code modified is that it can cause unexpected bugs in the app itself. If you choose to run a custom version of any code, don't go filing bugs unless you can reproduce them in the original app!

A Funny Epilogue

After going through all this, I found out there's an easier way to change Discord's window size that does not require modifying the source code.

But, you know, where would the fun be in that?

Happy Hacking!

💖 💪 🙅 🚩
essentialrandom
Essential Randomness

Posted on March 1, 2019

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related

Adventures in Hacking Electron Apps