kuzushikiのぺーじ

セキュリティに関することを書きたいですね

GitHub Twitter

b01lers CTF write up

b01lers CTFに参加しました!

(team: team_Yamasan)

某CTFをやってみよう会にて参加しました。

自分は303点中301点を入れ、順位は129位 / 660チーム(0点は除く)でした。

FireShot Capture 024 - b01lers CTF - ctfd2 ctf b01lers com

今回のCTFはなんと公式が全問題のwrite upを書いてくれます。

ここに投稿されます

しかし、投稿予定日が金曜日と遅いので、復習もかねて解けた問題のwrite upを書くことにします。

Harvesting Season (CRYPTO, 100 pts, 66 Solves)

Can you xor your way out of this? (Length of key: 4)

問題ファイル: fr3sh_h4rv3st.jpg

画像のExifになにやら怪しい数列が確認できます。

キャプチャ

おそらくこれが暗号文でしょう。

次に問題文から以下のことが分かります。

  1. この暗号はXORで暗号化されたものである
  2. 鍵長は4である

また、平文にはフラグフォーマットであるpctfが含まれるはずです。

よって、暗号文における長さ4の文字列とpctfのXORをとったものを鍵として、暗号文全体を復号(もう一度XOR)すればフラグが得られます。

プログラムは以下の通りです。

from Crypto.Util import number 
enc = 0x1921754512366910363569105a73727c592c5e5701715e571b76304d3625317c1b72744d0d1d354d0d1d73131c2c655e
enc = number.long_to_bytes(enc)

flag_format = b'pctf'

# まず鍵のリストを作る
key_list = []
for i in range(len(enc)- len(flag_format) + 1):
    key = b''
    for j in range(len(flag_format)):
        key += chr(enc[i+j] ^ flag_format[j]).encode()
    key_list.append(key)

# 鍵リストの鍵を一個ずつ試す
for i,key in enumerate(key_list):
    dec = b''
    for j in range(len(enc)):
        dec += chr(enc[(i+j) % len(enc)] ^ key[j % len(key)]).encode()
    print(dec)

復号結果が出力されます

PS C:\Users\kuzushiki> & C:/Users/kuzushiki/AppData/Local/Programs/Python/Python37/python.exe c:/share/solve.py
b'pctf{th3_wh331s_0n_th3_tr41n_g0_r0und_4nd_r0und}'
b'pctfg\x7f!Bd\x7f!."dM-}Hfu Hfo\'&|Bt\'Mo#b|yL#|yLe"h}som'
b'pctfl6Pel6<#wZ?|[qg![q}&5kPu4Z}"qkkM0kkMv5z|`x\x7fq'
b'pctf%Gwf%+1}I(nQbp3Qbj4?xGg>Ij0{x|_:x|_|&mnjkhcz'
b"pctfT`tf8&o\n;yC!c$C!y#-;Tp,\ny'i;oH(;oHne~yx({th3"
:
:

フラグが出てきました! pctf{th3_wh331s_0n_th3_tr41n_g0_r0und_4nd_r0und}

Chugga Chugga (REV, 100 pts, 69 solves)

"I think I can. I think I can. I think I can. I know I can!"

They can. Can you?

問題ファイル: chugga

fileコマンドの結果は以下の通りです。

root@kali:/media/sf_share/CTF/bo1lers CTF/rev# file chugga
chugga: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), statically linked, Go BuildID=t_iW0Uowrq5QAERg05Sl/OsLe0r4GSD-eggXryF3i/uvJJhPJWDTRopyOSvtbf/IvJzDFAUciDYpwCfCrlV, not stripped

ELFなので実行ファイルだということが分かります。

実行してみましょう!

root@kali:/media/sf_share/CTF/bo1lers CTF/rev# ./chugga
We're in train car:  0
The door is locked, but luckily, you're the conductor! Input your code:

どうやらコードの入力を求められるようです。

root@kali:/media/sf_share/CTF/bo1lers CTF/rev# ./chugga
We're in train car:  0
The door is locked, but luckily, you're the conductor! Input your code: 
test
Boom! You are dead. You come back to life in the next car.
We're in train car:  1
The door is locked, but luckily, you're the conductor! Input your code: 

間違えてしまいましたが、何回でも入力できるようです。

正解のコードを調べるためにGhidraで静的解析を行います。

[function]->[main]->[mian.main]にてコードのチェックが行われていました。

Ghidraで逆コンパイルを行い、コードのチェック部分を抽出しました。

if (pcVar3 < (char *)0x3) break;
if (pcVar4[2] == 't') {
  if (pcVar3 < &DAT_0000000a) break;
  param_2 = (undefined1 *)(ulong)(byte)pcVar4[9];
  if (pcVar4[9] != 99) goto code_r0x00493067;
  if (pcVar3 < &DAT_00000011) break;
  param_1 = (long **)(ulong)(byte)pcVar4[0x10];
  if (pcVar4[0x10] != 0x6e) goto code_r0x00493067;
  if (pcVar3 < &DAT_00000016) break;
  param_5 = (long **)(ulong)(byte)pcVar4[0x15];
  if (pcVar4[0x15] != 0x7a) goto code_r0x00493067;
  if (pcVar3 < &DAT_00000017) break;
  if ((((pcVar4[0x16] != '}') || (param_6 = (long **)(ulong)(byte)pcVar4[5], pcVar4[5] != 0x73))
      || (pcVar4[3] != 'f')) ||
     (((pcVar4[1] != 'c' || (param_2 = (undefined1 *)(ulong)(byte)pcVar4[7], pcVar4[7] != 100))
      || ((pcVar4[0xc] != pcVar4[0xd] || (pcVar4[0x13] != 'z')))))) goto code_r0x00493067;
  bVar1 = pcVar4[0xe];
  param_5 = (long **)(ulong)bVar1;
  bVar2 = pcVar4[6];
  if ((((byte)(bVar2 + bVar1) != 'h') || (pcVar4[4] != '{')) ||
     ((pcVar4[0xf] != pcVar4[8] || (pcVar4[8] != '_')))) goto code_r0x00493067;
  bVar8 = pcVar4[0x11];
  if ((byte)(-0x5b - bVar8) != pcVar4[0xb]) goto code_r0x00493067;
  iVar6 = (uint)(byte)pcVar4[0x12] - (uint)bVar8;
  cVar5 = (char)iVar6;
  if ((byte)(((pcVar4[0xb] + -0x73) - pcVar4[0x12]) + bVar8) != cVar5) goto code_r0x00493067;
  uVar9 = (uint)bVar2 - (uint)bVar8;
  bVar8 = (byte)uVar9;
  if (((*pcVar4 != (byte)((bVar8 >> 1) * cVar5 + 'n')) ||
      ((char)(pcVar4[0xd] + '\x01') != pcVar4[10])) ||
     (param_2 = (undefined1 *)(ulong)uVar9, (byte)(bVar8 * '\x03' + '\\') != pcVar4[10]))
  goto code_r0x00493067;
  uVar9 = iVar6 * 2;
  if ((((char)((uint)(byte)pcVar4[0x14] - 99) != (char)uVar9) ||
      (param_6 = (long **)&DAT_0000001d, bVar8 != (byte)(cVar5 * '\x04'))) || (bVar2 != bVar1))
  goto code_r0x00493067;
  main.win(param_1,(int *)param_2,(ulong)((uint)(byte)pcVar4[0x14] - 99),(ulong)uVar9,param_5,
           (long **)&DAT_0000001d);

pcVar4が入力されたコードで、main.winがゴールのようです。

例えば、if (pcVar4[9] != 99)が真になると、main.winに行かなくなってしまうため、

pcVar4[9] == 99となれば良いことが分かります。

判定文の場所を抽出してみます。

pcVar4[2] == 't'
pcVar4[9] != 99
pcVar4[0x10] != 0x6e
pcVar4[0x15] != 0x7a
pcVar4[0x16] != '}'
pcVar4[5] != 0x73
pcVar4[3] != 'f'
pcVar4[1] != 'c'
pcVar4[7] != 100
pcVar4[0xc] != pcVar4[0xd]
pcVar4[0x13] != 'z'
(pcVar4[6] + pcVar4[0xe]) != 'h'
pcVar4[4] != '{'
pcVar4[0xf] != pcVar4[8]
pcVar4[8] != '_'
-0x5b - pcVar4[0x11] != pcVar4[0xb]
pcVar4[0xb] - 0x73 - pcVar4[0x12] + pcVar4[0x11] != pcVar4[0x12] - pcVar4[0x11]
pcVar4[0] != ((pcVar4[6] - pcVar4[0x11] >> 1) * pcVar4[0x12] - pcVar4[0x11] + 'n')
pcVar4[0xd] + '\x01' != pcVar4[10]
(pcVar4[6] - pcVar4[0x11] * '\x03' + '\\') != pcVar4[10]
pcVar4[0x14] - 99 != (pcVar4[0x12] - pcVar4[0x11]) * 2
pcVar4[6] - pcVar4[0x11] != (pcVar4[0x12] - pcVar4[0x11]) * '\x04'
pcVar4[6] != pcVar4[0xe]

これらの条件をすべて満たすpcVar4を求めれば良いのですが、

条件がかなり多く面倒だったので、z3というソルバを用いて解くことにします。

条件を指定してあげるとそれを満たす解を自動で求めてくれます。

プログラムは以下の通りです。

from z3 import *

# pcVar4[0x16] != '}'なのでフラグは0x17文字
flag = [BitVec("flag{:d}".format(i), 8)
        for i in range(0x17)]
s = Solver()

# 条件を列挙
s.add(flag[0] == ((flag[6] - flag[0x11] >> 1) * flag[0x12] - flag[0x11] + ord('n')))
s.add(flag[1] == ord("c"))
s.add(flag[2] == ord("t"))
s.add(flag[3] == ord("f"))
s.add(flag[4] == ord("{"))
s.add(flag[5] == 0x73)
s.add(flag[6] + flag[0xe] == ord('h'))
s.add(flag[6] == flag[0xe])
s.add(flag[6] - flag[0x11] == (flag[0x12] - flag[0x11]) * ord('\x04'))
s.add((flag[6] - flag[0x11] * ord('\x03') + ord('\\')) == flag[10])
s.add(flag[7] == 100)
s.add(flag[8] == ord("_"))
s.add(flag[9] == 99)
s.add(flag[0xb] - 0x73 - flag[0x12] + flag[0x11] == flag[0x12] - flag[0x11])
s.add(flag[0xc] == flag[0xd])
s.add(flag[0xd] + ord('\x01') == flag[10])
s.add(flag[0xf] == flag[8])
s.add(flag[0x10] == 0x6e)
s.add(-0x5b - flag[0x11] == flag[0xb])
s.add(flag[0x13] == ord('z'))
s.add(flag[0x14] - 99 == (flag[0x12] - flag[0x11]) * 2)
s.add(flag[0x15] == 0x7a)
s.add(flag[0x16] == ord('}'))

while True:
    answer = ['?' for i in range(0x17)]
    r = s.check()
    if r == sat:  # 与えた問題が充足可能かどうか
        m = s.model()
        for d in m.decls():
            answer[int(d.name().replace('flag', ''))] = chr(m[d].as_long())  # Z3からの型変換
        answer = ''.join(answer)
        print(answer)
        exit()
    else:
        print(r)

実行結果は以下の通りです。

PS C:\Users\kuzushiki> & C:/Users/kuzushiki/AppData/Local/Programs/Python/Python37/python.exe c:/Users/kuzushiki/Desktop/zsolve.py
 ctf{s4d_c uÿÿ4_n01zez}

なぜか解けませんでした…

以下の条件により、フラグの未知の文字を?*を用いて表すと、

フラグはpctf{s4d_c?u**4_n01zez}となります。

  1. フラグの一文字目はp
  2. フラグに ÿ含まれない
  3. flag[0xc] == flag[0xd]

未知の文字はたった2種類なので、総当たりで解きましょう

前述のように、chuggaは何度でも入力を試せます。

プログラムは以下の通りです。

import string
from pwn import *
io = process('chugga')
io.recv()
flag = list('pctf{s4d_c?u**4_n01zez}')
mystring = string.ascii_letters + string.digits + '_'
for s1 in mystring:
    flag[10] = s1
    for s2 in mystring:
        flag[12] = s2
        flag[13] = s2
        gflag = ''.join(flag)
        io.sendline(gflag)
        recv = io.recv()
        print(recv)
        if (b'Boom!' not in recv) and (b'in train car' not in recv) and (b'locked' not in recv):
            print(gflag)
            exit()

実行するとフラグが分かりました!!

[!] Could not find executable 'chugga' in $PATH, using './chugga' instead
[+] Starting local process './chugga': pid 1843
b"Boom! You are dead. You come back to life in the next car.\nWe're in train car:  1\nThe door is locked, but luckily, you're the conductor! Input your code: \n"
b"Boom! You are dead. You come back to life in the next car.\nWe're in train car:  2\nThe door is locked, but luckily, you're the conductor! Input your code: \n"

:
(省略)
:

b"Boom! You are dead. You come back to life in the next car.\nWe're in train car:  447\nThe door is locked, but luckily, you're the conductor! Input your code: \n"
b"You've done it! You've saved the train!\n"
pctf{s4d_chugg4_n01zez}

pctf{s4d_chugg4_n01zez}

Welcome to Earth (WEB, 100 pts, 419 Solves)

This was supposed to be my weekend off, but noooo, you got me out here, draggin' your heavy ass through the burning desert, with your dreadlocks sticking out the back of my parachute. You gotta come down here with an attitude, actin' all big and bad. And what the hell is that smell? I coulda been at a barbecue, but I ain't mad.

問題ページ

まるでゲームブックのような問題でした。

問題ページにアクセスすると、「AMBUSH!」と表示されます。

VirtualBox_Kali-Linux-2019 2-vbox-amd64_16_03_2020_17_13_36

そのまま放置していると/die/に飛ばされ、死んでしまいます。

VirtualBox_Kali-Linux-2019 2-vbox-amd64_16_03_2020_17_14_37

トップページのソースコードを確認してみます

<!DOCTYPE html>
<html>
  <head>
    <title>Welcome to Earth</title>
  </head>
  <body>
    <h1>AMBUSH!</h1>
    <p>You've gotta escape!</p>
    <img src="/static/img/f18.png" alt="alien mothership" style="width:60vw;" />
    <script>
      document.onkeydown = function(event) {
        event = event || window.event;
        if (event.keyCode == 27) {
          event.preventDefault();
          window.location = "/chase/";
        } else die();
      };

      function sleep(ms) {
        return new Promise(resolve => setTimeout(resolve, ms));
      }

      async function dietimer() {
        await sleep(10000);
        die();
      }

      function die() {
        window.location = "/die/";
      }

      dietimer();
    </script>
  </body>
</html>

window.location = "/chase/";の部分から、/chase/にアクセスすれば良いことが分かります。

/chase/にアクセスすると、一瞬何かが表示されますが、すぐに/die/に飛ばされてしまいます。

/chase/のソースコードを確認してみましょう

<!DOCTYPE html>
<html>
  <head>
    <title>Welcome to Earth</title>
  </head>
  <body>
    <h1>CHASE!</h1>
    <p>
      You managed to chase one of the enemy fighters, but there's a wall coming
      up fast!
    </p>
    <button onclick="left()">Left</button>
    <button onclick="right()">Right</button>

    <img
      src="/static/img/Canyon_Chase_16.png"
      alt="canyon chase"
      style="width:60vw;"
    />
    <script>
      function sleep(ms) {
        return new Promise(resolve => setTimeout(resolve, ms));
      }

      async function dietimer() {
        await sleep(1000);
        die();
      }

      function die() {
        window.location = "/die/";
      }

      function left() {
        window.location = "/die/";
      }

      function leftt() {
        window.location = "/leftt/";
      }

      function right() {
        window.location = "/die/";
      }

      dietimer();
    </script>
  </body>
</html>

window.location = "/leftt/";と書かれているので、/leftt/にアクセスしてみましょう

VirtualBox_Kali-Linux-2019 2-vbox-amd64_16_03_2020_17_20_50

take the shootと書かれたボタンがありあますが、ここをクリックすると/die/に飛ばされてしまいます。

/leftt/のソースコードを確認しましょう

<!DOCTYPE html>
<html>
  <head>
    <title>Welcome to Earth</title>
  </head>
  <body>
    <h1>SHOOT IT</h1>
    <p>You've got the bogey in your sights, take the shot!</p>
    <img
      src="/static/img/locked.png"
      alt="locked on"
      style="width:60vw;"
    />
    </br>
    <button onClick="window.location='/die/'">Take the shot</button>
    <!-- <button onClick="window.location='/shoot/'">Take the shot</button> -->
  </body>
</html>

コメントアウトされている部分に"window.location='/shoot/'"との記述があります。

/shoot/にアクセスしてみましょう。

VirtualBox_Kali-Linux-2019 2-vbox-amd64_16_03_2020_17_23_43

右下にあるcontinueと書かれたボタンを押すと、次のステージである/door/に進みます。

VirtualBox_Kali-Linux-2019 2-vbox-amd64_16_03_2020_17_24_10

大量のラジオボタンからたった1つの正解を見つけなくてはならないようです。

とりあえずソースコードを見てみましょう

<!DOCTYPE html>
<html>
  <head>
    <title>Welcome to Earth</title>
    <script src="/static/js/door.js"></script>
  </head>
  <body>
    <h1>YOU APPROACH THE ALIEN CRAFT!</h1>
    <p>How do you get inside?</p>
    <img src="/static/img/ship.png" alt="crashed ship" style="width:60vw;" />
    <form id="door_form">
      <input type="radio" name="side" value="0" />0
      <input type="radio" name="side" value="1" />1
      <input type="radio" name="side" value="2" />2
        :
        (長いので省略)
        :
      <input type="radio" name="side" value="357" />357
      <input type="radio" name="side" value="358" />358
      <input type="radio" name="side" value="359" />359
    </form>
    <button onClick="check_door()">Check</button>
  </body>
</html>

src="/static/js/door.js"内のcheck_door()という関数で正解か判定しているのが分かります。

"/static/js/door.js"を見てみましょう。

function check_door() {
  var all_radio = document.getElementById("door_form").elements;
  var guess = null;

  for (var i = 0; i < all_radio.length; i++)
    if (all_radio[i].checked) guess = all_radio[i].value;

  rand = Math.floor(Math.random() * 360);
  if (rand == guess) window.location = "/open/";
  else window.location = "/die/";
}

正解はランダムに変わるようです。

1/360を引くのは至難の技です。

(実はCTF中に引けました)

正解なら/open/に飛ぶみたいなので、/open/にアクセスしましょう。

VirtualBox_Kali-Linux-2019 2-vbox-amd64_16_03_2020_17_32_25

ページを見渡しても何も見つからないので、ソースコードを見てみましょう。

<!DOCTYPE html>
<html>
  <head>
    <title>Welcome to Earth</title>
    <script src="/static/js/open_sesame.js"></script>
  </head>
  <body>
    <h1>YOU FOUND THE DOOR!</h1>
    <p>How do you open it?</p>
    <img src="/static/img/door.jpg" alt="door" style="width:60vw;" />
    <script>
      open(0);
    </script>
  </body>
</html>

src="/static/js/open_sesame.js"というファイルが存在するようです。

/static/js/open_sesame.jsにアクセスしてみましょう。

function sleep(ms) {
  return new Promise(resolve => setTimeout(resolve, ms));
}

function open(i) {
  sleep(1).then(() => {
    open(i + 1);
  });
  if (i == 4000000000) window.location = "/fight/";
}

4000000秒(約46日!)待てば/fight/に遷移するようですが、そんなに待てないのでこちらから/fight/にアクセスしましょう。

VirtualBox_Kali-Linux-2019 2-vbox-amd64_16_03_2020_17_38_31

いよいよ最終決戦です。

エイリアンを倒すために下部のフォームに文字を入力し、Fight!ボタンを押しても、何も起こりません。

ソースコードを見てみましょう。

<!DOCTYPE html>
<html>
  <head>
    <title>Welcome to Earth</title>
    <script src="/static/js/fight.js"></script>
  </head>
  <body>
    <h1>AN ALIEN!</h1>
    <p>What do you do?</p>
    <img
      src="/static/img/alien.png"
      alt="door"
      style="width:60vw;"
    />
    </br>
    <input type="text" id="action">
    <button onClick="check_action()">Fight!</button>
  </body>
</html>

/static/js/fight.js内のcheck_action()にて判定が行われるようです。

/static/js/fight.jsにアクセスしてみましょう。

// Run to scramble original flag
//console.log(scramble(flag, action));
function scramble(flag, key) {
  for (var i = 0; i < key.length; i++) {
    let n = key.charCodeAt(i) % flag.length;
    let temp = flag[i];
    flag[i] = flag[n];
    flag[n] = temp;
  }
  return flag;
}

function check_action() {
  var action = document.getElementById("action").value;
  var flag = ["{hey", "_boy", "aaaa", "s_im", "ck!}", "_baa", "aaaa", "pctf"];

  // TODO: unscramble function
}

フラグは["{hey", "_boy", "aaaa", "s_im", "ck!}", "_baa", "aaaa", "pctf"]を正しく並び替えたものだと推測できます。

pctf{hey*****ck!}の部分は確定なので、自然な文章になるように残りの文字列を当てはめていきます。

フラグが分かりました!!

pctf{hey_boys_im_baaaaaaaaaack!}

Welcome! (A WELCOME CHALLENGE, 1 pts, 567 Solves)

Welcome to b01lers CTF! A couple of things:

Unless otherwise specified, the flag format is pctf{...}.

This is a 48-Hour CTF. Details can be found at https://ctf.b01lers.com.

pctf{all_ur_hack_are_belong_to_us}

問題文に書かれているフラグをそのまま入れましょう

pctf{all_ur_hack_are_belong_to_us}

Discord Flag (A WELCOME CHALLENGE, 1 pts, 422 Solves)

Join our discord! discord.gg/tBMqujE

大会のdiscordに参加しましょう。

pctfで検索すると、フラグが見つかりました。

pctf{all_ur_hack_are_belong_to_us}

Survey (A WELCOME CHALLENGE, 1 pts, 166 Solves)

大会のアンケートに回答するとフラグがもらえます。

(回答しなくてもソースコードにフラグが書かれています)

pctf{s33_y0u_n3xT_Y34r_ch0o_chO0}

感想

「初心者向け」とのことでしたが、あまり解けませんでした。

公式write upが投稿されたらそれを見て復習します。