@@ -7,10 +7,12 @@ description = "AK Cryptography!!!"
77tags = [" idekCTF" , " Team" , " WriteUp" , " Cryptography" , " Reverse" , " Web" ]
88+++
99
10- 大家在开赛后临时创号玩的,二进制哥们很忙,于是我们就做了一些别的题
10+ 大家在开赛后临时创号玩的,二进制哥们很忙,于是我们就做了一些别的题,但也遗憾在这里,只靠密码,前 24h 便来到第九名,后续没题可做了
1111
1212质量不错,也许值 65+ 权重
1313
14+ 另外,本场比赛遇到不少 Golang 可能遇到的问题,并解决了
15+
1416> 博客还在调试,图片显示可能存在问题,在寻找一个好用的对象存储
1517
1618# idekCTF 2025 Write-ups / Challenge List
@@ -1096,6 +1098,366 @@ c2 = 304939267693072796204027153778258043309446776809271703887768911528314257867
10961098"""
10971099```
10981100
1101+ 这题一开始思路被带偏,上来就是 $\pmod (p-1)$ 走了多少弯路
1102+
1103+ 因为有爆破的成分在,我们继续 Golang
1104+
1105+ ## 1. 问题概述
1106+
1107+ 我们需要求出 ** ` diamond_ticket ` ** 的值,它是一个长度为 20 字节的可打印 ASCII 字符串,记为整数
1108+
1109+ $$
1110+ m \in [m_{\text{min}},\,m_{\text{max}}] .
1111+ $$
1112+
1113+ 在题目提供的 Python 脚本中,` m ` 被视作一个 20 字节的整数,并被代入函数
1114+
1115+ $$
1116+ \text{flag\_chocolate}= (a^m+b^m)\bmod p ,
1117+ $$
1118+
1119+ 其中
1120+
1121+ * $p$ 为大素数
1122+ * $a,b$ 为已知常数
1123+
1124+ 后半段涉及 RSA 加密,但 ** 公钥指数 $e$ 为偶数** ,导致 $\gcd(e,\varphi(N))\neq 1$ 且私钥不可求,暗示 RSA 部分是误导,真正的突破点在 ** 第一部分的数论约束** 。
1125+
1126+ 下面的分析以 ** Go** 实现的搜索程序为出发点,说明其数学原理。
1127+
1128+ ---
1129+
1130+ ## 2. 数学原理
1131+
1132+ ### 2.1 费马小定理与阶
1133+
1134+ 对任意整数 $x$ 与素数 $p$($p\nmid x$)有
1135+
1136+ $$
1137+ x^{p-1}\equiv 1 \pmod{p}.
1138+ $$
1139+
1140+ 若整数 $x$ 在模 $p$ 下的 ** 阶** ($\operatorname{ord}_ p(x)$)定义为
1141+
1142+ $$
1143+ \operatorname{ord}_p(x)=\min\{k>0\mid x^k\equiv 1\pmod{p}\},
1144+ $$
1145+
1146+ 则 $\operatorname{ord}_ p(x)$ 必是 $p-1$ 的约数。
1147+
1148+ ### 2.2 关键参数
1149+
1150+ * 题目给出的素数
1151+
1152+ $$
1153+ p_{\text{crypto}} = 170\,829\,625\,398\,370\,252\,501\,980\,763\,763\,988\,409\,583 .
1154+ $$
1155+
1156+ * Go 代码中使用的
1157+
1158+ $$
1159+ q = p_{\text{crypto}}-1\; / \; 2 = 85\,414\,812\,699\,185\,126\,250\,990\,381\,881\,994\,204\,791 .
1160+ $$
1161+
1162+ 计算可得
1163+
1164+ $$
1165+ q = \frac{p_{\text{crypto}}-1}{2}.
1166+ $$
1167+
1168+ 注释 ` a.multiplicative_order()+1 ` 表明
1169+
1170+ $$
1171+ \operatorname{ord}_{p_{\text{crypto}}}(a) = q,
1172+ $$
1173+
1174+ 即 $a$ 的阶恰为 $q$。于是
1175+
1176+ $$
1177+ a^{\frac{p_{\text{crypto}}-1}{2}} \equiv 1 \pmod{p_{\text{crypto}}}.
1178+ $$
1179+
1180+ ### 2.3 同余关系
1181+
1182+ 任意整数 $m$ 可写成
1183+
1184+ $$
1185+ m = k\;q + r,\qquad 0\le r < q,
1186+ $$
1187+
1188+ 即
1189+
1190+ $$
1191+ m \equiv r \pmod{q}.
1192+ $$
1193+
1194+ 将其代入 $a^m$:
1195+
1196+ $$
1197+ a^{m}=a^{kq+r}=(a^{q})^{k}\,a^{r}\equiv 1^{k}\,a^{r}\equiv a^{r}\pmod{p_{\text{crypto}}}.
1198+ $$
1199+
1200+ 因此 $a^m$ 只依赖于 ** 余数** $r = m \bmod q$。
1201+
1202+ Go 代码直接给出了
1203+
1204+ $$
1205+ r_{\text{go}} = 4\,807\,895\,356\,063\,327\,854\,843\,653\,048\,517\,090\,061 .
1206+ $$
1207+
1208+ 于是核心约束化为
1209+
1210+ $$
1211+ \boxed{\,m \equiv r_{\text{go}}\pmod{q}\,}.
1212+ $$
1213+
1214+ ---
1215+
1216+ ## 3. 搜索算法的数学推导
1217+
1218+ ### 3.1 可打印字符串的取值范围
1219+
1220+ ASCII 可打印字符范围为
1221+
1222+ $$
1223+ 32 \le \text{byte} \le 126 .
1224+ $$
1225+
1226+ 因此 20 字节字符串对应的整数范围为
1227+
1228+ $$
1229+ m_{\text{min}} = \underbrace{0x20\cdots20}_{20\text{ 个空格}} ,
1230+ \qquad
1231+ m_{\text{max}} = \underbrace{0x7E\cdots7E}_{20\text{ 个波浪号}} .
1232+ $$
1233+
1234+ ### 3.2 约束的整数解
1235+
1236+ 我们同时需要满足
1237+
1238+ $$
1239+ \begin{cases}
1240+ m = k\,q + r_{\text{go}} ,\\
1241+ m_{\text{min}} \le m \le m_{\text{max}} .
1242+ \end{cases}
1243+ $$
1244+
1245+ 代入得到关于 $k$ 的不等式
1246+
1247+ $$
1248+ m_{\text{min}} - r_{\text{go}} \le k\,q \le m_{\text{max}} - r_{\text{go}} .
1249+ $$
1250+
1251+ 除以 $q$ 并取整数,得到
1252+
1253+ $$
1254+ k_{\text{start}} = \left\lceil \dfrac{m_{\text{min}} - r_{\text{go}}}{q}\right\rceil,
1255+ \qquad
1256+ k_{\text{end}} = \left\lfloor \dfrac{m_{\text{max}} - r_{\text{go}}}{q}\right\rfloor .
1257+ $$
1258+
1259+ 这正是 Go 程序中 ` kStart ` 与 ` kEnd ` 的计算方式。
1260+
1261+ ### 3.3 搜索流程(不涉及代码)
1262+
1263+ 1 . ** 初始化** :设定 $q$ 与 $r_ {\text{go}}$。
1264+ 2 . ** 计算范围** :由可打印字符计算 $m_ {\text{min}},\, m_ {\text{max}}$,进而得到 $[ k_ {\text{start}},k_ {\text{end}}] $。
1265+ 3 . ** 并行遍历** :将该区间划分为若干块,利用多核 CPU 并行遍历每个 $k$。
1266+ 4 . ** 候选验证** :对每个 $k$ 计算
1267+
1268+ $$
1269+ m = k\,q + r_{\text{go}} .
1270+ $$
1271+
1272+ 将整数 $m$ 转为 20 字节序列,检查每个字节是否落在 $[ 32,126] $ 区间。
1273+ 5 . ** 输出** :找到满足所有条件的唯一 $m$ 后,将其格式化为
1274+
1275+ $$
1276+ \text{idek\{ \ldots \}} .
1277+ $$
1278+
1279+ ---
1280+
1281+ ## 4. 结论
1282+
1283+ 通过 ** 模运算** 与 ** 阶的性质** ,原本看似复杂的密码学问题被化简为单一同余方程
1284+
1285+ $$
1286+ m \equiv r_{\text{go}} \pmod{q},
1287+ $$
1288+
1289+ 再结合 “可打印 20 字节字符串” 的物理约束,搜索空间被大幅缩小。
1290+ Go 实现的并行搜索遍历所有满足数论约束的候选,最终得到隐藏的 ** ` diamond_ticket ` ** ,即题目所求的 20 字符密钥。
1291+
1292+ ``` golang
1293+ package main
1294+
1295+ import (
1296+ " bytes"
1297+ " fmt"
1298+ " math/big"
1299+ " os"
1300+ " runtime"
1301+ " sync"
1302+ " time"
1303+
1304+ " github.com/schollz/progressbar/v3"
1305+ )
1306+
1307+ func isPrintable (data []byte ) bool {
1308+ for _ , b := range data {
1309+ if b < 32 || b > 126 {
1310+ return false
1311+ }
1312+ }
1313+ return true
1314+ }
1315+
1316+ func worker (
1317+ k_start, k_end *big.Int ,
1318+ p_minus_1, r *big.Int ,
1319+ wg *sync.WaitGroup ,
1320+ bar *progressbar.ProgressBar ,
1321+ foundMutex *sync.Mutex ,
1322+ outputFile *os.File ,
1323+ ) {
1324+ defer wg.Done ()
1325+ m := new (big.Int ).Mul (k_start, p_minus_1)
1326+ m.Add (m, r)
1327+ const flagContentLength = 20
1328+ mBytes := make ([]byte , flagContentLength)
1329+
1330+ const batchSize = 10000
1331+ var batchCounter int64 = 0
1332+
1333+ loopCount := new (big.Int ).Sub (k_end, k_start)
1334+ loopCount.Add (loopCount, big.NewInt (1 ))
1335+
1336+ one := big.NewInt (1 )
1337+ i := new (big.Int )
1338+
1339+ for i.Cmp (loopCount) < 0 {
1340+ if m.BitLen () <= flagContentLength*8 {
1341+ m.FillBytes (mBytes)
1342+ if isPrintable (mBytes) {
1343+ foundMutex.Lock ()
1344+ flag := fmt.Sprintf (" idek{%s }" , string (mBytes))
1345+ fmt.Println (" \n [+] possible flag:" , flag)
1346+ _ , err := outputFile.WriteString (flag + " \n " )
1347+ if err != nil {
1348+ fmt.Println (" [-] err:" , err)
1349+ }
1350+ foundMutex.Unlock ()
1351+ }
1352+ }
1353+
1354+ m.Add (m, p_minus_1)
1355+
1356+ batchCounter++
1357+ if batchCounter == batchSize {
1358+ bar.Add64 (batchSize)
1359+ batchCounter = 0
1360+ }
1361+
1362+ i.Add (i, one)
1363+ }
1364+
1365+ if batchCounter > 0 {
1366+ bar.Add64 (batchCounter)
1367+ }
1368+ }
1369+
1370+ func main () {
1371+ // a.multiplicative_order()+1,因为后面有个p-1,懒得改了
1372+ pStr := " 85414812699185126250990381881994204792"
1373+ rStr := " 4807895356063327854843653048517090061"
1374+
1375+ p , _ := new (big.Int ).SetString (pStr, 10 )
1376+ r , _ := new (big.Int ).SetString (rStr, 10 )
1377+
1378+ pMinus1 := new (big.Int ).Sub (p, big.NewInt (1 ))
1379+ const flagContentLength = 20
1380+
1381+ mMinBytes := bytes.Repeat ([]byte {32 }, flagContentLength)
1382+ mMaxBytes := bytes.Repeat ([]byte {126 }, flagContentLength)
1383+ mMin := new (big.Int ).SetBytes (mMinBytes)
1384+ mMax := new (big.Int ).SetBytes (mMaxBytes)
1385+ kStartNum := new (big.Int ).Sub (mMin, r)
1386+ kStart := new (big.Int ).Div (kStartNum, pMinus1)
1387+
1388+ mCheck := new (big.Int ).Mul (kStart, pMinus1)
1389+ mCheck.Add (mCheck, r)
1390+ if mCheck.Cmp (mMin) < 0 {
1391+ kStart.Add (kStart, big.NewInt (1 ))
1392+ }
1393+
1394+ kEndNum := new (big.Int ).Sub (mMax, r)
1395+ kEnd := new (big.Int ).Div (kEndNum, pMinus1)
1396+
1397+ fmt.Printf (" [*] k_start: %s \n " , kStart.String ())
1398+ fmt.Printf (" [*] k_end: %s \n " , kEnd.String ())
1399+
1400+ kRange := new (big.Int ).Sub (kEnd, kStart)
1401+ kRange.Add (kRange, big.NewInt (1 ))
1402+
1403+ bar := progressbar.NewOptions64 (
1404+ kRange.Int64 (),
1405+ progressbar.OptionSetDescription (" Searching..." ),
1406+ progressbar.OptionSetWriter (os.Stderr ),
1407+ progressbar.OptionShowBytes (false ),
1408+ progressbar.OptionSetWidth (40 ),
1409+ progressbar.OptionShowCount (),
1410+ progressbar.OptionThrottle (100 *time.Millisecond ),
1411+ progressbar.OptionSpinnerType (14 ),
1412+ progressbar.OptionFullWidth (),
1413+ progressbar.OptionSetRenderBlankState (true ),
1414+ progressbar.OptionShowElapsedTimeOnFinish (),
1415+ )
1416+
1417+ numWorkers := runtime.NumCPU ()
1418+ runtime.GOMAXPROCS (numWorkers)
1419+ var wg sync.WaitGroup
1420+ var foundMutex sync.Mutex
1421+ outputFile , err := os.Create (" found_flags.txt" )
1422+ if err != nil {
1423+ fmt.Println (" [-] err:" , err)
1424+ return
1425+ }
1426+ defer outputFile.Close ()
1427+
1428+ totalWork := new (big.Int ).Set (kRange)
1429+ chunkSize := new (big.Int ).Div (totalWork, big.NewInt (int64 (numWorkers)))
1430+ currentK := new (big.Int ).Set (kStart)
1431+ one := big.NewInt (1 )
1432+
1433+ for i := 0 ; i < numWorkers; i++ {
1434+ wg.Add (1 )
1435+ workerKStart := new (big.Int ).Set (currentK)
1436+ var workerKEnd *big.Int
1437+ if i == numWorkers-1 {
1438+ workerKEnd = new (big.Int ).Set (kEnd)
1439+ } else {
1440+ workerKEnd = new (big.Int ).Add (workerKStart, chunkSize)
1441+ workerKEnd.Sub (workerKEnd, one)
1442+ }
1443+ if workerKStart.Cmp (kEnd) > 0 {
1444+ wg.Done ()
1445+ continue
1446+ }
1447+ go worker (workerKStart, workerKEnd, pMinus1, r, &wg, bar, &foundMutex, outputFile)
1448+ currentK.Add (workerKEnd, one)
1449+ }
1450+
1451+ wg.Wait ()
1452+ bar.Finish ()
1453+ fmt.Println (" \n [+] Results saved to found_flags.txt." )
1454+ }
1455+ ```
1456+
1457+ ```
1458+ idek{tks_f0r_ur_t1ck3t_xD}
1459+ ```
1460+
10991461## Sadness ECC - Revenge
11001462
11011463Revenge 换了更麻烦的 PoW, 糊一个解决 PoW 的代码段上去就行
0 commit comments