使用原生桥保护基于 Electron 的应用程序
在本文中,您将学习一种使用 C++ 原生库来保护基于 Electron 的应用程序中的关键代码逻辑的简单方法。
背景
如今,我们每个人都生活在 **HTML5 + CSS3** 的世界里。我们中的许多人每天都会访问数百个网页并使用许多 Web 应用程序。使用 HTML5 构建应用程序非常容易且有趣。网上有如此多的组件和主题可以加快工作流程。在前端开发中,HTML5 因其灵活性且可跨平台发布,已成为最受欢迎的选择之一。
市面上有许多基于框架的项目,可以帮助我们快速开始构建应用程序。
其中一个著名且流行的框架是 **Electron**。Electron 结合了 **Chromium、Node.js、V8 JavaScript 虚拟机**,为开发者提供了一个强大的工具。许多著名的项目和服务都使用 Electron 作为其框架。**Discord、Typora、Medal.TV、Visual Studio Code 和 Unity Hub** 都是使用 HTML5 和 CSS3 在 Electron 中构建的,是不是很棒?
然而,Electron 在生产环境中存在许多可能令人头疼的问题。例如,我个人很不满意仅为一个小型应用程序(如安装程序或在线服务)就发布一个 250MB 以上的应用程序。您可以通过用原生操作系统 Web 浏览器替换 Electron 中的 Chromium 来解决此问题。
Electron 的另一个严重问题是**代码的安全性**。使用 Electron,您需要知道**您的代码总是暴露在外**。如果您的应用程序需要密钥激活系统,它可以在一分钟内被破解。您所需要的只是获取 7-Zip 的 ASAR 插件并编辑您想要的内容。
概述
所以,我产生了一个小想法。如果我将代码的关键部分放在 C++ 模块中,并在那里处理所有逻辑,而不是使用 JavaScript,而仅将 Electron 用作前端,会怎么样?这听起来是个好主意,不是吗?问题是 Electron 本身不支持自定义 DLL 和模块。要从 Electron 调用本地 DLL,您需要使用 node-ffi 库,这个库……在 2018 年后就没有更新过了,为新的 Node.js 构建它非常麻烦。而且您在 Electron 中的使用方式非常拙劣,我不太喜欢。我想要一个更好、更简单、更干净的解决方案,并且我做到了。
现在是时候在这篇短文中快速教会您了。**准备好了吗!**
注意此方法并不能使您的应用程序无法破解,它只是增加了额外的保护层,使得分析和逆向工程更加困难。
首先
在我们开始之前,让我们准备好本文所需的所有东西。我们需要以下内容:
- Electron 预编译二进制文件 (下载/页面)
- CFF Explorer (下载)
- Visual Studio 2019/2022 (C++ 构建工具链)
- 7-Zip + ASAR 插件 (下载/页面)
- Node.js + npm (下载/页面)*
注意我们需要 *Node.js* 来使用 `Electron-Forge` 并为我们的 Electron 构建自定义图标和文件版本。本文不涉及此内容,您可以阅读 此处的指南。
在获取所有这些之后,将 Electron 解压到一个文件夹中,用 7-Zip 打开 `resources\default_app.asar`,然后将内容解压到 `resources\app`。这是为了在您完成后测试我们的代码。只需再次将其打包成 `default_app.asar`。
内部模块
我们需要做的第一件事是创建我们的内部模块,它将执行我们想要的所有关键操作,例如:
- 加密/解密
- 以特殊方式与服务器通信
- 加密消息
- 存储页面内容*
剧透警告在我的下一篇文章中,我将向您展示如何制作自己的安全、优化的数据库、文件归档和二进制序列化器。您可以使用它来制作您自己的 HTML 页面数据归档,并直接从您的 C++ 模块和加密归档中设置 Electron 的内容。您还可以使用我之前的文章通过您自己的 PE 包装器来打包内部模块。
内部模块是您应用程序逻辑的核心。您可以使用 Rust 或其他原生语言来构建它,但我更喜欢 C++。
-
在 Visual Studio 中创建一个新的 C++ 项目,将构建模式设置为**动态库(*.dll)**,并添加以下代码:
#include <Windows.h> #include <string> // Verify And Execute BOOL APIENTRY DllMain(HMODULE hModule, DWORD reason, LPVOID reserved) { if (reason == DLL_PROCESS_ATTACH) { MessageBoxA(0, "Hello from Electron!", 0, 0); } return TRUE; } // Proxy Export extern "C" _declspec(dllexport) void _Proxy() {}
构建它,现在您就有了 `electron_x64.dll`。现在我们修补我们的 Electron EXE 文件,并在其导入地址表 (IAT) 中添加一个代理导入。这将导致 Electron 在启动后加载我们的 DLL。
-
使用 CFF Explorer 打开 `electron.exe`。在 40MB 限制消息处单击**否**。
-
转到**导入添加器 (Import Adder)** 选项卡,单击**添加 (Add)**,选择 `electron_x64.dll`,选择 `_Proxy` 函数,单击**按名称导入 (Import By Name)**,勾选**创建新节 (Create New Section)**,最后单击**重建导入表 (Rebuild Import Table)**。
-
保存 EXE 并运行 Electron。您将看到多个消息框弹出。这是因为 Electron 使用多进程模型,每个进程实例都用于特定任务,如渲染、通信等。
-
为了解决这个问题,我们在 `DLL_PROCESS_ATTACH` 中通过比较命令行数据来简单地进行检查:
std::string cmd = GetCommandLineA(); char pathBuffer[MAX_PATH]; GetModuleFileNameA(0, pathBuffer, sizeof pathBuffer); std::string moduleName(pathBuffer); moduleName.insert(moduleName.begin(), '"'); moduleName += '"'; if(cmd[cmd.size()-1] == ' ') cmd = cmd.substr(0, cmd.size() - 1); // It's All Good if (cmd == moduleName) { MessageBoxA(0, "Hello from Electron!", 0, 0); }
现在运行 Electron,您将看到我们的消息框只从主实例显示一次。我们完成了。现在我们正式进入 Electron 进程了!
开放世界通信
为了在 Electron 和我们的原生内部模块之间进行通信,我们将使用 **WebSocket/HTTP** 连接。它不需要是安全的,因为它只向内部模块发送命令并检索结果。
为了保持简洁且不弄得脏兮兮,我们需要一个用 C 制作的非常小的 `WebSocket` 服务器,无 SSL,不加任何额外的东西。在哪里能找到这样的东西?嗯……我进行了长时间的搜索,找到了这个 宝贝。
WebSocket 不支持按需响应。这意味着当您向服务器发送内容时,服务器不会将数据返回给您。因此,我们使用**自定义请求模型**。我们简单地创建一个包含唯一 ID 的请求列表。我们在 `Request` 和 `Response` 中都包含它。然后执行我们检索到的数据。或者,您可以使用一个简单的小型 HTTP 服务器,例如 这个很棒且轻量级的库。通过使用 HTTP,您可以按需发送请求并检索数据。
克隆 `wsServer` 仓库并将其添加到您的内部模块中。
在您的 DLL 代码中添加以下事件:
#include "wsSrv/ws.h"
// Websocket Server Events
void OnClientConnect(ws_cli_conn_t* client)
{
char* cli = ws_getaddress(client);
printf("[LOG] Client Connected, Endpoint Address: %s\n", cli);
}
void OnClientDisconnect(ws_cli_conn_t* client)
{
char* cli = ws_getaddress(client);
printf("[LOG] Client Disconnected, Endpoint Address: %s\n", cli);
}
void OnClientRequest(ws_cli_conn_t* client, const unsigned char* msg,
uint64_t size, int type)
{
char* cli = ws_getaddress(client);
printf("[LOG] Client Sent Request, Endpoint Address: %s\n", cli);
if (type == 1) // Data is Text Message
{
std::string msgstr((char*)msg, size);
// Handle Commands
if (msgstr == "(ON_STARTUP)")
{
ws_sendframe_txt(client, "(LOAD_SPLASH_PAGE)");
return;
}
}
}
您还需要 Windows 的 `pthread`。只需从 此仓库获取它,并将其构建为 **MT/静态**库。
将您的 DLL 链接到 `ws2_32.lib` 和 `pthreadVSE3.lib`,然后为此函数添加服务器创建:
// Server Thread
void StartWebsocketServer()
{
Sleep(200);
/* Register events. */
struct ws_events evs;
evs.onopen = &OnClientConnect;
evs.onclose = &OnClientDisconnect;
evs.onmessage = &OnClientRequest;
// Start Server
ws_socket(&evs, 5995, 0, 1000); // 5995 is the Port, you may want
// to check the port before listening
}
现在转到您的 `Dllmain`,进行以下更改:
if (cmd == moduleName)
{
::SetThreadDpiAwarenessContext(DPI_AWARENESS_CONTEXT_SYSTEM_AWARE);
::SetProcessDPIAware();
CreateThread(NULL, NULL, (LPTHREAD_START_ROUTINE)StartWebsocketServer,
NULL, NULL, NULL);
}
一切就绪。现在,当 Electron 启动时,您的内部模块会创建一个微小的 WebSocket 服务器并等待连接。
注意您可能需要在**预处理器定义 (Preprocessor Definition)** 中添加 `_CRT_SECURE_NO_WARNINGS` 和 `PTW32_STATIC_LIB`。
主框架
为了能够动态加载您的网页,您需要一个主框架。这个主框架可以用作覆盖整个 HTML 的代理页面,或者可以包含一个宿主元素,用于在其中编写新的 HTML 代码。在本文中,我们覆盖整个页面。
转到 Electron 数据目录下的 `app` 文件夹,然后在文本编辑器/HTML 编辑器中打开 `index.html`,并写入以下 HTML 代码。我使用的是 **Visual Studio Code**。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
<title>My Application Name</title>
<meta content="width=device-width, initial-scale=1.0,
shrink-to-fit=no" name="viewport">
<meta name="keywords" content="application, webapp, html5, css3, cpp">
<meta name="description" content="My Application Mainframe">
<meta itemprop="name" content="Mainframe">
<meta itemprop="description" content="My Application Mainframe">
<link rel="stylesheet" href="baseStyle.css">
<script src="internal.js"></script>
</head>
<body class="mainframe-body" onload="StartupEvent();">
<div id="mainframe">
<div id="mainframe-host"></div>
</div>
</body>
</html>
现在我们有一个空的页面可以加载内容。如果您的所有页面都使用相同的样式和布局,您只需要在 `mainframe-host` 区域内从内部模块加载新内容。
在 `app` 文件夹中创建一个 `baseStyle.css` 文件,并在编辑器中打开它。写入以下样式代码:
.mainframe-body
{
-webkit-user-select: none; user-select: none;
}
#mainframe
{
position: fixed;
top: 0; left: 0; bottom: 0; right: 0;
overflow: auto; background-color: transparent;
}
#mainframe-host
{
width: 100%;
height: 100%;
}
控制中心
现在,是时候创建我们的跨页面脚本了,它将由主框架页面加载,并创建一个到内部服务器的 WebSocket 连接来控制我们的前端。创建 `internal.js` 文件并在其中写入以下代码:
/* Values */
var socket;
var connected = false;
/* Page Events */
function StartupEvent()
{
socket = new WebSocket("ws://:5995");
/* WebSocket Events */
socket.onopen = function(e)
{
socket.send("(ON_STARTUP)");
connected = true;
};
socket.onmessage = function(event)
{
/* Handle Commands */
if(event.data == "(LOAD_SPLASH_PAGE)")
{
// Note : Also we can set this data from internal module
// if it's critical page
fetch('./splash.html').then(response => response.text()).then(text =>
{
document.open(); document.write(text); document.close();
})
}
};
socket.onclose = function(event)
{
connected = false;
};
socket.onerror = function(error)
{
connected = false;
};
}
/* Websocket Functions */
function DisconnectWS()
{
if (connected) socket.close();
}
好的,创建您的 `splash.html` 并运行 Electron,**然后,瞧……** 现在我们已经与内部模块建立了连接!
(鸣谢:免费模板 TheEvent)
注意您不应该使用 `href` 来跳转到新页面。如果您这样做,当新页面以新进程打开时,`internal.js` 的上下文将被销毁。您可以这样做,但需要重新连接到 WebSocket,然而,在 `WebSocket` 方法中不推荐这样做。
提示如果在页面切换时遇到闪烁,请将 `body` 的 `opacity` 设置为 `0`,并在页面加载时添加 `onLoad` 事件,并在页面完全加载后将 `opacity` 恢复。
请求/响应系统
我们已经实现了双向连接。现在我们需要一个小的请求/响应系统来远程执行我们的函数并获取数据。
对于请求/响应系统,我们在 C++ 和 JavaScript 中都使用 JSON。要在 C++ 中解析 JSON 数据,我们使用 这个 非常棒的单头文件、易于使用的库。
让我们先编写请求系统,打开 `internal.js` 并在值后面写入以下代码:
/* Structs */
class RequestMetadata
{
constructor(requestId, onResponse)
{
this.requestId = requestId;
this.onResponse = onResponse;
}
}
/* Request List*/
var requests = [];
- `RequestMetadata`:我们创建一个简单的类用作 `struct`。它包含一个唯一的请求 ID 和一个处理响应结果的函数。
- `requests`:我们创建一个简单的数组,用于存储请求直到收到响应,收到响应后,我们将其从数组中移除。
现在让我们创建我们的请求生成器函数,它将负责创建请求和处理响应:
/* Websocket Based Functions */
function RequestDataFromInternal()
{
// Response Function
function onResponseEvent(responseData)
{
document.getElementById("center_text").innerHTML = responseData;
return true;
}
// Create Request And Send it
var requestId = GetANewRequestID();
var requestMeta = new RequestMetadata(requestId, onResponseEvent);
requests.push(requestMeta);
socket.send(JSON.stringify({id:requestId, funcId:1001,
requestStringID:"CENTER_TITLE"}));
}
/* Utilities */
function GetANewRequestID()
{
return Math.random() * (999999 - 111111) + 111111;
}
我们的请求布局包含两个**必需**参数:
- `id`:一个唯一的 ID,用于标识请求,并将包含在响应中。
- `funcId`:一个唯一的 ID,用于标识函数的本质。
其余的是 C++ 代码中函数的参数,可以是任何内容,数字、字符串等。
最后,我们添加响应执行器。进入 `onmessage` 事件并写入以下代码:
/* Handle Commands */
if(event.data == "(LOAD_SPLASH_PAGE)")
{
fetch('./splash.html').then(response => response.text()).then(text =>
{
document.open(); document.write(text); document.close();
})
return;
}
/* Handle Responses */
var response = JSON.parse(event.data);
requests.forEach((request) =>
{
if(Math.floor(request.requestId) === Math.floor(response.responseID))
{
var result = request.onResponse(response.responseData);
/* Remove Request*/
requests = RemoveRequestMeta(request);
}
});
...
/* Utilities */
function RemoveRequestMeta(value)
{
var index = requests.indexOf(value);
if (index > -1) requests.splice(index, 1);
return requests;
}
完成了!您可以使用 7-Zip 将所有内容打包成 `default_app.asar`。现在是时候制作我们的 C++ 响应系统了……
在内部模块代码中,包含 `json.hpp`:
#include "jsonpp/json.hpp"
现在转到 `OnClientRequest` 事件并写入以下代码:
// Handle Requests
auto jsonData = json::parse(msgstr);
int requestID = jsonData["id"].get<int>();
int functionID = jsonData["funcId"].get<int>();
// Handle Functions
if (functionID == 1001)
{
std::string requestData = jsonData["requestStringID"].get<std::string>();
// Create Response
json response;
response["responseID"] = requestID;
if (requestData == "CENTER_TITLE")
response["responseData"] = "Hey!<br><span>Electron</span> Welcomes you!";
if (requestData == "CENTER_ALTERNATIVE_TITLE")
response["responseData"] = "THIS IS A DEMO<br><span>WEB PAGE</span>
For CodeProject";
// Send Response Back
ws_sendframe_txt(client, response.dump().c_str());
}
构建内部模块并运行 Electron,**恭喜!** 您已经创建了您的请求/响应系统!
回归经典!(奖励)
好的,我知道您到目前为止都很喜欢这篇文章。所以这里有一个关于创建经典 HTTP 请求/响应系统的奖励!
首先,删除 WebSocket 的伪影和文件,或者您可以保留它们,同时拥有 WebSocket 和 HTTP。请记住,WebSocket 是双向的,您的内部模块可以从服务器接收数据、解密它并调用 Electron 中的任何内容,但使用 HTTP 时,Electron 始终需要先发送数据。
C++ 端(URL 方法)
-
将 HttpLib 头文件添加到您的源代码中:
#include "httplib/httplib.h"
**提示**:如果您遇到编译器错误,只需将 `#include
` 移到 `#include ` 的上方即可。 -
在命名空间之后添加一个服务器值:
// HTTP Server httplib::Server httpSrv;
-
使用以下代码创建服务器线程函数:
// Server Thread void StartHttpServer() { Sleep(200); // Create Events InitializeServerEvents(); // Start Server httpSrv.listen("localhost", 5995); // 5995 is the Port, // You may want to check the port before listening }
-
使用以下代码添加事件初始化函数:
// Http Server Events void InitializeServerEvents() { httpSrv.Get("/requestCenterText", [](const httplib::Request& req, httplib::Response& res) { if (req.has_param("requestStringID")) { std::string requestData = req.get_param_value("requestStringID"); // Create Response if (requestData == "CENTER_TITLE") res.set_content("Hey!<br><span>Electron</span> Welcomes you!", "text/html"); if (requestData == "CENTER_ALTERNATIVE_TITLE") res.set_content("THIS IS A DEMO<br><span>WEB PAGE</span> For CodeProject", "text/html"); } }); }
-
在 `DllMain` 中更新 `CreateThread`:
CreateThread(NULL, NULL, (LPTHREAD_START_ROUTINE)StartHttpServer, NULL, NULL, NULL);
JavaScript 端(URL 方法)
打开 `internal.js` 并将其更改为如下代码:
/* Page Events */
function StartupEvent()
{
fetch('./splash.html').then(response => response.text()).then(text =>
{
document.open(); document.write(text); document.close();
})
}
/* Http Based Functions */
function RequestDataFromInternal()
{
fetch('https://:5995/requestCenterText?requestStringID=CENTER_TITLE')
.then(response => response.text()).then(responseData =>
{
document.getElementById("center_text").innerHTML = responseData;
})
}
现在您可以测试代码并看到与 `WebSocket` 方法相同的结果!
C++ 端(POST 方法)
为了使此方法更好、更灵活,我们通过实现 `POST` 方法来改进它。
转到您的 `InitializeServerEvents` 函数并添加以下代码:
httpSrv.Post("/remoteNative", [](const httplib::Request& req, httplib::Response& res)
{
auto jsonData = json::parse(req.body);
int functionID = jsonData["funcId"].get<int>();
// Handle Functions
if (functionID == 1001)
{
std::string requestData = jsonData["requestStringID"].get<std::string>();
// Create Response
json response;
response["responseData"] = "INVALID_STRING_ID";
if (requestData == "CENTER_TITLE")
response["responseData"] = "This is a <br><span>response</span>
from POST method!";
// Set Response
res.set_content(response.dump(), "text/html");
}
});
JavaScript 端(POST 方法)
为了使用 `POST` 方法,我们可以采用不同的方式,但在本文中,我使用 jQuery Ajax,因为它简单而优雅。
- 下载 `jquery-3.X.X.min.js` 并在主框架中将其包含在 `internal.js` 之前。
- 创建一个 `ajax` 请求并处理响应:
function RequestDataUsingPost() { $.ajax({ type: 'post', url: 'https://:5995/remoteNative', data: JSON.stringify({funcId:1001, requestStringID:"CENTER_TITLE"}), contentType: "application/json; charset=utf-8", traditional: true, success: function (data) { var response = JSON.parse(data); document.getElementById("center_text").innerHTML = response.responseData; } }); }
- 运行 Electron,享受您的经典请求/响应系统!
结论
好了,我的又一篇文章到此结束,希望您喜欢并觉得它有用。我不是一名 Web 开发人员,本文中我使用的 JavaScript 代码大部分是我在 Google 上找到的简单结果。 :D
您可以使用以下方法将应用程序的所有关键部分转换为原生代码,并对其进行极强的保护。您可以对其进行打包、虚拟化、混淆或人们用于保护其原生二进制文件的任何操作。此外,您还可以保护您的敏感内容、资源,为 SSL 添加额外的加密等。
您也可以在 CodeProject 上下载完整的源代码。
下次再见!
历史
- 2023 年 1 月 16 日:初始版本