Skip to main content

· 6 min read

Using kotlin Inline functions to help secure against de/recompiling android apps​

When trying to attack an android application, attackers often try to circumvent some of the protections you've introduced into your app. For example, you might have a signature check added in order to prevent attackers from adding malware into your app and republishing it:

Safe to run - signature verification

They might also reverse your app to remove any root protection / detection you've added:

Safe to run - root detection

Or they might try to remove any other checks you have added, for example, checks to stop it running on an emulator:

Safe to run - emulator check

In order to make this harder, we can implement these checks using the inline keyword in kotlin. What makes it easy now? To understand how inlining functions can help, we should first look at how easy it is without inlining. If we take an application compiled with the 'Safe to run' reporting checks Safe to run

The standard approach​

Let's suppose we add this configuration

SafeToRun.init(
configure {
osDetectionCheck(banAvdEmulator()).error()
}
)

To our Application or in MainActivity. Then we run one check at launch (in MainActivity onCreate())

if (SafeToRun.isSafeToRun().anyFailures()) {
throw RuntimeException("Abc")
}

And we also add a button to do a check, let's say we have a button like this:

<Button android:id="@+id/runSensitiveAction"
android:text="Run sensitive action"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
android:layout_width="match_parent"
android:layout_height="wrap_content"/>

And program it up like this:

binding.runSensitiveAction.setOnClickListener {
if (SafeToRun.isSafeToRun().anyFailures()) {
Toast.makeText(this, "Not safe to run", Toast.LENGTH_LONG).show()
} else {
Toast.makeText(this, "Performed sensitive action!!", Toast.LENGTH_LONG).show()
}
}

When you run the application on an emulator, it will throw an exception. So an attacker might try and take the emulator detection out - let's look at how this application decompiles. We might see something similar to this:

public final void invoke(SafeToRunConfiguration $this$configure) {
Intrinsics.checkNotNullParameter($this$configure,"$receiver");
$this$configure.error(OSDetectionCheckKt.osDetectionCheck(this.this$0,C01051.INSTANCE));
}

If we can identify this single line and remove it, then we'll be able to recompile having removed the root detection. Let's demonstrate. I'm using apk tool (full instructions on de/recompiling are a bit out of scope for this article but I'll add some of the steps for info):

apktool.sh d app-debug.apk

This gives me the files as smali. If I look for the line of code doing the config I'll see this:

check-cast v1, Lkotlin/jvm/functions/Function1;

invoke-static {v0, v1}, Lio/github/dllewellyn/safetorun/features/rootdetection/RootDetectionConfigKt;->rootDetection(Landroid/content/Context;Lkotlin/jvm/functions/Function1;)Lio/github/dllewellyn/safetorun/checks/SafeToRunCheck;

move-result-object v0

.line 94
invoke-virtual {p1, v0}, Lio/github/dllewellyn/safetorun/SafeToRunConfiguration;->error(Lio/github/dllewellyn/safetorun/checks/SafeToRunCheck;)V

if I remove the final line, it will not run .error() and basically remove the check. So let's do that and recompile:

apktool.sh b -f -d app-debug

Zipalign the result

./zipalign -v 4 app-debug.apk app-debug-aligned.apk

Then sign (password is android)

apksigner sign --ks ~/.android/debug.keystore output.apk

Then install adb install app-debug-aligned.apk and you'll see it runs.

Click the 'sensitive button' and the action is performed.

The issue This was a fairly straight forward set of steps to remove our check, because you've removed the configuration in a single place, every place that safe to run is called has now been removed. In order to make all of this harder, we'd ideally want to make it so that if we add two checks, it takes double the effort to remove them (and three checks triple.. etc etc).

Inlining In SafeToRun 1.0.3 we have introduced a new function which uses inline functions to make de/recompilation harder. As a reminder, in usual compilation when you call a function, the compiled code looks similar to your un-compiled code - in the sense that it has a reference to that function, and jumps to where that function is in the binary. When we Inline a function, the whole function you're calling is copied inside the calling function at compile time.

Inlined function​

Let's add this function to MainActivity:

private inline fun canIRun(actionOnFailure: () -> Unit) {
if (safeToRun(buildSafeToRunCheckList {
add {
banAvdEmulatorCheck()
}
})()) {
actionOnFailure()
}
}

One thing to note is that the syntax for the inlined versioning of Safe to Run is still in active development at the time of writing, be sure to check the documentation for the most up-to-date syntax If also add this block of code into the button click, and also into onCreate for MainActivity:

canIRun { throw RuntimeException("Error with safe to run") }

We'll repeat the compilation and recompilation stage. After a bit of digging, I found something that looks like this:

.method public bridge synthetic invoke()Ljava/lang/Object;
.locals 1

invoke-virtual {p0}, Lcom/andro/secure/MainActivity$canIRun$$inlined$safeToRun$2;->invoke()Z

move-result v0

invoke-static {v0}, Ljava/lang/Boolean;->valueOf(Z)Ljava/lang/Boolean;

move-result-object v0

return-object v0

.end method

and I replaced it with this code:

.method public bridge synthetic invoke()Ljava/lang/Object;
.locals 1

invoke-virtual {p0}, Lcom/andro/secure/MainActivity$canIRun$$inlined$safeToRun$2;->invoke()Z

move-result v0

invoke-static {v0}, Ljava/lang/Boolean;->valueOf(Z)Ljava/lang/Boolean;

move-result-object v0

const v0, 0

invoke-static {v0}, Ljava/lang/Boolean;->valueOf(Z)Ljava/lang/Boolean;

move-result-object v0

return-object v0
.end method

All this does is returns 'false' (i.e. the check returns false in the inlined version we return a boolean indicating true if a check failed rather than a SafeToRunReport)

If we recompile using the same steps as before, you'll find that the app runs. However, clicking the button will still cause a RuntimeException. The reason is that the entire functions call chain is duplicated inside the button click. This adds some size to the binary but now an attacker would need to find the function in every place where you have called ' canIRun' making the job much harder.

Conclusions​

In this article we've demonstrated that by using inline functions (for the entire chain) and no classes etc, it is exponentially more difficult to remove device safety checks the more checks you add.

If you were to add a single check at runtime, it would not be any more difficult - however if you litter your code with Safe to run checks, you will find it harder to decompile and remove those checks. As ever, it's impossible to have a foolproof way of preventing re/decompiling due to the nature of the problem - but inlining functions in this way can help make it harder for an attacker

· 3 min read

Emulator detection with safe to run​

What?​

Emulator detection is the ability to tell when your application is running on an emulator rather than a real device, but why would you want to do this?

Why?​

Reverse engineers, pentesters and hackers tend to like running your app on an emulator can be make it far easier reveal what your application is doing. A somewhat convoluted example is looking at an application’s files in their private directory. For example:

In that case, we can see that by preventing our app running on an emulated device, it can make it more difficult for a penetration tester to observe our application.

How?​

We’re going to use the library Safe to run for both the detection and to help identify the emulator.

Core Safe to run (android library) The purpose of this configuration is to provide a simple and extensible framework

Safe to run has a utility to show device information:

Log.v("Device information", deviceInformation().toString())

If we run this into application on the Android emulator, it looks like this:

DeviceInformation(osCheck=OsCheck(osVersion=30, manufacturer=Google, model=sdk_gphone_x86, board=goldfish_x86, bootloader=unknown, cpuAbi=[x86, armeabi-v7a, armeabi], host=abfarm-us-west1-c-0089, hardware=ranchu, device=generic_x86_arm), installOrigin=InstallOrigin(installOriginPackageName=), signatureVerification=SignatureInformation(signature=))

And just to double check with another emulator we get this result

DeviceInformation(osCheck=OsCheck(osVersion=30, manufacturer=Google, model=sdk_gphone_x86_arm, board=goldfish_x86, bootloader=unknown, cpuAbi=[x86, armeabi-v7a, armeabi], host=abfarm-us-west1-c-0007, hardware=ranchu, device=generic_x86_arm), installOrigin=InstallOrigin(installOriginPackageName=), signatureVerification=SignatureInformation(signature=))

There’s da few things we could pick out here, but in particular we can probably start to build up this as an emulator check. We’ll write up a configuration for safe to run:

SafeToRun.init(
configure {
osDetectionCheck(
conditionalBuilder {
with(bannedBootloader("unknown"))
and(bannedDevice("generic_x86_arm"))
and(bannedBoard("goldfish_x86"))
}
).error()
}
)
Log.v("SafeToRunResult", SafeToRun.isSafeToRun().toString())

This check will result in the following failure

MultipleReports(reports=[MultipleReports(reports=[SafeToRunReportFailure(failureReason=os-config-failure, failureMessage=Banned bootloader unknown == unknown)])]) Genymotion Another popular emulator is GenyMotion. If we go to the cloud GenyMotion, we can test our experiments on devices there. After connecting and installing out devices, we can have a look again at the deviceInformation() output Genymotion Cloud SaaS Edit description cloud.geny.io

`DeviceInformation(osCheck=OsCheck(osVersion=28, manufacturer=Genymotion, model=Huawei P30 Pro, board=unknown, bootloader=unknown, cpuAbi=[x86], host=68d6ec695a7c, hardware=vbox86, device=vbox86p), installOrigin=InstallOrigin(installOriginPackageName=), signatureVerification=SignatureInformation(signature=))

Easy peasy, we can create a new rule for Genymotion:

osDetectionCheck(conditionalBuilder {
with(bannedBoard("unknown"))
and(notManufacturer("Genymotion"))
and(bannedBootloader("unknown"))
}).error()

Bluestacks Another popular emulator is bluestacks. After installing and running it, we get this:

DeviceInformation(osCheck=OsCheck(osVersion=25, manufacturer=samsung, model=SM-G955F, board=universal8895, bootloader=unknown, cpuAbi=[x86_64, x86, arm64-v8a, armeabi-v7a, armeabi], host=Build2, hardware=samsungexynos8895, device=dream2lte), installOrigin=InstallOrigin(installOriginPackageName=), signatureVerification=SignatureInformation(signature=))

This is actually a tricky one for now because the rest of the description is very similar to a real device that we wouldn’t want to block. We can safely use this rule for bluestacks though:

with(bannedBootloader(OsCheckConstants.UNKNOWN))

Making it easier From 1.0.2 onwards, Safe to run provides two utility functions for these two emulators (more coming soon)

SafeToRun.init {
osDetectionCheck(banAvdEmulator()).error()
osDetectionCheck(banGenymotionEmulator()).error()
osDetectionCheck(banBluestacksEmulator()).error()
}

Conclusion​

Hopefully that gives us an idea of how we can perform emulator detection on Android, Safe to run provides a number of ways of checking for device information so you can tweak parameters for emulator detection — naturally there are a plenty of emulators we’e not discussed here that you might want to write a check for. Enjoy

· 2 min read
danger

No library or app can guarantee not running on a rooted phone because of the nature of rooted phones, and any tamper detection could be removed or changed in reality — this app should work with most attackers, and make it hard enough to make it not worth it for many others. Background

Safe to run is intended to provide a layer of security for Android applications from rooted phones, reverse engineering, binary modification, malicious apps and some security vulnerabilities.

In principle, you set the parameters for a safe device, (one where the debugger is not attached, one with a minimum OS version, one not rooted etc) and ask ‘is it safe to run’.

You know best the time and place to ask the question-maybe you do it on app launch and throw an exception, maybe you ask when some tries to make a payment to someone else and reject the payment or maybe you do it before retrieving some data from the backend.

Checks​

We have the following checks that are configurable in safe to run

  • Signature checks — check the signature of the binary (set multiple for multiple certs)
  • Debug check — check if the debugger is attached or if the app is debuggable
  • Device check — blacklist a combination of OS version and device manufacturer
  • Root check — check for signs which point to the device being rooted
  • Other packages check — check for the presence of other packages, e.g. You might not want to run if you know a specific app is running because it’s known malware that might attack your app
  • Install origin check — check for the installing package of your app, this can help to make reverse engineering harder

Sample usage​

Every time you want to run a check execute:

SafeToRun.safeToRun()