Detecting Frida by Diffing In-Memory and On-Disk Function Bytes

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.

Detecting Frida by Diffing In-Memory and On-Disk Function Bytes

The idea is simple: Frida, once injected, inline-hooks a small, well-known set of libc functions. If you know which functions it hooks, you can read their entry bytes from memory, read the same bytes from the on-disk libc.so, and compare. A mismatch means someone patched them — almost certainly Frida.

1. frida-core: the exit monitor

After injecting into a target process, Frida uses Interceptor.attach to inline-hook the process-exit functions. There are three key functions to locate: exit, _exit, and abort.

Source: frida-core / lib/payload/exit-monitor.vala#L34

#if WINDOWS
			interceptor.attach ((void *) Gum.Process.find_module_by_name ("kernel32.dll").find_export_by_name ("ExitProcess"),
				listener);
#else
			var libc = Gum.Process.get_libc_module ();
			const string[] apis = {
				"exit",
				"_exit",
				"abort",
			};
			foreach (var symbol in apis) {
				interceptor.attach ((void *) libc.find_export_by_name (symbol), listener);
			}
#endif

2. frida-gum: the exceptor

Frida also calls gum_interceptor_replace to inline-hook signal and sigaction in libc.

Source: frida-gum / gum/backend-posix/gumexceptor-posix.c#L228

static void
gum_exceptor_backend_attach (GumExceptorBackend * self)
{
  GumInterceptor * interceptor = self->interceptor;
  const gint handled_signals[] = {
    SIGABRT,
    SIGSEGV,
    SIGBUS,
    SIGILL,
    SIGFPE,
    SIGTRAP,
    SIGSYS,
  };
  gint highest, i;
  struct sigaction action;

  highest = 0;
  for (i = 0; i != G_N_ELEMENTS (handled_signals); i++)
    highest = MAX (handled_signals[i], highest);
  g_assert (highest > 0);
  self->num_old_handlers = highest + 1;
  self->old_handlers = g_new0 (struct sigaction *, self->num_old_handlers);

  action.sa_sigaction = gum_exceptor_backend_on_signal;
  sigemptyset (&action.sa_mask);
  action.sa_flags = SA_SIGINFO | SA_NODEFER;
#ifdef SA_ONSTACK
  action.sa_flags |= SA_ONSTACK;
#endif
  for (i = 0; i != G_N_ELEMENTS (handled_signals); i++)
  {
    gint sig = handled_signals[i];
    struct sigaction * old_handler;

    old_handler = g_slice_new0 (struct sigaction);
    self->old_handlers[sig] = old_handler;
    gum_original_sigaction (sig, &action, old_handler);
  }

  gum_interceptor_begin_transaction (interceptor);

  gum_interceptor_replace (interceptor, gum_original_signal,
      gum_exceptor_backend_replacement_signal, self, NULL);
  gum_interceptor_replace (interceptor, gum_original_sigaction,
      gum_exceptor_backend_replacement_sigaction, self, NULL);

  gum_interceptor_end_transaction (interceptor);
}

3. The detection approach

Premise: we know Frida, once injected, hooks these libc functions — sigaction, signal, exit, abort, _exit.

a. Get each target function’s address in libc, and copy its entry machine code.

b. Get each target function’s offset.

c. Get the original machine code at each function’s entry: read the on-disk /system/lib64/libc.so (arm64-v8a here), then use the offset to locate the function and copy its entry bytes.

d. Compare the bytes read from disk against the bytes read from memory. If they differ, Frida is present.

Here’s the implementation:

#include <dlfcn.h>
#include <sys/mman.h>
#include <unistd.h>
#include <string.h>
#include <fcntl.h>
#include <elf.h>
#include <android/log.h>
#include <stdbool.h>
#include <errno.h>
#include <stdio.h>
#include <jni.h>

#define LOG_TAG "HookDetection"
#define LOGD(...) __android_log_print(ANDROID_LOG_DEBUG, LOG_TAG, __VA_ARGS__)
#define BYTE_BUFFER_SIZE 16

// get function address
void* get_function_address(void* handle, const char* func_name) {
    void* func_addr = dlsym(handle, func_name);
    if (!func_addr) {
        LOGD("[-] Function %s not found in global symbol table", func_name);
    } else {
        LOGD("[+] Function: %s, addr: 0x%lx", func_name, (uintptr_t)func_addr);
    }
    return func_addr;
}

// get function offset
uintptr_t get_function_offset(void* func_addr) {
    Dl_info info;
    if (dladdr(func_addr, &info) == 0) {
        LOGD("[-] Unable to get function info");
        return 0;
    }
    return (uintptr_t)func_addr - (uintptr_t)info.dli_fbase;
}

// read bytes from the library file
bool read_bytes_from_libso(const char* libpath, uintptr_t offset, uint8_t* buffer, size_t size) {
    int fd = open(libpath, O_RDONLY);
    if (fd == -1) {
        LOGD("[-] Failed to open %s: %s", libpath, strerror(errno));
        return false;
    }

    if (lseek(fd, offset, SEEK_SET) == -1) {
        LOGD("[-] Seek failed at %lx in %s: %s", (unsigned long)offset, libpath, strerror(errno));
        close(fd);
        return false;
    }

    ssize_t bytes_read = read(fd, buffer, size);
    close(fd);

    if (bytes_read != (ssize_t)size) {
        LOGD("[-] Read %zd bytes, expected %zu from %s at offset %lx", bytes_read, size, libpath, (unsigned long)offset);
        return false;
    }

    return true;
}

// byte array to hex string
void bytes_to_hex_string(const uint8_t* bytes, size_t size, char* hex_string) {
    for (size_t i = 0; i < size; i++) {
        sprintf(hex_string + i * 2, "%02x", bytes[i]);
    }
    hex_string[size * 2] = '\0';
}

// compare in-memory bytes against on-disk library bytes
bool compare_function_bytes(void* func_addr, uint8_t* file_bytes, size_t size) {
    uint8_t mem_bytes[BYTE_BUFFER_SIZE];
    memcpy(mem_bytes, func_addr, size);

    char mem_hex_string[BYTE_BUFFER_SIZE * 2 + 1];
    char file_hex_string[BYTE_BUFFER_SIZE * 2 + 1];

    bytes_to_hex_string(mem_bytes, size, mem_hex_string);
    bytes_to_hex_string(file_bytes, size, file_hex_string);

    LOGD("[*] Memory: %s | File: %s", mem_hex_string, file_hex_string);

    return memcmp(mem_bytes, file_bytes, size) == 0;
}

// hook detection function
bool detect_hook(void* handle, const char* lib_path, const char* func_name) {
    void* func_addr = get_function_address(handle, func_name);
    if (!func_addr) return false;

    uintptr_t offset = get_function_offset(func_addr);
    if (offset == 0) {
        LOGD("[-] Failed to get offset for %s", func_name);
        return false;
    }

    uint8_t file_bytes[BYTE_BUFFER_SIZE];
    if (!read_bytes_from_libso(lib_path, offset, file_bytes, sizeof(file_bytes))) {
        LOGD("[-] Failed to read bytes for %s", func_name);
        return false;
    }

    bool is_hooked = !compare_function_bytes(func_addr, file_bytes, sizeof(file_bytes));
    LOGD("[+] %s in %s is %s", func_name, lib_path, is_hooked ? "HOOKED" : "NOT HOOKED");
    return is_hooked;
}

// run hook detection
int do_hook_check() {
    const char* libpath = "/system/lib64/libc.so";
    void* handle = dlopen(libpath, RTLD_LAZY);
    if (!handle) {
        LOGD("[-] Failed to load %s", libpath);
        return -1;
    }

    const char* funcs[] = {"sigaction", "signal", "exit", "abort", "_exit"};
    bool hooked = false;

    for (size_t i = 0; i < sizeof(funcs) / sizeof(funcs[0]); i++) {
        if (detect_hook(handle, libpath, funcs[i])) {
            hooked = true;
        }
    }

    dlclose(handle);
    return hooked;
}

4. Summary

Reading Frida’s source to learn exactly what it hooks, then checking for those specific patches, is like an open-book exam. The advantages are obvious: it’s highly targeted, the detection logic is trivially simple, and the performance cost is negligible. The open question is whether Frida changes this behavior in a future release — but for now it’s a cheap, reliable signal.


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