工具化Electron应用进行安全测试
随着Electron框架的日益普及,我们撰写了本文来总结几种可用于对基于Electron的应用进行工具化、改变其行为并执行深度安全评估的技术。
Electron与进程
Electron框架用于开发跨平台桌面应用程序,仅需使用HTML、JavaScript和CSS。它有两个核心组件:Node.js和来自Chromium项目的libchromiumcontent模块。
在Electron中,主进程是运行package.json主脚本的进程。该组件可访问Node.js原语,并负责启动其他进程。Chromium用于显示网页,这些页面在称为渲染器进程的单独进程中渲染。
与常规浏览器中网页在沙盒环境中运行且无法访问原生系统资源不同,Electron渲染器可以访问Node.js原语,并允许与底层操作系统进行更低级别的集成。Electron公开了对原生Node.js API的完全访问权限,同时也便于使用外部Node.js NPM模块。
正如您可能从近期公开的安全漏洞中了解到的那样,由于JavaScript代码可以访问文件系统、用户shell及更多原语,其安全影响是巨大的。随着授予应用程序代码的额外权限,固有的安全风险也会增加。例如,在非隔离的渲染器中显示来自不可信源的任意内容是一个严重的安全风险。您可以在官方安全建议文档中阅读更多关于Electron安全、加固和漏洞预防的内容。
解包ASAR存档
检查基于Electron的应用程序源代码的第一件事是解包应用程序捆绑包(.asar文件)。ASAR存档是一种简单的类似tar的格式,将文件连接成单个文件。
首先定位我们应用的主ASAR存档,通常命名为core.asar或app.asar。
一旦我们有了这个文件,就可以继续安装asar实用程序:
并解压整个存档:
1
|
asar extract core.asar destinationfolder
|
在最简单的版本中,一个Electron应用程序包含三个文件:index.js、index.html和package.json。
我们检查的第一个目标是package.json文件,因为它包含了应用程序“入口点”文件路径:
1
2
3
4
5
6
|
{
"name": "Example App",
"description": "Core App",
"main": "app/index.js",
"private": true
}
|
在我们的示例中,入口点是app文件夹内名为index.js的文件,它将作为主进程执行。如果未指定,index.js是默认的主文件。index.html文件和其他Web资源在渲染器进程中用于向用户显示实际内容。主进程中实例化的每个browserWindow都会创建一个新的渲染器进程。
为了能够在喜欢的IDE中跟踪函数和方法,建议解析应用的依赖关系:
我们还应该安装Devtron,这是一个用于检查、监控和调试Electron应用的工具(构建在Chrome开发者工具之上)。要使Devtron工作,必须开启NodeIntegration。
1
|
npm install --save-dev devtron
|
然后,在开发者工具的控制台选项卡中运行以下命令:
1
|
require('devtron').install()
|
处理混淆的JavaScript
当应用程序既未压缩也未混淆时,我们可以轻松检查代码。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
'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 -- ]
|
如果遇到混淆情况,没有万能药可以展开严重操纵的JavaScript代码。在这些情况下,需要结合自动工具和手动逆向工程来恢复原始源代码。
以这段糟糕的JS代码为例:
1
|
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')));
|
可以手动转换为:
1
2
3
4
5
6
7
8
9
10
11
|
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和其他类似工具,以获取人类可读的版本。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
'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应用程序评估中通常做的那样。因此,强烈建议在所有渲染器和标签中启用开发者工具。
Electron的主进程可以使用BrowserWindow API调用BrowserWindow方法并实例化新的渲染器。
在下面的示例中,我们正在创建一个具有特定属性的新BrowserWindow实例。此外,我们可以插入一条新语句来启动开发者工具:
/app/mainScreen.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
|
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;
//|--> 在此处我们可以挂钩并添加开发者工具 <--|
win.webContents.openDevTools({ mode: 'bottom' })
win.setMenuBarVisibility(true);
|
如果一切正常,我们应该为主UI屏幕启用了开发者工具。
从主开发者工具控制台,我们可以为其他渲染器(例如webview标签)打开额外的开发者工具窗口。
1
|
window.document.getElementsByTagName("webview")[0].openDevTools()
|
在阅读上面的代码时,您是否注意到了webPreference选项?
WebPreferences选项基本上是渲染器进程的设置,包括窗口大小、外观、颜色、安全功能等。其中一些设置对于调试目的也非常有用。
例如,我们可以使用WebPreferences的show属性使所有窗口可见:
1
|
BrowserWindow({show: true})
|
添加调试语句
在工具化过程中,包含调试代码非常有用,例如:
1
2
3
4
5
|
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只能调试在该窗口中执行的JavaScript(即网页)。要调试在主进程中执行的JavaScript,您需要利用原生调试器并使用–inspect或–inspect-brk开关启动Electron。
使用以下命令行开关之一启用主进程的调试:
用法:
1
|
electron --inspect=5858 your-app
|
您现在可以通过访问chrome://inspect连接Chrome,并分析那里启动的Electron应用。
拦截HTTP(s)流量
Chromium在所有平台上支持系统代理设置,因此设置代理并像往常一样添加Burp CA。
如果您直接运行Electron应用程序,甚至可以使用以下命令行参数。请注意,这在使用捆绑应用程序时无效。
1
|
--proxy-server=address:port
|
或者,通过主应用程序中的以下代码以编程方式实现:
1
2
|
const {app} = require('electron')
app.commandLine.appendSwitch('proxy-server', '127.0.0.1:8080')
|
对于Node,通过更改/etc/hosts或覆盖配置使用透明代理:
1
2
|
npm config set proxy http://localhost:8080
npm config set https-proxy http://localhost:8081
|
如果需要恢复代理设置,请使用:
1
2
|
npm config rm proxy
npm config rm https-proxy
|
但是,您需要使用以下代码在测试中的应用程序内禁用TLS验证:
1
|
process.env.NODE_TLS_REJECT_UNAUTHORIZED = "0";
|
结语
适当的工具化是执行全面安全测试的基本步骤。结合源代码审查、动态测试和客户端工具化,可以分析目标应用的每个方面。这些简单的技术使我们能够触及边缘情况,执行所有代码路径,并最终发现漏洞。
@voidsec @lucacarettoni
延伸阅读