How to Protect Your Android App Against Cracking
我曾经是一名独立开发者。作为独立开发者,应用被破解是一件非常让人烦恼的事情。在与破解者的长期博弈中我积累了一些防止 android 应用被破解的经验。在这篇文章(或者,在这个视频中)我将分享我的经验。这对你也会有一定的帮助。
I used to be an independent developer. As an indie developer, having my apps cracked was a constant headache. Through long-term battles with crackers, I've accumulated some practical experience in preventing Android apps from being hacked. In this article (or video), I'll share these insights---and I hope they can help you too.
1. Validate Application Information
对于一些针对性不强的破解工具,我们可以使用应用基础信息校验,防止应用被二次打包。这包括应用的包名校验、应用的启动类校验。
For less targeted cracking tools, we can use basic application information verification to prevent the app from being repackaged. This includes verifying the app's package name and launcher activity.
1.1 Pacakge Name Verification
我们可以使用如下代码获取应用的 Application 并进行比较,
We can use the following code to retrieve and compare the Application name:
java
public static String getApplicationName(final String pkgName) {
try {
PackageManager pm = UtilsApp.getApp().getPackageManager();
ApplicationInfo ai = pm.getApplicationInfo(pkgName, 0);
return ai == null ? null : ai.className;
} catch (PackageManager.NameNotFoundException e) {
e.printStackTrace();
return null;
}
}
1.2 Launcher Activity Verification
我们可以使用如下代码获取应用的启动类并进行比较,
We can use the following code to retrieve and compare the app's launcher activity:
java
public static String getAppLauncher(final String pkgName) {
try {
PackageManager pm = UtilsApp.getApp().getPackageManager();
PackageInfo pi = pm.getPackageInfo(pkgName, 0);
if (pi == null) return null;
Intent resolveIntent = new Intent(Intent.ACTION_MAIN, null);
resolveIntent.addCategory(Intent.CATEGORY_LAUNCHER);
resolveIntent.setPackage(pi.packageName);
List<ResolveInfo> resolveInfoList = pm.queryIntentActivities(resolveIntent, 0);
ResolveInfo resolveInfo = resolveInfoList.iterator().next();
if (resolveInfo != null) {
return resolveInfo.activityInfo.name;
}
return null;
} catch (PackageManager.NameNotFoundException e) {
e.printStackTrace();
return null;
}
}
2. 混淆
2. Obfuscation
2.1 Code Obfuscation (Easy)
混淆是一种通过转换代码结构和命名来降低程序可读性的技术,目的是增加逆向工程的难度,同时保持程序原有功能
Obfuscation is a technique that reduces the readability of a program by transforming its structure and naming conventions, with the aim of increasing the difficulty of reverse engineering while preserving the program's original functionality.
启用R8 (Android Studio的默认设置),我们可以收缩、模糊和优化我们的代码。这会重命名类/方法,删除未使用的代码,并使逆向工程复杂化。
Enable R8 (default in Android Studio), we are able to shrink, obfuscate, and optimize our code. This renames classes/methods, removes unused code, and complicates reverse-engineering.
kotlin
// Before obfuscation
public class UserManager {
public String getAuthToken(String username) { ... }
}
// After obfuscation
public class a {
public String a(String b) { ... }
}
2.2 Resources Obfuscation (Pro)
除了对代码进行混淆,我们还可以使用 AndResGuard 对资源进行混淆。它和代码混淆一样,只不过作用于资源名称,这不仅可以降低代码的可读性,还可以减少 APK 的文件大小。
Besides obfuscating the code, we can also use AndResGuard to obfuscate resources. Similar to code obfuscation, it acts on resource names, which not only reduces code readability but also shrinks the APK file size.
2.3 Improve Obfuscation Dictionaries (Boss)
但是,我们可以更近一步得提升代码阅读的难度------使用更复杂的自定义混淆字典。
However, we can take it a step further to increase the difficulty of code readability---by using more sophisticated custom obfuscation dictionaries. Such as,
erlang
ʻ
ʼ
ʽ
ʾ
ʿ
ˆ
ˈ
ˉ
ˊ
ˋ
ˎ
ˏ
ˑ
י
ـ
ٴ
...
然后在混淆的配置中增加添加如下配置,
Then add the following configurations to the obfuscation settings,
groovy
-obfuscationdictionary dict.txt
-classobfuscationdictionary dict.txt
-packageobfuscationdictionary dict.txt
3. 签名校验
3. Signature Verification
3.1 Basic Signature Verification (Easy)
为了防止应用被二次打包,我们可以很容易得想到使用签名校验
To prevent the app from being repackaged, using signature verification is an intuitive solution that comes to mind. We can use the code to verify app signature.
kotlin
fun verifyAppSignature(context: Context): Boolean {
val packageInfo = context.packageManager.getPackageInfo(
context.packageName, PackageManager.GET_SIGNATURES
)
// Compare signatures with your release key hash
return packageInfo.signatures[0].hashCode() == RELEASE_SIGNATURE_HASH
}
3.2 Signature Verification in NDK (Pro)
除了在 Java 层进行签名校验,我们还可以在 NDK 层进行签名校验。下面是我用到的获取应用签名信息的方法。它不过是 Java 方法的一个翻版,
In addition to performing signature verification at the Java layer, we can also do it at the NDK layer. Here's the method I use to obtain the app's signature information. It's just a port of the Java method,
C++
jbyteArray getSignatureByteArray(JNIEnv *env, jobject context, jstring algorithm) {
jclass context_clazz = env->GetObjectClass(context);
// context.getPackageManager()
jmethodID methodID_getPackageManager = env->GetMethodID(context_clazz, "getPackageManager", "()Landroid/content/pm/PackageManager;");
jobject packageManager = env->CallObjectMethod(context, methodID_getPackageManager);
jclass packageManager_clazz = env->GetObjectClass(packageManager);
jmethodID methodID_getPackageInfo = env->GetMethodID(packageManager_clazz, "getPackageInfo", "(Ljava/lang/String;I)Landroid/content/pm/PackageInfo;");
jmethodID methodID_getPackageName = env->GetMethodID(context_clazz, "getPackageName", "()Ljava/lang/String;");
// context.getPackageName()
jobject application_package_obj = env->CallObjectMethod(context, methodID_getPackageName);
jstring application_package = static_cast<jstring>(application_package_obj);
const char* package_name = env->GetStringUTFChars(application_package, JNI_FALSE);
// packageManager->getPackageInfo(packageName, GET_SIGNATURES);
jobject packageInfo = env->CallObjectMethod(packageManager, methodID_getPackageInfo, application_package_obj, /*GET_SIGNATURES*/ 64);
jclass packageinfo_clazz = env->GetObjectClass(packageInfo);
jfieldID fieldID_signatures = env->GetFieldID(packageinfo_clazz, "signatures", "[Landroid/content/pm/Signature;");
jobjectArray signature_arr = (jobjectArray)env->GetObjectField(packageInfo, fieldID_signatures);
// packageInfo.signatures[0]
jobject signature = env->GetObjectArrayElement(signature_arr, 0);
// signature.toByteArray()
jclass signature_clazz = env->GetObjectClass(signature);
jmethodID signature_toByteArray = env->GetMethodID(signature_clazz,"toByteArray", "()[B");
jbyteArray sig_bytes = (jbyteArray) env->CallObjectMethod(signature, signature_toByteArray);
// MessageDigest.getInstance("SHA1")
jclass message_digest_clazz = env->FindClass("java/security/MessageDigest");
jmethodID message_digest_getInstance = env->GetStaticMethodID(message_digest_clazz, "getInstance","(Ljava/lang/String;)Ljava/security/MessageDigest;");
const char* algorithm_bytes = env->GetStringUTFChars(algorithm, JNI_FALSE);
jstring algorithm_name = env->NewStringUTF(algorithm_bytes);
jobject message_object = env->CallStaticObjectMethod(message_digest_clazz, message_digest_getInstance, algorithm_name);
jthrowable exception = env->ExceptionOccurred();
env->ExceptionClear();
if (exception) return NULL;
// sha1.update()
jmethodID message_digest_update = env->GetMethodID(message_digest_clazz,"update","([B)V");
env->CallVoidMethod(message_object, message_digest_update, sig_bytes);
// sha1.digest()
jmethodID digest = env->GetMethodID(message_digest_clazz, "digest", "()[B");
jbyteArray sha1_bytes = (jbyteArray) env->CallObjectMethod(message_object, digest);
return sha1_bytes;
}
3.3 APK Signature Verification (Boss)
然而,不论上述哪种方法,当破解者都可以使用 Hook 技巧绕过。因此,这里我介绍一种新的签名校验思路------从系统目录中获取 APK 文件,读取 APK 文件的签名信息并进行校验。
However, attackers can bypass any of the above methods using hook techniques. Therefore, I'd like to introduce a novel approach to signature verification: retrieving the APK file directly from the system directory and validating its signature information.
我们可以通过如下代码获取当前应用的 APK 文件。
We can retrieve the APK file of the current application using the following code.
java
ApplicationInfo applicationInfo = context.getPackageManager().getApplicationInfo(context.getPackageName(), 0);
File sourceApk = new File(applicationInfo.sourceDir);
然后,我们可以使用 Android 系统源码中的签名工具 apksig 获取 APK 签名。
Then, we can use the signature verification tools apksig from the Android source code to obtain the APK signature.
需要注意的是,破解者可能同时 Hook 获取应用包名和安装目录的方法,因此,最好对这些方法也进行校验。
It's important to note that attackers might hook both the methods for retrieving the app's package name and installation directory. Therefore, it's advisable to validate these methods as well.
4. Networking
对于常规的 Https 和签名证书的配置我就不介绍了。这里我介绍一些其他的安全思路。
I won't cover the standard HTTPS and signature certificate configurations here. Instead, let me introduce some alternative security approaches.
4.1 Request Encryption
我们可以使用对称加密算法(比如 AES 或者 RC4),对接口的请求和响应信息进行加密。然后,分别在服务端和 Android 端进行加解密。这需要服务端配合。此外,还应该记得把加密的逻辑放到 NDK 里。
We can use symmetric encryption algorithms (such as AES or RC4) to encrypt the request and response data of API interfaces. Then, perform encryption/decryption on both the server side and Android client. This requires server-side coordination. Additionally, the encryption logic should be implemented in the NDK layer.

4.2 Partial Encryption
如果你觉得对整个接口的请求和响应进行加解密的成本比较高,那么你也可以尝试部分加解密。
If you find the cost of encrypting/decrypting the entire API request and response too high, you can also try partial encryption.

比如,我们给服务器发送了 v1 和 v2 两个值,此时,可以使用 SHA256 等哈希算法和 v1,v2 计算一个字段 v3,然后服务器使用同样的方式计算出一个值并和 v3 进行比较,以此来判断请求是否合法。同理,我们也可以在服务端加密,然后在客户端校验,来判断服务器返回的数据是否被篡改。
For example, when sending values v1 and v2 to the server, we can compute a field v3 using a hash algorithm like SHA256 with v1 and v2. The server then calculates the same value using the same method and compares it with v3 to verify the request's legitimacy. Similarly, we can encrypt data on the server and validate it on the client to detect tampering in the server's response.
5. Encryption & Data Protection
5.1 Data Storage
如果你的应用数据安全性非常重要,那么你应该使用常规的存储工具之外的其他存储方式:
- 使用 EncryptedSharedPreferences 代替 SharedPreferences
- 使用 SQLCipher 代替 SQLite
- 使用 AES/RC4/RSA 等对数据加密之后再写入文件
If the security of your application's data is of utmost importance, you should consider using storage methods beyond conventional tools:
- Replace SharedPreferences with EncryptedSharedPreferences
- Replace SQLite with SQLCipher
- Encrypt data using algorithms like AES/RC4/RSA before writing to files
5.2 Prevent Hardcoding Strings
硬编码字符串不仅容易泄漏重要的信息,还容易被破解者作为突破的关键。对于重要的字符串,我们需要将其加密存储。此外,对于不相对不太敏感的字符串,我们可以将其以字节数组而不是字符串的形式硬编码,然后在运行时读取为字符串。
Hardcoding strings not only risks leaking sensitive information but also serves as a key target for attackers. Critical strings should be encrypted and stored, while less-sensitive ones can be hardcoded as byte arrays instead of plain strings, then converted to strings at runtime.
java
static short[] getShortsFromBytes(String from) {
byte[] bytesFrom = from.getBytes();
int size = bytes.length%2==0 ? bytes.length/2 : bytes.length/2+1;
short[] shorts = new short[size];
int i = 0;
short s = 0;
for (byte b : bytes) {
if (i % 2 == 0) {
s = (short) (b << 8);
} else {
s = (short) (s | b);
}
shorts[i/2] = s;
i++;
}
return shorts;
}
6、Native 安全
6. Native Security
将代码放进 Native 层(C/C++ 层)可以增加代码被破解的难,但是不要忘了 native 方法本身的安全性。那么有什么办法可以保证 Native 代码的安全?
Putting code in the Native layer (C/C++ layer) can increase the difficulty of code cracking, but we must not overlook the security of the native methods themselves. So, what methods can be used to ensure the security of Native code?
6.1 在加载 so 的过程中验证
6.1 Verification During The Loading Process of The SO File
如下面的代码所示,我们可以在 JNI_OnLoad
中进行校验:
As shown in the following code, we can perform verification in JNI_OnLoad
method:
c++
jint JNI_OnLoad(JavaVM *vm, void *reserved) {
JNIEnv *env = nullptr;
if (vm->GetEnv((void **) &env, JNI_VERSION_1_4) != JNI_OK) {
// Error
return JNI_ERR;
}
// Verify
if (!verifyLauncher(env)
|| !verifyPackageName(env)
|| !verifyApplication(env)
|| !verifyAppSignature(env)) {
return JNI_ERR;
}
return JNI_VERSION_1_4;
}
6.2 验证方法的 native 性质
6.2 Verify The Native Nature of The Method
对于 native 类型的方法,一种常见的破解方式是直接通过修改 class 代码将其替换为非 native 方法,并修改其逻辑。所以,为了防止我们的代码被篡改,首先,应该避免直接返回 true
/false
. 因为这种返回值类型最容易被修改。
此外,我们可以通过反射判断方法是否是 native 的,如下所示。如果方法是非 native 的,那么就可以判断该方法被篡改了。
For native methods, a common cracking technique is to directly modify the class code to replace them with non-native methods and alter their logic. Therefore, to prevent our code from being tampered with, first, we should avoid directly returning true
/false
--- since methods with this type of return value is the easiest to modify.
Additionally, we can use reflection to determine if a method is native, as shown below. If the method is non-native, we can conclude that it has been tampered with.
kotlin
val method = cls.getMethod("method", Int::class.java)
Modifier.isNative(method.modifiers)
6.3 使用 inline 复用方法
6.3 Use inline
To Reuse Methods
不论是在 java 层(使用 kotlin)还是在 native 层,我们都可以使用 inline 关键字对某些方法进行复用,比如用户会员身份校验的逻辑。如果我们把校验逻辑写到一个方法里,并到处引用,那么就意味着破解者只需要对这一个方法进行 hook 或着篡改就可以破解我们的校验逻辑。而如果使用 inline 关键字进行复用,则可以增加破解的复杂度:破解者不仅需要到处修改校验逻辑;如果处理得好,还可以让他们找不到 hook 的位置。
Whether in the Java layer (using Kotlin) or the native layer, we can use the inline
keyword to reuse certain methods, such as the logic for verifying user membership status.
If we write the verification logic in a single method and reference it everywhere, it means attackers only need to hook or tamper with that one method to bypass our verification. However, reusing logic with the inline keyword increases the complexity of cracking: attackers would need to modify the verification logic everywhere it's inlined. With proper implementation, it can even make it impossible for them to find a stable hook point.
6.4 对 so 进行签名校验
6.4 Perform Signature Verification On the SO Files
我们可以在 Android 签名的基础上增加对 so 的校验。这种签名校验方法参考了一种多渠道打包框架 VasDolly 的思路。这种打包方式会以键值对的形式写入信息到 APK 文件。在这种安全方案中,我们会在打包 APK 文件之后按照既定的方式对 so 文件签名并将签名结果写入到 APK 文件上。然后,在运行时按照同样的逻辑,从系统获取 APK 的 so 文件并签名,然后对比结果并判断 so 文件是否被改动。比如,
We can enhance the verification of SO files based on Android's signing mechanism. This signature verification method draws inspiration from the multi-channel packaging framework VasDolly, which writes information into APK files in the form of key-value pairs.
In this security scheme, after packaging the APK, we sign the SO files according to a predefined method and write the signature results into the APK. Then, at runtime, we retrieve the SO files from the system, sign them using the same logic, and compare the results to determine if the SO files have been tampered with. For example:
java
public static String generateShieldResourceSignature(File srcApk) {
if (srcApk == null
|| !srcApk.exists()
|| !srcApk.isFile()
|| srcApk.length() == 0) {
return "";
}
try (JarFile jarFile = new JarFile(srcApk)) {
Enumeration<JarEntry> entries = jarFile.entries();
Map<String, String> signatures = new HashMap<>();
while (entries.hasMoreElements()) {
JarEntry entry = entries.nextElement();
if (entry.getName().endsWith(".so")) {
InputStream ins = jarFile.getInputStream(entry);
byte[] bytes = ins.readAllBytes();
String sha256 = EncryptUtils.sha256(bytes);
signatures.put(sha256, entry.getName());
}
}
List<String> sha256s = new ArrayList<>(signatures.keySet());
sha256s.sort(String::compareTo);
List<String> names = sha256s.stream().map(signatures::get).collect(Collectors.toList());
System.out.println("Resource shield signature so files order: " + join(names, ", "));
String connected = join(sha256s, "@@");
String signature = EncryptUtils.sha256(connected);
jarFile.close();
return signature;
} catch (IOException e) {
e.printStackTrace();
}
return "";
}
7、其他方案
7、Others
以上是一些我们日常开发过程中可以尝试的安全方案。成本和开发的门槛都比较低。除此此外,我们还可以采用一些其他方案。
The above are some security solutions that we can implement in daily development, with relatively low costs and development thresholds. In addition, we can also adopt some other approaches.
7.1 软件加壳
7.1 Software Packing
所谓的加壳,就是将 dex 和 so 等文件加密,然后在启动的过程中解密的方案。这种方案专业性和开发成本都比较高。
The so-called "packing" refers to a solution that encrypts files such as dex and so, then decrypts them during the startup process. This solution requires high professionalism and involves relatively high development costs.
7.2 在服务端进行身份校验
7.2 Perform Verification on The Server Side
如果你的业务依赖服务器,那么你可以考虑将验证的逻辑放在服务器完成。这是相对来说成本更低和更加保险的手段。不过,如果你的业务更多需要在客户端完成,那么这种方案就行不通了。
If your business relies on a server, you can consider placing the verification logic on the server. This is a relatively lower-cost and more secure approach. However, if your business operations mostly need to be completed on the client side, this solution won't work.
总结
Summary
在这篇文章中,我介绍了我在开发过程中经常使用到的安全方案和策略。如果你觉得对你有帮助,可以分享和点赞,这是对我们最大的鼓励。如果你有好的建议,也可以在评论区里留言,让我们一起学习和成长。
谢谢。
In this article, I have introduced the security solutions and strategies that I frequently use in the development process. If you find them helpful, feel free to share and like this article---this is the greatest encouragement for us. If you have any good suggestions, you can also leave a message in the comment section, so that we can learn and grow together.
Thank you.