Android applications security — part 1, reverse engineering and token storage problems by Adrian Defus

There are many ways you can store keys and tokens in your Android applications — directly in the code, inside your database or by using NDK layer. But in terms of security — should you do it at all?

This article will show you the most common ways you can use to store different types of keys and tokens in your Android applications, while proving that none of these methods is good enough 😉. The second part will contain more detailed considerations regarding how our application should communicate securely with the server, how to ensure the authentication of both entities, and how the attacker can affect our connection.

Looking inside our applications (reverse engineering)

The applications which we use on our devices are actually properly packed .apk files containing all the data required to operate correctly. If we want to view their content and see what is hidden there, we have to make some necessary preparations. First of all, we need a tool that will let us communicate with the mobile device using our computer — Android Debug Bridge is what we are looking for. Secondly if we want to disassemble our .apk files into separated source code files (.smali classes), Apktool seems to be the perfect choice. Now you’re probably wondering what this “.smali” format is, so let’s get into some theory. When you create your application code, the .apk file contains a .dex file with a binary Dalvik bytecode inside, which is the format that the platform actually understands, but it’s horrible to read or edit. Fortunately, there are tools like the aforementioned Apktool that convert .dex files into (and from) a human readable representation — and the most common format is known as Smali.

So what are the steps we need to perform to look inside any application in our mobile device?

  • Step 1: Print the names of the device packages (installed applications) with an associated base file using the Android Debug Bridge and command line:
adb shell pm list packages -f

Here is the sample output:

Device packages list output 

  • Step 2: Choose the app and download it using the adb tool:
adb pull <package-name>/base.apk

the base.apk file is now in our computer in the adb directory, so let’s decompile it

  • Step 3: Decompile the downloaded .apk file using apktool:
apktool d -r base.apk

‘d’ — decompile, ‘-r’ — do not decode resources

  • Step 4: Open the .smali files inside the created ‘base’ directory and try to find some secrets 😉

Token storage problem

In most cases when we create a web service designed to handle REST queries from our application, we need to be sure that they come from one specific source. The most common way to achieve this is to send a specific application token as a parameter directly in the url address, as an http Header or in a POST body. When the server receives a query containing such a token, it compares it with the one saved internally, and if both are the same, it sends the required information back to the client. As long as the token stored on the server’s side is relatively secure, there is a problem with the one on the client’s side. The following example shows the source code of a simple application with some web service included — the token used for authentication is stored as a constant value directly in the code.

class MainActivity : AppCompatActivity() {

    companion object {
        private const val SECRET_TOKEN = "jb&1a=U51-ng2="
    }

    private val superService by lazy {
        SuperService()
    }

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        connectToWebservice()
    }

    private fun connectToWebservice() {
        superService.connect(SECRET_TOKEN)
    }
}

Using the reverse engineering techniques described earlier we can now decompile an .apk file of the application containing the above code and read the contents of the .smali file. Let’s have a closer look at the connectToWebservice() function.

.method private final connectToWebservice()V
    .locals 2

    .line 38
    invoke-direct {p0}, Ltech/skyrise/adriandefus/testsecurityapp/MainActivity;->getSuperService()Ltech/skyrise/adriandefus/testsecurityapp/SuperService;

    move-result-object v0

    const-string v1, "jb&1a=U51-ng2="

    invoke-virtual {v0, v1}, Ltech/skyrise/adriandefus/testsecurityapp/SuperService;->connect(Ljava/lang/String;)V

    return-void
.end method

As you can see in line 9 our SECRET_TOKEN value is assigned to the variable v1, which is later used in the connect() method in line 11. In this example, the obtaining our secret key simply stored as a constant value in the source code was easy and did not require the use of any advanced techniques and tools.

The second example is the technique involving the project build tool — Gradle. First of all, we create a keystore.properties file in our root project directory, which contains the definition of our SECRET_TOKEN.

SECRET_TOKEN = "jb&1a=U51-ng2="

Then we include this file in our app build.gradle.

android {

    (...)
    
    def keystorePropertiesFile = rootProject.file("keystore.properties")
    def keyStoreProperties = new Properties()
    keyStoreProperties.load(new FileInputStream(keystorePropertiesFile))
    
    defaultConfig {
    
        (...)

        buildConfigField("String", "SECRET_TOKEN", keyStoreProperties["SECRET_TOKEN"])
    }
}

And finally, using the BuildConfig class, we can obtain the SECRET_TOKEN value in our connectToWebservice() function.

private fun connectToWebservice() {
    superService.connect(BuildConfig.SECRET_TOKEN)
}

Using the same techniques as before, let’s decompile the .apk file and look into the MainActivity.smali class.

.method private final connectToWebservice()V
    .locals 2

    .line 38
    invoke-direct {p0}, Ltech/skyrise/adriandefus/testsecurityapp/MainActivity;->getSuperService()Ltech/skyrise/adriandefus/testsecurityapp/SuperService;

    move-result-object v0

    const-string v1, "jb&1a=U51-ng2="

    invoke-virtual {v0, v1}, Ltech/skyrise/adriandefus/testsecurityapp/SuperService;->connect(Ljava/lang/String;)V

    return-void
.end method

As you can see, regardless of the steps we’ve taken to potentially provide a better way to secure our token, after decompiling the application its value is just as visible as before. It looks like it’s time to bring out the big guns — including the NDK layer.

The Android NDK is a toolset that lets us implement parts of our app in a native code, using languages such as C and C++. The main advantage of including native support is that the code written in C/C++ is compiled into machine instructions that are run directly by the device CPU. Many multimedia or video-processing applications use native code for processor-intensive tasks. Considering that the C/C ++ code is in a different layer, the decompiled class containing the NDK reference should not contain the code from the lower level, so if we place our SECRET_TOKEN there, we will not be able to see it. Let’s check it in our application.

class MainActivity : AppCompatActivity() {

    companion object {
        init {
            System.loadLibrary("native-lib")
        }
    }

    private external fun secretKey(): String

    private val superService by lazy {
        SuperService()
    }

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        connectToWebservice()
    }

    private fun connectToWebservice() {
        superService.connect(secretKey())
    }
}

So when our Activity class is created, the “native-lib” is loaded. Thanks to this we can use the external getSecretKey() function, returning String with our SECRET_TOKEN directly from the NDK layer. Let’s take a look into the C++ getSecretKey() implementation.

extern "C" JNIEXPORT jstring JNICALL
Java_tech_skyrise_adriandefus_testsecurityapp_MainActivity_secretKey(
        JNIEnv* env,
        jobject /* this */) {
    std::string appKey = "jb&1a=U51-ng2=";
    return env->NewStringUTF(appKey.c_str());
}

It’s just a simple function returning the hard-coded token value. Reverse engineering of our application proves that the SECRET_TOKEN is not visible any more.

.method private final connectToWebservice()V
    .locals 2

    .line 42
    invoke-direct {p0}, Ltech/skyrise/adriandefus/testsecurityapp/MainActivity;->getSuperService()Ltech/skyrise/adriandefus/testsecurityapp/SuperService;

    move-result-object v0

    invoke-direct {p0}, Ltech/skyrise/adriandefus/testsecurityapp/MainActivity;->secretKey()Ljava/lang/String;

    move-result-object v1

    invoke-virtual {v0, v1}, Ltech/skyrise/adriandefus/testsecurityapp/SuperService;->connect(Ljava/lang/String;)V

    return-void
.end method

Although our token is no longer visible in the .smali file, the only difficulty we’ve added for the potential attacker is simply that he has to take a few more steps and look deeper into our source code. Methods written in native layers are stored in the “lib” folder with the .so extension. We are able to decompile them to machine code using, for example, the IDA disassembler. Due to different processor architectures used by various Android devices there are 4 types of .so files inside our .apk — arm64-v8a, armeabi-v7a, x86 and x84_64. Let’s select x86 version of our native library. After successfully decompiling the file into machine code we have to search for the Java_<package-name>_secretKey() function and analyse its code.

.text:00030FF0                 public Java_tech_skyrise_adriandefus_testsecurityapp_MainActivity_secretKey
.text:00030FF0 Java_tech_skyrise_adriandefus_testsecurityapp_MainActivity_secretKey proc near
.text:00030FF0                                         ; DATA XREF: LOAD:00000230↑o
.text:00030FF0
.text:00030FF0 arg_0           = dword ptr  8
.text:00030FF0
.text:00030FF0                 push    ebp
.text:00030FF1                 mov     ebp, esp
.text:00030FF3                 push    ebx
.text:00030FF4                 push    edi
.text:00030FF5                 push    esi
.text:00030FF6                 and     esp, 0FFFFFFF0h
.text:00030FF9                 sub     esp, 10h
.text:00030FFC                 call    $+5
.text:00031001                 pop     ebx
.text:00031002                 add     ebx, 0A52C3h
.text:00031008                 mov     edi, [ebp+arg_0]
.text:0003100B                 mov     dword ptr [esp], 10h ; unsigned int
.text:00031012                 call    __Znwj          ; operator new(uint)
.text:00031017                 mov     esi, eax
.text:00031019                 mov     word ptr [eax+0Ch], 3D32h
.text:0003101F                 mov     dword ptr [eax+8], 676E2D31h
.text:00031026                 mov     dword ptr [eax+4], 35553D61h
.text:0003102D                 mov     dword ptr [eax], 3126626Ah
.text:00031033                 mov     byte ptr [eax+0Eh], 0
.text:00031037                 mov     eax, [edi]
.text:00031039                 mov     eax, [eax+29Ch]
.text:0003103F                 mov     [esp+4], esi
.text:00031043                 mov     [esp], edi
.text:00031046                 call    eax
.text:00031048                 mov     edi, eax
.text:0003104A                 mov     [esp], esi      ; void *
.text:0003104D                 call    __ZdlPv         ; operator delete(void *)
.text:00031052                 mov     eax, edi
.text:00031054                 lea     esp, [ebp-0Ch]
.text:00031057                 pop     esi
.text:00031058                 pop     edi
.text:00031059                 pop     ebx
.text:0003105A                 pop     ebp
.text:0003105B                 retn

Our secret token is stored at 00031019, 0003101F, 00031026 and 0003102D as hexadecimal arrays represented in little-endian format, so we have to concatenate them, convert the result to string and reverse its order, using e.g. a string-functions website. Summarizing, the whole process of the retrieval of our SECRET_TOKEN looks like this:

3D32676E2D3135553D613126626A → =2gn-15U=a1&bj → jb&1a=U51-ng2=

If we continued the process of hiding our token more and more deeply, we would probably eventually reach the OWASP Crackmes website containing applications written as key-retrieval challenges — regardless of their difficulties, the secret token hidden somewhere in the application was in each case more or less easy to recover.

Reverse engineering — code injection

Regardless of how deeply the secret key was hidden in our application, instead of wasting time on searching for it in the place where it was saved, we can try to capture it where it is actually used — e.g. placed as a parameter of the function sending it in some REST query. The easiest way to achieve this is to debug our application by setting a breakpoint in the place where the token has to be assigned to e.g. some function. However, the problem is that applications published on Google Play Store are by default non-debuggable. The solution is to use the simplest form of code injection — setting the android:debuggable=”true” parameter in the application downloaded to our computer. It has to be set in the AndroidManifest.xml file located in the directory containing the decompiled source of our app. After that, we can build a new .apk file containing changed data using Apktool.

apktool b base -o app-modified.apk

“b” — build “-o” — output

Unfortunately, if you try to install a modified .apk, you will get an error: [INSTALL_PARSE_FAILED_NO_CERTIFICATES]. It’s because Android requires that all APKs be digitally signed with a certificate before they can be installed. Let’s solve it by generating a key and keystore and signing it using jarsigner.

jarsigner -verbose -keystore <custom.keystore> -signedjar <signed-output>.apk <unsigned-app>.apk <keystore-alias>

Opening our modified re-signed APK using Android Studio’s “Profile or Debug APK” option (with a Smalidea plugin installed) and setting the breakpoint in the desired place results in revealing our secret token directly in the debug window.

Retrieving the secret token using .smali debugger 

By using more complex methods of code injection, we are able, for example, to change the way our application works to obtain certain data — e.g. creating a class that has a static method of accepting a string as a parameter and copying it to the device clipboard. By injecting a call to this method in a place where the token is used we will be able to easily obtain it. This is especially useful for applications that have security checks verifying whether our device is rooted, if the debug mode enabled or if the ADB is active — we can install the modified application on a ‘secure’ device and simply read the value directly from the clipboard. At the same time, it should be kept in mind that any interference with the source code requires us to re-sign the application with our custom certificate. Some apps may have mechanisms to check whether they have been signed with the appropriate key and in case of their incompatibility, they may not allow a further process to run. Fortunately, code injection also allows us to disable functions where these conditions are verified (see: OWASP Crackmes level 3 — disabling security check).

Summary

Regardless of how much we will try to protect our applications against the unauthorized use of our secret key, a potential attacker with the appropriate knowledge and tools will not have much difficulty in reading its value. Therefore, the only reasonable way to protect our key is to never let it touch our application. This is especially true for keys that can be used from any source, e.g. Google Places API keys. In this case, our application should query our internal server having the appropriate token, which then performs a query to some next source using our API client key. But if storing tokens inside our apps is not recommended, how can our server verify that the query it has just received comes from a trusted source? See the second part of this article!


Originally posted: https://medium.com/skyrise/android-applications-security-part-1-2782d73771e0

January 29, 2019
Subscribe
Notify of
guest

This site uses Akismet to reduce spam. Learn how your comment data is processed.

2 Comments
Newest
Oldest Most Voted
Inline Feedbacks
View all comments
© HAKIN9 MEDIA SP. Z O.O. SP. K. 2013