All Articles

Analyzing Android Native Library Functions and Decrypting RC4 and AES [WMCTF 2023]

This page has been machine-translated from the original page.

Table of Contents

ezAndroid(Rev)

search patiently

When I launched the APK file provided as the challenge binary in an emulator, it turned out to be an application that validates a username and password as shown below.

image-20230819104025590

I first unpacked the APK with apktool and checked the Manifest, which showed that debugging was enabled.

<?xml version="1.0" encoding="utf-8" standalone="no"?><manifest xmlns:android="http://schemas.android.com/apk/res/android" android:compileSdkVersion="33" android:compileSdkVersionCodename="13" package="com.wmctf.ezandroid" platformBuildVersionCode="33" platformBuildVersionName="13">
    <permission android:name="com.wmctf.ezandroid.DYNAMIC_RECEIVER_NOT_EXPORTED_PERMISSION" android:protectionLevel="signature"/>
    <uses-permission android:name="com.wmctf.ezandroid.DYNAMIC_RECEIVER_NOT_EXPORTED_PERMISSION"/>
    <application android:allowBackup="true" android:appComponentFactory="androidx.core.app.CoreComponentFactory" android:dataExtractionRules="@xml/data_extraction_rules" android:debuggable="true" android:fullBackupContent="@xml/backup_rules" android:icon="@mipmap/ic_launcher" android:label="@string/app_name" android:supportsRtl="true" android:theme="@style/Theme.EzAndroid">
        <activity android:exported="true" android:name="com.wmctf.ezandroid.MainActivity">
            <intent-filter>
                <action android:name="android.intent.action.MAIN"/>
                <category android:name="android.intent.category.LAUNCHER"/>
            </intent-filter>
        </activity>
        <provider android:authorities="com.wmctf.ezandroid.androidx-startup" android:exported="false" android:name="androidx.startup.InitializationProvider">
            <meta-data android:name="androidx.emoji2.text.EmojiCompatInitializer" android:value="androidx.startup"/>
            <meta-data android:name="androidx.lifecycle.ProcessLifecycleInitializer" android:value="androidx.startup"/>
        </provider>
    </application>
</manifest>

Next, I decompiled the smali file for com.wmctf.ezandroid.MainActivity into Java source using smali2java.

public class com.wmctf.ezandroid.MainActivity extends androidx.appcompat.app.AppCompatActivity {
	 /* .source "MainActivity.java" */
	 /* # static fields */
	 public static java.lang.String x;
	 /* # instance fields */
	 android.widget.Button checkoutButton;
	 android.widget.Button exitButton;
	 android.widget.EditText passwordInput;
	 android.widget.EditText usernameInput;
	 /* # direct methods */
	 static com.wmctf.ezandroid.MainActivity ( ) {
		 /* .locals 1 */
		 /* .line 22 */
		 final String v0 = "ezandroid"; // const-string v0, "ezandroid"
		 java.lang.System .loadLibrary ( v0 );
		 /* .line 23 */
		 return;
	 } // .end method
	 public com.wmctf.ezandroid.MainActivity ( ) {
		 /* .locals 0 */
		 /* .line 15 */
		 /* invoke-direct {p0}, Landroidx/appcompat/app/AppCompatActivity;-><init>()V */
		 return;
	 } // .end method
	 static void lambda$messageBox$0 ( android.content.DialogInterface p0, Integer p1 ) { //synthethic
		 /* .locals 0 */
		 /* .param p0, "dialogInterface" # Landroid/content/DialogInterface; */
		 /* .param p1, "i" # I */
		 /* .line 28 */
		 return;
	 } // .end method
	 /* # virtual methods */
	 public void CheckOutClick ( android.view.View p0 ) {
		 /* .locals 6 */
		 /* .param p1, "view" # Landroid/view/View; */
		 /* .line 32 */
		 v0 = this.usernameInput;
		 (( android.widget.EditText ) v0 ).getText ( ); // invoke-virtual {v0}, Landroid/widget/EditText;->getText()Landroid/text/Editable;
		 (( java.lang.Object ) v0 ).toString ( ); // invoke-virtual {v0}, Ljava/lang/Object;->toString()Ljava/lang/String;
		 /* .line 33 */
		 /* .local v0, "username":Ljava/lang/String; */
		 v1 = this.passwordInput;
		 (( android.widget.EditText ) v1 ).getText ( ); // invoke-virtual {v1}, Landroid/widget/EditText;->getText()Landroid/text/Editable;
		 (( java.lang.Object ) v1 ).toString ( ); // invoke-virtual {v1}, Ljava/lang/Object;->toString()Ljava/lang/String;
		 /* .line 34 */
		 /* .local v1, "password":Ljava/lang/String; */
		 final String v2 = ""; // const-string v2, ""
		 v3 = 		 (( java.lang.String ) v0 ).equals ( v2 ); // invoke-virtual {v0, v2}, Ljava/lang/String;->equals(Ljava/lang/Object;)Z
		 /* if-nez v3, :cond_3 */
		 v2 = 		 (( java.lang.String ) v1 ).equals ( v2 ); // invoke-virtual {v1, v2}, Ljava/lang/String;->equals(Ljava/lang/Object;)Z
		 if ( v2 != null) { // if-eqz v2, :cond_0
			 /* .line 37 */
		 } // :cond_0
		 v2 = 		 (( com.wmctf.ezandroid.MainActivity ) p0 ).CheckUsername ( v0 ); // invoke-virtual {p0, v0}, Lcom/wmctf/ezandroid/MainActivity;->CheckUsername(Ljava/lang/String;)I
		 final String v3 = "failed login"; // const-string v3, "failed login"
		 int v4 = 1; // const/4 v4, 0x1
		 /* if-eq v2, v4, :cond_1 */
		 /* .line 38 */
		 (( com.wmctf.ezandroid.MainActivity ) p0 ).messageBox ( v3 ); // invoke-virtual {p0, v3}, Lcom/wmctf/ezandroid/MainActivity;->messageBox(Ljava/lang/String;)V
		 /* .line 40 */
	 } // :cond_1
	 /* new-instance v2, Ljava/lang/StringBuilder; */
	 /* invoke-direct {v2}, Ljava/lang/StringBuilder;-><init>()V */
	 (( java.lang.StringBuilder ) v2 ).append ( v0 ); // invoke-virtual {v2, v0}, Ljava/lang/StringBuilder;->append(Ljava/lang/String;)Ljava/lang/StringBuilder;
	 final String v5 = "123456"; // const-string v5, "123456"
	 (( java.lang.StringBuilder ) v2 ).append ( v5 ); // invoke-virtual {v2, v5}, Ljava/lang/StringBuilder;->append(Ljava/lang/String;)Ljava/lang/StringBuilder;
	 (( java.lang.StringBuilder ) v2 ).toString ( ); // invoke-virtual {v2}, Ljava/lang/StringBuilder;->toString()Ljava/lang/String;
	 /* .line 41 */
	 v2 = 	 (( com.wmctf.ezandroid.MainActivity ) p0 ).check2 ( v1 ); // invoke-virtual {p0, v1}, Lcom/wmctf/ezandroid/MainActivity;->check2(Ljava/lang/String;)I
	 /* if-eq v2, v4, :cond_2 */
	 /* .line 42 */
	 (( com.wmctf.ezandroid.MainActivity ) p0 ).messageBox ( v3 ); // invoke-virtual {p0, v3}, Lcom/wmctf/ezandroid/MainActivity;->messageBox(Ljava/lang/String;)V
	 /* .line 44 */
} // :cond_2
/* new-instance v2, Ljava/lang/StringBuilder; */
/* invoke-direct {v2}, Ljava/lang/StringBuilder;-><init>()V */
final String v3 = "WMCTF{"; // const-string v3, "WMCTF{"
(( java.lang.StringBuilder ) v2 ).append ( v3 ); // invoke-virtual {v2, v3}, Ljava/lang/StringBuilder;->append(Ljava/lang/String;)Ljava/lang/StringBuilder;
(( java.lang.StringBuilder ) v2 ).append ( v0 ); // invoke-virtual {v2, v0}, Ljava/lang/StringBuilder;->append(Ljava/lang/String;)Ljava/lang/StringBuilder;
(( java.lang.StringBuilder ) v2 ).append ( v1 ); // invoke-virtual {v2, v1}, Ljava/lang/StringBuilder;->append(Ljava/lang/String;)Ljava/lang/StringBuilder;
/* const-string/jumbo v3, "}" */
(( java.lang.StringBuilder ) v2 ).append ( v3 ); // invoke-virtual {v2, v3}, Ljava/lang/StringBuilder;->append(Ljava/lang/String;)Ljava/lang/StringBuilder;
(( java.lang.StringBuilder ) v2 ).toString ( ); // invoke-virtual {v2}, Ljava/lang/StringBuilder;->toString()Ljava/lang/String;
/* .line 45 */
/* .local v2, "flag":Ljava/lang/String; */
(( com.wmctf.ezandroid.MainActivity ) p0 ).messageBox ( v2 ); // invoke-virtual {p0, v2}, Lcom/wmctf/ezandroid/MainActivity;->messageBox(Ljava/lang/String;)V
/* .line 35 */
} // .end local v2 # "flag":Ljava/lang/String;
} // :cond_3
} // :goto_0
/* const-string/jumbo v2, "username or password is empty!" */
(( com.wmctf.ezandroid.MainActivity ) p0 ).messageBox ( v2 ); // invoke-virtual {p0, v2}, Lcom/wmctf/ezandroid/MainActivity;->messageBox(Ljava/lang/String;)V
/* .line 50 */
} // :goto_1
return;
} // .end method
public native Integer CheckUsername ( java.lang.String p0 ) {
} // .end method
public native Integer check2 ( java.lang.String p0 ) {
} // .end method
public void messageBox ( java.lang.String p0 ) {
/* .locals 3 */
/* .param p1, "title" # Ljava/lang/String; */
/* .line 26 */
/* new-instance v0, Landroid/app/AlertDialog$Builder; */
/* invoke-direct {v0, p0}, Landroid/app/AlertDialog$Builder;-><init>(Landroid/content/Context;)V */
/* .line 27 */
/* .local v0, "dialog":Landroid/app/AlertDialog$Builder; */
(( android.app.AlertDialog$Builder ) v0 ).setMessage ( p1 ); // invoke-virtual {v0, p1}, Landroid/app/AlertDialog$Builder;->setMessage(Ljava/lang/CharSequence;)Landroid/app/AlertDialog$Builder;
/* .line 28 */
/* new-instance v1, Lcom/wmctf/ezandroid/MainActivity$$ExternalSyntheticLambda0; */
/* invoke-direct {v1}, Lcom/wmctf/ezandroid/MainActivity$$ExternalSyntheticLambda0;-><init>()V */
final String v2 = "OK"; // const-string v2, "OK"
(( android.app.AlertDialog$Builder ) v0 ).setPositiveButton ( v2, v1 ); // invoke-virtual {v0, v2, v1}, Landroid/app/AlertDialog$Builder;->setPositiveButton(Ljava/lang/CharSequence;Landroid/content/DialogInterface$OnClickListener;)Landroid/app/AlertDialog$Builder;
/* .line 29 */
(( android.app.AlertDialog$Builder ) v0 ).show ( ); // invoke-virtual {v0}, Landroid/app/AlertDialog$Builder;->show()Landroid/app/AlertDialog;
/* .line 30 */
return;
} // .end method
protected void onCreate ( android.os.Bundle p0 ) {
/* .locals 2 */
/* .param p1, "savedInstanceState" # Landroid/os/Bundle; */
/* .line 54 */
/* invoke-super {p0, p1}, Landroidx/appcompat/app/AppCompatActivity;->onCreate(Landroid/os/Bundle;)V */
/* .line 55 */
/* const v0, 0x7f0b001c */
(( com.wmctf.ezandroid.MainActivity ) p0 ).setContentView ( v0 ); // invoke-virtual {p0, v0}, Lcom/wmctf/ezandroid/MainActivity;->setContentView(I)V
/* .line 56 */
/* const v0, 0x7f0801de */
(( com.wmctf.ezandroid.MainActivity ) p0 ).findViewById ( v0 ); // invoke-virtual {p0, v0}, Lcom/wmctf/ezandroid/MainActivity;->findViewById(I)Landroid/view/View;
/* check-cast v0, Landroid/widget/EditText; */
this.usernameInput = v0;
/* .line 57 */
/* const v0, 0x7f08014a */
(( com.wmctf.ezandroid.MainActivity ) p0 ).findViewById ( v0 ); // invoke-virtual {p0, v0}, Lcom/wmctf/ezandroid/MainActivity;->findViewById(I)Landroid/view/View;
/* check-cast v0, Landroid/widget/EditText; */
this.passwordInput = v0;
/* .line 58 */
/* const v0, 0x7f08006f */
(( com.wmctf.ezandroid.MainActivity ) p0 ).findViewById ( v0 ); // invoke-virtual {p0, v0}, Lcom/wmctf/ezandroid/MainActivity;->findViewById(I)Landroid/view/View;
/* check-cast v0, Landroid/widget/Button; */
this.checkoutButton = v0;
/* .line 59 */
/* const v0, 0x7f0800b3 */
(( com.wmctf.ezandroid.MainActivity ) p0 ).findViewById ( v0 ); // invoke-virtual {p0, v0}, Lcom/wmctf/ezandroid/MainActivity;->findViewById(I)Landroid/view/View;
/* check-cast v0, Landroid/widget/Button; */
this.exitButton = v0;
/* .line 60 */
v0 = this.checkoutButton;
/* new-instance v1, Lcom/wmctf/ezandroid/MainActivity$1; */
/* invoke-direct {v1, p0}, Lcom/wmctf/ezandroid/MainActivity$1;-><init>(Lcom/wmctf/ezandroid/MainActivity;)V */
(( android.widget.Button ) v0 ).setOnClickListener ( v1 ); // invoke-virtual {v0, v1}, Landroid/widget/Button;->setOnClickListener(Landroid/view/View$OnClickListener;)V
/* .line 66 */
v0 = this.exitButton;
/* new-instance v1, Lcom/wmctf/ezandroid/MainActivity$2; */
/* invoke-direct {v1, p0}, Lcom/wmctf/ezandroid/MainActivity$2;-><init>(Lcom/wmctf/ezandroid/MainActivity;)V */
(( android.widget.Button ) v0 ).setOnClickListener ( v1 ); // invoke-virtual {v0, v1}, Landroid/widget/Button;->setOnClickListener(Landroid/view/View$OnClickListener;)V
/* .line 72 */
return;
} // .end method

First, notice that the native library functions CheckUsername and check2 are declared.

When you click the app’s check button, the entered username is passed to CheckUsername and the password to check2, so we can see that the validation happens there.

Using Ghidra and Android Studio, I managed to identify where the checks happen, but unfortunately I could not fully trace the processing inside the native library functions, so I gave up at that point.

From here on, I continue solving the challenge while referring to the official writeup.

Reference: wm-team/WMCTF-2023

Identifying the offsets of native library functions

To analyze the binary, I first needed to identify the offsets of the CheckUsername and check2 functions inside the native library.

In this binary, the functions were not registered in the export table of the native library.

So when I investigated it on my own, I set breakpoints on several likely-looking functions I found in Ghidra and used the Android Studio debugger to identify the correct ones.

According to the writeup, native libraries like this, whose functions are not registered in the export table, are implemented using a dynamic registration(?) mechanism via the JNI_onload function, and hooking the RegisterNatives method with Frida seems to be an effective way to identify function addresses cleanly.

Reference: java - What does the registerNatives() method do? - Stack Overflow

So, while recalling the content of Five techniques I learned so I won’t lose to Android app analysis challenges, I loaded the challenge APK into Android Studio, launched the emulator, and prepared a Frida hook.

First, run the following commands in order to start the Frida server on the emulator.

adb root
adb.exe install ezAndroid.apk
ren frida-server-16.1.3-android-x86 frida-server
adb push frida-server /data/local/tmp
adb shell chmod +x /data/local/tmp/frida-server
adb shell /data/local/tmp/frida-server

Next, save the following JavaScript code as payload.js to hook RegisterNatives.

function hook_RegisterNatives() {
    var symbols = Process.getModuleByName('libart.so').enumerateSymbols();
    var RegisterNatives_addr = null;
    for (let i = 0; i < symbols.length; i++) {
        var symbol = symbols[i];
        if (symbol.name.indexOf("RegisterNatives") != -1 && symbol.name.indexOf("CheckJNI") == -1) {
            RegisterNatives_addr = symbol.address;
        }
    }
    console.log("RegisterNatives_addr: ", RegisterNatives_addr);
    Interceptor.attach(RegisterNatives_addr, {
        onEnter: function (args) {
            var env = Java.vm.tryGetEnv();
            var className = env.getClassName(args[1]);
            var methodCount = args[3].toInt32();
            for (let i = 0; i < methodCount; i++) {
                var methodName = args[2].add(Process.pointerSize * 3 * i).add(Process.pointerSize * 0).readPointer().readCString();
                var signature = args[2].add(Process.pointerSize * 3 * i).add(Process.pointerSize * 1).readPointer().readCString();
                var fnPtr =
                    args[2].add(Process.pointerSize * 3 * i).add(Process.pointerSize * 2).readPointer();
                var module = Process.findModuleByAddress(fnPtr);
                console.log(className, methodName, signature, fnPtr, module.name, fnPtr.sub(module.base));
            }

        }, onLeave: function (retval) {
        }
    })
}

hook_RegisterNatives();

When I tried the Frida hook with the payload.js above, I found that the offset of CheckUsername was 0x28c0 and that of check2 was 0x2d20.

frida.exe -U -l payload.js -f com.wmctf.ezandroid
>
com.wmctf.ezandroid.MainActivity CheckUsername (Ljava/lang/String;)I 0xbc3ed8c0 libezandroid.so 0x28c0
com.wmctf.ezandroid.MainActivity check2 (Ljava/lang/String;)I 0xbc3edd20 libezandroid.so 0x2d20

image-20230823230030015

However, for some reason, the Frida process exits.

This appears to be caused by anti-debugging via Frida detection.

So I decided to switch for the time being to a static-analysis approach for these functions.

Analyzing CheckUserName

I started by analyzing the CheckUserName function.

IDA’s decompiled output was much easier to read than Ghidra’s, so I include it below.

int __cdecl sub_28C0(int a1, int a2, int a3)
{
  int v3; // eax
  int v4; // edx
  int v5; // eax
  int v6; // eax
  int v7; // edx
  int i; // [esp+44h] [ebp-68h]
  int v10; // [esp+48h] [ebp-64h]
  int v11; // [esp+4Ch] [ebp-60h]
  int v12; // [esp+50h] [ebp-5Ch]
  BOOL v13; // [esp+54h] [ebp-58h]
  _DWORD v14[2]; // [esp+8Fh] [ebp-1Dh] BYREF
  char v15; // [esp+97h] [ebp-15h]
  int v16; // [esp+98h] [ebp-14h]

  v12 = sub_2CA0(a1, a3, 0);
  v3 = strlen(v12);
  v11 = malloc(v3);
  v10 = strlen(v12);
  for ( i = -1175022871; ; i = v4 )
  {
    while ( 1 )
    {
      while ( i >= 196966936 )
      {
        v13 = i >= 712737466 && i < 1618600811;
        i = -449433691;
      }
      if ( i >= -1175022871 )
        break;
      sub_4BC0(&unk_9228, (char *)&unk_6650 + 18);
      v14[0] = *(&off_8FBC + 155);
      v14[1] = *(&off_8FBC + 156);
      v15 = *((_BYTE *)&off_8FBC + 628);
      v5 = strlen(v14);
      sub_5810(v12, v11, v10, v14, v5);
      v6 = memcmp(v11, &byte_9138, 10);
      v7 = 1618600811;
      if ( !v6 )
        v7 = 712737466;
      i = v7;
    }
    if ( i >= -449433691 )
      break;
    v4 = -1711309751;
    if ( v10 != 10 )
      v4 = 196966936;
  }
  if ( (*(&off_8FBC - 1))->d_tag == v16 )
    return v13;
  else
    return sub_2CA0(a1, a2, a3);
}

Looking from the top, the following lines show that some kind of length is stored in v10.

When I checked it in the Android Studio debugger, v12 was the username string I had entered, and v10 was its length.

v12 = sub_2CA0(a1, a3, 0);
v3 = strlen(v12);
v11 = malloc(v3);
v10 = strlen(v12);

So I renamed v12 and v10 accordingly.

Next, let us look at the processing inside the for and while loops.

First, since the initial value of i is -1175022871, we know the first loop will definitely reach the following block first.

v4 stores the next value of i within the for loop.

v4 = -1711309751;
if ( user_name_len != 10 ) v4 = 196966936;

If v4 remains -1711309751, execution cannot proceed any further, so the username must be 10 characters long.

When a 10-character username is entered, the following processing is called next.

sub_4BC0(&unk_9228, (char *)&unk_6650 + 18);
v14[0] = *(&off_8FBC + 155);
v14[1] = *(&off_8FBC + 156);
v15 = *((_BYTE *)&off_8FBC + 628);
v5 = strlen(v14);
sub_5810(user_name, user_name_heap_ptr, user_name_len, v14, v5);
v6 = memcmp(user_name_heap_ptr, &byte_9138, 10);
v7 = 1618600811;
if ( !v6 ) v7 = 712737466;
i = v7;

Jumping straight to the conclusion, it seems that the username check passes when the 10-byte sequence stored at the address user_name_heap_ptr matches the hard-coded &byte_9138.

So I looked at the sub_5810 function, which appears to fill this memory region with some value.

Below is the result of decompiling sub_5810 with IDA.

Elf32_Dyn **__cdecl sub_5810(int a1, int a2, int a3, int a4, int a5)
{
  int v5; // edx
  int v6; // edx
  int v7; // esi
  Elf32_Dyn **result; // eax
  int i; // [esp+1Ch] [ebp-230h]
  int v10; // [esp+20h] [ebp-22Ch]
  int v11; // [esp+24h] [ebp-228h]
  int v12; // [esp+30h] [ebp-21Ch]
  _BYTE v13[512]; // [esp+38h] [ebp-214h] BYREF
  int v14; // [esp+238h] [ebp-14h]

  v11 = 0;
  v10 = 0;
  v12 = 0;
  for ( i = 1757399473; ; i = -901255010 )
  {
    while ( 1 )
    {
      while ( 1 )
      {
        while ( i >= -368149380 )
        {
          if ( i < 1654659448 )
          {
            if ( i < 703941909 )
            {
              v7 = -417153812;
              if ( v10 < a3 )
                v7 = 703941909;
              i = v7;
            }
            else if ( i >= 836230828 )
            {
              v6 = -1661826464;
              if ( v12 < 256 )
                v6 = -1347250435;
              i = v6;
            }
            else
            {
              v12 = (v12 + 1) % 256;
              v11 = ((unsigned __int8)v13[v12 + 256] + v11) % 256;
              sub_57D0(&v13[v12 + 256], &v13[v11 + 256]);
              *(_BYTE *)(a2 + v10) = v10 ^ v13[((unsigned __int8)v13[v11 + 256] + (unsigned __int8)v13[v12 + 256]) % 256
                                             + 256] ^ *(_BYTE *)(a1 + v10);
              i = -1903061274;
            }
          }
          else if ( i < 1718231927 )
          {
            ++v12;
            i = 836230828;
          }
          else if ( i >= 1757399473 )
          {
            v5 = 1718231927;
            if ( v12 < 256 )
              v5 = -567528547;
            i = v5;
          }
          else
          {
            v12 = 0;
            i = 836230828;
          }
        }
        if ( i >= -901255010 )
          break;
        if ( i < -1661826464 )
        {
          ++v10;
          i = -368149380;
        }
        else if ( i < -1347250435 )
        {
          v11 = 0;
          v12 = 0;
          v10 = 0;
          i = -368149380;
        }
        else
        {
          v11 = ((unsigned __int8)v13[v12] + (unsigned __int8)v13[v12 + 256] + v11) % 256;
          sub_57D0(&v13[v12 + 256], &v13[v11 + 256]);
          i = 1654659448;
        }
      }
      if ( i >= -567528547 )
        break;
      ++v12;
      i = 1757399473;
    }
    if ( i >= -417153812 )
      break;
    v13[v12 + 256] = v12;
    v13[v12] = *(_BYTE *)(a4 + v12 % a5);
  }
  result = &off_8FBC;
  if ( _stack_chk_guard != v14 )
    return (Elf32_Dyn **)sub_5CB0();
  return result;
}

I got stuck here when I was actually working on the challenge, but apparently this function is implementing RC4 encryption.

Sure, if someone says the XOR behavior looks like RC4, I can kind of see it. But honestly, at my current skill level, recognizing this function as RC4 would have been quite difficult.

So I looked for a way to solve it even without recognizing it as RC4.

When I fed the function above into BingAI, it did not recognize it as RC4 either, but it came surprisingly close and said it was a function that performs encryption using a key generated by a pseudo-random generator.

If it is encryption using a key generated by a pseudo-random generator, then it may be a symmetric cipher that uses the same key and logic for both encryption and decryption.

In other words, if I feed the hard-coded ciphertext &byte_9138 into the mysterious sub_5810 instead of the input string, the output may become the plaintext of the correct input value.

Being able to infer whether an encryption routine is a symmetric cipher seems like a useful reversing technique.

I still could not figure out how to bypass Frida detection even after reading the writeup, so I simply used lldb on the Android emulator.

It is a bit annoying that the image base changes every time the app starts, but this approach did not require any troublesome bypasses.

image list
>
[272] 4E68710A-7885-E1FA-9BEB-555E9FB7D0ED-B7357321 0xc2299000 C:\Users\kash1064\.lldb\module_cache\remote-android\.cache\4E68710A-7885-E1FA-9BEB-555E9FB7D0ED-B7357321\libezandroid.so 

# 0xc2299000 + 0x2bcf
b *0xc229bbcf

# 0xc2299000+0x9138 = bss_start
memory read --size 8 --format x --count 1 0xc2299000+0x9138

# esp = 0xffb357a0
memory read --size 4 --format x --count 4 esp
memory write 0xbd1d7b70 -s 8 0xc1bdeb7ee66497e9
memory write 0xbd1d7b70+0x8 -s 2 0x43ab

With the commands above, I set a breakpoint at the call site of sub_5810, then extracted all four arguments from the stack.

The first argument contains the input value, so I replaced the memory at that address with the value of &byte_9138.

Finally, after stepping to the next line and checking the address where the post-encryption byte sequence was stored, I was able to obtain the plaintext of the correct username.

image-20230824215627455

With this, I determined that the username is Re_1s_eaSy.

Analyzing check2

Now that I had identified the username, I next looked at check2, the function that verifies the password.

Again, IDA’s decompiled output was easier to read, so I include it below.

int __cdecl sub_2D20(int a1, int a2, int a3)
{
  int v3; // eax
  int v4; // eax
  int v5; // edx
  int v6; // ebx
  int v8; // [esp+24h] [ebp-68h]
  int i; // [esp+40h] [ebp-4Ch]
  __m128i *v10; // [esp+44h] [ebp-48h]
  int v11; // [esp+48h] [ebp-44h]
  int v12; // [esp+58h] [ebp-34h]
  int v13; // [esp+74h] [ebp-18h]
  int v14; // [esp+78h] [ebp-14h]

  sub_4D40(&unk_9238, (char *)&unk_6650 + 79);
  v8 = sub_30F0(a1, &unk_9238);
  sub_4EC0(&(&off_8FBC)[21] + 1, &off_8FBC - 2612);
  sub_5040(&unk_9268, (char *)&unk_6650 + 195);
  v3 = sub_3140(a1, v8, &unk_9260, &unk_9268);
  v4 = sub_31D0(a1, v8, v3);
  v11 = sub_2CA0(a1, v4, 0);
  v10 = (__m128i *)sub_2CA0(a1, a3, 0);
  v13 = strlen(v10);
  for ( i = 1959410021; ; i = -19509138 )
  {
    while ( 1 )
    {
      while ( i >= 74238448 )
      {
        if ( i < 746085184 )
        {
          v12 = 1;
          i = -19509138;
        }
        else if ( i >= 1959410021 )
        {
          v5 = 746085184;
          if ( v13 != 16 )
            v5 = -23033666;
          i = v5;
        }
        else
        {
          sub_7B0(v10, 16, v11);
          v6 = -1371855156;
          if ( _mm_movemask_epi8(_mm_cmpeq_epi8(byte_9158[0], *v10)) == 0xFFFF )
            v6 = 74238448;
          i = v6;
        }
      }
      if ( i >= -23033666 )
        break;
      v12 = 0;
      i = -19509138;
    }
    if ( i >= -19509138 )
      break;
    v12 = 0;
  }
  if ( (*(&off_8FBC - 1))->d_tag == v14 )
    return v12;
  else
    return sub_30F0(a1, a2);
}

The first lines that stand out are the following.

v10 = (__m128i *)sub_2CA0(a1, a3, 0);
v13 = strlen(v10);

When I set a breakpoint here and inspected memory, v10 contained the password string I had entered, and v13 contained its length.

And just like the username verification, the value of i at the start of the following loop is fixed at 1959410021, so we know the first iteration will always execute the following processing.

else if ( i >= 1959410021 )
{
  v5 = 746085184;
  if ( v13 != 16 ) v5 = -23033666;
  i = v5;
}

In other words, I was able to determine that the correct password length is 16 characters.

The next point to pay attention to is probably the following part.

sub_7B0(password, 16, v11);
v6 = -1371855156;
if ( _mm_movemask_epi8(_mm_cmpeq_epi8(byte_9158[0], *password)) == 0xFFFF ) v6 = 74238448;
i = v6;

_mm_movemask_epi8(_mm_cmpeq_epi8(byte_9158[0], *password) appears to compare the address of the input password string with the hard-coded byte sequence byte_9158.

In other words, just like before, we can infer that this is checking whether the password transformed somehow by sub_7B0 matches the hard-coded value.

To test it, I entered a 16-character string as the password and set a breakpoint at the call site of sub_7B0.

image-20230824225645435

When I inspected the stack at the call site, I found that the third argument, v11, was the correct username with the string 123456 appended.

Since the password string and its length are both involved, maybe it is doing some kind of XOR or similar processing.

I let the sub_7B0 function execute once, and the value in the memory region where the password had been stored changed into what looked like an encrypted byte sequence.

image-20230824225939774

If I can identify an input value that makes this value match the hard-coded byte sequence byte_9158, it looks like I should be able to recover the correct password.

image-20230824230105773

The decompiled result of sub_7B0 is shown below.

Elf32_Dyn **__cdecl sub_7B0(int a1, int a2, int a3)
{
  int v3; // esi
  int v4; // edx
  Elf32_Dyn **result; // eax
  int v6; // [esp+30h] [ebp-7Ch]
  int v7; // [esp+34h] [ebp-78h]
  int v8; // [esp+38h] [ebp-74h]
  int v9; // [esp+50h] [ebp-5Ch] BYREF
  int v10; // [esp+54h] [ebp-58h]
  _DWORD v11[21]; // [esp+58h] [ebp-54h] BYREF

  v10 = -970393338;
  strlen(a3);
  v9 = -1522528529;
  sub_C40(&v9, a3);
  v8 = 0;
  v6 = -1028578145;
  do
  {
    while ( 1 )
    {
      while ( 1 )
      {
        while ( v6 >= -304130802 )
        {
          if ( v6 < 703599849 )
          {
            if ( v6 < 90447858 )
            {
              if ( v6 == -304130802 )
              {
                v4 = v10 - 1292562961;
                if ( v7 < 10 )
                  v4 = v10 + 1060841196;
                v6 = v4;
              }
            }
            else if ( v6 == 90447858 )
            {
              v9 = v10 + 1871195796;
              sub_15A0(&v9, v11);
              v9 = v10 + 41419369;
              sub_1840(&v9, v11);
              v9 = v10 + 475928445;
              sub_1B90(&v9, v11);
              v9 = v10 - 878836549;
              sub_12E0(&v9, v11, v7);
              v6 = v10 + 154390384;
            }
          }
          else if ( v6 < 2032010997 )
          {
            if ( v6 == 703599849 )
            {
              v8 += 16;
              v6 = v10 - 58184807;
            }
          }
          else if ( v6 == 2032010997 )
          {
            v9 = v10 + 1871195796;
            sub_15A0(&v9, v11);
            v9 = v10 + 41419369;
            sub_1840(&v9, v11);
            v9 = v10 - 878836549;
            sub_12E0(&v9, v11, 10);
            v9 = v10 + 1852563524;
            sub_2140(&v9, v11, v8 + a1);
            v6 = v10 + 1673993187;
          }
        }
        if ( v6 >= -582053114 )
          break;
        if ( v6 < -816002954 )
        {
          if ( v6 == -1028578145 )
          {
            v3 = v10 + 388340224;
            if ( v8 < a2 )
              v3 = v10 + 643763145;
            v6 = v3;
          }
        }
        else if ( v6 == -816002954 )
        {
          ++v7;
          v6 = v10 + 666262536;
        }
      }
      if ( v6 < -326630193 )
        break;
      if ( v6 == -326630193 )
      {
        v9 = v10 + 478257682;
        sub_1030(&v9, v8 + a1, v11);
        v9 = v10 - 878836549;
        sub_12E0(&v9, v11, 0);
        v7 = 1;
        v6 = v10 + 666262536;
      }
    }
  }
  while ( v6 != -582053114 );
  result = &off_8FBC;
  if ( _stack_chk_guard != v11[16] )
    return (Elf32_Dyn **)sub_C40(a1, a2);
  return result;
}

As usual, I still had no idea what was going on here at my current skill level.

However, after using ghidra-findcrypt for labeling, I confirmed that data that appears to be an AES S-box is defined as shown below.

image-20230824233754518

However, this S-box seems to get swapped somewhere in the processing, so it cannot be used as-is.

If you dump the S-box memory region after sub_7B0 has been called, you can obtain an S-box that can be used for decryption, as shown below.

(lldb) memory read --size 1 --format x --count 256 0xc1ffc000
>
0xc1ffc000: 0x29 0x40 0x57 0x6e 0x85 0x9c 0xb3 0xca
0xc1ffc008: 0xe1 0xf8 0x0f 0x26 0x3d 0x54 0x6b 0x82
0xc1ffc010: 0x99 0xb0 0xc7 0xde 0xf5 0x0c 0x23 0x3a
0xc1ffc018: 0x51 0x68 0x7f 0x96 0xad 0xc4 0xdb 0xf2
0xc1ffc020: 0x09 0x20 0x37 0x4e 0x65 0x7c 0x93 0xaa
0xc1ffc028: 0xc1 0xd8 0xef 0x06 0x1d 0x34 0x4b 0x62
0xc1ffc030: 0x79 0x90 0xa7 0xbe 0xd5 0xec 0x03 0x1a
0xc1ffc038: 0x31 0x48 0x5f 0x76 0x8d 0xa4 0xbb 0xd2
0xc1ffc040: 0xe9 0x00 0x17 0x2e 0x45 0x5c 0x73 0x8a
0xc1ffc048: 0xa1 0xb8 0xcf 0xe6 0xfd 0x14 0x2b 0x42
0xc1ffc050: 0x59 0x70 0x87 0x9e 0xb5 0xcc 0xe3 0xfa
0xc1ffc058: 0x11 0x28 0x3f 0x56 0x6d 0x84 0x9b 0xb2
0xc1ffc060: 0xc9 0xe0 0xf7 0x0e 0x25 0x3c 0x53 0x6a
0xc1ffc068: 0x81 0x98 0xaf 0xc6 0xdd 0xf4 0x0b 0x22
0xc1ffc070: 0x39 0x50 0x67 0x7e 0x95 0xac 0xc3 0xda
0xc1ffc078: 0xf1 0x08 0x1f 0x36 0x4d 0x64 0x7b 0x92
0xc1ffc080: 0xa9 0xc0 0xd7 0xee 0x05 0x1c 0x33 0x4a
0xc1ffc088: 0x61 0x78 0x8f 0xa6 0xbd 0xd4 0xeb 0x02
0xc1ffc090: 0x19 0x30 0x47 0x5e 0x75 0x8c 0xa3 0xba
0xc1ffc098: 0xd1 0xe8 0xff 0x16 0x2d 0x44 0x5b 0x72
0xc1ffc0a0: 0x89 0xa0 0xb7 0xce 0xe5 0xfc 0x13 0x2a
0xc1ffc0a8: 0x41 0x58 0x6f 0x86 0x9d 0xb4 0xcb 0xe2
0xc1ffc0b0: 0xf9 0x10 0x27 0x3e 0x55 0x6c 0x83 0x9a
0xc1ffc0b8: 0xb1 0xc8 0xdf 0xf6 0x0d 0x24 0x3b 0x52
0xc1ffc0c0: 0x69 0x80 0x97 0xae 0xc5 0xdc 0xf3 0x0a
0xc1ffc0c8: 0x21 0x38 0x4f 0x66 0x7d 0x94 0xab 0xc2
0xc1ffc0d0: 0xd9 0xf0 0x07 0x1e 0x35 0x4c 0x63 0x7a
0xc1ffc0d8: 0x91 0xa8 0xbf 0xd6 0xed 0x04 0x1b 0x32
0xc1ffc0e0: 0x49 0x60 0x77 0x8e 0xa5 0xbc 0xd3 0xea
0xc1ffc0e8: 0x01 0x18 0x2f 0x46 0x5d 0x74 0x8b 0xa2
0xc1ffc0f0: 0xb9 0xd0 0xe7 0xfe 0x15 0x2c 0x43 0x5a
0xc1ffc0f8: 0x71 0x88 0x9f 0xb6 0xcd 0xe4 0xfb 0x12

Next, the following Python script stores each S-box index into an array that uses the high and low nibbles of each S-box value as the index.

new_s_box = [
    0x29, 0x40, 0x57, 0x6E, 0x85, 0x9C, 0xB3, 0xCA, 0xE1, 0xF8,
    0x0F, 0x26, 0x3D, 0x54, 0x6B, 0x82, 0x99, 0xB0, 0xC7, 0xDE,
    0xF5, 0x0C, 0x23, 0x3A, 0x51, 0x68, 0x7F, 0x96, 0xAD, 0xC4,
    0xDB, 0xF2, 0x09, 0x20, 0x37, 0x4E, 0x65, 0x7C, 0x93, 0xAA,
    0xC1, 0xD8, 0xEF, 0x06, 0x1D, 0x34, 0x4B, 0x62, 0x79, 0x90,
    0xA7, 0xBE, 0xD5, 0xEC, 0x03, 0x1A, 0x31, 0x48, 0x5F, 0x76,
    0x8D, 0xA4, 0xBB, 0xD2, 0xE9, 0x00, 0x17, 0x2E, 0x45, 0x5C,
    0x73, 0x8A, 0xA1, 0xB8, 0xCF, 0xE6, 0xFD, 0x14, 0x2B, 0x42,
    0x59, 0x70, 0x87, 0x9E, 0xB5, 0xCC, 0xE3, 0xFA, 0x11, 0x28,
    0x3F, 0x56, 0x6D, 0x84, 0x9B, 0xB2, 0xC9, 0xE0, 0xF7, 0x0E,
    0x25, 0x3C, 0x53, 0x6A, 0x81, 0x98, 0xAF, 0xC6, 0xDD, 0xF4,
    0x0B, 0x22, 0x39, 0x50, 0x67, 0x7E, 0x95, 0xAC, 0xC3, 0xDA,
    0xF1, 0x08, 0x1F, 0x36, 0x4D, 0x64, 0x7B, 0x92, 0xA9, 0xC0,
    0xD7, 0xEE, 0x05, 0x1C, 0x33, 0x4A, 0x61, 0x78, 0x8F, 0xA6,
    0xBD, 0xD4, 0xEB, 0x02, 0x19, 0x30, 0x47, 0x5E, 0x75, 0x8C,
    0xA3, 0xBA, 0xD1, 0xE8, 0xFF, 0x16, 0x2D, 0x44, 0x5B, 0x72,
    0x89, 0xA0, 0xB7, 0xCE, 0xE5, 0xFC, 0x13, 0x2A, 0x41, 0x58,
    0x6F, 0x86, 0x9D, 0xB4, 0xCB, 0xE2, 0xF9, 0x10, 0x27, 0x3E,
    0x55, 0x6C, 0x83, 0x9A, 0xB1, 0xC8, 0xDF, 0xF6, 0x0D, 0x24,
    0x3B, 0x52, 0x69, 0x80, 0x97, 0xAE, 0xC5, 0xDC, 0xF3, 0x0A,
    0x21, 0x38, 0x4F, 0x66, 0x7D, 0x94, 0xAB, 0xC2, 0xD9, 0xF0,
    0x07, 0x1E, 0x35, 0x4C, 0x63, 0x7A, 0x91, 0xA8, 0xBF, 0xD6,
    0xED, 0x04, 0x1B, 0x32, 0x49, 0x60, 0x77, 0x8E, 0xA5, 0xBC,
    0xD3, 0xEA, 0x01, 0x18, 0x2F, 0x46, 0x5D, 0x74, 0x8B, 0xA2,
    0xB9, 0xD0, 0xE7, 0xFE, 0x15, 0x2C, 0x43, 0x5A, 0x71, 0x88,
    0x9F, 0xB6, 0xCD, 0xE4, 0xFB, 0x12
]
new_contrary_sbox = [0] * 256

for i in range(256):
    line = (new_s_box[i] & 0xf0) >> 4
    rol = new_s_box[i] & 0xf
    new_contrary_sbox[(line * 16) + rol] = i

for i in range(len(new_contrary_sbox)):
    if (i % 16 == 0):
        print('\n')
    print("0x%02X"%new_contrary_sbox[i],end=",")

To be honest, I still do not understand at all what the S2 array generated by the above process is used for, but I assume it is a standard AES decryption pattern, so I will study it properly another day.

Using this S-box, the username + 123456 string passed as the key, and the hard-coded encrypted byte sequence, I ran the following solver.

#include <stdio.h>
#include <stdlib.h>
#include <string.h>


static const int S[16][16] = { 
0x29,0x40,0x57,0x6e,0x85,0x9c,0xb3,0xca,
0xe1,0xf8,0x0f,0x26,0x3d,0x54,0x6b,0x82,
0x99,0xb0,0xc7,0xde,0xf5,0x0c,0x23,0x3a,
0x51,0x68,0x7f,0x96,0xad,0xc4,0xdb,0xf2,
0x09,0x20,0x37,0x4e,0x65,0x7c,0x93,0xaa,
0xc1,0xd8,0xef,0x06,0x1d,0x34,0x4b,0x62,
0x79,0x90,0xa7,0xbe,0xd5,0xec,0x03,0x1a,
0x31,0x48,0x5f,0x76,0x8d,0xa4,0xbb,0xd2,
0xe9,0x00,0x17,0x2e,0x45,0x5c,0x73,0x8a,
0xa1,0xb8,0xcf,0xe6,0xfd,0x14,0x2b,0x42,
0x59,0x70,0x87,0x9e,0xb5,0xcc,0xe3,0xfa,
0x11,0x28,0x3f,0x56,0x6d,0x84,0x9b,0xb2,
0xc9,0xe0,0xf7,0x0e,0x25,0x3c,0x53,0x6a,
0x81,0x98,0xaf,0xc6,0xdd,0xf4,0x0b,0x22,
0x39,0x50,0x67,0x7e,0x95,0xac,0xc3,0xda,
0xf1,0x08,0x1f,0x36,0x4d,0x64,0x7b,0x92,
0xa9,0xc0,0xd7,0xee,0x05,0x1c,0x33,0x4a,
0x61,0x78,0x8f,0xa6,0xbd,0xd4,0xeb,0x02,
0x19,0x30,0x47,0x5e,0x75,0x8c,0xa3,0xba,
0xd1,0xe8,0xff,0x16,0x2d,0x44,0x5b,0x72,
0x89,0xa0,0xb7,0xce,0xe5,0xfc,0x13,0x2a,
0x41,0x58,0x6f,0x86,0x9d,0xb4,0xcb,0xe2,
0xf9,0x10,0x27,0x3e,0x55,0x6c,0x83,0x9a,
0xb1,0xc8,0xdf,0xf6,0x0d,0x24,0x3b,0x52,
0x69,0x80,0x97,0xae,0xc5,0xdc,0xf3,0x0a,
0x21,0x38,0x4f,0x66,0x7d,0x94,0xab,0xc2,
0xd9,0xf0,0x07,0x1e,0x35,0x4c,0x63,0x7a,
0x91,0xa8,0xbf,0xd6,0xed,0x04,0x1b,0x32,
0x49,0x60,0x77,0x8e,0xa5,0xbc,0xd3,0xea,
0x01,0x18,0x2f,0x46,0x5d,0x74,0x8b,0xa2,
0xb9,0xd0,0xe7,0xfe,0x15,0x2c,0x43,0x5a,
0x71,0x88,0x9f,0xb6,0xcd,0xe4,0xfb,0x12
};


static const int S2[16][16] = {
0x41,0xE8,0x8F,0x36,0xDD,0x84,0x2B,0xD2,
0x79,0x20,0xC7,0x6E,0x15,0xBC,0x63,0x0A,
0xB1,0x58,0xFF,0xA6,0x4D,0xF4,0x9B,0x42,
0xE9,0x90,0x37,0xDE,0x85,0x2C,0xD3,0x7A,
0x21,0xC8,0x6F,0x16,0xBD,0x64,0x0B,0xB2,
0x59,0x00,0xA7,0x4E,0xF5,0x9C,0x43,0xEA,
0x91,0x38,0xDF,0x86,0x2D,0xD4,0x7B,0x22,
0xC9,0x70,0x17,0xBE,0x65,0x0C,0xB3,0x5A,
0x01,0xA8,0x4F,0xF6,0x9D,0x44,0xEB,0x92,
0x39,0xE0,0x87,0x2E,0xD5,0x7C,0x23,0xCA,
0x71,0x18,0xBF,0x66,0x0D,0xB4,0x5B,0x02,
0xA9,0x50,0xF7,0x9E,0x45,0xEC,0x93,0x3A,
0xE1,0x88,0x2F,0xD6,0x7D,0x24,0xCB,0x72,
0x19,0xC0,0x67,0x0E,0xB5,0x5C,0x03,0xAA,
0x51,0xF8,0x9F,0x46,0xED,0x94,0x3B,0xE2,
0x89,0x30,0xD7,0x7E,0x25,0xCC,0x73,0x1A,
0xC1,0x68,0x0F,0xB6,0x5D,0x04,0xAB,0x52,
0xF9,0xA0,0x47,0xEE,0x95,0x3C,0xE3,0x8A,
0x31,0xD8,0x7F,0x26,0xCD,0x74,0x1B,0xC2,
0x69,0x10,0xB7,0x5E,0x05,0xAC,0x53,0xFA,
0xA1,0x48,0xEF,0x96,0x3D,0xE4,0x8B,0x32,
0xD9,0x80,0x27,0xCE,0x75,0x1C,0xC3,0x6A,
0x11,0xB8,0x5F,0x06,0xAD,0x54,0xFB,0xA2,
0x49,0xF0,0x97,0x3E,0xE5,0x8C,0x33,0xDA,
0x81,0x28,0xCF,0x76,0x1D,0xC4,0x6B,0x12,
0xB9,0x60,0x07,0xAE,0x55,0xFC,0xA3,0x4A,
0xF1,0x98,0x3F,0xE6,0x8D,0x34,0xDB,0x82,
0x29,0xD0,0x77,0x1E,0xC5,0x6C,0x13,0xBA,
0x61,0x08,0xAF,0x56,0xFD,0xA4,0x4B,0xF2,
0x99,0x40,0xE7,0x8E,0x35,0xDC,0x83,0x2A,
0xD1,0x78,0x1F,0xC6,0x6D,0x14,0xBB,0x62,
0x09,0xB0,0x57,0xFE,0xA5,0x4C,0xF3,0x9A
};


static int getLeft4Bit(int num) {
    int left = num & 0x000000f0;
    return left >> 4;
}


static int getRight4Bit(int num) {
    return num & 0x0000000f;
}


static int getNumFromSBox(int index) {
    int row = getLeft4Bit(index);
    int col = getRight4Bit(index);
    return S[row][col];
}


static int getIntFromChar(char c) {
    int result = (int)c;
    return result & 0x000000ff;
}


static void convertToIntArray(char* str, int pa[4][4]) {
    int k = 0;
    int i, j;
    for (i = 0; i < 4; i++)
        for (j = 0; j < 4; j++) {
            pa[j][i] = getIntFromChar(str[k]);
            k++;
        }
}


static int getWordFromStr(char* str) {
    int one, two, three, four;
    one = getIntFromChar(str[0]);
    one = one << 24;
    two = getIntFromChar(str[1]);
    two = two << 16;
    three = getIntFromChar(str[2]);
    three = three << 8;
    four = getIntFromChar(str[3]);
    return one | two | three | four;
}


static void splitIntToArray(int num, int array[4]) {
    int one, two, three;
    one = num >> 24;
    array[0] = one & 0x000000ff;
    two = num >> 16;
    array[1] = two & 0x000000ff;
    three = num >> 8;
    array[2] = three & 0x000000ff;
    array[3] = num & 0x000000ff;
}


static void leftLoop4int(int array[4], int step) {
    int temp[4];
    int i;
    int index;
    for (i = 0; i < 4; i++)
        temp[i] = array[i];

    index = step % 4 == 0 ? 0 : step % 4;
    for (i = 0; i < 4; i++) {
        array[i] = temp[index];
        index++;
        index = index % 4;
    }
}


static int mergeArrayToInt(int array[4]) {
    int one = array[0] << 24;
    int two = array[1] << 16;
    int three = array[2] << 8;
    int four = array[3];
    return one | two | three | four;
}


static const int Rcon[10] = { 0x01000000, 0x02000000,
    0x04000000, 0x08000000,
    0x10000000, 0x20000000,
    0x40000000, 0x80000000,
    0x1b000000, 0x36000000 };


static int T(int num, int round) {
    int numArray[4];
    int i;
    int result;
    splitIntToArray(num, numArray);
    leftLoop4int(numArray, 1);

    for (i = 0; i < 4; i++)
        numArray[i] = getNumFromSBox(numArray[i]);

    result = mergeArrayToInt(numArray);
    return result ^ Rcon[round];
}


static int w[44];


static void extendKey(char* key) {
    int i, j;
    for (i = 0; i < 4; i++)
        w[i] = getWordFromStr(key + i * 4);

    for (i = 4, j = 0; i < 44; i++) {
        if (i % 4 == 0) {
            w[i] = w[i - 4] ^ T(w[i - 1], j);
            j++;
        }
        else {
            w[i] = w[i - 4] ^ w[i - 1];
        }
    }

}


static void addRoundKey(int array[4][4], int round) {
    int warray[4];
    int i, j;
    for (i = 0; i < 4; i++) {

        splitIntToArray(w[round * 4 + i], warray);

        for (j = 0; j < 4; j++) {
            array[j][i] = array[j][i] ^ warray[j];
        }
    }
}


static int GFMul2(int s) {
    int result = s << 1;
    int a7 = result & 0x00000100;

    if (a7 != 0) {
        result = result & 0x000000ff;
        result = result ^ 0x1b;
    }

    return result;
}

static int GFMul3(int s) {
    return GFMul2(s) ^ s;
}

static int GFMul4(int s) {
    return GFMul2(GFMul2(s));
}

static int GFMul8(int s) {
    return GFMul2(GFMul4(s));
}

static int GFMul9(int s) {
    return GFMul8(s) ^ s;
}

static int GFMul11(int s) {
    return GFMul9(s) ^ GFMul2(s);
}

static int GFMul12(int s) {
    return GFMul8(s) ^ GFMul4(s);
}

static int GFMul13(int s) {
    return GFMul12(s) ^ s;
}

static int GFMul14(int s) {
    return GFMul12(s) ^ GFMul2(s);
}


static int GFMul(int n, int s) {
    int result;

    if (n == 1)
        result = s;
    else if (n == 2)
        result = GFMul2(s);
    else if (n == 3)
        result = GFMul3(s);
    else if (n == 0x9)
        result = GFMul9(s);
    else if (n == 0xb)//11
        result = GFMul11(s);
    else if (n == 0xd)//13
        result = GFMul13(s);
    else if (n == 0xe)//14
        result = GFMul14(s);

    return result;
}


static void convertArrayToStr(int array[4][4], char* str) {
    int i, j;
    for (i = 0; i < 4; i++)
        for (j = 0; j < 4; j++)
            *str++ = (char)array[j][i];
}


static int getNumFromS1Box(int index) {
    int row = getLeft4Bit(index);
    int col = getRight4Bit(index);
    return S2[row][col];
}


static void deSubBytes(int array[4][4]) {
    int i, j;
    for (i = 0; i < 4; i++)
        for (j = 0; j < 4; j++)
            array[i][j] = getNumFromS1Box(array[i][j]);
}


static void rightLoop4int(int array[4], int step) {
    int temp[4];
    int i;
    int index;
    for (i = 0; i < 4; i++)
        temp[i] = array[i];

    index = step % 4 == 0 ? 0 : step % 4;
    index = 3 - index;
    for (i = 3; i >= 0; i--) {
        array[i] = temp[index];
        index--;
        index = index == -1 ? 3 : index;
    }
}


static void deShiftRows(int array[4][4]) {
    int rowTwo[4], rowThree[4], rowFour[4];
    int i;
    for (i = 0; i < 4; i++) {
        rowTwo[i] = array[1][i];
        rowThree[i] = array[2][i];
        rowFour[i] = array[3][i];
    }

    rightLoop4int(rowTwo, 1);
    rightLoop4int(rowThree, 2);
    rightLoop4int(rowFour, 3);

    for (i = 0; i < 4; i++) {
        array[1][i] = rowTwo[i];
        array[2][i] = rowThree[i];
        array[3][i] = rowFour[i];
    }
}


static const int deColM[4][4] = { 0xe, 0xb, 0xd, 0x9,
    0x9, 0xe, 0xb, 0xd,
    0xd, 0x9, 0xe, 0xb,
    0xb, 0xd, 0x9, 0xe };


static void deMixColumns(int array[4][4]) {
    int tempArray[4][4];
    int i, j;
    for (i = 0; i < 4; i++)
        for (j = 0; j < 4; j++)
            tempArray[i][j] = array[i][j];

    for (i = 0; i < 4; i++)
        for (j = 0; j < 4; j++) {
            array[i][j] = GFMul(deColM[i][0], tempArray[0][j]) ^ GFMul(deColM[i][1], tempArray[1][j])
                ^ GFMul(deColM[i][2], tempArray[2][j]) ^ GFMul(deColM[i][3], tempArray[3][j]);
        }
}


static void addRoundTowArray(int aArray[4][4], int bArray[4][4]) {
    int i, j;
    for (i = 0; i < 4; i++)
        for (j = 0; j < 4; j++)
            aArray[i][j] = aArray[i][j] ^ bArray[i][j];
}


static void getArrayFrom4W(int i, int array[4][4]) {
    int index, j;
    int colOne[4], colTwo[4], colThree[4], colFour[4];
    index = i * 4;
    splitIntToArray(w[index], colOne);
    splitIntToArray(w[index + 1], colTwo);
    splitIntToArray(w[index + 2], colThree);
    splitIntToArray(w[index + 3], colFour);

    for (j = 0; j < 4; j++) {
        array[j][0] = colOne[j];
        array[j][1] = colTwo[j];
        array[j][2] = colThree[j];
        array[j][3] = colFour[j];
    }

}


void deAes(char* c, int clen, char* key) {

    int cArray[4][4];
    int keylen, k;
    keylen = strlen(key);
    if (clen == 0 || clen % 16 != 0) {
        printf("Clen: %d\n", clen);
        exit(0);
    }

    extendKey(key);

    for (k = 0; k < clen; k += 16) {
        int i;
        int wArray[4][4];

        convertToIntArray(c + k, cArray);
        addRoundKey(cArray, 10);

        for (i = 9; i >= 1; i--) {
            deSubBytes(cArray);
            deShiftRows(cArray);
            deMixColumns(cArray);
            getArrayFrom4W(i, wArray);
            deMixColumns(wArray);

            addRoundTowArray(cArray, wArray);
        }
        deSubBytes(cArray);
        deShiftRows(cArray);
        addRoundKey(cArray, 0);
        convertArrayToStr(cArray, c + k);

    }
}


int main() {
    // 暗号化後のバイト列
    char encodebuffer[] = { 0x2B, 0xC8, 0x20, 0x8B, 0x5C, 0xD, 0xA7, 0x9B, 0x2A, 0x51, 0x3A, 0xD2, 0x71, 0x71, 0xCA, 0x50 };
    
    char* key = (char*)"Re_1s_eaSy123456";

    deAes(encodebuffer, 16, key);
    printf("%s", encodebuffer);
}

By running the above and obtaining the correct password, I was able to recover the correct flag as shown below.

image-20230825002938293

Summary

Is this really classified as an easy problem??

I really need to study crypto too…