As an indie developer, having your app cracked is a very frustrating experience. In one of my previous posts, a student asked if there were any Android anti-hacking methods. In many times reinforce, crack, reinforce again, crack again in the process, I also accumulated a few ideas and methods. Share here, if need to use, can make a reference.

A quick conclusion, and one of my foreign programmers on Stackoverflow,

That is to say, the APK package has been in the hands of others, we can do is to increase the difficulty of cracking, if really encounter very “persistent”, to crack the same crack. If the logic is very valuable, it is better to put the logic on the server. In addition, reinforcement is an optional solution. However, the current price of professional reinforcement in the market is not beautiful, the annual fee of each major platform varies from 30,000 to 80,000, and it is not friendly to individual developers.

Here are some of the strategies I adopted during development to prevent the application from being cracked.

1, some necessary basic knowledge

First, someone else has to crack your software. If he only uses it on his phone, he can hack it by modifying some of the system’s methods. That’s not on my radar, because their changes only work on their phones, so they can’t spread. My concern is the situation where APK files have been cracked.

We use some encryption or encoding methods when we encrypt. Common are, asymmetric encryption algorithm RSA; Symmetric encryption algorithms such as DES, 3DES and AES; Irreversible encryption, such as MD5 and SHA256.

In addition, we will implement the important encryption logic in the Native layer, so some JNI programming methods will also be required. However, the C/C++ requirement is not so high if it is only used for encryption. For more information on using JNI on Android, see my previous article “Summary of Using JNI on Android.”

2. Signature verification

2.1 Basic signature Verification

Verifying signatures in applications and SO is arguably the most basic security policy. Signature verification in an application prevents the application from being packaged twice. Because if someone changes your code, they’ll have to repackage it, and the signature will change. It is necessary to verify the signature of your SO, not only to prevent your application from being packaged, but also to prevent your SO from being stolen by others.

You can use the following code for signature verification in Java,

private static String getAppSignatureHash(final String packageName, final String algorithm) {
    if (StringUtils.isSpace(packageName)) return "";
    Signature[] signature = getAppSignature(packageName);
    if (signature == null || signature.length <= 0) return "";
    return StringUtils.bytes2HexString(EncryptUtils.hashTemplate(signature[0].toByteArray(), algorithm))
            .replaceAll("(? <=[0-9A-F]{2})[0-9A-F]{2}".": $0");
}
Copy the code

For signature verification in Native layer, the above methods can be translated into the corresponding JNI call, which will not be described here.

The above is the logic of the signature verification, seemingly beautiful, in fact, a little bit of experience with a crack can not stand. One of the methods I encountered before to crack the above signature verification is to read the APK signature in the custom Application onCreate() method and store it in the global variable, then Hook the Application signature method and return the real signature information read above. This bypasses signature verification logic.

2.2 Application Type Verification

The first method I came up with was to validate the Application type of the current Application. Because their Hook loading logic is done in a custom Application, if their Application does not match our own Application classpath, then the Application can be considered cracked.

But there are limits to this approach. I used this strategy in the belief that some attackers might be able to break all applications with a single script, so a change would prevent such attackers. But then I met some tough people. Since 360 reinforcement is used in my software, the Application of the reinforcement shell project is also considered legitimate. So, I saw some crack in my reinforcement package and made a layer of reinforcement…

2.3 Another signature verification method

The above signature verification is easy to be bypassed by Hook. We can also adopt another signature verification method.

As mentioned in the article “Some details of using APT to develop componentized framework”, one way for ARouter to load the routing information generated by APT is to get the APK of the software, and then get the class file named by the package name from the DEX of APK. So, can we also use this way to directly check the SIGNATURE of APK?

First, you can obtain the APK of your software by using the following methods:

ApplicationInfo applicationInfo = context.getPackageManager().getApplicationInfo(context.getPackageName(), 0);
File sourceApk = new File(applicationInfo.sourceDir);
Copy the code

APK signature information method is more, I have provided here is the signature of the package files in the Android source code, the code is: android.googlesource.com/platform/to…

In this way, when we get the APK, we can use the above method to verify the APK signature information directly.

3. Encryption of important information

We have mentioned some common encryption methods. Here I introduce how to encrypt important information of users when I design software and systems.

3.1 Use signature fields to prevent forged information

First of all, my application authenticates users through fields sent by the server. To prevent the information returned by the server from being tampered with or tampered with locally by users, I added a signature field to the returned authentication information. The logic goes like this,

  • After querying user information, the server concatenates a string according to predefined rules, and then uses SHA256 algorithm to encrypt the concatenated string irreversibly
  • Once the user information is retrieved from the server, it is thrown directly into the SharedPreference (preferably encrypted before stored)
  • When user authentication is required, the signature field is verified based on predefined rules to check whether the authentication information is tampered
  • If the authentication information is tampered with, it is a common user by default

In addition to the above methods, it is also necessary to configure SSL certificates for the server. Many cloud platforms now offer a one-year Trust Asia certificate (renewable when it expires) that is free to use.

3.2 Handle key-value pairs written to the local

In order to prevent the application logic from being cracked, when some important information (such as the authentication information above) is written to the local, I also do a layer of processing for the keys stored in the SharedPreference. It mainly uses device ID and key name concatenation to do SHA256 encryption as the key of the key-value pair. The device ID in this case is ANDROID_ID. Although ANDROID_ID is unreliable as a device ID, in this scenario it ensures that most users store different keys in the local key-value pair, making it more difficult for a hacker to crack a particular key-value pair.

3.3 Do not use strings for Important Information

Using strings directly in code is easy for others to search for, usually for important string information, we can first convert it to an integer array. And then you get the final string from the array in your code. For example, the following code converts a string to an array of short,

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;
}
Copy the code

3.4 Data security in Jetpack

In addition to some of the methods above, Jetpack for Android develops a Security library for data Security for devices running Android 6.0 and later. The Security library is aimed at the Security of reading and writing files in Android applications. Read the official documentation for details:

More safely processing data: developer.android.com/topic/secur…

4. Enhance obfuscation dictionaries

Confusion can make our code more difficult to read after someone decompiles it. This can enhance application security to some extent. The default obfuscation dictionary is made up of letters such as ABC and is readable. We can further make reading difficult by configuring obliquity dictionaries: special symbols, close characters like 0oO, and even Java keywords. The way you configure it is,

# method names such as confusion specified configuration - obfuscationdictionary dict. TXT confused # class name specified configuration - classobfuscationdictionary dict. TXT # package name confuse specified configuration -packageobfuscationdictionary dict.txtCopy the code

In general, when we customize obfuscation dictionaries, we need to consider the following two aspects:

  1. Obfuscating dictionaries makes decompilation difficult and makes code less readable
  2. Reduce the package size by reducing the method and field name lengths

For o0O, the readability is worse, but the code length is longer than the default obfuscation dictionary, which increases the package size of our application. I chose to obfuscate dictionaries using characters that are hard to remember. I’ve put the obfuscation dictionary up on Github so you can help yourself,

Confusion dictionary: github.com/Shouheng88/…

Here’s what happens when you mix it up,

This not only ensures that the package size does not increase, but also makes reading more difficult. However, we may encounter anti-obfuscation issues when we de-obfuscation, such as the SDK’s default anti-obfuscation tool (the tool itself).

5, so safety

I don’t have a particularly good method to crack SO. I had already moved some of the higher-level logic to native, but it was still cracked. If it is professional reinforcement, so will be reinforced at the same time. Personally, I am not very familiar with SO at present. It was cracked before because the content of SO was modified. The content related to SO will be further studied and supplemented later. The signature verification of the SO mentioned above can be used as one of the security checks, as well as some other recommendations during development.

5.1 Do not use Booleans as return types for important native methods

One disadvantage of using Booleans as return values for native methods is that they can be very easy for others to decipher. Because there are only two cases of true and false for Boolean types. As a result, a cracker can easily circumvent the validation logic by changing the class method to return true or false directly. A better way is to return an integer or string.

5.2 Native features of the verification method

If a method is a native method, we can determine whether the method has been modified by judging the attribute information of the method. As mentioned above, some native methods that return Boolean types may be tampered with to return true/false directly. At this point, the cracker modifies the native method into a normal one. Therefore, we can judge whether the method is tampered with by others by judging the native characteristics of the method. Here is an example method,

val method = cls.getMethod("method".Int: :class.java)
Modifier.isNative(method.modifiers)
Copy the code

6. Do not encapsulate validation logic in a method

Encapsulating a set of logic as a method is a good habit for normal business development. But encapsulating permission validation logic into a method is not necessarily the case. Because it’s enough for others to focus on your one method. This way, you can break all the security verification logic in your application with just one method.

However, if the logic of the same permission verification is copied everywhere the permission verification is required, the subsequent code maintenance will be very difficult. So is there a compromise method, which can realize centralized maintenance of logic, and can disperse the logic of permission verification to various places that need to do permission verification? The answer is yes, but only if the application uses kotlin.

Use inline for centralized management and decentralized invocation of permission verification: Inline is a kotlin keyword that has an effect similar to inline in C. The logic in the inline method is inlined to the place of the call at compile time. We can implement centralized management and decentralized invocation of permission validation by simply writing our permission validation logic to an inline method and then calling the inline method where authentication is required. So if we need to hack our verification logic, we need to hack each place in turn.

In addition,

1. Permission validation logic is best interwoven with business code rather than written separately. The reason above, separate write others as long as crack this method is enough. The C/C++ layer can also try inline.

7. Use the server for security verification

As stated above, the best security measure is to keep the important logic in the back end. However, for the application I developed, because it is basically used offline, so it cannot use the server to do authentication during operation. To do this, I used two schemes to get the server involved in anti-cracking.

The first is to enable the version configuration and send the forced upgrade information in the application configuration. When I first designed the server for the application, I designed the interface for the application to pull configuration information from the back end. This interface also delivers the version information and upgrade type of the application. If the upgrade is forced, a dialog box pops up that cannot be cancelled. This version is basically unusable. With this configuration, we can disable the cracked version of the application directly through the server configuration.

The second is reporting to the server when performing operations that require advanced privileges. The server determines whether the user has the permission based on the back-end storage user information. If you do not have permission, add a violation record and record the user information of the offending user. The background can be configured to disable a single user. As for the question why not directly disable the user here. As in the seven Samurai episode, good defense always leaves an opening. Direct disabling is easy for attackers to detect and deal with.

In addition, it is best not to throw an exception directly, and do not use plaintext strings for pop-up toasts. Either way, it’s easy for someone to directly locate the logic we’re checking. If you have to throw exceptions, it is recommended to trigger OOM!

conclusion

After writing so many things, I have no choice but to crack it is much easier than anti-crack. These are some of the basic skills I have summed up in the process of practice. I still have a lot to learn about Android app security. After all, security is another area of expertise for application-layer development. I can only “guard against the gentleman but not against the villain”. Later, I learned more content, did more offensive and defensive battles, summed up more experience and then supplemented. Alas, “this is the same root, why too urgent!”