原文: https://blog.doyensec.com/2018/07/19/instrumenting-electron-app.html
随着Electron框架的快速流行与发展,我们分享了此篇文章来总结一些技巧,用于对基于Electron框架应用进行安全测试和深入的安全评估。
Electron框架通过仅仅适用HTML,Javascript和CSS技术来构建跨平台的桌面应用。它有两个核心组件:Node.js
和 来自Chromium项目的 libchromiumcontent
模块。
在Electron中,运行package.json
中的main
脚本的进程被称为主进程,该进程能访问Node.js原语,并负责启动其他进程。Chromium则被用于展示Web网页,其中每个Web页面都在独立的渲染进程中进行渲染。
常规的浏览器中Web页面通常在一个沙盒环境中运行,不允许去访问原生的资源。但与之不同,Electron在Node.js的API支持下,可以在页面中和操作系统进行一些底层交互,同时也方便了对Node.js NPM模块的调用。
正如你从最近公开的安全事件中猜测的一样,由于Javascript代码可以访问文件系统,用户shell,以及更多的权限等,Electron存在很大的潜在安全隐患。随着更多额外功能的加入,安全隐患也会随之增加。比如,在非隔离渲染器(non-isolated renderer)中渲染来自不可信来源的任意内容就是一个很严重的安全风险。你可以在官方安全建议文档中读到更多关于Electron安全,加固和漏洞预防的信息。
审计Electron应用程序的第一步是解压缩应用程序包(.asar file)。ASAR存档是一种类似tar,将文件打包的格式。
首先,需要确定我们应用程序的主要ASAR存档, 通常会被命名为core.asar或者app.asar
获取到该文件后,我们可以安装asar工具:
npm install -g asar
然后对之前的.asar文件进行提取:
asar extract core.asar destinationfolder
在最简单的应用版本中,一个Electron文件包含下面三个文件:index.js
,index.html
,package.json
。
我们检查的第一个目标是package.json
,其中包含了所有应用入口点的对应文件路径:
{
"name": "Example App",
"description": "Core App",
"main": "app/index.js",
"private": true,
}
如上例子,入口点是位于app文件夹中名为index.js的文件,该文件将会作为主进程执行。如果没有特别的指定,index.js是默认的主文件。文件index.html和其他的web资源被用在渲染进程中,用来展示真实的内容给用户。一个新的渲染进程(renderer process)在主进程(main process)实例化每一个browserWindow时被创建。
为了能在IDE中使用/跟踪函数(functions)和方法(methods),最好解决一下应用的依赖问题:
npm install
同时我们也安装一下Devtron
,一个基于Chrome Developer Tools的工具,可以用来拦截,监控和调试我们的Electron应用。为了使用Devtron,需要开启NodeIntegration:
npm install --save-dev devtron
然后在Developer Tools的控制台Console
中运行下面命令:
require('devtron').install()
当应用没有经过压缩或者混淆时,我们可以轻松地审计代码:
'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 -- ]
在代码被混淆的情况下,没有统一的方案来解决这些被混淆的代码。在这些情况下就需要结合自动化工具和人工逆向来获得源代码。
以下面这可怕的JS代码为例:
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')));
它可以被转换成:
eval(function (c, d, e, f, g, h) {
g = function (i) {
return (i < d ? '' : g(parseInt(i / d))) + ((i = i % d) > 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']('|')));
然后,再通过JStillery, JS Nice等其他类似工具来获得可读性较强的代码:
'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]);
在测试中,如在正规Web应用测试中一样检查所有Web相关资源是及其重要的。因此非常建议在所有的渲染器和<webview>
标签中开启开发者工具(Developer Tools)功能。
Electron的主进程(Main process)可以通过BrowserWindow API调用BrowserWindow方法并实例化一个新的渲染器。
在下面的例子中,我们创建了一个新的具有特定属性的BrowserWindow实例。我们可以插入一条语句来启动开发者工具(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);
如果一切顺利,在主UI界面上能看到Developers Tools。
从主界面Developers Tools的控制台,可以为其他渲染器(比如,webview标签)开启其他的开发者工具窗口。
window.document.getElementsByTagName("webview")[0].openDevTools()
在阅读上面的代码时你是否注意到了webPreference选项?
WebPreferences选项用于对渲染器的基本设置,包括了如窗口大小(window size),外观(appearance),颜色(colors),安全功能(security features)等。其中的一些选项对调试非常有用。
比如,我们可以用WebPreferences的show属性来让所有的窗口可见:
BrowserWindow({show: true})
在测试过程中,包含如下的调试代码是很有用的:
console.log("\n--------------- Debug --------------------\n")
console.log(process.type)
console.log(process.pid)
console.log(process.argv)
console.log("\n--------------- Debug --------------------\n")
因为无法打开主进程的开发人员工具,因此调试这个主件有点棘手。但幸运的是,只要对Chromium的开发人员工具进行一些调整,就能完成对Electron主进程的调试。
Electron浏览器窗口中的Devtools只能调试在当前窗口(比如,web页面)中执行的Javascript代码。为了调试在主进程中的Javascript代码,你需要使用本机的调试器并通过--inspect
或者--inspect-brk
来启动Electron程序。
使用下面任一命令行来开启对主进程的调试:
–inspect=[port]
Electron将在指定的端口上监听V8监听器协议。外部的调试器则需要连接到这个端口,默认端口为5858。
–inspect-brk=[port]
如 –inspect 选项,但会在第一行的JS代码上暂停执行。
Usage: electron --inspect=5858 your-app
现在就可以通过访问chrome://inspect
来连接Chrome并分析其中已经启动的Electron程序。
Chromium支持跨平台的系统代理协议。所以照常设置代理,并安装Burp CA 证书。
如果直接运行Electron程序,我们甚至可以直接使用如下的命令行参数。但请注意对于捆绑的app,该方法并不起作用:
--proxy-server=address:port
或者,在主app中添加如下代码:
const {app} = require('electron')
app.commandLine.appendSwitch('proxy-server', '127.0.0.1:8080')
对于Node,通过更改/etc/hosts
或覆盖其他配置(如下)来实现透明代理:
npm config set proxy http://localhost:8080
npm config set https-proxy http://localhost:8081
在某些情况下若要还原代理环境,则:
npm config rm proxy
npm config rm https-proxy
但是,你要在正在测试的应用程序中禁用掉TLS验证:
process.env.NODE_TLS_REJECT_UNAUTHORIZED = “0";
适当的工具是开展全面的测试的基础。通过结合源代码审计和动态工具以及客户端工具,可以对目标应用的各个方面进行研究分析。这些简单的技术允许我们触及各种边界情况,测试所有的代码路径并最终发现漏洞。
@voidsec @lucacarettoni