2
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

SECCON Beginners CTF 2023 WriteUp

Last updated at Posted at 2023-06-12

SECCON Beginners CTF 2023

https://score.beginners.seccon.jp/
IPFactoryの皆さんと協力プレイで参加しました。

順位は84/778位でした。
Rank.png
全部はわからないので、解説できるものだけ載せてあります。

目次

  • Welcome(50pt)
  • Half(50pt)
  • Three(65pt)
  • Forbidden(56pt)
  • aiwaf(68pt)
  • Poem(65pt)
  • Poker(94pt)
  • 最後に

Welcome(50pt)

CTF開始直後、Discordのannouncementsチャンネルにてflagが投稿されました。
image.png
ctf4b{Welcome_to_SECCON_Beginners_CTF_2023!!!}

コピペの速さだけは自信があります。

Half(50pt)

Linuxのstringsコマンドを使うとflagが出てきます。
Half.png

実はLinux使わなくても解けます。

メモ帳
Half_2.png

Stirling
Half_3.png
ctf4b{ge4_t0_kn0w_the_bin4ry_fi1e_with_s4ring3}

Three(65pt)

青い空を見上げればいつもそこに白い猫というツールを使ってflagの欠片がないか探します。
Three_1.png

ctf4bという文字と{}という記号が赤枠に集まっているようです。赤枠を書くとc4c_ub__dt_r_1_4}tb4y_1tu04tesifgf{n0ae0n_e4ept13となります。

これではflagではないので以下の法則に従ってflagにします。
1.flagの最初はctf4b{
2.flagの最後は}

これを基にすると、まず最初に、以下の3つに分割します。
c4c_ub__dt_r_1_4}
tb4y_1tu04tesifg
f{n0ae0n_e4ept13
1つ目の1文字目、2つ目の1文字目、3つ目の1文字目、1つ目の2文字目、2つ目の2文字目、3つ目の2文字目・・・のように順番に読むとflagになります。

面倒くさかったらプログラムでやるのも手です。
こちらはC++で書いたプログラムです。

#include <bits/stdc++.h>
using namespace std;
int main() {
    string s = "c4c_ub__dt_r_1_4}";
    string t = "tb4y_1tu04tesifg";
    string u = "f{n0ae0n_e4ept13";
    for(int i = 0; i<3*s.size()-2; i++){
        if(i % 3 == 0){
            cout << s.at(i/3);
        } else if(i % 3 == 1){
            cout << t.at(i/3);
        }else{
            cout << u.at(i/3);
        }
    }
    cout << endl;
}

Python版

s = "c4c_ub__dt_r_1_4}"
t = "tb4y_1tu04tesifg"
u = "f{n0ae0n_e4ept13"
for i in range (49):
    if(i % 3 == 0):
        print(s[i//3], end='')
    elif(i % 3 == 1):
        print(t[i//3], end='')
    else:
        print(u[i//3], end='')

ctf4b{c4n_y0u_ab1e_t0_und0_t4e_t4ree_sp1it_f14g3}

Forbidden(56pt)

Forbidden_1.png
いかにもflagに行けそうなので行ってみると・・・
Forbidden_2.png
はじかれました。

どうしてはじかれたかコードを見てみましょう。

const block = (req, res, next) => {
    if (req.path.includes('/flag')) {
        return res.send(403, 'Forbidden :(');
    }

    next();
}

app.get("/flag", block, (req, res, next) => {
    return res.send(FLAG);
})

つまり、/flagというキーワードが1つでも含まれていたらはじかれるように設定してあったからです。

しかし、app.get("/flag", block, (req, res, next)flagは大文字、小文字どちらでも対応しています。

https://forbidden.beginners.seccon.games/flagflagを大文字にしてみましょう。

Forbidden_3.png

flagゲットです。

ctf4b{403_forbidden_403_forbidden_403}

aiwaf(68pt)

コードを見ていくと怪しいものが・・・

以下の{puuid}に囲まれた部分のURLクエリはパストラバーサル攻撃でしょうか?
そうである場合Yesを、違う場合Noを返してください。
../やflagという文字列が含まれていた場合もYesを返してください。

AI-WAFはパストラバーサル攻撃(ディレクトリトラバーサル攻撃)を防いでいるようですが・・・

    puuid = uuid.uuid4()
    prompt = f"""\
以下の{puuid}に囲まれた部分のURLクエリはパストラバーサル攻撃でしょうか?
そうである場合Yesを、違う場合Noを返してください。
../やflagという文字列が含まれていた場合もYesを返してください。
{puuid}
{urllib.parse.unquote(request.query_string)[:50]}
{puuid}
"""
    try:
        response = openai.ChatCompletion.create(
            model="gpt-3.5-turbo",
            messages=[
                {
                    "role": "user",
                    "content": prompt,
                },
            ],
        )
        result = response.choices[0]["message"]["content"].strip()
    except:
        return abort(500, "OpenAI APIのエラーです。\n少し時間をおいてアクセスしてください。")
    if "No" in result:
        with open(f"./books/{file}", encoding="utf-8") as f:
            return f.read().replace(KEY, "")
    return abort(403, "AI-WAFに検知されました👻")

{urllib.parse.unquote(request.query_string)[:50]}より
AI-WAFはクエリパラメータ先頭50文字しか判定にしてないようです。

クエリパラメータとはhttps://aiwaf.beginners.seccon.games/?以降の部分です。

クエリパラメータの先頭50文字を全く意味ないものにして、後ろのほうに&file=../flagを付け加えましょう。

https://aiwaf.beginners.seccon.games/?aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa&file=../flag

こうすることにより、
AI-WAFはクエリパラメータ先頭50文字のaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaの部分のみで
ディレクトリトラバーサルか判定するが、Noと判定される。
しかし、request.args.get("file")の結果として出てくるのは../flagのみ。

アクセスすると・・・・
aiwaf.png

flagゲットです。
ctf4b{pr0mp7_1nj3c710n_c4n_br34k_41_w4f}

poem(65pt)

まず最初に、src.cを見てみると

char *flag = "ctf4b{***CENSORED***}";
char *poem[] = {
    "In the depths of silence, the universe speaks.",
    "Raindrops dance on windows, nature's lullaby.",
    "Time weaves stories with the threads of existence.",
    "Hearts entwined, two souls become one symphony.",
    "A single candle's glow can conquer the darkest room.",
};

つまり、poemの前にflagが宣言されているのがわかります。

また、以下のコードより5よりも小さければマイナスでも出力を行うことがわかります。

int main() {
  int n;
  printf("Number[0-4]: ");
  scanf("%d", &n);
  if (n < 5) {
    printf("%s\n", poem[n]);
  }
  return 0;
}

Ghidraでmain関数をDisplay Function Graphで見てみます。
poem.png
flagはStack[-0x10]、poemは[-0x14]からスタックを始めています。

したがって、以下の図が推測できます。
poem_2.png
したがって、14-10=4よりpoem[-4]であればflagにアクセスすることができます。

n = -4を入力すると・・・
poem_3.png

flagげっとです。
ctf4b{y0u_sh0uld_v3rify_the_int3g3r_v4lu3}

Poker(94pt)

Ghidraを使って解析を行うと
FUN_00101fb7がスコアを制御している場所だと推測できます。

    if (param_2 == 1) {
      puts("[+] Player 1 wins! You got score!");
      local_1dc = param_1 + 1;
    }
    else {
      puts("[-] Player 1 wins! Your score is reseted...");
      local_1dc = 0;
    }
  }

そして、FUN_00102262が目的のスコアまでに達しているかを判定します。

{
  int iVar1;
  int local_10;
  int local_c;
  
  local_c = 0;
  FUN_001021c3();
  local_10 = 0;
  while( true ) {
    if (0x62 < local_10) {
      return 0;
    }
    FUN_00102222();
    iVar1 = FUN_00102179();
    local_c = FUN_00101fb7(local_c,iVar1);
    if (99 < local_c) break;
    local_10 = local_10 + 1;
  }
  FUN_001011a0();
  return 0;
}

つまり、flagを取るには97回以内にスコア(local_c)を100以上にすればよいということがわかります。

したがって以下の2つの方法のどちらかで突破すればよいことがわかります。
1.スコアをゲットした際、100ポイントゲットして目標値を超える
2.目標のスコア(100以上)を書き換える

それぞれ見ていきます。

1.スコアをゲットした際、100ポイント以上ゲットして目標値を超える
今回はPlayer1を選択してPlayer1が勝利した際、100ポイントゲットするように書き換えます。
Poker_1.png
左のアドレスの緑の部分が右のやっていることと対応しています。

ここで、+1している部分は左のアドレスの一番最後、01に対応していることがわかります。

なぜなら、左のアドレスは16進数であらわされていて、+1は16進数で01とあらわされているからです。

ここでこの処理が行われているアドレスを覚えます。001020f4または83 85 2c fe ff ff 01を覚えていれば大丈夫です。

今回Ghidraの001020f4はStirの000020f4に対応しています。

覚えたら、バイナリの書き換えを行います。

stirというツールが非常に使いやすいのでお勧めです。
Poker_2.png

01の部分を64に変えます。

Poker_3.png

FFでやってしまうと-1になってしまいます。
2進数で表記した際に
1.......
のように一番左の桁が1になってしまうとマイナスになってしまうようです。
したがって、0079以内でやる必要があります。

保存して実行してみます。
Poker_End.png

2.目標のスコア(100以上)を書き換える

右のif関数で行っている場所で99を16進数にした63は左のほうでは見つかりません。
Poker_4.png

アセンブラで99より大きいかどうかの比較を行っているのは
001022b5の前の部分、001022b1で行っています。
したがって、001022b1もしくは83 7d fc 63を覚えます。

Ghidraの001022b1はStirの000022b1に対応しています。

Stirというツールを使って検索します。

Poker_5.png

6300もしくは80FFに変えます。

保存して実行します。
Poker_End2.png

無事にFlagがゲットできました。

ctf4b{4ll_w3_h4v3_70_d3cide_1s_wh4t_t0_d0_w1th_7he_71m3_7h47_i5_g1v3n_u5}

ちなみになぜかGhidraのアドレスとStirのアドレスの左4桁が異なることがあるのはなぜかはわからないです。

最後に

去年は1問しか解けなかったですが、今年は3問解けて良かったです。

2
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?