ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

不明字符串排查指南:从编码识别到随机性检验

不明字符串排查指南:从编码识别到随机性检验 1. 起因朋友只丢给我一串字符其余全是空白那天下午一个做安全的朋友在聊天框里发来一串东西IAALKAKIAALKAEIAALEAENAALEAK然后跟了一句帮我看看这串是什么客户给的什么都没解释。没有截图没有文件没有关键词没有摘要连小写都没带一个。我盯着这串 28 个字母看了十秒钟第一反应是这怕不是谁在键盘上滚了一巴掌。但看着像乱码和它真的是乱码是两回事。干过 CTF、做过文件分析、处理过各种诡异数据的人都知道越是没有上下文的东西越要走正规的分析流程。你猜对了是运气你分析对了才是本事。这篇文章就是我当时完整排查过程的记录包括每一步为什么这么试、试到什么结果、以及最后我的判断。如果你也经常会遇到只给一串字符别的什么都不说这种需求这篇可以作为一份现成的排查手册。先说明我的结论这串字符经过我从编码层、经典密码、随机性检验三轮排查之后最合理的解释是——它大概率不是一个能被常规手段解出语义的密文更像是在一个受限字符集里生成的随机串或者某个缺少上下文的私有约定。但中间那些排查过程比结论值钱得多而且有几个细节当时真的让我多看了两眼。顺便说一句做这种无上下文字符串分析最忌讳的就是拿起来就猜。正确的心态是把字符串当成一个没有病历的病人先做体检再问病史最后才考虑下诊断书。我下面的所有操作都是按照这个顺序展开的。2. 拿到不明数据的基础体检先别急着解密遇到不明字符串最忌讳的就是直接拖进某个工具里瞎点。我的习惯是先做一套标准化体检15 分钟能出结论。这套体检包括四件事长度、字符集、频率分布、重复片段。这四样东西决定了后面该往哪个方向走。2.1 长度和字符集第一层身份信息先把字符串摆出来按位置编号位置12345678910111213141516171819202122232425262728字符IAALKAKIAALKAEIAALEAENAALEAK长度 28 个字符全大写没有数字没有符号。这本身就是一条信息它可以排除掉一大类东西。比如 URL 编码、Base16 这类通常会有特定格式再比如常见的哈希值MD5 是 32 位十六进制SHA1 是 40 位长度和字符集都对不上。28 位这个长度如果在密码学场景里更像是一个被加密后的短消息、一个编码后的值或者干脆就是一个随机的口令片段。更关键的是字符集整个字符串只出现了 6 个不同的字母——A、E、I、K、L、N。我把这个叫做独有字符数。在大写字母里只挑出 6 个来用这个特征本身就很不寻常。后面你会看到光是这一条就足以推翻很多自以为是的猜测。2.2 频率统计和香农熵用数据说话我用一段简单的 Python 把频率分布拉出来from collections import Counter import math s IAALKAKIAALKAEIAALEAENAALEAK n len(s) cnt Counter(s) print(长度:, n) print(独有字符数:, len(cnt)) for ch, c in cnt.most_common(): p c / n print(f{ch}: {c} 次, 频率 {p:.2%}) entropy -sum(c / n * math.log2(c / n) for c in cnt.values()) print(f经验熵: {entropy:.3f} bit/字符) print(f6字符集的理论最大熵: {math.log2(len(cnt)):.3f} bit/字符)结果如下字符出现次数频率A1242.86%E414.29%K414.29%L414.29%I310.71%N13.57%A 一个字母就占了将近 43%。如果这是一段英文密文这个 A 的出现频率高得离谱——英文里最常见的字母 E 也不过 12.7% 左右。一个 28 字符的文本里某个字母占 43%要么明文本身不是正常英文要么加密方式不是简单的单表替换。同时我算了一下经验熵大约是 2.24 bit/字符。如果这 28 个字符是在 6 个字符的集合里均匀随机抽取的理论最大熵应该是 log2(6)≈2.585 bit/字符。现在实测熵低于最大值说明这个分布是有偏的A 明显偏多。这个偏差可能是随机波动但也可能是某种生成规律留下的痕迹。2.3 重复片段扫描让我停下来多看两眼的线索做完频率分析之后我用下面的脚本扫描固定长度的重复子串s IAALKAKIAALKAEIAALEAENAALEAK for size in range(3, 7): seen set() for i in range(len(s) - size 1): sub s[i:i size] if s.count(sub) 1 and sub not in seen: seen.add(sub) print(f长度{size}: {sub} 出现 {s.count(sub)} 次)扫描结果里有几个值得注意的重复片段长度为 6 的 IAALKA 出现了 2 次分别在位置 1 和位置 8长度为 5 的 AALEA 出现了 2 次分别在位置 16 和位置 23长度为 3 的 LKA、AAL 之类就更多了。AAL 这种三字母组合在全文里反复出现LEA 也出现了 2 次整个字符串的末尾正好是 LEAK 这四个字母。这里要小心人脑很容易在随机序列里看出模式LEAK 更像是一个巧合和我自己的脑补毕竟它前面并不是明显的英文结构。但 IAALKA 这个六连字符完整地出现两次而且相隔 7 个位置这个就不太像偶发了。如果这串字符是某个加密算法的输出重复片段可能意味着密钥周期和明文重复的某种耦合如果它是随机生成的六连字符重复的概率会非常低。这个矛盾点我放到后面专门分析。2.4 基础体检阶段就该放弃的念头做完上述四步我已经能明确放弃两个想法第一它不是常见的哈希摘要也不是十六进制第二它不是普通的 URL 编码或带格式的序列化文本。接下来要做的是把编码层和密码层分开排查一层一层排除。3. 编码层验证把常见的包装先全部试一遍很多时候一段看似无意义的字符串只是被某个编码包了一层。解码是先于解密的顺序不能反。我用一张表把所有候选编码过了一遍候选编码判断依据验证结果Base64字符集应为 A-Z a-z 0-9 /长度28不是4的倍数无填充解码后不可读排除Base32字符集为 A-Z 2-7本串全合法补位后解码为不可读字节排除Hex只能 0-9 A-F出现了 I/K/L/N直接排除URL 编码应含 % 和十六进制无 %直接排除Base58不含 0/O/I/l含 I 和 L排除ASCII85字符集为 ASCII 33-117本串虽合法但输出过于集中解码后无语义排除3.1 Base64 为什么第一个被排除Base64 是最常见的包装但它有几个特征字符集是 A-Z、a-z、0-9、、/而且标准形式需要补 填充长度通常是 4 的倍数。我这串字符串虽然全是字母理论上可以塞进 Base64 的字符集但 28 这个长度、完全没有填充、解码后根本不是可读文本这三条加在一起基本可以宣判排除。3.2 Base32 值得认真试一次Base32 的字符集是大写 A-Z 加上数字 2-7所以本串里的 A、E、I、K、L、N 全部合法。初看居然有点像 Base32 编码。我用 Python 试了一次import base64 s IAALKAKIAALKAEIAALEAENAALEAK padding * ((8 - len(s) % 8) % 8) try: raw base64.b32decode(s padding) print(raw) except Exception as e: print(Base32 解码失败:, e)结果是能解码出一段字节流但这段字节流既不是 UTF-8 文本也不是 GBK 文本往外打印全是不可打印的乱码。再加上一个重要的细节Base32 编码文本通常长度会是 8 的倍数28 不是这意味着要么它是缺了填充的非标准形式要么它压根不是 Base32。我后来又尝试了不补填充、按 28 直接手工拆分组结果同样不可读。到这一步Base32 这条路也算断了。3.3 其他包装的快速排除法和今天我会用的工具剩下的基本都是秒杀Hex 字符集只允许 0-9 和 A-F这里冒出了 I、K、L、N直接出局URL 编码必须出现 %Base58 为了可读性会避开 0/O/I/l可这里偏偏有 I 和 L跟 Base58 的设计理念对着干。在编码识别这件事上我日常最常用的不是某个本地工具而是 CyberChef 里的 Magic 操作。这段字符串丢进 CyberChef它会自动去识别 Base64、Base32、Hex、URL 解码等常见编码结果也是什么都没识别出来。dCode 的 Cipher Identifier 我也试了返回了一大堆候选密码算法但都是可能没有一个是确定。工具给不出确定性答案就说明形式化特征不够明显。4. 经典密码学套路逐个试然后逐个排除排除了编码层下一步进入密码学层。这里要讲一个容易被新手忽略的原则密码学是一个双向穷举的游戏除了要有一个密文你还需要一个或者多个假设——假设它是什么密码、假设密钥是什么、假设明文是什么语言。如果这些假设全是零那就不是解密是算命。4.1 凯撒位移和 ROT13从字符集就已经出局凯撒位移是最容易想到的我写了个脚本把所有 26 个位移跑了一遍s IAALKAKIAALKAEIAALEAENAALEAK for shift in range(26): t .join(chr((ord(c) - 65 shift) % 26 65) for c in s) if shift in (0, 13): print(fshift {shift}: {t})跑完没有任何一个位移能拼出英文单词。其实更本质的判断是凯撒密码是 26 个字母上的双射如果密文只用了 6 个字母那么明文也一定只用了 6 个字母。也就是说不管怎么位移出来的都还是 6 种字母来回组合。正常的英文文本不可能出现这种情况所以凯撒这条线根本不用等脚本跑完从字符集层面就已经出局了。ROT13 同理我试了结果是 NN YX XN VN... 一串同样没有意义的字母。4.2 频率分析和单表替换密码的困境频率分析是破解单表替换密码的经典手段。比如把英文中最高频的字母 E 映射到密文中最高频的字母再通过单词模式去反推。但在这个例子上这套手段失效了原因有两条。第一密文里 A 占了 42.86%而英文最高频字母 E 也只有 12.7%差了三个量级。如果这是单表替换说明明文的某个字母占了 43%这不像自然语言。第二单表替换要求密文字符集和明文字符集规模对等正常英文明文应该映射出 26 个字母中的大多数而不是只有 6 个。就算 28 个字符是很短的样本全篇只有一个低频字母 N 出现 1 次、其他全是高重复字母这个模式也无法用英文 单表替换解释。我还顺手跑了一下重合指数Index of Coincidence算出来高达 0.23 左右。英文的 IC 通常在 0.066 附近完全随机文本在 0.038 左右。0.23 这个数高得吓人。但注意28 个字符的样本量太小IC 会被一个高频字母严重拉高所以这个数字只能作为分布极度不均匀的佐证不能当作这是某种复杂密码的证据。4.3 多表替换维吉尼亚密码的问题出在样本量上既然单表替换解释不了自然会想到维吉尼亚密码这类多表替换。维吉尼亚的特点是同样的明文片段如果在密文里因为密钥周期而重复可以通过重复片段之间的距离推断密钥长度这就是卡西斯基检验。理论上我手里的 IAALKA 重复出现两次相距 7 个位置可以猜测密钥长度可能是 7 的因子。但是这里有个致命的局限全篇只有 28 个字符只有一个可用的重复片段统计上完全不够。卡西斯基检验需要大量重复片段才能给出可靠的密钥周期估计一个片段说明不了问题。硬猜密钥长度等于是在 26^7 个可能密钥里盲猜这是不现实的。我还试了用暴力方式遍历 1 到 7 的密钥长度再把每列拉出来做频率分析结果每列的长度只有 4 个字符连最基本的频率形状都看不出来。4.4 玩具式的自创编码把 6 个字母理解为 6 进制数字字符集只有 6 个字母这个特征太扎眼了。我产生了一个念头会不会有人把 A、E、I、K、L、N 当作 0 到 5 六个数字然后按某种进制去编码这种思路在 CTF 里偶尔会出现属于玩具式编码。把字母映射成数字A0E1I2K3L4N5。然后整个字符串变成了一串 0-5 的数字序列我尝试了两种分组方式两位一组按 6 进制转十进制再按 0A、1B 映射成字母三位一组按 6 进制转成 0-215 的数值再看是否落在 ASCII 可打印区间。两种尝试都没有产出可读的内容。第一种方式得到的前几个值是 12、4、18、20、0映射成字母是 M E S U A后面出现了一个超出 25 的数值直接断了第二种方式解出来的字节同样没有语义。这说明进制编码这个方向也只是碰巧字符集是 6 个字母并不构成真正的解码路径。我个人判断如果作者刻意设计了 6 进制编码他会在结果上留下更规整的分组特征而当前字符串的分组特征非常混乱。4.5 一个非常想试但必须忍住的想法flag 格式猜测做安全相关的东西太久看到不明字符串就会本能地想这会不会是 CTF 的 flag。我们常见的 flag 是 flag{...} 这种结构或者某种比赛的特定前缀。这串字符没有花括号、没有小写、没有数字和 flag 的形态差距很远。而且在缺少任何上下文的情况下强行套 flag 格式只会陷入确认偏误。你可以猜但不能把猜测当成结论。5. 键盘输入、随机性检验和它可能什么都不是的论据排查完经典密码之后我退了一步问了自己一个更基本的问题这串字符有没有可能根本不是什么高深的密文只是某种随机过程的产物这个假设反而更符合直觉。但觉得像乱码不能替代证明它随机所以我做了一组对比分析。5.1 会不会是键盘乱按出来的第一个假设是键盘瞎按。我观察了这 6 个字母在 QWERTY 键盘上的位置I 在上排右侧A 在中间偏左L 和 K 在右手中排E 在上排偏左N 在下排中间。这几个键在键盘上几乎没有形成任何手势路径不是那种手指滑过键盘自然形成的相邻字符序列。我还试过一种常见的情况打字时手指整体错位一格比如原本想打一串英文结果手放偏了。如果是这样密文字符集应该和那串英文按错位后的字符集保持一致。可问题是错位后的字符集一般是 20 个以上的不同字符而这串只有 6 个不像错位产生的。更合理的解释是有人在一个 6 键的字符集合里反复敲击或者某个程序只从这 6 个字符里随机取。还要补充一个细节全串没有小写、没有空格、没有标点这不像真人打字更像程序生成。5.2 用概率论直接戳破随机的伪装我算了一笔账。如果这 28 个字符是在 26 个大写字母里完全均匀随机抽取的那么所有字符恰好都落在某一个包含 6 个字母的子集里的概率是多少大致的上界估算是C(26,6) × (6/26)^28 ≈ 9.4 × 10^-14这个数字是什么概念比连续中两次彩票头奖还要低好几个数量级。所以在 26 个字母里均匀随机生成这个假设在这个证据面前可以直接否定。唯一合理的随机解释是生成的时候就已经限定在 6 个字符的小集合里了。换句话说这串字符串的低多样性本身就是最核心的特征它在告诉我们别往复杂密码的方向想了它的信息量就只有这么大。5.3 最短样本的信息论困境28 个字符按照前面的经验熵 2.24 bit/字符来算总信息量大约 62 bit。这个量级能表达的内容其实非常少。而在密码学里有一个最基本的现实如果没有密钥、没有上下文任何一段短密文都可以被解释成任何长度的任何明文。这就是著名的一次一密不可判定问题的通俗版你看到 28 个字符理论上你可以构造出 28 个字符的英文句子、中文拼音、日期坐标、坐标轴编码……随便什么只要你肯付出想象力和一个匹配的密钥生成规则。所以做这类分析最重要的是知道什么时候该收手证据不足的时候不下断言。6. 我的最终判断和沉淀下来的排查工具清单综合编码层、密码学层、随机性检验三层排查我最终的结论可以概括成两句话第一用常见的编码和经典密码学工具无法把这串字符解出任何可读语义第二它最可能的身份是一个受限字符集上的随机生成结果或者是某个只有发件人才知道规则的私有编码。在没有更多样本、没有密钥规则、没有上下文的前提下我无法、也不应该强行给出它一定是 XXX的结论。6.1 为什么我敢下这个判断敢下判断不是因为我识破了它的真面目而是因为所有常规路径都被证据堵死了。字符集只有 6 个字母这件事直接废掉了凯撒、单表替换、Base58 等一堆算法重复片段太少废掉了卡西斯基检验这条路Base32 解码虽然能解出字节流但解出来的东西没有任何文本特征而随机性检验又证明它不可能是 26 个字母空间里的均匀随机。所有线索都指向同一个方向这是一串在极小字符集里生成的、没有明显结构的字符串。6.2 沉淀下来的不明字符串排查工具包这次排查之后我把这套流程固化成了自己的工具包遇到类似问题直接按顺序走第一步基础画像长度、字符集、独有字符数、频率表、香农熵。用 Python 的 collections.Counter 加几行代码就能完成。第二步编码层识别扔进 CyberChef依次手测 Base64、Base32、Hex、URL、Base58、ASCII85。强烈建议用 CyberChef 的 Magic 做一次自动识别。第三步古典密码试探Caesar、ROT13、单表替换、维吉尼亚、仿射。dCode 的 Cipher Identifier 能给出候选清单但最终的判断要靠你自己结合字符集特征。第四步随机性检验算熵、算 IC、算独有字符的期望值判断它像不像某个字符集上的均匀随机。第五步也是最容易被忽略的一步回头找上下文。问清楚这串字符来自哪个文件、哪个系统、谁生成的、有没有时间戳、有没有伴随的其他样本。很多时候答案根本不在字符串里而在它周围的环境里。6.3 给同样爱分析字符串的朋友一点忠告我一向的原则是分析可以脑洞大开结论必须证据确凿。一个成熟的排查者不该因为看起来像就去编造一个解释。这串 IAALKAKIAALKAEIAALEAENAALEAK 也许以后某天会带着更多上下文重新出现在我面前到时候它可能会变成一句清晰的话也可能被证明就是一串随机数据。但在那之前我不会假装自己解开了它。如果哪天你也接到一个只有字符串、没有任何说明的任务希望这套流程能帮你少走弯路。至少下次再有人甩给你一串天书你可以先理直气壮地递给他一张频率统计表而不是对着屏幕干瞪眼。
返回列表