Frida-Seccomp: Generic svc Tracing and Hooking on Android

About the author. I’m Colin Xiao — an independent reverse engineer, mostly working solo on contract jobs. I dig into compiled code of every kind, including but not limited to: Android and iOS apps, x86/x64/ARM native binaries, packers and obfuscators, DRM and license checks, network protocols and APIs, crypto and signature algorithms, malware, game binaries, and embedded/IoT firmware. I also handle large-scale data collection and vulnerability research. If you have any project in mind, I’d be glad to take it on — whatever it is, as long as it’s within my wheelhouse. Just reach out. You can reach me on X at @colinxiao_lab or on Telegram at https://t.me/ColinXiao666.

This research is published for educational and defensive purposes. The target sample is anonymized throughout.


Frida-Seccomp: Generic svc Tracing and Hooking on Android

Currently ARM64 only; the ARM32 logic is identical, and porting is straightforward if you want it.

What it looks like

Tracing openat:

Tracing recvfrom:

What seccomp is

seccomp is a Linux application-sandboxing mechanism: it isolates processes mainly by restricting which syscalls they can make. seccomp-bpf is the extension that lets you configure the sandbox to permit other syscalls via a filter.

Combining seccomp with Frida

The core idea

Here’s a BPF rule:

struct sock_filter filter[] = {
        BPF_STMT(BPF_LD + BPF_W + BPF_ABS,
                 (offsetof(struct seccomp_data, nr))),
        BPF_JUMP(BPF_JMP + BPF_JEQ + BPF_K, nr, 0, 1),
        BPF_STMT(BPF_RET + BPF_K, SECCOMP_RET_TRAP),
        BPF_STMT(BPF_RET + BPF_K, SECCOMP_RET_ALLOW),
};

(For seccomp usage, see the “what seccomp is” reference.) When the return rule is SECCOMP_RET_TRAP, the target syscall makes seccomp raise a SIGSYS signal and a soft interrupt. Catch that SIGSYS and you can observe the svc call and print its arguments.

Installing the seccomp rules from a script

Use Frida’s CModule API — it gives you dynamic compilation so you can write C inside JS. From the Frida docs:

const cm = new CModule(`
#include <stdio.h>
void hello(void) {
  printf("Hello World from CModule\\n");
}
`);
const hello = new NativeFunction(cm.hello, 'void', []);
hello();

Catching the exception

Use Frida’s Process.setExceptionHandler to catch the exception and process the data in your callback. The logic is explained in the comments:

// Exception handler
    Process.setExceptionHandler(function (details) {
        const current_off = details.context.pc - 4;
        // Check if the exception is seccomp-caused: read opcode 010000d4 == svc 0
        if (details.message == "system error" && details.type == "system" && hex(ptr(current_off).readByteArray(4)) == "010000d4") {
            // Lock to avoid multithreading issues
            lock(syscall_thread_ptr)
            // Get the syscall number from the x8 register
            const nr = details.context.x8.toString(10);
            let loginfo = "\n=================="
            loginfo += `\nSVC[${syscalls[nr][1]}|${nr}] ==> PC:${addrToString(current_off)} P${Process.id}-T${Process.getCurrentThreadId()}`
            // Build the syscall thread's call arguments
            const args = Memory.alloc(7 * 8)
            args.writePointer(details.context.x8)
            let args_reg_arr = {}
            for (let index = 0; index < 6; index++) {
                eval(`args.add(8 * (index + 1)).writePointer(details.context.x${index})`)
                eval(`args_reg_arr["arg${index}"] = details.context.x${index}`)
            }
            // Get manual stack trace
            loginfo += "\n" + stacktrace(ptr(current_off), details.context.fp, details.context.sp).map(addrToString).join('\n')
            // Print arguments
            loginfo += "\nargs = " + JSON.stringify(args_reg_arr)
            // Call the syscall thread and write the result into x0
            details.context.x0 = call_task(syscall_thread_ptr, args, 0)
            loginfo += "\nret = " + details.context.x0.toString()
            // Print info
            call_thread_log(loginfo)
            // Unlock
            unlock(syscall_thread_ptr)
            return true;
        }
        return false;
    })

The gotchas

1. Resuming the syscall

The problem

Per the Frida docs, returning true from setExceptionHandler resumes the original thread — but it only skips the svc instruction; it doesn’t actually execute it. Not executing the syscall means corrupted app data at best, a crash at worst. So the exception callback has to call the syscall manually and write the result into x0.

That creates a new problem: once seccomp is enabled on the main thread, it also constrains every thread created afterward — so calling syscall from inside the exception handler trips the same seccomp rule and re-throws, i.e. a deadlock.

The fix

Create one thread before the main thread gets constrained. That thread is unconstrained. Borrowing the thread-pool idea, have this syscall thread loop on tasks, so syscalls run in an unconstrained thread.

2. Stack backtrace

The problem

Frida’s Thread.backtrace crashes easily — likely because the seccomp rules (or prctl(PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0)) tighten permissions in a way that conflicts with Frida’s backtrace implementation.

The fix

Implement the backtrace manually. On Arm64 every function saves x29/x30 to the stack in its prologue, so keep reading x29 upward to reconstruct the full stack:

function stacktrace(pc, fp, sp) {
    let n = 0, stack_arr = [], fp_c = fp;
    stack_arr[n++] = pc;
    const mem_region = call_thread_read_maps(sp);
    while (n < MAX_STACK_TRACE_DEPTH) {
        if (parseInt(fp_c.toString()) < parseInt(sp.toString()) || fp_c < mem_region.start || fp_c > mem_region.end) {
            break
        }
        let next_fp = fp_c.readPointer()
        let lr = fp_c.add(8).readPointer()
        fp_c = next_fp
        stack_arr[n++] = lr
    }
    return stack_arr;
}

3. Process.findModuleByAddress / Process.enumerateModules crash or find nothing

The problem

Probably the same cause as gotcha 2.

The fix

In the CModule, manually implement address → soinfo lookup (base, size, soname, etc.).

4. Frida’s send crashes under write constraints

The problem

Same as gotcha 2.

The fix

Use the syscall thread with __android_print_log to print instead.

What else you can do

Before/after the syscall in that thread, you can rewrite arguments, return values, and addresses — i.e. hook.

GitHub

https://github.com/Abbbbbi/Frida-Seccomp

How to use it

pip3 install frida
python3 multi_frida_seccomp.py

Filter logcat for seccomp to see the output. It’s also auto-saved to a {package}_{pid}_{timestamp} folder (multi-process supported).


Colin Xiao — I take on contract reverse-engineering work, and writeups like this are how I show what I can do. If you need help with anything in this space — including but not limited to the areas I cover above — I’m available. No project is too big or too small — if it’s reverse engineering and I can do it, I’ll take it. DM me on X (@colinxiao_lab) or Telegram (@ColinXiao666).

Get in touch