SECCON Beginners CTF 2021 WriteUpのようなもの
SECCON Beginners CTF 2021に参加しました!
SECCON Beginners CTF 2021にチームM15.(W|X)として参加し、3327点で23位(/943)でした!
チームメイトはsoeki, siro53, eren53でした。
今回は前回と比べ結構解けたかなという感じですね
嬉しい
SECCON Challenges
解けたもの
[reversing] only_read
名前の通り、challを読めばいい(機械語で)
ま、読むわけないんですけどね機械語でなんて。
ということでghidraにかけます
途中省略しています。
if (((((((char)local_28 == 'c') && (local_28._1_1_ == 't')) && (local_28._2_1_ == 'f')) && (((local_28._3_1_ == '4' && (local_28._4_1_ == 'b')) && ((local_28._5_1_ == '{' && ((local_28._6_1_ == 'c' && (local_28._7_1_ == '0')))))))) && (((char)local_20 == 'n' && ((((((local_20._1_1_ == '5' && (local_20._2_1_ == 't')) && (local_20._3_1_ == '4')) && ((local_20._4_1_ == 'n' && (local_20._5_1_ == 't')))) && ((local_20._6_1_ == '_' && ((local_20._7_1_ == 'f' && ((char)local_18 == '0')))))) && (local_18._1_1_ == 'l')))))) && ((((local_18._2_1_ == 'd' && (local_18._3_1_ == '1')) && ((char)local_14 == 'n')) && ((local_14._1_1_ == 'g' && (local_12 == '}')))))) { puts("Correct"); }
全部書いてありますね!
ctf4b{c0n5t4nt_f0ld1ng}
[reversing] children
10個の子プロセスを生成し、そのPIDを入力する問題。
ps aux
コマンドでプロセスを確認してPIDを入れるだけの簡単なお仕事。
最後にプロセスをいくつ生成した?って聞かれるけどそれもps aux
で出てきたのを数えれば終わり。
ctf4b{p0werfu1_tr4sing_t0015_15_usefu1}
[reversing] please_not_trace_me
これはプログラムはちゃんとflagを生成しているのにもかかわらず出力しないタイプのやつ。
でも問題名の通りトレースするとトレースすんなやゴラって怒られます。(prease not trace me...
っていう文言でやさしかったわ)
ということでいつものghidra
local_30 = 9; _global_argv = param_2; _global_argc = param_1; _global_envp = param_3; do { switch(local_30) { case 0: local_50 = 2; local_30 = 0xb; break; case 2: local_30 = _1_main_flag_func_0(local_38,0xffffffffffffffff,0x10,5); break; case 5: if (local_50 == 6) { local_30 = 0x12; } else { local_30 = 8; } break; case 6: if (local_40 == 0) { local_30 = 0; } else { local_30 = 0xb; } break; case 8: fwrite("prease not trace me...\n",1,0x17,stderr); /* WARNING: Subroutine does not return */ exit(1); case 9: local_54 = 0; local_30 = 10; break; case 10: switch(local_54) { case 0: local_30 = 0x16; break; case 1: local_30 = 0xf; break; case 2: local_30 = 0x13; break; case 3: local_30 = 0x14; break; case 4: local_30 = 0x11; break; case 5: local_30 = 0x15; break; default: local_30 = 0x12; } break; case 0xb: local_38 = ptrace(PTRACE_TRACEME,0,1,0); local_30 = 2; break; case 0xf: local_48 = malloc(0x10); local_30 = 0x12; break; case 0x10: local_50 = local_50 * 3; local_30 = 5; break; case 0x11: puts("flag decrypted. bye."); local_30 = 0x12; break; case 0x12: local_54 = local_54 + 1; local_30 = 10; break; case 0x13: generate_key(local_48); local_30 = 0x12; break; case 0x14: rc4(e,local_48); local_30 = 0x12; break; case 0x15: /* WARNING: Subroutine does not return */ exit(0); case 0x16: local_50 = 0; local_40 = ptrace(PTRACE_TRACEME,0,1,0); local_30 = 6; } } while( true );
これを見るとlocal_30というものでswitch文を回していることが分かる。
なので、local_30の値を弄ってptraceを通らないようにしてあげればいい。
具体的には0x0F→0x13→0x14って通ればよい。
で、弄ってからトレースする
ctf4b{d1d_y0u_d3crypt_rc4?}
[reversing] firmware
何かのファームウェアのバイナリが落ちてくる。(firmware.bin)
で、これは複数のファイルを一つにまとめたものであることが分かる。
なので、全部分割しちゃえ!!!ってことで分割をするのです。
firmware.binの1行目途中からファイル名とそのMD5ハッシュが記録されているのでそれを頼りに分割する。
んで、こうなる
これを見るとfirm以外重要そうなものが見つからないのでfirm(32bit ELF ARM)をghidraで見てみる(いつもの)
recv(local_11d8,abStack4116,0x1000,0); printf("%s",abStack4116); memcpy(auStack4520,&DAT_00010ea4,0xf4); __n = strlen((char *)abStack4116); if (__n != 0x3d) { local_10b4 = 0x6f636e49; uStack4272 = 0x63657272; uStack4268 = 0x61702074; uStack4264 = 0x6f777373; local_10a4 = 0xa2e6472; local_10a0 = 0; __n = strlen((char *)&local_10b4); send(local_11d8,&local_10b4,__n,0); close(local_11d8); } local_11e0 = 0; while (local_11e0 < 0x3d) { if ((uint)(abStack4116[local_11e0] ^ 0x53) != auStack4520[local_11e0]) { local_10b4 = 0x6f636e49; uStack4272 = 0x63657272; uStack4268 = 0x61702074; uStack4264 = 0x6f777373; local_10a4 = 0xa2e6472; local_10a0 = 0; __n = strlen((char *)&local_10b4); send(local_11d8,&local_10b4,__n,0); close(local_11d8); } local_11e0 = local_11e0 + 1; } local_10b4 = 0x72726f43; uStack4272 = 0x20746365; uStack4268 = 0x73736170; uStack4264 = 0x64726f77; local_10a4 = 0xa212121; local_10a0 = 0; __n = strlen((char *)&local_10b4); send(local_11d8,&local_10b4,__n,0); close(local_11d8);
すると、文字数(0x3d)と文字列を判別していることが分かる。
文字列については0x53とxor取ってから判別しているので、プログラムに埋め込まれているものに0x53でxorして出力。
#include <stdio.h> int main(int argc, char *argv[]) { char tmp[] = {0x30, 0x27, 0x35, 0x67, 0x31, 0x28, 0x3a, 0x63, 0x27, 0x0c, 0x37, 0x36, 0x25, 0x62, 0x30, 0x36, 0x0c, 0x35, 0x3a, 0x21, 0x3e, 0x24, 0x67, 0x21, 0x36, 0x0c, 0x32, 0x3d, 0x32, 0x62, 0x2a, 0x20, 0x3a, 0x60, 0x0c, 0x21, 0x36, 0x25, 0x60, 0x32, 0x62, 0x20, 0x0c, 0x32, 0x0c, 0x3f, 0x63, 0x27, 0x0c, 0x3c, 0x35, 0x0c, 0x66, 0x36, 0x30, 0x21, 0x36, 0x64, 0x20, 0x2e, 0x59}; int s = 61; for(int i = 0; i < s; i++) { printf("%c", (tmp[i] ^ 0x53)); } printf("\n"); }
で、終わり。
ctf4b{i0t_dev1ce_firmw4re_ana1ysi3_rev3a1s_a_l0t_of_5ecre7s}
[misc] Mail_Address_Validator
rubyのプログラムを拾えるので見てみると、正規表現を使ってチェックしてるだけ。
チェックがタイムアウト(5秒)するとflagが落ちてくるらしい。
正規表現でタイムアウトさせるためにクソ長メールアドレスっぽい文字列を入れた。(約80万文字)
するとあっさり表示された。
ctf4b{1t_15_n0t_0nly_th3_W3b_th4t_15_4ff3ct3d_by_ReDoS}
[misc] fly
Wolf RPGエディタのゲーム内のオブジェクトにflagが隠されているのだけが分かる。
でも壁があって通り抜けられないし煽ってくるキャラクターもいる。
Wolf RPGエディタは起動時引数にTest_Of_Main
を指定するとデバッグモードで起動することができるので、これを使わない手はない。
デバッグモード時では、F11キーを押すことで固定データのみ更新することができる(可変データはそのまま残る)。
ということで、Wolf RPGエディタをダウンロードした時にあったサンプルのデータを利用して起動する。
この時、Data.wolf
はData.wolf1
などと改名しておき、Game.exe
から見えないようにしておく。
その後、Game.exe Test_Of_Main
コマンドを利用して起動する。
するとサンプルデータのゲームが起動するので、スタートする。
ここでさっき改名しておいたData.wolf1
をData.wolf
に戻してF11キーを押すと楽しいことが起きる。
動けない場合は動けるような場所に最初から移動しておいてください。
ちなみに青い階段にはイベントが設定されていないのでflagを見つけることができない。
なので、サンプルデータのゲームで場面転換しておくといい。
サンプルデータのゲームで上へ行き、上のマップへ移動してください。
ここからはさっきと同じで、改名しておいたData.wolf1
をData.wolf
に戻してF11キーを押す。
すると…?
ctf4b{b3_c4r3ful_0f_fl135_wh3n_73l3p0r71n6}
[misc] depixelization
pythonのプログラムとflagの画像が落とせる。
ここで普通はプログラムで行っている処理の逆を行いflagを復元しよう!となるだろう。
私はこのプログラムには好きな文字列に同様の処理をすることができるのに気付いたので、全アルファベットと数字と{}_を処理した画像を出力した。
あとはこれのどれと完全一致するか、というのを判別するプログラムを書き、終わり。
import javax.imageio.ImageIO; import java.awt.image.BufferedImage; import java.io.File; public class Main { public static void main(String[] args) { try { String flagFile = "ファイル名書き換えて"; String alphabetFile = "ファイル名書き換えて"; BufferedImage flagImg = ImageIO.read(new File(flagFile)); BufferedImage alphabetImg = ImageIO.read(new File(alphabetFile)); int flagWidth = flagImg.getWidth(); int alphabetWidth = alphabetImg.getWidth(); for (int i = 0; i < flagWidth; i += 85) { boolean match = false; int matchnum = 0; for (int j = 0; j < alphabetWidth && !match; j += 85) { boolean ok = true; for (int x = 5; x < 85 && ok; x += 10) { for (int y = 5; y < 100 && ok; y += 10) { if (flagImg.getRGB(i + x, y) != alphabetImg.getRGB(j + x, y)) { ok = false; } } } if (ok) { match = true; matchnum = j / 85; } } if (match) { if (matchnum < 'z' - 'a') { System.out.print((char) (matchnum + 'a')); } else { matchnum = matchnum - 'z' + 'a' - 1; if (matchnum < 10) { System.out.print(matchnum); } else { char a = ' '; switch(matchnum) { case 10: a = '{'; break; case 11: a = '}'; break; case 12: a = '_'; break; } System.out.print(a); } } } else { System.out.print("."); } } } catch (Exception e) { e.printStackTrace(); } } }
これだとnとmの判別がつかないらしく、実際nがmとして認識された。
ctf4b{1f_y0u_p1x_y0u_c4m_d3p1x}
と出力されたが、間違いだったのでmをnに修正。
ctf4b{1f_y0u_p1x_y0u_c4n_d3p1x}
SECCONの感想
今回は前回と比べ倍以上の問題が解けたので大満足
PWNは前回同様解けませんでした。