Skip to content

Commit 75a37c2

Browse files
committed
content: finalize diamond-ticket solution and golang search section
1 parent 3bef17a commit 75a37c2

File tree

1 file changed

+363
-1
lines changed

1 file changed

+363
-1
lines changed

content/blog/idek2025_writeup.md

Lines changed: 363 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,12 @@ description = "AK Cryptography!!!"
77
tags = ["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

11011463
Revenge 换了更麻烦的 PoW, 糊一个解决 PoW 的代码段上去就行

0 commit comments

Comments
 (0)