2021年8月~9月にかけて行われたオンラインCTF「setodanoteCTF」のReversing(リバースエンジニアリング)問題のWriteUPを書いておく。
CTFのURLはこちら。
公式の方にも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
とりあえず解凍してみると、「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”・・・お前は誰だ?
この日に起きたイベント・・・
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やりたい)
とりあえず実行。
文字数を変えると・・・
結局試行錯誤すると・・・
文字数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ゲット!
だが、この日付はなんだ?
えっ・・・わからん・・・何も載っていない。
どゆこと?
よくわからんが次に行こう( ;∀;)
5問目:to_analyze
解析対象のファイルをダウンロードすると、to_analyze.exeという実行ファイルが手に入った。
たぶんPE形式だとあたりをつけて、今回はfileコマンドは省略。
PEヘッダを解析してみる。
ふーん。
ほーん。
ついでにIDAで開いてみる。
.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;
}
}
コメント