CTFするぞ

CTF以外のことも書くよ

【Cheat 100】lights out - InterKosenCTF 作問writeup

はじめに

この記事ではInterKosenCTFで出題した問題の解説を書きます。 他の問題のwriteupについては下記リンクから参照してください。

ptr-yudai.hatenablog.com

解説

Description: Turn all the lights on.
File: lights_out.tar.gz

.NET製のexeファイルがもらえます. ライツアウトというゲームで,すべてのマスを黄色にすればクリアです.(本当はライツアウトなので黒色にするゲームなんですけどね......) この問題に限らずCheatジャンルはあまりリバージングせずに解けるように設計されているのですが,dnSpyを使った方が多いようです. 研究室内CTFでかなり昔出題した問題で,Dotfuscatorか何かを使って難読化した記憶がありますが,あんまり覚えていません.

解法1

今のところ他の方のwriteupにはありませんが,動的にメモリハックする方法があります. プログラムを実行し,Cheat Engineで明かりの点灯を示す変数を探します. 一番左端のマスを対象にし,Value TypeをBinaryにし,Bitsにチェックを付けて検索します. いろんなマスを付けたり消したりして絞り込むとマスの状態を保存した配列のアドレスが得られます.

f:id:ptr-yudai:20190121221055p:plain

これを全て1(もしくは0)にして,すべてのマスを点灯させてやればフラグが表示されます.

f:id:ptr-yudai:20190121222018p:plain

解法2

解法3で処理を追いますが,メモリ上にフラグが展開されているのでそれを検索することも可能です.(Cheat問なのであんまりやってほしくはないですが...) Cheat Engineで"KOSENCTF"を検索すると一発で一致します. たぶんこれが一番早いと思います.

解法3

動かさずに解くこともできます.まずはdnSpyで調査してみましょう. メソッドやクラス名はハングル文字になっており,文字列は謎のメソッドから受け取った値を使っており読めないのですが,基本的な流れは分かるのでフラグが得られそうな箇所を探します. すると次のようなメソッドがあります.

f:id:ptr-yudai:20190121213920p:plain

20x20マスをチェックしているように見えるので,最後のMessageBoxでフラグが表示されるのではないかと予想できます. ここからも3種類解き方があって,1つ目はILを書き換える方法です. メソッドを右クリックしてEdit IL Instructionを選び,例えば12行目のretを右クリックしてNOP Instructionを選択すると,returnが消滅するのでMessageBoxが表示されます. ちなみにdnSpyはメソッドを編集できるのですが,今回は難読化の影響で上手く変更できません. 2つ目はデバッガを使う方法で,dnSpyのデバッグ機能を使うことで強制的にMessageBoxの箇所へ移動することができます. 3つ目はMessageBoxの引数を調べる方法です. 745BBE96-A34C-4723-B7D4-089F48D57F2E.헡()を見ると

public static string 헡()
{
    return 745BBE96-A34C-4723-B7D4-089F48D57F2E.<<EMPTY_NAME>>[1] ?? 745BBE96-A34C-4723-B7D4-089F48D57F2E.<<EMPTY_NAME>>(1, 15, 53);
}

となっており,さらに745BBE96-A34C-4723-B7D4-089F48D57F2Eには

private static string <<EMPTY_NAME>>(int num, int index, int count)
{
    string @string = Encoding.UTF8.GetString(745BBE96-A34C-4723-B7D4-089F48D57F2E.<<EMPTY_NAME>>, index, count);
    745BBE96-A34C-4723-B7D4-089F48D57F2E.<<EMPTY_NAME>>[num] = @string;
    return @string;
}

となっています. したがって,フラグは745BBE96-A34C-4723-B7D4-089F48D57F2E.<<EMPTY_NAME>>の15文字目から53文字ということになります. この変数を調べると,

static 745BBE96-A34C-4723-B7D4-089F48D57F2E()
{
    // Note: this type is marked as 'beforefieldinit'.
    745BBE96-A34C-4723-B7D4-089F48D57F2E.<<EMPTY_NAME>> = new byte[]
    {
        200,
        223,
        198,
[省略]
        134,
        187,
        187
    };
    for (int i = 0; i < 745BBE96-A34C-4723-B7D4-089F48D57F2E.<<EMPTY_NAME>>.Length; i++)
    {
        745BBE96-A34C-4723-B7D4-089F48D57F2E.<<EMPTY_NAME>>[i] = (byte)((int)745BBE96-A34C-4723-B7D4-089F48D57F2E.<<EMPTY_NAME>>[i] ^ i ^ 170);
    }
}

という形で初期化されています. この部分をpythonとかで実装すればフラグが出ます.

あとがき

割といろんな解き方があり,多くのチームが解いてくれました. 難易度と点数もちょうど良く,特に問題なかったと思います.