Nuit-du-hack-2012-android-challenge
Description
This challenge has been introduced at Nuit du Hack (NDH) 2012. The apk can be downloaded from here.
Let's first install the application on an emulator:
$ adb install NDH.apk 2716 KB/s (49536 bytes in 0.017s) pkg: /data/local/tmp/NDH.apk Success
The application looks like this:
Base code analysis
Decompilation
Let's first decompile the application using apktool:
mobisec@ubuntu:/data/ndh$ apktool d NDH.apk I: Using Apktool 2.0.0-RC3 on NDH.apk I: Loading resource table... I: Decoding AndroidManifest.xml with resources... I: Loading resource table from file: /home/mobisec/apktool/framework/1.apk I: Regular manifest package... I: Decoding file-resources... I: Decoding values */* XMLs... I: Baksmaling classes.dex... I: Copying assets and libs... I: Copying unknown files... I: Copying original files...
The AndroidManifest.xml file shows that there is 1 user interface: NDHActivity
<?xml version="1.0" encoding="utf-8" standalone="no"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android" package="com.app.ndh">
<uses-permission android:name="android.permission.READ_PHONE_STATE"/>
<application android:icon="@drawable/ndh" android:label="@string/app_name">
<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>
dex2jar
Now, let's convert the APK to JAR with dex2jar:
$ /data/tools/dex2jar-2.0/d2j-dex2jar.sh NDH.apk dex2jar NDH.apk -> ./NDH-dex2jar.jar
And open the JAR file with JD-GUI. Go to com.app.ndh > NDHActivity.
We see that a shared library (*.so) is loaded:
System.loadLibrary("verifyPass");
Also we can see an anti-debug technique based on the IMEI verification (getDeviceId returns the unique device ID of a subscription, for example, the IMEI for GSM and the MEID for CDMA phones). In an emulator, the IMEI is set to 0. The application checks that this value is not 0. Else, it will return "Wrong password", whatever value is entered:
if (Integer.decode(this.val$test.getDeviceId()).intValue() == 0)
Patch the application
We need to patch the application to get rid of this anti-debug trick. The test is referenced in the NDHActivity$2.smali file:
$ grep -R getDeviceId * smali/com/app/ndh/NDHActivity$2.smali: invoke-virtual {v1}, Landroid/telephony/TelephonyManager;->getDeviceId()Ljava/lang/String;
Edit the file and replace the if-nez instruction by an unconditional goto statement:
--- NDH/smali/com/app/ndh/NDHActivity$2.smali 2015-10-20 23:22:37.774831362 -0700 +++ NDH_patched/smali/com/app/ndh/NDHActivity$2.smali 2015-10-19 05:34:35.183056786 -0700 @@ -69,7 +69,7 @@ move-result v1 - if-nez v1, :cond_0 + goto :cond_0 .line 49 iget-object v1, p0, Lcom/app/ndh/NDHActivity$2;->val$builder:Landroid/app/AlertDialog$Builder;
Then we have to recompile the application:
$ apktool b -o NDH_patched.apk NDH/ I: Using Apktool 2.0.1 I: Checking whether sources has changed... I: Smaling smali folder into classes.dex... I: Checking whether resources has changed... I: Building resources... I: Copying libs... (/lib) I: Building apk file... I: Copying unknown files/dir...
And as we have modified the initial APK we must sign it to be able to install it:
$ java -jar signapk.jar certificate.pem key.pk8 ../ndh/NDH_patched.apk ../ndh/NDH_patched_signed.apk
Once done, uninstall the previous copy of the application on the emulator and install the new patched/signed application:
$ adb shell "pm uninstall com.app.ndh" Success $ adb install NDH_patched_signed.apk
Activation button
Back to JD-GUI, we continue the code analysis:
When the "Activation" button is clicked, the user input is passed to the print function in the shared library as seen previously:
localEditText.setInputType(0); Button localButton = new Button(this); localButton.setText("Activation"); localButton.setOnClickListener(new View.OnClickListener() { public void onClick(View paramAnonymousView) { Integer.decode(this.val$test.getDeviceId()).intValue(); while (true) { this.val$builder.create().show(); return; this.val$builder.setMessage(NDHActivity.this.print(localEditText.getText().toString())); }
Graph overview
Now, we need to analyze the shared library. Open lib/armeabi/libverifyPass.so in IDA-Pro. Here is the graph overview:
Initialization
At offset 0xF8A, the GetStingUTFChars is called and the user input is saved to a variable at offset 0xF8E.
.text:00000F76 LDR R3, [SP,#0x140+JNIEnv] ; \
.text:00000F78 LDR R2, [R3] ; |
.text:00000F7A MOVS R3, #0x2A4 ; |
.text:00000F7E LDR R3, [R2,R3] ; |
.text:00000F80 LDR R1, [SP,#0x140+JNIEnv] ; |
.text:00000F82 LDR R2, [SP,#0x140+user_input] ; |
.text:00000F84 MOVS R0, R1 ; |
.text:00000F86 MOVS R1, R2 ; |
.text:00000F88 MOVS R2, #0 ; |
.text:00000F8A BLX R3 ; / GetStringUTFChars
.text:00000F8C MOVS R3, R0 ; R3 = utf8(user_input)
.text:00000F8E STR R3, [SP,#0x140+utf_user_input]
Then 5 arrays of 12 DWORDs each are defined from buffers located in the RODATA section. Below is the code for the first 2 arrays:
.text:00000F9A ADD R3, SP, #0x140+buf_27E0 ; \
.text:00000F9C LDR R2, =(dword_27E0 - 0xFA2) ; |
.text:00000F9E ADD R2, PC ; dword_27E0 ; |
.text:00000FA0 LDMIA R2!, {R0,R1,R5} ; | R0 = 0xDB, R1 = 0xC4, R5 = 0x56, R2 = 0x7EC
.text:00000FA2 STMIA R3!, {R0,R1,R5} ; |
.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} ; /
.text:00000FB0 ADD R3, SP, #0x140+buf_2810 ; \
.text:00000FB2 LDR R2, =(dword_2810 - 0xFB8) ; |
.text:00000FB4 ADD R2, PC ; dword_2810 ; |
.text:00000FB6 LDMIA R2!, {R0,R1,R5} ; |
.text:00000FB8 STMIA R3!, {R0,R1,R5} ; |
.text:00000FBA LDMIA R2!, {R0,R1,R5} ; |
.text:00000FBC STMIA R3!, {R0,R1,R5} ; |
.text:00000FBE LDMIA R2!, {R0,R1,R5} ; |
.text:00000FC0 STMIA R3!, {R0,R1,R5} ; |
.text:00000FC2 LDMIA R2!, {R0,R1,R5} ; |
.text:00000FC4 STMIA R3!, {R0,R1,R5} ; /
Starting from offset 0xFF2, the user input is saved to the stack at SP+0x20+0xFF:
.text:00000FF2 MOV R3, SP ; \
.text:00000FF4 ADDS R3, #0x20 ; |
.text:00000FF6 ADDS R3, #0xFF ; |
.text:00000FF8 LDR R2, [SP,#0x140+utf_user_input] ; |
.text:00000FFA LDRB R2, [R2] ; |
.text:00000FFC STRB R2, [R3] ; / Store user input at SP+0x20+0xFF
The user input length is saved to a variable at 0x101C. 2 tests confirm that the expected length is 12 characters:
Main loop
Exit conditions
The main loop starts from offset 0x10B6. It stops if one of the following conditions is satisfied:
- when all characters have been checked:
.text:000010B6 MOV R3, SP
.text:000010B8 ADDS R3, #0x20
.text:000010BA ADDS R3, #0xFF
.text:000010BC LDRB R3, [R3] ; R3 = SP+0x20+0xFF = user_input
.text:000010BE CMP R3, #0 ; \
.text:000010C0 BNE loc_103A ; / Loop until last char of user
- or if the user input has more than 12 characters:
.text:0000103A LDR R3, [SP,#0x140+counter]
.text:0000103C CMP R3, #12 ; \ if len(user_input) > 12
.text:0000103E BGT loc_105E ; / goto badpassword
- or if the probed character is not the one expected:
.text:0000105A CMP R2, R3 ; compare each byte of user_input
.text:0000105A ; with expected XOR value
.text:0000105C BEQ loc_1078
XOR
The most interesting part is obviously the XOR operation performed between the bytes of the following arrays: buf_2810 and buf_2840, as depicted below:
.text:00001040 LDR R2, [SP,#0x140+counter]
.text:00001042 ADD R3, SP, #0x140+buf_2810
.text:00001044 LSLS R2, R2, #2 ; shiftleft 2 equivalent to *4
.text:00001046 LDR R2, [R2,R3]
.text:00001048 LDR R1, [SP,#0x140+counter]
.text:0000104A ADD R3, SP, #0x140+buf_2840
.text:0000104C LSLS R1, R1, #2
.text:0000104E LDR R3, [R1,R3]
.text:00001050 EORS R2, R3 ; R2 = buf_2810[index] ^ buf_2840[index]
.text:00001052 LDR R3, [SP,#0x140+counter]
.text:00001054 LDR R1, [SP,#0x140+utf_user_input] ; R1 = user_input
.text:00001056 ADDS R3, R1, R3
.text:00001058 LDRB R3, [R3]
.text:0000105A CMP R2, R3 ; compare each byte of user_input
.text:0000105A ; with expected XOR value
.text:0000105C BEQ loc_1078
The resulting XOR is saved to R2 at 0x1050 and compared to the probed character of the user input (R3) at offset 0x105A. If it doesn't match, the loop exits as explained previously.
The second block is not interesting. It is just incremented the counter and performing calculations that are not used.
Final tests
These blocks of code implement an anti-debugging technique based on 2 checkpoints based on the time, as described here:
If the elapsed time is too important, the application is likely to be debugged and the code will jump to the "bad password" branch.
Solution
As explained previously, the code is performing a rolling XOR of bytes contained in arrays located at memory locations 0x2810 and 0x2840. We can easily script the resulting XOR using the python console integrated in IDA-Pro as follows:
Python>''.join([chr(Dword(0x2810+i*4) ^ Dword(0x2840+i*4)) for i in range(12)]) STEAKT4RT4RE
The flag is STEAKT4RT4RE:
Comments
Keywords: reverse-engineering challenge NDH2K12 apk android arm