UofTCTF [rev] CEO’s Lost Password Writeup

はじめまして

情報系大学院1年生のtamiといいます

2024年開催の UofTCTF に、チームsayonaraとして参加しました

僕自身は、revを5問中4問解いて、チーム順位は1225チーム中94位でした

ググってもCEO’s Lost Passwordのwriteupがあまりなかったので、自分で書きました

ブログ・writeupを書くのは初めてなので、温かい目で見守ってください

実行・動作確認

問題からBankChallenge.jarファイルをダウンロードします

まずは実行して動作確認してみます

ユーザ名はadminらしいですね

$ java -jar BankChallenge.jar
==============================
Welcome to TotallySecureBank™
==============================

Please enter your username:
admin (入力)
Please enter your password:
hogehoge (入力)
Incorrect password!

hogehogeではダメでした…

jarファイル展開

jarファイルを以下コマンドでzipに変換して展開します

$ cp BankChallenge.jar BankChallenge.zip
$ unzip BankChallenge.zip

結果、Main.classとa.classが出現します

.classファイル逆コンパイル

jd-guiを用いてMain.classファイルを逆コンパイルします

main関数の解析

最初に、main関数を解析します (字が小さくてごめんなさい)

main関数の処理は以下と予想します

  1. System.out.println関数で、b関数によって復号した文字列を表示
  2. a関数を実行し、戻り値をexit関数に渡して終了

b関数の解析

次に、b関数を解析します

いくつか無駄な処理や謎の処理があるので、修正します。

  1. 配列bを用いた難読化
  2. ifとlabelを用いた難読化
難読化解除後のb関数

上記2つを解除したb関数のコードは以下です

public static String b(String paramString) {
	StringBuilder stringBuilder = new StringBuilder();
	int i = 0;
	while (i < paramString.length()) {
		char c = paramString.charAt(i);
		stringBuilder.append((char)(c ^ 0xCC77));
		i++;
	}
	return stringBuilder.toString();
}

結果、文字 xor 0xCC77 を計算していることが分かりました

よく見る形式ですね

b関数の動作確認

実装したb関数を、main関数の引数を設定して呼び出してみます

public static void main(String[] args) {
	String messEnc;
		
	messEnc = "\uCC4A\uCC4A\uCC4A\uCC4A\uCC4A\uCC4A\uCC4A\uCC4A\uCC4A\uCC4A\uCC4A\uCC4A\uCC4A\uCC4A\uCC4A\uCC4A\uCC4A\uCC4A\uCC4A\uCC4A\uCC4A\uCC4A\uCC4A\uCC4A\uCC4A\uCC4A\uCC4A\uCC4A\uCC4A\uCC4A";
	System.out.println(b(messEnc));

	messEnc = "\uCC20\uCC12\uCC1B\uCC14\uCC18\uCC1A\uCC12\uCC57\uCC03\uCC18\uCC57\uCC23\uCC18\uCC03\uCC16\uCC1B\uCC1B\uCC0E\uCC24\uCC12\uCC14\uCC02\uCC05\uCC12\uCC35\uCC16\uCC19\uCC1C\uED55";
	System.out.println(b(messEnc));
	
	messEnc = "\uCC4A\uCC4A\uCC4A\uCC4A\uCC4A\uCC4A\uCC4A\uCC4A\uCC4A\uCC4A\uCC4A\uCC4A\uCC4A\uCC4A\uCC4A\uCC4A\uCC4A\uCC4A\uCC4A\uCC4A\uCC4A\uCC4A\uCC4A\uCC4A\uCC4A\uCC4A\uCC4A\uCC4A\uCC4A\uCC4A";
	System.out.println(b(messEnc));

}	

これを下記コマンドでコンパイル・実行します

$ javac b_dec.java
$ java b_dec 
==============================
Welcome to TotallySecureBank™
==============================

動作確認で出現した文字列です!

うまく復号できています

引数無しのMain.a関数の解析

次に、main関数から呼ばれている引数無しのa関数を解析します

これも読みにくくなっているため、解除したものは以下です

難読化解除後の引数無しMain.a関数
static int a() {
	String uname, pass;
	Scanner scanner = new Scanner(System.in);
	Map<String, a> map = Map.of(
    	    "user", new a("j2ob77p+Pw==", 10.0F), 
    	    "admin", new a("te/fJg8aP0VUSVGCcI2Ll8erutizvrnJ", 100000.0F)
        );
	while (true) {
		System.out.println("Please enter your username:");
		uname = scanner.nextLine();
    	    	if (!map.containsKey(uname)) {
            	    	System.out.println("User not found")
			continue;
		}
      		break;
    	}

	while (true) {
		System.out.println("Please enter your password:");
		str1 = scanner.nextLine();
    	    	if (((a)map.get(uname)).a(pass)) {
		    	break;
		} else {
			System.out.println("Incorrect password!");
		}
	} 
	System.out.println("Welcome back " + uname + "! your balance is " + ((a)map.get(uname)).a());
	return 0;
}
補足 : 2バイトを1文字として捉えられた

5, 6行目のa関数に渡している文字列に注目します

b関数で復号した文字列を見ようとしました

しかし、ただb関数に入力するだけでは、戻り値がアルファベットで表示されませんでした

2バイト分が1文字として出力されていため、以下のコードで2文字に強制分割して表示しました

String mess = b(messEnc);
int i=0;
while (i < mess.length()) {
	char c = mess.charAt(i);
	System.out.print((char)((c & 0xFF00) >> 8));
	System.out.print((char)((c & 0x00FF)));
	i++;
}

結果、文字化けからbase64っぽいアルファベットに変換されました

user について
樲潢㜷瀫偷㴽 (2バイト1文字として表示)
j2ob77p+Pw== (1バイト1文字として表示)

admin について
瑥⽦䩧㡡倰噕卖䝃捉㉌永敲畴楺癲湊 (2バイト1文字として表示)
te/fJg8aP0VUSVGCcI2Ll8erutizvrnJ (1バイト1文字として表示)

a関数のコードを見ると、ユーザ名とパスワードを入力させ、照合している処理だと分かります。

アカウントは、”user”と”admin”の2つ存在します。

次に注目すべきは、mapの初期化と、パスワード比較処理です

Map<String, a> map = Map.of(
        "user", new a("j2ob77p+Pw==", 10.0F), 
    	"admin", new a("te/fJg8aP0VUSVGCcI2Ll8erutizvrnJ", 100000.0F)
);
------------------------------------------------------------------------
if (((a)map.get(uname)).a(pass))
map初期化処理

mapの初期化から見ていきます

base64文字列と数値をa関数に渡して初期化しています

aクラスはa.classファイルに記述されています

aクラスのコンストラクタで引数をフィールド変数に設定しています

パスワード比較処理

パスワード比較処理では、入力したパスワードをaクラスのa関数に渡しています

a.classファイルのa関数を読むと、以下のことが分かります

  1. Mainで記述されている引数有りのa関数に受け取った文字列を渡す
  2. a関数の結果と、コンストラクタで設定したフィールド変数を比較する
  3. 等しい場合はtrue, 異なる場合はfalseを返す

言い換えると、以下の関係式になるpasswordを特定する問題になります

Main.a(password) == a.a

a.aの内容 :  "j2ob77p+Pw==" or "te/fJg8aP0VUSVGCcI2Ll8erutizvrnJ"

a.aについては、map初期化の際に渡しているbase64っぽい文字列なので

Main.a関数の処理が分かればpasswordが分かりそうです

引数有りMain.a関数の解析

main関数から呼ばれている引数有りのa関数を解析します

この処理が分かれば、ソルバが書けそうです

これも読みにくくなっているため、解除したものは以下です

難読化解除後の引数有りMain.a関数
static String a(String paramString) {
	byte[] arrayOfByte = paramString.getBytes(StandardCharsets.UTF_8);
	int i = 1;
	while (i <= paramString.length()) {
		int j = 0;
		while (j < arrayOfByte.length) {
			arrayOfByte[j] = (byte)(arrayOfByte[j] + ((i - 0xC) * j + 6));
			j++;
		}
		i++;
	}
	return new String(Base64.getEncoder().encode(arrayOfByte), 
		StandardCharsets.UTF_16);
}

結果、文字列を以下の処理で1文字ずつ暗号化し、base64エンコーディングしていることが分かります

arrayOfByte[j] = (byte)(arrayOfByte[j] + ((i - 0xC) * j + 6));

この逆操作をすれば復号できそうです

arrayOfByteに足されている数値を引けば良さそうです

arrayOfByte[j] = (byte)(arrayOfByte[j] - ((i - 0xC) * j + 6));

ソルバの作成

情報を以下にまとめます

パスワードはMain.a関数によって暗号化、base64エンコーディングされている
この処理の結果、赤マークの文字列が出力される
"user", "j2ob77p+Pw==" 
"admin","te/fJg8aP0VUSVGCcI2Ll8erutizvrnJ"

復号にはbase64デコーディング、暗号化の逆演算を記述すれば良さそう

ということで、ソルバは以下です

ソルバ (DecryptFunction.java)

public class DecryptFunction {
    public static void main(String[] args) {

        // Main.a関数によって暗号化されたパスワード
        String encryptedInput = "te/fJg8aP0VUSVGCcI2Ll8erutizvrnJ";

        // 復号
        String decryptedOutput = decrypt(encryptedInput);
        System.out.println("Decrypted Output: " + decryptedOutput);
    }

    private static String decrypt(String base64Input) {
        // Base64デコード
        byte[] decodedBytes = java.util.Base64.getDecoder().decode(base64Input);

        // 逆変換(加工の逆操作)
        int i = 1;
        while (i <= decodedBytes.length) {
            int j = 0;
            while (j < decodedBytes.length) {
                decodedBytes[j] = (byte) (decodedBytes[j] - ((i - 0xC) * j + 6));
                j++;
            }
            i++;
        }

        // バイト列をUTF-8文字列に変換
        String decryptedString = new String(decodedBytes, 
                java.nio.charset.StandardCharsets.UTF_8);

        return decryptedString;
    }
}

実行結果は以下の通りです

$ javac DecryptFunction.java
$ java DecryptFunction
Decrypted Output: %S7rONgadMInPaSSwORd32%
flag : uoftctf{%S7rONgadMInPaSSwORd32%}

拙い文章ですが、最後まで読んでいただき、ありがとうございました。

久しぶりに java 系の問題を解いたので、変なこと書いてないか心配で心臓がバクバクしています

コメント

タイトルとURLをコピーしました