Bypassing freeRASP Callbacks - Flag Validator Write Up - CTF BHack 2024
2024-12-5 22:5:0 Author: fireshellsecurity.team(查看原文) 阅读量:7 收藏

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?

Confused Nazare

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.

Monitoring Intents from freeRASP

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;
  };
});

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.

Flag Validator App

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;
  };
});

Challenge Solve

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

That's all Folks

Capture the Flag , Mobile , Writeup


文章来源: https://fireshellsecurity.team/bhackctf2024-bypass-freerasp-callbacks/
如有侵权请联系:admin#unsafe.sh