We presented earlier this year at the RSA eFraud Global Forum on the topic of mobile app anti-tampering effectiveness. The focus of this talk was on the runtime attack surface of iOS apps and the benefits of deploying self-defending techniques into higher risk mobile apps. Since February we’ve continued to use runtime manipulation techniques to great effect in our mobile app testing and want to raise some awareness of the issue so that technology risk management and mobile development teams can up their defensive game.
Why care about the runtime?
It’s important to begin by stating that runtime protection is best suited for apps which need to run on untrusted devices (likely but not necessarily consumer devices) and contain local security logic or sensitive data on the device. This is because security logic can sometimes be trivially manipulated in the runtime. A simple example: an app’s local authentication is governed by a function running in memory which states isLoggedIn=false. By accessing the runtime we can replace the false with true in real-time, or even completely replace the isLoggedIn method implementation with a version that always returns true. We can change the value or replace the function wholesale in an unprotected runtime. A few more examples:
The goal of runtime security is to achieve a comfort level that the app is self-defending and capable of operating independent of the device’s native security features.
Mobile Device Management and Jailbreak Detection
While we just finished saying that runtime protection is for consumer apps, there is one notable Enterprise exception: Mobile Device Management / Application Management (MDM/MAM). We have tested most of the major MDM/MAM tools at least once each over the past few years and have consistently used runtime manipulation methods to bypass device compliance checks such as Jailbreak/rooted detection. We hunt down and “patch-out” the compliance checks one-by-one, then combine the modifications needed into a single library that is dynamically injected into the target app at runtime.
Using these methods, we’ve demonstrated (at the direction of our clients) to some of these vendors that through runtime hacks their master encryption keys can be stolen (thereby decrypting a protected container) or a container’s local passcode can be brute-forced once compliance checks are fooled. MDM/MAM apps should be among the most secure on the planet and most (still) don’t use advanced runtime protection. Their lessons should be a cautionary tale for high-sensitivity consumer apps, which typically cannot rely on controls such as passcode or device integrity. Adversaries can cut straight to the work of runtime manipulation without first having to strip out pesky layers of jailbreak detection.
iPhone/iPad (iOS) Runtime Manipulation
While runtime manipulation is also applicable to Android, we will focus on iOS for the remainder of this article. Any attack begins with reconnaissance, and there are several methods available to profile an iOS app and identify the classes and methods (business logic) worth targeting. This process begins with class-dump-z to obtain a view of the application’s custom business logic, then involves detailed tracing of the application to observe its runtime behavior. Once interesting functions are found, three main tools facilitate the manipulation: GDB, Mobile Substrate and Cycript.
Universe of mobile anti-tampering controls
Now that we’ve discussed the potential impact of runtime manipulation, let’s take a brief look at the many different mobile app protection strategies, ranging from built-in platform to more advanced runtime protection measures such as anti-reversing and integrity checking. The table below identifies a number of different protection methods, with our view on varying degrees of effectiveness and level of effort to implement.
Our diagram quickly points to what we believe is a high return and lower integration effort - dynamic integrity checking. This control aims to protect the runtime integrity of the app by preventing the attacker/malware from changing code which is running in memory. This approach injects a large number of integrity checks into the code to detect tampering activity and take evasive action, such as silently wiping user data and keys, preventing further app execution, and possibly sending alerts to an administrator or backend fraud engine. For example, when someone attempts to hook a method in your app and replace ‘isCompliant=false’ with ‘isCompliant=true’, the activity will be detected as an unauthorized code injection attempt and the app can be shutdown. Note there is likely little “customer experience” risk since that kind of tampering is not going to be mistaken for legitimate user activity. As in our previous examples, it’s just evildoing.
Case Study: Before and After Effects of Dynamic Integrity Checks on Runtime Manipulation Attacks
We recently evaluated a commercial mobile app anti-tampering solution, testing the app in its naked state (which actually had some built-in jailbreak detection) and then the same app with dynamic integrity check protection. We used our standard iOS toolkit to target the application runtime, including tools such as GDB, Cycript, MobileSubstrate, and the excellent Snoop-it tool created by Andreas Kurtz. During our testing we noted the following outcomes:
- GNU Debugger: When attaching to the protected app with GDB and setting different breakpoints, the application consistently crashed. Without the ability to hit breakpoints on interesting methods, runtime modification with GDB was effectively prevented.
- Runtime Hooking with MobileSubstrate: In the unprotected version of the app, we created a MobileSubstrate extension using Theos that successfully patched the Jailbreak checks performed by the app. The protected app would not load our malicious dynamic library at runtime, thwarting our little jailbreak disappearance act. Since many attack tools and potential malware also load dynamic libraries, the anti-tamper protection appears to prevent a broad range of targeted code injection attacks.
However, the anti-tamper protection did not prevent interception of calls to sensitive system API’s from the application, such as file system activity, network requests, and Keychain interaction. Important distinction: anti-tamper protects the app and its custom business logic, not the entire device and iOS system APIs. What this means is your app’s sensitive network communication (e.g. login request) and read/writes from the iOS Keychain could still be captured by malware on the device.
- Method Tracing with Snoop-it: Using the Snoop-it iOS assessment tool we attempted to enable detailed method tracing and it crashed the protected app because we were attempting to inject Snoop-it’s dynamic library into the app (same behavior as attack #2 above).
- Runtime Hooking with Cycript: When attaching to the protected app with Cycript and exploring the Objective-C runtime, we found that many of Cycript’s capabilities were limited and crashed the app, such as defining new functions to help analyze the app runtime. Using Cycript’s built-in shortcut functions to enumerate classes, methods, and instance variables also crashed the app.
However, it was still possible to inspect the app and access class, instance variable and property values from the runtime, and invoke trivial methods directly from the Cycript console. So if the app was storing a security value, as long as we didn’t try to patch the code and rather just asked for real stored values (politely of course), we could extract that data. For example, creditCardNum() might be polled for the stored value as long as we don’t try to replace the method’s implementation or inject code, we can still interact with the app’s runtime. The lesson is that there is no cure for lame security logic, or keeping sensitive data around longer than needed.
Conclusion and Take-Aways
The most effective form of anti-tampering controls we’ve seen is dynamic integrity checking. Ideal candidates include apps needing more robust enforcement of local security controls, better protection against targeted exploits, or enhanced protection against account takeover and data theft.
- Integrity checking is effective at reducing the attack surface of the mobile client. It does not address service layer or backend risks. Look to your architecture design and appsec program to cover those.
- Integrity checking’s philosophy is the attacker can look but they can’t touch. You can’t fully prevent app analysis, but you can prevent alteration.
- You can’t use integrity checking to cure insecure app workflow such as accessing resources that don’t require user-level authorizations.
- Integrity checking makes sense when client side security checks need to be reliable – offline logins, encrypted data stored on device, geo-restrictions, Jailbreak & other compliance checks.