0%

WHUCTF2025

WHUCTF2025复盘


  • LSFR_Signin

原题代码如下

1
2
3
4
5
6
7
8
9
10
11
12
13
from Crypto.Util.number import *
flag = b"whuctf{}"

flag = list(bin(bytes_to_long(flag))[2:])
assert(len(flag) == 255)

for i in range(len(flag)):
flag[i] = int(flag[i])

for i in range(2025):
flag.append(flag[i] ^ flag[i+20] ^ flag[i+25] ^ flag[i+250] ^ flag[-1])
print(flag[-1], end="")

已知flag为255位,对于线性寄存器,根据i i+20 i+25 i+250 以及最后一位就能解出下一位,于是我们从第256位开始往后,有

flagi = flagi + 20xorflagi + 25xorflagi + 250xorflagi + 254xorflagi + 256

i取0到254,而我们已知2552280位,取255510位来解密就可以,脚本如下

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
from Crypto.Util.number import long_to_bytes

# 初始化长度为255的全0数组
s = [0] * 255

# 给定的01串
bit_string = "110000011011110000010101100011111101011011111111111111101011000111001010111000101111101100011011000110011000100010011111110111010110000111111111111101111011011101000000010011110010111000110100110011101110101010110001110100111001100011100001001000000011010011001101001000000000110110100101000110000011011100011100001000010001110000111110000110010001110001101011101110100011010000101101000000000001101111111001010100011110110001101010010100011010011010010110010110100011001100010010010110110010010001111010111100011101100001111111110101011010011111110101000110010000101011011101000000111000001011010010001010101101111111001100010001001011100100111000010100011001001111011110111111101100111001011100001110110110100010011010011111110010111001101000011000011111001101100111001111000010011110011111001010001111110001010100100011001000100011001010010111010000011101011001111111010010010101001010011010000010000100001010111000000000010011011110110001101010010101001010100100010110001001000101000001011111010110101110111100101001100101011000010000101010001010111010111010010110001111010000001101101100101111001010010010011010101110001101001111011010001000010111010011010001011011011000111101010001101110000100100011010011111110110000001101100010011000110100010101010010101100101011001001100010100111011101111100010111100010001101100101100111110101001111101000010110110011000111100110101001111001100110111100111111000101101101000011110011001101100111100111001001001001100101111101110111011111110110101000001100010110101101100100001110100110101100101011010101101101100011011000001111001010001110000110001001011001001110111110000001000011000011000101010101010010010100010011011000011100111011101111110100101111111001011010110010010011101011001011110001101110110110111110100000100001111100101000001101010000011001001100100010101111010100000010110010010111000000010010101001011001011001111001000100010100101000011110110101001011111011111001010111101111000001101101100101111010101100110000111011101100100000011001110011000110110100101010100"

# 将01串添加到数组后面
for i in range(len(bit_string)):
s.append(int(bit_string[i]))

# 从i=254开始向前计算
for i in range(254, -1, -1):
s[i] = s[i+20] ^ s[i+25] ^ s[i+250] ^ s[i+254] ^ s[i+255]

# 截取前255位并转换为字符串
result_bits = ''.join(str(bit) for bit in s[:255])

# 转换为长整型然后转为字节
result_long = int(result_bits, 2)
result_bytes = long_to_bytes(result_long)

print(result_bytes)

#whuctf{quit3_ea5y_Sign1n_R1ght?}
  • RSAASR

题干:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
from Crypto.Util.number import getPrime, isPrime, bytes_to_long, long_to_bytes

def generate(bit):
while True:
p = getPrime(bit)
q = rev(p)
if isPrime(q):
break
return p, q


rev = lambda x: int(bin(x)[:1:-1], 2) # 二进制反转整数
flag = b"??????"
p, q = generate(512)
n = p * q
e = 65537
c = pow(bytes_to_long(flag), e, n)
print(f"n={n}")
print(f"e={e}")
print(f"c={c}")

p和q二进制颠倒,采用爆破的思路,同时估计p q的范围,并且每次取p q的低k位(k为已经爆破出来的位)相乘,对结果取低k位,如果和n的低k位相同就继续,不同就剪去,代码如下,板子参照博客:Crypto趣题-剪枝 | 糖醋小鸡块的blog (tangcuxiaojikuai.xyz)

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
from Crypto.Util.number import *
import sys
sys.setrecursionlimit(1500)

n=89260288112458610375700543707493254232809306221431627423709616690294586688526862549905410606087786699242563057156677052913617284849136716660502920085006747882186134482309361626185003661858419446057779826705477210404882478906671799290032009310469036065257789664458482249297907582602310789531951177426393110643
e=65537
c=34953739673730018843655174314108340461262205663805875643136393046216892771730195951086950749299233260612871271352091804579992550715616098448464010205976283620661044089962336249776561849400241337436006809354102892524119722533361144592982143227173415365371111087024439252557012289555411199194971295453523635612

def find(ph,qh):
l = len(ph)
pl = qh[::-1]
ql = ph[::-1]
p_max = ph + (512-2*l)*'1' + pl
q_max = qh + (512-2*l)*'1' + ql
p_min = ph + (512-2*l)*'0' + pl
q_min = qh + (512-2*l)*'0' + ql
if(int(p_max,2) * int(q_max,2) < n):
return
if(int(p_min,2) * int(q_min,2) > n):
return
if(int(pl,2) * int(ql,2) % (2**(l-1)) != n % (2**(l-1))):
return

if(l == 256):
pp0 = int(p_max,2)
if(n % pp0 == 0):
pf = pp0
qf = n//pp0
phi = (pf - 1)*(qf - 1)
d = inverse(e,phi)
m1 = pow(c,d,n)
print(long_to_bytes(m1))
exit()

else:
find(ph+'1',qh+'1')
find(ph+'1',qh+'0')
find(ph+'0',qh+'1')
find(ph+'0',qh+'0')

find('1','1')
# WHUCTF{cryptography_and_reverse}
  • *ez_lattice

格密码相关,题干

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
from Crypto.Util.number import *
flag = b"whuctf{}"
blen = 512

l = len(flag) // 4 #未知
n = 2
X = []
a = [bytes_to_long(flag[i * l: i * l + l]) for i in range(2)] #切成两部分
b = 0
p = getPrime(blen)

for i in range(2):
X.append(getRandomNBitInteger(blen))
b = (a[i] * X[i]) % p
assert b.bit_length() < 110

print("p =", p)
print("X =", X)

# p = 12478746590758967738992827236548867094406642228843048782158822830242432957850861746109083849369751421558416546441433265483311369062332823391326650330844473
# X = [4370703796271085517745653374714633557060694569231794372714420305839580193452505356598920188429238758568075323630107438853033389535935767953293146851021439, 5636765597544539887670148818611437395262628189014720546978418282055551396918915796702935478309173130501906553399905160951176701403838275497327658585404887]

n = 2
X = []
a = [bytes_to_long(flag[i * l: i * l + l]) for i in range(2, 4)]
print(a)
p = getPrime(blen)

for i in range(n):
X.append(getRandomNBitInteger(blen))
b = (a[i] * X[i]) % p
assert b.bit_length() <= 55

s = getRandomNBitInteger(55)
P = p - s

print("P =", P)
print("X =", X)

# P = 8064317391291915578249751043887298750752952396481901402238164933671762816998644264248732894561122039999833298392825353792148892469165631966482732750535761
# X = [6042201174605160506707043360458329015685676206288676104013330039569480295420873678739841513174948925787517746114885517054730046775608073287427260847787072, 6232867934334525782602291010514616748943593081406115516232887372014738839717093295759414233886061184914495957664550361507367497641317336980894814940037711]

简单分析可知分为两个部分,前半段是 b = kp + aiXi 本质是两个方程,这里有两种方法可以解决,都写一下

  • 1.根据两个方程造3*3的格子

    将上式展开,有 观察式子,不难得到矩阵运算式 接下来运用Hermite

    上界为 ,大概是170位,右边的目标向量长度取决于a1,a2,b但肯定不会超过上界,我们可以先解一下看看

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
X1 = X[0]
X2 = X[1]

# 构造 3×3 格基
L = Matrix(ZZ, [
[1, 0, X1],
[0, 1, X2],
[0, 0, p]
])

a1 = L.LLL()[0][0]
a2 = L.LLL()[0][1]
a1 = abs(a1)
a2 = abs(a2)
print(long_to_bytes(a1))
print(long_to_bytes(a2))
#b'whuctf{'
#b'Lattice'
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
L0 = Matrix(ZZ,[
[1,X[0]],
[0,p]
])

M0 = L0.LLL()[0]
v0 = L0.solve_left(M0)
for i in range(1,10000):
a0 = abs(v0[0]) * i
m0 = long_to_bytes(int(a0))
tag = True
for m in m0:
if not chr(m) in string.printable:
tag = false
break
if tag:
print(long_to_bytes(int(a0)))
  • *pollard&williams

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
from Crypto.Util.number import *
import os

flag = b'whuctf{}'
blen = 256


def rsa(p, q, message):
n = p * q
e = 65537

pad_length = n.bit_length() // 8 - len(message) - 2 #块长为 n的长度除8(字节数) 减去mes长度再减2
message += os.urandom(pad_length) #生成pad_length个随机字节
m = bytes_to_long(message)
return n, pow(m, e, n)


def part1(message1, message2):
while True:
p1 = getPrime(blen)
p2 = (p1 - 1) // 2 #相当于p1右移一位,且放掉最低位
if isPrime(p2):
break

q1 = getPrime(blen)
q2 = getPrime(blen)

return rsa(p1, q1, message1), rsa(p2, q2, message2)


def part2(message1, message2):
while True:
p1 = getPrime(blen)
p2 = (p1 + 1) // 2 #没有明显关系
if isPrime(p2):
break

q1 = getPrime(blen)
q2 = getPrime(blen)

return rsa(p1, q1, message1), rsa(p2, q2, message2)



assert len(flag) == 44
l = len(flag) // 4
m1, m2, m3, m4 = [flag[i * l: i * l + l] for i in range(4)]
# 切成四段
c1, c2 = part1(m1, m2)
c3, c4 = part2(m3, m4)

print(f'{c1 = }')
print(f'{c2 = }')
print(f'{c3 = }')
print(f'{c4 = }')

# c1 = (6053032598894343876848386724367478876865502990878797490385487692233771017587839889683279773931697102081210221515871925626229356354906807395177342943323369, 4066195854081844630643812355140109730178549671457699640787009592379117222130777528564788537029636082768525403919530491221982157867347461546035515101540809)
# c2 = (3881600892538209342174115382004433032693183438455968854185245139152150453077746028435728337685187304179257593974737056409431270271087770400534952463611803, 3170419555737452151768856928448822332346045957475336562622244748908867061340721719260259808765271614258250388620180512676045609008728482012225062330421389)
# c3 = (12299016617136978588548772285625358530978334196485520160172325214608426825374255755330322407319092229940503630270734074076341447314630647646764214262929507, 318163940794629731124968470499655451861010987042419720693423620230895540439020747998494269609254222775880714679954773027280497632868550785421041286883861)
# c4 = (4549315768074822845197072475333248869579555413221208949230121240611191001190288208256119819724334902434536556333152862828649067092565476816480268615884657, 1882968780168858989700488482275734089425710600149658668167954773629584030303631176914870357507995175067079535271674721507969999430710585448040194277936142)

基于pollard p-1和willams p+1两个大数分解算法

  • 1

    可知 由欧拉定理不难得到 直接算就行,代码如下

    1
    2
    3
    4
    p1 = gmpy2.gcd(gmpy2.powmod(3, 2 * n2, n1) - 1, n1)
    p2 = (p1 - 1) // 2
    q1 = n1 // p1
    q2 = n2 // p2
  • 2

    证明比较繁琐,先给出结论

    对于如下Lucas序列 D = A2 − 4,若p ∤ D

思路就是,枚举A,1到15往往就够了,找到一个D不是p的二次剩余,也就是有p + 1|m

这里我们容易发现p1 + 1|2n2,那么2n2就是我们代入的m,接下来用我们枚举的A,找到的m来迭代运算lucas序列,求出来之后做两次gcd即可,最终 p1 = gcd(gcd(V2n2 − 2, n1) , n1) 脚本在这里

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
def lucas_v(a,n):
v0 = 2
v1 = a
R = ZZ
M = matrix(R,[[a,-1],[1,0]])
v = M**(n-1) * vector(R,[v1,v0])
return v[0]

for a in range(2,10):
p3 = ZZ(gcd(lucas_v(a,2 * n4) - 2, n3))
if 1< p3 < n3:
break
p4 = (p3+1) // 2
q3 = n3 // p3
q4 = n4 // p4
  • *seista’s revenge

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
from Crypto.Util.Padding import pad
from Crypto.Util.number import *
from Crypto.Cipher import AES

from hashlib import *
flag = b"whuctf{}"
blen = 512

p = getPrime(blen)
s = getRandomNBitInteger(100)
P = p + s

t = 2
X = []
a = [getPrime(160) for _ in range(t)]
for i in range(t):
X.append(inverse(a[i], p))

key = sha256(str(a[0]*a[1]).encode()).digest()[:16]
iv = b"0" * 16
AES = AES.new(key, AES.MODE_CBC, iv)

print("X =", X)
print("P =", P)
print("ct =", AES.encrypt(pad(flag, AES.block_size)))

# X = [1266403423628708294851978766647131186574350037928491893316575383770634141679199238688724846443316942748685589080912612989737322832820423142859211423222170, 10633805933378187507165706136587361125130747673943368523389315948924728188453225153073019422908293191827053741582511390426559341625596650317484672418362991]
# P = 12727949469666331910572325155797935927989546075198211256583307434798528241134917675474139742863165705376701853130873014549089300596914514323642506815012401
# ct = b'\xe9\x87\x942\xbc\x94`t\x85^r\xb8\xd2\x00\xfb\xb0Ni\x08\xcf\x07\xf1\xae\x95U{\xf1\xd4\xda}@H'

# Lattice

ez_lattice的part2稍微改了一下上来的,求a0,a1就可以了

这里有 aiXi ≡ 1  modp

其实不论b怎么样,我们都是用类似的方法求解

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
from Crypto.Util.number import *
import gmpy2
from sage.all import *
import libnum
import string
from Crypto.Cipher import AES
from Crypto.Util.Padding import unpad
from hashlib import sha256

X = [1266403423628708294851978766647131186574350037928491893316575383770634141679199238688724846443316942748685589080912612989737322832820423142859211423222170, 10633805933378187507165706136587361125130747673943368523389315948924728188453225153073019422908293191827053741582511390426559341625596650317484672418362991]
P = 12727949469666331910572325155797935927989546075198211256583307434798528241134917675474139742863165705376701853130873014549089300596914514323642506815012401
ct = b'\xe9\x87\x942\xbc\x94`t\x85^r\xb8\xd2\x00\xfb\xb0Ni\x08\xcf\x07\xf1\xae\x95U{\xf1\xd4\xda}@H'
# 这里s只有100位,可以估计二者是几乎相等的


L0 = Matrix(ZZ,[
[1,X[0]],
[0,P]
])

M0 = L0.LLL()
W0 = M0[0]

v0 = L0.solve_left(W0)


L1 = Matrix(ZZ,[
[1,X[1]],
[0,P]
])

M1 = L1.LLL()
W1 = M1[0]

v1 = L1.solve_left(W1)


for i in range(1,10000):
a = abs(v0[0] * v1[0]) * i
key = sha256(str(a).encode()).digest()[:16]
iv = b"0" * 16
AES_cipher = AES.new(key, AES.MODE_CBC, iv)
decrypted_data = unpad(AES_cipher.decrypt(ct), AES.block_size)
m0 = decrypted_data.decode()
tag = True
for m in m0:
if not m in string.printable:
tag = false
break
if tag:
print(m0)


# whuctf{You_w1ll_never_kn0w_1t!}
  • onlyAES

交互题,代码如下

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
import os
import socket
import threading
from Crypto.Cipher import AES
from Crypto.Random import get_random_bytes
from Crypto.Util.Padding import pad, unpad
from string import ascii_lowercase


global flag


def xor(a, b):
if len(a) < len(b):
a, b = b, a
c = []
for i in range(len(a)):
c.append(a[i] ^ b[i % len(b)])
return bytes(c)


class myAES:
def __init__(self):
self.flag = flag
self.key = get_random_bytes(16)
self.cipher = AES.new(self.key, AES.MODE_ECB)

def encrypt1(self, data):
"""先异或flag,再加密"""
data = xor(data, self.flag)
pdata = pad(data, AES.block_size)
return self.cipher.encrypt(pdata).hex().encode()

def encrypt2(self, data):
"""拼接flag,将每个块的加密结果异或得到最终结果"""
data = data + self.flag
pdata = pad(data, AES.block_size)
c = self.cipher.encrypt(pdata)
C = [c[i : i + 16] for i in range(0, len(c), 16)]
for i in range(1, len(C)):
C[0] = xor(C[0], C[i])
return C[0].hex().encode()


def challenge(client: socket.socket):
cipher = myAES()
client.sendall(b"Here is an AES system, try hack it !\n")
client.sendall(b"\t1. Encrypt 1 \n")
client.sendall(b"\t2. Encrypt 2 \n")
while 1:
try:
client.sendall(b"your choice > ")
try:
cho = int(client.recv(1024).decode().strip())
except ValueError:
client.sendall(b"Invalid choice!\n")
continue
except:
break
if cho == 1:
client.sendall(b"Input your data(hex): ")
try:
data = bytes.fromhex(client.recv(1024).strip().decode())
if len(data) == 0:
client.sendall(b"No input!\n")
continue
client.sendall(b"Encrypted data(hex): " + cipher.encrypt1(data) + b"\n")
except Exception as e:
print(e)
client.sendall(b"Invalid data!\n")
elif cho == 2:
client.sendall(b"Input your data(hex): ")
try:
data = bytes.fromhex(client.recv(1024).strip().decode())
if len(data) == 0:
client.sendall(b"No input!\n")
continue
client.sendall(b"Encrypted data(hex): " + cipher.encrypt2(data) + b"\n")
except Exception as e:
print(e)
client.sendall(b"Invalid data!\n")
continue
else:
client.sendall(b"Invalid choice!\n")
continue


def main():
server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
server.bind(("0.0.0.0", 2025))
server.listen(2)
server.settimeout(5)
try:
while True:
try:
client, addr = server.accept()
client_thread = threading.Thread(target=challenge, args=(client,))
client_thread.start()
except socket.timeout:
pass
except KeyboardInterrupt:
server.close()


if __name__ == "__main__":
flag = os.getenv("GZCTF_FLAG")
flag = flag.lstrip("WHUCTF{").rstrip("}")
charset = ascii_lowercase + "_"
assert len(flag) % 16 == 3
assert all(c in charset for c in flag)
flag = flag.encode()
main()

可见就是选择一种加密方式,再输入data数据,进行加密,注意这里的异或方法

对于第一种加密方式,输入全0的data块,那么实际上就是对flag进行了一次加密,不妨设flag是两个块+3个字符,那么容易有填充后得到flag0 flag1 Pad(flag2)这三个块,加密后得到encflag0 encflag1 encPad(flag2)三个块,截取前面两个

对于第二种加密方式是先将flag拼接在输入的data后方,再16字节分块,加密之后用一个C存储各个块异或的结果,如果我们输入了Pad(flag2),那么Pad之后会得到 Pad(flag2) flag0 flag1 Pad(flag2) 加密得到encPad(flag2) encflag0 encflag1 encPad(flag2),异或的结果就会是encflag0 XOR encflag1,然而这个我们是知道的,由第一次加密的结果,也就是说,我们穷举flag3所有的可能,一共也就27^3种,pad之后传入,只要输出的值为encflag1 XOR encflag0 那么我们就构造对了

接下来,我们应用这里的异或性质,构造比flag长1位的0串,那么flag0的第一位会复制到flag3的末尾,这时候如法炮制爆破现在的flag3就可以了,能在27次以内搞出来,以此循环即可,稍微注意下当爆破位数超过16时候的块构造

贴一下出题人keaton师傅的题解

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
def enc1(data: bytes) -> bytes:
r.sendlineafter(b"your choice > ", b"1")
data = data.hex().encode()
r.sendlineafter(b"Input your data(hex): ", data)
encdata = r.recvline_contains(b"Encrypted data(hex): ").strip().split(b": ")[1]
return bytes.fromhex(encdata.decode())


def enc2(data: bytes) -> bytes:
r.sendlineafter(b"your choice > ", b"2")
data = data.hex().encode()
r.sendlineafter(b"Input your data(hex): ", data)
encdata = r.recvline_contains(b"Encrypted data(hex): ").strip().split(b": ")[1]
return bytes.fromhex(encdata.decode())


def getTail():
"""尾部有几个字节数据的单块"""
encflag = enc1(b"\x00" * flag_len)
print("flag密文:", encflag.hex()) #flag各个块加密后的结果
encflag = [encflag[i : i + 16] for i in range(0, len(encflag), 16)] #密文分块
for i in product(charset[::-1], repeat=tail_len):
data = ("".join(i)).encode()
print(data) #穷举所有可能的flag末尾3位
pdata = pad(data, AES.block_size)
res = enc2(pdata)
if res == xor(*encflag[: len(encflag) - 1]):
print("尾部:", data)
return data


def recover():
"""逐字节破解"""
flag = getTail()
mask = enc2(b"a" * 32) #得到encflag0 XOR encflag1 XOR encPad(flag2)
mask = xor(*[mask[i : i + 16] for i in range(0, len(mask), 16)])
for i in range(1, flag_len + 1 - tail_len):
c1 = enc1(b"\x00" * (flag_len + i))
c1 = [c1[_ : _ + 16] for _ in range(0, len(c1), 16)]
for j in charset:
data = flag + j.encode()
print(data)
pdata = pad(data, AES.block_size)
res = enc2(pdata)
if xor(res, mask) == xor(*c1[(flag_len // 16) :]):
flag += j.encode()
print(flag[tail_len:] + flag[:tail_len])
break
flag = flag[tail_len:] + flag[:tail_len]
flag = "WHUCTF{" + flag.decode() + "}"
print(flag)


if __name__ == "__main__":
tail_len = 3
flag_len = 35
recover()


碎碎念

第一次正式接触比赛中的密码题,和校内23 24级的其他师傅有着不小的差距,也是逐渐意识到了各类板子,思路的重要性,写复盘的初衷也是希望以后或许用得到其中的思路&脚本,不过python代码能力确实还得好好练习呢,加油