2073 words
10 minutes
Patch 7 — Automating the Double-Encoding with a Python Script

( بِسْمِ اللَّـهِ الرَّحْمَـٰنِ الرَّحِيمِ )

CAUTION

#FreePalestine

WARNING

This is just for educational purposes.


Patch 7 — Script Overhaul: Expanded Charset, .rodata String Reversal, and gdbus Thread#

Introduction#

Patches 3 through 5 built up src/anti-anti-frida.py incrementally — each adding one new obfuscation step. Patch 7 is a consolidating overhaul of the entire script. It introduces four distinct improvements in one commit:

  1. Expanded random charset — the character pool for random replacements is upgraded from limited lowercase or uppercase sets to the full mixed-case alphanumeric alphabet, making generated names harder to characterize by case alone.
  2. .rodata section string patching — a new LIEF-based loop scans the .rodata section of the compiled agent for four additional Frida-branded string literals (FridaScriptEngine, GLib-GIO, GDBusProxy, GumScript) and reverses their bytes in-place, destroying the readable form without changing the string length.
  3. gdbus thread name randomization — a fourth sed -b -i replacement is added, covering GLib’s D-Bus background thread name gdbus.
  4. Colored console output — a log_color() helper wraps all status messages in ANSI red-on-black formatting for build-log visibility.

The Original Functions / Strings#

This patch targets four categories of artifacts simultaneously.

A — Expanded random charset#

Previous patches used:

  • "ABCDEFGHIJKLMNO" (15 uppercase chars) for symbol names
  • "abcdefghijklmn" (14 lowercase chars) for thread names

Both pools are small and their case restriction means the generated names have a detectable statistical property: all-uppercase or all-lowercase 5-character strings are unusual in legitimate native library symbol tables. This patch unifies both to:

random_charset = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"

52 characters, mixed case. random.sample(random_charset, 5) now produces names like xKbQm, TzaRc, nLpWj — indistinguishable in case distribution from real symbol names in any native library.

B — .rodata string literals: FridaScriptEngine, GLib-GIO, GDBusProxy, GumScript#

File location: .rodata section of frida-agent.so

These four strings are SDK-level identifiers baked into Frida’s source:

StringOrigin / Purpose
FridaScriptEngineFrida’s JS engine class/type name used in log messages and GObject type registration
GLib-GIOGLib’s GIO subsystem identifier, appears in GLib error domain strings and D-Bus log output
GDBusProxyGLib D-Bus proxy class name, emitted in GLib warning/error messages during IPC operations
GumScriptFrida-Gum’s script class name, appears in GumJS debug output and error messages

None of these are ELF symbol names — they are plain C string literals stored in .rodata. LIEF’s .symbols iterator does not reach them — only direct section scanning can find and patch them. Because they contain either Frida, Gum, or well-known GLib class names associated exclusively with Frida’s dependency stack, they are valid static signatures.

Why reversal instead of random replacement:

Unlike symbol names (which are null-terminated and have no length constraint enforced by the ELF spec) and thread names (which sed handles at exact known lengths), these .rodata strings can appear adjacent to other data with no padding. Replacing them with a different string of the same length preserves binary layout integrity. Reversal is the simplest same- length transformation: FridaScriptEngineenigEnpircSadirF. It is deterministic, requires no random state, and produces a string that will not match any forward-scanning signature.

C — gdbus thread name#

GLib’s D-Bus implementation creates a background thread named gdbus when any D-Bus connection is initialised. Frida uses GLib’s D-Bus stack internally for its own IPC. The gdbus thread therefore appears in /proc/<pid>/task/<tid>/comm alongside gum-js-loop and gmain whenever the agent is active. It was not covered by earlier patches.

Why it is a detection surface:

gdbus is a known GLib thread name. While it can appear in legitimate Android apps that use GLib (uncommon), its simultaneous presence with gum-js-loop or gmain in the same process is a high-confidence Frida indicator.


Before Patching — Behavior on Android#

All four detectable strings present in .rodata:

$ adb shell strings /data/local/tmp/frida-agent-64.so | grep -E "FridaScriptEngine|GLib-GIO|GDBusProxy|GumScript"
FridaScriptEngine
GLib-GIO
GDBusProxy
GumScript

All four Frida-specific thread names visible at runtime:

$ adb shell ps -T -p $(pidof com.example.targetapp) | grep -E "gum-js-loop|gmain|gdbus"
u0_a123  12345 12351 ... gum-js-loop
u0_a123  12345 12352 ... gmain
u0_a123  12345 12353 ... gdbus

/proc task comm files:

android:/ # cat /proc/12345/task/12353/comm
gdbus

How Apps Detect It#

Method 1 — Extended static string scan (shell / Python)#

Detectors that already scan frida-agent.so for the strings from earlier patches maintain a list of additional secondary signatures:

import subprocess

SIGNATURES = [
    # Primary (covered by earlier patches)
    "frida:rpc", "frida_agent_main", "gum-js-loop", "gmain",
    # Secondary — caught by this patch
    "FridaScriptEngine", "GLib-GIO", "GDBusProxy", "GumScript", "gdbus",
]

def scan_binary(path: str) -> list[str]:
    result = subprocess.run(["strings", path], capture_output=True, text=True)
    found = []
    for sig in SIGNATURES:
        if sig in result.stdout:
            found.append(sig)
    return found

hits = scan_binary("/data/local/tmp/frida-agent-64.so")
if hits:
    print(f"[!] Frida signatures found: {hits}")

Method 2 — GObject type name probe (C++)#

FridaScriptEngine and GumScript are registered as GObject type names inside the agent. A detector that has already identified the agent’s base address in memory can search for these strings in .rodata directly:

#include <string.h>

// Called after finding frida-agent's load base (e.g. from /proc/self/maps by UUID name)
bool detectFridaGObjectTypes(const uint8_t *rodata_base, size_t rodata_size) {
    const char *targets[] = {
        "FridaScriptEngine",
        "GumScript",
        "GDBusProxy",
        "GLib-GIO",
        NULL
    };

    for (int i = 0; targets[i] != NULL; i++) {
        size_t len = strlen(targets[i]);
        for (size_t off = 0; off + len < rodata_size; off++) {
            if (memcmp(rodata_base + off, targets[i], len) == 0) {
                return true;
            }
        }
    }
    return false;
}

Method 3 — Combined thread name set detection including gdbus (C++)#

static const char *FRIDA_THREADS[] = {
    "gum-js-loop",
    "gmain",
    "gdbus",       // newly added in this patch's threat model
    NULL
};

bool detectAllFridaThreads() {
    DIR *task_dir = opendir("/proc/self/task");
    if (!task_dir) return false;

    struct dirent *entry;
    while ((entry = readdir(task_dir)) != NULL) {
        if (entry->d_name[0] == '.') continue;

        char comm_path[64], comm[32] = {0};
        snprintf(comm_path, sizeof(comm_path), "/proc/self/task/%s/comm", entry->d_name);

        FILE *f = fopen(comm_path, "r");
        if (!f) continue;
        fgets(comm, sizeof(comm), f);
        fclose(f);
        comm[strcspn(comm, "\n")] = '\0';

        for (int i = 0; FRIDA_THREADS[i]; i++) {
            if (strcmp(comm, FRIDA_THREADS[i]) == 0) {
                closedir(task_dir);
                return true;
            }
        }
    }
    closedir(task_dir);
    return false;
}

The Patch#

Full diff#

--- a/src/anti-anti-frida.py
+++ b/src/anti-anti-frida.py
@@ -2,36 +2,59 @@ import lief
 import sys
 import random
 import os
-
+
+def log_color(msg):
+    print(f"\033[1;31;40m{msg}\033[0m")
+
 if __name__ == "__main__":
     input_file = sys.argv[1]
-    print(f"[*] Patch frida-agent: {input_file}")
-    random_name = "".join(random.sample("ABCDEFGHIJKLMNO", 5))
-    print(f"[*] Patch `frida` to `{random_name}``")
-
+    random_charset = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"
+    log_color(f"[*] Patch frida-agent: {input_file}")
     binary = lief.parse(input_file)
-
+
     if not binary:
+        log_color(f"[*] Not elf, exit")
         exit()
+
+    random_name = "".join(random.sample(random_charset, 5))
+    log_color(f"[*] Patch `frida` to `{random_name}`")
 
     for symbol in binary.symbols:
         if symbol.name == "frida_agent_main":
             symbol.name = "main"
-
+
         if "frida" in symbol.name:
             symbol.name = symbol.name.replace("frida", random_name)
-
+
         if "FRIDA" in symbol.name:
             symbol.name = symbol.name.replace("FRIDA", random_name)
-
+
+    all_patch_string = ["FridaScriptEngine", "GLib-GIO", "GDBusProxy", "GumScript"]
+    for section in binary.sections:
+        if section.name != ".rodata":
+            continue
+        for patch_str in all_patch_string:
+            addr_all = section.search_all(patch_str)
+            for addr in addr_all:
+                patch = [ord(n) for n in list(patch_str)[::-1]]
+                log_color(f"[*] Patching section name={section.name} offset={hex(section.file_offset + addr)} orig:{patch_str} new:{''.join(list(patch_str)[::-1])}")
+                binary.patch_address(section.file_offset + addr, patch)
+
     binary.write(input_file)
-
-    # gum-js-loop thread
-    random_name = "".join(random.sample("abcdefghijklmn", 11))
-    print(f"[*] Patch `gum-js-loop` to `{random_name}`")
+
+    # thread_gum_js_loop
+    random_name = "".join(random.sample(random_charset, 11))
+    log_color(f"[*] Patch `gum-js-loop` to `{random_name}`")
     os.system(f"sed -b -i s/gum-js-loop/{random_name}/g {input_file}")
-
-    # gmain thread
-    random_name = "".join(random.sample("abcdefghijklmn", 5))
-    print(f"[*] Patch `gmain` to `{random_name}`")
-    os.system(f"sed -b -i s/gmain/{random_name}/g {input_file}")
+
+    # thread_gmain
+    random_name = "".join(random.sample(random_charset, 5))
+    log_color(f"[*] Patch `gmain` to `{random_name}`")
+    os.system(f"sed -b -i s/gmain/{random_name}/g {input_file}")
+
+    # thread_gdbus
+    random_name = "".join(random.sample(random_charset, 5))
+    log_color(f"[*] Patch `gdbus` to `{random_name}`")
+    os.system(f"sed -b -i s/gdbus/{random_name}/g {input_file}")
+
+    log_color(f"[*] Patch Finish")

Breaking down the .rodata reversal logic#

all_patch_string = ["FridaScriptEngine", "GLib-GIO", "GDBusProxy", "GumScript"]
for section in binary.sections:
    if section.name != ".rodata":
        continue
    for patch_str in all_patch_string:
        addr_all = section.search_all(patch_str)       # find all byte offsets within section
        for addr in addr_all:
            patch = [ord(n) for n in list(patch_str)[::-1]]   # reverse → list of ints
            binary.patch_address(section.file_offset + addr, patch)

Step by step for FridaScriptEngine (17 bytes):

Original bytes:  F  r  i  d  a  S  c  r  i  p  t  E  n  g  i  n  e
                46 72 69 64 61 53 63 72 69 70 74 45 6e 67 69 6e 65

Reversed bytes:  e  n  i  g  n  E  t  p  i  r  c  S  a  d  i  r  F
                65 6e 69 67 6e 45 74 70 69 72 63 53 61 64 69 72 46

Result string:   "enigEnpircSadirF"

The byte count is identical (17 bytes in, 17 bytes out), so no adjacent data is disturbed. The reversed string is nonsense to any forward-scanning string tool.

Why not use random bytes? Reversal guarantees the same length with zero risk of accidentally generating a valid keyword or partial match. It also means the operation is fully reproducible from the original string if needed during debugging — no random state needs to be saved.

Complete state of anti-anti-frida.py after this patch:

import lief, sys, random, os

def log_color(msg):
    print(f"\033[1;31;40m{msg}\033[0m")

if __name__ == "__main__":
    input_file = sys.argv[1]
    random_charset = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"
    log_color(f"[*] Patch frida-agent: {input_file}")

    binary = lief.parse(input_file)
    if not binary:
        log_color(f"[*] Not elf, exit")
        exit()

    random_name = "".join(random.sample(random_charset, 5))
    log_color(f"[*] Patch `frida` to `{random_name}`")

    for symbol in binary.symbols:
        if symbol.name == "frida_agent_main":
            symbol.name = "main"
        if "frida" in symbol.name:
            symbol.name = symbol.name.replace("frida", random_name)
        if "FRIDA" in symbol.name:
            symbol.name = symbol.name.replace("FRIDA", random_name)

    all_patch_string = ["FridaScriptEngine", "GLib-GIO", "GDBusProxy", "GumScript"]
    for section in binary.sections:
        if section.name != ".rodata":
            continue
        for patch_str in all_patch_string:
            addr_all = section.search_all(patch_str)
            for addr in addr_all:
                patch = [ord(n) for n in list(patch_str)[::-1]]
                log_color(f"[*] Patching section name={section.name} offset={hex(section.file_offset + addr)} orig:{patch_str} new:{''.join(list(patch_str)[::-1])}")
                binary.patch_address(section.file_offset + addr, patch)

    binary.write(input_file)

    # thread_gum_js_loop
    random_name = "".join(random.sample(random_charset, 11))
    log_color(f"[*] Patch `gum-js-loop` to `{random_name}`")
    os.system(f"sed -b -i s/gum-js-loop/{random_name}/g {input_file}")

    # thread_gmain
    random_name = "".join(random.sample(random_charset, 5))
    log_color(f"[*] Patch `gmain` to `{random_name}`")
    os.system(f"sed -b -i s/gmain/{random_name}/g {input_file}")

    # thread_gdbus
    random_name = "".join(random.sample(random_charset, 5))
    log_color(f"[*] Patch `gdbus` to `{random_name}`")
    os.system(f"sed -b -i s/gdbus/{random_name}/g {input_file}")

    log_color(f"[*] Patch Finish")

After Patching — Behavior on Android#

Static string scan — all four .rodata strings gone:

$ adb shell strings /data/local/tmp/frida-agent-patched-64.so | grep -E "FridaScriptEngine|GLib-GIO|GDBusProxy|GumScript"
(no output)

$ adb shell strings /data/local/tmp/frida-agent-patched-64.so | grep -E "enigEn|OIG-biLG|yxorPsuBDG|tpircSmuG"
enigEnpircSadirF
OIG-biLG
yxorPsuBDG
tpircSmuG

The reversed strings are present at the same file offsets — nothing shifted — but are completely opaque to any forward-scanning string matcher.

Thread listing — all three Frida threads now have random names:

$ adb shell ps -T -p $(pidof com.example.targetapp) | grep -E "gum-js-loop|gmain|gdbus"
(no output)

$ adb shell ps -T -p $(pidof com.example.targetapp)
USER           PID   TID PPID  VSZ   RSS WCHAN         PC S NAME
u0_a123      12345 12345 1234  2.1G  45M futex_wait    0  S com.example.targetapp
u0_a123      12345 12346 1234  2.1G  45M futex_wait    0  S Jit thread pool
u0_a123      12345 12347 1234  2.1G  45M futex_wait    0  S RenderThread
u0_a123      12345 12351 1234  2.1G  45M futex_wait    0  S xKbQmTzaRc1
u0_a123      12345 12352 1234  2.1G  45M futex_wait    0  S nLpWj
u0_a123      12345 12353 1234  2.1G  45M futex_wait    0  S jRmNb

Build-time console output (colored red in terminal):

[*] Patch frida-agent: ./out/frida-agent-64.so
[*] Patch `frida` to `xKbQm`
[*] Patching section name=.rodata offset=0x1a3f20 orig:FridaScriptEngine new:enigEnpircSadirF
[*] Patching section name=.rodata offset=0x1a4100 orig:GLib-GIO new:OIG-biLG
[*] Patching section name=.rodata offset=0x1a4280 orig:GDBusProxy new:yxorPsuBDG
[*] Patching section name=.rodata offset=0x1a4350 orig:GumScript new:tpircSmuG
[*] Patch `gum-js-loop` to `xKbQmTzaRc1`
[*] Patch `gmain` to `nLpWj`
[*] Patch `gdbus` to `jRmNb`
[*] Patch Finish

The combined effect across all patches so far is that frida-agent.so no longer contains any of its original identifying strings — neither in the symbol table, nor in .rodata, nor as live thread names in the kernel’s task list.


Noob Section (In Simple Words)#

The problem:

Even after earlier patches cleaned up the function names and thread names, the Frida agent library still had four readable strings buried in its data section: FridaScriptEngine, GLib-GIO, GDBusProxy, and GumScript. These are internal class/type names that Frida and its libraries use, and any scanner searching the file for Frida-related keywords would find them. On top of that, there was a third thread name (gdbus) that hadn’t been randomized yet, and the random character pools used in earlier patches were too small and predictable (all-uppercase or all-lowercase).

What this patch does:

Three improvements in one go. First, the character pool for generating random names is expanded to the full alphabet (uppercase + lowercase, 52 characters) so generated names look more natural. Second, the four data-section strings are reversed in place, so FridaScriptEngine becomes enigEnpircSadirF, GDBusProxy becomes yxorPsuBDG, and so on. Reversing keeps them the same length so nothing in the file shifts. Third, the gdbus thread name gets the same random-replacement treatment that gum-js-loop and gmain already got.

The result:

A scanner searching for any of those four strings finds nothing readable. The reversed versions are gibberish to any forward-scanning tool. The gdbus thread now has a random name too, and all random names use a much larger character set that blends in better. At this point, the agent binary has no Frida-related text left in its function list, data section, or thread names.