RCTF2025
suanp0ly
这做了三个题,不过就只能把其中一个做出来,可惜,实力有限.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64
| from sage.all import GF, ZZ, sample, gcd, PolynomialRing
from Crypto.Cipher import AES
from hashlib import md5
import os
r, d = 16381, 41
R.<x> = PolynomialRing(GF(2))
S.<X> = R.quo(x^r - 1)
def suan_p01y(nt, db):
return sum(x^i for i in set(sample(range(db+1), nt)))
while True:
t = [suan_p01y(d, r//3) for _ in range(2)]
if gcd(t[0], t[1]) != 1:
print(f'11')
continue
h = [ti * X for ti in t]
if h[0]:
break
hint=h[1]/h[0]
with open("output.txt", "w") as f:
f.write(f"hint = {h[1]/h[0]}\n")
f.write(
AES.new(
key=md5(str(h[0]).encode()).digest(),
nonce=b"suanp01y",
mode=AES.MODE_CTR
).encrypt(os.environ.get("FLAG", "RCTF{fake_flag}").encode()).hex()
)
|
看到别人基本AI梭了,不过我是没梭出来哈哈哈,不过做法其实就是拿rational_reconstruction做就行了,了解少了不知道这个工具,不过我就直接没看这个题的复现了,最近事情比较多先.知道了咋做就行这个
reparping
rust代码看不懂一点,给了让ai审了一下代码,发现就是输出会给你c1,c2,c3.等待输入是否和c1,c2,c3一样.通过构造c1’,c2’,c3’不同但是仍能返回正确的key就能求解出flag.唯一做出来的一道,感谢suan没让我zero
let c2_prime = c2 + g1_gen;
let c3_prime = c3 + h1;
let c1_prime = c1 * pk;
如下构造
suanhash
这个题最有趣的,当时太困没想出来,现在看来不算非常难,不过是真的有意思
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210
| import os
import secrets
from Crypto.Cipher import AES
class SuanHash:
state_bits = 128
rate_bits = 64
cap_bits = state_bits - rate_bits
digest_size = state_bits // 8
block_size = state_bits // 8
_rate_mask = (1 << rate_bits) - 1
_cap_mask = (1 << cap_bits) - 1
def __init__(self, data: bytes = b""):
self._perm_seed = os.urandom(16)
self._cipher = AES.new(self._perm_seed, AES.MODE_ECB)
self._cfg_hi = secrets.randbits(self.rate_bits)
self._cfg_lo = secrets.randbits(self.cap_bits)
self._buf = bytearray()
self._done = False
self._value = None
if data:
self.update(data)
def _permute(self, x: int) -> int:
return int.from_bytes(self._cipher.encrypt(x.to_bytes(16, "big")), "big")
def _pad_to_blocks(self, msg: bytes) -> list[int]:
n = self.block_size
if not msg:
data = b"\x80" + b"\x00" * (n - 1)
else:
data = msg + b"\x80"
data += b"\x00" * (-len(data) % n)
print(data)
return [int.from_bytes(data[i : i + n], "big") for i in range(0, len(data), n)]
def _core(self, blocks: list[int], out_bits: int | None = None) -> int:
print(f'len={blocks}')
if out_bits is None:
out_bits = self.state_bits
b, r, c = self.state_bits, self.rate_bits, self.cap_bits
rm, cm = self._rate_mask, self._cap_mask
s0 = self._permute(((self._cfg_hi & rm) << c) | (self._cfg_lo & cm))
upper, lower, last_low = s0 >> c, s0 & cm, 0
print(f's0={s0}')
for blk in blocks:
mixed = ((upper << c) | lower) ^ blk
print(f'mixed={bin(mixed)}')
s = self._permute(mixed)
print(f's={bin(s)}')
upper, lower = s >> c, s & cm
last_low = mixed & cm
need = (out_bits + b - 1) // b
acc_hi = acc_lo = used_hi = used_lo = 0
cur_hi, cur_lo, cur_w = upper, lower, last_low
for i in range(need):
acc_hi = (acc_hi << r) | (cur_hi & rm)
used_hi += r
acc_lo = (acc_lo << c) | ((cur_w ^ cur_lo) & cm)
used_lo += c
if i < need - 1:
nxt_in = ((cur_hi & rm) << c) | (cur_lo & cm)
nxt = self._permute(nxt_in)
print(f'need={need}')
cur_hi, cur_lo, cur_w = nxt >> c, nxt & cm, nxt_in & cm
full = (acc_hi << used_lo) | acc_lo
return (full >> (used_hi + used_lo - out_bits)) & ((1 << out_bits) - 1)
def update(self, data: bytes):
if self._done:
raise ValueError("hash object already finalized")
self._buf.extend(data)
return self
def _compute(self) -> int:
return self._core(self._pad_to_blocks(bytes(self._buf)), self.state_bits)
def digest(self) -> bytes:
if not self._done:
self._value = self._compute()
self._done = True
return self._value.to_bytes(self.digest_size, "big")
def hexdigest(self) -> str:
return self.digest().hex()
def copy(self):
other = self.__class__.__new__(self.__class__)
other._perm_seed = self._perm_seed
other._cipher = AES.new(self._perm_seed, AES.MODE_ECB)
other._cfg_hi = self._cfg_hi
other._cfg_lo = self._cfg_lo
other._buf = bytearray(self._buf)
other._done = False
other._value = None
return other
@classmethod
def new(cls, data: bytes = b""):
h = cls()
if data:
h.update(data)
return h
|
一个自定义的hash函数主要看这里,前面其实就是填充这些,后面的话其实就是一个合并.
我们得到的,主要看这里,blocks是一个数组里面存储了若干个分好组的块.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| for blk in blocks:
mixed = ((upper << c) | lower) ^ blk
print(f'mixed={bin(mixed)}')
s = self._permute(mixed)
print(f's={bin(s)}')
upper, lower = s >> c, s & cm
last_low = mixed & cm
|
$mixed=s_{i-1}\oplus blk$
$s_{i}=AES(mixed)$
$s_{i}=uppper\ll 64|| lower$
$hash=upper\ll 64||(lower\oplus mix_{low})$
其实只要我们让最后一次的mixed输入的一样就行了.
这个碰撞前面两个可以不一样
$h_{1}=hash(IN_{1})$ $h_{2}=hash(IN_{2})$
$h_{1}=s_{1}\ll 64|| (s_{0}\oplus IN_{1}){low}$
$h{2}=st_{1}\ll 64|| (st_{0}\oplus IN_{2}){low}$
$IN{2low}=IN_{1low}$
$s_{1}\oplus st_{1}=h_{1}\oplus h_{2} \oplus IN_{1low} \oplus IN_{2low}$
然后我们可以用
$x_{1}\oplus x_{2}=s_{1}\oplus st_{2}$
$s_{1}\oplus x_{1}=x_{2}\oplus st_{2}$
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229
|
from __future__ import annotations
import os
from typing import Tuple
from pwn import context, remote
HOST = "1.14.196.78"
PORT = 42103
ROUNDS = 20 * 25
context.log_level = "info"
def pad_block_of_short_msg(msg: bytes) -> bytes:
"""Return the unique padded block for a <=15 byte message."""
assert len(msg) <= 15
return msg + b"\x80" + b"\x00" * (16 - len(msg) - 1)
def b2i(data: bytes) -> int:
return int.from_bytes(data, "big")
def i2b(x: int) -> bytes:
return x.to_bytes(16, "big")
def xor_bytes(a: bytes, b: bytes) -> bytes:
return bytes(x ^ y for x, y in zip(a, b))
def recv_prompt(io) -> None:
io.recvuntil(b"(hex): ")
def send_msg(io, msg: bytes) -> bytes:
io.sendline(msg.hex())
line = io.recvline(timeout=20)
if not line:
raise EOFError("server closed connection before returning hash")
text = line.decode(errors="ignore").strip()
assert text.startswith("H = "), text
return bytes.fromhex(text[4:])
def recv_round_result(io) -> None:
res = io.recvline(timeout=10)
if not res:
raise EOFError("server closed before round result")
print(res.decode(errors="ignore"), end="")
def derive_state_xor(h1: bytes, h2: bytes, blk1: bytes, blk2: bytes) -> bytes:
"""Recover s1 ^ s2 from the two one-block hashes."""
h1_hi, h1_lo = b2i(h1[:8]), b2i(h1[8:])
h2_hi, h2_lo = b2i(h2[:8]), b2i(h2[8:])
b1_lo, b2_lo = b2i(blk1[8:]), b2i(blk2[8:])
delta_hi = h1_hi ^ h2_hi
delta_lo = (h1_lo ^ h2_lo) ^ (b1_lo ^ b2_lo)
return i2b((delta_hi << 64) | delta_lo)
def play_round(io, round_idx: int) -> None:
short_a = b""
short_b = b"\x00"
blk_a = pad_block_of_short_msg(short_a)
blk_b = pad_block_of_short_msg(short_b)
recv_prompt(io)
h_a = send_msg(io, short_a)
recv_prompt(io)
h_b = send_msg(io, short_b)
delta = derive_state_xor(h_a, h_b, blk_a, blk_b)
x = os.urandom(16)
xp = xor_bytes(x, delta)
long_a = blk_a + x
long_b = blk_b + xp
recv_prompt(io)
send_msg(io, long_a)
recv_prompt(io)
send_msg(io, long_b)
recv_round_result(io)
def main() -> None:
io = remote(HOST, PORT)
try:
for rnd in range(1, ROUNDS + 1):
print(f"[+] Round {rnd}")
play_round(io, rnd)
while True:
chunk = io.recv(timeout=2)
if not chunk:
break
print(chunk.decode(errors="ignore"), end="")
finally:
io.close()
if __name__ == "__main__":
main()
|
第一次拿codex写的脚本,你别说给了思路就写改改就能用,不过写sage就依托了,以后有空看看能不能炼一个写sage的.
赛后体会
还需要多练TT.
避免通宵crypto,是真的困.还没用,清醒点还更容易有思路