< Back to blog
Max SavoninChief Executive Officer at KeenEthics

Node.js Inject: How to Conduct and Why to Use

How to inject and run custom code inside a target Node.js process

Today, I would like to show you how to execute custom JS code in another Node.js process and to get a proper result. These are relatively simple code examples, which demonstrate basic ideas of native code calls with the help of Frida framework.

Node

Frida Framework

Frida by itself already has an example of code injection for Node.js, but it seems that it is a bit outdated and could work only with a certain Node.js version – GUIDE. It uses V8 embed code to execute a JS string. I have updated the code to Node.js v8.16.0 x64, but I will not elaborate on injection details here. You can easily find out more via the following link, so instead, let’s go to the injected code.

At first, let’s see, which Frida code we will use (you can get more details here).

  1. Module.findExportByName

     

    – it finds C exported function by its name. There is one little caveat – so-called

     

    . In short, when we are trying to investigate C++ compiled code, we could see a lot of names like ?GetCurrent@Isolate@v8@@SAPEAV12@XZ. First of our points is to find all required function names in a disassembler like IDA or OllyDbg and to get the memory address of it.

  2. NativeFunction(POINTER, RETURN_VALUE, ARGUMENTS)

     

    – this function is used to define in-js bindings for a native function. Here, we need to describe correct arguments count and return values to call these functions later.

  3. NativeCallback

     

    – a code that will be executed after a native function call.

  4. WeakRef.bind

     

    is used to monitor specified pointer and the callback when it gets garbage-collected or on Frida detach.

  5. Memory.alloc(N)

     

    – allocates N bytes in memory and returns pointer for this memory region.

Concepts and Functions Used

Here, I want to define some concepts that I am going to use:

  • IsolateV8

  • Context

     

    stands for a sandboxed execution context with its own set of built-in objects and functions.

  • HandleScope

     

    is a stack-allocated class, which governs a number of local handles. After a handle scope has been created, all local handles will be allocated within that handle scope until either the handle scope is deleted or another handle scope is created. If there is already a handle scope and a new one is created, all allocations will take place in the new handle scope until it is deleted. After that, new handles will again be allocated in the original handle scope. After the handle scope of a local handle has been deleted, the garbage collector will no longer track the object stored in the handle and may deallocate it. The behavior of accessing a handle for which the handle scope has been deleted is undefined.

  • Script

     

    stands for a compiled JavaScript script.

  • String

     

    is a JS string value.

  • Value

     

    is a superclass of all JavaScript values and objects.

Next, I describe the functions of libuv that we will use – a lib that allows using an async, event-driven style of programming:

  1. uv_async_init

     

    – initializes handle, specifies the callback that will be executed inside the event loop;

  2. uv_default_loop

     

    – takes default event loop;

  3. uv_async_send

     

    – calls the callback;

  4. uv_unref

     

    – destroys the object created;

  5. uv_close

     

    – requests handle to be closed.

We will also use some V8 functions (formatted as real method name --> name of binded js function). In particular:

  1. V8::Isolate::GetCurrent(v8_Isolate_GetCurrent)

     

    – takes the instance of a current Isolate.

  2. V8::Isolate::GetCurrentContext(v8_Isolate_GetCurrentContext)

     

    – gets context from the current Isolate.

  3. V8::Context::Enter(v8_Context_Enter)

     

    – enters this context. After entering a context, all code compiled and run is compiled and run in this context. If another context is already entered, this old context is saved so it can be restored when the new context is exited.

  4. V8::HandleScope::Init(v8_HandleScope_init)

     

    – initializes HandleScope.

  5. V8::String::NewFromUtf8(v8_String_NewFromUtf8)

     

    – gets JS string from const char *.

  6. V8::Script::Compile(v8_Script_Compile)

     

    – compiles the specified script (bound to current context).

  7. V8::Script::Run(v8_Script_Run)

     

    – runs the script returning the resulting value. If the script is context-independent (created using ::New), it will be run in the currently entered context. If it is context-specific (created using ::Compile), it will be run in the context in which it was compiled.

  8. v8::Value::Int32Value(v8_Value_Int32Value)

     

    – gets C int from JS value. Value has a lot of functions for different types.

Code Listing

So, let’s check the code now:

Code Explained

createFunc is a helper that is used to create JS bind with a specified signature to reuse later.

All of the calls like const uv_default_loop = createFunc('uv_default_loop', 'pointer', []) are required to define JS bindings to native functions.

scriptToExecute is our injected code.

We define uv async handler, then bind processPending to the default event loop, then “wake up” that event loop for a callback call.

Inside the callback, we receive an Isolate instance, initialize new HandleScope, and take current context.

Then, we convert the JS string to V8 string, compile, and run it. After all, we just extract C int value from the script execution result.

The Use of Node.js Inject

That is a relatively simple example of how we can inject and run custom code inside a target Node.js process. It is not just about hacking. Instead, this concept allows us to write some kind of run-time plugins for Node.js, which will help to improve something specific, something that we cannot change without recompiling.