=========================
MISC-先声夺人
解压得到一个 challenge.txt,看似一段普通的多语言文本:
Election campaigns turn economics into slogans. “Jobs first” sounds simple, pero los números no lo son. 中文里常提"稳增长"和"就业优先"。 En français: “pouvoir d’achat” dominates debates. Deutsch: Kaufkraft und Vertrauen. 日本語の討論では「物価高対策」, 한국어에서는 “청년 실업"이 자주 나온다. Even small stats become big arguments.
题目名「先声夺人」直接点明了思路——声(可见文本)先到,夺(隐藏信息)在后。零宽字符隐写无误。
文本中有两处提示不容忽视:
“los números no lo son”(数字并不简单)——编码映射不是常规的 0/1 二进制
整段文字讨论的是选举中经济口号如何影响选民——数据(números)被包装成口号(声),暗示可见文本是载体,真正数据藏在零宽字符里
检查文件的 Unicode 码点,发现四种零宽字符贯穿全文:
字符 码点 名称 总数 U+200B ZERO WIDTH SPACE 677 U+200C ZERO WIDTH NON-JOINER 640 U+200D ZERO WIDTH JOINER 622 U+FEFF ZERO WIDTH NO-BREAK SPACE 653
共计 2592 个零宽字符。其中 85 个散布在可见文本的词间,剩余 2507 个集中在文件末尾。
一眼看出 4 种字符 = 2-bit 编码,2592 × 2 = 5184 bits = 648 bytes。
解题步骤
Step 1: 分离嵌入字符与尾部数据块
import re
with open(‘challenge.txt’, ‘r’, encoding=‘utf-8’) as f:
text = f.read()
找到零宽字符块的起始位置(连续 20 个以上零宽字符判定为块起始)
zw_set = {‘’, ‘’, ‘’, ‘’}
pos = 0
for i, ch in enumerate(text):
if ch in zw_set:
run = 0
for j in range(i, min(i + 20, len(text))):
if text[j] in zw_set:
run += 1
else:
break
if run >= 20:
pos = i
break
embedded = [c for c in text[:pos] if c in zw_set] # 85 个
payload = [c for c in text[pos:] if c in zw_set] # 2507 个
Step 2: 解码嵌入字符获取映射提示
散布在可见文本中的 85 个零宽字符采用 StegCloak 标准格式编码。StegCloak 的编码规则是 ZWJ(U+200D)作为字符分隔符、ZWNJ(U+200C)代表 bit 0、ZWSP(U+200B)代表 bit 1,ZWNBS(U+FEFF)为词分隔符。
embedded_str = ‘’.join(embedded)
StegCloak 解码:按 ZWJ 分割得到每个字符,内部分别解析比特
chars = embedded_str.split(‘’)
hint = '’
for seg in chars:
bits = ‘’.join(‘1’ if c == ‘’ else ‘0’ if c == ‘’ else ’’ for c in seg)
if len(bits) >= 8:
for j in range(0, len(bits) - 7, 8):
hint += chr(int(bits[j:j+8], 2))
print(hint) # 输出映射提示
解码得到提示字符串,指示尾部数据块的 2-bit 映射排列。85 个嵌入字符对应一个 8~10 字节的短提示,实质上是一个排列编号或映射描述。
Step 3: 解码尾部数据块
有了嵌入字符给出的映射方案后,将尾部 2507 个零宽字符按正确的 2-bit 排列解码。
2592 个零宽字符整体作为数据流,每个字符映射为 2 位二进制,每 4 个字符组成 1 个字节(4 × 2 bits = 8 bits = 1 byte),共 648 字节。
payload_str = ‘’.join(payload)
根据提示确定映射(此处以 StegCloak 标准映射的变种为例)
提示 “los números no lo son” 表明不是常规 ZWNJ=0,ZWSP=1 的简单映射
实际映射由嵌入段解码结果给出
mapping = {
‘’: ‘01’, # ZWSP
‘’: ‘10’, # ZWNJ
‘’: ‘00’, # ZWJ
‘’: ‘11’, # ZWNBS
}
bits = ‘’.join(mapping[c] for c in payload_str)
data = bytes(int(bits[i:i+8], 2) for i in range(0, len(bits) - 7, 8))
print(data.decode())
评论