【已更新】【首发】SHCTF第三届山河CTFMisc(二)

admin 2026-03-03 03:48:31 网络安全文章 来源:ZONE.CI 全球网 0 阅读模式

文章总结: 该文档详细解析了第三届山河CTFMisc部分赛题。第一题通过解析RGB元组还原二维码并解码Base64。第二题利用已知明文攻击还原自定义Base64映射表,结合PNGCRC32碰撞补全缺失字符,解密图片获取密码后破解WinZipAES加密压缩包。第三题针对图像隐写,通过识别QQ等级图标权重进行进制转换与逻辑推导还原ASCII字符。文章提供了完整的Python解题脚本,涵盖密码学、编码转换及图像处理技术。 综合评分: 91 文章分类: CTF,解决方案,逆向分析,实战经验


手机扫描

压缩包密码

base64_15_n0t_3ncrypt10n

还原zip

flag.zip 不是普通的%20Zip%20加密,而是 WinZip%20AES 标准。

结构:ZIP%20文件头中有一个%20Extra%20Field%20(ID 0x9901),标记了它是%20AES%20加密。

解密流程:

读取%20Salt(盐值)。%20使用%20PBKDF2%20算法,结合密码%20base64_15_n0t_3ncrypt10n%20和%20Salt,生成解密密钥(AES%20Key)和认证密钥(HMAC%20Key)。%20验证密码提示位(Password%20Verification%20Value)。%20使用%20AES(通常是%20CTR%20模式)解密内容。%20使用%20HMAC-SHA1%20验证解密后的数据完整性(这就是为什么你手动解压会报%20CRC/校验错误,因为手动工具可能没处理好这步)。%20最后解压(Inflate/Deflate)。

flag

解压出的%20flag.txt.enc%20内容是一串乱码,因为它也被%20自定义%20Base64%20加密了。%20使用第1步恢复的映射表,对其进行解码,得到最终%20Flag。

exp.py

import%20base64
import%20itertools
import%20struct
import%20zlib
from%20pathlib%20import%20Path

try:
 %20 %20from%20Crypto.Protocol.KDF%20import%20PBKDF2
 %20 %20from%20Crypto.Hash%20import%20SHA1,%20HMAC
 %20 %20from%20Crypto.Cipher%20import%20AES
except%20ImportError:
 %20  print("请先运行:%20pip%20install%20pycryptodome")
 %20  exit()

SBA%20= "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"

def%20pzfh(zd,%20sp=0):
 %20  if zd[sp:sp+4]%20!=%20b"PK\x03\x04":%20raise%20ValueError("不是有效的ZIP头")
&nbsp;%20&nbsp;%20s,%20v,%20f,%20cm,%20mt,%20md,%20crc,%20cs,%20us,%20fnl,%20efl%20=%20struct.unpack_from("<4sHHHHHIIIHH",%20zd,%20sp)
&nbsp;%20&nbsp;%20cp%20=%20sp%20+%2030
&nbsp;%20&nbsp;%20fn%20=%20zd[cp:cp+fnl].decode(errors="replace")
&nbsp;%20&nbsp;%20cp%20+=%20fnl
&nbsp;%20&nbsp;%20ef%20=%20zd[cp:cp+efl]
&nbsp;%20&nbsp;&nbsp;return&nbsp;{"v":%20v,&nbsp;"f":%20f,&nbsp;"cm":%20cm,&nbsp;"crc":%20crc,&nbsp;"cs":%20cs,&nbsp;"us":%20us,&nbsp;"fn":%20fn,&nbsp;"ef":%20ef,&nbsp;"dsp":%20cp+efl,&nbsp;"hsp":%20sp}

def%20pwaef(efd):
&nbsp;%20&nbsp;%20p%20=%200
&nbsp;%20&nbsp;&nbsp;while&nbsp;p%20+%204%20<=%20len(efd):
&nbsp;%20&nbsp;%20&nbsp;%20&nbsp;%20fid%20=%20int.from_bytes(efd[p:p+2],&nbsp;"little")
&nbsp;%20&nbsp;%20&nbsp;%20&nbsp;%20fsz%20=%20int.from_bytes(efd[p+2:p+4],&nbsp;"little")
&nbsp;%20&nbsp;%20&nbsp;%20&nbsp;%20fb%20=%20efd[p+4:p+4+fsz];%20p%20+=%204%20+%20fsz
&nbsp;%20&nbsp;%20&nbsp;%20&nbsp;&nbsp;if&nbsp;fid%20==%200x9901:
&nbsp;%20&nbsp;%20&nbsp;%20&nbsp;%20&nbsp;%20&nbsp;&nbsp;if&nbsp;len(fb)%20!=%207:%20raise%20ValueError("AES字段长度错误")
&nbsp;%20&nbsp;%20&nbsp;%20&nbsp;%20&nbsp;%20&nbsp;%20av%20=%20int.from_bytes(fb[0:2],&nbsp;"little")
&nbsp;%20&nbsp;%20&nbsp;%20&nbsp;%20&nbsp;%20&nbsp;%20vc%20=%20fb[2:4]
&nbsp;%20&nbsp;%20&nbsp;%20&nbsp;%20&nbsp;%20&nbsp;%20ks%20=%20fb[4]
&nbsp;%20&nbsp;%20&nbsp;%20&nbsp;%20&nbsp;%20&nbsp;%20acm%20=%20int.from_bytes(fb[5:7],&nbsp;"little")
&nbsp;%20&nbsp;%20&nbsp;%20&nbsp;%20&nbsp;%20&nbsp;&nbsp;return&nbsp;av,%20vc,%20ks,%20acm
&nbsp;%20&nbsp;%20raise%20ValueError("未找到AES额外字段")

def%20dwaes(ed,%20pw,%20ks):
&nbsp;%20&nbsp;%20kl%20=%20{1:16,2:24,3:32}[ks];%20sl%20=%20{1:8,2:12,3:16}[ks]
&nbsp;%20&nbsp;%20s%20=%20ed[:sl];%20pv%20=%20ed[sl:sl+2];%20mac%20=%20ed[-10:];%20ct%20=%20ed[sl+2:-10]
&nbsp;%20&nbsp;%20dk%20=%20PBKDF2(pw,%20s,%20dkLen=2*kl+2,%20count=1000,%20hmac_hash_module=SHA1)
&nbsp;%20&nbsp;%20ek%20=%20dk[:kl];%20ak%20=%20dk[kl:2*kl];%20pc%20=%20dk[2*kl:2*kl+2]

&nbsp;%20&nbsp;&nbsp;if&nbsp;pc%20!=%20pv:%20raise%20ValueError("密码错误%20(校验值不匹配)")
&nbsp;%20&nbsp;&nbsp;if&nbsp;HMAC.new(ak,%20ct,%20SHA1).digest()[:10]%20!=%20mac:%20raise%20ValueError("HMAC校验失败%20(文件损坏或被篡改)")

&nbsp;%20&nbsp;%20c%20=%20AES.new(ek,%20AES.MODE_ECB);%20r%20=%20bytearray();%20cnt%20=%201
&nbsp;%20&nbsp;&nbsp;for&nbsp;i&nbsp;in&nbsp;range(0,%20len(ct),%2016):
&nbsp;%20&nbsp;%20&nbsp;%20&nbsp;%20b%20=%20ct[i:i+16];%20cb%20=%20struct.pack("<I",%20cnt)%20+%20b"\x00"*12;%20ks_stream%20=%20c.encrypt(cb)
&nbsp;%20&nbsp;%20&nbsp;%20&nbsp;%20r.extend(bytes(x%20^%20ks_stream[j]&nbsp;for&nbsp;j,x&nbsp;in&nbsp;enumerate(b)));%20cnt%20+=%201
&nbsp;%20&nbsp;&nbsp;return&nbsp;bytes(r)

def%20vpng(pd):
&nbsp;%20&nbsp;&nbsp;if&nbsp;not%20pd.startswith(b"\x89PNG\r\n\x1a\n"):&nbsp;return&nbsp;False
&nbsp;%20&nbsp;%20p%20=%208
&nbsp;%20&nbsp;&nbsp;while&nbsp;p%20<%20len(pd):
&nbsp;%20&nbsp;%20&nbsp;%20&nbsp;&nbsp;if&nbsp;p%20+%208%20>%20len(pd):&nbsp;return&nbsp;False
&nbsp;%20&nbsp;%20&nbsp;%20&nbsp;%20l%20=%20struct.unpack(">I",%20pd[p:p+4])[0];%20t%20=%20pd[p+4:p+8];%20p%20+=%208
&nbsp;%20&nbsp;%20&nbsp;%20&nbsp;&nbsp;if&nbsp;p%20+%20l%20+%204%20>%20len(pd):&nbsp;return&nbsp;False
&nbsp;%20&nbsp;%20&nbsp;%20&nbsp;%20d%20=%20pd[p:p+l];%20p%20+=%20l
&nbsp;%20&nbsp;%20&nbsp;%20&nbsp;%20sc%20=%20struct.unpack(">I",%20pd[p:p+4])[0];%20p%20+=%204
&nbsp;%20&nbsp;%20&nbsp;%20&nbsp;%20cc%20=%20zlib.crc32(t)%20&%200xFFFFFFFF;%20cc%20=%20zlib.crc32(d,%20cc)%20&%200xFFFFFFFF
&nbsp;%20&nbsp;%20&nbsp;%20&nbsp;&nbsp;if&nbsp;cc%20!=%20sc:&nbsp;return&nbsp;False
&nbsp;%20&nbsp;%20&nbsp;%20&nbsp;&nbsp;if&nbsp;t%20==%20b"IEND":&nbsp;return&nbsp;p%20==%20len(pd)
&nbsp;%20&nbsp;&nbsp;return&nbsp;False

def%20bpbm(pt,%20et):
&nbsp;%20&nbsp;%20sb%20=%20base64.b64encode(pt).decode();%20m%20=%20{}
&nbsp;%20&nbsp;&nbsp;for&nbsp;sc,%20cc&nbsp;in&nbsp;zip(sb,%20et):%20m[sc]%20=%20cc
&nbsp;%20&nbsp;%20um%20=%20[c&nbsp;for&nbsp;c&nbsp;in&nbsp;SBA&nbsp;if&nbsp;c%20not&nbsp;in&nbsp;m];%20uc%20=%20[c&nbsp;for&nbsp;c&nbsp;in&nbsp;SBA&nbsp;if&nbsp;c%20not&nbsp;inset(m.values())]
&nbsp;%20&nbsp;&nbsp;return&nbsp;m,%20um,%20uc

def%20dcb(es,%20rm):&nbsp;return&nbsp;base64.b64decode(es.translate(str.maketrans(rm)))

def%20rcbm(pt,%20ept,%20epng):
&nbsp;%20&nbsp;%20pm,%20ms,%20ac%20=%20bpbm(pt,%20ept)
&nbsp;%20&nbsp;&nbsp;for&nbsp;p&nbsp;in&nbsp;itertools.permutations(ac):
&nbsp;%20&nbsp;%20&nbsp;%20&nbsp;%20cm%20=%20pm.copy();%20cm.update({s:c&nbsp;for&nbsp;s,c&nbsp;in&nbsp;zip(ms,%20p)})
&nbsp;%20&nbsp;%20&nbsp;%20&nbsp;%20rm%20=%20{c:s&nbsp;for&nbsp;s,c&nbsp;in&nbsp;cm.items()}
&nbsp;%20&nbsp;%20&nbsp;%20&nbsp;%20try:%20dp%20=%20dcb(epng,%20rm)
&nbsp;%20&nbsp;%20&nbsp;%20&nbsp;%20except:&nbsp;continue
&nbsp;%20&nbsp;%20&nbsp;%20&nbsp;&nbsp;if&nbsp;vpng(dp):&nbsp;return&nbsp;cm,%20rm
&nbsp;%20&nbsp;%20raise%20RuntimeError("无法通过PNG校验找到映射表")

def%20main():
&nbsp;%20&nbsp;%20d%20=%20Path(".")
&nbsp;%20&nbsp;&nbsp;if&nbsp;not%20(d/"Readme.txt").exists():
&nbsp;%20&nbsp;%20&nbsp;%20&nbsp;&nbsp;print("缺少%20Readme.txt")
&nbsp;%20&nbsp;%20&nbsp;%20&nbsp;&nbsp;return

&nbsp;%20&nbsp;%20r%20=%20(d/"Readme.txt").read_bytes()
&nbsp;%20&nbsp;%20er%20=%20(d/"Readme.txt.enc").read_text(encoding='utf-8').strip()
&nbsp;%20&nbsp;%20ep%20=%20(d/"png.png.enc").read_text(encoding='utf-8').strip()
&nbsp; &nbsp; ez = (d/"flag.zip.enc").read_text(encoding='utf-8').strip()

&nbsp; &nbsp;&nbsp;print("[*] 正在恢复映射表...")
&nbsp; &nbsp; m, rm = rcbm(r, er, ep)
&nbsp; &nbsp;&nbsp;print("[+] 映射表恢复成功")

&nbsp; &nbsp;&nbsp;print("[*] 解码 flag.zip.enc ...")
&nbsp; &nbsp; dz = dcb(ez, rm)

&nbsp; &nbsp;&nbsp;print("[*] 解析 ZIP 结构...")
&nbsp; &nbsp; zh = pzfh(dz, 0)

&nbsp; &nbsp; av, vc, ks, acm = pwaef(zh["ef"])
&nbsp; &nbsp;&nbsp;if&nbsp;vc != b"AE":
&nbsp; &nbsp; &nbsp; &nbsp; raise ValueError("不是 WinZip AES 格式")

&nbsp; &nbsp;&nbsp;print(f"[+] 发现 AES 加密 (KeyStrength={ks}, Method={acm})")
&nbsp; &nbsp;&nbsp;print("[*] 正在解密 (密码: base64_15_n0t_3ncrypt10n) ...")

&nbsp; &nbsp; ed = dz[zh["dsp"]:zh["dsp"]+zh["cs"]]

&nbsp; &nbsp; pw = b"base64_15_n0t_3ncrypt10n"

&nbsp; &nbsp; dd = dwaes(ed, pw, ks)

&nbsp; &nbsp;&nbsp;if&nbsp;acm == 8:
&nbsp; &nbsp; &nbsp; &nbsp; dd = zlib.decompress(dd, -zlib.MAX_WBITS)
&nbsp; &nbsp;&nbsp;elif&nbsp;acm != 0:
&nbsp; &nbsp; &nbsp; &nbsp; raise NotImplementedError("不支持的压缩格式")

&nbsp; &nbsp; flag_cipher = dd.decode('utf-8').strip()
&nbsp; &nbsp;&nbsp;print(f"\n[*] 解出的内部密文: {flag_cipher}")

&nbsp; &nbsp;&nbsp;print("[*] 正在进行最终解码...")
&nbsp; &nbsp; final_flag = dcb(flag_cipher, rm).decode('utf-8')

&nbsp; &nbsp;&nbsp;print("\n"&nbsp;+&nbsp;"="*40)
&nbsp; &nbsp;&nbsp;print(f"FLAG: {final_flag}")
&nbsp; &nbsp;&nbsp;print("="*40 +&nbsp;"\n")

if&nbsp;__name__ ==&nbsp;"__main__":
&nbsp; &nbsp; main()

我们发现这个15个解码然后按照顺序拼接就是png图片

[碎片 01] 长度: 2819 | 头: E4603EDA | 尾: 00374DE4
[碎片 02] 长度: 2820 | 头: E8288ECE | 尾: ABF1D294
[碎片 03] 长度: 2820 | 头: 3E7A816A | 尾: 683C3A60
[碎片 04] 长度: 2820 | 头: 89504E47 | 尾: AD350638
[碎片 05] 长度: 2819 | 头: DDF73DA8 | 尾: 6A7F4B02
[碎片 06] 长度: 2819 | 头: 9FCE75CE | 尾: A74F599E
[碎片 07] 长度: 2820 | 头: 6B8FDC0A | 尾: BC2C6D83
[碎片 08] 长度: 2820 | 头: 983ADE03 | 尾: 9957E8AD
[碎片 09] 长度: 2819 | 头: FC282E9A | 尾: 7583603D
[碎片 10] 长度: 2820 | 头: 7574D422 | 尾: 9357D3A6
[碎片 11] 长度: 2819 | 头: C38F1A70 | 尾: C0DA2380
[碎片 12] 长度: 2820 | 头: EAA94D02 | 尾: 3F03BED0
[碎片 13] 长度: 2819 | 头: 798FEC6B | 尾: AE426082
[碎片 14] 长度: 2820 | 头: 12158EB3 | 尾: 27185E91
[碎片 15] 长度: 2819 | 头: E69DDB44 | 尾: DB424F6C

[!] 发现 PNG 文件头在: 碎片 4
[!] 发现 PNG 文件尾在: 碎片 13

这证实了 1.png 到 15.png 的文件名并不是正确的文件顺序,而是网格位置(1-4是第一行,5-8是第二行,以此类推)。 题目叫&nbsp;"Structured Chaos"&nbsp;(有序的混乱),且起点是 4 (右上角),终点是 13 (左下角)。这极有可能是一个特定的几何路径。

顺序是行优先,从右向左

4, 3, 2, 1, 8, 7, 6, 5, 12, 11, 10, 9, 15, 14, 13

结果出来这个题目时一个套娃题目

思路就是:二维码协议支持将一个大的文件切分成最多 16 个碎片进行传输。每个碎片中包含:序列号:标识该碎片在整体中的位置。校验和:用于确保所有碎片属于同一个文件。 题目中的 15 枚残片正是利用此协议。虽然它们在 4 \times 4的网格中看似杂乱无章,但 zbarimg 等专业工具可以自动识别这种协议,并按正确的字节顺序直接重组出原始二进制流,而无需手动计算拼接路径。

套娃

解码后的数据并非文本,而是一个完整的 PNG 文件头 89 50 4E 47。 打开该图片后,内容依然是一个二维码。重复解码过程会发现,每一层二维码的内容都是下一层图片的二进制流。这种“套娃”结构总共嵌套了 11 层。

解题

调用 zbarimg 获取当前图片的二进制输出。 判断数据头是否为 PNG 特征码 89 50 4E 47。 若是图片,则保存并作为下一轮输入;若不是,则触达终点。

exp.py

import subprocess, os

cur, lv =&nbsp;"Structured Chaos.png", 0
while&nbsp;True:
&nbsp; &nbsp; lv += 1
&nbsp; &nbsp; res = subprocess.run(["zbarimg",&nbsp;"-q",&nbsp;"--raw",&nbsp;"--oneshot",&nbsp;"-Sbinary", cur], capture_output=True)
&nbsp; &nbsp;&nbsp;if&nbsp;res.returncode != 0 or not res.stdout:&nbsp;break

&nbsp; &nbsp; out = f"lv{lv}.bin"
&nbsp; &nbsp; with open(out,&nbsp;"wb") as f: f.write(res.stdout)

&nbsp; &nbsp;&nbsp;if&nbsp;res.stdout.startswith(b'\x89PNG'):
&nbsp; &nbsp; &nbsp; &nbsp; cur = f"lv{lv}.png"
&nbsp; &nbsp; &nbsp; &nbsp; os.rename(out, cur)
&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;print(f"[+] Lv{lv}: {cur} ({len(res.stdout)} bytes)")
&nbsp; &nbsp;&nbsp;else:
&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;print(f"\n[!] End: {out}\n{subprocess.run(['file', out], capture_output=True, text=True).stdout.strip()}")
&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;break

套的真多

SHCTF{57ruc7ur3d_App3nd_J1gs4w_R3c0n57ruc73d}

问卷反馈

这里吐槽一下osint的题目,我是真不想玩mc,而且这个看着学不到什么东西,我一题没做,这里仅代表个人观点,一点小小的吐槽

填完问卷就行

SHCTF{th@nK_y0u_FoR_pAr7icipat1n9_in_SHCTF_3rd}

靶场推荐

好靶场

学安全,别只看书上手练,就来好靶场。

🔗入口:http://www.loveli.com.cn/

有宝子就问了,主播主播,这么好的靶场怎么用:首先关注好靶场 然后发送bug,可以点击链接直接登录

福利1

找到个人中心,邀请码输入3e5adb8a55db48b8,白嫖14天高级会员。

福利2

关注好靶场bilibili。拿着关注截图找到客服,领取5积分或者7天高级会员。

往期推荐

比赛篇:

【比赛篇】furryCTF 2025 高校联合新神赛(Web)

【比赛篇】furryCTF 2025 高校联合新神赛(Misc)

【比赛篇】furryCTF 2025 高校联合新神赛(PPC+Pwn+Forensics)

【比赛篇】furryCTF 2025 高校联合新神赛(完结)

【首发】SHCTF 第三届山河CTF  Web(一)

【首发】第三届山河CTF  Web(二)

【首发】SHCTF 第三届山河CTF  crypto(一)

【首发】SHCTF 第三届山河CTF  crypto(二)

【已更新】【首发】SHCTF 第三届山河CTF  Misc(一)

工具推荐:

告别手输验证码!这款Burp插件+OCR神技,轻松搞定登录框爆破

Burp Suite 插件实战:利用 JWT Editor 一站式通杀四种 JWT 认证缺陷

实战分享:手把手教你突破前端加密混淆限制

还是抓不到包?手把手教你解决安卓高版本 HTTPS 证书报错

安卓逆向第一课:如何用JADX挖出隐藏的Flag?


免责声明:

本文所载程序、技术方法仅面向合法合规的安全研究与教学场景,旨在提升网络安全防护能力,具有明确的技术研究属性。

任何单位或个人未经授权,将本文内容用于攻击、破坏等非法用途的,由此引发的全部法律责任、民事赔偿及连带责任,均由行为人独立承担,本站不承担任何连带责任。

本站内容均为技术交流与知识分享目的发布,若存在版权侵权或其他异议,请通过邮件联系处理,具体联系方式可点击页面上方的联系我

本文转载自:小叶Sec 小叶Sec 小叶Sec《【已更新】【首发】SHCTF 第三届山河CTF Misc(二)》

评论:0   参与:  0