Misc Ctf

CTF - Solving Medium

CTF - Solving Medium

In this post I will solve the challenge called Medium, created for the Ekoparty 2022 - Mobile Hacking Space CTF. The challenge can be found in the following repo.

medium.apk

I will devide the blogpost in different sections so you can decide which part to read and maybe jump to a section you are interested in.

  • Recon of application
  • Reverse Engineering application - Java Layer
  • Reverse Engineering application - Native Layer
  • Fixing application with Frida

Recon of application

The first thing I usually do whenever I have a CTF challenge is installing the file in a phone or emulator and run it. In this case whenever I did it I had a crash. Apparently the application was not meant to be run or it was somehow broken and we had to fix it. In order to discard an environment issue I decompiled the application and checked if the application was built with any particular native library that was crashing in my emulator. In order to do that I decompiled the application with unzip and checked the folder /lib in order to see if there was any library and which CPU architecture was it compatible with. I found the following folders:

  • lib/arm64-v8a/libmedium.so
  • lib/armeabi-v7a/libmedium.so
  • lib/x86/libmedium.so
  • lib/x86_64/libmedium.so

So the application could be run almost in any device or emulator. The alternative in this case was probably that the application was broken and maybe the solution must come from a fix on the app or from a reverse engineering approach.

Reverse Engineering application - Java Layer

The next step was opening the application with jadx-gui and checking the content and the logic inside. The first thing to check was the strings.xml file which sometimes have some content or clue about what should be done. I found the following content:

<resources>
    ...
    <string name="the_key">a7rrg10/97BjqVgKu+eU</string>
</resources>

Based on the description it could be the encrypted flag or a key to decrypt the flag. I had to look for another value or understand in the application how the_key could be used. In order to do that I decided check the Java layer in order to find out more information.

In the application we had only five classes created in the package com.mhs.medium:

  • MainActivity: which is the activity launched when the application is started
  • SupportOne: which has one method to encrypt a char array and another to decrypt it. The decryptCipher method is interesting because it gets only one parameter, which matches the the_key content.
  • SupportTwo: which has another method related to encrypt and decrypt which receives only one parameter.
  • SupportThree: which is an interface to the libmedium.so libray I found in the first recon.
  • SupportFour: which has methods that picks some values from an array static value. At that point the class seemed to be useless.

After the first overview of the source code we seemed to have three potential candidates in order to solve the challenge. As the SupportOne and SupportTwo classes had simple methods we could try them by creating a class in java and using the the_key to see if any of them works:

public class Main {

  //Content from SupportOne
    static int a = 17;
    static int b = 20;
  
    static String decryptCipherSupportOne(String str) {
        int i = 0;
        for (int i2 = 0; i2 < 26; i2++) {
            if ((a * i2) % 26 == 1) {
                i = i2;
            }
        }
        String str2 = "";
        for (int i3 = 0; i3 < str.length(); i3++) {
            if (str.charAt(i3) != ' ') {
                str2 = str2 + ((char) (((((str.charAt(i3) + 'A') - b) * i) % 26) + 65));
            } else {
                str2 = str2 + str.charAt(i3);
            }
        }
        return str2;
    }
  
    //Content from SupportTwo
    static String decryptMessageSupportTwo(char[] cArr) {
        char[] cArr2 = new char[cArr.length / 2];
        for (int i = 0; i < cArr.length / 2; i += 2) {
            cArr2[i / 2] = cArr[i];
        }
        return new String(cArr2);
    }    
    
  public static void main(String[] args) {
    
    String the_key = "a7rrg10/97BjqVgKu+eU";
    System.out.println("Return of SupportOne: " + decryptCipherSupportOne(the_key));
    System.out.println("Return of SupportTwo: " + decryptMessageSupportTwo(the_key.toCharArray()));
  }

}

The result of executing the code is:

Return of SupportOne: QMRRYEHKGMFPUXYEIWEA
Return of SupportTwo: arg09

Even when it made sense, the keys didn’t work whenever I used them on the CTFd platform. So I discarded those two classes and went for the option of SupportThree, which had a native library.

Reverse Engineering application - Native Layer

After discarding the two options I followed with the analysis of the SupportThree class. This one required us to reverse engineer the libmedium.so library. If you do not know how to do it I created a small video explaining what is JNI and how to reverse engineer the library with Ghidra: https://www.youtube.com/watch?v=wZC7Pzm3jRA&ab_channel=AgeOfEntropy

We see in the MainActivity the following code that gives us a clue about how to call the methods of the native interface:

protected void onCreate(Bundle bundle) {
    super.onCreate(bundle);
    setContentView(R.layout.activity_main);
    SupportTwo.encryptMessage(SupportOne.encryptMessage("aguante_el_paco".toCharArray()).toCharArray());
    String finalEncrypt = SupportThree.finalEncrypt(getApplicationContext(), Base64.encodeToString("test_it".getBytes(), 0));
    Log.i("ENCRYPT", finalEncrypt);
    Log.i("ENCRYPT", new String(Base64.decode(SupportThree.finalEncrypt(getApplicationContext(), finalEncrypt).getBytes(), 0)));
}

This class calls two methods. The first one is encryptMessage which receives a char array and the second one is finalEncrypt which receives a Base64 encoded String. If you paid attention to the the_key value it seems to be a Base64 encoded String, so without going any further on the decryption I suspect the candidate for the solution is in it.

So in Ghidra I had to check the following method: Java_com_mhs_medium_SupportThree_finalEncrypt (the finalEncrypt method from com.medium.SupportThree class) after importing the jni header in the project and changing the types of the method from the following ones:

undefined8
Java_com_mhs_medium_SupportThree_finalEncrypt
          (long *param_1,undefined8 param_2,_jmethodID *param_3,undefined8 param_4)

to:

jstring Java_com_mhs_medium_SupportThree_finalEncrypt
                  (JNIEnv *env,jclass supprtThreeClass,jobject context,jstring message)

The first two parameters are the ones that any JNI methods receives from Java when a static method is being called, which are a pointer to JNIEnv (which is used to call methods, classes from the JVM environment) and the jclass (which is the representation of the SupportThree class in the scope of the native library). The other parameters, if any, depend on the specification of the method defined. In this case the method receives two parameters:

public static native String finalEncrypt(Context context, String message);

In JNI any particular instance of a class can be defined as a jobject, and in the case of the String class, it can be converted to jstring as we did in this case. The result of the pseudo-code generated in GHidra is the following one:

jstring Java_com_mhs_medium_SupportThree_finalEncrypt
                  (JNIEnv *env,jclass supprtThreeClass,jobject context,jstring message)

{
  int iVar1;
  int iVar2;
  jfieldID p_Var3;
  ulong uVar4;
  jobject p_Var5;
  jmethodID p_Var6;
  jsize jVar7;
  jclass p_Var8;
  jmethodID p_Var9;
  _jmethodID *p_Var10;
  jclass p_Var11;
  jmethodID p_Var12;
  jmethodID p_Var13;
  undefined8 uVar14;
  jstring p_Var15;
  jarray array;
  jarray array_00;
  jsize jVar16;
  char *__dest;
  jbyte *pjVar17;
  char *__dest_00;
  jbyteArray array_01;
  jstring unaff_x23;
  
  do {
    iVar1 = validation1((_JNIEnv *)env);
    if (iVar1 != 1) {
      return unaff_x23;
    }
    p_Var8 = (*(*env)->GetObjectClass)(env,context);
    p_Var9 = (*(*env)->GetMethodID)
                       (env,p_Var8,"getPackageManager","()Landroid/content/pm/PackageManager;");
    p_Var10 = (_jmethodID *)_JNIEnv::CallObjectMethod((_jobject *)env,(_jmethodID *)context,p_Var9);
    p_Var11 = (*(*env)->FindClass)(env,"android/content/pm/PackageManager");
    p_Var9 = (*(*env)->GetMethodID)
                       (env,p_Var11,"getPackageInfo",
                        "(Ljava/lang/String;I)Landroid/content/pm/PackageInfo;");
    iVar1 = validation2((_JNIEnv *)env);
    if (iVar1 == 1) {
      p_Var12 = (*(*env)->GetMethodID)(env,p_Var8,"getPackageName","()Ljava/lang/String;");
      unaff_x23 = (jstring)_JNIEnv::CallObjectMethod((_jobject *)env,(_jmethodID *)context,p_Var12);
    }
    p_Var3 = (*(*env)->GetStaticFieldID)(env,p_Var11,"GET_SIGNATURES","I");
    uVar4 = (*(*env)->GetStaticIntField)(env,p_Var11,p_Var3);
    p_Var5 = (jobject)_JNIEnv::CallObjectMethod
                                ((_jobject *)env,p_Var10,p_Var9,unaff_x23,uVar4 & 0xffffffff);
    p_Var8 = (*(*env)->FindClass)(env,"android/content/pm/PackageInfo");
    p_Var3 = (*(*env)->GetFieldID)(env,p_Var8,"signatures","[Landroid/content/pm/Signature;");
    p_Var5 = (*(*env)->GetObjectField)(env,p_Var5,p_Var3);
    p_Var8 = (*(*env)->FindClass)(env,"java/security/MessageDigest");
    p_Var9 = (*(*env)->GetStaticMethodID)
                       (env,p_Var8,"getInstance","(Ljava/lang/String;)Ljava/security/MessageDigest;"
                       );
    p_Var12 = (*(*env)->GetMethodID)(env,p_Var8,"update","([B)V");
    p_Var6 = (*(*env)->GetMethodID)(env,p_Var8,"digest","()[B");
    jVar7 = (*(*env)->GetArrayLength)(env,p_Var5);
  } while ((int)jVar7 < 1);
  p_Var5 = (*(*env)->GetObjectArrayElement)(env,p_Var5,0);
  p_Var11 = (*(*env)->FindClass)(env,"android/content/pm/Signature");
  p_Var13 = (*(*env)->GetMethodID)(env,p_Var11,"toByteArray","()[B");
  uVar14 = _JNIEnv::CallObjectMethod((_jobject *)env,(_jmethodID *)p_Var5,p_Var13);
  p_Var15 = (*(*env)->NewStringUTF)(env,"SHA");
  p_Var10 = (_jmethodID *)
            _JNIEnv::CallStaticObjectMethod((_jclass *)env,(_jmethodID *)p_Var8,p_Var9,p_Var15);
  _JNIEnv::CallVoidMethod((_jobject *)env,p_Var10,p_Var12,uVar14);
  array = (jarray)_JNIEnv::CallObjectMethod((_jobject *)env,p_Var10,p_Var6);
  p_Var8 = (*(*env)->FindClass)(env,"android/util/Base64");
  p_Var9 = (*(*env)->GetStaticMethodID)(env,p_Var8,"decode","(Ljava/lang/String;I)[B");
  p_Var3 = (*(*env)->GetStaticFieldID)(env,p_Var8,"DEFAULT","I");
  uVar4 = (*(*env)->GetStaticIntField)(env,p_Var8,p_Var3);
  array_00 = (jarray)_JNIEnv::CallStaticObjectMethod
                               ((_jclass *)env,(_jmethodID *)p_Var8,p_Var9,message,
                                uVar4 & 0xffffffff);
  jVar7 = (*(*env)->GetArrayLength)(env,array_00);
  jVar16 = (*(*env)->GetArrayLength)(env,array_00);
  iVar1 = (int)jVar16;
  __dest = (char *)malloc((long)(iVar1 + 1));
  if ((__dest != (char *)0x0) &&
     (pjVar17 = (*(*env)->GetByteArrayElements)(env,array_00,(jboolean *)0x0),
     pjVar17 != (jbyte *)0x0)) {
    memcpy(__dest,pjVar17,(long)iVar1);
    __dest[iVar1] = '\0';
  }
  jVar16 = (*(*env)->GetArrayLength)(env,array);
  iVar1 = (int)jVar16;
  __dest_00 = (char *)malloc((long)(iVar1 + 1));
  if ((__dest_00 != (char *)0x0) &&
     (pjVar17 = (*(*env)->GetByteArrayElements)(env,array,(jboolean *)0x0), pjVar17 != (jbyte *)0x0)
     ) {
    memcpy(__dest_00,pjVar17,(long)iVar1);
    __dest_00[iVar1] = '\0';
  }
  iVar1 = isHidden;
  iVar2 = validation3((_JNIEnv *)env);
  pjVar17 = (jbyte *)encryptDecrypt(__dest,__dest_00,iVar2 + iVar1);
  array_01 = (*(*env)->NewByteArray)(env,(jsize)jVar7);
  (*(*env)->SetByteArrayRegion)(env,array_01,0,(jsize)jVar7,pjVar17);
  p_Var8 = (*(*env)->FindClass)(env,"android/util/Base64");
  p_Var9 = (*(*env)->GetStaticMethodID)(env,p_Var8,"encodeToString","([BI)Ljava/lang/String;");
  p_Var3 = (*(*env)->GetStaticFieldID)(env,p_Var8,"DEFAULT","I");
  uVar4 = (*(*env)->GetStaticIntField)(env,p_Var8,p_Var3);
  p_Var15 = (jstring)_JNIEnv::CallStaticObjectMethod
                               ((_jclass *)env,(_jmethodID *)p_Var8,p_Var9,array_01,
                                uVar4 & 0xffffffff);
  return p_Var15;
}

After the change the method could be a bit simpler to understand. In this case there is a heavy use of the JNIEnv, so in order to facilitate the understanding of the code you can use the following rules:

The way of using the env parameter.

(*(*env)->GetObjectClass)(env,context);

can be written a bit simpler:

env->GetObjectClass(env,context)

Calling a static method

p_Var8 = (*(*env)->GetObjectClass)(env,context);
p_Var9 = (*(*env)->GetMethodID)
                       (env,p_Var8,"getPackageManager","()Landroid/content/pm/PackageManager;");
p_Var10 = (_jmethodID *)_JNIEnv::CallObjectMethod((_jobject *)env,(_jmethodID *)context,p_Var9);

can be written as well as:

p_Var8 = env->GetObjectClass(context);
p_Var9 = env->GetMethodID(p_Var8,"getPackageManager","()Landroid/content/pm/PackageManager;");
p_Var10 = env->CallObjectMethod(context,p_Var9);

which might be confusing but basically it does the following. In the line 1 it gets the class of the jobject (in this case context). Using the class as a parameter it gets a particular method using two parameters: getPackageManager and ()Landroid/content/pm/PackageManager; which is the signature of the method translated to smali. The third line calls the method with the reference to the object, the method retrieved in the second line and the parameters it receives (if any). So In Java the previous method would be something like this:

PackageManager packageManager = context.getPackageManager();

Note: Check the logic of the data types from this post: Datatypes in smali

If we have patience and follow the rules we can get to the following pseudo-code in Java:

jstring Java_com_mhs_medium_SupportThree_finalEncrypt
                  (JNIEnv *env,jclass supprtThreeClass,jobject context,jstring message)
{
  
  do {
    //native validation1
    if (validation1(env) != 1) {
      return unaff_x23;
    }
    PackageManager packageManager = context.getPackageManager();
    //native validation2
    if (validation2(env) == 1) {
      unaff_x23 = context.getPackageName();
    }
    int signatureId = packageManaget.GET_SIGNATURES;
    PackageInfo packageInfo = packageManager.getPackageInfo(unaff_x23,signatureId);

  Signature[] signatures = packageInfo.signatures;
  } while (signatures.length() < 1);
  Signature signature = signatures[0];
  byte[] signatureArray = signature.toByteArray();
  byte[] mdigest = MessageDigest.getInstance("SHA").digest(); 
  
  byte[] decodedParam = Base64.decode(message,Base64.DEFAULT);
  //digestC = lines that converts the digest from jbyteArray to a C array
  //messageC = lines that converts the message from jbyteArray to a C array

  //third method with static value
  outVal3 = validation3(env);
  output = encryptDecrypt(digestC,messageC,outVal3 + isHidden);
  return Base64.encodeToString(output,Base64.DEFAULT);
}

So we see three validations that might be the cause of the crash of the application and a method that decrypts/encrypts the content received in the message parameter with the message digest and a paramter that is the sum of isHidden (a parameter in the lib) plus the output of validation3.

Then I checked the validation1 function and executed the same reverse engineering process that I did with the previous function. I discovered that the function did the following:

  1. var1 = Gets SupportFour.getFirst(0) //47
  2. var2 = Gets SupportFour.getSecond(0) //56
  3. var3 = Gets SupportFour.getThird(0) //87
  4. var4 = Gets SupportFour.getFourth(0) //140
  5. Executes the following arithmetic operations: 5.1- resop1 = (var2 * var1) / var3; (int value kept) 5.2- return var2 * var1 - resop1 * var3 == var4;

So after replacing the variables with the values taken from the array:

5.1 - 56 * 47 / 87 = 30 5.2 - 56 * 47 - 30 * 87 = 2632 - 2610 = 22 != 140

So because the function returns 0 (false in C is represented as 0), the method gets in if statement:

  if (iVar1 != 1) {
    return unaff_x23;
  }

In this case the problem is that unaff_x23 is never instantiated, so whenever Java wants to shape the content to a String (what it was expecting based on the signature of the JNI method), but the variable points to anywhere so the application crashes.

Making simple fixes on the binary is not possible because the method encryptDecrypt uses in some way the signature of the application to decrypt the message. Because of that changing the binary would change the signature as well (because of how the APK signatures work). So we would need to do a lot of modifications to the binary in order to hardcode the signature and then make the changes on the validations.

Another alternative would be to use Frida, inject dynamically the modifications on the methods, which would not ruin the signature at all because the changes are made in the memory of the process. So this seems the easiest path to follow.

Fixing application with Frida

In order to inject effectively the modifications I used the early instrumentation process as the method that crashed was called when the application was started. I executed the following command:

frida -U -f com.mhs.medium

Note that I did not use the no-pause flag in order to execute some scripts before starting the application. In this case I had to find the names of the validation1 to valitadion3 methods. In order to do so I used the following frida script in the frida-cli:

var exports = Process.getModuleByName("libmedium.so").enumerateExports()

for (var i = 0; i < exports.length; i++) {
  if (exports[i].name.includes("validation")) {
    console.log(exports[i].name);
  }
}

With this approach I got an error saying that the library was not loaded. I weas shocked initially, but then I realized that as I was using early instrumentation the library might not be yet loaded in memory (because the class was not loaded yet). So this apporach would not work.

I shifted the strategy to overwrite the method onCreate of the MainActivity to avoid the call to the method that crashes. This can be done with early instrumentation:

Java.perform( function () {
  var MainClass = Java.use("com.mhs.medium.MainActivity");
  var String = Java.use("java.lang.String");
  var SupportThree = Java.use("com.mhs.medium.SupportThree");
  MainClass.onCreate.implementation = function (bundle) {
    this.$super.onCreate(bundle);
  }
});

After the change when I run the script to get the names of the methods I got:

_Z11validation3P7_JNIEnv
_Z11validation2P7_JNIEnv
_Z11validation1P7_JNIEnv

So based on the analysis I executed on the libmedium.so we needed to get the following conditions (I leave the analysis of validation3 to you):

  • validation1 return 1
  • validation2 return 1
  • validation3 return more than 1

Here is the script to patch the native library:

console.log(1);
Interceptor.attach(Module.findExportByName("libmedium.so","_Z11validation1P7_JNIEnv"), {
  onEnter: function (args) {
    console.log("Call validation1");
  },
  onLeave: function (retval) {
    retval.replace(1);
  }
});

console.log(2);
Interceptor.attach(Module.findExportByName("libmedium.so","_Z11validation2P7_JNIEnv"), {
  onEnter: function (args) {
    console.log("Call validation2");
  },
  onLeave: function (retval) {
    retval.replace(1);
  }
});


console.log(3);
Interceptor.attach(Module.findExportByName("libmedium.so","_Z11validation3P7_JNIEnv"), {
  onEnter: function (args) {
    console.log("Call validation3");
  },
  onLeave: function (retval) {
    retval.replace(2);
  }
});

After that, the remaining thing to do is calling the method from SupportThree manually, in order to get the key decrypted:

Java.perform( function () {
  Java.choose("com.mhs.medium.MainActivity", {
            onMatch: function (instance) {
                console.log("Found instance: " + instance);
                console.log("Application context: " + instance.getApplicationContext());
                var String = Java.use("java.lang.String");
        var SupportThree = Java.use("com.mhs.medium.SupportThree");
        console.log(SupportThree.finalEncrypt(instance.getApplicationContext(), String.$new("a7rrg10/97BjqVgKu+eU")));
  
            },
            onComplete: function () { }

        });
})

The method returned the value RElPJCRtdWQ0X21VZGF9 which could be Base64 decoded in order to get the value: DIO$$mud4_mUda}%, which was the right one!!