( بِسْمِ اللَّـهِ الرَّحْمَـٰنِ الرَّحِيمِ )
CAUTION#FreePalestine
WARNINGThis is just for educational purposes.
Patch 5 — Randomizing the gmain Thread Name
Introduction
Alongside gum-js-loop, Frida’s agent spawns a second identifiable native thread called gmain. This is GLib’s main event loop thread — the GMainLoop that powers the agent’s control-channel message handling, timer callbacks, and internal IPC. Unlike gum-js-loop which only appears when the JavaScript engine is running, gmain is created earlier in the agent’s startup sequence and persists as long as the agent is alive. Its name appears in /proc/<pid>/task/<tid>/comm just as predictably as gum-js-loop, and it is equally well-known to Frida detection engineers.
This patch adds a third post-processing step to src/anti-anti-frida.py: after the gum-js-loop replacement, gmain is replaced in the compiled binary with a random 5-character lowercase string via the same sed -b -i technique.
The Original Function / String
String: gmain
Origin: GLib’s GMainLoop implementation. When g_main_loop_run() is called from a dedicated thread, GLib assigns the thread the name gmain via pthread_setname_np(). Frida’s agent startup code creates exactly such a thread to drive the GLib event loop that handles agent lifecycle events.
Location in binary: the string literal "gmain" lives in the .rodata section of frida-agent.so, contributed by GLib which is statically linked into the agent.
The key distinction from gum-js-loop:
| Thread | Created by | Lifetime |
|---|---|---|
gum-js-loop | GumJS scheduler | While JS engine is active |
gmain | GLib GMainLoop | From agent init until agent teardown |
gmain has a longer observable window — it is present from the moment the agent is injected and initialised, even before any JavaScript session is established. A detector that polls thread names at injection time will see gmain before it ever sees gum-js-loop.
Why it is a detection surface:
gmainis 5 characters — short, memorable, and easy to match with a simplestrcmp.- No legitimate Android framework thread uses the name
gmain. It is not a system thread name. - Because it is a GLib-internal name assigned programmatically, it cannot be changed at the Vala/C source level without patching GLib itself — making a binary-level replacement the only viable approach without forking the entire dependency.
gmainis shorter thangum-js-loop, which means it also appears as a substring in other GLib-related strings (e.g.gmainloop, internal debug messages). Thesed -b -iglobal replacement handles all occurrences at once.
Before Patching — Behavior on Android
Thread listing showing both Frida threads:
$ 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 gum-js-loop
u0_a123 12345 12352 1234 2.1G 45M futex_wait 0 S gmainReading gmain directly from the /proc task comm file:
$ adb shell cat /proc/12345/task/12352/comm
gmaingmain appears even before any script is loaded — checking immediately after injection, before frida.attach() fully completes a session:
$ adb shell
android:/ # cat /proc/12345/task/12352/comm
gmain
android:/ # cat /proc/12345/task/12351/comm
cat: /proc/12345/task/12351/comm: No such file or directoryThe gmain thread (TID 12352) is already running; gum-js-loop (TID 12351) has not been created yet because no script has been attached. This makes gmain detectable at the earliest possible point in the injection lifecycle.
Confirming the string in the binary:
$ adb shell strings /data/local/tmp/frida-agent-64.so | grep -w gmain
gmainHow Apps Detect It
Method 1 — Poll /proc/self/task at startup and periodically (C++)
Because gmain appears before gum-js-loop, detectors often run a background watchdog that starts polling immediately at JNI_OnLoad time:
#include <stdio.h>
#include <string.h>
#include <dirent.h>
#include <pthread.h>
#include <unistd.h>
static const char *FRIDA_THREAD_NAMES[] = {
"gum-js-loop",
"gmain",
NULL
};
bool detectFridaThreadNames() {
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];
char 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_THREAD_NAMES[i] != NULL; i++) {
if (strcmp(comm, FRIDA_THREAD_NAMES[i]) == 0) {
closedir(task_dir);
return true;
}
}
}
closedir(task_dir);
return false;
}
// Watchdog thread: keeps polling every 500ms
void *watchdog(void *arg) {
while (1) {
if (detectFridaThreadNames()) {
// Trigger tamper response
raise(SIGKILL);
}
usleep(500000);
}
return NULL;
}Method 2 — Timing-based early detection at JNI_OnLoad (C++ / JNI)
Since gmain is created before any JS session, a check in JNI_OnLoad can catch Frida before it has had a chance to hook anything:
JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM *vm, void *reserved) {
// Check for Frida's gmain thread immediately at library load
if (detectFridaThreadNames()) {
// Self-destruct or trigger enforcement before any hooks land
abort();
}
// ... normal initialisation ...
return JNI_VERSION_1_6;
}Method 3 — Combined thread name set check (Kotlin + JNI)
Some SDKs pair the Java Thread enumeration with a native /proc check, expecting to find a specific set of suspicious thread names together:
object FridaThreadDetector {
private val SUSPICIOUS_NAMES = setOf("gum-js-loop", "gmain", "frida")
// Native call drops to C++ /proc scanner for accurate results
external fun nativeGetThreadNames(): Array<String>
fun detect(): Boolean {
val names = nativeGetThreadNames().toSet()
return names.intersect(SUSPICIOUS_NAMES).isNotEmpty()
}
}The Patch
What changed: A third block is appended to src/anti-anti-frida.py, running immediately after the gum-js-loop replacement:
## 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}")Full diff:
--- a/src/anti-anti-frida.py
+++ b/src/anti-anti-frida.py
@@ -29,4 +29,9 @@ if __name__ == "__main__":
# gum-js-loop thread
random_name = "".join(random.sample("abcdefghijklmn", 11))
print(f"[*] Patch `gum-js-loop` to `{random_name}`")
- os.system(f"sed -b -i s/gum-js-loop/{random_name}/g {input_file}")
+ 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}")Why random.sample("abcdefghijklmn", 5) for gmain:
gmain is 5 bytes. As with gum-js-loop in Patch 4, the replacement must be exactly the same length as the original string to avoid shifting downstream bytes in the binary. random.sample without repetition from 14 lowercase characters produces names like kecbf, mjald, ngbhc — opaque 5-character strings with no semantic meaning.
Why sed -b is used instead of LIEF:
gmain is a raw C string literal in .rodata, not an ELF symbol entry. LIEF’s symbol iteration API does not reach into section data to find and replace arbitrary byte sequences. sed -b (binary mode, suppresses CRLF translation) replaces all occurrences of the exact byte sequence gmain anywhere in the file — this covers the .rodata occurrence, any occurrence in .dynstr if the symbol also appears there, and any debug section occurrences.
The complete anti-anti-frida.py script at this point in the patch series:
import lief, sys, random, os
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}`")
binary = lief.parse(input_file)
if not binary:
exit()
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)
binary.write(input_file)
# gum-js-loop thread
random_name = "".join(random.sample("abcdefghijklmn", 11))
print(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}")Sample build-time console output after both thread patches are in place:
[*] Patch frida-agent: ./out/frida-agent-64.so
[*] Patch `frida` to `NCHBM`
[*] Patch `gum-js-loop` to `kcfbdhigjml`
[*] Patch `gmain` to `kecbf`After Patching — Behavior on Android
Thread listing — both Frida threads now show random names:
$ 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 kcfbdhigjml
u0_a123 12345 12352 1234 2.1G 45M futex_wait 0 S kecbf/proc comm files:
android:/ # cat /proc/12345/task/12352/comm
kecbf
android:/ # cat /proc/12345/task/12351/comm
kcfbdhigjmlStatic strings scan — no gmain:
$ adb shell strings /data/local/tmp/frida-agent-patched-64.so | grep -w gmain
(no output)
$ adb shell strings /data/local/tmp/frida-agent-patched-64.so | grep "kecbf"
kecbfEarly JNI_OnLoad detection — clean:
// Called immediately at library load, before any Frida hooks
detectFridaThreadNames(); // returns false
// "gmain" → "kecbf" and "gum-js-loop" → "kcfbdhigjml" — neither matchesWith both gum-js-loop and gmain randomized, the full set of Frida-specific thread names that detection SDKs enumerate is exhausted. The agent is operationally intact; the GLib event loop and GumJS scheduler run normally under their new opaque names.
Noob Section (In Simple Words)
The problem:
Frida creates a second recognizable thread called gmain. This one handles Frida’s internal messaging and event system (powered by a library called GLib). Unlike gum-js-loop which only appears when JavaScript hooks are running, gmain shows up the moment Frida is injected, even before any script is loaded. That makes it detectable even earlier. Security tools that check thread names right at startup will catch gmain before they ever see gum-js-loop.
What this patch does:
The same sed find-and-replace technique from Patch 4 is used again. The 5 bytes of gmain in the compiled binary are swapped with a random 5-character string like kecbf. Same length, same idea.
The result:
Now both of Frida’s signature threads have random names. gum-js-loop became something like kcfbdhigjml (Patch 4) and gmain became something like kecbf (this patch). A security tool listing all threads in the app sees nothing Frida-related. Both threads still do their jobs normally, they just no longer advertise who they belong to.
