【WriteUp】setodanoteCTFのReversing問題の答え合わせ

CTF
この記事は約14分で読めます。

2021年8月~9月にかけて行われたオンラインCTF「setodanoteCTF」のReversing(リバースエンジニアリング)問題のWriteUPを書いておく。

CTFのURLはこちら。

setodaNote CTF

公式の方にもWriteUP公開や引用・スクショ利用OKとあるので、いろいろ載せていきます。
運営の方々ありがとうございました。楽しかったです。

1問目:Helloworld

なんか文才を感じるな・・・描写が目に浮かぶ。

ということで第1問。
ひとまずZIPファイルをダウンロードして解凍する。

解凍すると、helloworld.exeというファイルがひとつだけ。

とりあえず実行してみると、「Nice.try, please set some word when you run me」と出力された。

適当にaaaと引数を入れると、「flag」といれろと出力されたので、それを引数に実行してflagをゲット。

ユーザー名が平仮名なのはご愛敬

2問目:ELF

問題よりも世界観の方が気になる

さて、2問目のタイトルは「ELF」。

ということで、とりあえずLinux側でファイルをダウンロードして解凍してみる。
「elf」という名前のファイルがあり。どう考えても実行ファイル。
ひとまずファイル形式の確認。

$file elf
elf: data

え、なんで?
しかし、ただのバイナリファイルなわけがない。

次にマジックナンバーを見てみる。
マジックナンバーとは、ファイルの先頭にあるバイト列のことで、これを確認すればどんな種類のファイルなのかわかる。
例えば、exeだったりjpgだったりbmpだったりとわかる。

$xxd -l100 elf
00000000: 5858 5858 0201 0100 0000 0000 0000 0000  XXXX............
00000010: 0300 3e00 0100 0000 5010 0000 0000 0000  ..>.....P.......
00000020: 4000 0000 0000 0000 5031 0000 0000 0000  @.......P1......
00000030: 0000 0000 4000 3800 0b00 4000 1c00 1b00  ....@.8...@.....
00000040: 0600 0000 0400 0000 4000 0000 0000 0000  ........@.......
00000050: 4000 0000 0000 0000 4000 0000 0000 0000  @.......@.......
00000060: 6802 0000                                h...

ファイルの先頭がXXXX・・・明らかにマジックナンバーがおかしいということ。
ちなみにオプションのl(小文字のL)は表示するオクテットを制限するオプション。今回の場合、100個までに制限した。
制限しないと一番最後のバイナリまで表示されるので、スクロールして上にいくのがめちゃ大変。

ELFファイルのマジックナンバーは、16進数で「7F45」なので、いったんファイルをWindows側に移してStirlingでいじくることに。

やっぱこのツールめちゃ使いやすいわー

簡単だね。

そして再度Linux側に戻して実行することでFLAGゲット。

 $./elf
flag{run_makiba}

3問目:Passcode

JOJOでいうところのサンタナ的な感じなのね

とりあえず解凍してみると、「passcode」というファイルがあった。形式を確認してみる。

$ file passcode
passcode: ELF 64-bit LSB shared object, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, BuildID[sha1]=8be572b7a0563868ee29af143b2df0c7d6b1636d, for GNU/Linux 3.2.0, stripped

ELFファイルということなので、実行した。

$ ./passcode
Enter the passcode: flag
Invalid passcode. Too short.

ですよねー。

パスコードを突破しない限り、FLAGはゲットできない仕様か。

しかたがないのでマントノン侯爵夫人を召喚することにした。

マントノン「およびかしら」

サイバーセキュリティのイメージガール?が来たからにはもう怖くない。
とりあえずディスアセンブル。

なんかめっちゃ初期化してますねー

分岐するグラフみたいな表示(何とかビュー)にしてみた。

めちゃ分岐しますやん。まるで棚田。

どうもこの分岐の右下の一つがゴールのようだった。

“The passcode has been verified” (パスコードを確認できました)とある。

これこそが目指すところに違いない。
とりあえずそこを目指してデバッグしてみる。
入力や引数は気にせず、とりあえずフラグレジスタを操作して無理やり進めてみよう。

いくぜ、マントノン!君に決めた!
ブレイクポイントだ!

ブレイクポイントを作り・・・
試練(ゼロフラグ)を打ち砕き・・・
時には運命(キャリーフラグ)をも操って・・・

そして・・・

突如レジスタに現れた謎の日付、20150109
どう考えても次のシステムコールの引数。
そのシステムコールはゴール分岐前の最後のコンペア。
つまり、これが例のPasscodeということだ。


だが・・・

“20150109”・・・お前は誰だ?

2015年 - Wikipedia

この日に起きたイベント・・・

1月8日 -スリランカ大統領選挙で、与党スリランカ自由党の前幹事長で現職のマヒンダ・ラージャパクサ大統領に反旗を翻して新民主戦線(英語版)から野党統一候補として出馬したマイトリーパーラ・シリセーナが現職を破り当選、翌9日に就任[10]。(上記Wikipediaページから引用)

絶対違うな!
IT要素ZEROやわ!

一応この日付を入力するとFLAGはゲットできた。

$ ./passcode 20150109
Enter the passcode: 20150109
The passcode has been verified.

Flag is : flag{20150109}

ということでこの問題はクリア。

だが、例の日付の意味は未だにわからないままだ・・・
CTF運営主のペットの誕生日だろうか。
真のFLAGはいずこに・・・

4問目:Passcode2

サンタナと違って逃げ出せないらしい

続けて4問目。

渡されたファイルはpasscode2というファイル。

$file passcode2
passcode2: ELF 64-bit LSB shared object, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, BuildID[sha1]=a396332a87a60f8e353e93a001a1a9521673f19d, for GNU/Linux 3.2.0, stripped

またELFファイルか。(普通のPEやりたい)

とりあえず実行。

Too shortだと?

文字数を変えると・・・

Too longだと?

結局試行錯誤すると・・・

Nice try

文字数11文字が正解らしい。

これ以上は難しそうなので、デバッガを使うことに。
おいで、マントノン!!

マントノン「およびかしら」

ひとまず逆アセンブルしてみよう。

分岐多いなぁ・・・

とりあえず文字列を探してみると、分岐の果てに”The passcode has been verified”という文字を発見した。

要するにゴールですな

ただ、残念ながらひとつ前の問題のようにstrcmp関数が見当たらず、strlen関数ばかりなので文字数の検査だけをしているよう。
つまり、このままフラグレジスタを捻じ曲げて分岐を進んでいきゴール地点まで行ったとしても、パスコードが見つからない可能性が高いということ。
(strcmpがあり、なおかつその比較対象が初期からメモリに展開される文字列だった場合、その文字列=分岐条件=パスコードとほぼいえるから)

実際に前問同様のブレイクスルーデバッグ(造語)を数回やってみたがうまくいかなかった。

デバッグをしてみると気付いたが、どうも最後の出力の前にforやwhileのような繰り返し処理があるようで、おそらくこの処理でパスコードを生成していると推測。
(前の画像の左上、xorとかしているところが繰り返し処理)

繰り返し処理の詳細を調べる必要があるため、Ghidraでデコンパイルしてみる。

むしゃむしゃ
これアセンブラで読み取るのは大変だわ

ゆーっくりとこれを読んでいくと・・・

  • scanfでターミナルからの入力を受け付けてlocal118変数に保存。
  • 入力された文字列の長さをチェックし、11未満でエラー。12以上でもエラー。つまり文字数11が正解。
  • while文を使って11回ループ。
  • ループの内容は、local124(配列なのか?)の各要素を0x2aの即値でxorしながら、順番にlocal118に上書き格納。

とわかった。

そこで、この関数の初めの方を見てみると、最初の方にスタックを使った初期化処理をしていた。

順番に見ていくと、

0x18
0x1f
0x4
0x79
0x4f
0x5a
0x4
0x18
0x1a
0x1b
0x1e

これらの16進数で初期化されていた。

これを0x2aでxorすると

0x18 32 
0x1f 35
0x4  2e
0x79 53
0x4f 65
0x5a 70
0x4  2e
0x18 32
0x1a 30
0x1b 31
0x1e 34

flagは含まれてないだろうな・・・と思ったがASCIIに直すと・・・

0x18 32 2
0x1f 35 5
0x4  2e .
0x79 53 S
0x4f 65 e
0x5a 70 p
0x4  2e .
0x18 32 2
0x1a 30 0
0x1b 31 1
0x1e 34 4

25.Sep.2014???

$./passcode2
Enter the passcode: 25.Sep.2014
The passcode has been verified.

Flag is : flag{25.Sep.2014}

FLAGゲット!

だが、この日付はなんだ?

2014年 - Wikipedia

えっ・・・わからん・・・何も載っていない。
どゆこと?

よくわからんが次に行こう( ;∀;)

5問目:to_analyze

サンタナ脱出できたん?

解析対象のファイルをダウンロードすると、to_analyze.exeという実行ファイルが手に入った。

たぶんPE形式だとあたりをつけて、今回はfileコマンドは省略。
PEヘッダを解析してみる。

ExeinfoPEはPEヘッダを解析してくれる。左下の横長の2行はいろいろ面白いものを出力してくれる。雰囲気は15年位前のフリーツール。

ふーん。

CFF ExplorerもPEヘッダの解析ツール。情報量多め。スタイリッシュ系。

ほーん。

ついでにIDAで開いてみる。

IDAもPEヘッダを解析してくれる。ガチ解析をしているだろうが、最初の画面の表示情報は少なめ。

.NETですわ!
これ.NETで作られてますわ!!

.NETで作られた実行ファイルにはすごい特徴がある。

マジで元のソースコードを復元できるということ。

.NETのデコンパイルソフトとして有名なILSPYで読み込むと・・・

ホントに読み込んだだけ

タブを開くと・・・

ソースコード復元完了。

このソースコードをよく読んでみると、なんともおかしなことに気づいた。
それは、同じ名前の関数がいくつもあるということ。
そして、それらは引数がバイナリだったり、intだったりとバラバラ。
さらに関数同士で呼び出しあっている。

あーつまり・・・
修正して動くようにしろってこと?

プログラム作るの苦手なんだけど・・・

仕方がないので一番中心にありそうなやつをMain関数にして、それ以外の関数名をabcdみたいにバラバラにして、さらにちょっといじってMain関数に合流させたりして・・・

開発環境の作り方わかんなかったからじゃないんだからねっ!

ばんばん数字出ましたわ・・・

CyberChefで10進数をASCIIに直すと・・・

ということでFLAGゲット。

攻略時のC#のコードは参考までにこの記事の一番下に載せておく。

まとめ

いやー!非常に楽しかった!setodanoteCTF!

時間の都合でリバースエンジニアリング系だけになってしまったのが残念だったけれど、本当に楽しかった。
特に、ラストの問題はねちっこいプログラムだったので、コード苦手な私はけっこう苦戦。

運営さんに感謝です。サンキュー。
あと、あなたのいつもブログで勉強してます。ありがとうございます。

#参考 5問目をクリアしたときのコード(メモとか入っていて汚いけど許して)

internal class a
{
	private static void Main(string[] A_0)
	{
		byte[] array = new byte[15]
		{
			65, 127, 89, 80, 182, 160, 183, 182, 89, 118,
			119, 116, 177, 189, 177
		};
		for (int i = 0; i < array.Length; i++)
		{
			array[i] ^= 35;
			if (c(array[i], 119))
			{
				array[i] += 3;
			}
			array[i] ^= 21;
			array[i] -= 32;
			array[i] = b(array[i], 39);
		}
		//a(Encoding.ASCII.GetString(array), array);
       for (int i = 0; i < array.Length; i++)
        {
		   //System.Console.WriteLine((array[i]));
        }
        System.Console.WriteLine("*****************");
        //ここから後半戦
        System.Console.WriteLine("Yes, that's the right answer.");
		byte[] array2 = new byte[27]
		{
				9, 37, 48, 34, 41, 61, 199, 49, 220, 63,
				115, 59, 220, 200, 46, 115, 57, 220, 214, 50,
				53, 46, 47, 37, 124, 62, 9
		};
		for (int i = 0; i < array2.Length; i++)
		{
			array2[i] ^= array[12];
			array2[i] ^= array[8];
			array2[i] ^= array[3];
			array2[i] ^= 35;
			if (c(array2[i], 113))
			{
				array2[i] += 3;
			}
			array2[i] ^= 21;
			array2[i] -= 32;
			array2[i] = b(array2[i], 114);
		}
		//	System.Console.WriteLine(Encoding.ASCII.GetString(array));
		for (int i = 0; i < array2.Length; i++)
        {
		   System.Console.WriteLine((array2[i]));
        }
       //ここまで後半戦 
        
	}
	
		private static byte b(byte A_0, int A_1)
	{
		switch (A_1)
		{
		case 114:
			A_0 = (byte)(A_0 ^ 0x28u);
			break;
		case 39:
			A_0 = (byte)(A_0 ^ 0x13u);
			break;
		}
		return A_0;
	}
	
		private static bool c(byte A_0, int A_1)
	{
		if (A_1 == 119)
		{
			if (A_0 == 107 || A_0 == 117 || A_0 == 108 || A_0 == 102 || A_0 == 98)
			{
				return true;
			}
		}
		else if (A_0 == 110 || A_0 == 119 || A_0 == 99 || A_0 == 111 || A_0 == 97 || A_0 == 101 || A_0 == 112 || A_0 == 103 || A_0 == 108 || A_0 == 107 || A_0 == 112 || A_0 == 113)
		{
			return true;
		}
		return false;
	}
	
	
	

}

コメント

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