#!/usr/bin/env python3 import os import random imp...
Created on: December 27, 2025
Answered using GPT-5.2 Thinking by Chat01
Created on: December 27, 2025
Answered using GPT-5.2 Thinking by Chat01
#!/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)
textdef 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()
textif 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
pythondef 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# 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# 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, [])
pythondef 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)
pythondef 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# 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))
pythonimport 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)
pythonfrom 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)])
pythondef 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}))
pythonimport 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
pythonddt[1] [0, 0, 0, 4, 0, 0, 0, 4, 0, 4, 0, 0, 0, 4, 0, 0]
pythonddt[4] [0, 0, 0, 0, 0, 4, 2, 2, 0, 2, 2, 0, 2, 0, 2, 0]
pythonallowed_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# 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# 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))
pythondef 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))
pythondef 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)}
pythonfor 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)
pythondef 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)
pythondef 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)
pythondef 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)]
pythondef 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# 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')]
pythongood_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')]
pythongood_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')]
pythongood_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')]
pythondef 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))
这题的本质:你有一个16-bit、3 轮的 SPN加密 oracle,最后一轮是
c = state ^ K3mask 给你看,你只要提交 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) | 0x0D00 | 0x4 | 密文差分 nibble1、nibble3 为 0 |
| K3[11:8](pos=1) | 0x7000 | 0x2 | 密文差分 nibble0、nibble3 为 0 |
| K3[7:4](pos=2) | 0x9000 | 0x4 | 密文差分 nibble1、nibble3 为 0 |
| K3[3:0](pos=3,最低 nibble) | 0xD000 | 0x4 | 密文差分 nibble0、nibble2 为 0 |
然后对通过过滤的对 (C, C'),对目标 nibble 猜 k in [0..15],算:
textu = invS( C_nib ^ k ) u' = invS( C'_nib ^ k ) 如果 (u ^ u') == 期望差分 => 给这个 k +1 分
得分最高的 k 就是该 nibble 的 K3。
注意:这题的 S 盒里,输入差分为 0x1 会有“键等价”的坑,所以上面我特意选了在目标 nibble 上差分是 0x2 或 0x4 的组合,能稳定唯一恢复。
下面脚本默认每个 Δ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()
服务端打印的是:You can smile: + FLAG,FLAG 可能不是 flag{...} 这种格式(出题人提示的意思大概率就是这个)。脚本拿到服务端返回啥就会原样打印。
如果你把题目远程的 HOST:PORT 给我,我也可以帮你把脚本改成更适合那边 IO 的版本(比如 pwntools、并发批量发送、更快更稳)。
HOST:PORT那个文件中有啊 port是37344
pythonimport 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))
你给的这个服务端脚本里只写死了默认端口 37344,并没有写死 HOST(它要么是本地 stdin/stdout 模式,要么你启动时用 --listen 指定监听地址)。所以解题脚本里 HOST 需要你自己填:
127.0.0.1:3734437344我把 exploit 改成了命令行传 host/port,你直接用就行。
先把你那份 server 脚本跑起来(监听 TCP):
bashpython3 server.py --listen 127.0.0.1 --port 37344
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()
bashpython3 solve.py 127.0.0.1 37344
HOST 换成题目给的域名/IP):bashpython3 solve.py HOST 37344
题目提示“不是正常flag”没关系:服务端回什么这脚本就原样打印什么(可能不是
flag{...}格式)。
如果你把题目平台给的那个 HOST(域名/IP)发我一句,我也可以顺手帮你确认脚本在那边的输出格式是否需要微调(有些平台会多几行 banner 或者输出不在同一行)。
报错└─# 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'
这个报错很正常:你在 read_banner_and_mask() 里读到 mask 那一行就返回了,但服务端 mask 后面还有几行 banner(比如 Goal: ...、分割线),导致你第一次 enc() 的 readline() 读到的还是 banner 行,于是 regex 匹配不到 Output: 就直接 raise 了。
把 Oracle.enc() 改成“一直读到出现 Output 为止(中间的 banner 行全部丢掉)”就好了。同理提交 k 的时候也建议做成“读到连接关闭/或读到结果行”。
下面是最小补丁(你直接替换 Oracle.enc 就能跑):
pythondef 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 查询总数。
┌──(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
你这个 IndentationError 是因为你把 enc() 的函数体粘贴进去后,没有在 def enc(...): 下面再缩进一层(Python 必须缩进)。
最省事:你直接把你文件里的 整个 Oracle 类替换成下面这一段(保证缩进完全正确),其它地方不用动。
pythonclass 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() 里还是写着:
pythonk, kept, margin, scores = recover_one_nibble(...)
但你的 recover_one_nibble() 只 return best, kept, margin(3 个值),那会再报 “not enough values to unpack”。
解决:把那行改成 3 个变量:
pythonk, kept, margin = recover_one_nibble(...)
你改完再跑一次就应该能继续恢复 K3 了。要是你愿意,把你当前 recover_one_nibble 和 main 那几行也贴出来,我可以直接帮你对齐成一份可运行的完整 solve.py。
你直接给我完整的可运行的solve.py
pythonimport 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)
pythonfor N in [50,80,100,120,150,200,250,300]: rate,_ = trial(N, 200) print(N, rate)
下面给你一份完整可运行的 solve.py(已修好 banner/提示行导致的读取错位问题;并且默认查询次数保证 <4096)。
把它保存为 solve.py,然后运行:
python3 solve.py 127.0.0.1 37344python3 solve.py 题目给的HOST 37344python#!/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)。