RCTF-2025部分题目

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#AES(mix)_high||mix_low^AES(mix)^

#output=hig||low^statelow

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") # type: ignore



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 # reset finalized flag

other._value = None # clear cached digest

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#AES(mix)_high||mix_low^AES(mix)^

#output=hig||low^statelow

$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
#!/usr/bin/env python3





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

# low part compensates for the fact that output_low = s_low ^ mixed_low

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)



# read the final flag banner

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,是真的困.还没用,清醒点还更容易有思路


RCTF-2025部分题目
http://example.com/2025/11/19/RCTF-wp/
Beitragsautor
fox
Veröffentlicht am
November 19, 2025
Urheberrechtshinweis