#!/usr/bin/env python3 import os import random imp...

Tạo vào: 27 tháng 12, 2025

Trả lời bằng GPT-5.2 Thinking bởi Chat01

Câu hỏi

#!/usr/bin/env python3
import os
import random
import socket
import socketserver
import argparse

def load_flag():
for p in ("/flag", "./flag.txt"):
if os.path.exists(p):
with open(p, "r", encoding="utf-8") as f:
return f.read().strip()
return "flag{test_flag}"

FLAG = load_flag()

class CathylinFourPlus:
def init(self, r, key):
self.r = r
self.S = (0xC, 0x5, 0x6, 0xB,
0x9, 0x0, 0xA, 0xD,
0x3, 0xE, 0xF, 0x8,
0x4, 0x7, 0x1, 0x2)
self.P = (0, 5, 10, 15,
4, 9, 14, 3,
8, 13, 2, 7,
12, 1, 6, 11)
self.RC = (0x0F0F, 0x3333, 0x55AA)
self.key = key
assert len(key) == self.r + 1

text
@staticmethod def deblock(m): return [(m >> 12) & 0xF, (m >> 8) & 0xF, (m >> 4) & 0xF, m & 0xF] @staticmethod def block(nibbles): return (nibbles[0] << 12) | (nibbles[1] << 8) | (nibbles[2] << 4) | nibbles[3] def s_layer(self, x): n = self.deblock(x) n = [self.S[i] for i in n] return self.block(n) def permute_bits(self, x): bits = [(x >> (15 - i)) & 1 for i in range(16)] new_bits = [0] * 16 for i in range(16): new_bits[self.P[i]] = bits[i] out = 0 for b in new_bits: out = (out << 1) | b return out & 0xFFFF def encrypt(self, m): p = m & 0xFFFF for t in range(self.r - 1): p ^= self.key[t] p ^= self.RC[t] p = self.s_layer(p) p = self.permute_bits(p) p ^= self.key[self.r - 1] p ^= self.RC[self.r - 1] p = self.s_layer(p) c = p ^ self.key[-1] return c & 0xFFFF

def parse_hex16(s: str) -> int:
s = s.strip().lower()
if s.startswith("0x"):
s = s[2:]
if not (1 <= len(s) <= 4) or any(ch not in "0123456789abcdef" for ch in s):
raise ValueError("bad hex")
return int(s, 16) & 0xFFFF

def serve_one_client(rfile, wfile):
# 每连接随机 key / mask
r = 3
key = [random.getrandbits(16) for _ in range(r + 1)]
enc = CathylinFourPlus(r, key)
mask = random.getrandbits(16)

text
def wln(msg=""): wfile.write((msg + "\n").encode()) wfile.flush() def write_prompt(): wfile.write(b"Your input: ") wfile.flush() wln("=== CathylinFour++ (SPN 16-bit) ===") wln("Input: hex (e.g. 0x1234). Commands: h/help, k, q") wln(f"mask = 0x{mask:04x}") wln("Goal: submit (K3 ^ mask) to get the flag.") wln("----------------------------------") qcnt = 0 QMAX = 4096 while True: write_prompt() line = rfile.readline() if not line: break x = line.decode(errors="ignore").strip().lower() if x in ("q", "quit", "exit"): wln("Bye.") break if x in ("h", "help", "?"): wln("Commands:") wln(" <hex> encrypt a 16-bit block, e.g. 0x1234") wln(" k submit key proof (K3 ^ mask)") wln(" q quit") continue if x == "k": wfile.write(b"Your key proof (hex): ") wfile.flush() ki = rfile.readline() if not ki: break try: proof = parse_hex16(ki.decode(errors="ignore")) except Exception: wln("Bad format. Bye.") break if proof == (key[-1] ^ mask): wln("You can smile: " + FLAG) else: wln("Wrong. Bye.") break try: a = parse_hex16(x) except Exception: wln("Bad format. Tips: 0x???? (1~4 hex digits).") continue qcnt += 1 if qcnt > QMAX: wln("Too many queries. Bye.") break c = enc.encrypt(a) wln("Output: " + f"0x{c:04x}")

class ThreadedTCPServer(socketserver.ThreadingMixIn, socketserver.TCPServer):
allow_reuse_address = True
daemon_threads = True

class Handler(socketserver.BaseRequestHandler):
def handle(self):
# 每连接超时:防止挂死占用线程
self.request.settimeout(60)
rfile = self.request.makefile("rb", buffering=0)
wfile = self.request.makefile("wb", buffering=0)
try:
serve_one_client(rfile, wfile)
except (socket.timeout, BrokenPipeError, ConnectionResetError):
pass
finally:
try:
rfile.close()
except Exception:
pass
try:
wfile.close()
except Exception:
pass

def main():
ap = argparse.ArgumentParser()
ap.add_argument("--listen", default=None, help="listen address, e.g. 0.0.0.0")
ap.add_argument("--port", type=int, default=37344)
args = ap.parse_args()

text
if args.listen: with ThreadedTCPServer((args.listen, args.port), Handler) as srv: srv.serve_forever() else: # 本地 stdin/stdout 单机调试模式 import sys serve_one_client(sys.stdin.buffer, sys.stdout.buffer)

if name == "main":
main() 这是一道ctf 的crypto题目 提示:此题不是正常flag

Python

python
def dist_after_two(delta): cnt=defaultdict(int) for x in range(0x10000): y=two_rounds(x) y2=two_rounds(x^delta) cnt[y^y2]+=1 return cnt # candidate deltas: single nonzero nibble cands=[] for pos in range(4): shift=12-4*pos for val in range(1,16): cands.append(val<<shift) len(cands) 60

Python

python
# precompute f f=[0]*0x10000 for x in range(0x10000): f[x]=two_rounds(x) # function dist def dist_after_two_fast(delta): cnt=defaultdict(int) for x in range(0x10000): cnt[f[x]^f[x^delta]] += 1 return cnt # compute best for those deltas based on max count and active nibbles best_info=[] for delta in cands: cnt=dist_after_two_fast(delta) # find top outcomes top=sorted(cnt.items(), key=lambda kv: kv[1], reverse=True)[:5] # pick best outcome with minimal active nibbles maybe top_best=min(top, key=lambda kv: (active_nibbles(kv[0]), -kv[1])) best_info.append((delta, top_best[0], top_best[1], active_nibbles(top_best[0]), top[:3])) # sort by count desc and active nibbles small best_info_sorted=sorted(best_info, key=lambda t: (t[3], -t[2]))[:20] best_info_sorted[:10] [(28672, 528, 4096, 2, [(528, 4096), (24, 4096), (16408, 4096)]), (36864, 4160, 4096, 2, [(4160, 4096), (260, 2048), (4162, 2048)]), (53248, 260, 4096, 2, [(260, 4096), (528, 2048), (24, 2048)]), (61440, 4160, 4096, 2, [(4160, 4096), (528, 4096), (24, 4096)]), (1792, 33, 4096, 2, [(33, 4096), (33793, 4096), (1057, 4096)]), (2304, 260, 4096, 2, [(260, 4096), (16400, 2048), (8452, 2048)]), (3328, 16400, 4096, 2, [(16400, 4096), (33, 2048), (33793, 2048)]), (3840, 260, 4096, 2, [(260, 4096), (33, 4096), (33793, 4096)]), (112, 4098, 4096, 2, [(4098, 4096), (6144, 4096), (4162, 4096)]), (144, 16400, 4096, 2, [(16400, 4096), (1025, 2048), (16912, 2048)])]

Python

python
# check if any candidate delta has deterministic output diff after 2 rounds det=[] for delta in cands: cnt=dist_after_two_fast(delta) if len(cnt)==1: det.append((delta, next(iter(cnt.items())))) len(det), det[:5] (0, [])

Python

python
def best_with_active(delta, act): cnt=dist_after_two_fast(delta) items=[(d,c) for d,c in cnt.items() if active_nibbles(d)==act and d!=0] if not items: return None return max(items, key=lambda kv: kv[1]) best_act1=[] for delta in cands: b=best_with_active(delta,1) if b: best_act1.append((delta,b[0],b[1])) sorted(best_act1, key=lambda t: -t[2])[:10], len(best_act1) ([], 0)

Python

python
def nibble_positions(diff): # positions 0..3 from MS nibble to LS pos=[] for i in range(4): if ((diff>>(12-4*i))&0xF)!=0: pos.append(i) return tuple(pos) cand_data=[] for delta in cands: cnt=dist_after_two_fast(delta) # get maximum count maxc=max(cnt.values()) # get deltas with maxc tops=[d for d,c in cnt.items() if c==maxc] # pick one with minimal active_nibbles and then smallest tops_sorted=sorted(tops, key=lambda d:(active_nibbles(d), d)) deltaX=tops_sorted[0] cand_data.append((delta, deltaX, maxc, active_nibbles(deltaX), nibble_positions(deltaX), tops_sorted[:5])) # filter those with maxc high sorted(cand_data, key=lambda t: -t[2])[:10] [(28672, 24, 4096, 2, (2, 3), [24, 528, 16408, 16912]), (36864, 4160, 4096, 2, (0, 2), [4160]), (45056, 32801, 4096, 3, (0, 2, 3), [32801, 33825]), (53248, 260, 4096, 2, (1, 3), [260]), (61440, 24, 4096, 2, (2, 3), [24, 528, 4160, 16408, 16912]), (1792, 33, 4096, 2, (2, 3), [33, 32769, 1057, 33793]), (2304, 260, 4096, 2, (1, 3), [260]), (2816, 6146, 4096, 3, (0, 1, 3), [6146, 6210]), (3328, 16400, 4096, 2, (0, 2), [16400]), (3840, 33, 4096, 2, (2, 3), [33, 260, 32769, 1057, 33793])]

Python

python
# compute DDT for S ddt=[[0]*16 for _ in range(16)] for a in range(16): for x in range(16): b=S[x]^S[x^a] ddt[a][b]+=1 # show best output diff for input diff a best_out={} for a in range(1,16): row=ddt[a] maxc=max(row) outs=[b for b,c in enumerate(row) if c==maxc] best_out[a]=(outs,maxc) best_out[1], best_out[4] (([3, 7, 9, 13], 4), ([5], 4))

Python

python
import random, math RC = [0x0F0F,0x3333,0x55AA] def encrypt_full(m, key): r=3 p=m&0xFFFF # rounds 0,1 for t in range(r-1): p ^= key[t] p ^= RC[t] p = s_layer(p) p = permute_bits(p) # last round p ^= key[r-1] p ^= RC[r-1] p = s_layer(p) c = p ^ key[-1] return c&0xFFFF INV = inv_sbox() def inv_s_layer(x): out=0 for i in range(4): nib=(x>>(12-4*i))&0xF out=(out<<4)|INV[nib] return out # scoring function for a deltaP and expected deltaX def recover_nibbles(key, deltaP, expected_deltaX, positions, Npairs=2048): # positions list of tuples: (pos, expected_nibble_diff) scores={pos:[0]*16 for pos,_ in positions} # build Npairs random base plaintexts for _ in range(Npairs): m=random.getrandbits(16) m2=m^deltaP c=encrypt_full(m,key) c2=encrypt_full(m2,key) for pos,ed in positions: shift=12-4*pos cn=(c>>shift)&0xF cn2=(c2>>shift)&0xF for kguess in range(16): x=INV[cn^kguess] x2=INV[cn2^kguess] if (x^x2)==ed: scores[pos][kguess]+=1 # pick best best={} for pos,_ in positions: arr=scores[pos] mx=max(arr); bestk=arr.index(mx) # compute margin sorted_arr=sorted(arr, reverse=True) best[pos]=(bestk,mx,sorted_arr[1]) return best # run multiple trials def trial(Npairs=2048): key=[random.getrandbits(16) for _ in range(4)] K3=key[3] # attack1 deltaP1=0x9000 expX1=0x1040 b1 = recover_nibbles(key, deltaP1, expX1, [(0,0x1),(2,0x4)], Npairs) # attack2 deltaP2=0xD000 expX2=0x0104 b2 = recover_nibbles(key, deltaP2, expX2, [(1,0x1),(3,0x4)], Npairs) rec_k0=b1[0][0]; rec_k2=b1[2][0]; rec_k1=b2[1][0]; rec_k3=b2[3][0] recK3=(rec_k0<<12)|(rec_k1<<8)|(rec_k2<<4)|rec_k3 ok = (recK3==K3) margins=(b1[0][1]-b1[0][2], b1[2][1]-b1[2][2], b2[1][1]-b2[1][2], b2[3][1]-b2[3][2]) return ok, margins, K3, recK3, b1, b2 # test success rate succ=0 trials=50 margins=[] for _ in range(trials): ok, mar, *_ = trial(2048) succ += ok margins.append(mar) succ, trials (12, 50)

Python

python
from collections import Counter def dist_two(delta): cnt=Counter() for x in range(0x10000): cnt[f[x]^f[x^delta]] += 1 return cnt cnt9000=dist_two(0x9000) len(cnt9000), cnt9000.most_common(10)[:5] (405, [(4160, 4096), (260, 2048), (4162, 2048), (2114, 2048), (6144, 2048)])

Python

python
def marginal_nibble_dist(delta, pos): cnt=Counter() for x in range(0x10000): dx=f[x]^f[x^delta] nib=(dx>>(12-4*pos))&0xF cnt[nib]+=1 return cnt m0 = marginal_nibble_dist(0x9000,0) m2 = marginal_nibble_dist(0x9000,2) m0, m2 (Counter({0: 18432, 1: 12288, 8: 12288, 2: 7168, 9: 6144, 3: 3072, 11: 3072, 10: 3072}), Counter({4: 18432, 0: 12288, 6: 9216, 2: 9216, 8: 5632, 12: 4608, 14: 4608, 10: 1536}))

Python

python
import numpy as np, random # precompute inv_s_layer via table for 16-bit? inv_s_layer_table = np.zeros(0x10000, dtype=np.uint16) for x in range(0x10000): inv_s_layer_table[x]=inv_s_layer(x) # brute scoring function def brute_k3(pairs, expected_dx): # pairs: list of (c,c2) # returns best k3 guess bestk=None; bestscore=-1 for k in range(0x10000): score=0 # compute per pair for c,c2 in pairs: x=inv_s_layer_table[c^k] x2=inv_s_layer_table[c2^k] if (x^x2)==expected_dx: score += 1 if score>bestscore: bestscore=score; bestk=k return bestk, bestscore def trial_brute(Npairs=1024): key=[random.getrandbits(16) for _ in range(4)] K3=key[3] delta=0x9000 expected_dx=0x1040 # from earlier pairs=[] for _ in range(Npairs): m=random.getrandbits(16) c=encrypt_full(m,key) c2=encrypt_full(m^delta,key) pairs.append((c,c2)) guess,score=brute_k3(pairs, expected_dx) return guess==K3, score, K3, guess succ=0 tr=20 for _ in range(tr): ok,score, K3,guess = trial_brute(256) # fewer to speed succ+=ok succ,tr

Python

python
ddt[1] [0, 0, 0, 4, 0, 0, 0, 4, 0, 4, 0, 0, 0, 4, 0, 0]

Python

python
ddt[4] [0, 0, 0, 0, 0, 4, 2, 2, 0, 2, 2, 0, 2, 0, 2, 0]

Python

python
allowed_out = {1:set([3,7,9,13]), 4:set([5,6,7,9,10,12,14])} def attack_with_filter(key, deltaP, expected_dx, active_pos, Npairs=2048, use_out_filter=True): # active_pos list [(pos, expected_input_diff)] ; expected_dx defines these anyway # inactive positions are others scores={pos:[0]*16 for pos,_ in active_pos} pairs_kept=0 for _ in range(Npairs): m=random.getrandbits(16) c=encrypt_full(m,key) c2=encrypt_full(m^deltaP,key) cd = c^c2 # filter: inactive nibbles in ciphertext diff must be 0 ok=True for pos in range(4): if pos not in [p for p,_ in active_pos]: if ((cd>>(12-4*pos))&0xF)!=0: ok=False; break if not ok: continue if use_out_filter: for pos,edin in active_pos: outd = (cd>>(12-4*pos))&0xF if outd not in allowed_out[edin]: ok=False; break if not ok: continue pairs_kept += 1 for pos,edin in active_pos: shift=12-4*pos cn=(c>>shift)&0xF cn2=(c2>>shift)&0xF for kguess in range(16): x=INV[cn^kguess] x2=INV[cn2^kguess] if (x^x2)==edin: scores[pos][kguess]+=1 best={} for pos,_ in active_pos: arr=scores[pos] mx=max(arr); bestk=arr.index(mx) best[pos]=(bestk, mx, sorted(arr, reverse=True)[1]) return best, pairs_kept def trial_filtered(Npairs=2048): key=[random.getrandbits(16) for _ in range(4)] K3=key[3] # attack1 deltaP1=0x9000 active1=[(0,1),(2,4)] b1, kept1 = attack_with_filter(key, deltaP1, 0x1040, active1, Npairs, use_out_filter=True) # attack2 deltaP2=0xD000 active2=[(1,1),(3,4)] b2, kept2 = attack_with_filter(key, deltaP2, 0x0104, active2, Npairs, use_out_filter=True) rec=(b1[0][0]<<12)|(b2[1][0]<<8)|(b1[2][0]<<4)|b2[3][0] ok=(rec==K3) return ok, (kept1, kept2), K3, rec, b1, b2 succ=0 tr=50 kepts=[] for _ in range(tr): ok, kept, *_ = trial_filtered(2048) succ += ok kepts.append(kept) succ, tr, sum(k[0] for k in kepts)/tr, sum(k[1] for k in kepts)/tr (12, 50, 129.24, 141.54)

Python

python
# gather some sample margins def test_margins(trials=20,Npairs=2048): stats=[] for _ in range(trials): ok, kept, K3, rec, b1, b2 = trial_filtered(Npairs) mar = (b1[0][1]-b1[0][2], b1[2][1]-b1[2][2], b2[1][1]-b2[1][2], b2[3][1]-b2[3][2]) stats.append((ok, kept, mar, K3, rec, b1, b2)) return stats stats=test_margins(10,2048) [(s[0],s[1],s[2]) for s in stats] [(False, (134, 136), (0, 77, 0, 77)), (False, (119, 135), (0, 66, 0, 71)), (True, (129, 131), (0, 80, 0, 75)), (True, (160, 119), (0, 92, 0, 71)), (False, (136, 135), (0, 88, 0, 80)), (False, (133, 137), (0, 80, 0, 68)), (False, (155, 142), (0, 89, 0, 82)), (False, (115, 134), (0, 64, 0, 68)), (False, (161, 125), (0, 84, 0, 80)), (True, (110, 152), (0, 74, 0, 88))]

Python

python
# For each possible ciphertext diff d, see for which input diff a invS(y)^invS(y^d)==a for all y def constant_diff_under_invs(d): vals=set() for y in range(16): vals.add(INV[y]^INV[y^d]) return vals for d in [3,7,9,13]: print(d, constant_diff_under_invs(d))

Python

python
def recover_nibble_from_pairs(pairs, pos, expected_in_diff, out_filter=None, inactive_positions=None): # pairs: list of (c,c2) # out_filter: set of allowed output diff values at pos, or None for no filter scores=[0]*16 kept=0 for c,c2 in pairs: cd=c^c2 if inactive_positions: ok=True for ip in inactive_positions: if ((cd>>(12-4*ip))&0xF)!=0: ok=False; break if not ok: continue if out_filter is not None: outd=(cd>>(12-4*pos))&0xF if outd not in out_filter: continue kept += 1 shift=12-4*pos cn=(c>>shift)&0xF cn2=(c2>>shift)&0xF for kguess in range(16): if (INV[cn^kguess]^INV[cn2^kguess])==expected_in_diff: scores[kguess]+=1 mx=max(scores); best=scores.index(mx) second=sorted(scores, reverse=True)[1] return best, mx, second, kept, scores def simulate_attack(Npairs_each=1024, trials=50, b_for_diff1=3, b_for_diff4=5): succ=0 kept_stats=[] for _ in range(trials): key=[random.getrandbits(16) for _ in range(4)] K3=key[3] # collect pairs for delta1 delta1=0x9000 pairs1=[] for _ in range(Npairs_each): m=random.getrandbits(16) pairs1.append((encrypt_full(m,key), encrypt_full(m^delta1,key))) # collect pairs for delta2 delta2=0xD000 pairs2=[] for _ in range(Npairs_each): m=random.getrandbits(16) pairs2.append((encrypt_full(m,key), encrypt_full(m^delta2,key))) # recover k0 using out diff b_for_diff1 at nibble0 and inactive nibbles [1,3] zero (we won't constrain nibble2) k0, s0, s0b, kept0,_ = recover_nibble_from_pairs(pairs1, pos=0, expected_in_diff=1, out_filter={b_for_diff1}, inactive_positions=[1,3]) # recover k2 using out diff b_for_diff4 at nibble2 and inactive [1,3] zero k2, s2, s2b, kept2,_ = recover_nibble_from_pairs(pairs1, pos=2, expected_in_diff=4, out_filter={b_for_diff4}, inactive_positions=[1,3]) # recover k1 using out diff b_for_diff1 at nibble1 and inactive [0,2] zero k1, s1, s1b, kept1,_ = recover_nibble_from_pairs(pairs2, pos=1, expected_in_diff=1, out_filter={b_for_diff1}, inactive_positions=[0,2]) # recover k3 using out diff b_for_diff4 at nibble3 and inactive [0,2] zero k3n, s3, s3b, kept3,_ = recover_nibble_from_pairs(pairs2, pos=3, expected_in_diff=4, out_filter={b_for_diff4}, inactive_positions=[0,2]) rec=(k0<<12)|(k1<<8)|(k2<<4)|k3n ok=(rec==K3) succ += ok kept_stats.append((kept0,kept2,kept1,kept3,s0-s0b,s2-s2b,s1-s1b,s3-s3b)) return succ/trials, kept_stats rate, stats = simulate_attack(1024, trials=50) rate, stats[0] (0.0, (17, 20, 15, 34, 0, 0, 0, 0))

Python

python
def check_key_invariance(a,b): # for each d, compute count of x s.t. S(x)^S(x^a)=b and INV[S(x)^d]^INV[S(x^a)^d]==a res={} for d in range(16): cnt=0 total=0 for x in range(16): if (S[x]^S[x^a])==b: total += 1 if (INV[S[x]^d]^INV[S[x^a]^d])==a: cnt += 1 res[d]=(cnt,total) return res res=check_key_invariance(1,3) res {0: (4, 4), 1: (0, 4), 2: (0, 4), 3: (4, 4), 4: (0, 4), 5: (4, 4), 6: (4, 4), 7: (0, 4), 8: (0, 4), 9: (0, 4), 10: (0, 4), 11: (0, 4), 12: (0, 4), 13: (0, 4), 14: (0, 4), 15: (0, 4)}

Python

python
for b in [3,7,9,13]: res=check_key_invariance(1,b) good=[d for d,(cnt,total) in res.items() if cnt==total and total>0] print("b",b,"good d",good)

Python

python
def good_d_for_a(a): goods=[] for b in range(16): # consider b that occurs if ddt[a][b]==0: continue res=check_key_invariance(a,b) good=[d for d,(cnt,total) in res.items() if total>0 and cnt==total] goods.append(set(good)) if not goods: return None inter=set.intersection(*goods) return inter, goods for a in range(1,16): inter,_=good_d_for_a(a) print(a, inter)

Python

python
def top_deltaX_with_positions(deltaP, positions=(0,2), forbid_diffs=set([1,15]), topn=10): cnt=dist_two(deltaP) cand=[] for dx,c in cnt.items(): if nibble_positions(dx)==positions: # check forbid on first pos? We'll check all active nibbles diffs good=True for pos in positions: diff=(dx>>(12-4*pos))&0xF if diff in forbid_diffs: good=False; break if good: cand.append((dx,c)) cand_sorted=sorted(cand, key=lambda kv:-kv[1])[:topn] return cand_sorted top_deltaX_with_positions(0x9000,(0,2))[:10], len(top_deltaX_with_positions(0x9000,(0,2), topn=100)) ([(8320, 1024), (12480, 256)], 2)

Python

python
def top_dx_positions_any(deltaP, positions, forbid={1,15}, topn=10): cnt=dist_two(deltaP) cand=[] for dx,c in cnt.items(): if nibble_positions(dx)==positions: good=True for pos in positions: diff=(dx>>(12-4*pos))&0xF if diff in forbid: good=False; break if good: cand.append((dx,c)) cand=sorted(cand, key=lambda kv:-kv[1])[:topn] return cand top_dx_positions_any(0xD000,(1,3)) [(2050, 1024)]

Python

python
def simulate_new_trails(Npairs_each=1024, trials=50): succ=0 info=[] for _ in range(trials): key=[random.getrandbits(16) for _ in range(4)] K3=key[3] # deltaP1=0x9000 target dx1=0x2080 (pos0 diff2, pos2 diff8) delta1=0x9000 pairs1=[] for _ in range(Npairs_each): m=random.getrandbits(16) pairs1.append((encrypt_full(m,key), encrypt_full(m^delta1,key))) # deltaP2=0xD000 target dx2=0x0802 (pos1 diff8, pos3 diff2) delta2=0xD000 pairs2=[] for _ in range(Npairs_each): m=random.getrandbits(16) pairs2.append((encrypt_full(m,key), encrypt_full(m^delta2,key))) # recover nibble0 with expected diff2, filter inactive nibbles 1,3 output diff 0 k0, s0, s0b, kept0,_ = recover_nibble_from_pairs(pairs1, pos=0, expected_in_diff=2, out_filter=None, inactive_positions=[1,3]) # recover nibble2 with expected diff8, filter inactive 1,3 k2, s2, s2b, kept2,_ = recover_nibble_from_pairs(pairs1, pos=2, expected_in_diff=8, out_filter=None, inactive_positions=[1,3]) # recover nibble1 expected diff8, filter inactive 0,2 k1, s1, s1b, kept1,_ = recover_nibble_from_pairs(pairs2, pos=1, expected_in_diff=8, out_filter=None, inactive_positions=[0,2]) # recover nibble3 expected diff2, filter inactive 0,2 k3n, s3, s3b, kept3,_ = recover_nibble_from_pairs(pairs2, pos=3, expected_in_diff=2, out_filter=None, inactive_positions=[0,2]) rec=(k0<<12)|(k1<<8)|(k2<<4)|k3n ok=(rec==K3) succ += ok info.append((ok, (kept0,kept2,kept1,kept3), (s0-s0b,s2-s2b,s1-s1b,s3-s3b), K3, rec)) return succ/trials, info rate, info = simulate_new_trails(1024, trials=50) rate, info[0] (0.0, (False, (98, 98, 91, 91), (4, 1, 14, 6), 3157, 58177))

Python

python
# For each deltaP candidate, list its top dx values with count. def top_dx(deltaP, topk=10): cnt=dist_two(deltaP) return cnt.most_common(topk) # gather those where top dx has active positions includes nibble0 and diff not in {1,15} good_trails=[] for deltaP in cands: cnt=dist_two(deltaP) # consider top 20 dx outcomes for dx,count in cnt.most_common(20): pos=nibble_positions(dx) if 0 in pos: diff0=(dx>>12)&0xF if diff0 not in (1,15) and active_nibbles(dx)==2: # also require maybe other inactive nibble? already 2 active. good_trails.append((deltaP,dx,count,pos,(diff0,))) # sort by count good_trails_sorted=sorted(good_trails, key=lambda t:-t[2])[:20] [(hex(t[0]),hex(t[1]),t[2],t[3],hex((t[1]>>12)&0xF),hex((t[1]>>4)&0xF)) for t in good_trails_sorted[:10]] [('0x700', '0x8001', 4096, (0, 3), '0x8', '0x0'), ('0xd00', '0x4010', 4096, (0, 2), '0x4', '0x1'), ('0xf00', '0x8001', 4096, (0, 3), '0x8', '0x0'), ('0x90', '0x4010', 4096, (0, 2), '0x4', '0x1'), ('0xf0', '0x4010', 4096, (0, 2), '0x4', '0x1'), ('0x7', '0x2100', 4096, (0, 1), '0x2', '0x0'), ('0xf', '0x2100', 4096, (0, 1), '0x2', '0x0'), ('0xb000', '0x8001', 2048, (0, 3), '0x8', '0x0'), ('0xd000', '0x2100', 2048, (0, 1), '0x2', '0x0'), ('0xd000', '0x2004', 2048, (0, 3), '0x2', '0x0')]

Python

python
good_trails1=[] for deltaP in cands: cnt=dist_two(deltaP) for dx,count in cnt.most_common(30): pos=nibble_positions(dx) if 1 in pos and active_nibbles(dx)==2: diff1=(dx>>8)&0xF if diff1 not in (1,15): good_trails1.append((deltaP,dx,count,pos,diff1)) good_trails1_sorted=sorted(good_trails1, key=lambda t:-t[2])[:20] [(hex(t[0]),hex(t[1]),t[2],t[3],hex(t[4])) for t in good_trails1_sorted[:10]] [('0x7000', '0x210', 4096, (1, 2), '0x2'), ('0xf000', '0x210', 4096, (1, 2), '0x2'), ('0x70', '0x1800', 4096, (0, 1), '0x8'), ('0xd0', '0x401', 4096, (1, 3), '0x4'), ('0xf0', '0x1800', 4096, (0, 1), '0x8'), ('0x9', '0x401', 4096, (1, 3), '0x4'), ('0xf', '0x401', 4096, (1, 3), '0x4'), ('0x3000', '0x210', 2048, (1, 2), '0x2'), ('0x5000', '0x210', 2048, (1, 2), '0x2'), ('0x9000', '0x1800', 2048, (0, 1), '0x8')]

Python

python
good_trails3=[] for deltaP in cands: cnt=dist_two(deltaP) for dx,count in cnt.most_common(30): pos=nibble_positions(dx) if 3 in pos and active_nibbles(dx)==2: diff3=dx & 0xF if diff3 not in (1,15): good_trails3.append((deltaP,dx,count,pos,diff3)) good_trails3_sorted=sorted(good_trails3, key=lambda t:-t[2])[:20] [(hex(t[0]),hex(t[1]),t[2],t[3],hex(t[4])) for t in good_trails3_sorted[:10]] [('0x7000', '0x18', 4096, (2, 3), '0x8'), ('0xd000', '0x104', 4096, (1, 3), '0x4'), ('0xf000', '0x18', 4096, (2, 3), '0x8'), ('0x900', '0x104', 4096, (1, 3), '0x4'), ('0xf00', '0x104', 4096, (1, 3), '0x4'), ('0x70', '0x1002', 4096, (0, 3), '0x2'), ('0xf0', '0x1002', 4096, (0, 3), '0x2'), ('0x3000', '0x18', 2048, (2, 3), '0x8'), ('0x5000', '0x18', 2048, (2, 3), '0x8'), ('0x6000', '0x104', 2048, (1, 3), '0x4')]

Python

python
good_trails2=[] for deltaP in cands: cnt=dist_two(deltaP) for dx,count in cnt.most_common(30): pos=nibble_positions(dx) if 2 in pos and active_nibbles(dx)==2: diff2=(dx>>4)&0xF if diff2 not in (1,15): good_trails2.append((deltaP,dx,count,pos,diff2)) good_trails2_sorted=sorted(good_trails2, key=lambda t:-t[2])[:20] [(hex(t[0]),hex(t[1]),t[2],t[3],hex(t[4])) for t in good_trails2_sorted[:10]] [('0x9000', '0x1040', 4096, (0, 2), '0x4'), ('0xf000', '0x1040', 4096, (0, 2), '0x4'), ('0x700', '0x21', 4096, (2, 3), '0x2'), ('0xf00', '0x21', 4096, (2, 3), '0x2'), ('0x7', '0x180', 4096, (1, 2), '0x8'), ('0xd', '0x1040', 4096, (0, 2), '0x4'), ('0xf', '0x180', 4096, (1, 2), '0x8'), ('0x3000', '0x1040', 2048, (0, 2), '0x4'), ('0x5000', '0x1040', 2048, (0, 2), '0x4'), ('0x9000', '0x840', 2048, (1, 2), '0x4')]

Python

python
def collect_pairs(key, deltaP, Npairs): pairs=[] for _ in range(Npairs): m=random.getrandbits(16) pairs.append((encrypt_full(m,key), encrypt_full(m^deltaP,key))) return pairs def score_nibble(pairs, pos, expected_a, inactive_positions): scores=[0]*16 kept=0 for c,c2 in pairs: cd=c^c2 ok=True for ip in inactive_positions: if ((cd>>(12-4*ip))&0xF)!=0: ok=False; break if not ok: continue kept+=1 shift=12-4*pos cn=(c>>shift)&0xF cn2=(c2>>shift)&0xF for k in range(16): if (INV[cn^k]^INV[cn2^k])==expected_a: scores[k]+=1 mx=max(scores); best=scores.index(mx) second=sorted(scores, reverse=True)[1] return best, mx, second, kept def simulate_four_deltas(Npairs=512, trials=50): succ=0 kepts=[] for _ in range(trials): key=[random.getrandbits(16) for _ in range(4)] K3=key[3] # delta for nibble0: 0x0D00; expect a=4 at pos0; inactive pos [1,3] pairs0=collect_pairs(key, 0x0D00, Npairs) k0, s0, s0b, kept0 = score_nibble(pairs0, 0, 4, [1,3]) # nibble1: delta 0x7000; expect a=2 at pos1; inactive [0,3] pairs1=collect_pairs(key, 0x7000, Npairs) k1, s1, s1b, kept1 = score_nibble(pairs1, 1, 2, [0,3]) # nibble2: delta 0x9000; expect a=4 at pos2; inactive [1,3] pairs2=collect_pairs(key, 0x9000, Npairs) k2, s2, s2b, kept2 = score_nibble(pairs2, 2, 4, [1,3]) # nibble3: delta 0xD000; expect a=4 at pos3; inactive [0,2] pairs3=collect_pairs(key, 0xD000, Npairs) k3n, s3, s3b, kept3 = score_nibble(pairs3, 3, 4, [0,2]) rec=(k0<<12)|(k1<<8)|(k2<<4)|k3n ok=(rec==K3) succ += ok kepts.append((kept0,kept1,kept2,kept3, s0-s0b, s1-s1b, s2-s2b, s3-s3b)) return succ/trials, kepts rate, kepts = simulate_four_deltas(512, trials=50) rate, kepts[0] (1.0, (43, 34, 47, 34, 23, 20, 18, 17))

Suy nghĩ

Choosing the number of pairs and ensuring accuracy

With 256 pairs per delta, the success rate is 99%, but to be safe, I’ll go with 400 pairs. This gives 3200 queries, within the limits. The key point is ensuring the correct deltaX is used. We rely on inactive nibble outputs, and with proper delta values, we expect the right diff output. This setup should hold, confirming expected diffs match at each nibble.

Ensuring correct pairs and handling socket interactions

We're filtering based on inactive nibble differences being zero, which should still lead us to the correct pairs, though some noise may appear. Our next step is ensuring one unique key per connection, as both mask and key are tied to the connection. I’ll implement line-by-line reading with sockets while syncing up with the service’s prompts.

Implementing robust socket handling and prompt reading

I’ll create a function to read until the prompt ("Your input: "), then send data. After sending, we'll read the response, which includes "Output: 0x....". To handle this efficiently, I’ll use recvuntil with the Python socket library, ensuring robust reading and parsing for responses using regex where needed.

Handling prompt and output with chunked reading

I’ll use recv_until(b"Your input: ") to read chunks until the prompt appears. After sending the query, I'll read the "Output: " line with readline() and handle any leftover prompt data correctly. While this may seem a bit slow, it ensures reading the expected output and keeps things efficient enough.

Optimizing query execution with pipelining

To improve speed, I’ll send multiple queries at once without waiting for prompts. The server processes them line by line, and we can then read the output sequentially. I'll disregard the "Your input: " prompts since they don’t end with a newline and scan for "Output: 0x” patterns. Should be much faster.

Clarifying server-client interaction

The server sends the prompt, waits for client input, and then outputs the result. There’s no newline between prompt and output in the stream; the client’s terminal adds separation. After sending input, the client receives the prompt, followed by the output. I’ll just parse the output from this combined line.

Optimizing query handling and attack simulation

For each query, I’ll send the line, then read the response with readline() and parse the last token, simplifying the process by skipping recv_until. If I batch the queries, I can read all responses at once. By using regex to extract the relevant output, I'll implement the attack and scoring steps efficiently.

Handling proof submission and reading response

I’m finalizing the process for submitting the proof by sending the correct command when ready. After sending the “k\n” command, I’ll skip the prompt and send the proof immediately. Then, I’ll read a few lines to capture the result, which will include the “You can smile” message.

Explaining cryptanalysis and flag extraction

After sending the proof, I’ll read the response line that includes both the prompt and the result. The flag will be extracted from the line by splitting. I'll mention that the flag format might be unconventional, perhaps saying “You can smile...” instead of the usual flag{...}. Now, I’ll craft a concise explanation for the user.

Thought for 13m 36s

Trả lời

这题的本质:你有一个16-bit、3 轮的 SPN加密 oracle,最后一轮是

  • 做完最后一层 S 盒后,直接 c = state ^ K3
  • 服务端把 mask 给你看,你只要提交 K3 ^ mask 就出 flag

所以目标就是在同一条连接里把最后一轮子密钥 K3(16-bit)搞出来。

关键点:XOR 轮密钥/轮常量不影响差分。因此可以对最后一轮做经典“猜子密钥 + 逆 S 盒”的差分恢复。


用什么差分(这题最省事的一组)

对这套 S/P(代码里那两个表),如果把“前两轮(到最后一轮 S 盒输入前)”当作一个 2-round SPN 函数,可以离线算到下面这些高概率差分模式(我直接给你可用的):

我们分别用 4 个明文差分 ΔP,每个用来恢复 K3 的一个 nibble

恢复哪一段 K3选 ΔP期望“最后一轮 S 盒输入差分”在该 nibble 上是同时要求哪些 nibble 的密文差分为 0(用于过滤)
K3[15:12](最高 nibble,pos=0)0x0D000x4密文差分 nibble1、nibble3 为 0
K3[11:8](pos=1)0x70000x2密文差分 nibble0、nibble3 为 0
K3[7:4](pos=2)0x90000x4密文差分 nibble1、nibble3 为 0
K3[3:0](pos=3,最低 nibble)0xD0000x4密文差分 nibble0、nibble2 为 0

然后对通过过滤的对 (C, C'),对目标 nibble 猜 k in [0..15],算:

text
u = invS( C_nib ^ k ) u' = invS( C'_nib ^ k ) 如果 (u ^ u') == 期望差分 => 给这个 k +1 分

得分最高的 k 就是该 nibble 的 K3。

注意:这题的 S 盒里,输入差分为 0x1 会有“键等价”的坑,所以上面我特意选了在目标 nibble 上差分是 0x2 或 0x4 的组合,能稳定唯一恢复。


直接可用的 exploit(不需要满 4096 次查询)

下面脚本默认每个 ΔP 跑 N=400 对(总查询 4 * 2 * 400 = 3200 < 4096,留足余量),基本稳定。

HOST/PORT 改成题目地址即可:

python
#!/usr/bin/env python3 import socket import re import random S = [0xC, 0x5, 0x6, 0xB, 0x9, 0x0, 0xA, 0xD, 0x3, 0xE, 0xF, 0x8, 0x4, 0x7, 0x1, 0x2] INV = [0]*16 for i,v in enumerate(S): INV[v] = i def nib(x, pos): """pos: 0..3 from MS nibble to LS nibble""" return (x >> (12 - 4*pos)) & 0xF def set_nib(x, pos, v): sh = 12 - 4*pos x &= ~(0xF << sh) x |= (v & 0xF) << sh return x class Oracle: def __init__(self, host, port): self.s = socket.create_connection((host, port)) self.rf = self.s.makefile("rb", buffering=0) self.wf = self.s.makefile("wb", buffering=0) self.mask = None def read_banner_and_mask(self): buf = b"" # 读到出现 mask 行为止 while True: line = self.rf.readline() if not line: raise RuntimeError("connection closed while reading banner") buf += line m = re.search(rb"mask\s*=\s*0x([0-9a-fA-F]{4})", buf) if m: self.mask = int(m.group(1), 16) return def enc(self, pt16): self.wf.write(f"0x{pt16 & 0xFFFF:04x}\n".encode()) self.wf.flush() line = self.rf.readline() if not line: raise RuntimeError("connection closed during query") m = re.search(rb"Output:\s*0x([0-9a-fA-F]{4})", line) if not m: raise RuntimeError(f"unexpected oracle line: {line!r}") return int(m.group(1), 16) def submit(self, proof16): # 直接连发,不用等提示 self.wf.write(b"k\n") self.wf.write(f"0x{proof16 & 0xFFFF:04x}\n".encode()) self.wf.flush() # 读几行把 flag 打出来 out = b"" while True: line = self.rf.readline() if not line: break out += line return out.decode(errors="ignore") def recover_one_nibble(oracle, deltaP, target_pos, expected_diff, inactive_zero_positions, N=400): scores = [0]*16 kept = 0 for _ in range(N): p = random.getrandbits(16) p2 = p ^ deltaP c = oracle.enc(p) c2 = oracle.enc(p2) cd = c ^ c2 # 过滤:要求某些 nibble 的密文差分为 0 ok = True for ip in inactive_zero_positions: if nib(cd, ip) != 0: ok = False break if not ok: continue kept += 1 cn = nib(c, target_pos) cn2 = nib(c2, target_pos) for k in range(16): u = INV[cn ^ k] u2 = INV[cn2 ^ k] if (u ^ u2) == expected_diff: scores[k] += 1 best = max(range(16), key=lambda k: scores[k]) scores_sorted = sorted(scores, reverse=True) margin = scores_sorted[0] - scores_sorted[1] return best, kept, margin, scores def main(): HOST = "127.0.0.1" PORT = 37344 o = Oracle(HOST, PORT) o.read_banner_and_mask() print(f"[+] mask = 0x{o.mask:04x}") # 四个配置:各恢复一个 nibble # pos: 0..3 (MS nibble -> LS nibble) jobs = [ # recover K3 nibble0 (MS) dict(deltaP=0x0D00, pos=0, a=0x4, inactive=[1,3]), # recover K3 nibble1 dict(deltaP=0x7000, pos=1, a=0x2, inactive=[0,3]), # recover K3 nibble2 dict(deltaP=0x9000, pos=2, a=0x4, inactive=[1,3]), # recover K3 nibble3 (LS) dict(deltaP=0xD000, pos=3, a=0x4, inactive=[0,2]), ] k3_nibs = [0,0,0,0] for j in jobs: k, kept, margin, scores = recover_one_nibble( o, deltaP=j["deltaP"], target_pos=j["pos"], expected_diff=j["a"], inactive_zero_positions=j["inactive"], N=400, # 总查询 3200 次,< 4096 ) k3_nibs[j["pos"]] = k print(f"[+] pos={j['pos']} deltaP=0x{j['deltaP']:04x} kept={kept} best=0x{k:x} margin={margin}") K3 = (k3_nibs[0]<<12) | (k3_nibs[1]<<8) | (k3_nibs[2]<<4) | (k3_nibs[3]) print(f"[+] Recovered K3 = 0x{K3:04x}") proof = K3 ^ o.mask print(f"[+] Sending proof = K3 ^ mask = 0x{proof:04x}") resp = o.submit(proof) print(resp) if __name__ == "__main__": main()

你可能关心的“此题不是正常 flag”

服务端打印的是:You can smile: + FLAG,FLAG 可能不是 flag{...} 这种格式(出题人提示的意思大概率就是这个)。脚本拿到服务端返回啥就会原样打印。


如果你把题目远程的 HOST:PORT 给我,我也可以帮你把脚本改成更适合那边 IO 的版本(比如 pwntools、并发批量发送、更快更稳)。

Câu hỏi

HOST:PORT那个文件中有啊 port是37344

Python

python
import random, math, statistics S = [0xC,0x5,0x6,0xB,0x9,0x0,0xA,0xD,0x3,0xE,0xF,0x8,0x4,0x7,0x1,0x2] INV=[0]*16 for i,v in enumerate(S): INV[v]=i P = (0,5,10,15, 4,9,14,3, 8,13,2,7, 12,1,6,11) RC = (0x0F0F,0x3333,0x55AA) def deblock(m): return [(m>>12)&0xF,(m>>8)&0xF,(m>>4)&0xF,m&0xF] def block(n): return (n[0]<<12)|(n[1]<<8)|(n[2]<<4)|n[3] def s_layer(x): n=deblock(x) n=[S[i] for i in n] return block(n) def permute_bits(x): bits=[(x>>(15-i))&1 for i in range(16)] new=[0]*16 for i in range(16): new[P[i]] = bits[i] out=0 for b in new: out=(out<<1)|b return out&0xFFFF def encrypt(m, key, r=3): p=m&0xFFFF for t in range(r-1): p ^= key[t] p ^= RC[t] p = s_layer(p) p = permute_bits(p) p ^= key[r-1] p ^= RC[r-1] p = s_layer(p) c = p ^ key[-1] return c&0xFFFF def nib(x,pos): return (x>>(12-4*pos)) & 0xF def recover_one(enc_oracle, deltaP, target_pos, expected_diff, inactive, N=400): scores=[0]*16 kept=0 for _ in range(N): p=random.getrandbits(16) c=enc_oracle(p) c2=enc_oracle(p^deltaP) cd=c^c2 ok=True for ip in inactive: if nib(cd,ip)!=0: ok=False; break if not ok: continue kept+=1 cn=nib(c,target_pos); cn2=nib(c2,target_pos) for k in range(16): u=INV[cn^k]; u2=INV[cn2^k] if (u^u2)==expected_diff: scores[k]+=1 best=max(range(16), key=lambda k: scores[k]) sorted_scores=sorted(scores, reverse=True) margin=sorted_scores[0]-sorted_scores[1] return best, kept, margin, scores jobs=[ dict(deltaP=0x0D00,pos=0,a=0x4,inactive=[1,3]), dict(deltaP=0x7000,pos=1,a=0x2,inactive=[0,3]), dict(deltaP=0x9000,pos=2,a=0x4,inactive=[1,3]), dict(deltaP=0xD000,pos=3,a=0x4,inactive=[0,2]), ] def trial(N=400): key=[random.getrandbits(16) for _ in range(4)] oracle=lambda m: encrypt(m,key,3) k3=key[-1] rec=[0,0,0,0] info=[] for j in jobs: b, kept, margin, scores = recover_one(oracle, j["deltaP"], j["pos"], j["a"], j["inactive"], N=N) rec[j["pos"]] = b info.append((kept, margin, scores)) K3=(rec[0]<<12)|(rec[1]<<8)|(rec[2]<<4)|rec[3] return (K3==k3), k3, K3, info # run multiple trials succ=0 margins=[] kepts=[] for t in range(200): ok, k3, K3, info = trial(N=400) succ += ok margins.append([x[1] for x in info]) kepts.append([x[0] for x in info]) succ, succ/200, (statistics.mean([m[0] for m in margins]), statistics.mean([m[1] for m in margins]), statistics.mean([m[2] for m in margins]), statistics.mean([m[3] for m in margins])), (statistics.mean([k[0] for k in kepts]), statistics.mean([k[1] for k in kepts]), statistics.mean([k[2] for k in kepts]), statistics.mean([k[3] for k in kepts])) (200, 1.0, (14.06, 15.085, 14.505, 14.02), (31.41, 25.41, 33.01, 31.38))

Suy nghĩ

Clarifying host:port and script modifications

It looks like the user is referencing a file with port 37344, but no host is provided. I’ll modify the script to accept arguments, defaulting the host to '127.0.0.1' and port to 37344. I’ll also provide instructions on running locally for debugging with a --listen option.

Clarifying host:port settings

For remote servers, they would use --listen 0.0.0.0 and port 37344. Since the host is likely provided in the challenge statement, not the code, I’ll suggest adding command-line options for the host. For local testing, users can use 127.0.0.1:37344. I’ll include a safe exploit script with argparse and offer clear explanation about

Thought for 35s

Trả lời

你给的这个服务端脚本里只写死了默认端口 37344,并没有写死 HOST(它要么是本地 stdin/stdout 模式,要么你启动时用 --listen 指定监听地址)。所以解题脚本里 HOST 需要你自己填:

  • 本地测试一般用:127.0.0.1:37344
  • 远程就是题目平台给你的域名/IP + 37344

我把 exploit 改成了命令行传 host/port,你直接用就行。


1)本地怎么跑(可选)

先把你那份 server 脚本跑起来(监听 TCP):

bash
python3 server.py --listen 127.0.0.1 --port 37344

2)解题脚本(solve.py)

python
#!/usr/bin/env python3 import socket import re import random import argparse S = [0xC, 0x5, 0x6, 0xB, 0x9, 0x0, 0xA, 0xD, 0x3, 0xE, 0xF, 0x8, 0x4, 0x7, 0x1, 0x2] INV = [0]*16 for i, v in enumerate(S): INV[v] = i def nib(x, pos): """pos: 0..3 from MS nibble to LS nibble""" return (x >> (12 - 4*pos)) & 0xF class Oracle: def __init__(self, host, port, timeout=10): self.s = socket.create_connection((host, port), timeout=timeout) self.rf = self.s.makefile("rb", buffering=0) self.wf = self.s.makefile("wb", buffering=0) self.mask = None def read_banner_and_mask(self): buf = b"" while True: line = self.rf.readline() if not line: raise RuntimeError("connection closed while reading banner") buf += line m = re.search(rb"mask\s*=\s*0x([0-9a-fA-F]{4})", buf) if m: self.mask = int(m.group(1), 16) return def enc(self, pt16): self.wf.write(f"0x{pt16 & 0xFFFF:04x}\n".encode()) self.wf.flush() line = self.rf.readline() if not line: raise RuntimeError("connection closed during query") m = re.search(rb"Output:\s*0x([0-9a-fA-F]{4})", line) if not m: raise RuntimeError(f"unexpected oracle line: {line!r}") return int(m.group(1), 16) def submit(self, proof16): self.wf.write(b"k\n") self.wf.write(f"0x{proof16 & 0xFFFF:04x}\n".encode()) self.wf.flush() out = b"" while True: line = self.rf.readline() if not line: break out += line return out.decode(errors="ignore") def recover_one_nibble(oracle, deltaP, target_pos, expected_diff, inactive_zero_positions, N=400): scores = [0]*16 kept = 0 for _ in range(N): p = random.getrandbits(16) p2 = p ^ deltaP c = oracle.enc(p) c2 = oracle.enc(p2) cd = c ^ c2 # 过滤:要求某些 nibble 的密文差分为 0 ok = True for ip in inactive_zero_positions: if nib(cd, ip) != 0: ok = False break if not ok: continue kept += 1 cn = nib(c, target_pos) cn2 = nib(c2, target_pos) for k in range(16): u = INV[cn ^ k] u2 = INV[cn2 ^ k] if (u ^ u2) == expected_diff: scores[k] += 1 best = max(range(16), key=lambda k: scores[k]) scores_sorted = sorted(scores, reverse=True) margin = scores_sorted[0] - scores_sorted[1] return best, kept, margin def main(): ap = argparse.ArgumentParser() ap.add_argument("host", nargs="?", default="127.0.0.1") ap.add_argument("port", nargs="?", type=int, default=37344) ap.add_argument("-n", "--pairs", type=int, default=400, help="pairs per job (default 400)") args = ap.parse_args() o = Oracle(args.host, args.port) o.read_banner_and_mask() print(f"[+] mask = 0x{o.mask:04x}") # 四个配置:各恢复一个 nibble(总查询约 4*2*pairs,默认 3200 < 4096) jobs = [ dict(deltaP=0x0D00, pos=0, a=0x4, inactive=[1, 3]), # K3[15:12] dict(deltaP=0x7000, pos=1, a=0x2, inactive=[0, 3]), # K3[11:8] dict(deltaP=0x9000, pos=2, a=0x4, inactive=[1, 3]), # K3[7:4] dict(deltaP=0xD000, pos=3, a=0x4, inactive=[0, 2]), # K3[3:0] ] k3_nibs = [0, 0, 0, 0] for j in jobs: k, kept, margin = recover_one_nibble( o, deltaP=j["deltaP"], target_pos=j["pos"], expected_diff=j["a"], inactive_zero_positions=j["inactive"], N=args.pairs, ) k3_nibs[j["pos"]] = k print(f"[+] pos={j['pos']} deltaP=0x{j['deltaP']:04x} kept={kept} best=0x{k:x} margin={margin}") K3 = (k3_nibs[0] << 12) | (k3_nibs[1] << 8) | (k3_nibs[2] << 4) | k3_nibs[3] print(f"[+] Recovered K3 = 0x{K3:04x}") proof = K3 ^ o.mask print(f"[+] Sending proof = 0x{proof:04x}") resp = o.submit(proof) print(resp) if __name__ == "__main__": main()

3)怎么用

  • 本地:
bash
python3 solve.py 127.0.0.1 37344
  • 远程(把 HOST 换成题目给的域名/IP):
bash
python3 solve.py HOST 37344

题目提示“不是正常flag”没关系:服务端回什么这脚本就原样打印什么(可能不是 flag{...} 格式)。

如果你把题目平台给的那个 HOST(域名/IP)发我一句,我也可以顺手帮你确认脚本在那边的输出格式是否需要微调(有些平台会多几行 banner 或者输出不在同一行)。

Câu hỏi

报错└─# python3 solve.py 127.0.0.1 37344
[+] mask = 0x6335
Traceback (most recent call last):
File "/home/kali/solve.py", line 148, in <module>
main()
~~~~^^
File "/home/kali/solve.py", line 128, in main
k, kept, margin, scores = recover_one_nibble(
~~~~~~~~~~~~~~~~~~^
o,
^^
...<4 lines>...
N=400, # 总查询 3200 次,< 4096
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
)
^
File "/home/kali/solve.py", line 77, in recover_one_nibble
c = oracle.enc(p)
File "/home/kali/solve.py", line 53, in enc
raise RuntimeError(f"unexpected oracle line: {line!r}")
RuntimeError: unexpected oracle line: b'Goal: submit (K3 ^ mask) to get the flag.\n'

Suy nghĩ

Refining read logic for prompt handling

Okay, so in read_banner_and_mask, after finding the mask, I should keep reading lines until hitting the separator or a prompt. A simpler approach could be modifying enc() to tolerate extra lines until it matches the expected output, but I might run into issues with the prompt staying in the buffer if not handled properly.

Handling newline and prompt byte issues

The readline method will stop at a newline, but the prompt doesn't have one. After the server sends an output line, it includes a prompt, and this could confuse the regex, expecting "Output" at the start. A cleaner approach might be to use a regex search for "Output" anywhere in the line, not just at the start.

Improving line reading with output matching

The error suggests that we didn't properly loop to keep reading lines. To fix this, I'll modify enc() to continue reading lines until we find a match, while ensuring that any prompt-output mix is handled. We’ll also need to handle potential infinite loops if the connection closes and handle banner lines properly.

Fixing readline handling and banner skipping

The issue arises because when the prompt bytes still sit in the buffer, readline blocks until the newline, returning a mix of prompt and output. To fix this, I’ll update the encoding function to skip until the match is found instead of raising an error. I’ll also clarify the connection setup details and respond in Chinese with the updated code.

Thought for 20s

Trả lời

这个报错很正常:你在 read_banner_and_mask()读到 mask 那一行就返回了,但服务端 mask 后面还有几行 banner(比如 Goal: ...、分割线),导致你第一次 enc()readline() 读到的还是 banner 行,于是 regex 匹配不到 Output: 就直接 raise 了。

Oracle.enc() 改成“一直读到出现 Output 为止(中间的 banner 行全部丢掉)”就好了。同理提交 k 的时候也建议做成“读到连接关闭/或读到结果行”。

下面是最小补丁(你直接替换 Oracle.enc 就能跑):

python
def enc(self, pt16): self.wf.write(f"0x{pt16 & 0xFFFF:04x}\n".encode()) self.wf.flush() while True: line = self.rf.readline() if not line: raise RuntimeError("connection closed during query") # 这里用 search,所以即使是 "Your input: Output: 0x...." 也能匹配 m = re.search(rb"Output:\s*0x([0-9a-fA-F]{4})", line) if m: return int(m.group(1), 16) # 否则就是 banner/提示/残留行,丢掉继续读

如果你想更稳一点(避免 k 提交时也被残留行影响),也可以把 submit() 写成“读到包含 flag/You can smile/Wrong 之类关键字就返回”,不过通常你现在这个 submit() 读到断开就够用了。

你改完再跑一次应该就能继续往下打分恢复 K3 了。要是之后出现“margin 很小/偶尔猜错 nibble”,把 -n/--pairs 调大点(比如 600)即可,但别超过 4096 查询总数。

Câu hỏi

┌──(root㉿kali)-[/home/kali]
└─# python3 solve.py 127.0.0.1 37344
File "/home/kali/solve.py", line 45
self.wf.write(f"0x{pt16 & 0xFFFF:04x}\n".encode())
^^^^
IndentationError: expected an indented block after function definition on line 44

Suy nghĩ

Thought for 8s

Trả lời

你这个 IndentationError 是因为你把 enc() 的函数体粘贴进去后,没有在 def enc(...): 下面再缩进一层(Python 必须缩进)。

最省事:你直接把你文件里的 整个 Oracle替换成下面这一段(保证缩进完全正确),其它地方不用动。

python
class Oracle: def __init__(self, host, port, timeout=10): self.s = socket.create_connection((host, port), timeout=timeout) self.rf = self.s.makefile("rb", buffering=0) self.wf = self.s.makefile("wb", buffering=0) self.mask = None def read_banner_and_mask(self): buf = b"" while True: line = self.rf.readline() if not line: raise RuntimeError("connection closed while reading banner") buf += line m = re.search(rb"mask\s*=\s*0x([0-9a-fA-F]{4})", buf) if m: self.mask = int(m.group(1), 16) return def enc(self, pt16): # 发送明文 self.wf.write(f"0x{pt16 & 0xFFFF:04x}\n".encode()) self.wf.flush() # 一直读到真正的 Output 行(中间 banner/提示行全部跳过) while True: line = self.rf.readline() if not line: raise RuntimeError("connection closed during query") m = re.search(rb"Output:\s*0x([0-9a-fA-F]{4})", line) if m: return int(m.group(1), 16) # 否则丢掉继续读 def submit(self, proof16): self.wf.write(b"k\n") self.wf.write(f"0x{proof16 & 0xFFFF:04x}\n".encode()) self.wf.flush() out = b"" while True: line = self.rf.readline() if not line: break out += line return out.decode(errors="ignore")

为什么会这样

你之前的报错 unexpected oracle line: b'Goal: ...' 是因为 banner 没读完;我这个 enc() 会自动把 banner 行吃掉,直到读到 Output: 0x....

如果你还遇到第二个坑

如果你现在的 main() 里还是写着:

python
k, kept, margin, scores = recover_one_nibble(...)

但你的 recover_one_nibble()return best, kept, margin(3 个值),那会再报 “not enough values to unpack”。
解决:把那行改成 3 个变量:

python
k, kept, margin = recover_one_nibble(...)

你改完再跑一次就应该能继续恢复 K3 了。要是你愿意,把你当前 recover_one_nibblemain 那几行也贴出来,我可以直接帮你对齐成一份可运行的完整 solve.py

Câu hỏi

你直接给我完整的可运行的solve.py

Python

python
import random, math, statistics S = [0xC, 0x5, 0x6, 0xB, 0x9, 0x0, 0xA, 0xD, 0x3, 0xE, 0xF, 0x8, 0x4, 0x7, 0x1, 0x2] P = (0, 5, 10, 15, 4, 9, 14, 3, 8, 13, 2, 7, 12, 1, 6, 11) RC = (0x0F0F, 0x3333, 0x55AA) INV = [0]*16 for i,v in enumerate(S): INV[v]=i def deblock(m): return [(m>>12)&0xF,(m>>8)&0xF,(m>>4)&0xF,m&0xF] def block(n): return (n[0]<<12)|(n[1]<<8)|(n[2]<<4)|n[3] def s_layer(x): n=deblock(x); n=[S[i] for i in n]; return block(n) def permute_bits(x): bits=[(x>>(15-i))&1 for i in range(16)] new=[0]*16 for i in range(16): new[P[i]]=bits[i] out=0 for b in new: out=(out<<1)|b return out&0xFFFF def encrypt(m,key): r=3 p=m&0xFFFF for t in range(r-1): p ^= key[t]; p ^= RC[t]; p=s_layer(p); p=permute_bits(p) p ^= key[r-1]; p ^= RC[r-1]; p=s_layer(p) c = p ^ key[-1] return c & 0xFFFF def nib(x,pos): return (x>>(12-4*pos))&0xF def recover_nibble_oracle(encrypt_oracle, deltaP, pos, expected_diff, inactive, N=400): scores=[0]*16 for _ in range(N): p=random.getrandbits(16) p2=p^deltaP c=encrypt_oracle(p) c2=encrypt_oracle(p2) cd=c^c2 if any(nib(cd,ip)!=0 for ip in inactive): continue cn=nib(c,pos); cn2=nib(c2,pos) for k in range(16): u=INV[cn^k]; u2=INV[cn2^k] if (u^u2)==expected_diff: scores[k]+=1 best=max(range(16), key=lambda k:scores[k]) sorted_scores=sorted(scores, reverse=True) margin=sorted_scores[0]-sorted_scores[1] return best, margin, scores # test success rate over random keys def trial(Npairs=400, trials=200): jobs=[ dict(deltaP=0x0D00,pos=0,a=0x4,inactive=[1,3]), dict(deltaP=0x7000,pos=1,a=0x2,inactive=[0,3]), dict(deltaP=0x9000,pos=2,a=0x4,inactive=[1,3]), dict(deltaP=0xD000,pos=3,a=0x4,inactive=[0,2]), ] ok=0 margins=[] for t in range(trials): key=[random.getrandbits(16) for _ in range(4)] K3=key[-1] def oracle(m): return encrypt(m,key) recovered=[0]*4 mar=[] for j in jobs: k,margin,scores=recover_nibble_oracle(oracle,j["deltaP"],j["pos"],j["a"],j["inactive"],Npairs) recovered[j["pos"]]=k mar.append(margin) K3rec=(recovered[0]<<12)|(recovered[1]<<8)|(recovered[2]<<4)|recovered[3] if K3rec==K3: ok+=1 margins.append(mar) return ok/trials, margins rate, margins = trial(400, 200) rate, statistics.mean([m[0] for m in margins]), statistics.mean([m[1] for m in margins]), statistics.mean([m[2] for m in margins]), statistics.mean([m[3] for m in margins]) (1.0, 13.825, 14.59, 15.09, 13.56)

Python

python
for N in [50,80,100,120,150,200,250,300]: rate,_ = trial(N, 200) print(N, rate)

Suy nghĩ

Ensuring stability and refining reading behavior

To ensure stability, I’ll go with the default N=400, which gives a total of 3200 queries. I'll add an optional "warmup" to handle leftover lines after the mask. While not needed for robust encryption, it’s useful in remote environments where banners might differ. In submit, I’ll check for the connection closing after reading the result, though issues may arise if the connection doesn't close after an error.

Finalizing script and handling edge cases

The script will handle networking exceptions and ensure it adheres to QMAX 4096 per connection. To avoid exceeding this, the default will be 400 pairs, with an automatic reduction or warning for sizes over 3800 queries. The script will cap pairs at 500, ensuring a safe maximum of 480 pairs. We’ll also provide a usage guide with comments and a simple shebang for the Kali setup.

Thought for 1m 35s

Trả lời

下面给你一份完整可运行solve.py(已修好 banner/提示行导致的读取错位问题;并且默认查询次数保证 <4096)。

把它保存为 solve.py,然后运行:

  • 本地:python3 solve.py 127.0.0.1 37344
  • 远程:python3 solve.py 题目给的HOST 37344
python
#!/usr/bin/env python3 import socket import re import random import argparse # ------------------------ # S-box and inverse S-box # ------------------------ S = [0xC, 0x5, 0x6, 0xB, 0x9, 0x0, 0xA, 0xD, 0x3, 0xE, 0xF, 0x8, 0x4, 0x7, 0x1, 0x2] INV = [0] * 16 for i, v in enumerate(S): INV[v] = i def nib(x, pos): """pos: 0..3 from MS nibble to LS nibble""" return (x >> (12 - 4 * pos)) & 0xF # ------------------------ # Oracle wrapper # ------------------------ class Oracle: def __init__(self, host, port, timeout=10): self.s = socket.create_connection((host, port), timeout=timeout) self.rf = self.s.makefile("rb", buffering=0) self.wf = self.s.makefile("wb", buffering=0) self.mask = None def read_mask_only(self): """ 只读到 mask 行就返回。 注意:服务端之后还会输出几行 banner + 一个没有换行的 prompt, 所以这里不能贪多读,否则会卡在 readline() 等换行。 """ buf = b"" while True: line = self.rf.readline() if not line: raise RuntimeError("connection closed while reading banner") buf += line m = re.search(rb"mask\s*=\s*0x([0-9a-fA-F]{4})", buf) if m: self.mask = int(m.group(1), 16) return def enc(self, pt16): """ 发送明文后,不断读取直到匹配到 Output: 0x.... 这样可以自动吃掉 banner 残留行/提示行/以及 'Your input: ' 这种无换行 prompt 拼接的行。 """ self.wf.write(f"0x{pt16 & 0xFFFF:04x}\n".encode()) self.wf.flush() while True: line = self.rf.readline() if not line: raise RuntimeError("connection closed during query") m = re.search(rb"Output:\s*0x([0-9a-fA-F]{4})", line) if m: return int(m.group(1), 16) # 否则是 banner/提示/残留行,丢掉继续读 def submit(self, proof16): # 进入提交模式 self.wf.write(b"k\n") self.wf.write(f"0x{proof16 & 0xFFFF:04x}\n".encode()) self.wf.flush() # 读到断开为止(服务端会 break 然后关闭连接) out = b"" while True: chunk = self.rf.readline() if not chunk: break out += chunk return out.decode(errors="ignore") # ------------------------ # Differential last-round nibble recovery # ------------------------ def recover_one_nibble(oracle, deltaP, target_pos, expected_diff, inactive_zero_positions, N=400): scores = [0] * 16 kept = 0 for _ in range(N): p = random.getrandbits(16) p2 = p ^ deltaP c = oracle.enc(p) c2 = oracle.enc(p2) cd = c ^ c2 # 过滤:要求某些 nibble 的密文差分为 0 ok = True for ip in inactive_zero_positions: if nib(cd, ip) != 0: ok = False break if not ok: continue kept += 1 cn = nib(c, target_pos) cn2 = nib(c2, target_pos) # 猜最后一轮子密钥该 nibble(4-bit) for k in range(16): u = INV[cn ^ k] u2 = INV[cn2 ^ k] if (u ^ u2) == expected_diff: scores[k] += 1 best = max(range(16), key=lambda k: scores[k]) top2 = sorted(scores, reverse=True)[:2] margin = top2[0] - top2[1] if len(top2) == 2 else top2[0] return best, kept, margin, scores # ------------------------ # Main # ------------------------ def main(): ap = argparse.ArgumentParser(description="CathylinFour++ solver: recover K3 then submit K3^mask") ap.add_argument("host", nargs="?", default="127.0.0.1") ap.add_argument("port", nargs="?", type=int, default=37344) ap.add_argument("-n", "--pairs", type=int, default=400, help="pairs per job (default 400). total queries ~= 8*pairs") args = ap.parse_args() # 保证不超过 QMAX=4096(每 pair 需要 2 次 enc;4 个 job => 8 次 enc) max_pairs = 4096 // 8 # 512 if args.pairs > max_pairs: print(f"[!] pairs too large, cap to {max_pairs} to stay under 4096 queries.") args.pairs = max_pairs o = Oracle(args.host, args.port) o.read_mask_only() print(f"[+] mask = 0x{o.mask:04x}") # 4 个 job:分别恢复 K3 的 4 个 nibble(pos 0..3 从高到低) jobs = [ dict(deltaP=0x0D00, pos=0, a=0x4, inactive=[1, 3]), # K3[15:12] dict(deltaP=0x7000, pos=1, a=0x2, inactive=[0, 3]), # K3[11:8] dict(deltaP=0x9000, pos=2, a=0x4, inactive=[1, 3]), # K3[7:4] dict(deltaP=0xD000, pos=3, a=0x4, inactive=[0, 2]), # K3[3:0] ] k3_nibs = [0, 0, 0, 0] for j in jobs: k, kept, margin, scores = recover_one_nibble( o, deltaP=j["deltaP"], target_pos=j["pos"], expected_diff=j["a"], inactive_zero_positions=j["inactive"], N=args.pairs ) k3_nibs[j["pos"]] = k print(f"[+] pos={j['pos']} deltaP=0x{j['deltaP']:04x} kept={kept} best=0x{k:x} margin={margin}") K3 = (k3_nibs[0] << 12) | (k3_nibs[1] << 8) | (k3_nibs[2] << 4) | k3_nibs[3] print(f"[+] Recovered K3 = 0x{K3:04x}") proof = K3 ^ o.mask print(f"[+] Sending proof = K3 ^ mask = 0x{proof:04x}") resp = o.submit(proof) print(resp) if __name__ == "__main__": main()

如果你跑出来 margin 很小(比如 0/1)导致某个 nibble 偶尔不稳,就把 -n 稍微加大一点(例如 -n 450,总查询约 3600 仍然 <4096)。

Chia sẻ Q&A này