Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / security

Protecting Electron Based Applications by Using a Native Bridge

5.00/5 (5 votes)
16 Jan 2023CPOL10 min read 11.1K   91  
In this article, you will learn an easy way to protect your critical code logic in electron-based applications using a C++ native library.
Another serious issue of electron is Security of your Code. By using electron, you need to know your codes are always naked. If your application needs a key activation system, it can be cracked in less than one minute, all you need to do is get ASAR plugin for 7-Zip and edit whatever you want...

Background

Nowadays, everybody is dealing with HTML5 + CSS3 in their lives. Many of us visit hundreds of web pages and use many web apps every day. Making an app using HTML5 is very easy and fun. There are so many components and themes available online to speed up the workflow. In front-end development, HTML5 now is one of the most favorites since it's flexible and can be shipped as cross-platform.

There are many framework based projects out there which help us to start making our application in no time.

One of the famous and popular ones is Electron. Electron combines Chromium, Node.js, V8 JavaScript Virtual Machine to deliver a powerful tool to the developers. Many famous projects and services are using electron as their framework. Discord, Typora, Medal.TV, Visual Studio Code and Unity Hub are made in electron using HTML5 and CSS3, Pretty awesome stuff, right?

However, Electron has many issues which can be problematic in production level. For example, I personally hate to ship a 250+ MB app just for a small application like an installer or online service, You can fix this issue by replacing chromium in Electron with native OS web browser.

Another serious issue of electron is Security of your Code. By using electron, you need to know Your codes are always naked. If your application needs a key activation system, it can be cracked in less than one minute. All you need to do is getting ASAR plugin for 7-Zip and edit whatever you want.

Overview

So I had this tiny idea. What if I put critical parts of my code in a C++ module and do all the logic there instead of using JavaScript and use electron as my front-end only. Looks like a good idea, no? The problem is electron doesn't support custom DLLs and modules natively. To calling a native DLL from electron, you need to use node-ffi library which, well... did not get any updates after 2018 and building it for new Node.js is a pain in the ass. And the way you need to use it in electron is just dirty, I did not like it, I wanted a Better, Easier and Clean solution and I made it.

Now it's time to teach it to you real quick in this small article. Get Ready!

Note

This method doesn't make your application uncrackable, It just adds extra protection layers which make it harder to analyze and reverse engineering.

First Things First

Before we start, let's prepare everything we need for this article. Here's what we need:

Note

We need Node.js to use Electron-Forge and build our electron with custom icon and file version, I do not cover it in this article, You can read the guide here.

After you got them all, Extract electron into a folder, open resources\default_app.asar with 7-Zip and extract the content to resources\app. This is for testing our code after you are done. Simply package it to default_app.asar again.

Internal Module

First thing we need to do is create our internal module which does all the critical things we want, like:

  • Encryption/Decryption
  • Communicating with Server in Special Ways
  • Ciphering Messages
  • Holding the Content of Pages*
Spoiler Alert

In my next article, I will show you how to make your own secured, optimized database, file archive and binary serializer, You can use it to make your own data archive of html pages and set the electron content directly from your C++ module and encrypted archive. You can also use my previous articles to pack the internal module by your own PE packer.

Internal module is the core of your app logic. You can use rust or other native languages to make it, but I prefer C++.

  1. Create a new C++ project in Visual Studio, Set the build mode to Dynamic Library (.dll) and add this following code:

    C++
    #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() {}

    Build it and now you have your electron_x64.dll. Now we patch our electron EXE file and add a proxy import to its Import Address Table (IAT). This will cause electron to load our DLL after it gets launched.

  2. Open electron.exe with CFF Explorer. Click on No at 40MB limit message.

  3. Head to Import Adder tab and click on Add, select electron_x64.dll, select _Proxy function, click on Import By Name, check Create New Section and finally click on Rebuild Import Table.

    Image 1

  4. Save EXE and run electron. You will see multiply message boxes show up. This is because electron uses multi process model and each process instance is for a special task like rendering, communication, etc.

  5. To fix this, we simply do a check in our DLL_PROCESS_ATTACH by comparing command line data:

    C++
    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);
    }

    Now run electron and you will see our message box only shows once from main instance. We're done. Now we are officially inside electron process!

Open World Communication

To communicate between electron and our native internal module, we will use a WebSocket/HTTP connection. It doesn't need to be secured as it only send commands to internal module and retrieves the result.

As we want to keep it clean and don't get dirty, we need a very tiny small WebSocket server made in C, no SSL, not a single extra thing. Where can we find such a thing? Well... I did take a long search and found this gold.

WebSockets don't support response on demand. It means when you send something to server, Server doesn't return data back to you, Due to this reason, we use a Custom Request Model. We simply make a list of requests with unique ID. We include it in both Request and Response. Then execute the data we retrieved. Alternatively, you can use a simple and small HTTP server like this great and lightweight library. By using HTTP, you can send requests and retrieve the data on demand.

Clone wsServer repo and add it to your internal module.

Add the following Events in your DLL code

C++
#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;
        }
    }
}

You also need pthread for windows. Simply grab it from this repo and build it as MT/Static library.

Link your DLL against ws2_32.lib and pthreadVSE3.lib, then add this function for server creation:

C++
// 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
}

Now head to your Dllmain, apply the following changes:

C++
if (cmd == moduleName)
{
    ::SetThreadDpiAwarenessContext(DPI_AWARENESS_CONTEXT_SYSTEM_AWARE);
    ::SetProcessDPIAware();
    CreateThread(NULL, NULL, (LPTHREAD_START_ROUTINE)StartWebsocketServer,
                              NULL, NULL, NULL);
}

All done. Now when electron launches your internal module creates a tiny websocket server and wait for connections.

Note

You may need to add _CRT_SECURE_NO_WARNINGS and PTW32_STATIC_LIB to your Preprocessor Definition

The Mainframe

To be able to load your web pages dynamically, you need a mainframe. This mainframe can be used as a proxy page to overwrite the entire HTML to it or can contain a host element which can be used to write new HTML code inside it. In this article, we overwrite the entire page.

Head to app folder of electron data and open index.html in a text editor/html editor and write the following HTML code. I use Visual Studio Code.

HTML
<!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>

Now we have an empty page to load content into. If all of your pages use same style and layout, you only need to load new content from internal module inside mainframe-host division.

Create baseStyle.css file in app folder and open it in editor. Write the following style code:

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%;
}

Control Center

Now, it's time to create our cross-page script which will be loaded by mainframe page and it creates a websocket connection to our internal server for control our front-end. Create internal.js and write the following code inside it:

JavaScript
/* Values */
var socket;
var connected = false;
​
/* Page Events */
function StartupEvent()
{
    socket = new WebSocket("ws://localhost: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();
}

Alright, create your splash.html and run the electron and Here we go... Now we have a connection to our internal module:

Image 2

(Credits : Free Template TheEvent)
Note

You shouldn't use href to go to a new page, If you do it, internal.js context gets destroyed as the new page opens as new process. You can do it but you need to connect to WebSocket again, however it's not recommended on WebSocket method.

Tip

If you faced flicks on page switching, set your body opacity to 0 and add a onLoad event to your page and set opacity back when page is fully loaded.

Request/Response System

We got our bidirectional connection working, Now what we need is a small request/response system to execute our functions remotely and get the data.

For request/response system, we use JSON in both C++ and JavaScript. For parsing JSON data in C++, we use this great single header and easy to use library.

Let's write our request system first, open internal.js and write the following code after values:

JavaScript
/* Structs */
class RequestMetadata
{
    constructor(requestId, onResponse)
    {
        this.requestId = requestId;
        this.onResponse = onResponse;
    }
}
​
/* Request List*/
var requests = [];  
  • RequestMetadata: We create a simple class to use as struct. It holds a unique request ID and a function which process the response result.
  • requests: We create a simple array that holds the requests until they get response, after response, we remove them from the array.

Now let's create our request generator function which handle creating the request and processing response:

JavaScript
/* 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;
}

Our request layout contains two must have parameters:

  • id: An unique ID which identifies the request and will be included in response
  • funcId: An unique ID which identifies the function essence

The rest are the parameters of your function in C++ code, It can be anything, Numbers, Strings, etc.

And finally, we add the response executer. Go in onmessage event and write the following code:

JavaScript
/* 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;
}

We're done! You can pack everything to default_app.asar with 7-Zip. Now it's time to make our C++ response system...

In internal module code, include json.hpp:

C++
#include "jsonpp/json.hpp"

Now head to OnClientRequest event and write the following code:

C++
// 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());
}

Build internal module and run electron, Congrats! You've made your request/response system!

Image 3

Let's Go Old School! (Bonus)

Ok, I know you loved the article so far. So here's a bonus on creating old school HTTP request/response system!

First, remove websocket artifacts and files or you can keep it and have a WebSocket and HTTP at same time. Remember websocket is bidirectional and your internal module can receive data from server, decrypt it and call anything in electron but by using HTTP electron always needs to send data first.

C++ Side (URL Method)

  1. Add HttpLib header to your source:

    C++
    #include "httplib/httplib.h"
     

    Tip: If you faced compiler error simply move the #include <httplib.h> above #include <Windows.h>

  2. Add a server value after namespaces:

    C++
    // HTTP Server
    httplib::Server httpSrv;
  3. Create server thread function with the following code:

    C++
    // 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. Add event initializer function with the following code:

    C++
    // 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. Update CreateThread in DllMain:

    C++
    CreateThread(NULL, NULL, (LPTHREAD_START_ROUTINE)StartHttpServer, NULL, NULL, NULL);

JavaScript Side (URL Method)

Open internal.js and change it to look like the following code:

JavaScript
/* 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('http://localhost:5995/requestCenterText?requestStringID=CENTER_TITLE')
    .then(response => response.text()).then(responseData =>
    {
        document.getElementById("center_text").innerHTML = responseData;
    })
}

Now you can test the code and see the same result as WebSocket method!

C++ Side (POST Method)

To make this method better and more flexible, let's improve it by implementing POST method.

Head to your InitializeServerEvents function and add this following code:

C++
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 Side (POST Method)

To use POST method, we can use different ways but in this article, I use jQuery Ajax because it's simple and nice.

  1. Download jquery-3.X.X.min.js and include it in your main frame before internal.js.
  2. Create an ajax request and handle the response:
    JavaScript
    function RequestDataUsingPost()
    {
        $.ajax({
            type: 'post',
            url: 'http://localhost: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. Run electron and enjoy your old school request/response system!

Conclusion

Alright, another article of mine ends here, I hope you like it and find it useful, I'm not a web developer and most of the JavaScript codes I've used in this article are simple results I found on Google. :D

You can use the following method and convert all of your critical parts of application to native code and use extreme protection on it, You can pack it, virtualize it, obfuscate it or do whatever people do to protect their native binaries, Also, you can protect your sensitive content, assets, add extra encryption to SSL, etc.

You can download the full source code on CodeProject as well.

See ya in the next article!

History

  • 16th January, 2023: Initial version

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)