# `*.ile` 文件混淆算法详解 ## 1. 文档目标与范围 本文独立说明 `Encrypt` 生成混淆文件的完整过程,包括: - 文件由哪些区域组成; - 38 字节元数据区如何构造; - 64 字节混淆头如何生成; - 设置密码与不设置密码时,混淆头有什么区别; - 官方读取逻辑如何恢复元数据、判断密码并读取正文; - 正文如何经过固定密钥表和前一密文字节进行链式 XOR; - 如何用一段清晰的 C 代码恢复元数据和正文。 本文把该方案称为“混淆”,因为它使用固定密钥和可逆 XOR,不具备现代密码学加密算法应有的安全性。 --- ## 2. 基础概念 ### 2.1 bit、byte 和十六进制 计算机中的最小信息单位是 bit(位),一个 bit 只能取 `0` 或 `1`。 在当前 x86-64 平台上: ```text 1 byte = 8 bit ``` 一个字节可以表示 `0~255`。使用十六进制表示时,范围是: ```text 0x00 ~ 0xFF ``` 一个字节本身只是数字。只有按照某种字符编码解释时,它才可能表示字符。例如 ASCII/UTF-8 中: | 内容 | 字节数 | 十六进制字节 | | ---------------- | -----: | -------------- | | `A` | 1 | `41` | | `a` | 1 | `61` | | `0` | 1 | `30` | | 空格 | 1 | `20` | | 换行`\n` | 1 | `0A` | | 字符串结束符`\0` | 1 | `00` | | `中` | 3 | `E4 B8 AD` | | 常见 emoji | 通常 4 | 取决于具体字符 | C 字符串以 `\0` 结束。因此字符串 `"ABC"` 在内存中占 4 字节: ```text 41 42 43 00 A B C \0 ``` 本文中的“密码最多 15 字节”是字节限制,不是字符数量限制。15 个 ASCII 字符正好是 15 字节;常见中文字符在 UTF-8 中通常每个占 3 字节,因此 15 字节通常只能完整容纳 5 个中文字符。 ### 2.2 文件偏移 文件偏移表示一个字节距离文件开头有多远,从 `0` 开始计数: ```text 文件顺序:第1字节 第2字节 第3字节 ... 第65字节 第66字节 文件偏移: 0 1 2 ... 64 65 ``` 因此: - 偏移 `0` 是文件第 1 字节; - 偏移 `64` 是文件第 65 字节; - 偏移 `65` 是文件第 66 字节。 C 数组下标采用相同规则: ```c data[0] /* 文件第 1 字节 */ data[64] /* 文件第 65 字节 */ data[65] /* 文件第 66 字节 */ ``` 十六进制 `0x40` 等于十进制 `64`,`0x41` 等于十进制 `65`。 ### 2.3 XOR 异或 C 语言中的 `^` 是按位 XOR(异或): ```text 0 ^ 0 = 0 0 ^ 1 = 1 1 ^ 0 = 1 1 ^ 1 = 0 ``` XOR 有三个重要性质: ```text A ^ A = 0 A ^ 0 = A A ^ B ^ B = A ``` 因此,如果: ```c cipher = plain ^ mask; ``` 再次 XOR 相同的 `mask` 就能恢复: ```c plain = cipher ^ mask; ``` --- ## 3. 文件整体结构 生成的文件分为三个区域: | 文件偏移 | 长度 | 内容 | 作用 | | -------- | ------: | -------- | -------------------------------------- | | `0` | 1 字节 | `0x02` | 加密/混淆文件标志 | | `1~64` | 64 字节 | 混淆头 | 隐藏密码和可选参数,并提供正文初始状态 | | `65~EOF` | 可变 | 正文密文 | 规范化源码及包装文本的链式 XOR 结果 | 图示如下: ```text +-----------+--------------------------------------+-------------------------+ | 偏移 0 | 偏移 1~64 | 偏移 65~文件结束 | +-----------+--------------------------------------+-------------------------+ | 标志 0x02 | 64 字节混淆头 | 正文密文 | | | 38 个元数据槽 + 26 个随机干扰槽 | 从 key[126] 开始循环 | +-----------+--------------------------------------+-------------------------+ ``` 偏移 `64` 是混淆头最后一个字节。它不是正文的明文,但会直接作为正文混淆的初始“前一个密文字节”。 --- ## 4. 总体生成流程 ```mermaid flowchart TD A["接收源文件、目标文件、可选密码和可选整数参数"] --> B{"源文件名和目标文件名相同?"} B -- "是" --> Z["报错并停止"] B -- "否" --> C["调用 ilCompress 解析并规范化源文件"] C --> D["目标文件写入标志 0x02"] D --> E["使用当前时间初始化随机数"] E --> F["构造 38 字节 metadata"] F --> G["将 metadata 分散到 64 字节混淆头"] G --> H["保存偏移 64 作为正文 previous_cipher"] H --> I["生成开头时间包装文本"] I --> J["链式 XOR 写入规范化源码"] J --> K["生成并链式 XOR 写入结尾时间包装文本"] K --> L["关闭文件并删除临时文件"] ``` --- ## 5. 输入参数和固定初始值 ### 5.1 函数参数 加密逻辑可以整理为以下函数形式: ```c ilEncrypt( char *source_file, const char *output_file, const char *password, int option ); ``` 参数含义: | 参数 | 含义 | | ------------- | ---------------------------------------- | | `source_file` | 待处理源文件路径 | | `output_file` | 输出文件路径 | | `password` | 可选密码;未提供时包装层传入空字符串 | | `option` | 可选整数;转换为十进制文本后放入元数据区 | ### 5.2 固定初始值 | 名称 | 固定值 | 作用 | | ---------------- | -----------: | ---------------------------------- | | 文件标志 | `0x02` | 表示这是混淆文件 | | 元数据长度 | 38 字节 | 保存密码、参数及随机填充 | | 混淆头长度 | 64 字节 | 隐藏 38 字节元数据 | | 头部初始 XOR 值 | `5` | 生成第一个混淆头字节 | | 第一段选择状态 | `0x25ACCB75` | 控制前半段元数据位置 | | 第二段选择状态 | `0x577CCEB7` | 在指定位置重置后控制后半段 | | 正文密钥长度 | 127 字节 | 固定循环密钥表 | | 正文初始密钥索引 | `126` | 第一个正文字符使用最后一个密钥字节 | ### 5.3 随机数 写入 `0x02` 后,程序执行相当于: ```c srand((unsigned int)time(NULL)); ``` 随后使用 `random()`,并只保留结果的最低 8 位: ```c uint8_t random_byte = (uint8_t)random(); ``` 随机数共用于两处: 1. 先调用 38 次,初始化完整的 `metadata[38]`; 2. 生成 64 字节混淆头时,26 个非元数据位置各调用一次。 因此完整头部生成阶段总共调用 64 次 `random()`。密码和参数会覆盖 metadata 中的部分随机字节,但不会改变随机调用次数。 相同源文件和相同密码在不同时间运行,通常也会生成不同的混淆头。读取程序不需要重新生成这些随机数。 --- ## 6. 加密前的正文处理 ### 6.1 `ilCompress` 的实际作用 程序首先把源文件处理到临时文件。这里的 `ilCompress` 不是 ZIP、gzip 或其他通用压缩算法,而是针对 SKILL/IL 文本的解析和重新打印: 1. 使用内部解析器读取输入; 2. 将解析出的对象重新打印; 3. 对象之间插入空格; 4. 每组对象后写入换行; 5. 跳过解析器认定为空或无须输出的内容。 这意味着输出正文通常仍是文本,但原始缩进、换行布局和部分注释可能丢失。因此该过程并非严格可逆的字节压缩。 ### 6.2 实际进入正文 XOR 的明文 程序不会只混淆规范化源码,还会在前后加入两段文本: ```text /* <开始时间计算值> */\n <规范化后的源码> ; <结束时间计算值>\n ``` 开头文本使用: ```c sprintf(buffer, "/* %d */\n", value); ``` 其中 `value` 来自开始时间的部分位运算和乘法。结尾文本使用: ```c sprintf(buffer, "; %.16e\n", time_value); ``` 其中 `time_value` 是开始时间和结束时间转换成浮点数后的乘积。这些时间字段不参与密码验证,也不生成正文密钥,主要起包装和格式标记作用。 --- ## 7. 38 字节 metadata 的生成 ### 7.1 先全部随机初始化 逻辑等价于: ```c uint8_t metadata[38]; for (int i = 0; i < 38; ++i) metadata[i] = (uint8_t)random(); ``` 这一步完成后,所有位置都有随机值。随后固定字段覆盖其中一部分。 ### 7.2 完整布局 ```text 索引 0 1................15 16 17............28 29 30 31......37 内容 固定0 密码区域 0 option 文本 0 0 随机填充 ``` | metadata 索引 | 长度 | 内容 | | ------------- | ---: | ----------------------------------------- | | `[0]` | 1 | 固定写成`0` | | `[1..15]` | 15 | 密码,最多保存 15 字节 | | `[16]` | 1 | 固定写成`0`,确保密码结束 | | `[17..28]` | 12 | `option` 的十进制字符串及未覆盖的随机填充 | | `[29]` | 1 | 固定写成`0` | | `[30]` | 1 | 固定写成`0` | | `[31..37]` | 7 | 保留初始随机填充 | 对应的写入逻辑为: ```c metadata[0] = 0; strncpy((char *)&metadata[1], password, 15); metadata[16] = 0; if (option != 0) sprintf((char *)&metadata[17], "%d", option); else metadata[17] = 0; metadata[29] = 0; metadata[30] = 0; ``` `sprintf` 会在十进制数字后写入 `\0`。数字字符串结束后的未覆盖位置仍可能保留最初的随机字节。 ### 7.3 不设置密码 未提供密码时,包装层传入空字符串: ```c password = ""; ``` 然后执行: ```c strncpy((char *)&metadata[1], "", 15); ``` 由于源字符串长度为 0,`strncpy` 会使用 `0` 填满整个 15 字节区域: ```text metadata[1] = 00 metadata[2] = 00 ... metadata[15] = 00 metadata[16] = 00 ``` 无密码时最关键的判定值是: ```c metadata[1] == 0; ``` 混淆头仍然是完整的 64 字节,随机位置仍然存在,正文仍然使用相同的 127 字节固定密钥。无密码不会缩短或省略头部。 ### 7.4 设置短密码 以密码 `"abc"` 为例: ```text 'a' = 0x61 'b' = 0x62 'c' = 0x63 ``` `strncpy` 的结果为: ```text metadata[1] = 61 metadata[2] = 62 metadata[3] = 63 metadata[4] = 00 metadata[5] = 00 ... metadata[15] = 00 metadata[16] = 00 ``` 短密码后面会补零,因此官方的 15 字节比较仍能判断完整字符串是否一致。例如保存的是 `"abc"` 时,输入 `"abcXYZ"` 不会通过,因为第 4 个比较字节分别是 `0x00` 和 `'X'`。 ### 7.5 设置长密码 如果密码超过 15 字节: ```c strncpy((char *)&metadata[1], password, 15); metadata[16] = 0; ``` 只会保存前 15 字节,后续字节从未写入文件。例如: ```text 输入密码:123456789012345ABCDEFG 实际保存:123456789012345 ``` 官方读取时也只比较前 15 字节。因此两个输入只要前 15 字节相同,就会被认为密码相同。 如果 UTF-8 多字节字符刚好跨越第 15 字节边界,保存内容可能截断在一个字符的中间。底层比较仍按字节进行,不理解字符边界。 ### 7.6 `option` 字段 当 `option = 123` 时: ```c sprintf((char *)&metadata[17], "%d", 123); ``` 写入的是文本: ```text metadata[17] = 31 // '1' metadata[18] = 32 // '2' metadata[19] = 33 // '3' metadata[20] = 00 // 字符串结束 ``` 当 `option == 0` 时,程序只设置: ```c metadata[17] = 0; ``` 当前库中的本地密码读取路径会恢复全部 38 字节 metadata,但只使用密码区域;没有继续使用这个整数文本字段。它可能用于兼容其他版本或外部读取者。 --- ## 8. 64 字节混淆头的生成 ### 8.1 三个初始状态 生成头部前,程序设置: ```c uint8_t previous = 5; uint32_t state = 0x25ACCB75; int metadata_index = 0; ``` 含义如下: - `previous`:上一轮生成的头部字节;第一轮没有上一字节,因此使用固定值 `5`; - `state`:只负责决定当前槽使用 metadata 还是随机字节; - `metadata_index`:下一个待隐藏的 metadata 索引。 `state` 不由密码生成,也不是正文密钥。 ### 8.2 64 轮生成逻辑 循环计数从 `2` 到 `65`,一共执行 64 次。循环计数与文件偏移的关系是: ```c file_offset = position - 1; ``` 所以 `position=2` 写入文件偏移 1,`position=65` 写入偏移 64。 清理后的逻辑如下: ```c for (int position = 2; position <= 65; ++position) { uint8_t selected; if ((state & 0x80000000U) != 0 && metadata_index < 38) selected = metadata[metadata_index++]; else selected = (uint8_t)random(); previous ^= selected; fputc(previous, output); if (position == 33) state = 0x577CCEB7U; else state <<= 1; } ``` 使用 `uint32_t` 是为了明确表达 32 位左移和最高位检查,避免有符号整数溢出的语言细节。 ### 8.3 选择 metadata 还是随机数 条件: ```c (state & 0x80000000U) != 0 ``` 检查 `state` 的最高位: ```text 最高位为 1:选择下一个 metadata 字节 最高位为 0:调用 random() 产生随机干扰字节 ``` 开始几轮的状态如下: | position | 文件偏移 | state | 最高位 | 数据来源 | | -------: | -------: | -----------: | -----: | ------------- | | 2 | 1 | `0x25ACCB75` | 0 | 随机字节 | | 3 | 2 | `0x4B5996EA` | 0 | 随机字节 | | 4 | 3 | `0x96B32DD4` | 1 | `metadata[0]` | 处理完 `position=33` 后,状态被固定重置为: ```c state = 0x577CCEB7; ``` 这个固定模式在 64 轮中恰好选择 38 次 metadata,因此 `metadata[0..37]` 全部被隐藏一次;另外 26 轮使用新的随机字节。 ### 8.4 链式生成公式 每一轮先选择 `selected`,再执行: ```c current = previous ^ selected; ``` 写入后: ```c previous = current; ``` 设 64 个被选中的值是 `X0~X63`,64 个头部输出是 `H0~H63`,则: ```text H0 = 5 ^ X0 H1 = H0 ^ X1 H2 = H1 ^ X2 ... H63 = H62 ^ X63 ``` 展开最后一个头部字节: ```text H63 = 5 ^ X0 ^ X1 ^ X2 ^ ... ^ X63 ``` 在文件中: ```text H0 位于 file[1] H63 位于 file[64] ``` ### 8.5 metadata 与文件偏移的分组映射 | metadata 范围 | 含义 | 对应文件偏移 | | ------------- | ------------- | --------------------------------------------------------- | | `[0]` | 固定 0 | `3` | | `[1..15]` | 密码 | `6, 8, 9, 11, 13, 14, 17, 18, 21, 23, 24, 26, 27, 28, 30` | | `[16]` | 密码结束符 | `32` | | `[17..28]` | `option` 区域 | `34, 36, 38, 39, 40, 42, 43, 44, 45, 46, 49, 50` | | `[29..30]` | 固定 0 | `53, 54` | | `[31..37]` | 随机填充 | `55, 57, 59, 60, 62, 63, 64` | 这里的“对应文件偏移”并不表示该位置直接等于 metadata。实际关系是: ```c file[offset] = file[offset - 1] ^ metadata[index]; ``` 因此恢复公式是: ```c metadata[index] = file[offset] ^ file[offset - 1]; ``` ### 8.6 纯随机位置 未列入映射表的 26 个偏移是随机干扰位置。在这些位置: ```c file[offset] = file[offset - 1] ^ random_byte; ``` 虽然也能通过相邻字节 XOR 算出 `random_byte`,但它没有字段含义,官方读取过程不会保存它。 ### 8.7 偏移 64 的形成 最后一轮为: ```text position = 65 file_offset = 64 state = 0x80000000 metadata_index = 37 ``` 最高位为 1,因此选择: ```c selected = metadata[37]; ``` `metadata[37]` 没有被密码或参数覆盖,是初始阶段留下的随机字节。因此: ```c file[64] = file[63] ^ metadata[37]; ``` 头部结束后,变量 `previous` 就等于 `file[64]`。程序把它直接作为正文的初始 `previous_cipher`。 ### 8.8 混淆头生成流程图 ```mermaid flowchart TD A["初始化 previous=5、state=0x25ACCB75、metadata_index=0"] --> B["position 从 2 开始"] B --> C{"state 最高位为 1 且 metadata 尚未取完?"} C -- "是" --> D["selected = metadata[metadata_index++]"] C -- "否" --> E["selected = random() 的低 8 位"] D --> F["previous = previous XOR selected"] E --> F F --> G["将 previous 写入文件"] G --> H{"position 等于 33?"} H -- "是" --> I["state = 0x577CCEB7"] H -- "否" --> J["state 左移 1 位"] I --> K{"position 等于 65?"} J --> K K -- "否" --> L["position 加 1"] L --> C K -- "是" --> M["file[64] 成为正文 previous_cipher"] ``` --- ## 9. 有密码与无密码的头部对比 ### 9.1 无密码示例 无密码时: ```text metadata[1..15] = 00 metadata[16] = 00 ``` 密码第一个字节对应文件偏移 6,因此: ```c file[6] = file[5] ^ metadata[1]; ``` 因为 `metadata[1] == 0`: ```c file[6] = file[5] ^ 0; file[6] = file[5]; ``` 密码区域其他位置也有相同特征:在准确的元数据槽上,当前头部字节等于前一个头部字节。随机槽不遵守这个特征,因此必须根据固定位置判断,不能简单搜索所有相等字节。 恢复时: ```c metadata[1] = file[6] ^ file[5]; ``` 结果为 `0`,官方逻辑据此认定没有密码。 ### 9.2 设置 `"abc"` 的示例 密码字节为: ```text metadata[1] = 0x61 metadata[2] = 0x62 metadata[3] = 0x63 metadata[4] = 0x00 ``` 对应关系: ```c file[6] = file[5] ^ 0x61; /* 'a' */ file[8] = file[7] ^ 0x62; /* 'b' */ file[9] = file[8] ^ 0x63; /* 'c' */ file[11] = file[10] ^ 0x00; /* 字符串结束 */ ``` 官方读取时: ```c metadata[1] = file[6] ^ file[5]; metadata[2] = file[8] ^ file[7]; metadata[3] = file[9] ^ file[8]; metadata[4] = file[11] ^ file[10]; ``` 恢复得到: ```text 61 62 63 00 a b c \0 ``` ### 9.3 两种情况的比较 | 项目 | 无密码 | 有密码 | | -------------------- | ----------- | ------------------------------ | | 文件标志 | `0x02` | `0x02` | | 混淆头长度 | 64 字节 | 64 字节 | | 密码区域 | 15 个`0x00` | 最多 15 个密码字节,短密码补零 | | `metadata[1]` | `0` | 通常非`0` | | 官方读取要求 | 直接继续 | 必须提供匹配密码 | | 正文固定 key | 相同 | 相同 | | 密码是否参与正文 key | 否 | 否 | --- ## 10. 官方程序如何读取混淆头 ### 10.1 检查标志 官方读取流程先读取文件偏移 0: ```c int marker = getc(file); ``` 如果: ```c marker == 0x02 ``` 则进入混淆头读取流程;否则把该字节放回输入流并按普通文件处理。 ### 10.2 恢复 metadata 官方 `ilReadHeader` 按顺序读取偏移 `1~64`,使用与生成端相同的 `state` 变化规律识别 38 个元数据槽。 在元数据槽上,恢复公式是: ```c metadata[index] = current_header ^ previous_header; ``` 因为生成端满足: ```c current_header = previous_header ^ metadata[index]; ``` 代入后: ```text previous_header ^ current_header = previous_header ^ previous_header ^ metadata[index] = 0 ^ metadata[index] = metadata[index] ``` 读取端不需要知道: - `srand` 使用的时间; - 38 字节初始随机区的原始随机值; - 26 个随机干扰槽使用了什么随机数; - 头部生成初始值 `5`。 它只需要知道固定的元数据位置,并读取相邻两个实际头部字节。 ### 10.3 等价的固定偏移恢复方式 如果不复现 `state`,也可以直接使用固定偏移表: ```c static const uint8_t metadata_offsets[38] = { 3, 6, 8, 9, 11, 13, 14, 17, 18, 21, 23, 24, 26, 27, 28, 30, 32, 34, 36, 38, 39, 40, 42, 43, 44, 45, 46, 49, 50, 53, 54, 55, 57, 59, 60, 62, 63, 64 }; for (int i = 0; i < 38; ++i) { uint8_t offset = metadata_offsets[i]; metadata[i] = file_data[offset] ^ file_data[offset - 1]; } ``` 这与官方状态机的结果相同。 ### 10.4 判断是否有密码 恢复后,官方程序检查密码区域第一个字节: ```c if (metadata[1] == 0) { /* 没有密码 */ } else { /* 文件要求密码 */ } ``` 这里没有单独的“密码存在”标志。空密码通过 `metadata[1] == 0` 表示。 ### 10.5 密码验证 如果 `metadata[1] != 0`: 1. 检查调用者是否提供了待读取密码; 2. 未提供时关闭文件并报告“需要密码”; 3. 已提供时执行最多 15 字节比较; 4. 不匹配时关闭文件并报告“密码错误”; 5. 匹配时允许继续读取正文。 比较逻辑相当于: ```c strncmp(input_password, (const char *)&metadata[1], 15); ``` 短密码依靠 `\0` 和后续补零进行精确比较;长度达到或超过 15 字节时,只比较前 15 字节。 ### 10.6 初始化正文读取状态 读取完偏移 64 后,官方程序保存两个状态: ```c previous_cipher = file[64]; key_index = 126; ``` 偏移 64 不需要还原成某个“正文明文”。读取端直接使用文件中保存的原始字节值。 ### 10.7 官方读取流程图 ```mermaid flowchart TD A["读取文件偏移 0"] --> B{"字节等于 0x02?"} B -- "否" --> C["按普通文件处理"] B -- "是" --> D["读取偏移 1~64"] D --> E["在固定槽位计算 current XOR previous"] E --> F["恢复 metadata[0..37]"] F --> G{"metadata[1] 等于 0?"} G -- "是" --> K["不要求密码"] G -- "否" --> H{"调用者提供了密码?"} H -- "否" --> I["报告需要密码并停止"] H -- "是" --> J{"前 15 字节匹配?"} J -- "否" --> L["报告密码错误并停止"] J -- "是" --> K K --> M["previous_cipher = file[64]"] M --> N["key_index = 126"] N --> O["开始还原正文"] ``` --- ## 11. 正文链式 XOR 混淆 ### 11.1 正文初始状态 头部完成后: ```c previous_cipher = file[64]; key_index = 126; ``` 正文第一个输出写入文件偏移 `65`。 ### 11.2 单字节生成公式 对于一个明文字节 `plain`: ```c current_cipher = plain ^ previous_cipher ^ body_key[key_index]; ``` 写出后更新: ```c previous_cipher = current_cipher; if (--key_index < 0) key_index = 126; ``` 密钥索引顺序为: ```text 126, 125, 124, ..., 2, 1, 0, 126, 125, ... ``` 每处理 127 字节循环一次。 ### 11.3 第一个正文字符 正文明文首先是开头包装字符串的 `/`: ```text '/' = 0x2F ``` 第一个正文密文字节为: ```c file[65] = 0x2F ^ file[64] ^ body_key[126]; ``` 固定表中: ```text body_key[126] = 0xF7 ``` 假设: ```text file[64] = 0x9A ``` 则: ```text file[65] = 0x2F ^ 0x9A ^ 0xF7 = 0x42 ``` 还原时: ```text 0x42 ^ 0x9A ^ 0xF7 = 0x2F = '/' ``` ### 11.4 后续正文字符 第二个正文字符使用第一个正文密文字节作为 `previous_cipher`: ```c file[66] = plain[1] ^ file[65] ^ body_key[125]; ``` 第三个字符: ```c file[67] = plain[2] ^ file[66] ^ body_key[124]; ``` 规律如下: | 正文相对索引 | 文件偏移 | 前一个密文字节 | 密钥索引 | | -----------: | -------: | -------------- | --------------: | | `0` | `65` | `file[64]` | `126` | | `1` | `66` | `file[65]` | `125` | | `2` | `67` | `file[66]` | `124` | | `j` | `65+j` | `file[64+j]` | `126-(j % 127)` | 通用生成公式: ```c file[65 + j] = plain[j] ^ file[64 + j] ^ body_key[126 - (j % 127)]; ``` ### 11.5 正文还原公式 由于 XOR 可以抵消,还原公式和生成公式形式相同: ```c plain[j] = file[65 + j] ^ file[64 + j] ^ body_key[126 - (j % 127)]; ``` 证明: ```text current_cipher ^ previous_cipher ^ key = plain ^ previous_cipher ^ key ^ previous_cipher ^ key = plain ^ (previous_cipher ^ previous_cipher) ^ (key ^ key) = plain ^ 0 ^ 0 = plain ``` ### 11.6 正确更新 previous 还原一字节后,必须保存当前的原始密文字节: ```c previous_cipher = current_cipher; ``` 不能写成: ```c previous_cipher = plain; /* 错误 */ ``` 因为下一字节的公式依赖前一个密文字节,而不是前一个明文字节。 ### 11.7 是否必须从头开始 自然实现从偏移 65 向后处理最简单。但任意正文位置都可以单独还原,因为当前密文字节和前一个密文字节都保存在文件中,密钥索引也能由偏移计算: ```c body_index = file_offset - 65; key_index = 126 - (body_index % 127); ``` 因此并不需要从文件结尾倒序处理,也不需要先求出偏移 64 对应的某个明文。 --- ## 12. 官方正文读取过程 官方单字节读取逻辑可以整理为: ```c int read_plain_byte(FILE *input) { int value = fgetc(input); if (value == EOF) return EOF; uint8_t current_cipher = (uint8_t)value; uint8_t plain = current_cipher ^ previous_cipher ^ body_key[key_index]; previous_cipher = current_cipher; key_index = (key_index == 0) ? 126 : key_index - 1; return plain; } ``` 官方端口对象还会保存 `previous_cipher` 和 `key_index`,这样在不同输入端口之间切换后,可以恢复每个端口自己的读取位置和链式状态。 密码验证只决定官方读取过程是否允许继续。密码没有参与上面的正文 XOR,也没有改变固定 `body_key`。 --- ## 13. 简洁的 C 语言还原示例 下面代码仅展示核心操作: 从偏移 65 开始还原完整正文流。 ```c unsigned char previous_cipher = file_data[64]; int key_index = 126; for (size_t offset = 65; offset < file_size; offset++) { unsigned char current_cipher = file_data[offset]; unsigned char plain = current_cipher ^ previous_cipher ^ key[key_index]; previous_cipher = current_cipher; if (--key_index < 0) key_index = 126; } ``` 仅用于算法研究,生产代码请各位自行编写。 --- ## 14. 完整小型演算 假设密码是 `"abc"`,并假设头部生成过程中某些随机字节已经确定。 ### 14.1 metadata ```text metadata[0] = 00 metadata[1] = 61 ('a') metadata[2] = 62 ('b') metadata[3] = 63 ('c') metadata[4] = 00 ... metadata[16] = 00 ``` ### 14.2 开始生成头部 用 `Q0, Q1, ...` 表示生成头部时额外取得的随机干扰字节: ```text file[1] = 0x05 ^ Q0 file[2] = file[1] ^ Q1 file[3] = file[2] ^ metadata[0] file[4] = file[3] ^ Q2 file[5] = file[4] ^ Q3 file[6] = file[5] ^ metadata[1] ``` 由于 `metadata[0] == 0`: ```text file[3] == file[2] ``` 密码第一个字节: ```text file[6] = file[5] ^ 0x61 ``` 读取时: ```text file[6] ^ file[5] = 0x61 = 'a' ``` 密码第二、第三字节分别从偏移 `8` 和 `9` 恢复。 ### 14.3 形成偏移 64 最后一轮: ```text file[64] = file[63] ^ metadata[37] ``` 假设: ```text file[63] = 0xA6 metadata[37] = 0x3C ``` 那么: ```text file[64] = 0xA6 ^ 0x3C = 0x9A ``` ### 14.4 生成第一个正文密文字节 正文第一个明文字符为 `/`: ```text plain[0] = 0x2F file[64] = 0x9A body_key[126]= 0xF7 ``` 生成: ```text file[65] = 0x2F ^ 0x9A ^ 0xF7 = 0x42 ``` 还原: ```text plain[0] = 0x42 ^ 0x9A ^ 0xF7 = 0x2F = '/' ``` 这说明混淆头和正文之间的连接点只有一个:偏移 `64` 的原始字节值。 --- ## 15. 算法特性和限制 ### 15.1 密码不是正文密钥 密码只存放在 metadata 中,并由官方读取流程检查。它没有参与 127 字节固定密钥表的生成,也没有参与正文 key 索引变化。 因此,从算法角度看,即使不知道密码,只要知道文件格式和固定密钥表,仍然可以恢复正文。密码只是一层官方读取路径的访问控制。 ### 15.2 密码最多有效 15 字节 - 短密码完整保存并补零; - 超过 15 字节的后缀不会写入文件; - 官方比较最多 15 字节; - UTF-8 字符可能在第 15 字节处被截断。 ### 15.3 随机数不提供强安全性 随机数主要用于: - 让相同输入在不同运行中得到不同头部; - 填充无意义槽位; - 改变偏移 64,进而改变整段正文密文。 但 metadata 的位置固定,相邻字节 XOR 可以直接恢复内容;读取者也不需要知道随机种子。 ### 15.4 正文是线性 XOR 固定 key、当前密文和前一个密文之间是线性关系。已知算法后,可以直接反向计算。它适合隐藏文本,不能替代 AES-GCM、ChaCha20-Poly1305 等现代认证加密算法。 ### 15.5 字节损坏的影响 如果一个正文密文字节损坏: - 对应位置的明文字节会错误; - 下一位置也会错误,因为它使用了损坏字节作为 `previous_cipher`; - 再后面通常恢复正常,因为后续使用文件中保存的下一个原始密文字节。 该格式没有认证标签,无法可靠判断文件是否被恶意修改。 --- ## 附录 A:完整 127 字节固定密钥 ```text 36 61 3E F1 69 3C 2E 8E 55 CF C2 86 9C 3B 94 22 64 41 0B 6B E5 AC A9 19 D3 D0 AA 9E 49 05 03 7F 67 42 72 50 FD A0 5E 53 EF 21 59 0C 5C EE AE C0 AF B9 AB 15 E6 D4 2E BA A6 59 59 6F 5E 5D EF 45 1F E1 95 9C 02 F4 70 72 95 C9 FD F2 B8 AC 33 E7 67 5F FD CD 34 2C 88 5A 85 62 CA 64 3F BA A9 DD 1C 40 7A 9D B4 6A 10 4A 35 0F BC 6D BB 70 56 A2 CF 54 70 04 00 79 5F 86 5B A9 EA 9A E3 14 F7 ``` 前 7 行各 16 字节,最后一行 15 字节,总计: ```text 7 * 16 + 15 = 127 字节 ``` --- ## 附录 B:metadata 索引与文件偏移完整表 | metadata | 字段 | 文件偏移 | 恢复公式 | | -------: | ----------------------- | -------: | --------------------- | | 0 | 固定 0 | 3 | `file[3] ^ file[2]` | | 1 | 密码字节 0 | 6 | `file[6] ^ file[5]` | | 2 | 密码字节 1 | 8 | `file[8] ^ file[7]` | | 3 | 密码字节 2 | 9 | `file[9] ^ file[8]` | | 4 | 密码字节 3 | 11 | `file[11] ^ file[10]` | | 5 | 密码字节 4 | 13 | `file[13] ^ file[12]` | | 6 | 密码字节 5 | 14 | `file[14] ^ file[13]` | | 7 | 密码字节 6 | 17 | `file[17] ^ file[16]` | | 8 | 密码字节 7 | 18 | `file[18] ^ file[17]` | | 9 | 密码字节 8 | 21 | `file[21] ^ file[20]` | | 10 | 密码字节 9 | 23 | `file[23] ^ file[22]` | | 11 | 密码字节 10 | 24 | `file[24] ^ file[23]` | | 12 | 密码字节 11 | 26 | `file[26] ^ file[25]` | | 13 | 密码字节 12 | 27 | `file[27] ^ file[26]` | | 14 | 密码字节 13 | 28 | `file[28] ^ file[27]` | | 15 | 密码字节 14 | 30 | `file[30] ^ file[29]` | | 16 | 密码结束符 | 32 | `file[32] ^ file[31]` | | 17 | option 字节 0 | 34 | `file[34] ^ file[33]` | | 18 | option 字节 1 | 36 | `file[36] ^ file[35]` | | 19 | option 字节 2 | 38 | `file[38] ^ file[37]` | | 20 | option 字节 3 | 39 | `file[39] ^ file[38]` | | 21 | option 字节 4 | 40 | `file[40] ^ file[39]` | | 22 | option 字节 5 | 42 | `file[42] ^ file[41]` | | 23 | option 字节 6 | 43 | `file[43] ^ file[42]` | | 24 | option 字节 7 | 44 | `file[44] ^ file[43]` | | 25 | option 字节 8 | 45 | `file[45] ^ file[44]` | | 26 | option 字节 9 | 46 | `file[46] ^ file[45]` | | 27 | option 字节 10 | 49 | `file[49] ^ file[48]` | | 28 | option 字节 11 | 50 | `file[50] ^ file[49]` | | 29 | 固定 0 | 53 | `file[53] ^ file[52]` | | 30 | 固定 0 | 54 | `file[54] ^ file[53]` | | 31 | 随机填充 | 55 | `file[55] ^ file[54]` | | 32 | 随机填充 | 57 | `file[57] ^ file[56]` | | 33 | 随机填充 | 59 | `file[59] ^ file[58]` | | 34 | 随机填充 | 60 | `file[60] ^ file[59]` | | 35 | 随机填充 | 62 | `file[62] ^ file[61]` | | 36 | 随机填充 | 63 | `file[63] ^ file[62]` | | 37 | 随机填充/正文初始链来源 | 64 | `file[64] ^ file[63]` | --- ## 附录 C:术语对照 | 术语 | 本文含义 | | ----------------- | --------------------------------------------------------- | | 文件标志 | 偏移 0 的固定字节`0x02` | | metadata | 被分散隐藏的 38 字节元数据 | | 混淆头 | 偏移`1~64` 的 64 字节输出 | | 元数据槽 | 固定选择 metadata 的 38 个头部位置 | | 随机槽 | 选择`random()` 低字节的 26 个头部位置 | | `previous` | 生成混淆头时的前一个头部输出 | | `previous_cipher` | 正文生成/读取时的前一个正文密文字节;第一轮使用`file[64]` | | `state` | 决定头部当前槽使用 metadata 还是随机字节的固定选择状态 | | `key_index` | 正文固定密钥表索引,从 126 递减并循环 | | 正文相对索引 | 从文件偏移 65 开始重新以 0 计数的位置 | Loading... # `*.ile` 文件混淆算法详解 ## 1. 文档目标与范围 本文独立说明 `Encrypt` 生成混淆文件的完整过程,包括: - 文件由哪些区域组成; - 38 字节元数据区如何构造; - 64 字节混淆头如何生成; - 设置密码与不设置密码时,混淆头有什么区别; - 官方读取逻辑如何恢复元数据、判断密码并读取正文; - 正文如何经过固定密钥表和前一密文字节进行链式 XOR; - 如何用一段清晰的 C 代码恢复元数据和正文。 本文把该方案称为“混淆”,因为它使用固定密钥和可逆 XOR,不具备现代密码学加密算法应有的安全性。 --- ## 2. 基础概念 ### 2.1 bit、byte 和十六进制 计算机中的最小信息单位是 bit(位),一个 bit 只能取 `0` 或 `1`。 在当前 x86-64 平台上: ```text 1 byte = 8 bit ``` 一个字节可以表示 `0~255`。使用十六进制表示时,范围是: ```text 0x00 ~ 0xFF ``` 一个字节本身只是数字。只有按照某种字符编码解释时,它才可能表示字符。例如 ASCII/UTF-8 中: | 内容 | 字节数 | 十六进制字节 | | ---------------- | -----: | -------------- | | `A` | 1 | `41` | | `a` | 1 | `61` | | `0` | 1 | `30` | | 空格 | 1 | `20` | | 换行`\n` | 1 | `0A` | | 字符串结束符`\0` | 1 | `00` | | `中` | 3 | `E4 B8 AD` | | 常见 emoji | 通常 4 | 取决于具体字符 | C 字符串以 `\0` 结束。因此字符串 `"ABC"` 在内存中占 4 字节: ```text 41 42 43 00 A B C \0 ``` 本文中的“密码最多 15 字节”是字节限制,不是字符数量限制。15 个 ASCII 字符正好是 15 字节;常见中文字符在 UTF-8 中通常每个占 3 字节,因此 15 字节通常只能完整容纳 5 个中文字符。 ### 2.2 文件偏移 文件偏移表示一个字节距离文件开头有多远,从 `0` 开始计数: ```text 文件顺序:第1字节 第2字节 第3字节 ... 第65字节 第66字节 文件偏移: 0 1 2 ... 64 65 ``` 因此: - 偏移 `0` 是文件第 1 字节; - 偏移 `64` 是文件第 65 字节; - 偏移 `65` 是文件第 66 字节。 C 数组下标采用相同规则: ```c data[0] /* 文件第 1 字节 */ data[64] /* 文件第 65 字节 */ data[65] /* 文件第 66 字节 */ ``` 十六进制 `0x40` 等于十进制 `64`,`0x41` 等于十进制 `65`。 ### 2.3 XOR 异或 C 语言中的 `^` 是按位 XOR(异或): ```text 0 ^ 0 = 0 0 ^ 1 = 1 1 ^ 0 = 1 1 ^ 1 = 0 ``` XOR 有三个重要性质: ```text A ^ A = 0 A ^ 0 = A A ^ B ^ B = A ``` 因此,如果: ```c cipher = plain ^ mask; ``` 再次 XOR 相同的 `mask` 就能恢复: ```c plain = cipher ^ mask; ``` --- ## 3. 文件整体结构 生成的文件分为三个区域: | 文件偏移 | 长度 | 内容 | 作用 | | -------- | ------: | -------- | -------------------------------------- | | `0` | 1 字节 | `0x02` | 加密/混淆文件标志 | | `1~64` | 64 字节 | 混淆头 | 隐藏密码和可选参数,并提供正文初始状态 | | `65~EOF` | 可变 | 正文密文 | 规范化源码及包装文本的链式 XOR 结果 | 图示如下: ```text +-----------+--------------------------------------+-------------------------+ | 偏移 0 | 偏移 1~64 | 偏移 65~文件结束 | +-----------+--------------------------------------+-------------------------+ | 标志 0x02 | 64 字节混淆头 | 正文密文 | | | 38 个元数据槽 + 26 个随机干扰槽 | 从 key[126] 开始循环 | +-----------+--------------------------------------+-------------------------+ ``` 偏移 `64` 是混淆头最后一个字节。它不是正文的明文,但会直接作为正文混淆的初始“前一个密文字节”。 --- ## 4. 总体生成流程 ```mermaid flowchart TD A["接收源文件、目标文件、可选密码和可选整数参数"] --> B{"源文件名和目标文件名相同?"} B -- "是" --> Z["报错并停止"] B -- "否" --> C["调用 ilCompress 解析并规范化源文件"] C --> D["目标文件写入标志 0x02"] D --> E["使用当前时间初始化随机数"] E --> F["构造 38 字节 metadata"] F --> G["将 metadata 分散到 64 字节混淆头"] G --> H["保存偏移 64 作为正文 previous_cipher"] H --> I["生成开头时间包装文本"] I --> J["链式 XOR 写入规范化源码"] J --> K["生成并链式 XOR 写入结尾时间包装文本"] K --> L["关闭文件并删除临时文件"] ``` --- ## 5. 输入参数和固定初始值 ### 5.1 函数参数 加密逻辑可以整理为以下函数形式: ```c ilEncrypt( char *source_file, const char *output_file, const char *password, int option ); ``` 参数含义: | 参数 | 含义 | | ------------- | ---------------------------------------- | | `source_file` | 待处理源文件路径 | | `output_file` | 输出文件路径 | | `password` | 可选密码;未提供时包装层传入空字符串 | | `option` | 可选整数;转换为十进制文本后放入元数据区 | ### 5.2 固定初始值 | 名称 | 固定值 | 作用 | | ---------------- | -----------: | ---------------------------------- | | 文件标志 | `0x02` | 表示这是混淆文件 | | 元数据长度 | 38 字节 | 保存密码、参数及随机填充 | | 混淆头长度 | 64 字节 | 隐藏 38 字节元数据 | | 头部初始 XOR 值 | `5` | 生成第一个混淆头字节 | | 第一段选择状态 | `0x25ACCB75` | 控制前半段元数据位置 | | 第二段选择状态 | `0x577CCEB7` | 在指定位置重置后控制后半段 | | 正文密钥长度 | 127 字节 | 固定循环密钥表 | | 正文初始密钥索引 | `126` | 第一个正文字符使用最后一个密钥字节 | ### 5.3 随机数 写入 `0x02` 后,程序执行相当于: ```c srand((unsigned int)time(NULL)); ``` 随后使用 `random()`,并只保留结果的最低 8 位: ```c uint8_t random_byte = (uint8_t)random(); ``` 随机数共用于两处: 1. 先调用 38 次,初始化完整的 `metadata[38]`; 2. 生成 64 字节混淆头时,26 个非元数据位置各调用一次。 因此完整头部生成阶段总共调用 64 次 `random()`。密码和参数会覆盖 metadata 中的部分随机字节,但不会改变随机调用次数。 相同源文件和相同密码在不同时间运行,通常也会生成不同的混淆头。读取程序不需要重新生成这些随机数。 --- ## 6. 加密前的正文处理 ### 6.1 `ilCompress` 的实际作用 程序首先把源文件处理到临时文件。这里的 `ilCompress` 不是 ZIP、gzip 或其他通用压缩算法,而是针对 SKILL/IL 文本的解析和重新打印: 1. 使用内部解析器读取输入; 2. 将解析出的对象重新打印; 3. 对象之间插入空格; 4. 每组对象后写入换行; 5. 跳过解析器认定为空或无须输出的内容。 这意味着输出正文通常仍是文本,但原始缩进、换行布局和部分注释可能丢失。因此该过程并非严格可逆的字节压缩。 ### 6.2 实际进入正文 XOR 的明文 程序不会只混淆规范化源码,还会在前后加入两段文本: ```text /* <开始时间计算值> */\n <规范化后的源码> ; <结束时间计算值>\n ``` 开头文本使用: ```c sprintf(buffer, "/* %d */\n", value); ``` 其中 `value` 来自开始时间的部分位运算和乘法。结尾文本使用: ```c sprintf(buffer, "; %.16e\n", time_value); ``` 其中 `time_value` 是开始时间和结束时间转换成浮点数后的乘积。这些时间字段不参与密码验证,也不生成正文密钥,主要起包装和格式标记作用。 --- ## 7. 38 字节 metadata 的生成 ### 7.1 先全部随机初始化 逻辑等价于: ```c uint8_t metadata[38]; for (int i = 0; i < 38; ++i) metadata[i] = (uint8_t)random(); ``` 这一步完成后,所有位置都有随机值。随后固定字段覆盖其中一部分。 ### 7.2 完整布局 ```text 索引 0 1................15 16 17............28 29 30 31......37 内容 固定0 密码区域 0 option 文本 0 0 随机填充 ``` | metadata 索引 | 长度 | 内容 | | ------------- | ---: | ----------------------------------------- | | `[0]` | 1 | 固定写成`0` | | `[1..15]` | 15 | 密码,最多保存 15 字节 | | `[16]` | 1 | 固定写成`0`,确保密码结束 | | `[17..28]` | 12 | `option` 的十进制字符串及未覆盖的随机填充 | | `[29]` | 1 | 固定写成`0` | | `[30]` | 1 | 固定写成`0` | | `[31..37]` | 7 | 保留初始随机填充 | 对应的写入逻辑为: ```c metadata[0] = 0; strncpy((char *)&metadata[1], password, 15); metadata[16] = 0; if (option != 0) sprintf((char *)&metadata[17], "%d", option); else metadata[17] = 0; metadata[29] = 0; metadata[30] = 0; ``` `sprintf` 会在十进制数字后写入 `\0`。数字字符串结束后的未覆盖位置仍可能保留最初的随机字节。 ### 7.3 不设置密码 未提供密码时,包装层传入空字符串: ```c password = ""; ``` 然后执行: ```c strncpy((char *)&metadata[1], "", 15); ``` 由于源字符串长度为 0,`strncpy` 会使用 `0` 填满整个 15 字节区域: ```text metadata[1] = 00 metadata[2] = 00 ... metadata[15] = 00 metadata[16] = 00 ``` 无密码时最关键的判定值是: ```c metadata[1] == 0; ``` 混淆头仍然是完整的 64 字节,随机位置仍然存在,正文仍然使用相同的 127 字节固定密钥。无密码不会缩短或省略头部。 ### 7.4 设置短密码 以密码 `"abc"` 为例: ```text 'a' = 0x61 'b' = 0x62 'c' = 0x63 ``` `strncpy` 的结果为: ```text metadata[1] = 61 metadata[2] = 62 metadata[3] = 63 metadata[4] = 00 metadata[5] = 00 ... metadata[15] = 00 metadata[16] = 00 ``` 短密码后面会补零,因此官方的 15 字节比较仍能判断完整字符串是否一致。例如保存的是 `"abc"` 时,输入 `"abcXYZ"` 不会通过,因为第 4 个比较字节分别是 `0x00` 和 `'X'`。 ### 7.5 设置长密码 如果密码超过 15 字节: ```c strncpy((char *)&metadata[1], password, 15); metadata[16] = 0; ``` 只会保存前 15 字节,后续字节从未写入文件。例如: ```text 输入密码:123456789012345ABCDEFG 实际保存:123456789012345 ``` 官方读取时也只比较前 15 字节。因此两个输入只要前 15 字节相同,就会被认为密码相同。 如果 UTF-8 多字节字符刚好跨越第 15 字节边界,保存内容可能截断在一个字符的中间。底层比较仍按字节进行,不理解字符边界。 ### 7.6 `option` 字段 当 `option = 123` 时: ```c sprintf((char *)&metadata[17], "%d", 123); ``` 写入的是文本: ```text metadata[17] = 31 // '1' metadata[18] = 32 // '2' metadata[19] = 33 // '3' metadata[20] = 00 // 字符串结束 ``` 当 `option == 0` 时,程序只设置: ```c metadata[17] = 0; ``` 当前库中的本地密码读取路径会恢复全部 38 字节 metadata,但只使用密码区域;没有继续使用这个整数文本字段。它可能用于兼容其他版本或外部读取者。 --- ## 8. 64 字节混淆头的生成 ### 8.1 三个初始状态 生成头部前,程序设置: ```c uint8_t previous = 5; uint32_t state = 0x25ACCB75; int metadata_index = 0; ``` 含义如下: - `previous`:上一轮生成的头部字节;第一轮没有上一字节,因此使用固定值 `5`; - `state`:只负责决定当前槽使用 metadata 还是随机字节; - `metadata_index`:下一个待隐藏的 metadata 索引。 `state` 不由密码生成,也不是正文密钥。 ### 8.2 64 轮生成逻辑 循环计数从 `2` 到 `65`,一共执行 64 次。循环计数与文件偏移的关系是: ```c file_offset = position - 1; ``` 所以 `position=2` 写入文件偏移 1,`position=65` 写入偏移 64。 清理后的逻辑如下: ```c for (int position = 2; position <= 65; ++position) { uint8_t selected; if ((state & 0x80000000U) != 0 && metadata_index < 38) selected = metadata[metadata_index++]; else selected = (uint8_t)random(); previous ^= selected; fputc(previous, output); if (position == 33) state = 0x577CCEB7U; else state <<= 1; } ``` 使用 `uint32_t` 是为了明确表达 32 位左移和最高位检查,避免有符号整数溢出的语言细节。 ### 8.3 选择 metadata 还是随机数 条件: ```c (state & 0x80000000U) != 0 ``` 检查 `state` 的最高位: ```text 最高位为 1:选择下一个 metadata 字节 最高位为 0:调用 random() 产生随机干扰字节 ``` 开始几轮的状态如下: | position | 文件偏移 | state | 最高位 | 数据来源 | | -------: | -------: | -----------: | -----: | ------------- | | 2 | 1 | `0x25ACCB75` | 0 | 随机字节 | | 3 | 2 | `0x4B5996EA` | 0 | 随机字节 | | 4 | 3 | `0x96B32DD4` | 1 | `metadata[0]` | 处理完 `position=33` 后,状态被固定重置为: ```c state = 0x577CCEB7; ``` 这个固定模式在 64 轮中恰好选择 38 次 metadata,因此 `metadata[0..37]` 全部被隐藏一次;另外 26 轮使用新的随机字节。 ### 8.4 链式生成公式 每一轮先选择 `selected`,再执行: ```c current = previous ^ selected; ``` 写入后: ```c previous = current; ``` 设 64 个被选中的值是 `X0~X63`,64 个头部输出是 `H0~H63`,则: ```text H0 = 5 ^ X0 H1 = H0 ^ X1 H2 = H1 ^ X2 ... H63 = H62 ^ X63 ``` 展开最后一个头部字节: ```text H63 = 5 ^ X0 ^ X1 ^ X2 ^ ... ^ X63 ``` 在文件中: ```text H0 位于 file[1] H63 位于 file[64] ``` ### 8.5 metadata 与文件偏移的分组映射 | metadata 范围 | 含义 | 对应文件偏移 | | ------------- | ------------- | --------------------------------------------------------- | | `[0]` | 固定 0 | `3` | | `[1..15]` | 密码 | `6, 8, 9, 11, 13, 14, 17, 18, 21, 23, 24, 26, 27, 28, 30` | | `[16]` | 密码结束符 | `32` | | `[17..28]` | `option` 区域 | `34, 36, 38, 39, 40, 42, 43, 44, 45, 46, 49, 50` | | `[29..30]` | 固定 0 | `53, 54` | | `[31..37]` | 随机填充 | `55, 57, 59, 60, 62, 63, 64` | 这里的“对应文件偏移”并不表示该位置直接等于 metadata。实际关系是: ```c file[offset] = file[offset - 1] ^ metadata[index]; ``` 因此恢复公式是: ```c metadata[index] = file[offset] ^ file[offset - 1]; ``` ### 8.6 纯随机位置 未列入映射表的 26 个偏移是随机干扰位置。在这些位置: ```c file[offset] = file[offset - 1] ^ random_byte; ``` 虽然也能通过相邻字节 XOR 算出 `random_byte`,但它没有字段含义,官方读取过程不会保存它。 ### 8.7 偏移 64 的形成 最后一轮为: ```text position = 65 file_offset = 64 state = 0x80000000 metadata_index = 37 ``` 最高位为 1,因此选择: ```c selected = metadata[37]; ``` `metadata[37]` 没有被密码或参数覆盖,是初始阶段留下的随机字节。因此: ```c file[64] = file[63] ^ metadata[37]; ``` 头部结束后,变量 `previous` 就等于 `file[64]`。程序把它直接作为正文的初始 `previous_cipher`。 ### 8.8 混淆头生成流程图 ```mermaid flowchart TD A["初始化 previous=5、state=0x25ACCB75、metadata_index=0"] --> B["position 从 2 开始"] B --> C{"state 最高位为 1 且 metadata 尚未取完?"} C -- "是" --> D["selected = metadata[metadata_index++]"] C -- "否" --> E["selected = random() 的低 8 位"] D --> F["previous = previous XOR selected"] E --> F F --> G["将 previous 写入文件"] G --> H{"position 等于 33?"} H -- "是" --> I["state = 0x577CCEB7"] H -- "否" --> J["state 左移 1 位"] I --> K{"position 等于 65?"} J --> K K -- "否" --> L["position 加 1"] L --> C K -- "是" --> M["file[64] 成为正文 previous_cipher"] ``` --- ## 9. 有密码与无密码的头部对比 ### 9.1 无密码示例 无密码时: ```text metadata[1..15] = 00 metadata[16] = 00 ``` 密码第一个字节对应文件偏移 6,因此: ```c file[6] = file[5] ^ metadata[1]; ``` 因为 `metadata[1] == 0`: ```c file[6] = file[5] ^ 0; file[6] = file[5]; ``` 密码区域其他位置也有相同特征:在准确的元数据槽上,当前头部字节等于前一个头部字节。随机槽不遵守这个特征,因此必须根据固定位置判断,不能简单搜索所有相等字节。 恢复时: ```c metadata[1] = file[6] ^ file[5]; ``` 结果为 `0`,官方逻辑据此认定没有密码。 ### 9.2 设置 `"abc"` 的示例 密码字节为: ```text metadata[1] = 0x61 metadata[2] = 0x62 metadata[3] = 0x63 metadata[4] = 0x00 ``` 对应关系: ```c file[6] = file[5] ^ 0x61; /* 'a' */ file[8] = file[7] ^ 0x62; /* 'b' */ file[9] = file[8] ^ 0x63; /* 'c' */ file[11] = file[10] ^ 0x00; /* 字符串结束 */ ``` 官方读取时: ```c metadata[1] = file[6] ^ file[5]; metadata[2] = file[8] ^ file[7]; metadata[3] = file[9] ^ file[8]; metadata[4] = file[11] ^ file[10]; ``` 恢复得到: ```text 61 62 63 00 a b c \0 ``` ### 9.3 两种情况的比较 | 项目 | 无密码 | 有密码 | | -------------------- | ----------- | ------------------------------ | | 文件标志 | `0x02` | `0x02` | | 混淆头长度 | 64 字节 | 64 字节 | | 密码区域 | 15 个`0x00` | 最多 15 个密码字节,短密码补零 | | `metadata[1]` | `0` | 通常非`0` | | 官方读取要求 | 直接继续 | 必须提供匹配密码 | | 正文固定 key | 相同 | 相同 | | 密码是否参与正文 key | 否 | 否 | --- ## 10. 官方程序如何读取混淆头 ### 10.1 检查标志 官方读取流程先读取文件偏移 0: ```c int marker = getc(file); ``` 如果: ```c marker == 0x02 ``` 则进入混淆头读取流程;否则把该字节放回输入流并按普通文件处理。 ### 10.2 恢复 metadata 官方 `ilReadHeader` 按顺序读取偏移 `1~64`,使用与生成端相同的 `state` 变化规律识别 38 个元数据槽。 在元数据槽上,恢复公式是: ```c metadata[index] = current_header ^ previous_header; ``` 因为生成端满足: ```c current_header = previous_header ^ metadata[index]; ``` 代入后: ```text previous_header ^ current_header = previous_header ^ previous_header ^ metadata[index] = 0 ^ metadata[index] = metadata[index] ``` 读取端不需要知道: - `srand` 使用的时间; - 38 字节初始随机区的原始随机值; - 26 个随机干扰槽使用了什么随机数; - 头部生成初始值 `5`。 它只需要知道固定的元数据位置,并读取相邻两个实际头部字节。 ### 10.3 等价的固定偏移恢复方式 如果不复现 `state`,也可以直接使用固定偏移表: ```c static const uint8_t metadata_offsets[38] = { 3, 6, 8, 9, 11, 13, 14, 17, 18, 21, 23, 24, 26, 27, 28, 30, 32, 34, 36, 38, 39, 40, 42, 43, 44, 45, 46, 49, 50, 53, 54, 55, 57, 59, 60, 62, 63, 64 }; for (int i = 0; i < 38; ++i) { uint8_t offset = metadata_offsets[i]; metadata[i] = file_data[offset] ^ file_data[offset - 1]; } ``` 这与官方状态机的结果相同。 ### 10.4 判断是否有密码 恢复后,官方程序检查密码区域第一个字节: ```c if (metadata[1] == 0) { /* 没有密码 */ } else { /* 文件要求密码 */ } ``` 这里没有单独的“密码存在”标志。空密码通过 `metadata[1] == 0` 表示。 ### 10.5 密码验证 如果 `metadata[1] != 0`: 1. 检查调用者是否提供了待读取密码; 2. 未提供时关闭文件并报告“需要密码”; 3. 已提供时执行最多 15 字节比较; 4. 不匹配时关闭文件并报告“密码错误”; 5. 匹配时允许继续读取正文。 比较逻辑相当于: ```c strncmp(input_password, (const char *)&metadata[1], 15); ``` 短密码依靠 `\0` 和后续补零进行精确比较;长度达到或超过 15 字节时,只比较前 15 字节。 ### 10.6 初始化正文读取状态 读取完偏移 64 后,官方程序保存两个状态: ```c previous_cipher = file[64]; key_index = 126; ``` 偏移 64 不需要还原成某个“正文明文”。读取端直接使用文件中保存的原始字节值。 ### 10.7 官方读取流程图 ```mermaid flowchart TD A["读取文件偏移 0"] --> B{"字节等于 0x02?"} B -- "否" --> C["按普通文件处理"] B -- "是" --> D["读取偏移 1~64"] D --> E["在固定槽位计算 current XOR previous"] E --> F["恢复 metadata[0..37]"] F --> G{"metadata[1] 等于 0?"} G -- "是" --> K["不要求密码"] G -- "否" --> H{"调用者提供了密码?"} H -- "否" --> I["报告需要密码并停止"] H -- "是" --> J{"前 15 字节匹配?"} J -- "否" --> L["报告密码错误并停止"] J -- "是" --> K K --> M["previous_cipher = file[64]"] M --> N["key_index = 126"] N --> O["开始还原正文"] ``` --- ## 11. 正文链式 XOR 混淆 ### 11.1 正文初始状态 头部完成后: ```c previous_cipher = file[64]; key_index = 126; ``` 正文第一个输出写入文件偏移 `65`。 ### 11.2 单字节生成公式 对于一个明文字节 `plain`: ```c current_cipher = plain ^ previous_cipher ^ body_key[key_index]; ``` 写出后更新: ```c previous_cipher = current_cipher; if (--key_index < 0) key_index = 126; ``` 密钥索引顺序为: ```text 126, 125, 124, ..., 2, 1, 0, 126, 125, ... ``` 每处理 127 字节循环一次。 ### 11.3 第一个正文字符 正文明文首先是开头包装字符串的 `/`: ```text '/' = 0x2F ``` 第一个正文密文字节为: ```c file[65] = 0x2F ^ file[64] ^ body_key[126]; ``` 固定表中: ```text body_key[126] = 0xF7 ``` 假设: ```text file[64] = 0x9A ``` 则: ```text file[65] = 0x2F ^ 0x9A ^ 0xF7 = 0x42 ``` 还原时: ```text 0x42 ^ 0x9A ^ 0xF7 = 0x2F = '/' ``` ### 11.4 后续正文字符 第二个正文字符使用第一个正文密文字节作为 `previous_cipher`: ```c file[66] = plain[1] ^ file[65] ^ body_key[125]; ``` 第三个字符: ```c file[67] = plain[2] ^ file[66] ^ body_key[124]; ``` 规律如下: | 正文相对索引 | 文件偏移 | 前一个密文字节 | 密钥索引 | | -----------: | -------: | -------------- | --------------: | | `0` | `65` | `file[64]` | `126` | | `1` | `66` | `file[65]` | `125` | | `2` | `67` | `file[66]` | `124` | | `j` | `65+j` | `file[64+j]` | `126-(j % 127)` | 通用生成公式: ```c file[65 + j] = plain[j] ^ file[64 + j] ^ body_key[126 - (j % 127)]; ``` ### 11.5 正文还原公式 由于 XOR 可以抵消,还原公式和生成公式形式相同: ```c plain[j] = file[65 + j] ^ file[64 + j] ^ body_key[126 - (j % 127)]; ``` 证明: ```text current_cipher ^ previous_cipher ^ key = plain ^ previous_cipher ^ key ^ previous_cipher ^ key = plain ^ (previous_cipher ^ previous_cipher) ^ (key ^ key) = plain ^ 0 ^ 0 = plain ``` ### 11.6 正确更新 previous 还原一字节后,必须保存当前的原始密文字节: ```c previous_cipher = current_cipher; ``` 不能写成: ```c previous_cipher = plain; /* 错误 */ ``` 因为下一字节的公式依赖前一个密文字节,而不是前一个明文字节。 ### 11.7 是否必须从头开始 自然实现从偏移 65 向后处理最简单。但任意正文位置都可以单独还原,因为当前密文字节和前一个密文字节都保存在文件中,密钥索引也能由偏移计算: ```c body_index = file_offset - 65; key_index = 126 - (body_index % 127); ``` 因此并不需要从文件结尾倒序处理,也不需要先求出偏移 64 对应的某个明文。 --- ## 12. 官方正文读取过程 官方单字节读取逻辑可以整理为: ```c int read_plain_byte(FILE *input) { int value = fgetc(input); if (value == EOF) return EOF; uint8_t current_cipher = (uint8_t)value; uint8_t plain = current_cipher ^ previous_cipher ^ body_key[key_index]; previous_cipher = current_cipher; key_index = (key_index == 0) ? 126 : key_index - 1; return plain; } ``` 官方端口对象还会保存 `previous_cipher` 和 `key_index`,这样在不同输入端口之间切换后,可以恢复每个端口自己的读取位置和链式状态。 密码验证只决定官方读取过程是否允许继续。密码没有参与上面的正文 XOR,也没有改变固定 `body_key`。 --- ## 13. 简洁的 C 语言还原示例 下面代码仅展示核心操作: 从偏移 65 开始还原完整正文流。 ```c unsigned char previous_cipher = file_data[64]; int key_index = 126; for (size_t offset = 65; offset < file_size; offset++) { unsigned char current_cipher = file_data[offset]; unsigned char plain = current_cipher ^ previous_cipher ^ key[key_index]; previous_cipher = current_cipher; if (--key_index < 0) key_index = 126; } ``` 仅用于算法研究,生产代码请各位自行编写。 --- ## 14. 完整小型演算 假设密码是 `"abc"`,并假设头部生成过程中某些随机字节已经确定。 ### 14.1 metadata ```text metadata[0] = 00 metadata[1] = 61 ('a') metadata[2] = 62 ('b') metadata[3] = 63 ('c') metadata[4] = 00 ... metadata[16] = 00 ``` ### 14.2 开始生成头部 用 `Q0, Q1, ...` 表示生成头部时额外取得的随机干扰字节: ```text file[1] = 0x05 ^ Q0 file[2] = file[1] ^ Q1 file[3] = file[2] ^ metadata[0] file[4] = file[3] ^ Q2 file[5] = file[4] ^ Q3 file[6] = file[5] ^ metadata[1] ``` 由于 `metadata[0] == 0`: ```text file[3] == file[2] ``` 密码第一个字节: ```text file[6] = file[5] ^ 0x61 ``` 读取时: ```text file[6] ^ file[5] = 0x61 = 'a' ``` 密码第二、第三字节分别从偏移 `8` 和 `9` 恢复。 ### 14.3 形成偏移 64 最后一轮: ```text file[64] = file[63] ^ metadata[37] ``` 假设: ```text file[63] = 0xA6 metadata[37] = 0x3C ``` 那么: ```text file[64] = 0xA6 ^ 0x3C = 0x9A ``` ### 14.4 生成第一个正文密文字节 正文第一个明文字符为 `/`: ```text plain[0] = 0x2F file[64] = 0x9A body_key[126]= 0xF7 ``` 生成: ```text file[65] = 0x2F ^ 0x9A ^ 0xF7 = 0x42 ``` 还原: ```text plain[0] = 0x42 ^ 0x9A ^ 0xF7 = 0x2F = '/' ``` 这说明混淆头和正文之间的连接点只有一个:偏移 `64` 的原始字节值。 --- ## 15. 算法特性和限制 ### 15.1 密码不是正文密钥 密码只存放在 metadata 中,并由官方读取流程检查。它没有参与 127 字节固定密钥表的生成,也没有参与正文 key 索引变化。 因此,从算法角度看,即使不知道密码,只要知道文件格式和固定密钥表,仍然可以恢复正文。密码只是一层官方读取路径的访问控制。 ### 15.2 密码最多有效 15 字节 - 短密码完整保存并补零; - 超过 15 字节的后缀不会写入文件; - 官方比较最多 15 字节; - UTF-8 字符可能在第 15 字节处被截断。 ### 15.3 随机数不提供强安全性 随机数主要用于: - 让相同输入在不同运行中得到不同头部; - 填充无意义槽位; - 改变偏移 64,进而改变整段正文密文。 但 metadata 的位置固定,相邻字节 XOR 可以直接恢复内容;读取者也不需要知道随机种子。 ### 15.4 正文是线性 XOR 固定 key、当前密文和前一个密文之间是线性关系。已知算法后,可以直接反向计算。它适合隐藏文本,不能替代 AES-GCM、ChaCha20-Poly1305 等现代认证加密算法。 ### 15.5 字节损坏的影响 如果一个正文密文字节损坏: - 对应位置的明文字节会错误; - 下一位置也会错误,因为它使用了损坏字节作为 `previous_cipher`; - 再后面通常恢复正常,因为后续使用文件中保存的下一个原始密文字节。 该格式没有认证标签,无法可靠判断文件是否被恶意修改。 --- ## 附录 A:完整 127 字节固定密钥 ```text 36 61 3E F1 69 3C 2E 8E 55 CF C2 86 9C 3B 94 22 64 41 0B 6B E5 AC A9 19 D3 D0 AA 9E 49 05 03 7F 67 42 72 50 FD A0 5E 53 EF 21 59 0C 5C EE AE C0 AF B9 AB 15 E6 D4 2E BA A6 59 59 6F 5E 5D EF 45 1F E1 95 9C 02 F4 70 72 95 C9 FD F2 B8 AC 33 E7 67 5F FD CD 34 2C 88 5A 85 62 CA 64 3F BA A9 DD 1C 40 7A 9D B4 6A 10 4A 35 0F BC 6D BB 70 56 A2 CF 54 70 04 00 79 5F 86 5B A9 EA 9A E3 14 F7 ``` 前 7 行各 16 字节,最后一行 15 字节,总计: ```text 7 * 16 + 15 = 127 字节 ``` --- ## 附录 B:metadata 索引与文件偏移完整表 | metadata | 字段 | 文件偏移 | 恢复公式 | | -------: | ----------------------- | -------: | --------------------- | | 0 | 固定 0 | 3 | `file[3] ^ file[2]` | | 1 | 密码字节 0 | 6 | `file[6] ^ file[5]` | | 2 | 密码字节 1 | 8 | `file[8] ^ file[7]` | | 3 | 密码字节 2 | 9 | `file[9] ^ file[8]` | | 4 | 密码字节 3 | 11 | `file[11] ^ file[10]` | | 5 | 密码字节 4 | 13 | `file[13] ^ file[12]` | | 6 | 密码字节 5 | 14 | `file[14] ^ file[13]` | | 7 | 密码字节 6 | 17 | `file[17] ^ file[16]` | | 8 | 密码字节 7 | 18 | `file[18] ^ file[17]` | | 9 | 密码字节 8 | 21 | `file[21] ^ file[20]` | | 10 | 密码字节 9 | 23 | `file[23] ^ file[22]` | | 11 | 密码字节 10 | 24 | `file[24] ^ file[23]` | | 12 | 密码字节 11 | 26 | `file[26] ^ file[25]` | | 13 | 密码字节 12 | 27 | `file[27] ^ file[26]` | | 14 | 密码字节 13 | 28 | `file[28] ^ file[27]` | | 15 | 密码字节 14 | 30 | `file[30] ^ file[29]` | | 16 | 密码结束符 | 32 | `file[32] ^ file[31]` | | 17 | option 字节 0 | 34 | `file[34] ^ file[33]` | | 18 | option 字节 1 | 36 | `file[36] ^ file[35]` | | 19 | option 字节 2 | 38 | `file[38] ^ file[37]` | | 20 | option 字节 3 | 39 | `file[39] ^ file[38]` | | 21 | option 字节 4 | 40 | `file[40] ^ file[39]` | | 22 | option 字节 5 | 42 | `file[42] ^ file[41]` | | 23 | option 字节 6 | 43 | `file[43] ^ file[42]` | | 24 | option 字节 7 | 44 | `file[44] ^ file[43]` | | 25 | option 字节 8 | 45 | `file[45] ^ file[44]` | | 26 | option 字节 9 | 46 | `file[46] ^ file[45]` | | 27 | option 字节 10 | 49 | `file[49] ^ file[48]` | | 28 | option 字节 11 | 50 | `file[50] ^ file[49]` | | 29 | 固定 0 | 53 | `file[53] ^ file[52]` | | 30 | 固定 0 | 54 | `file[54] ^ file[53]` | | 31 | 随机填充 | 55 | `file[55] ^ file[54]` | | 32 | 随机填充 | 57 | `file[57] ^ file[56]` | | 33 | 随机填充 | 59 | `file[59] ^ file[58]` | | 34 | 随机填充 | 60 | `file[60] ^ file[59]` | | 35 | 随机填充 | 62 | `file[62] ^ file[61]` | | 36 | 随机填充 | 63 | `file[63] ^ file[62]` | | 37 | 随机填充/正文初始链来源 | 64 | `file[64] ^ file[63]` | --- ## 附录 C:术语对照 | 术语 | 本文含义 | | ----------------- | --------------------------------------------------------- | | 文件标志 | 偏移 0 的固定字节`0x02` | | metadata | 被分散隐藏的 38 字节元数据 | | 混淆头 | 偏移`1~64` 的 64 字节输出 | | 元数据槽 | 固定选择 metadata 的 38 个头部位置 | | 随机槽 | 选择`random()` 低字节的 26 个头部位置 | | `previous` | 生成混淆头时的前一个头部输出 | | `previous_cipher` | 正文生成/读取时的前一个正文密文字节;第一轮使用`file[64]` | | `state` | 决定头部当前槽使用 metadata 还是随机字节的固定选择状态 | | `key_index` | 正文固定密钥表索引,从 126 递减并循环 | | 正文相对索引 | 从文件偏移 65 开始重新以 0 计数的位置 | 最后修改:2026 年 07 月 12 日 © 允许规范转载 赞 如果觉得我的文章对你有用,请随意赞赏