How Not To Protect Your Android Applications

Yoni Golst
Lightricks Tech Blog
7 min readApr 30, 2024

This article takes an uncommon approach to security articles. Insteading of suggesting ways to enhance your application’s security, this one aims to share insights we’ve gained through our experience dealing with a broad spectrum of threats targeting Lightricks apps. We’ll also advise you on what not to do when securing your Android application.

Before we dive in, the purpose of this article is to establish a mindset about client-side security. It acknowledges that your application may never be fully protected against all possible attacks. However, you might be able to choose the level of threats you’re prepared to confront.

1. Don’t rely on common methods

Relying on common protection methods can sometimes backfire. A classic example is the reliance on signing certificates to verify application integrity.

Transforming an Android application into an APK or a bundle file is a complex, multi-step process. A critical step in this process is signing the application. According to Android’s security policies, every application running on the platform must be signed by its developer. During this signing step, a specific cryptographic algorithm generates a unique and fixed-size numerical code, known as a hash, which is used to quickly verify or compare data without exposing its full contents. After this hash is generated, it’s combined into the final APK or bundle file according to the signature scheme.

Since the signing certificate information is part of the final file and changes dynamically during compile time, many Android developers utilize this information to verify app integrity at runtime. The application code checks if the signing certificate information matches the expected certificate; if it does, the running version is considered a legitimate version of the app.

However, this method of protection has become so common that numerous automatic tools now exist that can bypass the signature mechanism, causing any version of the application to be considered legitimate, even if attackers have modified the application’s content.

2. Don’t trust the framework API

Here’s a Kotlin snippet:

private fun Context.getFirstSigners(): Signature {
val packInfo = packageManager
.getPackageInfo(packageName, PackageManager.GET_SIGNING_CERTIFICATES)

return packInfo.signingInfo.apkContentsSigners.first()
}

The function above, returns a Signature object, which actually represents the certificate information used to sign the application. It’s important to note that a common mistake is to think of this object as the signature itself, which it isn’t.

Despite being part of the framework API, it’s still vulnerable to manipulation through Reflection. With Reflection, attackers can modify its behavior at runtime and change the framework API to fit their requirements. For example:

fun override(clazz: KClass<*>, field: String, obj: Any) {
getField(clazz, field)?.set(null, obj)
}


fun getField(clazz: KClass<*>, name: String): Field? {
return try {
clazz.java.getDeclaredField(name).also {
it.isAccessible = true
}
} catch (e: Exception) {
// Look for the field in the superclass?
null
}
}

This simple code overrides a field value and changes it to return a different value. With this capability, an attacker can easily override the `PackageManager.Creator` to return the original certificate information, even if the APK content has been modified, like this:

fun manipulate(originalSign: String) {
val originalSig = Signature(Base64.decode(originalSign, Base64.DEFAULT))
val originalCreator = PackageInfo.CREATOR

val creator: Parcelable.Creator<PackageInfo> = object : Parcelable.Creator<PackageInfo> {
override fun createFromParcel(source: Parcel): PackageInfo {
val packageInfo = originalCreator.createFromParcel(source)

if (packageInfo.signatures != null && packageInfo.signatures.isNotEmpty()) {
packageInfo.signatures[0] = originalSig
}

if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
if (packageInfo.signingInfo != null) {
val signaturesArray = packageInfo.signingInfo.apkContentsSigners
if (signaturesArray != null && signaturesArray.isNotEmpty()) {
signaturesArray[0] = originalSig
}
}
}

return packageInfo
}

override fun newArray(size: Int): Array<PackageInfo> {
return originalCreator.newArray(size)
}
}

override(PackageInfo::class, "CREATOR", creator)
}

The above code generates two new instances: Signature and Parcelable.Creator. The Signature is the original one, before the APK was modified, and the Creator is the overridden value of the real PackageManager. This makes the framework API return the original certificate information whenever the application requests it. By integrating this code at the application’s entry point, like in the onCreate() method of the Application class, it ensures that any mechanism depending on this value will not be aware that the integrity check failed.

3. Don’t trust checksum & native code

A very interesting topic that often gets overlooked in Android security is the vulnerability of binary files. Historically, Android is open-source and built on Linux, which leads us toward the realm of binary exploitation, particularly on Linux systems.

So, what exactly is an ELF (Executable and Linkable Format) file? It’s a commonly used file format for executables, object code, and shared libraries.

It’s important to note that the ELF file structure is complex and extensive, making it more than we can fully explore here. We’ll focus mainly on dynamic linking and the relevant file sections from a high-level viewpoint.

There’s a common belief in Android that binary files are relatively safe because analyzing them requires a skill set that’s different from reverse engineering byte-code and similar tasks. While there might be some truth to this, it’s important to remember that it’s not always the case, as we’re about to see.

With this introduction out of the way, let’s turn to the interesting world of hooking functions and references.

Picture from: LIEF Documentation 05 — Infecting the plt/got

At the heart of the concept, there are two types of binaries on any system: dynamically linked and statically linked.

Dynamically linked binaries depend on other files, such as shared libraries, to provide much of their functionality. On the other hand, statically linked binaries are self-contained, they contain all the necessary code within a single file and don’t rely on any external libraries.

Binary files are made up of several sections, a few of which are:

.text: This section holds the executable instructions, or the “code.”

.plt and .got (among others): Each external reference in the code, like shared libraries, corresponds to an entry in either the PLT (Procedure Linkage Table) or GOT (Global Offset Table), which shows where the function or reference is located in memory.

In this type of hook attack, the attacker manipulates the .plt and .got sections to point to a different memory address that executes logic serving their needs. A common target for such hook attacks are the open* calls, which are provided by the shared library `libc` (the standard C programming language library).

Many automated tools target the .plt and .got sections, manipulating them to trigger malicious open* functions instead of the original code. This manipulation forces the system to return the contents of a different file than the one requested. For example, it might return the original APK content instead of the modified one, when the app tries to read the file from the path provided by `Context.getPackageResourcePath`. For example:

int fake_openat(int fd, const char *pathname, int flags, mode_t mode) {
if (strcmp(pathname, [apk path]) == 0){
return old_openat(fd, [original apk], flags, mode);
}
return original_openat(fd, pathname, flags, mode);
}

4. Don’t completely rule out cracked versions.

The instinct is often to be defensive, but is that always necessary? Upon careful consideration, you might discover that unconventional scenarios could actually be beneficial.

Consider the scenario where a developer finds that cracked versions of their applications are freely available for download. While such situations are typically viewed negatively, they could potentially offer unexpected advantages.

The viability of leveraging these scenarios depends on specific factors such as individual strategy, brand considerations, and legal constraints. Yet, adopting a high-level perspective might reveal that utilizing cracked versions could be a more cost-effective strategy with potentially higher conversion rates. Given the costs associated with marketing campaigns and the challenge of accurately targeting the right customer base — along with backend expenses, these cracked versions could serve as an alternative distribution channel to increase the app’s exposure to additional users who might eventually pay for a subscription.

The key to this approach is awareness. By setting up systems to detect cracked versions and regularly assessing the costs and benefits for both legitimate and malicious users, decision-making becomes more straightforward and informed.

Conclusion

This article highlights how sticking to traditional and widely used security strategies might unexpectedly make our apps more vulnerable. There’s often a belief that complexity ensures security. However, our experiences have shown that thoroughly understanding the weaknesses in the security measures you’ve chosen and learning how to turn them to your advantage can significantly boost your overall strategy. This method allows us to tackle challenges strategically, choosing precisely when and why we face them. It’s about being smart, not just accurate.

Feel free to share your thoughts and ask any questions. Thanks for reading!

--

--