2025 “泉城杯” 济南市网络安全大赛 题目复现

其实就是去年大概七月的事情 感觉时间好快 马上又要比CTF了 准备拿一些之前没做出来的逆向密码深入学习一下下(

2025 “泉城杯” 济南市网络安全大赛 题目分享:https://blog.x-z-z.com/article/2025-07-26-19-30

Misc

b64

1
2
3
4
5
题目名称:b64
题目内容:b64
题目分值:25.0
题目难度:非常容易
相关附件:b64.zip

打开压缩包 内容如下

1
REFTQ1RGe0hlbGxvX1dvcmxkfQ==

暗示Base64解码 进行解码 得到答案

image-20250715103825106

misc-pic-1-2

1
2
3
4
5
题目名称:misc-pic-1-2
题目内容:数码相机照片中的秘密
题目分值:25.0
题目难度:非常容易
相关附件:misc-pic-1-2的附件.zip

解压压缩包 找到图片文件 右键点击属性

image-20250715104220608

复制备注 进行解码 解题成功

image-20250715104237917

ezusb

1
2
3
4
5
题目名称:ezusb
题目内容:大黑阔给我发了一个流量包和一串看不懂的字符,里面究竟藏了什么惊天大秘密呢?
题目分值:50.0
题目难度:容易
相关附件:ezusb的附件.zip

这道题 打开发现是一个sercet.txt和flag.pcapng

image-20260412171559813

打开后发现协议都为usb 用UsbKeyboardDataHacker解密

image-20260412171645986

用UsbKeyboardDataHacker解密数据包

1
python3 UsbKeyboardDataHacker.py --input ~/Downloads/ezusb/flag.pcapng

image-20260415154613741

1
congratulations,you<SPACE>finlly<SPACE>find<SPACE>me,but<SPACE>what<SPACE>i<SPACE>want<SPACE>to<SPACE>tell<SPACE>you<SPACE>is<SPACE>that<SPACE>roman<SPACE>roland<SPACE>once<SPACE>said<SPACE>thar<SPACE>there<SPACE>is<SPACE>only<SPACE>one<SPACE>kind<SPACE>of<SPACE>heroism<SPACE>in<SPACE>the<SPACE>worlld,that<SPACE>is<SPACE>to<SPACE>know<SPACE>the<SPACE>cruelty<SPACE>of<SPACE>the<SPACE>life<SPACE>but<SPACE>still<SPACE>love<SPACE>it.<CAP><CAP>ok,<CAP><CAP>get<SPACE>to<SPACE>the<SPACE>point:the<SPACE>{}_<SPACE>three<SPACE>symbols<SPACE>were<SPACE>added<SPACE>to<SPACE>the<SPACE>front<SPACE>of<SPACE>the<SPACE>base64<SPACE>table<SPACE>and<SPACE>handed<SPACE>to<SPACE>caesar.if<SPACE>you<SPACE>can<SPACE>decrypto<SPACE>the<SPACE>sercet<SPACE>you<SPACE>can<SPACE>get<SPACE>the<SPACE>half<SPACE>of<SPACE>flag.

可以得到 base64的编码表前需要加入 “{}_” 而且之前正好提供了一个密文

image-20260415154807016

用随波逐流 再配合表前加 “{}” 得到flag前段 DASCTF{JUST_3aSY_Ca3SaR_aND_MaUSE

image-20260420125250055

提取设备的按键码

1
tshark -r flag.pcapng -T fields -e usb.capdata > usbdata.txt

用python脚本解析里面不同的位 并翻译成二进制 最后转ascii码

1
2
3
4
5
with open('usbdata.txt', 'r') as f:
bits = [line[2:4] for line in f if len(line) == 15 and line[2:4] != "00"]
binary = ''.join(bits).replace('02', '0').replace('01', '1')
result = ''.join(chr(int(binary[i:i+8], 2)) for i in range(0, len(binary), 8))
print(result)

最后getflag的尾段 Keyb0rad_@nd_USB!}

image-20260420143018421

将flag组合一下可得 DASCTF{JUST_3aSY_Ca3SaR_aND_MaUSEKeyb0rad_@nd_USB!}

Reverse

ezre_1

1
2
3
4
5
题目名称:ezre_1
题目内容:这是一个普通的逆向题
题目分值:50.0
题目难度:容易
相关附件:ezre_1的附件.zip

先查一下有没有壳 发现有upx壳

image-20260401222610754

用upx直接脱壳即可

1
.\upx.exe -d ezre.exe

image-20260401222714402

丢到ida 看样子反编译成功啦

image-20260401222905633

点进check 看一下函数逻辑

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
int check()
{
unsigned int v0; // eax
char Str[1024]; // [rsp+90h] [rbp+10h] BYREF
char Str2[48]; // [rsp+490h] [rbp+410h] BYREF
char v4[16]; // [rsp+4C0h] [rbp+440h] BYREF
char Buffer[8]; // [rsp+4D0h] [rbp+450h] BYREF
char *Str1; // [rsp+4D8h] [rbp+458h]
void *v7; // [rsp+4E0h] [rbp+460h]
void *v8; // [rsp+4E8h] [rbp+468h]

system("cls");
puts("welcome to DASCTF");
puts("plz check your flag here!!!!");
strcpy(Buffer, "correct");
strcpy(v4, "error");
strcpy(Str2, "aOYanlkVkemSmRgYlWi0Nc1P3JPIfoMoQJ2I20w=");
v8 = malloc(0x37ui64);
printf("plz input your flag:");
scanf("%s", Str);
getchar();
v7 = malloc(0x400ui64);
memset(v7, 0, 0x400ui64);
v0 = strlen(Str);
v8 = (void *)base64_encode(Str, v7, v0);
Str1 = (char *)malloc(0x64ui64);
Str1 = (char *)chang(v8);
if ( !strcmp(Str1, Str2) )
return puts(Buffer);
else
return puts(v4);
}

预设正确的密文Str2 = "aOYanlkVkemSmRgYlWi0Nc1P3JPIfoMoQJ2I20w="

对输入进行 Base64 编码 v8 = base64_encode(Str, v7, v0)

再经过 chang() 函数处理 Str1 = chang(v8) 最后有个比较

这样的话再进入chang函数看逻辑即可

image-20260402193321240

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
char *__fastcall chang(const char *a1)
{
void *v1; // rsp
int v2; // eax
void *v3; // rsp
__int64 v5[3]; // [rsp+20h] [rbp-60h] BYREF
char *Source; // [rsp+38h] [rbp-48h]
__int64 v7; // [rsp+40h] [rbp-40h]
char *Destination; // [rsp+48h] [rbp-38h]
__int64 v9; // [rsp+50h] [rbp-30h]
int v10; // [rsp+5Ch] [rbp-24h]
__int64 *v11; // [rsp+60h] [rbp-20h]
__int64 *v12; // [rsp+68h] [rbp-18h]
int i; // [rsp+74h] [rbp-Ch]
int v14; // [rsp+78h] [rbp-8h]
int v15; // [rsp+7Ch] [rbp-4h]

v10 = strlen(a1);
v15 = 0;
v14 = 0;
v9 = v10 + 1 - 1i64;
v5[0] = v10 + 1;
v5[1] = 0i64;
v1 = alloca(16 * ((unsigned __int64)(v5[0] + 15) >> 4));
Destination = (char *)v5;
v2 = (v10 + 1) / 2;
v7 = v2 - 1i64;
v3 = alloca(16 * ((unsigned __int64)(v2 + 15i64) >> 4));
Source = (char *)v5;
v12 = v5;
v11 = v5;
for ( i = 0; i < v10; ++i )
{
if ( a1[i] == 32 )
{
++v14;
}
else if ( v15 )
{
*(_BYTE *)v11 = a1[i];
v11 = (__int64 *)((char *)v11 + 1);
v15 = 0;
}
else
{
*(_BYTE *)v12 = a1[i];
v12 = (__int64 *)((char *)v12 + 1);
v15 = 1;
}
}
*(_BYTE *)v12 = 0;
*(_BYTE *)v11 = 0;
strcat(Destination, Source);
return Destination;
}

大概含义是把输入字符串中的字符按照“奇偶位置”分成两段,然后拼接在一起 写一个简单的脚本来解密一下就好

我直接跑了一次 发现怎么样都是乱码

image-20260402194046893

把思路再回到ida 去跟进base64 encode函数

image-20260402194124191

找到这个base64表再跟进 发现不是标准的表

image-20260402194156481

再次编写脚本 这一次带上自定义base64表再进行解码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import base64
custom_table = 'AKLMNOPQRSTUVWXZYabcdefghijklmnopqrstuvwxyzBDCEFGHIJ0123456789+/'
standard_table = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/'
def reverse_chang(s):
n = len(s)
mid = (n + 1) // 2
even = s[:mid]
odd = s[mid:]
result = []
for i in range(mid):
result.append(even[i])
if i < len(odd):
result.append(odd[i])
return ''.join(result)
Str2 = "aOYanlkVkemSmRgYlWi0Nc1P3JPIfoMoQJ2I20w="
original_b64 = reverse_chang(Str2)
mapping = {custom_table[i]: standard_table[i] for i in range(64)}
standard_str = ''.join(mapping.get(ch, ch) for ch in original_b64)
flag = base64.b64decode(standard_str).decode()
print(flag)

成功getflag!

image-20260402194341042

go_bytes

1
2
3
4
5
题目名称:go_bytes
题目内容:easy go!
题目分值:50.0
题目难度:容易
相关附件:go_bytes的附件.zip

ida 打开 main的伪代码如下

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
// main.main
void __fastcall main_main()
{
__int64 v0; // r14
const char *v1; // rdx
char *ptr; // rbx
__int64 v3; // rax
__int64 i; // rcx
__int64 v5; // rsi
char v6; // di
__int64 v7; // rax
__int64 j; // rcx
__int64 v9; // rdx
__int64 v10; // [rsp+48h] [rbp-1C8h]
char *v11; // [rsp+50h] [rbp-1C0h]
char v12[32]; // [rsp+58h] [rbp-1B8h] BYREF
_QWORD v13[3]; // [rsp+78h] [rbp-198h] BYREF
char v14; // [rsp+90h] [rbp-180h] BYREF
__int64 v15; // [rsp+1B8h] [rbp-58h]
string *p_string; // [rsp+1C0h] [rbp-50h]
_QWORD v17[2]; // [rsp+1C8h] [rbp-48h] BYREF
_QWORD v18[2]; // [rsp+1D8h] [rbp-38h] BYREF
_QWORD v19[2]; // [rsp+1E8h] [rbp-28h] BYREF
_QWORD v20[2]; // [rsp+1F8h] [rbp-18h] BYREF

while ( (unsigned __int64)&v14 <= *(_QWORD *)(v0 + 16) )
runtime_morestack_noctxt();
p_string = (string *)runtime_newobject((const RTYPE *)&RTYPE_string);
p_string->ptr = 0LL;
((void (__fastcall *)(_QWORD *, void *))loc_45F168)(v13, &unk_4DE3D0);
v20[0] = &RTYPE_string;
v20[1] = &off_4DCF00;
fmt_Fprintln(&go_itab__os_File_io_Writer, os_Stdout, v20, 1LL, 1LL);
v19[0] = &RTYPE__ptr_string;
v19[1] = p_string;
fmt_Fscanf(2LL, v19, v1, "%s", 1LL, 1LL);
ptr = p_string->ptr;
v3 = runtime_stringtoslicebyte(v12, p_string->ptr, p_string->len);
for ( i = 0LL; i < 40; ++i )
{
if ( (unsigned __int64)ptr <= i )
runtime_panicIndex(i, ptr, ptr);
v5 = v3;
v6 = *(_BYTE *)(v3 + i);
v7 = i
- 40 * ((__int64)(((unsigned __int128)((i + 1) * (__int128)(__int64)0xCCCCCCCCCCCCCCCDLL) >> 64) + i + 1) >> 5);
if ( (unsigned __int64)ptr <= v7 + 1 )
runtime_panicIndex(v7 + 1, ptr, ptr);
*(_BYTE *)(v5 + i) = (*(_BYTE *)(v7 + v5 + 1) >> 4) | (16 * v6);
v3 = v5;
}
v11 = ptr;
v15 = v3;
for ( j = 0LL; j < 40; ++j )
{
if ( (unsigned __int64)ptr <= j )
runtime_panicIndex(j, ptr, ptr);
v9 = *(unsigned __int8 *)(v3 + j);
main_tmp = (unsigned __int16)(291 * main_tmp + 1110);
if ( v13[j] != (main_tmp ^ v9) )
{
v10 = j;
v18[0] = &RTYPE_string;
v18[1] = &off_4DCF10;
fmt_Fprintln(&go_itab__os_File_io_Writer, os_Stdout, v18, 1LL, 1LL);
os_Exit(0LL);
v3 = v15;
j = v10;
ptr = v11;
}
}
v17[0] = &RTYPE_string;
v17[1] = &off_4DCF20;
fmt_Fprintln(&go_itab__os_File_io_Writer, os_Stdout, v17, 1LL, 1LL);
}

v13 是程序里写死的常量表 tmp 是一个不断更新的 16 位数 enc[j] 是你真正要还原出来的中间结果

1
2
3
main_tmp = (unsigned __int16)(291 * main_tmp + 1110);
if ( v13[j] != (main_tmp ^ v9) )
fail;

先从程序常量里拿到 v13 关键看以下伪代码

1
((void (__fastcall *)(_QWORD *, void *))loc_45F168)(v13, &unk_4DE3D0);

说明 v13 这张表的数据,就放在 .rdata 里的 unk_4DE3D0 这个位置。

进入之后 每 8 个 db 看成一组 取前两个有效字节 按小端序倒过来写成十六进制数

image-20260420214339823

然后将v13转换成的十六进制数先写出来

1
2
3
4
5
6
7
v13 = [
0x22b9, 0xc9f8, 0x8c89, 0xff18, 0x1439, 0x4e0a, 0x2a8b, 0x07cb,
0xbdeb, 0xfaab, 0x3ffb, 0x784b, 0x9f1e, 0x4feb, 0x4d0b, 0xd08e,
0x38bb, 0xcbae, 0xd2ce, 0x913e, 0x0a6b, 0xf03b, 0x507b, 0x398b,
0x93de, 0x3cce, 0x459e, 0x4abe, 0x553e, 0x316e, 0x33be, 0x42fe,
0xcece, 0x4dde, 0x982b, 0xa31b, 0x802e, 0x12ee, 0xf67a, 0xeb79
]

前面已经从 .rdata 里把 v13 提出来了,后半段校验逻辑是:

1
2
3
main_tmp = (unsigned __int16)(291 * main_tmp + 1110);
if ( v13[j] != (main_tmp ^ v9) )
fail;

简单变形一下就是

1
2
tmp = (291 * tmp + 1110) & 0xffff
enc[j] = v13[j] ^ tmp

v13[j] 是程序里存好的常量 tmp 每轮按固定公式更新 enc[j] 就是这一轮真正应该得到的中间字节

enc[i] 的高四位,来自当前字符 enc[i] 的低四位,来自下一个字符

1
inp[i] = ((enc[(i-1)%40] & 0x0f) << 4) | ((enc[i] >> 4) & 0x0f)

所以接下来用 v13tmp 更新公式算出 enc 再用 enc 反推出原始输入

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
v13 = [
0x22b9, 0xc9f8, 0x8c89, 0xff18, 0x1439, 0x4e0a, 0x2a8b, 0x07cb,
0xbdeb, 0xfaab, 0x3ffb, 0x784b, 0x9f1e, 0x4feb, 0x4d0b, 0xd08e,
0x38bb, 0xcbae, 0xd2ce, 0x913e, 0x0a6b, 0xf03b, 0x507b, 0x398b,
0x93de, 0x3cce, 0x459e, 0x4abe, 0x553e, 0x316e, 0x33be, 0x42fe,
0xcece, 0x4dde, 0x982b, 0xa31b, 0x802e, 0x12ee, 0xf67a, 0xeb79
]

init = 0xDEAD

tmp = init
enc = []
for j in range(40):
tmp = (291 * tmp + 1110) & 0xffff
enc.append(v13[j] ^ tmp)

flag = []
for i in range(40):
ch = ((enc[(i - 1) % 40] & 0x0f) << 4) | ((enc[i] >> 4) & 0x0f)
flag.append(ch)

print(bytes(flag).decode())

getflag

image-20260420215025951

测试一下也是正确的flag

image-20260420215051629

Crypto

Old_story

1
2
3
4
5
题目名称:Old_story
题目内容:Atbash and Caesar XIII grew up inside the walls and they both loved the number three
题目分值:25.0
题目难度:非常容易
相关附件:Old_story的附件.zip

这道题题目上说的挺清楚的 用Atbash和Cot13加上栅栏提示里面有三 作为key解码即可

这道题不要用随波逐流 我当时用这个解码出不来 不知道为什么

image-20260402162826200

魔棒点一下 base64解密 得到flag

image-20260402162846431

withoutN

1
2
3
4
5
题目名称:withoutN
题目内容:无
题目分值:50.0
题目难度:中等
相关附件:withoutN的附件.zip

解压后打开如下

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

secret = b'xxx'
m = bytes_to_long(secret)
assert m.bit_length() == 1047

SEED = mpz(hexlify(os.urandom(32)).decode(), 16)
STATE = random_state(SEED)

def get_prime(state, bits):
return next_prime(mpz_urandomb(state, bits) | (1 << (bits - 1)))

def get_special_prime(state, bits, imbalance=19):
p = mpz(2)
p_factors = [p]
while p.bit_length() < bits - 2 * imbalance:
factor = get_prime(state, imbalance)
p_factors.append(factor)
p *= factor

rbits = (bits - p.bit_length()) // 2

while True:
prime1 = get_prime(state, rbits)
prime2 = get_prime(state, rbits)
tmpp = p * prime1 * prime2
if tmpp.bit_length() < bits:
rbits += 1
continue
if tmpp.bit_length() > bits:
rbits -= 1
continue
if is_prime(tmpp + 1):
p_factors.append(prime1)
p_factors.append(prime2)
p = tmpp + 1
break

p_factors.sort()

return (p, p_factors)

e = 65537

while True:
p, p_factors = get_special_prime(STATE, 524, 18)
if len(p_factors) != len(set(p_factors)):
continue
q, q_factors = get_special_prime(STATE, 522, 19)
if len(q_factors) != len(set(q_factors)):
continue
factors = p_factors + q_factors
if e not in factors:
break

N = p * q
hint = []
tmp = 2
for i in range(3):
hint.append(int(pow(tmp, e, N)))
tmp = tmp**2

print(hint)
# hint = [34269022101152860551725933914862436322106939923476933016987904029460421275438055036090946119718655825103569877285538620832767198916391223591291325768882755364293986571312268682165658024432285920259028396107930868898063849543505142719590520375793982298603932737829888735365992104333282699376290143167724276496398378, 225616705401043186855915297759317199704941098680481618098433662063234394068166613712655667405272039899699196525685355530579060180020380469846664610739330564884711517682761881243799649783418042889530617331623138371682907664356352277785886802809183595500135434409396697051290833693714804557826464664729075177328207917, 189292832257014617978009107779803546393639972413154827495693589945033483913819006917586032476574410935265598294642928107150993970225047245997181637404550314215981657073675161756760137526462735869545306462606696486263406814197214235839159004460270249593554521293718798462366032108738987393640780166766845775471957265]
m = bytes_to_long(secret)
e = 65537
c = pow(m,e,N)
print(c)
#137442780020809189453553660397527651179261704001323847401570173163394990609119181567758246535963673084225666280526062904998477018134540129111524495787086037001773689812097023323500428710698130717534602755962566528520191957453794853205089587566996614753795540976769251563343237097781147768919054028245898266057355245

之前打CTF密码学一般就AI一把嗦了 但线下赛的本地小模型还是有点太笨了 发现自己一点密码都不会(这一次主要是看一下逻辑 等有机会打线下至少有点思路

将hint转换成n

原本的常规RSA密码学解题思路是 知道 NN 分成 p*q 然后就能推出私钥进行解密

这道题就给了hint 把思路放在生成hint的代码

1
2
3
4
5
hint = []
tmp = 2
for i in range(3):
hint.append(int(pow(tmp, e, N)))
tmp = tmp**2

一个简单的循环语句 三个hint实际上就是

1
2
3
hint[0] = 2^e mod N
hint[1] = 4^e mod N
hint[2] = 16^e mod N

意思就是三个hint 分别是2 4 16的e次方对N取模

但是因为2 4 16 本身就是次方关系 是有递推关系

2的二次方是4 4的二次方是16

1
2
hint[0]^2 ≡ hint[1] (mod N)
hint[1]^2 ≡ hint[2] (mod N)

现在算一个小学数学题( 假设 N = 5 我们看两个数:12 和 7

它们分别模 5:

1
2
12 mod 5 = 2
7 mod 5 = 2

也就是说,12 和 7 除以 5 的余数一样。 那它们一减 刚好就是 5 的倍数。

1
12 - 7 = 5

再看一个 还是 N = 5

1
2
22 mod 5 = 2
12 mod 5 = 2

余数还是一样 那它们相减 10 也是 5 的倍数。

1
22 - 12 = 10

可以得到规律 如果两个数除以 N 的余数相同,那么它们的差一定能被 N 整除。

hint[0]^2hint[1] 除以 N 的余数一样,所以 它们的差N 的倍数

1
hint[0]^2 ≡ hint[1] (mod N)

同理hint[1]^2hint[2] 除以 N 的余数一样,所以 它们的差 也是 N 的倍数

1
hint[1]^2 ≡ hint[2] (mod N)

既然这两个大数里都藏着 N,那最自然的想法就是去求它们的最大公约数,也就是 gcd

1
2
3
from math import gcd

g = gcd(hint[0]*hint[0] - hint[1], hint[1]*hint[1] - hint[2])

这题实际算出来的是 3N,所以再除以 3 就能恢复真正的模数

1
N = g // 3

Pollard p-1分解n

原本题目没有直接给出的 N 就找到啦 去看一下p和q的生成原理

意思大概是从 2 开始,不停随机找一些小素数,然后把它们一个一个乘起来,直到这个乘积差不多够大为止。

1
2
3
4
5
6
7
def get_special_prime(state, bits, imbalance=19):
p = mpz(2)
p_factors = [p]
while p.bit_length() < bits - 2 * imbalance:
factor = get_prime(state, imbalance)
p_factors.append(factor)
p *= factor

也就是说,这里先得到的还不是最终的素数,而是一个由很多小素数乘出来的大整数。

后面代码又会再乘两个素数,并检查 tmpp + 1 是否为素数。如果是执行以下代码

1
p = tmpp + 1

转一下符号可以得到

1
p - 1 = tmpp

也就是说,p-1 其实是由很多小素数再乘上两个中等素数构成的,q-1 也是同样的结构。

这就说明,这题的 pq 不是普通随机生成的素数,而是故意让 p-1q-1 带有很多小因子。这样的结构正适合用 Pollard p-1 去分解。

得到 pq 后,后续就回到了普通 RSA 的解题流程:先求欧拉函数 phi,再求私钥 d,最后对密文 c 进行解密。

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
from math import gcd
from sympy import primerange
import math

hint = [
34269022101152860551725933914862436322106939923476933016987904029460421275438055036090946119718655825103569877285538620832767198916391223591291325768882755364293986571312268682165658024432285920259028396107930868898063849543505142719590520375793982298603932737829888735365992104333282699376290143167724276496398378,
225616705401043186855915297759317199704941098680481618098433662063234394068166613712655667405272039899699196525685355530579060180020380469846664610739330564884711517682761881243799649783418042889530617331623138371682907664356352277785886802809183595500135434409396697051290833693714804557826464664729075177328207917,
189292832257014617978009107779803546393639972413154827495693589945033483913819006917586032476574410935265598294642928107150993970225047245997181637404550314215981657073675161756760137526462735869545306462606696486263406814197214235839159004460270249593554521293718798462366032108738987393640780166766845775471957265
]

c = 137442780020809189453553660397527651179261704001323847401570173163394990609119181567758246535963673084225666280526062904998477018134540129111524495787086037001773689812097023323500428710698130717534602755962566528520191957453794853205089587566996614753795540976769251563343237097781147768919054028245898266057355245
e = 65537

# step1: recover N
N0 = gcd(hint[0]*hint[0] - hint[1], hint[1]*hint[1] - hint[2])
N = N0 // 3

# step2: Pollard p-1
def pollard_pm1(n, B=262144, a=2):
x = a
for p in primerange(2, B + 1):
pe = p
while pe * p <= B:
pe *= p
x = pow(x, pe, n)
g = math.gcd(x - 1, n)
return g

p = pollard_pm1(N)
q = N // p

# step3: decrypt
phi = (p - 1) * (q - 1)
d = pow(e, -1, phi)
m0 = pow(c, d, N)

# because original m > N, try m0 + kN
for k in range(1, 6):
m = m0 + k * N
b = m.to_bytes((m.bit_length() + 7) // 8, 'big')
try:
s = b.decode()
if all(32 <= ord(ch) < 127 for ch in s):
print("k =", k)
print(s)
except:
pass

getflag

image-20260410154846161

babysm2

1
2
3
4
5
题目名称:babysm2
题目内容:来点国密
题目分值:75.0
题目难度:中等
相关附件:babysm2的附件.zip

解压后打开如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
from Crypto.Util.number import *
from random import getrandbits

flag = b"xxx"

assert flag.startswith(b'DASCTF{') and flag.endswith(b'}')
assert len(flag) == 40

ecc_table = {
'n': 'FFFFFFFEFFFFFFFFFFFFFFFFFFFFFFFF7203DF6B21C6052B53BBF40939D54123',
'p': 'FFFFFFFEFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF00000000FFFFFFFFFFFFFFFF',
'g': '32c4ae2c1f1981195f9904466a39c9948fe30bbff2660be1715a4589334c74c7'
'bc3736a2f4f6779c59bdcee36b692153d0a9877cc62a474002df32e52139f0a0',
'a': 'FFFFFFFEFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF00000000FFFFFFFFFFFFFFFC',
'b': '28E9FA9E9D9F5E344D5A9E4BCF6509A7F39789F515AB8F92DDBCBD414D940E93',
}

class TSM2(object):
def __init__(self, sk):
self.n = int(ecc_table['n'], 16)
self.para_len = len(ecc_table['n'])
self.ecc_a3 = (
int(ecc_table['a'], base=16) + 3) % int(ecc_table['p'], base=16)
self.ecc_table = ecc_table

self.sk = sk
self.pk = self._kg(self.sk, ecc_table['g'])

def _kg(self, k, Point):
if (k % self.n) == 0:
return '0' * 128
Point = '%s%s' % (Point, '1')
mask_str = '8'
for i in range(self.para_len - 1):
mask_str += '0'
mask = int(mask_str, 16)
Temp = Point
flag = False
for n in range(self.para_len * 4):
if (flag):
Temp = self._double_point(Temp)
if (k & mask) != 0:
if (flag):
Temp = self._add_point(Temp, Point)
else:
flag = True
Temp = Point
k = k << 1
return self._convert_jacb_to_nor(Temp)

def _double_point(self, Point):
l = len(Point)
len_2 = 2 * self.para_len
if l < self.para_len * 2:
return None
else:
x1 = int(Point[0:self.para_len], 16)
y1 = int(Point[self.para_len:len_2], 16)
if l == len_2:
z1 = 1
else:
z1 = int(Point[len_2:], 16)

T6 = (z1 * z1) % int(self.ecc_table['p'], base=16)
T2 = (y1 * y1) % int(self.ecc_table['p'], base=16)
T3 = (x1 + T6) % int(self.ecc_table['p'], base=16)
T4 = (x1 - T6) % int(self.ecc_table['p'], base=16)
T1 = (T3 * T4) % int(self.ecc_table['p'], base=16)
T3 = (y1 * z1) % int(self.ecc_table['p'], base=16)
T4 = (T2 * 8) % int(self.ecc_table['p'], base=16)
T5 = (x1 * T4) % int(self.ecc_table['p'], base=16)
T1 = (T1 * 3) % int(self.ecc_table['p'], base=16)
T6 = (T6 * T6) % int(self.ecc_table['p'], base=16)
T6 = (self.ecc_a3 * T6) % int(self.ecc_table['p'], base=16)
T1 = (T1 + T6) % int(self.ecc_table['p'], base=16)
z3 = (T3 + T3) % int(self.ecc_table['p'], base=16)
T3 = (T1 * T1) % int(self.ecc_table['p'], base=16)
T2 = (T2 * T4) % int(self.ecc_table['p'], base=16)
x3 = (T3 - T5) % int(self.ecc_table['p'], base=16)

if (T5 % 2) == 1:
T4 = (T5 + ((T5 + int(self.ecc_table['p'], base=16)) >> 1) - T3) % int(
self.ecc_table['p'], base=16)
else:
T4 = (T5 + (T5 >> 1) - T3) % int(self.ecc_table['p'], base=16)

T1 = (T1 * T4) % int(self.ecc_table['p'], base=16)
y3 = (T1 - T2) % int(self.ecc_table['p'], base=16)

form = '%%0%dx' % self.para_len
form = form * 3
return form % (x3, y3, z3)

def _add_point(self, P1, P2):
if P1 == '0' * 128:
return '%s%s' % (P2, '1')
if P2 == '0' * 128:
return '%s%s' % (P1, '1')
len_2 = 2 * self.para_len
l1 = len(P1)
l2 = len(P2)
if (l1 < len_2) or (l2 < len_2):
return None
else:
X1 = int(P1[0:self.para_len], 16)
Y1 = int(P1[self.para_len:len_2], 16)
if l1 == len_2:
Z1 = 1
else:
Z1 = int(P1[len_2:], 16)
x2 = int(P2[0:self.para_len], 16)
y2 = int(P2[self.para_len:len_2], 16)

T1 = (Z1 * Z1) % int(self.ecc_table['p'], base=16)
T2 = (y2 * Z1) % int(self.ecc_table['p'], base=16)
T3 = (x2 * T1) % int(self.ecc_table['p'], base=16)
T1 = (T1 * T2) % int(self.ecc_table['p'], base=16)
T2 = (T3 - X1) % int(self.ecc_table['p'], base=16)
T3 = (T3 + X1) % int(self.ecc_table['p'], base=16)
T4 = (T2 * T2) % int(self.ecc_table['p'], base=16)
T1 = (T1 - Y1) % int(self.ecc_table['p'], base=16)
Z3 = (Z1 * T2) % int(self.ecc_table['p'], base=16)
T2 = (T2 * T4) % int(self.ecc_table['p'], base=16)
T3 = (T3 * T4) % int(self.ecc_table['p'], base=16)
T5 = (T1 * T1) % int(self.ecc_table['p'], base=16)
T4 = (X1 * T4) % int(self.ecc_table['p'], base=16)
X3 = (T5 - T3) % int(self.ecc_table['p'], base=16)
T2 = (Y1 * T2) % int(self.ecc_table['p'], base=16)
T3 = (T4 - X3) % int(self.ecc_table['p'], base=16)
T1 = (T1 * T3) % int(self.ecc_table['p'], base=16)
Y3 = (T1 - T2) % int(self.ecc_table['p'], base=16)

form = '%%0%dx' % self.para_len
form = form * 3
return form % (X3, Y3, Z3)

def _convert_jacb_to_nor(self, Point):
len_2 = 2 * self.para_len
x = int(Point[0:self.para_len], 16)
y = int(Point[self.para_len:len_2], 16)
z = int(Point[len_2:], 16)
z_inv = pow(
z, int(self.ecc_table['p'], base=16) - 2, int(self.ecc_table['p'], base=16))
z_invSquar = (z_inv * z_inv) % int(self.ecc_table['p'], base=16)
z_invQube = (z_invSquar * z_inv) % int(self.ecc_table['p'], base=16)
x_new = (x * z_invSquar) % int(self.ecc_table['p'], base=16)
y_new = (y * z_invQube) % int(self.ecc_table['p'], base=16)
z_new = (z * z_inv) % int(self.ecc_table['p'], base=16)
if z_new == 1:
form = '%%0%dx' % self.para_len
form = form * 2
return form % (x_new, y_new)
else:
return None

def sign(self, data, K):
e = data
d = self.sk
k = K

P1 = self._kg(k, self.ecc_table['g'])

x = int(P1[0:self.para_len], 16)
R = ((e + x) % int(self.ecc_table['n'], base=16))
if R == 0 or R + k == int(self.ecc_table['n'], base=16):
return None
d_1 = pow(
d+1, int(self.ecc_table['n'], base=16) - 2, int(self.ecc_table['n'], base=16))
S = (d_1*(k + R) - R) % int(self.ecc_table['n'], base=16)
if S == 0:
return None
else:
return '%064x%064x' % (R, S)

sk = (bytes_to_long(flag) & 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF000000000000000000) >> (18*4)

p = getPrime(512)
q = getPrime(512)
print('c = ', pow(bytes_to_long(flag), 3, p*q))
print('n = ', p*q)
sm2 = TSM2(sk)

msg1 = getrandbits(250) % int(ecc_table['n'], 16)
msg2 = getrandbits(250) % int(ecc_table['n'], 16)
k1 = getrandbits(250) % int(ecc_table['n'], 16)
k2 = 1234*k1 + 56789
print(sm2.sign(msg1, k1))
print(sm2.sign(msg2, k2))

'''
c = 184706733600030957047359404181817675818892386111176322073429745870653619214537156430838299086812442928966325279937093205055617117660063552877204397406286418216571923934403472586373040945073648087723537769956453122736694720492867275533136819468120987571478140733898424962906469962174126437
n = 93382226148016226378493612791149852310908382710594867903057178066314084342295156211305540726138333971327516322441003390326758135890045955265896303020696024403023845615104907773233525350279901353440475577613919901780343029818600918619060869206892412915500952626458605167340404638154628966463023355180219729809
f755b6855d4b50ee2b42efdc43d21dcf9e2b5b9fff517e2271c065831dbb5985b6834b5ba768c7dd6de24292585de71cf4aaea8b3805fcf4434f6fb344d16aef
00b0d0a4deb07ead9baaa265173a94cf1050b235f85f494f5b3adf5edf97fb0425eda7444ad7fdeac81a3aa645021a5a6cdcb629ba6ade635c24d6c19bfd71d9
'''

分析代码

flag 被切了一部分作为 SM2 私钥 sk

1
2
3
4
5
6
flag = b"xxx"

assert flag.startswith(b'DASCTF{') and flag.endswith(b'}')
assert len(flag) == 40

sk = (bytes_to_long(flag) & 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF000000000000000000) >> (18*4)

整个 flag 被 RSA 加密,指数 e=3

1
2
3
4
p = getPrime(512)
q = getPrime(512)
print('c = ', pow(bytes_to_long(flag), 3, p*q))
print('n = ', p*q)

SM2 签名函数 签名 S 同时依赖 私钥 d 和 随机数 k

1
2
3
4
5
6
7
8
9
10
11
12
13
14
def sign(self, data, K):
e = data
d = self.sk
k = K

P1 = self._kg(k, self.ecc_table['g'])

x = int(P1[0:self.para_len], 16)
R = ((e + x) % int(self.ecc_table['n'], base=16))

d_1 = pow(d+1, int(self.ecc_table['n'], base=16) - 2, int(self.ecc_table['n'], base=16))
S = (d_1*(k + R) - R) % int(self.ecc_table['n'], base=16)

return '%064x%064x' % (R, S)

两次签名使用的 k 有关系

1
2
3
4
5
6
7
8
msg1 = getrandbits(250) % int(ecc_table['n'], 16)
msg2 = getrandbits(250) % int(ecc_table['n'], 16)

k1 = getrandbits(250) % int(ecc_table['n'], 16)
k2 = 1234*k1 + 56789

print(sm2.sign(msg1, k1))
print(sm2.sign(msg2, k2))

解题思路

SM2 部分

1
2
3
4
5
6
7
8
msg1 = getrandbits(250) % int(ecc_table['n'], 16)
msg2 = getrandbits(250) % int(ecc_table['n'], 16)

k1 = getrandbits(250) % int(ecc_table['n'], 16)
k2 = 1234*k1 + 56789

print(sm2.sign(msg1, k1))
print(sm2.sign(msg2, k2))

两次签名的随机数 k 存在线性关系

S 同时依赖 k 和私钥 d

1
2
3
k2 = 1234*k1 + 56789

S = (d_1*(k + R) - R)

利用两组签名 (R1,S1), (R2,S2) k2 与 k1 的线性关系 联立方程消掉 k 解出私钥 d(sk) 即可得到flag高位

1
sk = flag #flag高位部分

RSA 部分

指数 e = 3 RSA低指数攻击即可

1
2
3
4
p = getPrime(512)
q = getPrime(512)
print('c = ', pow(bytes_to_long(flag), 3, p*q))
print('n = ', p*q)

编写脚本

导入工具并读取题目给出的所有参数

1
2
3
4
5
6
7
8
9
10
11
from Crypto.Util.number import *
from gmpy2 import invert, iroot

n = 93382226148016226378493612791149852310908382710594867903057178066314084342295156211305540726138333971327516322441003390326758135890045955265896303020696024403023845615104907773233525350279901353440475577613919901780343029818600918619060869206892412915500952626458605167340404638154628966463023355180219729809

c = 184706733600030957047359404181817675818892386111176322073429745870653619214537156430838299086812442928966325279937093205055617117660063552877204397406286418216571923934403472586373040945073648087723537769956453122736694720492867275533136819468120987571478140733898424962906469962174126437

sig1 = "f755b6855d4b50ee2b42efdc43d21dcf9e2b5b9fff517e2271c065831dbb5985b6834b5ba768c7dd6de24292585de71cf4aaea8b3805fcf4434f6fb344d16aef"
sig2 = "00b0d0a4deb07ead9baaa265173a94cf1050b235f85f494f5b3adf5edf97fb0425eda7444ad7fdeac81a3aa645021a5a6cdcb629ba6ade635c24d6c19bfd71d9"

curve_n = int("FFFFFFFEFFFFFFFFFFFFFFFFFFFFFFFF7203DF6B21C6052B53BBF40939D54123", 16)

把签名拆成 (R, S)

1
2
3
4
5
6
7
def split_sig(sig):
R = int(sig[:64], 16)
S = int(sig[64:], 16)
return R, S

R1, S1 = split_sig(sig1)
R2, S2 = split_sig(sig2)

利用 k 的关系解出 SM2 私钥 d

1
2
3
4
5
A = (S2 + R2) - 1234 * (S1 + R1)
B = (R2 - 1234 * R1 - 56789) % curve_n

d = (B * invert(A, curve_n)) % curve_n
print(d)

RSA e=3,直接开三次方恢复 flag

1
2
3
flag_int, _ = iroot(c, 3)
flag = long_to_bytes(flag_int)
print(flag)

最终的完整脚本如下

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

n = 93382226148016226378493612791149852310908382710594867903057178066314084342295156211305540726138333971327516322441003390326758135890045955265896303020696024403023845615104907773233525350279901353440475577613919901780343029818600918619060869206892412915500952626458605167340404638154628966463023355180219729809

c = 184706733600030957047359404181817675818892386111176322073429745870653619214537156430838299086812442928966325279937093205055617117660063552877204397406286418216571923934403472586373040945073648087723537769956453122736694720492867275533136819468120987571478140733898424962906469962174126437

sig1 = "f755b6855d4b50ee2b42efdc43d21dcf9e2b5b9fff517e2271c065831dbb5985b6834b5ba768c7dd6de24292585de71cf4aaea8b3805fcf4434f6fb344d16aef"
sig2 = "00b0d0a4deb07ead9baaa265173a94cf1050b235f85f494f5b3adf5edf97fb0425eda7444ad7fdeac81a3aa645021a5a6cdcb629ba6ade635c24d6c19bfd71d9"

curve_n = int("FFFFFFFEFFFFFFFFFFFFFFFFFFFFFFFF7203DF6B21C6052B53BBF40939D54123", 16)

def split_sig(sig):
R = int(sig[:64], 16)
S = int(sig[64:], 16)
return R, S

R1, S1 = split_sig(sig1)
R2, S2 = split_sig(sig2)

A = (S2 + R2) - 1234 * (S1 + R1)
B = (R2 - 1234 * R1 - 56789) % curve_n

d = (B * invert(A, curve_n)) % curve_n
print("[+] d (sk) =", d)

flag_int, exact = iroot(c, 3)
flag = long_to_bytes(flag_int)
print("[+] flag =", flag)

Getflag!

image-20260420012116429