65.9K
CodeProject 正在变化。 阅读更多。
Home

使用原生桥保护基于 Electron 的应用程序

starIconstarIconstarIconstarIconstarIcon

5.00/5 (5投票s)

2023年1月16日

CPOL

10分钟阅读

viewsIcon

11794

downloadIcon

131

在本文中,您将学习一种使用 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 中的使用方式非常拙劣,我不太喜欢。我想要一个更好、更简单、更干净的解决方案,并且我做到了。

现在是时候在这篇短文中快速教会您了。**准备好了吗!**

注意

此方法并不能使您的应用程序无法破解,它只是增加了额外的保护层,使得分析和逆向工程更加困难。

首先

在我们开始之前,让我们准备好本文所需的所有东西。我们需要以下内容:

注意

我们需要 *Node.js* 来使用 `Electron-Forge` 并为我们的 Electron 构建自定义图标和文件版本。本文不涉及此内容,您可以阅读 此处的指南

在获取所有这些之后,将 Electron 解压到一个文件夹中,用 7-Zip 打开 `resources\default_app.asar`,然后将内容解压到 `resources\app`。这是为了在您完成后测试我们的代码。只需再次将其打包成 `default_app.asar`。

内部模块

我们需要做的第一件事是创建我们的内部模块,它将执行我们想要的所有关键操作,例如:

  • 加密/解密
  • 以特殊方式与服务器通信
  • 加密消息
  • 存储页面内容*
剧透警告

在我的下一篇文章中,我将向您展示如何制作自己的安全、优化的数据库、文件归档和二进制序列化器。您可以使用它来制作您自己的 HTML 页面数据归档,并直接从您的 C++ 模块和加密归档中设置 Electron 的内容。您还可以使用我之前的文章通过您自己的 PE 包装器来打包内部模块。

内部模块是您应用程序逻辑的核心。您可以使用 Rust 或其他原生语言来构建它,但我更喜欢 C++。

  1. 在 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。

  2. 使用 CFF Explorer 打开 `electron.exe`。在 40MB 限制消息处单击**否**。

  3. 转到**导入添加器 (Import Adder)** 选项卡,单击**添加 (Add)**,选择 `electron_x64.dll`,选择 `_Proxy` 函数,单击**按名称导入 (Import By Name)**,勾选**创建新节 (Create New Section)**,最后单击**重建导入表 (Rebuild Import Table)**。

  4. 保存 EXE 并运行 Electron。您将看到多个消息框弹出。这是因为 Electron 使用多进程模型,每个进程实例都用于特定任务,如渲染、通信等。

  5. 为了解决这个问题,我们在 `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 方法)

  1. HttpLib 头文件添加到您的源代码中:

    #include "httplib/httplib.h"
     

    **提示**:如果您遇到编译器错误,只需将 `#include ` 移到 `#include ` 的上方即可。

  2. 在命名空间之后添加一个服务器值:

    // HTTP Server
    httplib::Server httpSrv;
  3. 使用以下代码创建服务器线程函数:

    // 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
    }
  4. 使用以下代码添加事件初始化函数:

    // 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");
            }
        });
    }
  5. 在 `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,因为它简单而优雅。

  1. 下载 `jquery-3.X.X.min.js` 并在主框架中将其包含在 `internal.js` 之前。
  2. 创建一个 `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;
            }
        });
    }
  3. 运行 Electron,享受您的经典请求/响应系统!

结论

好了,我的又一篇文章到此结束,希望您喜欢并觉得它有用。我不是一名 Web 开发人员,本文中我使用的 JavaScript 代码大部分是我在 Google 上找到的简单结果。 :D

您可以使用以下方法将应用程序的所有关键部分转换为原生代码,并对其进行极强的保护。您可以对其进行打包、虚拟化、混淆或人们用于保护其原生二进制文件的任何操作。此外,您还可以保护您的敏感内容、资源,为 SSL 添加额外的加密等。

您也可以在 CodeProject 上下载完整的源代码。

下次再见!

历史

  • 2023 年 1 月 16 日:初始版本
© . All rights reserved.