August 24, 2021

    Misconceptions about Mobile Platform Security

    Many developers rely on the belief that their apps cannot be tampered with. They often lean on in-app purchases or ads to monetize their work and rely on a certain security level to provide a fun and safe playing environment to their customers. So if applications are modified, or in-app security checks can be bypassed, it can create a slew of challenges, ranging from security issues for developers and end users to impacting brand reputation and causing public relations problems.

    Basically, developers can lose customers and revenue.

    So what platform is more secure? It is generally known that Android applications are relatively easy to reverse engineer and can be modified by malicious actors. But it is a common misconception that the Apple iOS platform is inherently more secure in this respect. In general, applications on iOS are just as susceptible to being tampered with as ones on Android.

    Both Android and iOS platforms contain multiple strong security features, such as biometric authentication, secure local storage, and a permission system. But it’s important to note that those techniques — and even the more technical ones, like codesigning and sandboxing — are primarily focused on protecting the end-user, not the app itself.

    To demonstrate the misconceptions of mobile platform security, we will patch out the ads from an existing application on each platform and repackage them. We will also show how in-app purchase implementations can be bypassed with relative ease.

    Proof of concept: Tampering with “Flashlight” apps

    In this section we will show that, on both platforms, we can easily disable advertisements and fake purchases on both platforms using the following techniques:

    • Download the application to a computer to analyze where sensitive logic might reside.
    • Patch certain code and repackage the application, then run the modified version on a device.
    • Modify application logic while the application is running on the device, without the need to repackage it.

    By using these techniques, an attacker can expose the application developer to the previously mentioned risks.

    To show the limitations of the built-in platform security, we looked at some of the free “Flashlight” applications available for both platforms. These apps can be used to toggle the built-in flashlight and typically have an in-app purchase option to remove ads. They will serve as an example to show how these attacks are done in practice.

    iOS Flashlight app

    Android Flashlight app

    Our proof of concept consists of three parts: analyzing the apps, statically patching out the ads, and bypassing in-app purchases dynamically for both applications.

    Analyzing the apps

    First, we want to analyze the apps to figure out how the ads are loaded. To do this, we first need to download the apps to our computer.

    For iOS, we use the frida-ios-dump tool to retrieve the application from a device. For Android, we can pull it from a device in the same way, or use one of the numerous online tools, like https://apps.evozi.com/apk-downloader, which we used in this blog post. This gives us an IPA file for iOS and an APK file for Android.

    To analyze the internal logic of applications, typically a disassembler is used and optionally a decompiler.

    • A disassembler is a tool that translates the machine code instructions into human readable text.
    • A decompiler attempts to convert the machine code instructions into the original source code.

    For iOS, we use the Hopper disassembler and decompiler. For Android, we use the Smali disassembler and the JD decompiler.

    iOS

    On iOS, we open the app in Hopper and search for function names that contain “ads”. This shows us that the classALAdServiceis included. Googling this class brings us to theAppLovinSDK that is used by this app to load the ads.

    A look at the AppLovin demo application reveals that the main developer interaction is done through the “ALSdk” class:

    [ALSdk shared].mediationProvider = ALMediationProviderMAX;
    [[ALSdk shared] initializeSdkWithCompletionHandler:^(ALSdkConfiguration 
      *configuration) {
        // AppLovin SDK is initialized,
        // start loading ads now or later if ad gate is reached.
    }];



      Android

    We start by disassembling the APK and searching for mentions of “ad” in the code. We find the functionbilling.d.a(Context), shown in the screenshot below. If we decompile this function, we get something like:

    paramContext.getSharedPreferences("BillingSp", 0).getBoolean("ad", false);

    To check whether ads should be shown, the app calls this function, which retrieves theBillingSpshared preference containing theadboolean. This boolean value is used to decide whether ads should be shown or not.


    For some applications, we might not directly find anything useful when searching for “ads”, or the advertisement SDK might not be publicly available. In these cases, the analysis is still possible, but a bit more digging is needed. The attacker might start by just looking through all the function names, to see if anything interesting stands out. But there are also many other ways available to analyze applications, like attaching a debugger, looking at the strings and their usages in the binary, and analyzing the call graph, among others.

    Removing the ads and repackaging the apps

    Now that we understand how the ads are loaded, we will modify the apps to disable them. Since doing this changes the original application, its code signature becomes invalid. Both platforms only accept applications with valid signatures. But it’s possible to just repackage and re-sign the modified apps with our own certificates. That way we get a modified version of the app with a valid signature that can be installed on any device.

    Most popular disassemblers also have an assembler built in. This allows us to change actual instructions or values of the binary and write out the modified version of the application. By doing this, we can adjust the application logic to our liking.

    iOS

    During our analysis, we found theinitializeSdkWithCompletionHandlermethod, which is used to set up the ad SDK. If we modify this function to exit / return immediately (retfor ARM), we ensure the setup code is never called. Thus preventing ads from ever loading

    We then use zsign to modify the bundle ID of the app to one of ours and resign the patched application. We now have a modified application with a valid code signature which can be installed on an iPhone.

    When we start it, we see that the ads are not loaded anymore.

      Android

    We learned that thebilling.d.a(Context)method is queried to decide whether the ads should be shown. If it returnstrue, ads will be hidden.

    Looking at thegetBooleanAPI  shows us that the second parameter is a default value, which is returned if the preference does not exist. In this app, theBillingSpshared preference is only created after a purchase is made. So as long as no purchase is made, this default value is returned.

    In the bytecode, we see that the defaultfalsevalue is stored as constantconst/4 v0, 0x0. If we patch this constant to0x1(true), the ads will now be hidden, even if no purchase has been made.

    Note that this is just one of many ways to disable ads in this app. Alternatively, we could also create the BillingSpshared preference manually with the expected boolean value.

    Using Apktool to recompile the app, and apksigner to resign the application, we now see that the application runs without any ads.


     

    It would now be possible to use the modified app without ads for personal use or to distribute it. A similar attack could be used to swap out the API key of the ad service, thus stealing the ad revenue gained from anyone using the patched version.

    Distributing modified apps is generally not difficult to do on either platform. For iOS, for example, a free developer account can be used to resign an application. Sometimes, the private keys of the enterprise certificate of a company leak, which can then be used to sign any iOS application for any device. Many tools exist to make this repackaging process very easy for end-users. Easily accessible third-party app stores exist on both platforms and are used to distribute repackaged apps. On jailbroken devices, the signing checks can be disabled completely, removing the need to resign modified apps altogether.

    Bypassing In-App Purchases

    As we showed in the previous section, modifying an application and repackaging it is not difficult. Now we will show that in-app purchases on both platforms are typically just verified using one or more conditional checks that decide whether “premium” functionality has been purchased or not. These checks can be bypassed, thus unlocking the functionality without paying.

    Here we will modify the application dynamically on the device itself, removing the need to repackage the application.

    iOS

    On iOS, in-app purchases are done through the StoreKit library, which is included on all devices.

    Whenever an in-app purchase is requested, StoreKit will return a transaction (SKPaymentTransaction). A bit of digging in the documentation shows that each one of those has a fieldtransactionState, which holds the result of any StoreKit transaction.

    Our goal is to ensure that each time a transaction state is queried, we return theSKPaymentTransactionStatePurchasedstate, showing that the purchase was successful.

    We use Frida to hook into the application and achieve this with the following Frida script:

    var transactionState = ObjC.classes.SKPaymentTransaction["- transactionState"];
    Interceptor.attach(transactionState.implementation, {
        onLeave: function(retval) {
            console.log("Current return value: " + retval);
            // Success state is enum value 1. 
            retval.replace(1);
            console.log("Return value replaced with SKPaymentTransactionStatePurchased");
        }
    });

    We launch the purchase flow in the app by pressing “GO FLASH PRO” and we see that even though the purchase window is shown, the purchase was already accepted in the background.

    We now unlocked all the pro features without any purchase!

    ios-app-purchasing_2

     

      Android

    On Android, the in-app purchases are handled throughInAppBillingService. Whenever a purchase is made, it returns an object containing the transaction success state and a digital signature. This signature can then be verified to check the integrity of the received purchase data.

    This app uses OpenIAB to take care of the interaction with theInAppBillingService. The verification of the signature is done through thejava.security.Signature.verify(byte[])API.

    We use the Xposed framework to hook the payment logic. Using this framework, we can create our own in-house Xposed module that bypasses in-app purchases. The bypassing technique of the Xposed module consists of two steps.

    First, it will intercept calls from our app to theInAppBillingService. It will ensure no real purchase is made and that a successful purchase object is returned. However, it’s not possible to create a valid signature for this object, since we don’t have access to the needed certificates. Instead, we just return an invalid dummy signature.

    Second, the module will also hook the signature verification function to always returntrueto work around the dummy signature that was used in the previous step.

    This way, when we launch the purchase flow, the fake transaction data is immediately returned, and the signature validation will succeed.

    The following Xposed module is used:

    public final class InAppPurchaseBypass implements IXposedHookLoadPackage {
    @Override
    public final void handleLoadPackage(final LoadPackageParam lpparam) {
    
        // Only hook the app we care about.
        if (!"com.domain.app".equals(lpparam.packageName)) {
          return;
        }
    
        // Always return `true` when this app calls Signature.verify(byte[]).
        XposedHelpers.findAndHookMethod(Signature.class, "verify",
          byte[].class, XC_MethodReplacement.returnConstant(true));
    
        Class < ? > purchaseDialogClass = XposedHelpers.findClass(    
          "com.domain.app.pages.common.PurchaseDialog", lpparam.classLoader);
    
        // Replace the purchase invocation. 
        XposedHelpers.findAndHookMethod(purchaseDialogClass, "clickHandle", View.class,      
          new XC_MethodReplacement() {
    
          // Don't call Google Billing Service, but just return a successful purchase.
          @Override
          protected final Object replaceHookedMethod(final MethodHookParam param) {
    
            // Do some setup that is done in the original `clickHandle` function.
            // Gets the BillingManager field
            Object billingManagerField = 
              XposedHelpers.getObjectField(param.thisObject, "d");
              
            // Gets the IabHelper field
            Object iabHelperField = 
              XposedHelpers.getObjectField(billingManagerFieldfield_d, "f");
            
            // Sets the OnIabPurchaseFinishedListener to notify when a purchase is done.
            XposedHelpers.setObjectField(
              iabHelperField, "q", billingManagerField);
    
            Intent data = new Intent();
    
            // Here we return a successful purchase response with an invalid signature.
            data.putExtra("RESPONSE_CODE", 0) // SUCCESS response code.
              .putExtra("INAPP_PURCHASE_DATA", "{}") // Empty purchase data.
              .putExtra("INAPP_DATA_SIGNATURE", "INVALID_SIGNATURE"); // Dummy signature
    
            // Required to manage the in-app purchase by OpenIAB 
            XposedHelpers.callMethod(
              param.thisObject, "onActivityResult",0, Activity.RESULT_OK, data);
           
           return null;
          }
        });
      }
    }

     

    Almost every app that uses in-app purchases will be built in a similar way using the same underlying APIs provided by the platform. It’s quite clear that once we figure out how to bypass in-app purchases in a single application, this can generally be extended to other applications.

    For both platforms, we can download tools like LocalIAPStore (iOS) or LuckyPatcher (Android) that offer an easy user interface to bypass almost all in-app purchases without needing any reverse engineering skills. Oftentimes, the patched versions of popular applications are also available on third-party app stores or just distributed online. In these cases, an end-user can just download this patched version and use it directly, making it trivial for anyone to gain access to modified apps.

    Note that the modifications shown here are very basic and will not work in every case or for other apps. Instead, these modifications are meant to serve as an educational example to show that application tampering is not difficult, and highlight what the results of these attacks might look like in practice.

    Mitigations

    The attacks mentioned above are possible because, in general, it is quite easy to analyze applications and figure out their internal logic. Using the information gathered, modifications can be made to bypass specific checks or to modify their logic.

    By properly obfuscating both the semantic information (such as class names, function names, and string values) and logic (like control flow), static analysis of an application becomes much more difficult.

    Run-time threats can be mitigated by adding run-time application self-protection (RASP) checks. These checks ensure application or library logic is not hooked (by tools like Frida, Cydia Substrate and Xposed), that the application is not running in an untrusted (jailbroken or rooted) environment and that the application was never modified and repackaged.

    When used together, these protection features ensure that every part of the attack becomes significantly more laborious.

    Conclusion

    Even though both the Android and iOS platforms offer many security features, they often don’t extend to the apps themselves.

    We’ve shown that cracking mobile applications on either platform is not particularly difficult. By distributing these modified versions, or providing easy-to-use cracking tools, non-technical end-users can also leverage the results of these attacks. Because a modified application can cause monetary and reputational loss, this can cause real problems for developers.

    Adding proper, layered and polymorphic protection to your application is key to raising the bar for analyzing and patching mobile applications.

     

    Dennis Frett - Software Engineer

    Discover how one customer is protecting their end user's applications from being run on jailbroken or rooted devices

    Learn more >

    Other posts you might be interested in