Instrumenting Electron Apps for Security Testing

Back to Posts

Instrumenting Electron Apps for Security Testing

This is a re-posting of the original article “Instrumenting Electron Apps for Security Testing” that I have wrote on Doyensec

What is Electron?

The Electron Framework is used to develop multi-platform desktop applications with nothing more than HTML, JavaScript and CSS. It was initially developed for GitHub’s Atom editor and since then it was used to create applications like Discord, Ghost, GitHub, Skype, Slack, Wire and a lot more.

While for the traditional desktop application various security techniques exists in order to mitigate specific vulnerabilities, the problem with the Electron framework is that developers are “porting” traditional web-based vulnerabilities into desktop context. The consequences can be unexpected and critical: Turning XSS into RCE or Windows Protocol Handler RCE CVE-2018-1000006 bypass.

Electron Architecture & Security

The Electron Framework has two core components: Node.js and the libchromiumcontent module (from the Chromium project).

In Electron the main process is the process that runs package.json’s main script, it can display a GUI by creating web pages. This component has always access to Node.js primitives and it is responsible for starting other processes. An Electron app always has one main process, but never more.

Chromium is used for displaying web pages, each web page in Electron runs in its own process, which is called the renderer process.

In Electron, there are different ways to communicate between the main process and renderer processes: ipcRenderer and ipcMain modules for sending messages and the remote module for RPC style communication.

Unlike regular browsers where web pages run in a sandboxed environment and do not have access to native system resources, Electron renderers have access to Node.js primitives thus allowing lower level integration with the underlying operating system. Electron exposes full access to native Node.js APIs, but it also facilitates the use of external Node.js NPM modules.

Electron exposes full access to Node.js both in the main and the renderer process, that’s mean:

  • All APIs available in Node.js are also available in Electron.
  • JS modules can be used in an application.

As you might have guessed from recent security vulnerabilities, this has substantial security implications since JavaScript code can access the filesystem, user shell, and many more primitives. The inherent security risks increase with the additional power granted to application code. For instance, displaying arbitrary content from untrusted sources inside a non-isolated renderer is a severe security risk.

You can read more about Electron Security, hardening and vulnerabilities prevention in the official Checklist Security Recommendations.

Instrumenting Electron-based applications

With the increasing popularity of the Electron Framework, we have created this post to summarize a few techniques which can be used in order to instrument an Electron-based application, change its behaviour, and perform in-depth security assessments.

Unpacking the ASAR archive

The first thing to do in order to inspect the source code of an Electron-based application is to unpack the application bundle (.asar file). ASAR archives are a simple tar-like format that concatenates files into a single one.

We should first locate the main ASAR archive of our app, usually named core.asar or app.asar.

Once we have this file we can proceed with installing the asar utility:
npm install -g asar

and extract the whole archive:
asar extract core.asar destinationfolder

At its simplest version, an Electron application includes three files: index.js, index.html and package.json.

Our first target to inspect is the package.json file, it holds the path of the file responsible for the “entry pointof our application:

{
    "name": "Example App",
    "description": "Core App",
    "main": "app/index.js",
    "private": true,
}

In our example the entry point is the file called index.js located within the app folder and it will be executed as the main process. If not specified, index.js is the default main file. The file index.htm_ and other web resources are used in renderer processes to display actual content to the user. A new renderer process is created for every browserWindow instantiated in the main process.

In order to be able to follow functions and methods in our favourite IDE we should also resolve the dependencies of our app: npm install

We should also install Devtron, a tool (built on top of the Chrome Developer Tools) to inspect, monitor and debug our Electron app. For Devtron to work, NodeIntegration must be on.

npm install --save-dev devtron

Then, run the following from the Console tab of the Developer Tools

require('devtron').install()

Dealing with obfuscated javascript

Whenever the application is neither minimized nor obfuscated, we can easily inspect the code.

'use strict';
Object.defineProperty(exports, "__esModule", {
    value: true
});
exports.startup = startup;
exports.handleSingleInstance = handleSingleInstance;
exports.setMainWindowVisible = setMainWindowVisible;
var _require = require('electron'),
    Menu = _require.Menu;
var mainScreen = void 0;

function startup(bootstrapModules) {
    [--cut--]

In case of obfuscation, there are no silver bullets to unfold heavily manipulated javascript code. In these situations, a combination of automatic tools and manual reverse engineering is required to get back to the original source.

Take this horrendous piece of JS as an example:

eval(function(c,d,e,f,g,h){g=function(i){return(i<d?'':g(parseInt(i/d)))+((i=i%d)>0x23?String['\x66\x72\x6f\x6d\x43\x68\x61\x72\x43\x6f\x64\x65'](i+0x1d):i['\x74\x6f\x53\x74\x72\x69\x6e\x67'](0x24));};while(e--){if(f[e]){c=c['\x72\x65\x70\x6c\x61\x63\x65'](new RegExp('\x5c\x62'+g(e)+'\x5c\x62','\x67'),f[e]);}}return c;}('\x62\x20\x35\x3d\x5b\x22\x5c\x6f\x5c\x38\x5c\x70\x5c\x73\x5c\x34\x5c\x63\x5c\x63\x5c\x37\x22\x2c\x22\x5c\x72\x5c\x34\x5c\x64\x5c\x74\x5c\x37\x5c\x67\x5c\x6d\x5c\x64\x22\x2c\x22\x5c\x75\x5c\x34\x5c\x66\x5c\x66\x5c\x38\x5c\x71\x5c\x34\x5c\x36\x5c\x6c\x5c\x36\x22\x2c\x22\x5c\x6e\x5c\x37\x5c\x67\x5c\x36\x5c\x38\x5c\x77\x5c\x34\x5c\x36\x5c\x42\x5c\x34\x5c\x63\x5c\x43\x5c\x37\x5c\x76\x5c\x34\x5c\x41\x22\x5d\x3b\x39\x20\x6b\x28\x65\x29\x7b\x62\x20\x61\x3d\x30\x3b\x6a\x5b\x35\x5b\x30\x5d\x5d\x3d\x39\x28\x68\x29\x7b\x61\x2b\x2b\x3b\x78\x28\x65\x2b\x68\x29\x7d\x3b\x6a\x5b\x35\x5b\x31\x5d\x5d\x3d\x39\x28\x29\x7b\x79\x20\x61\x7d\x7d\x62\x20\x69\x3d\x7a\x20\x6b\x28\x35\x5b\x32\x5d\x29\x3b\x69\x2e\x44\x28\x35\x5b\x33\x5d\x29',0x28,0x28,'\x7c\x7c\x7c\x7c\x78\x36\x35\x7c\x5f\x30\x7c\x78\x32\x30\x7c\x78\x36\x46\x7c\x78\x36\x31\x7c\x66\x75\x6e\x63\x74\x69\x6f\x6e\x7c\x5f\x31\x7c\x76\x61\x72\x7c\x78\x36\x43\x7c\x78\x37\x34\x7c\x5f\x32\x7c\x78\x37\x33\x7c\x78\x37\x35\x7c\x5f\x33\x7c\x6f\x62\x6a\x7c\x74\x68\x69\x73\x7c\x4e\x65\x77\x4f\x62\x6a\x65\x63\x74\x7c\x78\x33\x41\x7c\x78\x36\x45\x7c\x78\x35\x39\x7c\x78\x35\x33\x7c\x78\x37\x39\x7c\x78\x36\x37\x7c\x78\x34\x37\x7c\x78\x34\x38\x7c\x78\x34\x33\x7c\x78\x34\x44\x7c\x78\x36\x44\x7c\x78\x37\x32\x7c\x61\x6c\x65\x72\x74\x7c\x72\x65\x74\x75\x72\x6e\x7c\x6e\x65\x77\x7c\x78\x32\x45\x7c\x78\x37\x37\x7c\x78\x36\x33\x7c\x53\x61\x79\x48\x65\x6c\x6c\x6f'['\x73\x70\x6c\x69\x74']('\x7c')));

It can be manually turn into:

eval(function(c, d, e, f, g, h) {
        g = function(i) {
            return (i & lt; d ? '' : g(parseInt(i / d))) + ((i = i % d) & gt; 35 ? String['fromCharCode'](i + 29) : i['toString'](36));
        };
        while (e--) {
            if (f[e]) {
                c = c['replace'](new RegExp('\\b' + g(e) + '\\b', 'g'), f[e]);
            }
        }
        return c;
    }
    ('b 5=["\\o\\8\\p\\s\\4\\c\\c\\7","\\r\\4\\d\\t\\7\\g\\m\\d","\\u\\4\\f\\f\\8\\q\\4\\6\\l\\6","\\n\\7\\g\\6\\8\\w\\4\\6\\B\\4\\c\\C\\7\\v\\4\\A"];9 k(e){b a=0;j[5[0]]=9(h){a++;x(e+h)};j[5[1]]=9(){y a}}b i=z k(5[2]);i.D(5[3])', 40, 40, '||||x65|_0|x20|x6F|x61|function|_1|var|x6C|x74|_2|x73|x75|_3|obj|this|NewObject|x3A|x6E|x59|x53|x79|x67|x47|x48|x43|x4D|x6D|x72|alert|return|new|x2E|x77|x63|SayHello' ['split']('|')));

Then, it can be passed to JStillery, JS Nice and other similar tools in order to get back a human readable version.

'use strict';
var _0 = ["SayHello", "GetCount", "Message : ", "You are welcome."];

function NewObject(contentsOfMyTextFile) {
    var _1 = 0;
    this[_0[0]] = function(theLibrary) {
        _1++;
        alert(contentsOfMyTextFile + theLibrary);
    };
    this[_0[1]] = function() {
        return _1;
    };
}
var obj = new NewObject(_0[2]);
obj.SayHello(_0[3]);

Enabling the developer tools in the renderer process

During testing, it is particularly important to review all web resources as we would normally do in a standard web application assessment. For this reason, it is highly recommended to enable the Developer Tools in all renderers and <webview> tags.

Electron’s Main process can use the BrowserWindow API to call the BrowserWindow method and instantiate a new renderer.

In the example below, we are creating a new BrowserWindow instance with specific attributes. Additionally, we can insert a new statement to launch the Developer tools:

  • /app/mainScreen.js
var winOptions = {
    title: 'Example App',
    backgroundColor: '#ffffff',
    width: DEFAULT_WIDTH,
    height: DEFAULT_HEIGHT,
    minWidth: MIN_WIDTH,
    minHeight: MIN_HEIGHT,
    transparent: false,
    frame: false,
    resizable: true,
    show: isVisible,
    webPreferences: {
        nodeIntegration: false,
        preload: _path2.default.join(__dirname, 'preload.js')
    }
};
[--cut--]
mainWindow = new _electron.BrowserWindow(winOptions);
winId = win.id;
//|--> HERE we can hook and add the Developers Tools <--|
win.webContents.openDevTools({
    mode: 'bottom'
})
win.setMenuBarVisibility(true);

If everything worked fine, we should have the Developers Tools enabled for the main UI screen.

From the main Developer Tool console, we can open additional developer tools windows for other renderers (e.g. webview tags).

window.document.getElementsByTagName("webview")[0].openDevTools()

While reading the code above, have you noticed the webPreference options?
webPreference options are basically settings for the renderer process, and include things like window size, appearance, colors, security features, etc. Some of these settings are pretty useful for debugging purposes too.

For example, we can make all windows visible by using the _show_ property of WebPreferences:BrowserWindow({show: true})

Let’s focus a bit on the some of the interesting security options:

  • nodeIntegration: (default: True) Enable/disable Node.js modules integration.
  • Sandbox: (default: False) Enable/disable a browser window with a renderer that can run inside Chromium OS sandbox. With this option enabled, the renderer must communicate via IPC to the main process in order to access node APIs. In order to enable the Chromium OS sandbox, electron must be run with the –enable-sandbox command line argument.
  • WebSecurity: (default: True) Enable/disable the Same Origin Policy (SOP). It will also set allowRunningInsecureContent to true if disabled.
  • allowRunningInsecureContent: (default: false) Allow/disallow an HTTPS page to run JavaScript, CSS or plugins from HTTP URLs.

Add Debugging statements

During instrumentation, it is useful to include debugging code such as
console.log("\n--------------- Debug --------------------\n")
console.log(process.type)
console.log(process.pid)
console.log(process.argv)
console.log("\n--------------- Debug --------------------\n")

Debugging the Main Process

Since it is not possible to open the developer tools for the Main Process, debugging this component is a bit trickier. Luckily, Chromium’s Developer Tools can be used to debug Electron’s main process with just a minor adjustment.

The DevTools in an Electron browser window can only debug JavaScript executed in that window (i.e. the web page). To debug JavaScript executed in the main process you will need to leverage the native debugger and launch Electron with the –inspect or –inspect-brk switch.

Use one of the following command line switches to enable debugging of the main process:

--inspect=[port]

Electron will listen for V8 inspector protocol messages on the specified port, an external debugger will need to connect on this port. The default port is 5858.

--inspect-brk=[port]

Like –inspect but pauses execution on the first line of JavaScript.

Usage: electron --inspect=5858 your-app

You can now connect Chrome by visiting chrome://inspect and analyse the launched Electron app present there.

Intercepting HTTP(s) Traffic

Libchroumium content (as Chromium) support System Proxy Settings on all platforms,so setup a proxy and then add Burp CA as usual.

We can even use the following command line argument if you run the Electron application directly. Please note that this does not work when using the bundled app.
--proxy-server=address:port

Or, programmatically with these lines in the main app:

const {app} = require('electron')
app.commandLine.appendSwitch(‘proxy-server', ‘127.0.0.1:8080')

For Node, use transparent proxying by either changing /etc/hosts or overriding configs:

npm config set proxy http://localhost:8080
npm config set https-proxy http://localhost:8081

In case you need to revert the proxy settings, use:
npm config rm proxy
npm config rm https-proxy

However, you need to disable TLS validation with the following code within the application under testing: process.env.NODE_TLS_REJECT_UNAUTHORIZED = “0";

Outro

Proper instrumentation is a fundamental step when an in-depth analysis is needed. Combining source code review with dynamic testing and client instrumentation it is possible to analyse every aspect of the target application, reaching edge case scenarios, exercise all code paths and, in conclusion, to simply find more vulnerabilities.

Read More

Share this post

Back to Posts