https://github.com/abashh/v8-native-wrapper

Introduction

This post will describe how to interface with the event emitter of a running electron process natively. Electron is a widely popular framework for building desktop applications using JavaScript. It abstracts all of the complex ui code necessary for a modern desktop application behind the standard web page “index.js” folder structure.

Electron Anatomy

Electron combines chromium and nodejs to selectively expose a native nodejs interface to web code. Chromium is used to execute each webpage in an isolated renderer child process which can send/receive events to/from the main process running the nodejs environment. This is very important to understand as any frontend action can be interpreted as a sequence of event emitter events.

Ex

The interfaces are defined in the preload.js file of each webpage folder. The preload.js file is of particular interest as it defines what events are exposed to a child process.

Native Interface

In normal electron javascript code getting and using the event emitter in the context of the main process looks something like this.

const e = require("electron");
e.ipcmain.emit("read-file", "test.txt");

So to do this natively we’d want to emulate this behavior. Since this code is executed in node it’s pertinent to get the node structure representing this execution environment. Electron wraps the node environment in its’ electron browser main parts class which can be found via sig scanning the static getter.

After some poking around the node codebase I found this which tells us that all we need is the process object. Conveniently the process object is referenced in the electron codebase here.

So, the tldr is something like this

    void*(*electron_browsermainparts_get)();
    electron_browsermainparts_get = reinterpret_cast<decltype(electron_browsermainparts_get)>(resolve_scan(sig_scan(textsection_start, textsection_size,
      "\xE8\x00\x00\x00\x00\x8B\x17\x48\x89\xC1\xE8\x00\x00\x00\x00\x84\xC0", "x????xxxxxx????xx"), 1));

    auto browser_main_parts = electron_browsermainparts_get();
    auto env = *(void**)((uint64_t)browser_main_parts + 80);
    auto principal_realm = *(void**)((uint64_t)env + 2768);
    auto process_object = *(void**)((uint64_t)principal_realm + 464);

I found the offsets by cross referencing this in IDA. process object IDA

However, now we enter the realm of v8 as the process object is a javascript object. To progress we’ll need to either compile an identical v8 engine or find a way to call the v8 functions in the electron binary. It turns out the second approach is very easy as electron binaries export all the v8 routines we need.

Ex

But, before calling a v8 function we’ll need a handle context and a context scope. Luckily our lives are made easy as the constructors/destructors for these objects are exported. Ex

Now we can go down the chain of objects to get the require function javascript object using more exports from the binary. Ex

    auto process = v8w::object(process_object, ctx, isolate);
    auto mm = process.get("mainModule");
    auto req = mm.value()->get("require");

This leads us to the final problem. How do we even execute our code? Trying this in an injected dll will not work as it runs in a seperate thread. So, we need to hijack the thread which executes the node environment. To do this I simply hook the libuv event loop handler(uv_run) which node relies on for various I/O.

  void main(){
    orig_uv_run = get_export("program.exe", "uv_run");
    hook_uv_run(orig_uv_run, uv_run_hook);
  }

  int uv_run_hook(void* loop, uint64_t mode){
    //executing in the javascript loop thread
    do_stuff();
    return (((decltype(uv_run_hook)*)orig_uv_run))(loop, mode);
  }

A C++ wrapper around the handlescope/contextscope objects could look something like this. Where v8w::handlers holds the necessary exports of the electron binary.

    class HandleScope{
        public:
        HandleScope(void* isolate){
            v8w::handlers.handle_create((void**)&_scope[0], isolate);
        }
        ~HandleScope(){
            v8w::handlers.handle_destroy(&_scope[0]);
        }
        private:
        char _scope[200];
    };

    class Context{
        public:
        Context(void* ctx): _ctx(ctx){
            v8w::handlers.enter_context(ctx);
        }
        ~Context(){
            v8w::handlers.exit_context(_ctx);
        }
        private:
        void* _ctx;
    };

Full Example Code



//no error checking is done for brevity
static void*(*electron_browsermainparts_get)();
static void* orig_uv_run = 0;
void main(){

  //signature scan for the function that returns the static global browsermainparts variable
  //"ElectronBrowserMainParts* ElectronBrowserMainParts::Get()" in the electron source
  electron_browsermainparts_get = reinterpret_cast<decltype(electron_browsermainparts_get)>(resolve_scan(sig_scan(textsection_start, textsection_size,
      "\xE8\x00\x00\x00\x00\x8B\x17\x48\x89\xC1\xE8\x00\x00\x00\x00\x84\xC0", "x????xxxxxx????xx"), 1));

  v8w::init();

  //electron binaries will export uv_run by default
  orig_uv_run = get_export("program.exe", "uv_run");

  //use any standard hooking method
  hook_uv_run(orig_uv_run, uv_run_hook);
}

int uv_run_hook(void* loop, uint64_t mode){

  //browsermainparts may be 0 depending on when injection
    auto browser_main_parts = electron_browsermainparts_get();

    //the isolate and ctx are also stored in browser_main_parts
    //but since we're in uv_run which is called by node with the main isolate/context we can get the current isolate/context
    void* isolate = v8w::handlers.try_get_current(); 
    void* ctx = 0;
    v8w::handlers.get_current_context(isolate, &ctx);

    //these types are cooked to manually call the handlescope/context constructors/destructors which are acquired via the export table
    auto hscope = v8w::HandleScope(isolate);
    auto ctxscope = v8w::Context(ctx);

    //get the node environment
    auto env = *(void**)((uint64_t)browser_main_parts + 80);

    //get the principal realm
    auto principal_realm = *(void**)((uint64_t)env + 2768);

    //get the process argument that node passes to every script
    auto proc = *(void**)((uint64_t)principal_realm + 464);

    //wrap the raw process ptr in a helper clas
    auto process = v8w::object(proc, ctx, isolate);

    //get the main module as described here
    //https://nodejs.org/api/process.html#processmainmodule
    auto mm = process.get("mainModule");

    //get the standard require function which can import modules
    auto req = mm.value()->get("require");

    //make a v8 string
    auto electron_string = v8w::make_str("electron", ctx, isolate);

    //equivalent to the javscript: require("electron")
    void* params[1] = {electron_string.value()->getraw()};
    auto electron_module = req.value()->call(params);

    //now we have the event emitter of the main module. 
    //This allows us to arbitrarily call or overwrite api handlers
    //https://nodejs.org/api/events.html
    auto ipcmain = electron_module.value()->get("ipcMain");

    //example emitting an event

    //get emit
    auto emit = ipcmain->get("emit");

    //payload will depend on the api
    auto payload = v8w::neww(ctx, isolate);
    payload.value()->set<std::string>("file", "something.txt");

    //api (channel) name
    auto channel = v8w::make_str("write-file", ctx, isolate);

    //event object used to get the return value
    auto event = v8w::neww(ctx, isolate);

    //call emit(ipcmain, event, payload)
    void* params[3] = {channel.value()->getraw(), event.value()->getraw(), payload.value()->getraw()};
    emit.value()->call(params, ipcmain->getraw());

    //get the return value from the event object we made
    auto return_val = event.value()->get("returnValue");

    return (((decltype(uv_run_hook)*)orig_uv_run))(loop, mode);
}

Further Reading