The FireShell Security Team was responsible for BHack CTF for one more year, and this year was my first time creating challenges. After thinking about and researching an interesting mobile challenge, I discovered an interesting trick to bypass freeRASP, and then the “Flag Validator” challenge was born. In this article, I will dive deep into the process of discovering this bypass trick and the write-up for the challenge.
RASP stands for Runtime Application Security Protection. In the mobile context, it’s an SDK or library used to check dangerous behavior using runtime instrumentation, blocking and detecting attacks or unsafe environments. In this article, I will talk specifically about freeRASP.
Looking at the definition from Talsec:
freeRASP is a lightweight and easy-to-integrate mobile security library designed to protect apps from potential threats during the application’s runtime. It contains multiple security checks, each aimed at covering possible attack vectors to ensure high application security.
Also, looking at the documentation, it’s possible to see that it provides protection against a bunch of different behaviors, such as:
- Using rooted or jailbroken devices (e.g., su, Magisk, unc0ver, check1rain, Dopamine).
- Reverse engineering attempts.
- Running hooking frameworks (e.g., Frida, Xposed or Shadow).
- Tampering or repackaging the application.
- Installing the app through untrusted methods/unofficial stores.
- Running the app in various emulators.
More details of freeRASP could be obtained from the repository or the official documentation.
Implementation and callback Handling
The key feature that makes bypassing this RASP more difficult is the detection based on security threats via a callback mechanism. The implementation code of this detection is similar to the following snippet:
class TalsecApplication : Application(), ThreatListener.ThreatDetected {
// ...
override fun onRootDetected() {
Log.e("RASP", "Root Detected")
exitProcess(0) // Close App
}
// ...
}
To check all available methods and the entire implementation refer to code from FreeRASPDemoApp.
Common Bypassing Techniques
Before bypassing freeRASP, let’s take a look at how we can bypass other Security SDKs. A common approach to bypass checks are usually performed by intercepting the call to functions with validations, and just overwriting the return or the whole function in some cases. As an example, we can take a look at RootBeer, a very large usage library to detect Root on devices. RootBeer’s implementation usually uses the function isRooted()
to perform a series of detections and searches, such as su
binary and Magisk App.
RootBeer rootBeer = new RootBeer(context);
if (rootBeer.isRooted()) {
//we found indication of root
} else {
//we didn't find an indication of root
}
As the point of validation is a single function that returns a boolean
value, we can easily hook this function call using Frida and overwrite the return to false
.
Java.perform(function() {
// Get a reference to the "RootBeer" class from the app's package.
var RootBeer = Java.use("com.scottyab.rootbeer.RootBeer");
// Override the implementation of the "isRooted()" method in the RootBeer class.
RootBeer.isRooted.implementation = function() {
console.log("[*] Bypassing RootBeer.isRooted()");
// Always return "false"
return false;
};
});
This approach works in MANY cases, but on the RASP that we are looking for we have a different behavior. The callback can be triggered from different locations, and we have a lot of different obfuscated functions and classes used to detect and protect the mobile. To build a bypass we will need a lot of code to cover all the validations, and this code would be broken by launching a new version updating the obfuscation of SDK. So… How can we build a single script dynamic enough to bypass all the validations?
Bypassing freeRASP Callbacks
When I was implementing freeRASP, I filled the callback functions with exitProcess(0)
, as the following code snippet shows:
//
30 override fun onRootDetected() {
31 exitProcess(0)
32 }
33
34 override fun onDebuggerDetected() {
35 exitProcess(0)
36 }
37
38 override fun onEmulatorDetected() {
39 exitProcess(0)
40 }
41
42 override fun onTamperDetected() {
43 exitProcess(0)
44 }
45
46 override fun onUntrustedInstallationSourceDetected() {
47 exitProcess(0)
48 }
49
50 override fun onHookDetected() {
51 exitProcess(0)
52 }
53
54 override fun onDeviceBindingDetected() {
55 exitProcess(0)
56 }
57
58 override fun onObfuscationIssuesDetected() {
59 exitProcess(0)
60 }
61
62 override fun onMalwareDetected(p0: MutableList<SuspiciousAppInfo>?) {
63 exitProcess(0)
64 }
65 }
An interesting tip for Kotlin Obfuscated code is that the exitProcess(0)
becomes the function System.exit()
followed by and RuntimeException
.
System.exit(0);
throw new RuntimeException("System.exit returned normally, while it was supposed to halt JVM.");
So, after built the APK, reversed the code using jadx, and searched for this pattern, I found the following very interesting code:
134 switch (c3) {
135 case 0:
136 ((TalsecApplication) dVar).getClass();
137 System.exit(0);
138 throw new RuntimeException("System.exit returned normally, while it was supposed to halt JVM.");
139 case 1:
140 ((TalsecApplication) dVar).getClass();
141 System.exit(0);
142 throw new RuntimeException("System.exit returned normally, while it was supposed to halt JVM.");
143 case 2:
144 ((TalsecApplication) dVar).getClass();
145 System.exit(0);
146 throw new RuntimeException("System.exit returned normally, while it was supposed to halt JVM.");
147 case AbstractC0857b.f7554e /* 3 */:
148 ((TalsecApplication) dVar).getClass();
149 System.exit(0);
150 throw new RuntimeException("System.exit returned normally, while it was supposed to halt JVM.");
151 case 4:
152 ((TalsecApplication) dVar).getClass();
153 System.exit(0);
154 throw new RuntimeException("System.exit returned normally, while it was supposed to halt JVM.");
155 case 5:
156 ((TalsecApplication) dVar).getClass();
157 System.exit(0);
158 throw new RuntimeException("System.exit returned normally, while it was supposed to halt JVM.");
159 case 6:
160 ((TalsecApplication) dVar).getClass();
161 System.exit(0);
162 throw new RuntimeException("System.exit returned normally, while it was supposed to halt JVM.");
163 case 7:
164 ((TalsecApplication) dVar).getClass();
165 System.exit(0);
166 throw new RuntimeException("System.exit returned normally, while it was supposed to halt JVM.");
So we have a very similar structure in the both code, and that was a sign that I was going to the right direction. Obviously, I only noticed this because I implemented it, but this highlights my main tip to bypass open-source or free and available code: Implement it and understand what’s happening.
Looking more at this function I realized that the “callback” is nothing more than a BroadcastReceiver that receives an extra string param with probably an identifier of detection. After some obfuscated string checks the code identified the detection and handled it as the developer implemented.
// m1.e
12 public final class e extends BroadcastReceiver {
// ...
30 public final void onReceive(Context context, Intent intent) {
31 char c3;
32 if (intent == null || !intent.hasExtra(AbstractC0857b.c(AbstractC0857b.h("FD81C3788D06D99B4F")))) {
33 return;
34 }
35 String stringExtra = intent.getStringExtra(AbstractC0857b.c(AbstractC0857b.h("FD81C3788D06D99B4F")));
36 if (context != null) {
37 switch (stringExtra.hashCode()) {
Another way to prove this is running the app and hooking all Intents using for example the android-intent-monitor script.
To better understand what’s happening, we need to clarify what a Broadcast is in Android. According to the Android documentation, it is defined as follows:
Android apps send and receive broadcast messages from the Android system and other Android apps, similar to the publish-subscribe design pattern. The system and apps typically send broadcasts when certain events occur. For example, the Android system sends broadcasts when various system events occur, such as system boot or device charging. Apps also send custom broadcasts, for example, to notify other apps of something that might interest them (for example, new data download).
Additionally, the onReceive function takes two parameters Context
and Intent
.
An Intent in Android has two main primary pieces of information: the action and the data. The action is a string that describes the action to be performed, and the data is a URI that describes the data to be acted upon. On the code we can see that getAction()
functions, retrieves the string TALSEC_INFO
, which is the action that the freeRASP is using to send the detection information.
In addition to this, the Intent has secondary attributes, such as the category, type, component and extras. Focusing in this last one, the extras are defined in documentation as:
This is a Bundle of any additional information. This can be used to provide extended information to the component. For example, if we have a action to send an e-mail message, we could also include extra pieces of data here to supply a subject, body, etc.
Summarizing, the extras are used to pass information to the component that is going to receive the Intent. In our case, the freeRASP is using the extras to send the detection information, which is retrieved by getStringExtra()
function.
After all this concepts, we can finally understand how bypass this protection. We just need to hook the function getStringExtra()
and overwrite the return value when the action is TALSEC_INFO
. Looks simples, right? And it really is!!! The final bypass code is:
Java.perform(function () {
// Get a reference to "Intent" class from the Android framework.
var Intent = Java.use("android.content.Intent");
// Override the "getStringExtra(String)" method in the Intent class.
Intent.getStringExtra.overload("java.lang.String").implementation = function (
str
) {
// Call the original implementation of "getStringExtra" to get the actual value of the extra.
let extra = this.getStringExtra(str);
// Retrieve the action associated with the current Intent.
let action = this.getAction();
// Check if the action of the Intent is "TALSEC_INFO".
if (action == "TALSEC_INFO") {
// Just printing stuff to debugging purpose
console.log(`[+] Hooking getStringExtra("${str}") from ${action}`);
console.log(`\t Bypassing ${extra} detection`);
// Override the extra value with an empty string
extra = "";
}
return extra;
};
});
- https://gist.github.com/luca-regne/6d32789e6bf1e355a14986a43272b290
- https://codeshare.frida.re/@luca-regne/android-freerasp-bypass/
After bypassing the protection we can finally start to analyze the app. The application has a very simple interface, we have an input that validates if the flag is right or not.
Searching for the string “Flag is incorrect!” in the code, we found the function E.r.a()
.
139 String A03 = L1.n.A0(arrayList3, "_", null, null, null, 62);
140 String str4 = dVar.f4253d;
141 if (str4 == null) {
142 AbstractC0857b.S("inputFormat");
143 throw null;
144 }
145 ((InterfaceC0063m0) obj2).setValue(AbstractC0857b.g(d2.g.H0(str4, "*", A03), str) ? "Flag is correct!" : "Flag is incorrect!");
146 ((S.f) ((S.e) obj)).a(false, true);
147 return;
148 }
149 }
The function gets a list of bytes and performs a series of operations and compare with user input. So the easiest way to solve this challenge is to hook the final function used to build the flag and compare with out input.
((InterfaceC0063m0) obj2).setValue(AbstractC0857b.g(d2.g.H0(str4, "*", A03), str) ? "Flag is correct!" : "Flag is incorrect!");
We can translate the code to something like this:
String flag = d2.g.H0(str4, "*", A03);
String retStr = AbstractC0857b.g(flag , str) ? "Flag is correct!" : "Flag is incorrect!");
((InterfaceC0063m0) obj2).setValue(retStr);
Now we have two options, hook AbstractC0857b.g
or d2.g.H0()
to get the plain text flag, on my script I used the last one. Then our final script is:
Java.perform(function () {
// Print empty line
console.log();
// Hook freeRASP "callbacks" and overwrite the
// return of getStringExtra() to empty string
var Intent = Java.use("android.content.Intent");
Intent.getStringExtra.overload("java.lang.String").implementation = function (
str
) {
let extra = this.getStringExtra(str);
let action = this.getAction();
if (action == "TALSEC_INFO") {
console.log(`[+] Hooking getStringExtra("${str}") from ${action}`);
console.log(`\\t Bypassing ${extra} detection`);
extra = "";
}
return extra;
};
// Hook final function used to build the flag, print params and return
Java.use("d2.g").H0.implementation = function (str, oldString, newString) {
let returnString = this.H0(str, oldString, newString);
console.log(
"[+] Hooked d2.g.H0",
"\\n\\tParams: ",
str,
oldString,
newString,
"\\n\\tReturn: ",
returnString
);
return returnString;
};
});
A fun fact about this challenge is that when I started creating it, I didn’t actually know how to bypass the protection. That’s why I implemented it first and then worked on bypassing it afterwards. This process was incredibly fun, and I learned so much along the way. I believe that approaching problems from a developer’s perspective is one of the best ways to truly understand how things work.
I hope this article has taught you something new and inspired you to dive deeper into mobile security. Thanks for reading, and as always — keep hacking! ;p