首页
社区
课程
招聘
[原创]看雪·2025 KCTF 第九题 WriteUP
发表于: 2025-8-31 21:46 2720

[原创]看雪·2025 KCTF 第九题 WriteUP

2025-8-31 21:46
2720

GPT-5 还是 Pro 好用,Thinking 老是走弯路,浪费了不少时间

本文的分析由 GPT-5 Pro + Thinking 写成,我只干了脱壳的事(❁´◡`❁)

LLM 万岁(雾

用 DIE 查壳,发现加了 Enigma Virtual Box 的壳,用 EnigmaVBUnpacker 脱壳后可得到源代码。

图片描述

图片描述

图片描述

对于释放出来的 20250805Calc, 直接在程序运行的时候从temp复制过来就行,或者 dnSpy 直接导出就可以得到原始可执行文件

图片描述

图片描述

对 20250805Calc.exe 使用 pyinstxtractor + PyLingual 还原原始代码

图片描述

图片描述

下面给你把这个 crackme 的校验流程完整梳理一遍,并给出 Name = KCTF 时应该如何反推出合法 SN 的办法。我也把关键源码位置都标好了,方便你对照。

程序读入 NameSN;若为空直接退出。随后把 Name 的 UTF‑8 字节逐个异或,取结果 % 64 得到一个数 num;如果 num 为偶数再 +1。接着把 num 的十进制字符串记为 A(作为被除数的“分子”)。
*对 Name = KCTF:ASCII 分别为 K=75, C=67, T=84, F=70,逐位 XOR:75^67^84^70 = 2626 % 64 = 26,偶数 → num = 27,所以 A = "27"。*

对输入的 SN 先做一次「特殊的游程解码」(只能包含数字、l.)。这个解码器把形如

的编码翻译为一个只由 数字和至多一个小数点组成的 B 字符串。B 后续将作为“分母”。长度<26 或 >50、以 0 开头、以 . 结尾、计数有前导 0 等情况都会被判无效。

程序把一个随附的 Python 可执行(你提供的源码 20250805Calc-pub.py)释放到临时目录并执行,计算

其中 Decimal 精度被设置为 (2040+1)×2025 = 4,133,025 个有效数字,并把结果打印到标准输出。

C# 只接受 科学计数法(包含 E)的输出;它会把形如 1.xxxxxE-kkk 的结果,转成一个超长纯数字串:

之后把 Quotient 从左到右按 每 2025 位一块切片,得到块 Q[0], Q[1], ...,然后在 j=0..2036 范围内检查下面的线性关系是否成立:

476K9s2c8@1M7q4)9K6b7g2)9J5c8W2)9J5c8Y4N6%4N6#2)9J5k6i4M7K6i4K6u0W2L8%4u0Y4i4K6u0r3x3e0V1&6z5q4)9J5c8V1#2S2N6r3S2Q4x3V1k6y4j5i4c8Z5e0f1H3`.">Q[j+]Q[j]+Q[j+]+Q[j+]Q[j+3] \stackrel{?}= 5\cdot Q[j] + 8\cdot Q[j+1] + 9\cdot Q[j+2]Q[j+3]=?5Q[j]+8Q[j+1]+9Q[j+2]

这里的乘法与加法是十进制大整数的真运算(程序自己实现的大整数字符串乘/加),并且不允许出现 Q[j]==Q[j+1]==Q[j+2] 的三连相等。期望的“第一次不满足”应当恰好发生在 j=2036(代码在不满足时会 j++break,随后检查 j == 2040-3),否则判定失败。

小结:SN 的本质是让 B 经过上面的链条生成的 Quotient 满足 2025 位分块后的长距离线性递推关系,且第一次失败点恰好落在规定的位置。

解码器的核心规则(从源码 KX_RGVjb2Rl 直接整理)如下:

允许的字符:0–9l.

语法(伪 EBNF):

约束:

举例:"2""1""l""9""1""l""0""6""l" 会被解码为 29 后面 6 个 0,即 29000000

要点是:我们希望 R = 27/B 的十进制(科学计数法)转成的 Quotient(去掉小数点和 E- 后补零)在长度 4,133,025 的数字串上,按 2025 位分块满足前述线性递推,并且第一次不满足恰好出现在 j=2036

一个可操作的通用构造法:

直接在“块空间”里造一个满足递推的序列:任选 3 个 2025 位的非全零首块 Q[0], Q[1], Q[2](避免三连相等),按

50aK9s2c8@1M7q4)9K6b7g2)9J5c8W2)9J5c8Y4N6%4N6#2)9J5k6i4M7K6i4K6u0W2L8%4u0Y4i4K6u0r3x3e0V1&6z5q4)9J5c8V1#2S2N6r3S2Q4x3V1k6y4j5i4c8Z5e0f1H3`.">Q[j+]=Q[j]+Q[j+]+Q[j+]Q[j+3] = 5Q[j] + 8Q[j+1] + 9Q[j+2]Q[j+3]=5Q[j]+8Q[j+1]+9Q[j+2]

推到 j=2035。然后特意让 Q[2039] \ne 5Q[2036]+8Q[2037]+9Q[2038](比如把 Q[2039] 的末位改一下),从而把“第一次不满足”固定在 j=2036

把这些块首尾拼接成一个总长 2041×2025 = 4,133,025 位的数字串 D;定义

d87K9s2c8@1M7q4)9K6b7g2)9J5c8W2)9J5c8Y4N6%4N6#2)9J5k6i4M7K6i4K6u0W2L8%4u0Y4i4K6u0r3x3e0V1&6z5q4)9J5c8V1#2S2N6r3S2Q4x3V1k6y4j5i4c8Z5e0f1H3`.">R=D×KR \;=\; 1.D \times 10^{-K}R=1.D×10K

其中选择一个足够大的 K(至少使 Python 以 E- 输出;比如 K≥7 就够了),这样 C# 会按它的逻辑把 R 还原回我们拼出来的 Quotient

cfdK9s2c8@1M7q4)9K6b7g2)9J5c8W2)9J5c8Y4N6%4N6#2)9J5k6i4M7K6i4K6u0W2L8%4u0Y4i4K6u0r3x3e0V1&6z5q4)9J5c8V1#2S2N6r3S2Q4x3V1k6y4j5i4c8Z5e0f1H3`.">B=B \;=\; \frac{27}{R}B=R27

然后把十进制的 B 用上面的游程编码压缩成一个长度 26~50 的 SN。(为了让编码尽量短,可让 B 的十进制形态由极少种数字的长段组成,比如“前缀若干位 + 很长一段 0 + 很长一段 9 + …”,从而用极少几个 <digit><count>l 就能覆盖。)

最后用源码里的同一套字符串乘/加校验一次(完全照抄 KX_TXVsdGlTdHJpbmdzKX_QWRkU3RyaW5ncw雪雪 算法)以确保通过。

说明:之所以这样构造是因为程序并不从 B 直接检验什么“数学性质”;它只关心通过 Python 得到的 数字串在 2025 位块上的线性关系。因此**先造数字串,再反推 B**是最稳妥的路线。

你可以把自己想要的 Q[0..2039](或直接把最终的 B)喂进下面这段流程做本地验证:

下面是我把思路 + 代码揉成一篇完整 write-up 的版本。它从还原校验逻辑开始,解释为什么“构造一个特定分母”能一招过关,然后给出确定性构造器的实现要点与可直接运行的脚本/命令。为便于核对,我在关键处都标了源码引用。

给定脱壳后的 C# crackme 与它释放调用的 Python 计算器 20250805Calc-pub.py,当输入 Name="KCTF" 时,求一组满足校验的 SN
校验大意是:把 A=int(NameRule) 与经“自定义游程编码”解出的 B 送去 Python 做高精度除法,拿回科学计数法结果,再把 mantissa 去小数点、按指数左补 0 得到一个超长纯数字串 Quotient;将它按 2025 位切块,检查一个跨 2040 个块的线性递推是否成立,且“第一次不满足”出现在最后的窗口边界。

程序把 Name 的 UTF-8 字节做异或、对 64 取模,若为偶数再加 1。A = num.ToString()。对 KCTF 来说,最后得到 A="27"

SN 只能含 数字/l/.,语法上是一串“数字 + 计数 + l”的 run,可在 run 与 run 之间插入 最多一个小数点。长度必须在 [26,50],不许以 0 开头,也不许以 . 结尾;计数不允许前导 0;收尾允许省略最后一个 run 的 l(源码尾段会补齐这一段)。这些条件不满足直接失败。

程序还在进入主流程前强制:AB最后一位都不能是 '0'

C# 把 AB 作为参数调用释放出的 Python 可执行,Python 端仅做:
getcontext().prec = prec; print(Decimal(A) / Decimal(B))。其中 prec = (2040+1) * 2025 = 4,133,025

C# 端读取输出,只在包含 E(科学计数法)时,才把结果切成 ["1", 小数部分, 指数] 三段:

Quotient每 2025 位一块切分为 Q[0], Q[1], ...,然后在 j=0..2036 依次检查:

9adK9s2c8@1M7q4)9K6b7g2)9J5c8W2)9J5c8Y4N6%4N6#2)9J5k6i4M7K6i4K6u0W2L8%4u0Y4i4K6u0r3x3e0V1&6z5q4)9J5c8V1#2S2N6r3S2Q4x3V1k6y4j5i4c8Z5e0f1H3`.">Q[j+]Q[j]+Q[j+]+Q[j+]Q[j+3] \;\stackrel{?}{=}\; 5\cdot Q[j] \;+\; 8\cdot Q[j+1] \;+\; 9\cdot Q[j+2]Q[j+3]=?5Q[j]+8Q[j+1]+9Q[j+2]

这里的加/乘都是十进制大整数的真运算(源码自己写了“字符串 × 常数”“字符串 + 字符串”)。如果 Q[j]==Q[j+1]==Q[j+2] 的三连块相等出现,直接把 jint.MaxValue 使其失败。整个循环第一次不满足时会 j++ 再跳出,最终要求 j == 2040-3,否则打印 Failed!

乘法 ×5/8/9 与加法的字符串实现分别在 KX_TXVsdGlTdHJpbmdzKX_QWRkU3RyaW5ncw雪雪,都是从最低位起逐位进位的朴素实现。

观察到递推关系写在“块空间”里,且块宽正好是 2025 位。只要我们能让十进制长除出来的 Quotient 在 2025 位的分块意义下满足

557K9s2c8@1M7q4)9K6b7g2)9J5c8W2)9J5c8Y4N6%4N6#2)9J5k6i4M7K6i4K6u0W2L8%4u0Y4i4K6u0r3x3e0V1&6z5q4)9J5c8V1#2S2N6r3S2Q4x3V1k6y4j5i4c8Z5e0f1H3`.">=++,Q_{j+3} = 5Q_j + 8Q_{j+1} + 9Q_{j+2},Qj+3=5Qj+8Qj+1+9Qj+2,

那么它自然能通过校验。因此更稳妥的办法是——先在“块域”把递推写进分母,再反推出合法的 SN

令基数 035K9s2c8@1M7q4)9K6b7g2)9J5c8W2)9J5c8Y4N6%4N6#2)9J5k6i4M7K6i4K6u0W2L8%4u0Y4i4K6u0r3x3e0V1&6z5q4)9J5c8V1#2S2N6r3S2Q4x3V1k6y4j5i4c8Z5e0f1H3`.">B=mB = 10^{m}B=10mm=2025),把 2025 位看成“一个 B 进制数字”。

构造分母多项式

ae1K9s2c8@1M7q4)9K6b7g2)9J5c8W2)9J5c8Y4N6%4N6#2)9J5k6i4M7K6i4K6u0W2L8%4u0Y4i4K6u0r3x3e0V1&6z5q4)9J5c8V1#2S2N6r3S2Q4x3V1k6y4j5i4c8Z5e0f1H3`.">D=BBB=mmmD \;=\; B^3 \;-\; 9\,B^2 \;-\; 8\,B \;-\; 5 \;=\; 10^{3m} - 9\cdot 10^{2m} - 8\cdot 10^{m} - 5.D=B39B28B5=103m9102m810m5.

最终输入分母476K9s2c8@1M7q4)9K6b7g2)9J5c8W2)9J5c8Y4N6%4N6#2)9J5k6i4M7K6i4K6u0W2L8%4u0Y4i4K6u0r3x3e0V1&6z5q4)9J5c8V1#2S2N6r3S2Q4x3V1k6y4j5i4c8Z5e0f1H3`.">=AD\textbf{B} = A \cdot DB=AD。由于 A=27,此时

d48K9s2c8@1M7q4)9K6b7g2)9J5c8W2)9J5c8Y4N6%4N6#2)9J5k6i4M7K6i4K6u0W2L8%4u0Y4i4K6u0r3x3e0V1&6z5q4)9J5c8V1#2S2N6r3S2Q4x3V1k6y4j5i4c8Z5e0f1H3`.">==.\frac{A}{\textbf{B}} \;=\; \frac{27}{27D} \;=\; \frac{1}{D}.BA=27D27=D1.

在“以 b5dK9s2c8@1M7q4)9K6b7g2)9J5c8W2)9J5c8Y4N6%4N6#2)9J5k6i4M7K6i4K6u0W2L8%4u0Y4i4K6u0r3x3e0V1&6z5q4)9J5c8V1#2S2N6r3S2Q4x3V1k6y4j5i4c8Z5e0f1H3`.">B=mB=10^mB=10m 为基数”的视角看,757K9s2c8@1M7q4)9K6b7g2)9J5c8W2)9J5c8Y4N6%4N6#2)9J5k6i4M7K6i4K6u0W2L8%4u0Y4i4K6u0r3x3e0V1&6z5q4)9J5c8V1#2S2N6r3S2Q4x3V1k6y4j5i4c8Z5e0f1H3`.">\frac{1}{D}D1 的长除过程天然满足上面的三阶线性递推,恰与程序检查一致。

Python 打印 1.xxxxxE-…,C# 再把 1 与小数部分拼起来并按指数左补 0,得到的 Quotient 就是我们在“块域”里预期的巨大十进制串。

要注意两点工程细节:

我在求解器里加了确定性构造器,关键函数如下(已简化展示):

我用确定性构造器跑出的 SN 为(50 字符):

脚本会逐步做:SN 解码 → 调用与 crackme 一致的 Decimal 除法精度 → 构造 Quotient → 2025 位分块大整数检验;并报告“第一次失败”的 j 值应为 2040-3。精度与流程与原程序严格一致。

# Decompiled with PyLingual (3b3K9s2c8@1M7s2y4Q4x3@1q4Q4x3V1k6Q4x3V1k6H3P5h3I4A6L8X3N6#2j5h3I4Q4x3X3g2A6L8#2)9J5z5b7`.`.
# Internal filename: 20250805Calc-pub.py
# Bytecode version: 3.12.0rc2 (3531)
# Source timestamp: 1970-01-01 00:00:00 UTC (0)
 
from decimal import Decimal, getcontext
import os
import sys
 
def reciprocal(m, n, prec):
    getcontext().prec = int(prec)
    result = Decimal(m) / Decimal(n)
    return result
 
def main(arg1, arg2, arg3):
    sys.set_int_max_str_digits(int(arg3))
    reciprocal_value = reciprocal(arg1, arg2, arg3)
    print(reciprocal_value)
if __name__ == '__main__':
    arg1 = sys.argv[1]
    arg2 = sys.argv[2]
    arg3 = sys.argv[3]
    main(arg1, arg2, arg3)
# Decompiled with PyLingual (3f5K9s2c8@1M7s2y4Q4x3@1q4Q4x3V1k6Q4x3V1k6H3P5h3I4A6L8X3N6#2j5h3I4Q4x3X3g2A6L8#2)9J5z5b7`.`.
# Internal filename: 20250805Calc-pub.py
# Bytecode version: 3.12.0rc2 (3531)
# Source timestamp: 1970-01-01 00:00:00 UTC (0)
 
from decimal import Decimal, getcontext
import os
import sys
 
def reciprocal(m, n, prec):
    getcontext().prec = int(prec)
    result = Decimal(m) / Decimal(n)
    return result
 
def main(arg1, arg2, arg3):
    sys.set_int_max_str_digits(int(arg3))
    reciprocal_value = reciprocal(arg1, arg2, arg3)
    print(reciprocal_value)
if __name__ == '__main__':
    arg1 = sys.argv[1]
    arg2 = sys.argv[2]
    arg3 = sys.argv[3]
    main(arg1, arg2, arg3)
<digit><count>l<digit><count>l... [可选插入 '.']
<digit><count>l<digit><count>l... [可选插入 '.']
R = Decimal(A) / Decimal(B)
R = Decimal(A) / Decimal(B)
SN  := run { "." run } [ 可结尾处省略最后一个 'l' ]
run := digit count "l"
digit := '0'..'9'               // 本段要重复输出的数字
count := '1'..'9' { '0'..'9' // 计数,禁止前导0,且 > 0
SN  := run { "." run } [ 可结尾处省略最后一个 'l' ]
run := digit count "l"
digit := '0'..'9'               // 本段要重复输出的数字
count := '1'..'9' { '0'..'9' // 计数,禁止前导0,且 > 0
def _build_D_digits(m=2025) -> str:
    # 直接在十进制字符串上构造 D = 10^(3m) - 9·10^(2m) - 8·10^m - 5
    digs = ['0']*(3*m+1)
    digs[0] = '1'
    sub_at_power(2*m, 9); sub_at_power(1*m, 8); sub_at_power(0, 5)
    return "".join(digs)
 
def deterministic_sn_for_name(name, m=2025) -> str:
    A = int(name_to_A(name))       # A="27" for "KCTF"
    D = _build_D_digits(m)         # 长度 6076,尾数 ...5
    B = _mul_small_str(D, A)       # B = A*D,尾数 ...5
    runs = _rle_digits(B)          # 只有 ~14 段
    return _encode_runs_to_sn(runs, omit_last_l=True)
def _build_D_digits(m=2025) -> str:
    # 直接在十进制字符串上构造 D = 10^(3m) - 9·10^(2m) - 8·10^m - 5
    digs = ['0']*(3*m+1)
    digs[0] = '1'

传播安全知识、拓宽行业人脉——看雪讲师团队等你加入!

最后于 2025-9-1 10:31 被DNLINYJ编辑 ,原因:
上传的附件:
收藏
免费 1
支持
分享
最新回复 (0)
游客
登录 | 注册 方可回帖
返回