All Articles

Android のネイティブライブラリ関数解析と RC4 と AES の復号を行う【WMCTF 2023】

もくじ

ezAndroid(Rev)

search patiently

問題バイナリとして与えられた apk ファイルをエミュレータで起動すると、以下のようなユーザ名とパスワードの検証を行うアプリケーションでした。

image-20230819104025590

とりあえず apktool で apk ファイルを展開し、Manifest を参照したところ、デバッグは有効化されていることがわかりました。

<?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>

続いて、com.wmctf.ezandroid.MainActivity の smali ファイルを smali2java を使用して Java ソースにデコンパイルします。

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

まず、ネイティブライブラリ関数 CheckUsername と check2 が宣言されていることに着目します。

アプリ側で検証ボタンをクリックすると、入力したユーザ名が CheckUsername 関数に、パスワードが check2 関数に与えられ、検証を実施していることがわかります。

そこで、Ghidra と Android Studio を駆使して検証箇所の特定までは進めたものの、残念ながらネイティブライブラリ関数内の処理を追い切れず、断念しました。

以降は、公式の Writeup を参考にしつつ問題を解き進めていきます。

参考:wm-team/WMCTF-2023

ネイティブライブラリ関数のオフセットを特定する

解析のためにはまず、ネイティブライブラリ内の CheckUsername と check2 の関数のオフセットの特定が必要です。

今回のバイナリでは、ネイティブライブラリのエクスポートテーブルには関数が登録されていませんでした。

そのため、自力で調査した際には Ghidra で見つけたそれっぽい関数にいくつかブレークポイントを設定して Android Studio のデバッガで関数を特定しました。

Writeup を見ると、このようなエクスポートテーブルに関数が登録されていないネイティブライブラリは JNI_onload 関数による dynamic registration(?) の仕組みによって実現しており、スマートに関数アドレスを特定するには RegisterNatives メソッドに Frida hook を仕掛けるのが有効なようです。

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

そこで、Android アプリの解析問題に負けないために学んだ 5 つのテクニック の内容を思い出しつつ、問題の apk を Android Studio にロードしてエミュレータを起動して Frida hook の準備をしていきます。

まずは以下のコマンドを順に実行してエミュレータで Frida サーバを起動します。

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

続いて、RegisterNatives をフックする以下の Javascript コードを payload.js として保存します。

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();

上記の payload.js を使用して Frida hook を試してみたところ、CheckUsername のオフセットが 0x28c0 であり、check2 が 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

しかし、何らかの理由で Frida のプロセスが終了します。

これはどうやら Frida Defection による Anti Debug による問題のようです。

そのため、一旦静的解析のアプローチでこれらの関数の解析を進めていくことにします。

CheckUserName を解析する

まずは CheckUserName 関数から解析を進めていきます。

Ghidra のデコンパイル結果より IDA のデコンパイル結果の方がかなり読みやすかったので、以下に記載します。

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);
}

上から見ていくと、以下の行で v10 に何かの長さが格納されることがわかります。

こちらですが、Android Studio のデバッガで確認してみたところ v12 が入力したユーザ名の文字列で、v10 がその長さになっていました。

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

というわけで、v12 と v10 をそれぞれリネームします。

続いて for と while 文の中の処理を見ていきます。

まず、i の初期値が -1175022871 であることから、1 回目のループでは確実に以下の箇所に最初に到達することがわかります。

v4 には for ループ内における次の i の値が格納されます。

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

ここで、v4 に -1711309751 が格納されたままだと以降の処理に進めなくなるため、ユーザ名は 10 文字である必要があることがわかります。

10 文字のユーザ名が入力されると、次に以下の処理が呼び出されます。

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;

結論から見ていくと、usernameheapptr のアドレスに格納されている 10 バイト分のバイト列がハードコードされた &byte9138 と一致する場合にユーザ名の検証をパスできるようです。

そこで、このメモリ領域に何らかの値を埋め込んでいると思しき sub_5810 関数を見ていきます。

以下が、IDA でデコンパイルした sub_5810 の結果です。

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;
}

問題やってた当時はここで詰んだのですが、どうやらこの関数は RC4 で暗号化をしているらしいです。

確かに、XOR の感じとかが RC4 っぽいと言われればそんな気もしてきますが、正直今の僕のレベルだとこの関数を RC4 だと見抜くのは結構厳しいかなぁという感じでした。

というわけで、RC4 だと見抜けなくても解ける方法を模索していきます。

上記の関数を BingAI に突っ込んだところ、RC4 とは見抜けませんでしたが、「疑似暗号生成器で生成した鍵を用いて暗号化を行う関数である」というかなり惜しいところまで教えてくれました。

疑似暗号生成器で生成した鍵を使った暗号化処理ということは、暗号化と復号に同じ鍵とロジック使用する対象暗号である可能性が考えられます。

つまり、この何だかよくわからない sub5810 の関数に入力文字列ではなく、ハードコードされた暗号化文字列 &byte9138 を突っ込むと、出力結果が正しい入力値の平文になる(可能性がある)というわけです。

暗号化処理の実装が対象暗号かどうかを推測する方法はテクニックとして有効なように思いました。

Frida Detection のバイパス方法は Writeup を見てもよくわからなかったので、普通に Android エミュレータの lldb を使用しました。

イメージベースがアプリの起動ごとに変化するのが少々面倒ですが、こちらは面倒なバイパスなどは不要でした。

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

上記のコマンドで、sub_5810 関数の呼び出し位置にブレークポイントを仕掛けた後、スタックから 4 つの引数をすべて抜き出します。

第 1 引数には入力値が格納されているので、そのアドレスのメモリを &byte_9138 の値に置き換えます。

最後に、関数の次の行に進めて暗号化処理後のバイト列が格納されるアドレスを確認したところ、正しいユーザ名の平文を取得できました。

image-20230824215627455

これでユーザ名がRe_1s_eaSyであることを特定できました。

check2 を解析する

ユーザ名を特定できたので、次はパスワードの検証を行う関数 check2 を見ていきます。

こちらもデコンパイル結果は IDA の方が読みやすかったので IDA の結果を以下に記載します。

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);
}

まず気になるのは最初の以下の行です。

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

ここにブレークポイントを仕掛けてメモリを確認したところ、v10 には入力したパスワード文字列、v13 にはその長さが格納されていました。

そして、ユーザ名の検証と同じく以降のループ開始時の i の値は 1959410021 に固定のため、最初のループでは必ず以下の処理が呼び出されることがわかります。

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

つまり、正しいパスワードの文字数は 16 文字であることが特定できました。

次に着目すべき点は恐らく以下になると思います。

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) は、入力したパスワード文字列のアドレスと、ハードコードされたバイト列 byte_9158 を比較しているように見えます。

つまり、先ほどと同じく sub_7B0 で何らかの形で暗号化されたパスワードがハードコードされた値と一致するかを確認する処理であることが推測できます。

実際に、パスワードに 16 文字の文字列を入力した状態で、sub_7B0 の呼び出し位置にブレークポイントを仕掛けてみました。

image-20230824225645435

呼び出し時のスタックを確認すると、第 3 引数の v11 は、正しいユーザ名に 123456 という文字列を追加した値であることがわかります。

パスワードの文字列と文字数を合わせているあたり、何かしらの XOR などの処理を行うのかもしれません。

一旦 sub_7B0 の関数を実行させてみたところ、パスワードが格納されていたメモリ領域の値が暗号化されたようなバイト列に変化していました。

image-20230824225939774

恐らくここの値が、ハードコードされたバイト列 byte_9158 に一致するような入力値を特定できれば、正しいパスワードを取得できそうです。

image-20230824230105773

sub_7B0 のデコンパイル結果は以下でした。

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;
}

相変わらず僕のレベルでは何が書いてあるのかさっぱりわかりません。

ただ、ghidra-findcrypt を使用してラベリングを行ったところ、以下の通り AES の SBox と思われるデータが定義されていることを確認しました。

image-20230824233754518

ただし、この SBox はどこかの処理でスワッピングされるようで、そのまま使用することができません。

関数 sub_7B0 を呼び出した後のタイミングで SBox のメモリ領域をダンプすると、以下の通り復号に使用できる SBox を取得できました。

(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

続いて、以下の Python スクリプトで SBox の各値の 上位 8 bit と下位 8bit の和をインデックスとする配列に、SBox のインデックスを代入します。

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=",")

正直上記の処理で生成される S2 の配列の用途については全く理解に至っていないですが、恐らく AES 復号の典型なのだと思うので後日改めて勉強します。

この SBox と、Key として受け渡しているユーザ名+123456の文字列、そしてハードコードされた暗号化バイト列の 3 つを利用して、以下の 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);
}

上記を実行して正しいパスワードを得ることで、以下の通り正しい Flag を取得することに成功しました。

image-20230825002938293

まとめ

これ Easy 問扱いなのまじなんだろうか??

Crypto も勉強しないとだめですね。。