JNI Reverse Engineering

Reversing Android Native Libraries for Fun and Cats
Panda

TripleSec

About me

  • Panda - @pandasec_
  • R&D Engineer
  • Interested in low-level stuff, reverse engineering and exploitation.

Introduction to JNI

Java Native Interface Overview


  • Native programming interface
  • Mechanism allowing:
    • Java programs to call functions in C or C++ programs
    • C or C++ programs to call methods in Java programs
  • JNI available for Android Runtime

JavaVM and JNIEnv


  • JNI defines two key data structures:
    • JavaVM provides the invocation interface functions, which allow you to create and destroy a JavaVM (Android only allows one though)
    • JNIEnv provides most of the JNI functions (first argument of all native function calls).
  • Essentially, pointers to pointers to function tables

Using Native Libraries


  • Call System.loadLibrary from a static class initializer (undecorated library name, e.g. fubar for libfubar.so)
  • static {
      System.loadLibrary("fubar");
    }
  • Provide a JNI_OnLoad
  • In JNI_OnLoad, register all native methods using RegisterNatives
JNIEXPORT jint JNI_OnLoad(JavaVM* vm, void* reserved) {
  JNIEnv* env;
  if (vm->GetEnv(reinterpret_cast<void**>(&env), JNI_VERSION_1_6) != JNI_OK) {
      return -1;
  }
  // Get jclass with env->FindClass.
  // Register methods with env->RegisterNatives.
  return JNI_VERSION_1_6;
}

Cat Image Enterprise Edition

Our Target


  • Android application providing cat images
  • Can be found on APKPure
  • Made by haxelion
  • Requires a serial...
  • Reverse engineering time!

Extracting Java Source Code


Analyzing Java Source Code


ImageViewer.java

public class ImageViewer extends AppCompatActivity {
    LinearLayout imageList;
    static {
        System.loadLibrary("secure");
    }
    private native byte[] loadSecureResource(String var1);
    @Override
    protected void onCreate(Bundle bitmap) {
        // [...]
        for (int i = 0; i < 5; ++i) {
            bitmap = this.loadSecureResource("cat00" + Integer.toString(i) + ".jpg.enc");
            // [...]
        }
    }
}

Analyzing Java Source Code


Unlock.java - Part 1

public class Unlock extends AppCompatActivity {
    //[...]
    @Override
    protected void onCreate(Bundle bundle) {
        // [...]
        this.checkButton.setOnClickListener(new View.OnClickListener(){
            public void onClick(View object) {
                object = Unlock.this.serialText.getText().toString();
                if (Unlock.this.checkSerial((String)object, Unlock.this.did)) {
                    // [...]
                    Unlock.this.launchImageViewer();
                }
            }
        });
    }
}

Analyzing Java Source Code


Unlock.java - Part 2

static {
    System.loadLibrary("secure");
}

private native boolean checkSerial(String var1, String var2);

Don't Skip ARM Day

Extracting libsecure.so


[panda@bamboo cat_images]$ unzip cat_image_enterprise_edition.apk 
Archive:  cat_image_enterprise_edition.apk
  inflating: AndroidManifest.xml     
  inflating: META-INF/CERT.RSA       
  inflating: META-INF/CERT.SF        
  inflating: META-INF/MANIFEST.MF    
 extracting: assets/cat000.jpg.enc   
 [...]
[panda@bamboo cat_images]$ ls -la lib/armeabi-v7a/
total 16
drwxr-xr-x 2 panda panda    60 Oct 27 20:03 .
drwxr-xr-x 5 panda panda   100 Oct 27 20:03 ..
-rw-r--r-- 1 panda panda 13776 Dec 31  1979 libsecure.so
[panda@bamboo cat_images]$ file lib/armeabi-v7a/libsecure.so 
lib/armeabi-v7a/libsecure.so: ELF 32-bit LSB shared object, ARM [...] stripped

Overview of the checkSerial Function


JNI Call


When checkSerial is called, the registers hold the following values:

  • R0 points to JNIEnv
  • R1 references the Java object inside which this native method has been declared
  • R2 points to the serial passed by the user to the application
  • R3 points to the device ID of the current phone

Using IDA


Actually Using IDA


Finding function names using the JNINativeInterface structure

Where Do We Go From Here ?


  • Statically reverse the binary
  • Modify the APK to be debuggable with gdb
  • Use Frida on a rooted phone
  • Use LIEF and Frida because you're too lazy to root your phone

Frida + LIEF


  • Frida is a dynamic instrumentation toolkit
  • LIEF is a a cross platform library which can parse, modify and abstract ELF, PE and MachO formats.
  • LIEF can be used to add a Frida library as a dependency of one of the native libraries of the application

Changing the Dependencies


  • Current dependencies of libsecure.so
  • [panda@bamboo armeabi-v7a]$ readelf -d ./libsecure.so | grep NEEDED
       0x00000001 (NEEDED)                     Shared library: [libandroid.so]
       0x00000001 (NEEDED)                     Shared library: [liblog.so]
       0x00000001 (NEEDED)                     Shared library: [libm.so]
       0x00000001 (NEEDED)                     Shared library: [libdl.so]
       0x00000001 (NEEDED)                     Shared library: [libc.so]
  • The goal is to add libgadget.so, which is Frida's frida-gadget.so, as a dependency
  • [panda@bamboo armeabi-v7a]$ ls -la
    total 14172
    drwxr-xr-x 2 panda panda       80 Oct 28 00:07 .
    drwxr-xr-x 5 panda panda      100 Oct 27 20:03 ..
    -rw-r--r-- 1 panda panda 14493092 Oct 23 05:45 libgadget.so
    -rw-r--r-- 1 panda panda    13776 Dec 31  1979 libsecure.so

Changing the Dependencies


  • Achieved with 3 lines of code with LIEF
  • [panda@bamboo armeabi-v7a]$ ipython
    Python 3.7.0 (default, Sep 15 2018, 19:13:07) 
    Type 'copyright', 'credits' or 'license' for more information
    IPython 6.5.0 -- An enhanced Interactive Python. Type '?' for help.
    
    In [1]: import lief
       ...: 
       ...: libnative = lief.parse("libsecure.so")
       ...: libnative.add_library("libgadget.so") # Injection!
       ...: libnative.write("libsecure.so")
    
    [panda@bamboo armeabi-v7a]$ readelf -d ./libsecure.so | grep NEEDED
     0x00000001 (NEEDED)                     Shared library: [libgadget.so]   <--- \o/
     0x00000001 (NEEDED)                     Shared library: [libandroid.so]
     0x00000001 (NEEDED)                     Shared library: [liblog.so]
     0x00000001 (NEEDED)                     Shared library: [libm.so]
     0x00000001 (NEEDED)                     Shared library: [libdl.so]
     0x00000001 (NEEDED)                     Shared library: [libc.so]
    

Configuring libgadget.so


  • The configuration file must meet the following requirements:
    • Must be named after the frida-gadget library
      (e.g. libgadget.so -> libgadget.config.so)
    • Must be placed in the same directory as the frida-gadget library
  • The JavaScript payload will be located at /data/local/tmp/myscript.js on the phone
  • {
      "interaction": {
        "type": "script",
        "path": "/data/local/tmp/myscript.js",
        "on_change": "reload"
      }
    }

Recreating an APK


  • Files are zipped in cat_images.apk
  • Generating a keystore to sign the APK
  • keytool -genkey -v -keystore debug.keystore -alias \
      key -storepass android -keypass android -keyalg RSA -validity 14000
  • Signing the APK
  • jarsigner -verbose -sigalg SHA1withRSA -digestalg SHA1 \
      -keystore debug.keystore cat_images.apk key
  • Installing the APK
  • [panda@bamboo tmp]$ adb install ./cat_images.apk
    ./cat_images.apk: 1 file pushed. 6.6 MB/s (11900035 bytes in 1.714s)
      pkg: /data/local/tmp/cat_images.apk
    Success
    

Checking that Everything Works


  • /data/local/tmp/myscript.js
  • 'use strict';
    Java.perform(function () {
      Interceptor.attach(Module.findBaseAddress("libsecure.so").add(0x3131), {
          onEnter: function(args) {
            Log.v("keycat", "+-----------------------------------------------------------------------+");
            Log.v("keycat", "Entering Java_eu_haxelion_CatImageEnterpriseEdition_Unlock_checkSerial");
            Log.v("keycat", "args[0] = " + args[0].toString());
            Log.v("keycat", "args[1] = " + args[1].toString());
            Log.v("keycat", "args[2] = " + Memory.readCString(Memory.readPointer(args[2])));
            Log.v("keycat", "args[2] = " + Memory.readCString(Memory.readPointer(args[3])));
            Log.v("keycat", "+-----------------------------------------------------------------------+");
          }
        }
      )
    });

Checking that Everything Works


  • Displaying the logs
  • adb logcat -s "keycat:V"
    V/keycat  (14878): +-----------------------------------------------------------------------------+
    V/keycat  (14878): Entering Java_eu_haxelion_CatImageEnterpriseEdition_Unlock_checkSerial
    V/keycat  (14878): args[0] = 0xb4871240
    V/keycat  (14878): args[1] = 0xbedc3eec
    V/keycat  (14878): args[2] = �wp
    V/keycat  (14878): args[2] = �wp
    V/keycat  (14878): +-----------------------------------------------------------------------------+
    

Keygen Me

First Steps


  • Same function called twice in green and yellow
  • Takes either the device id or the serial as the first argument, an address on the stack as the second and an integer as the third
  • Hypothesis: R0 = Src, R1 = Dest, R2 = Size ?

Frida to the Rescue


  • Hooking the function and displaying the arguments before and after the call
  • V/keycat  (14975): +-----------------------------------------------------------------------------+
    V/keycat  (14975): Entering sub_10ac
    V/keycat  (14975): args[0] = abcdef0987654321 <---- serial
    V/keycat  (14975): 
    V/keycat  (14975): Leaving sub_10ac
    V/keycat  (14975): args[1] = 0xab, 0xcd, 0xef, 0x9, 0x87, 0x65, 0x43, 0x21, 
    V/keycat  (14975): +-----------------------------------------------------------------------------+
    V/keycat  (14975): +-----------------------------------------------------------------------------+
    V/keycat  (14975): Entering sub_10ac
    V/keycat  (14975): args[0] = b927f9baf693d02f <---- device id
    V/keycat  (14975): 
    V/keycat  (14975): Leaving sub_10ac
    V/keycat  (14975): args[1] = 0xb9, 0x27, 0xf9, 0xba, 0xf6, 0x93, 0xd0, 0x2f, 
    V/keycat  (14975): +-----------------------------------------------------------------------------+
    
  • A simple conversion from a string to a hex value

Going Deeper


  • Orange just releases the JNI strings
  • Red implements the following algorithm:
R1 = 0x42
first_stage = b''
for i in range(8):
  R1 = serial[i] + (device_id[i] ^ R1)
  R1 = R1 & 0xff
  first_stage += bytes([R1])

  • Calculates the value first_stage

Even Further Beyond


  • Pink calls the second_stage_function
  • Arguments:
    • R0 holds the first_stage value
    • R1 holds the address to a key (0x15 bytes long value found in the .data section)
    • R2 holds the value 0x15
  • The resulting value is compared to the string This serial is valid

Even Further Beyond


second_stage_function algorithm
key2 = b'HAXELION'
for i in range(0x15):
  LR = key[i]
  R4 = i % 0x8
  R5 = key2[R4]
  R6 = first_stage[R4]
  key2[R4] = LR
  R6 = (LR + (R5 ^ R6)) & 0xff
  key[i] = R6

You're a Wizard Harry


  • Just a matter of finding the inverse functions
  • Not the subject of this talk
  • I usually reimplement stuff in Python and try to work my way from here
[panda@bamboo jni]$ python keygen.py --id b927f9baf693d02f
[+] Expected first stage value: 8e6f50cce87737bf
[+] Expected serial: 93c6bae2aefc90a7

Demo

Questions ?