( بِسْمِ اللَّـهِ الرَّحْمَـٰنِ الرَّحِيمِ )
CAUTION#FreePalestine
WARNINGThis is just for educational purposes.
Patch 1 — Obfuscating the frida:rpc Protocol Identifier String
Introduction
Every Frida RPC call that travels between a host and an instrumented Android process carries a well-known protocol tag: the string frida:rpc. Because this tag is a hardcoded literal in Frida’s source, it ends up verbatim inside the compiled shared library (frida-agent.so) that gets injected into target processes. Any tool that can scan memory or intercept JSON messages sees it immediately, making it one of the most reliable Frida fingerprints available to detection engineers.
This patch, labeled “Florida: string_frida_rpc”, replaces every occurrence of the literal "frida:rpc" in lib/base/rpc.vala with a runtime call to a small helper that reconstructs the string on-the-fly via a double-decoded Base64 operation. The result is that the string never appears as a contiguous literal in the .rodata section of the final binary — it only materialises in process memory at the exact moment it is needed.
The Original Function / String
File: lib/base/rpc.vala
Namespace: Frida
Class: RpcClient
rpc.vala is Frida’s implementation of its host-to-agent remote procedure call protocol. Every time the host instrumentation script calls an exported function on the agent side (the classic rpc.exports pattern), a JSON array is constructed and sent down the transport channel. The first element of that array is the fixed namespace marker "frida:rpc", which both ends use to identify and validate messages as belonging to the Frida RPC protocol.
Before this patch the string appears literally in three places inside RpcClient:
// 1. Building an outgoing RPC request
request
.begin_array ()
.add_string_value ("frida:rpc") // ← hardcoded
...
// 2. Fast-path check on any incoming message
if (json.index_of ("\"frida:rpc\"") == -1) // ← hardcoded inside a quoted form
return false;
// 3. Strict type validation after full JSON parse
if (type == null || type != "frida:rpc") // ← hardcoded
return false;Because Vala compiles to C which is then compiled to a native shared object, these string literals are embedded directly into the .rodata (read-only data) section of frida-agent.so. strings(1), static binary scanners, and in-memory scanners all find them without any effort.
Why it is a detection surface:
frida-agent.sois injected into the target application’s address space. Once it is mapped, the.rodatasection is readable by any thread in that process.- A single
mmap+memcmploop across the process’s own loaded libraries is enough to locate the literal. - The string is unique enough (
frida:rpcdoes not appear in any legitimate Android framework library) that a single hit is a definitive indicator of Frida presence.
Before Patching — Behavior on Android
When an unpatched frida-agent.so is injected, the string lives in the binary and is also briefly present in heap allocations each time an RPC message is serialised. Both surfaces are exploitable by detectors.
Confirming the string in the pushed binary on-device:
$ adb push frida-agent.so /data/local/tmp/
frida-agent.so: 1 file pushed, 0 skipped
$ adb shell strings /data/local/tmp/frida-agent.so | grep -i "frida:rpc"
frida:rpcConfirming it in a live injected process (pid 12345 = target app):
$ adb shell
android:/ # grep -c "frida-agent" /proc/12345/maps
1
android:/ # cat /proc/12345/maps | grep frida
7a3c000000-7a3c800000 r--p 00000000 fc:00 131072 /data/local/tmp/frida-agent.so
android:/ # dd if=/proc/12345/mem bs=1 skip=$((0x7a3c000000)) count=$((0x800000)) 2>/dev/null \
| strings | grep "frida:rpc"
frida:rpcThe string is unmistakable and appears at a stable offset relative to the base load address of the agent library.
How Apps Detect It
Detection routines typically operate at two layers simultaneously: static (scanning the mapped file or .rodata segment) and dynamic (intercepting or monitoring JSON messages).
Layer 1 — In-memory .rodata scan (C/C++ native detector)
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
// Iterate /proc/self/maps, find every loaded .so, map it, search for the literal.
bool hasFridaRpcString() {
const char *needle = "frida:rpc";
const size_t needle_len = strlen(needle);
FILE *maps = fopen("/proc/self/maps", "r");
if (!maps) return false;
char line[512];
while (fgets(line, sizeof(line), maps)) {
// Only care about readable, file-backed mappings
if (line[3] != 'p') continue; // skip shared mappings
if (strstr(line, ".so") == NULL) continue;
unsigned long start, end;
sscanf(line, "%lx-%lx", &start, &end);
const uint8_t *region = (const uint8_t *)start;
size_t region_size = end - start;
for (size_t i = 0; i + needle_len < region_size; i++) {
if (memcmp(region + i, needle, needle_len) == 0) {
fclose(maps);
return true; // Frida detected
}
}
}
fclose(maps);
return false;
}Layer 2 — Message-level detection (Java, within a WebSocket or socket bridge)
Some apps expose their own JS-to-Java bridge over a local socket. If Frida’s RPC layer is observed communicating through it, the app can sniff messages:
// MessageInterceptor.java (simplified)
public class MessageInterceptor {
private static final String FRIDA_RPC_MARKER = "frida:rpc";
public boolean isFridaRpcMessage(String jsonPayload) {
// Frida always places "frida:rpc" as the first element
// of the JSON array it sends.
return jsonPayload != null && jsonPayload.contains(FRIDA_RPC_MARKER);
}
public void onMessageReceived(String json) {
if (isFridaRpcMessage(json)) {
triggerTamperResponse();
}
}
private void triggerTamperResponse() {
// Log, crash, report to server, etc.
throw new SecurityException("Frida RPC detected");
}
}Both layers are defeated by the same root fix: ensure the string frida:rpc never appears as a contiguous sequence of bytes anywhere in the static binary.
The Patch
What changed: All three hardcoded occurrences of "frida:rpc" in lib/base/rpc.vala are replaced by calls to a new helper method getRpcStr(bool quote) which reconstructs the string at runtime through a double Base64 decode.
The helper method introduced:
public string getRpcStr(bool quote){
string result = (string) GLib.Base64.decode(
(string) GLib.Base64.decode("Wm5KcFpHRTZjbkJq")
);
if(quote){
return "\"" + result + "\"";
}else{
return result;
}
}Decoding the double-encoded constant:
Step 1 — inner decode:
Base64("Wm5KcFpHRTZjbkJq") → "ZnJpZGE6cnBj"
Step 2 — outer decode:
Base64("ZnJpZGE6cnBj") → "frida:rpc"The only string that now appears as a literal in the compiled binary is "Wm5KcFpHRTZjbkJq" — an opaque Base64 blob that carries no semantic meaning to a static scanner. Neither frida, nor rpc, nor the colon separator appears anywhere in that encoded form.
The quote parameter handles the two variants the original code needed:
getRpcStr(false)→frida:rpc— used when building the outgoing JSON array element and when doing strict equality comparison after parsing.getRpcStr(true)→"frida:rpc"— used for the fast-pathindex_ofcheck that searches the raw JSON string including its surrounding quotation marks.
Full diff:
--- a/lib/base/rpc.vala
+++ b/lib/base/rpc.vala
@@ -11,13 +11,22 @@ namespace Frida {
Object (peer: peer);
}
+ public string getRpcStr(bool quote){
+ string result = (string) GLib.Base64.decode((string) GLib.Base64.decode("Wm5KcFpHRTZjbkJq"));
+ if(quote){
+ return "\"" + result + "\"";
+ }else{
+ return result;
+ }
+ }
+
public async Json.Node call (string method, Json.Node[] args, Bytes? data, Cancellable? cancellable) throws Error, IOError {
string request_id = Uuid.string_random ();
var request = new Json.Builder ();
request
.begin_array ()
- .add_string_value ("frida:rpc")
+ .add_string_value (getRpcStr(false))
.add_string_value (request_id)
@@ -70,7 +79,7 @@ namespace Frida {
public bool try_handle_message (string json) {
- if (json.index_of ("\"frida:rpc\"") == -1)
+ if (json.index_of (getRpcStr(true)) == -1)
return false;
@@ -99,7 +108,7 @@ namespace Frida {
- if (type == null || type != "frida:rpc")
+ if (type == null || type != getRpcStr(false))
return false;Why this does not break Frida’s functionality:
The RPC protocol itself is unchanged. Both the host side and the agent side go through the same patched build, so they both reconstruct the identical string "frida:rpc" at runtime and the protocol handshake succeeds exactly as before. The decode is cheap (two tiny Base64 operations) and happens per-message, which introduces negligible overhead compared to the JSON serialisation cost that already surrounds it.
After Patching — Behavior on Android
Static scan of the patched binary — no hit:
$ adb push frida-agent-patched.so /data/local/tmp/
frida-agent-patched.so: 1 file pushed, 0 skipped
$ adb shell strings /data/local/tmp/frida-agent-patched.so | grep -i "frida:rpc"
(no output)
$ adb shell strings /data/local/tmp/frida-agent-patched.so | grep "Wm5K"
Wm5KcFpHRTZjbkJqThe only string in .rodata is the opaque double-encoded blob. A signature-based scanner looking for frida:rpc finds nothing.
Live in-memory scan of the injected process — no hit:
android:/ # grep -c "frida-agent" /proc/12345/maps
1
android:/ # dd if=/proc/12345/mem bs=1 skip=$((0x7a3c000000)) count=$((0x800000)) 2>/dev/null \
| strings | grep "frida:rpc"
(no output)The decoded string frida:rpc does appear transiently on the heap at the moment an RPC call is in-flight, but it is short-lived and heap-allocated at a non-deterministic address — making a reliable real-time heap scan significantly more expensive and fragile compared to the trivial .rodata search that worked before.
RPC functionality confirmed still operational:
$ frida -U -n com.example.targetapp -e "
rpc.exports = { hello: function() { return 'world'; } };
"
$ frida-compile agent.js -o agent.js && frida -U ... --load agent.js
[Android::com.example.targetapp]-> rpc.exports.hello()
"world"The protocol negotiation completes successfully — the runtime-decoded frida:rpc tag matches on both ends, and detection based on the static string signature no longer fires.
Noob Section (In Simple Words)
The problem:
When Frida talks to an app it’s hooking, every message it sends includes a label that says "frida:rpc" think of it like writing “FROM: FRIDA” on every envelope you send. That label is also baked directly into the Frida library file sitting on the phone. So if the app simply searches its own memory or looks at the files loaded into it and finds the text frida:rpc, it instantly knows Frida is there.
What this patch does:
Instead of storing the readable text frida:rpc inside the file, the patch stores a scrambled version of it, encoded twice using Base64 (a way of turning text into unrecognizable gibberish). The scrambled version looks like Wm5KcFpHRTZjbkJq, completely meaningless to any scanner looking for frida:rpc.
When Frida actually needs to send a message, it unscrambles the gibberish back into frida:rpc on the spot, uses it, and then it’s gone. The readable text only exists for a split second in temporary memory, instead of being permanently written in the file.
The result:
Any security tool that searches for the text frida:rpc in the app’s files or memory will come up empty. Frida still works exactly the same, the messages still carry the right label so both sides understand each other, but the label is now hidden until the very moment it’s needed.
