From aldeid
Jump to navigation Jump to search


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

The application looks like this:

Ndh2k12-apk-running-0.png Ndh2k12-apk-running-1.png Ndh2k12-apk-running.png

Base code analysis


Let's first decompile the application using apktool:

[email protected]:/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="" package="">
    <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">
                <action android:name="android.intent.action.MAIN"/>
                <category android:name="android.intent.category.LAUNCHER"/>


Now, let's convert the APK to JAR with dex2jar:

$ /data/tools/dex2jar-2.0/ NDH.apk 
dex2jar NDH.apk -> ./NDH-dex2jar.jar

And open the JAR file with JD-GUI. Go to > NDHActivity.

We see that a shared library (*.so) is loaded:


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"
$ 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:

   Button localButton = new Button(this);
   localButton.setOnClickListener(new View.OnClickListener()
     public void onClick(View paramAnonymousView)
       while (true)

Analysis of the shared library

Graph overview

Now, we need to analyze the shared library. Open lib/armeabi/ in IDA-Pro. Here is the graph overview:

Ndh2k12-libverify so-layout.png


Refer to this page to learn how to resolve JNI functions.

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


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.


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)])


The flag is STEAKT4RT4RE:



blog comments powered by Disqus

Keywords: reverse-engineering challenge NDH2K12 apk android arm