NDH2k12-wargame Write-up CrackMe Android
By w3pwnz on Monday, July 2 2012, 20:31 :: ndh2k12 :: Permalink
File : NDH.apk
We were asked to reverse an Android application, coming as an APK file.
The first move was to get the Java source of that application with dex2jar in order to decompile the APK into a plain JAR file.
If you ever want more information about this step, check out that link http://tinyurl.com/blcp353.
Once we have the JAR file, we opened it with JD-GUI and located the main Activity, that is to say the entry point of an application.
Here's the code :
package com.app.ndh; import android.app.Activity; import android.app.AlertDialog; import android.app.AlertDialog.Builder; import android.content.DialogInterface; import android.content.DialogInterface.OnClickListener; import android.os.Bundle; import android.telephony.TelephonyManager; import android.text.Editable; import android.view.View; import android.view.View.OnClickListener; import android.widget.Button; import android.widget.EditText; import android.widget.TableLayout; import android.widget.TextView; public class NDHActivity extends Activity { static { System.loadLibrary("verifyPass"); } private native String print(String paramString); public void onCreate(Bundle paramBundle) { super.onCreate(paramBundle); TextView localTextView = new TextView(this); localTextView.setText("Enter the code to activate this software :"); AlertDialog.Builder localBuilder1 = new AlertDialog.Builder(this); AlertDialog.Builder localBuilder2 = localBuilder1.setCancelable(false); 1 local1 = new DialogInterface.OnClickListener() { public void onClick(DialogInterface paramDialogInterface, int paramInt) { } }; AlertDialog.Builder localBuilder3 = localBuilder1.setPositiveButton("OK", local1); EditText localEditText = new EditText(this); localEditText.setInputType(0); Button localButton = new Button(this); localButton.setText("Activation"); TelephonyManager localTelephonyManager = (TelephonyManager)getSystemService("phone"); 2 local2 = new View.OnClickListener(localTelephonyManager, localBuilder1, localEditText) { public void onClick(View paramView) { if (Integer.decode(this.val$test.getDeviceId()).intValue() == 0) AlertDialog.Builder localBuilder1 = this.val$builder.setMessage("Bad Password"); while (true) { this.val$builder.create().show(); return; AlertDialog.Builder localBuilder2 = this.val$builder; NDHActivity localNDHActivity = NDHActivity.this; String str1 = this.val$tv2.getText().toString(); String str2 = localNDHActivity.print(str1); AlertDialog.Builder localBuilder3 = localBuilder2.setMessage(str2); } } }; localButton.setOnClickListener(local2); TableLayout localTableLayout = new TableLayout(this); localTableLayout.addView(localTextView); localTableLayout.addView(localEditText); localTableLayout.addView(localButton); setContentView(localTableLayout); } }
Before going any further with that code, we wanted it to be running on a Android emulator and we had to patch few things.
This time, we completely disassembled the APK with apktool. This tool is BakSmaling (disassembling) the compiled code whose assembly representation is called Smali and unpacking all the files contained in an APK (certificates, libs etc.).
It also disassembles the graphical resources files and the AndroidManifest.xml file, which lists all the required settings for an application to run (platform version, permissions etc.).
android@honeynet:~/tools/apktool$ apktool d ~/ndh/NDH.apk ~/ndh/NDH_Decomp I: Baksmaling... I: Loading resource table... I: Loaded. I: Loading resource table from file: /home/android/apktool/framework/1.apk I: Loaded. I: Decoding file-resources... I: Decoding values*/* XMLs... I: Done. I: Copying assets and libs...
Here's the AndroidManifest.xml :
<?xml version="1.0" encoding="utf-8"?> <manifest android:versionCode="1" android:versionName="1.0" package="com.app.ndh" xmlns:android="http://schemas.android.com/apk/res/android"> <uses-sdk android:minSdkVersion="15" /> <uses-permission android:name="android.permission.READ_PHONE_STATE" /> <application android:label="@string/app_name" android:icon="@drawable/ndh"> <activity android:label="@string/app_name" android:name=".NDHActivity"> <intent-filter> <action android:name="android.intent.action.MAIN" /> <category android:name="android.intent.category.LAUNCHER" /> </intent-filter> </activity> </application> </manifest>
That manifest requires Android 4.0.3 (API 15) as minimum SDK version. Having a 2.3.3 (API 10) emulator, we just removed that line in the Manifest.
Now, let's go back into the code.
Just by reading the source, we're aware that this application graphically consists of a EditText (a textbox) and a Button. So basically, we have to enter an activation code and hit the button for it to be checked.
Once hit, the first check is :
if (Integer.decode(this.val$test.getDeviceId()).intValue() == 0) AlertDialog.Builder localBuilder1 = this.val$builder.setMessage("Bad Password");
To be read as : if the IMEI of the terminal equals 0, then always display "Bad Password", whatever activation code you typed (even the good one :)).
As the emulated terminals have no real IMEI, this value is set to 0. This first check is actually an anti-emulator feature :)
In order to remove that check, we had to directly patch the Smali code. Here's the original Smali section, in the NDH_Decomp/smali/com/app/ndh/NDHActivity$2.smali file :
iget-object v1, p0, Lcom/app/ndh/NDHActivity$2;->val$test:Landroid/telephony/TelephonyManager; ; Grab the terminal's IMEI invoke-virtual {v1}, Landroid/telephony/TelephonyManager;->getDeviceId()Ljava/lang/String; move-result-object v1 ; Decode it as an integer value invoke-static {v1}, Ljava/lang/Integer;->decode(Ljava/lang/String;)Ljava/lang/Integer; move-result-object v1 invoke-virtual {v1}, Ljava/lang/Integer;->intValue()I move-result v1 ; if (v1 == IMEI) != 0 then jump to the cond_0 label (the legit "while(true){}" section), else print "Bad Password" if-nez v1, :cond_0 iget-object v1, p0, Lcom/app/ndh/NDHActivity$2;->val$builder:Landroid/app/AlertDialog$Builder; const-string v2, "Bad Password"
We simply transformed the conditional jump "if-nez v1, :cond_0" to a unconditional jump, that's to say : "goto :cond_0"
Having both the Smali code and the Manifest patched, we rebuilt the application with apktool :
android@honeynet:~/tools/apktool$ apktool b ~/ndh/NDH_Decomp ~/ndh/New_NDH.apk I: Checking whether sources has changed... I: Checking whether resources has changed... I: Building apk file...
Still, we needed to sign the application, as the emulator won't install an unsigned APK. Here comes the signing command, with a self-signed certificate :
android@honeynet:~/tools/apktool$ jarsigner -verbose -sigalg MD5withRSA -digestalg SHA1 -keystore /home/android/my-release-key.keystore -storepass my_pass ~/ndh/New_NDH.apk maz adding: META-INF/MANIFEST.MF adding: META-INF/MAZ.SF adding: META-INF/MAZ.RSA signing: lib/armeabi/libverifyPass.so signing: res/drawable-hdpi/ic_launcher.png signing: res/drawable-hdpi/ndh.png signing: res/drawable-ldpi/ic_launcher.png signing: res/drawable-ldpi/ndh.png signing: res/drawable-mdpi/ic_launcher.png signing: res/drawable-mdpi/ndh.png signing: res/drawable-xhdpi/ic_launcher.png signing: res/drawable-xhdpi/ndh.png signing: res/layout/main.xml signing: AndroidManifest.xml signing: classes.dex signing: resources.arsc
The patched APK can be found here.
So now we can install and execute that application on an emulator.

Let's reverse that crackme ! Going back into the code, we have those 3 code sections:
static { System.loadLibrary("verifyPass"); }
private native String print(String paramString);
NDHActivity localNDHActivity = NDHActivity.this; String str1 = this.val$tv2.getText().toString(); String str2 = localNDHActivity.print(str1); AlertDialog.Builder localBuilder3 = localBuilder2.setMessage(str2);
While the first instruction loads a native library, a .so file written in C and compiled with the NDK (Native Development Kit), the second stands for the declaration of the native method, here called "print".
The last part indicates that the text from the EditText element is passed to the native "print" method and its result is then displayed in a popup.
All in all, the validation routine is contained in that lib.
Thus, the next step was to reverse the "libverifyPass.so" file.
As said, apktool unpacked every file in the APK.
android@honeynet:~/ndh/NDH_Decomp$ file lib/armeabi/libverifyPass.so lib/armeabi/libverifyPass.so: ELF 32-bit LSB shared object, ARM, version 1 (SYSV), dynamically linked, stripped
Let's start by opening libverifyPass in IDA.
In the "Strings" window, we only have two strings : "Bad password" & "Good password, this is the flag too".
Those two strings are only referenced in one function : "Java_com_app_ndh_NDHActivity_print", which is the function called from the Java source.
Here is a global view of this function :
We can split it in a few basic blocks :
- purple : initialization
- yellow : main loop
- blue : final tests
- red : fail (references "bad password")
- green : success (references "Good password")
Before going any further, let's see a few specific details of the JNI (Java Native Interface) :
.text:00000F68 STR R0, [SP,#0x140+ptr_vtable_like] .text:00000F6A STR R1, [SP,#0x140+not_used] .text:00000F6C STR R2, [SP,#0x140+user_string]
We can see in the above snippet of the initialization block, that the function takes 3 parameters (in R0-R2) that are immediatly saved in local variables on the stack. But the Java source only use one argument, so the first two must be implicit arguments from the JNI.
The second is unused, but the first is used like a pointer on a vtable in C++, containing some function pointers used to do virtual calls, as in the following snippet :
.text:00000F78 LDR R2, [R3] ; Address of the "vtable-like" in R2 .text:00000F7A MOVS R3, 0x2A4 ; Offset = 0x2A4 .text:00000F7E LDR R3, [R2,R3] ; Address of the "virtual function" .text:00000F80 LDR R1, [SP,#0x140+ptr_vtable_like] .text:00000F82 LDR R2, [SP,#0x140+user_string] .text:00000F84 MOVS R0, R1 ; Arg 1 -> our pointer .text:00000F86 MOVS R1, R2 ; Arg 2 -> our string .text:00000F88 MOVS R2, #0 ; Arg 3 .text:00000F8A BLX R3 ; "virtual call"
As explained in the wiki page, this is how the JNI works. All functions take a JNIEnv pointer as first parameter, which is a structure containing pointers to all the required method to interface with the JVM :
/* Example of a JNI function, taking a JNIEnv pointer as first parameter */ JNIEXPORT void JNICALL Java_ClassName_MethodName(JNIEnv *env, jobject obj, jstring javaString) { /* Example of a function call, using a function pointer in JNIEnv */ const char *nativeString = (*env)->GetStringUTFChars(env, javaString, 0); [...] }
Our program use this type of call 3 times, with the indexes 0x2A4, 0x290 and 0x29C. To retreive the names of the corresponding fonctions, we can use directly the header file jni.h or this documentation which contains the indexes in the JNIEnv interface function table :
0x24A / 4 = 169 --> const jbyte* GetStringUTFChars(JNIEnv *env, jstring string, jboolean *isCopy); 0x290 / 4 = 164 --> jsize GetStringLength(JNIEnv *env, jstring string); 0x29C / 4 = 167 --> jstring NewStringUTF(JNIEnv *env, const char *bytes);
We can now analyse our routine with a full knowledge of which function it's using :)
Starting with the end (the blue "final tests" block), we can see that the block doesn't really test anything on the supplied string, except that the result of a previous call to "GetStringLength" must equals 0xC.
Besides, there's just some really basic anti debug trick here : a call to difftime() is made, with the return values of two calls to time() before and after the main loop, and the result is tested against a hardcoded double value, that it must not exeed (R2=0x00000000 R3=0x3FF00000, so our maximum value is 1.0, but we really don' care since we are doing this in static analysis :p)
So let's skip to the interesting part. In the initialization block, two counters are set to 0, and a call to GetStringLength is made (but not directly tested).
.text:00000FFE MOVS R3, #0 .text:00001000 STR R3, [SP,#0x140+counter1] .text:00001002 MOVS R3, #0 .text:00001004 STR R3, [SP,#0x140+counter2]
More important, some structures in the .rodata section are loaded into the stack, as we can see in the following snippet :
.text:00000F9A ADD R3, SP, #0x140+loaded_27E0 ; R3 = stack buffer .text:00000F9C LDR R2, =(dword_27E0 - 0xFA2) ; R2 = rodata buffer .text:00000F9E ADD R2, PC .text:00000FA0 LDMIA R2!, {R0,R1,R5} ; Load from R2 into multiple registers (R0, R1 and R5), and increment R2 .text:00000FA2 STMIA R3!, {R0,R1,R5} ; Store to R3 from multiple registers, and increment R3 .text:00000FA4 LDMIA R2!, {R0,R1,R5} .text:00000FA6 STMIA R3!, {R0,R1,R5} .text:00000FA8 LDMIA R2!, {R0,R1,R5} .text:00000FAA STMIA R3!, {R0,R1,R5} .text:00000FAC LDMIA R2!, {R0,R1,R5} .text:00000FAE STMIA R3!, {R0,R1,R5}
Using R0, R1, and R5, and repeating LDMIA/STMIA 4 times, we can see that this code loads 12 dwords. And there are 5 blocks like this one. The last action of the initialization block is to load the first char of our string somewhere in the stack (SP+0x20+0xFF) :
.text:00000FF2 MOV R3, SP ; stack .text:00000FF4 ADDS R3, #0x20 .text:00000FF6 ADDS R3, #0xFF .text:00000FF8 LDR R2, [SP,#0x140+c_string] ; our string, result of GetStringUTFChars .text:00000FFA LDRB R2, [R2] ; first char .text:00000FFC STRB R2, [R3] ; store it !
Now let's finish this crackme with the main loop :
It first exits the loop to the final block if the current character (stored in SP+0x20+0xFF) is zero, this is our stop condition. After entering the loop, it also exits and prints "bad password" if the counter1 is greater than 0xC. Otherwise, (this part is the interesting one) it computes a xor between two values in the previously loaded on the stack data, and compares the result with the current password :
.text:00001040 LDR R2, [SP,#0x140+counter1] .text:00001042 ADD R3, SP, #0x140+loaded_2810 .text:00001044 LSLS R2, R2, #2 ; R2 = counter1 * 4 .text:00001046 LDR R2, [R2,R3] ; R2 = loaded_2810[ counter1 * 4 ] .text:00001048 LDR R1, [SP,#0x140+counter1] .text:0000104A ADD R3, SP, #0x140+loaded_2840 .text:0000104C LSLS R1, R1, #2 ; R1 = counter1 * 4 .text:0000104E LDR R3, [R1,R3] ; R3 = loaded_2840[ counter1 * 4 ] .text:00001050 EORS R2, R3 ; R2 = loaded_2810[ counter1 * 4 ] ^ loaded_2840[ counter1 * 4 ] .text:00001052 LDR R3, [SP,#0x140+counter1] .text:00001054 LDR R1, [SP,#0x140+c_string] .text:00001056 ADDS R3, R1, R3 .text:00001058 LDRB R3, [R3] ; R3 = c_string[ counter1 ] .text:0000105A CMP R2, R3 ; compare R2 / R3
So this is it :)
The last block of the function doesn't do anything really important (increment the counters, update the current char tested at the beginning of the loop, do some xor on some previously loaded on the stack but unused data, ...)
We now have to dump the 2*12 dwords in the .rodata section, at 0x2810 and 0x2840, and xor them to obtain the password :
>>> a = [0x52,0x1A,0x09,0x7B,0x4B,0x5C,0x20,0x72,0x10,0x67,0x5E,0x49] >>> b = [0x01,0x4E,0x4C,0x3A,0x00,0x08,0x14,0x20,0x44,0x53,0x0C,0x0C] >>> "".join( [ chr(a[i] ^ b[i]) for i in range(len(a)) ] ) 'STEAKT4RT4RE'
Pwned !


