L3HCTF 2025 WriteUp

本文是对L3HCTF 2025的复现,包括Crypto和MISC两部分,记录了大概的做题过程,如有问题欢迎随时提出交流(^_^)

Crypto

EzECDSA

题目:

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
import hashlib
import random
from ecdsa import NIST256p, SigningKey

class FlawedNonceGenerator:
def __init__(self, n):
self.n = n
self.a = random.randrange(1, n)
self.b = random.randrange(1, n)
self.c = random.randrange(1, n)
self.last_k = random.randrange(1, n)

def generate_nonce(self):
current_k = self.last_k
next_k = (self.a * current_k**2 + self.b * current_k + self.c) % self.n
self.last_k = next_k

return current_k


curve = NIST256p
n = curve.order
print(f"Curve order n: {n}")
private_key = SigningKey.from_secret_exponent(random.randrange(1, n), curve=curve)
d = private_key.privkey.secret_multiplier
public_key = private_key.get_verifying_key()

messages = [
b"Hello player, welcome to L3HCTF 2025!",
b"This is a crypto challenge, as you can probably tell.",
b"It's about ECDSA, a very... robust algorithm.",
b"I'm sure there are no implementation flaws whatsoever.",
b"Anyway, here are your signatures. Good luck!",
f"Oh, and the flag is L3HCTF{{{d}}}. Don't tell anyone!".encode(),
]
nonce_generator = FlawedNonceGenerator(n)
f = open('signatures.txt', 'w')

for i in range(6):
k = nonce_generator.generate_nonce()
message = messages[i]
h = int.from_bytes(hashlib.sha256(message).digest(), 'big')
R = k * curve.generator
r = R.x() % n
s_inv = pow(k, -1, n)
s = (s_inv * (h + d * r)) % n
f.write(f"h: {h}, r: {r}, s: {s}\n")

分析: 利用所知道的关系列方程组,一共可以列出12个方程,总共有11个未知数,因此很有可能可以直接求出d,所以在sage中消元并解方程即可。

解答:

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
#sage
from ecdsa import NIST256p
import re
n = NIST256p.order

with open("signatures.txt") as f:
lines = f.readlines()

hs, rs, ss = [], [], []
for line in lines:
h = int(re.search(r"h: (\d+)", line).group(1))
r = int(re.search(r"r: (\d+)", line).group(1))
s = int(re.search(r"s: (\d+)", line).group(1))
hs.append(h)
rs.append(r)
ss.append(s)

R.<k1, k2, k3, k4, k5, k6, a, b, c, d> = PolynomialRing(Zmod(n))

eqs = [
k2 - (a*k1^2 + b*k1 + c),
k3 - (a*k2^2 + b*k2 + c),
k4 - (a*k3^2 + b*k3 + c),
k5 - (a*k4^2 + b*k4 + c),
k6 - (a*k5^2 + b*k5 + c),
ss[0]*k1 - (hs[0] + rs[0]*d),
ss[1]*k2 - (hs[1] + rs[1]*d),
ss[2]*k3 - (hs[2] + rs[2]*d),
ss[3]*k4 - (hs[3] + rs[3]*d),
ss[4]*k5 - (hs[4] + rs[4]*d),
ss[5]*k6 - (hs[5] + rs[5]*d)
]

I = ideal(eqs)
G = I.groebner_basis()
#print(G)
for g in G:
if g.variables() == (d,):
poly = g
break

sols = poly.univariate_polynomial().roots(multiplicities=False)[0]
print(f"L3HCTF{{{sols}}}")

math-problem

题目:

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
import gmpy2
from gmpy2 import *
from Crypto.Util.number import *
from random import randint
from gmpy2 import invert
from scret import flag

def myfunction(num):
output = 0
output=num**3
return output

if __name__ == '__main__':
flag_len = len(flag)
p, q = getPrime(512), getPrime(512)

while True:
r = getPrime(512)
R = bytes_to_long(str(r).encode())
if isPrime(R):
break

n = p * q * r
hint1 = R * r
mod = myfunction(n)
hint2 = pow(3*n+1, p % (2 ** 400), mod)
m = bytes_to_long(flag)
c = pow(m, 65537, n)

print('All data:')
print(f'n = {n}')
print(f'c = {c}')
print(f'hint1 = {hint1}')
print(f'hint2 = {hint2}')


'''
All data:
n = 1031361339208727791691298627543660626410606240120564103678654539403400080866317968868129842196968695881908504164493307869679126969820723174066217814377008485456923379924853652121682069359767219423414060835725846413022799109637665041081215491777412523849107017649039242068964400703052356256244423474207673552341406331476528847104738461329766566162770505123490007005634713729116037657261941371410447717090137275138353217951485412890440960756321099770208574858093921
c = 102236458296005878146044806702966879940747405722298512433320216536239393890381990624291341014929382445849345903174490221598574856359809965659167404530660264493014761156245994411400111564065685663103513911577275735398329066710295262831185375333970116921093419001584290401132157702732101670324984662104398372071827999099732380917953008348751083912048254277463410132465011554297806390512318512896160903564287060978724650580695287391837481366347198300815022619675984
hint1 = 41699797470148528118065605288197366862071963783170462567646805693192170424753713903885385414542846725515351517470807154959539734665451498128021839987009088359453952505767502787767811244460427708303466073939179073677508236152266192609771866449943129677399293427414429298810647511172104050713783858789512441818844085646242722591714271359623474775510189704720357600842458800685062043578453094042903696357669390327924676743287819794284636630926065882392099206000580093201362555407712118431477329843371699667742798025599077898845333
hint2 = 10565371682545827068628214330168936678432017129758459192768614958768416450293677581352009816968059122180962364167183380897064080110800683719854438826424680653506645748730410281261164772551926020079613841220031841169753076600288062149920421974462095373140575810644453412962829711044354434460214948130078789634468559296648856777594230611436313326135647906667484971720387096683685835063221395189609633921668472719627163647225857737284122295085955645299384331967103814148801560724293703790396208078532008033853743619829338796313296528242521122038216263850878753284443416054923259279068894310509509537975210875344702115518307484576582043341455081343814378133782821979252975223992920160189207341869819491668768770230707076868854748648405256689895041414944466320313193195829115278252603228975429163616907186455903997049788262936239949070310119041141829846270634673190618136793047062531806082102640644325030011059428082270352824026797462398349982925951981419189268790800571889709446027925165953065407940787203142846496246938799390975110032101769845148364390897424165932568423505644878118670783346937251004620653142783361686327652304482423795489977844150385264586056799848907
'''

分析:
(1)Part1:
$\because n = p\cdot q\cdot r,hint1=R\cdot r$
$\therefore gcd(n,hint1) = kr,这里的k=1或p或q或pq$
$\because 可以查看gcd(n,hint1).nbits()=512,则k=1$
$\therefore r=gcd(n,hint1)$
(2)Part2:
$\because hint2 = (3n+1)^{p \mod 2^{400}}\mod n^3,设p_{low}= p\mod 2^{400}$
$\therefore$ 由二项式定理得,

$$ hint2 = (_2^{p_{low}})9n^2+(^{p_{low}}_1)3n+1 \mod n^3 $$

$\therefore hint2 = \frac{p_{low}\cdot (p_{low}-1)}{2}\cdot 9n^2 + p_{low}\cdot 3n+1\mod n^3$
$\therefore hint2 \equiv p_{low}\cdot 3n+1 \mod n^2$
$\therefore (hint2-1)//n \equiv p_{low}\cdot 3$
$\therefore p_{low} \equiv (hint2-1)\cdot 3^{-1}\mod n$
之后利用coppersmith定理
解答:

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
#sage
import gmpy2
from Crypto.Util.number import long_to_bytes,inverse

n = 1031361339208727791691298627543660626410606240120564103678654539403400080866317968868129842196968695881908504164493307869679126969820723174066217814377008485456923379924853652121682069359767219423414060835725846413022799109637665041081215491777412523849107017649039242068964400703052356256244423474207673552341406331476528847104738461329766566162770505123490007005634713729116037657261941371410447717090137275138353217951485412890440960756321099770208574858093921
c = 102236458296005878146044806702966879940747405722298512433320216536239393890381990624291341014929382445849345903174490221598574856359809965659167404530660264493014761156245994411400111564065685663103513911577275735398329066710295262831185375333970116921093419001584290401132157702732101670324984662104398372071827999099732380917953008348751083912048254277463410132465011554297806390512318512896160903564287060978724650580695287391837481366347198300815022619675984
hint1 = 41699797470148528118065605288197366862071963783170462567646805693192170424753713903885385414542846725515351517470807154959539734665451498128021839987009088359453952505767502787767811244460427708303466073939179073677508236152266192609771866449943129677399293427414429298810647511172104050713783858789512441818844085646242722591714271359623474775510189704720357600842458800685062043578453094042903696357669390327924676743287819794284636630926065882392099206000580093201362555407712118431477329843371699667742798025599077898845333
hint2 = 10565371682545827068628214330168936678432017129758459192768614958768416450293677581352009816968059122180962364167183380897064080110800683719854438826424680653506645748730410281261164772551926020079613841220031841169753076600288062149920421974462095373140575810644453412962829711044354434460214948130078789634468559296648856777594230611436313326135647906667484971720387096683685835063221395189609633921668472719627163647225857737284122295085955645299384331967103814148801560724293703790396208078532008033853743619829338796313296528242521122038216263850878753284443416054923259279068894310509509537975210875344702115518307484576582043341455081343814378133782821979252975223992920160189207341869819491668768770230707076868854748648405256689895041414944466320313193195829115278252603228975429163616907186455903997049788262936239949070310119041141829846270634673190618136793047062531806082102640644325030011059428082270352824026797462398349982925951981419189268790800571889709446027925165953065407940787203142846496246938799390975110032101769845148364390897424165932568423505644878118670783346937251004620653142783361686327652304482423795489977844150385264586056799848907

r = gmpy2.gcd(hint1, n)
t = n // r

#恢复 p 的低 400 位 k
inv3 = inverse(3,n)
p_low = ((hint2-1)//n*inv3) %n

#使用 Coppersmith 方法恢复完整的 p
M = 2**400
P.<x> = PolynomialRing(Zmod(t))
f = x*2^400 + p_low
f = f.monic()
roots = f.small_roots(X=2^112,beta=0.44)

p = int(roots[0])*2^400 + p_low
q = t // p
phi = (p - 1) * (q - 1)*(r-1)
d = inverse(65537, phi)
m = pow(c, d, n)
print(long_to_bytes(m))

RRRSSSAAA

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
from sage.all import *
from secret import flag

def generate_vulnerable_key(bits=1024):
p_bits = bits // 2
q_bits = bits - p_bits
#p,q接近

while True:
p = random_prime(2**(p_bits), lbound=2**(p_bits-1))
q = random_prime(2**(q_bits), lbound=2**(q_bits-1))
if p != q and p > q and p < 2*q:#q<p<2*q
break

N = p * q
phi = (p**4 - 1) * (q**4 - 1)

d_bits = 1024
d_bound = 2**d_bits

while True:
d_small = randint(2, d_bound)
d = phi - d_small
if gcd(d, phi) == 1:
if d_small.bit_length() == 1021:
break

e = inverse_mod(d, phi)

return N, e

def encrypt(m, N, e):
n = 4
r = 2
R = Integers(N)
P = PolynomialRing(R, 't')
t = P.gen()
Q = P.quotient(t**n - r)

m_poly = Q([m, 0, 0, 0])

c_poly = m_poly ** e

return c_poly.lift()

if __name__ == "__main__":
N, e = generate_vulnerable_key()
m = int.from_bytes(flag, 'big')
c = encrypt(m, N, e)

print(f"N = {N}")
print(f"e = {e}")
print(f"c = {c}")

# N = 99697845285265879829811232968100099666254250525000506525475952592468738395250956460890611762459685140661035795964867321445992110528627232335703962897072608767840783176553829502743629914407970206513639916759403399986924602596286330464348286080258986075962271511105387188070309852907253486162504945490429185609
# e = 74900336437853271512557457581304251523854378376434438153117909482138661618901386551154807447783262736408028580620771857416463085746907317126876189023636958838207330193074215769008709076254356539808209005917645822989554532710565445155350102802675594603406077862472881027575871589046600011223990947361848608637247276816477996863812313225929441545045479384803449990623969591150979899801722841101938868710054151839628803383603849632857020369527380816687165487370957857737696187061619496102857237814447790678611448197153594917852504509869007597997670022501500067854210261136878917620198551551460145853528269270832725348151160651020188255399136483482428499340574623409209151124687319668989144444549871527949104436734300277004316939985015286758651969045396343970037328043635061226100170529991733947365830164811844853806681198818875837903563263114249814483901121700854712406832325690101810786429930813776784979083590353027191492894890551838308899148551566437532914838098811643805243593419063566975400775134981190248113477610235165151367913498299241375039256652674679958159505112725441797566678743542054295794919839551675786573113798857814005058856054462008797386322048089657472710775620574463924678367455233801970310210504653908307254926827
# c = 98460941530646528059934657633016619266170844887697553075379408285596784682803952762901219607460711533547279478564732097775812539176991062440097573591978613933775149262760936643842229597070673855940231912579258721734434631479496590694499265794576610924303262676255858387586947276246725949970866534023718638879

分析:
维纳攻击
这里回顾一下RSA维纳攻击的原理:
(1)攻击条件:
$d<\frac{1}{3}N^{\frac{1}{4}}$

为什么有这个条件呢,这里不得不提到勒让德定理(Legendre‘s theorem):
若$|\alpha - \frac{p}{q}|< \frac{1}{2q^2},则有\frac{p}{q}一定是\alpha的收敛子$

$\because ed = k\phi(N)+1$
$\therefore 两边同时除以d\phi(N),有\frac{c}{\phi(N)}-\frac{k}{d}=\frac{1}{d\phi(N)}$根据条件可以大概推出是满足勒让德定理的,但是我们不知道$\phi(N)$,因此要想办法用$N$代替。同时可以得到$\frac{k}{d}\approx \frac{e}{\phi(N)},则有k<d$
$\because N\approx \phi(N)$
$\therefore 记s=p+q-1计算|\frac{e}{N}-\frac{k}{d}|=|\frac{ed-kN}{dN}|=|\frac{k\phi(N)+1-kN}{d}N|=|\frac{1-ks}{dN}|$
$\because s=p+q-1<3\sqrt N,k<d,d<\frac{1}{3}N^{\frac{1}{4}}$
$\therefore |\frac{e}{N}-\frac{k}{d}|=|\frac{1-ks}{dN}|<|\frac{ks}{dN}|<|\frac{s}{N}|<\frac{3}{\sqrt N}<\frac{1}{3d^2}<\frac{1}{2d^2}$
$\therefore \frac{k}{d}是\frac{e}{N}的收敛子$
参考:wiener attack | Cot287’s Blog
结合本题:
本题中有,$2^{1020}\leq d_{small} <2^{1021},虽然N为1024位,$但结合上边的分析可知,我们使用的不一定是$N$,而是与$\phi(N)$相近的一个已知的数。且上边的$d$也不一定是$d$,只要是能够满足勒让德定理的一个关键的数即可,而这里$d=phi - d_{small}$,因此需要通过推导,让这里的$d_{small}$占据之前$d$的位置
$\because \phi(N)=(p^4-1)(q^4-1)$
$\therefore 这里使用N_4 = N^4=p^4q^4$
$\because ed =k\phi(N)+1$
$\therefore e(\phi(N)-d_{small})=k\phi(N)+1$
$\therefore ed_{small} = k\phi(N)-1$
$\therefore 求\frac{e}{N_4}$的连分数便能求得$\phi(N)$和$d$,之后便能够利用求得的$d$在简单的环上$RSA$解密
解答:

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

def decrypt(c_poly, N, d):
R = Integers(N)
P = PolynomialRing(R, 't')
t = P.gen()
Q = P.quotient(t**4 - 2)

c = Q(c_poly)
m_poly = c ** d
return m_poly.lift()[0]

def attack_d(N, e, c):
N = Integer(N)
e = Integer(e)
N4 = N ^ 4
cf = (e / N4).continued_fraction().convergents()

for conv in cf:
k = conv.numerator()
d_small = conv.denominator()

if d_small < 2^1020 or d_small >= 2^1021:
continue

if (e * d_small + 1) % k != 0:
continue
phi_candidate = (e * d_small + 1) // k
#print("phi=",phi_candidate)
d = phi_candidate - d_small
return d

print("Failed to find d")
return None, None


N = 99697845285265879829811232968100099666254250525000506525475952592468738395250956460890611762459685140661035795964867321445992110528627232335703962897072608767840783176553829502743629914407970206513639916759403399986924602596286330464348286080258986075962271511105387188070309852907253486162504945490429185609
e = 74900336437853271512557457581304251523854378376434438153117909482138661618901386551154807447783262736408028580620771857416463085746907317126876189023636958838207330193074215769008709076254356539808209005917645822989554532710565445155350102802675594603406077862472881027575871589046600011223990947361848608637247276816477996863812313225929441545045479384803449990623969591150979899801722841101938868710054151839628803383603849632857020369527380816687165487370957857737696187061619496102857237814447790678611448197153594917852504509869007597997670022501500067854210261136878917620198551551460145853528269270832725348151160651020188255399136483482428499340574623409209151124687319668989144444549871527949104436734300277004316939985015286758651969045396343970037328043635061226100170529991733947365830164811844853806681198818875837903563263114249814483901121700854712406832325690101810786429930813776784979083590353027191492894890551838308899148551566437532914838098811643805243593419063566975400775134981190248113477610235165151367913498299241375039256652674679958159505112725441797566678743542054295794919839551675786573113798857814005058856054462008797386322048089657472710775620574463924678367455233801970310210504653908307254926827
c = 98460941530646528059934657633016619266170844887697553075379408285596784682803952762901219607460711533547279478564732097775812539176991062440097573591978613933775149262760936643842229597070673855940231912579258721734434631479496590694499265794576610924303262676255858387586947276246725949970866534023718638879

d = attack_d(N, e, c)
ans = long_to_bytes(int(decrypt(c,N,d)))
print(ans)

MISC

量子双生影

分析:
本题中给了一个stream2.rar压缩包,查看二进制数据,发现压缩包中存在两个文件,然而直接解压只能够解压出一个图片文件。

扫码可以得到一个提示:

1
flag is not here, but I can give you the key: "quantum"

存在需要密码的隐写,由于还有一个文件没有解压出来,猜测是另一个文件的隐写密码为quantum
那么现在需要优先得到第二个文件,分析二进制数据

发现第二个文件像是一种数据流形式,结合文件名也是stream,猜测可能是NTFS 备用数据流隐写(ADS)。
使用ntfsstreamseditor工具进行分离数据流
成功导出,得到第二张图片文件

结合前边给出的密码,结合图片像是两张图片的融合,猜测需要先使用两张图片进行XOR,然后再使用quantum作为密码再次进行XOR,这里直接写一个简单脚本进行解密:

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
from PIL import Image
import numpy as np

def xor_images(path1, path2, out_path, key_str="quantum"):
im1 = Image.open(path1).convert("RGB")
im2 = Image.open(path2).convert("RGB")
if im1.size != im2.size:
raise ValueError("两张图片尺寸必须完全相同")

arr1 = np.array(im1, dtype=np.uint8)
arr2 = np.array(im2, dtype=np.uint8)

xor_arr = np.bitwise_xor(arr1, arr2)

key_bytes = key_str.encode()
flat = xor_arr.flatten()
key_extended = np.resize(np.frombuffer(key_bytes, dtype=np.uint8),flat.shape)
final_flat = np.bitwise_xor(flat, key_extended)

final_arr = final_flat.reshape(xor_arr.shape)
Image.fromarray(final_arr).save(out_path)
print(f"已保存结果到 {out_path}")

if __name__ == "__main__":
xor_images("1.webp", "stream2.webp", "out.png", key_str="quantum")

第一次异或之后的结果:

第二次异或之后的结果:

扫码得到flag:L3HCTF{Quantum_ADS_XOR}

Please Sign In

题目:
embedding.json文件+
server.py

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
import uvicorn
import torch
import json
import os
from fastapi import FastAPI, File, UploadFile
from PIL import Image
from torchvision import transforms
from torchvision.models import shufflenet_v2_x1_0, ShuffleNet_V2_X1_0_Weights

feature_extractor = shufflenet_v2_x1_0(weights=ShuffleNet_V2_X1_0_Weights.IMAGENET1K_V1)
feature_extractor.fc = torch.nn.Identity()
feature_extractor.eval()

weights = ShuffleNet_V2_X1_0_Weights.IMAGENET1K_V1
transform = transforms.Compose([
transforms.ToTensor(),
])

if not os.path.exists("embedding.json"):
user_image = Image.open("user_image.jpg").convert("RGB")
user_image = transform(user_image).unsqueeze(0)
with torch.no_grad():
user_embedding = feature_extractor(user_image)[0]

with open("embedding.json", "w") as f:
json.dump(user_embedding.tolist(), f)

user_embedding = json.load(open("embedding.json", "r"))
user_embedding = torch.tensor(user_embedding, dtype=torch.float32)
user_embedding = user_embedding.unsqueeze(0)

app = FastAPI()

@app.post("/signin/")
async def signin(file: UploadFile = File(...)):
submit_image = Image.open(file.file).convert("RGB")
submit_image = transform(submit_image).unsqueeze(0)
with torch.no_grad():
submit_embedding = feature_extractor(submit_image)[0]
diff = torch.mean((user_embedding - submit_embedding) ** 2)
result = {
"status": "L3HCTF{test_flag}" if diff.item() < 5e-6 else "failure"
}
return result

@app.get("/")
async def root():
return {"message": "Welcome to the Face Recognition API!"}

if __name__ == "__main__":
uvicorn.run(app, host="0.0.0.0", port=8000)

分析:
该题目的解题思路就是利用embedding.json逆向生成一张图片,使两者的embedding向量均方差值差异小于5e-6。可以直接AI一个脚本,注意要将最后得到的图片保存为无损压缩的png图片。
代码:

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
import torch
import torchvision.transforms as transforms
from torchvision.models import shufflenet_v2_x1_0, ShuffleNet_V2_X1_0_Weights
from PIL import Image
import json
import numpy as np

# 加载模型和user_embedding
feature_extractor = shufflenet_v2_x1_0(weights=ShuffleNet_V2_X1_0_Weights.IMAGENET1K_V1)
feature_extractor.fc = torch.nn.Identity()
feature_extractor.eval()

with open("embedding.json", "r") as f:
user_embedding = torch.tensor(json.load(f), dtype=torch.float32)

# 初始化随机图片
image = torch.randn(1, 3, 224, 224, requires_grad=True) # 随机初始化
optimizer = torch.optim.Adam([image], lr=0.1)
criterion = torch.nn.MSELoss()

# 优化循环
for step in range(2000):
optimizer.zero_grad()
generated_embedding = feature_extractor(image)[0]
loss = criterion(generated_embedding, user_embedding)
loss.backward()
optimizer.step()
# 限制像素值范围
image.data = torch.clamp(image.data, 0, 1)
if step % 100 == 0:
print(f"Step {step}, Loss: {loss.item()}")
if loss.item() < 5e-6:
break

# 保存生成的图片
generated_img = image.detach().squeeze(0).permute(1, 2, 0).numpy()
generated_img = (generated_img * 255).astype(np.uint8)
Image.fromarray(generated_img).save("generated_user_image.png")

接下来cmd执行:

1
curl -F "file=@generated_user_image.png"  http://1.95.8.146:50001/signin/

LearnRAG

分析:
RAG(检索增强生成) 是一种结合了信息检索技术与语言生成模型的人工智能技术。它将检索器和大语言模型(LLMs)结合使用,使得模型能够通过搜索库实时更新内容,可以实时查询外部知识。
查看文件字符串:

发现该pickle文件存储了以下内容:

  • 一个名为RagData的用户自定义类实例
  • sentence-transformers/gtr-t5-base模型的嵌入向量
    因此本题的思路便是通过嵌入向量embedding来反转嵌入,项目地址:GitHub - vec2text/vec2text
    稍微修改一下项目给出的脚本代码即可。
    代码:
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
import vec2text
import torch
import pickle
from transformers import AutoModel, AutoTokenizer

#跳过空壳类
class RagData:
pass

encoder = AutoModel.from_pretrained("sentence-transformers/gtr-t5-base").encoder.to("cuda")
tokenizer = AutoTokenizer.from_pretrained("sentence-transformers/gtr-t5-base")
corrector = vec2text.load_pretrained_corrector("gtr-base")

with open('rag_data.pkl', 'rb') as f:
obj = pickle.load(f)
embeddings = torch.tensor(obj.embeddings, dtype=torch.float32)


texts = vec2text.invert_embeddings(
embeddings=embeddings.cuda(),
corrector=corrector,
num_steps=20,
)
for i in texts:
print(i.strip())

PaperBack

根据题目提供的线索进行搜索,可以找到一款OllyDbg开发的名为PaperBack的工具,它能够将文件以超大位图的形式备份到普通纸张上。
工具:PaperBack
使用工具的Open Bitmap功能可以得到flag.ws文件,观察文件后缀和文件内容,推断应该是Whitespace代码,直接在线工具运行即可得到flag。

Why not read it out

先查看文件二进制数据,发现文件末尾像是base64

尝试之后,发现是base64逆序,解码之后得到提示IGN Review

同时观察到该文件是jpg文件,将后缀名改为.jpg

有点没看懂,谷歌识图一下,发现是一款游戏Trunic中的语言,结合前面得到的提示,搜索IGN review Trunic

前往IGN Reviewhttps://www.ign.com/articles/tunic-review-xbox-pc-steam 查看之后,发现前两段跟图片中的密文类似,因此便可以一个一个对照得到对照表,从而解出后五句关键密文。

解出的明文如下:

1
2
3
4
5
1. The content of flag is: come on little brave fox.
2. Replace letter O with number 0, letter L with number 1.
3. Replace letter A with symbol at(@).
4. Make every letter E uppercase.
5. Use underline to link each word.

L3HCTF 2025 WriteUp
http://ramoor.github.io/2025/07/20/L3HCTF 2025 WriteUp/
作者
Ramoor
发布于
2025年7月20日
许可协议