Bypassing Anti Frida on Android
This article explores a powerful technique to overcome these defenses: patching native libraries. By directly modifying the application's core components, we can effectively circumvent anti-Frida mechanisms and gain deeper insights into the app's functionality.
The world of reverse engineering is a constant battleground. Android applications are becoming increasingly sophisticated in protecting themselves against analysis tools like Frida. Frida, a powerful dynamic instrumentation toolkit, has emerged as a formidable weapon in the reverse engineer's arsenal. However, security researchers are always finding new ways to overcome these challenges.
For this article, we will assume using the com.redacted.mobile APK.
Understanding Anti-Frida Techniques
To effectively bypass anti-Frida protections, it's essential to understand the approaches usually used by application developers. Some of the most common anti-Frida measures are:
- Integrity checks: Applications can identify Frida's alterations by verifying the integrity of their own code or specific system libraries.
- Detection of Frida's presence: Applications may attempt to detect the presence of Frida by looking for certain memory patterns, API calls, or other signs.
- Obfuscation: Code obfuscation can make it difficult to understand and analyze an application's behavior, making it harder to identify vulnerable points.
- Runtime checks: Applications may perform runtime checks to detect tampering or unauthorized modifications.
Analyzing the Binary
Usually, the basic logic of anti-frida lies in the Binary. Whether it's in Java in the APK or in the Native Library. However, in general, anti-frida logic is found in the Native Library. Because there are many things that can be done when using Native Library. For this article, we will focus on the assumption that anti-frida lie in the Native Library.
One effective way to bypass anti-Frida protections is by directly modifying the application's native libraries. Native libraries, typically written in C or C++, often contain critical functionality that can be difficult to obfuscate. By patching these libraries, we can alter the application's behavior in a way that makes it easier to analyze.
The APK
Before going any further, let's first see what happens when we run the application with Frida.
As you can see, when the app detects Frida, it immediately crashes. By reading the backtrace, the cause of the crash is in the libalib.so library. We will investigate this later.
Let's open the com.redacted.mobile
APK with JADX-GUI, and search for frida
string.
As you can see, there is no logic coding for anti-frida. Instead, we only find a plain string.
This means that our target point for patching is not there. Let's try checking in the native library.
Let's search for System.loadLibrary
string.
System.loadLibrary
is a method in Java used to load native libraries (usually written in C or C++) at runtime. These libraries are often compiled into platform-specific shared libraries (like .so
files on Linux/Android, .dll
files on Windows, and .dylib
files on macOS).As you can see, there are many native libraries loaded. Let's take a look at alib
library.
The Native Library
To get the libalib file, we have to extract the contents of the APK. Let's decompile the APK with apktool
:
$ apktool d redacted.apk
Then, go to Decompiled Directory > lib > {arm_version} > libalib.so
For example, we will use the arm64 version of libalib, which you can find in the arm64-v8a directory.
Next, open libalib.so
with Ghidra.
If you don't have Ghidra, you can download it from here: https://ghidra-sre.org/
Next, go to Search > Program Text. And do search all for "Frida".
There's a lot of Frida Check function: frida-thread-check
, frida-named-check
, frida-memdisk-compare-check
, frida-check
, and frida-port-check
.
Let's check one of the functions, frida-trhead-check
.
undefined8 FUN_00123190(undefined8 param_1)
{
char *pcVar1;
char *pcVar2;
char *__ptr;
long lVar3;
__ptr = (char *)FUN_00121100("\"frida-thread-check\":",param_1);
if (*__ptr != '\0') {
lVar3 = 0;
do {
lVar3 = lVar3 + 1;
} while (__ptr[lVar3] != '\0');
if (lVar3 != 0) {
lVar3 = 0;
do {
pcVar1 = &DAT_00149aa0 + lVar3;
pcVar2 = __ptr + lVar3;
lVar3 = lVar3 + 1;
if (*pcVar1 != *pcVar2) {
lVar3 = 0;
while( true ) {
pcVar1 = "false" + lVar3;
pcVar2 = __ptr + lVar3;
lVar3 = lVar3 + 1;
if (*pcVar1 != *pcVar2) break;
if (*pcVar1 == '\0') {
free(__ptr);
return 0;
}
}
break;
}
} while (*pcVar1 != '\0');
}
}
free(__ptr);
return 1;
}
This function is likely used to perform validation on the string received via the param_1
parameter. It checks if the string contains “frida-thread-check”, then compares the result with the data present at address DAT_00149aa0
. If there is a difference, the string is checked if it contains “false”. If it does, the function returns 0, otherwise it returns 1.
So, the idea to bypass this function is... Let's change the return to 0. So, the function will always return false.
To do that, we can Patch Instruction. Right Click on w19,#0x1
, then choose "Patch Instruction".
Change the 0x1
to 0x0
.
Usually, all functions produce a Boolean return, which means they can return true or false.
So, you can do the same for Frida's other check functions.
In our case, we have the string FRIDA. When we explore, this is used as a string to check the Frida Agent in process memory.
This function reads the /proc/self/maps
file, which contains information about the memory map of the running process.
/* WARNING: Globals starting with '_' overlap smaller symbols at the same address */
void FUN_00127b88(void)
{
char *pcVar1;
int iVar2;
FILE *__stream;
char *pcVar3;
char *__ptr;
void *__ptr_00;
char *pcVar4;
long lVar5;
char *pcVar6;
long lVar7;
int iVar8;
char *local_428;
char *local_420;
char *local_418;
void *local_410;
char acStack_408 [512];
char local_208 [2];
char local_206;
long local_8;
local_8 = ___stack_chk_guard;
__stream = fopen("/proc/self/maps","r");
if (__stream != (FILE *)0x0) {
iVar8 = 0;
LAB_00127bf4:
pcVar3 = fgets(acStack_408,0x200,__stream);
if (pcVar3 != (char *)0x0) {
while( true ) {
memset(local_208,0,0x200);
sscanf(acStack_408,"%lx-%lx %s",&local_428,&local_420,local_208);
if (((((local_206 != 'x') || (pcVar3 = strrchr(acStack_408,0x2f), pcVar3 == (char *)0x0)) ||
(pcVar3 + 1 == (char *)0x0)) ||
((local_208[0] != 'r' &&
(iVar2 = mprotect(local_428,(long)local_420 - (long)local_428,5), iVar2 != 0)))) ||
(iVar2 = strncmp(pcVar3 + 1,"libalib.so",10), pcVar1 = local_420, pcVar3 = local_428,
iVar2 == 0)) break;
__ptr = (char *)FUN_0012165c(&DAT_0014a0f8,"FRIDA",0);
local_418 = __ptr;
__ptr_00 = (void *)FUN_0012165c("FRIDA",&DAT_0014a108,"AGENT",0);
lVar7 = 0;
local_410 = __ptr_00;
do {
pcVar6 = *(char **)((long)&local_418 + lVar7);
if (*pcVar6 == '\0') {
lVar5 = 0;
}
else {
lVar5 = 0;
do {
lVar5 = lVar5 + 1;
} while (pcVar6[lVar5] != '\0');
}
iVar2 = 0;
pcVar4 = pcVar3;
do {
if (*pcVar4 == pcVar6[iVar2]) {
iVar2 = iVar2 + 1;
if ((int)lVar5 <= iVar2) break;
}
else {
iVar2 = 0;
}
pcVar4 = pcVar4 + 1;
} while (pcVar4 < pcVar1);
if (iVar2 == (int)lVar5) {
iVar8 = iVar8 + 1;
free(__ptr);
free(__ptr_00);
goto LAB_00127bf4;
}
lVar7 = lVar7 + 8;
} while (lVar7 != 0x10);
free(__ptr);
free(__ptr_00);
pcVar3 = fgets(acStack_408,0x200,__stream);
if (pcVar3 == (char *)0x0) goto LAB_00127d88;
}
goto LAB_00127bf4;
}
LAB_00127d88:
fclose(__stream);
if ((iVar8 != 0) &&
(__android_log_print(6,"appcamo",&DAT_001481f0,0x10020005), DAT_0016f9b8 == '\0')) {
DAT_0016f9b8 = '\x01';
}
}
if (local_8 == ___stack_chk_guard) {
return;
}
/* WARNING: Subroutine does not return */
__stack_chk_fail();
}
/proc/self/maps
is a file in the Linux /proc
filesystem that provides a snapshot of the memory map of the currently running process (in this case, the process reading the file).The file is useful for understanding how memory is allocated and used by a process. It lists all the memory regions that the process is using, such as the stack, heap, shared libraries, mapped files, and more.
The FUN_00127b88
function appears to be used to detect whether an application is being debugged using a tool like Frida. It does so by checking the memory map of the current process and looking for specific libraries or unusual access permissions. If there are any indications from Frida, this function logs the results, possibly to provide a warning or take further action.
Next, let's change the FRIDA string to other like "FRISA". To do that, we can use Patch Data.
Once everything is patched, let's save and recompile it to libalib.so
. We can use Export Program on File Menu.
Choose format: Original File.
Move Modified libalib to the Phone
There are many ways we can do to move the libalib that we have patched into the smartphone or more precisely into the application library.
You can decompile the APK, then overwrite the lib. Or you can also directly put it into the application directory installed on the smartphone.
However, in this case, we will try to move it directly to the application directory on the smartphone. Because the binary in this case comes with an integrity check, we can't recompile the APK, so the only way is to overwrite the library in the app directory on our smartphone. But, you need a rooted device to do this.
First of all, let's move the libalib.so to /data/local/tmp
with adb
. We move the lib to that directory cause it's writeable without root permission.
adb push libalib.so /data/local/tmp/
Now, let's find the app directory. We can use pm
to do this.
First, let's check our app package name.
$ pm list packages
We can use pm path <package_name>
to find our app directory.
$ pm path <package_name>
package:/data/app/~~fu3kUwW607LDjcqpPITHfg==/com.{REDACTED}-OCLm5ho8Mi0cDl8VkiYBJg==/base.apk
$
Now, access that directory with superuser or root permission.
Copy our patched libalib at /data/local/tmp/libalib.so
to current directory.
cp /data/local/tmp/libalib.so .
Moment of Truth
Now, we can try running the app with Frida again.
TADAA!! We've successfully bypassed the Anti-Frida function.
That's all for this article, in closing let me quote the magic words of my hero.
If we can run frida , then we in a god mode. -Risky W
Haha! The quote means that, by successfully running an application with Frida, we can do anything with the application. We can bypass special functions, hook functions, change data, literally anything!