Child gating refers to the process by which the same hooks applied to a parent process, are reapplied to any children spawned by the parent.

Child gating should be useful in any scenario where your target is spawning multiple other binaries, for example a root detection technique may involve spawning the which binary and checking for su; This can be bypassed by:

Preventing the target from spawning which by hiding that binary or inhibiting the call to exec* funtions. Hiding the su binary by hooking open or access and enabling child gating. Finally, child gating may also be useful for more complicated binaries such as Electron applications. Electron will typically spawn a few processes, all of which have different purposes - child gating on the initially spawned process is critical in successfully hooking the application.

First we need to programs, one and two respectively. one will spawn two.

One

#include <stdlib.h>
#include <stdio.h>
#include <unistd.h>

int main(){
    puts("one");
    // This is where one will spawn two
    execve("./two", NULL, NULL); 
}

Two

#include <stdlib.h>
#include <stdio.h>

int main(){
    puts("two");
}

Now if we wanted to hook both calls to puts using Frida, we would need to use a feature called child gating. I didn’t include a writeup in exactly how this works, because I haven’t had the time to inspect the source code, but I gander it’d be interesting!

The code below is taken from the frida-python repository and slightly modified:

#!/usr/bin/env python3
from __future__ import print_function

import threading

import frida
from frida_tools.application import Reactor


class Application(object):
    def __init__(self):
        self._stop_requested = threading.Event()
        self._reactor = Reactor(run_until_return=lambda reactor: self._stop_requested.wait())

        self._device = frida.get_local_device()
        self._sessions = set()

        self._device.on("child-added", lambda child: self._reactor.schedule(lambda: self._on_child_added(child)))
        self._device.on("child-removed", lambda child: self._reactor.schedule(lambda: self._on_child_removed(child)))
        self._device.on("output", lambda pid, fd, data: self._reactor.schedule(lambda: self._on_output(pid, fd, data)))

    def run(self):
        self._reactor.schedule(lambda: self._start())
        self._reactor.run()

    def _start(self):
        argv = ["./one"]
        print("✔ spawn(argv={})".format(argv))
        pid = self._device.spawn(argv, env={}, stdio='pipe')
        self._instrument(pid)

    def _stop_if_idle(self):
        if len(self._sessions) == 0:
            self._stop_requested.set()

    def _instrument(self, pid):
        print("✔ attach(pid={})".format(pid))
        session = self._device.attach(pid)
        session.on("detached", lambda reason: self._reactor.schedule(lambda: self._on_detached(pid, session, reason)))
        
        # Enable child gating
        print("✔ enable_child_gating()")
        session.enable_child_gating()     

        # Load a frida script from disk
        print("✔ create_script()")
        with open('./agent.js') as fd:
            content = fd.read()
    

        script = session.create_script(content)
        script.on("message", lambda message, data: self._reactor.schedule(lambda: self._on_message(pid, message)))
        print("✔ load()")
        script.load()
        print("✔ resume(pid={})".format(pid))
        self._device.resume(pid)
        self._sessions.add(session)

    def _on_child_added(self, child):
        print("⚡ child_added: {}".format(child))
        self._instrument(child.pid)

    def _on_child_removed(self, child):
        print("⚡ child_removed: {}".format(child))

    def _on_output(self, pid, fd, data):
        print("⚡ output: pid={}, fd={}, data={}".format(pid, fd, repr(data)))

    def _on_detached(self, pid, session, reason):
        print("⚡ detached: pid={}, reason='{}'".format(pid, reason))
        self._sessions.remove(session)
        self._reactor.schedule(self._stop_if_idle, delay=0.5)

    def _on_message(self, pid, message):
        print("⚡ message: pid={}, payload={}".format(pid, message["payload"]))


app = Application()
app.run()

The agent source code is shown below, but it really only hooks puts.

Interceptor.attach(Module.findExportByName(null, 'puts'), {
    onEnter: function(args){
        send({
            type: 'puts',
            path: Memory.readUtf8String(args[0])
          });
    }
})

Viola! Now we have everything we need to see spawn gating in action. To compile the C programs, run the commands below:

clang one.c -o one
clang two.c -o two

Finally, you can execute the one binary by running the python script. Notice how both calls to puts are intercepted.