DWG 2004 文件头解析实战:0x6C 加密数据 XOR 解密与 3 个关键偏移量定位
DWG 2004 文件头解析实战:0x6C 加密数据 XOR 解密与 3 个关键偏移量定位
1. DWG 文件头结构概述
DWG 2004 文件头是一个固定长度为 0x100 字节的数据块,位于文件起始位置。这个头部包含了版本标识、安全标志以及指向文件其他关键部分的指针。对于逆向工程师和 CAD 开发者来说,理解这个文件头的结构是解析整个 DWG 文件的第一步。
文件头的主要组成部分包括:
- 版本标识:位于 0x00 偏移处,7 字节的字符串 "AC1018\0"
- 安全标志:位于 0x18 偏移处,4 字节的位掩码
- 加密数据块:位于 0x80 偏移处,0x6C 字节的 XOR 加密数据
- 填充数据:位于 0xEC 偏移处,0x14 字节的魔术字节序列
以下是 DWG 2004 文件头的完整内存布局:
| 偏移量 | 长度(字节) | 描述 |
|---|---|---|
| 0x00 | 7 | "AC1018\0" 版本标识 |
| 0x07 | 4 | 保留字段(全零) |
| 0x0B | 1 | 主版本号 |
| 0x0C | 1 | 标志位(0/1/3) |
| 0x0D | 4 | 预览图片地址 |
| 0x11 | 1 | DWG 版本号 |
| 0x12 | 1 | DWG 维护版本号 |
| 0x13 | 2 | 代码页索引 |
| 0x15 | 1 | 保留字段 |
| 0x16 | 1 | 应用版本号 |
| 0x17 | 1 | 应用维护版本号 |
| 0x18 | 4 | 安全标志位 |
| 0x1C | 4 | 未知字段 |
| 0x20 | 4 | SummaryInfo 地址 |
| 0x24 | 4 | VBA 工程地址 |
| 0x28 | 4 | 2004 头部地址(固定为 0x80) |
| 0x2C | 0x54 | 保留字段(全零) |
| 0x80 | 0x6C | 加密的 2004 头部数据 |
| 0xEC | 0x14 | 填充数据 |
2. 0x6C 加密数据 XOR 解密算法
位于 0x80 偏移处的 0x6C 字节数据是 DWG 文件解析的关键,这部分数据使用了一种基于伪随机数生成的 XOR 加密算法。解密过程需要与特定的魔术字节序列进行逐字节异或运算。
2.1 魔术字节序列生成
魔术字节序列可以通过以下算法生成:
unsigned char magic_bytes[0x6C]; int seed = 1; for (int i = 0; i < 0x6C; i++) { seed = seed * 0x343FD + 0x269EC3; magic_bytes[i] = (unsigned char)(seed >> 0x10); }生成的魔术字节序列如下(十六进制表示):
29 23 BE 84 E1 6C D6 AE 52 90 49 F1 F1 BB E9 EB B3 A6 DB 3C 87 0C 3E 99 24 5E 0D 1C 06 B7 47 DE B3 12 4D C8 43 BB 8B A6 1F 03 5A 7D 09 38 25 1F 5D D4 CB FC 96 F5 45 3B 13 0D 89 0A 1C DB AE 32 20 9A 50 EE 40 78 36 FD 12 49 32 F6 9E 7D 49 DC AD 4F 14 F2 44 40 66 D0 6B C4 30 B7 32 3B A1 22 F6 22 91 9D E1 8B 1F DA B0 CA 99 022.2 XOR 解密实现
解密过程就是将加密数据与魔术字节序列逐字节进行异或运算。以下是 C 语言实现示例:
void decrypt_2004_header(unsigned char* encrypted_data) { int seed = 1; for (int i = 0; i < 0x6C; i++) { seed = seed * 0x343FD + 0x269EC3; encrypted_data[i] ^= (seed >> 0x10); } }2.3 解密后的数据结构
解密后的 0x6C 字节数据具有以下结构:
| 偏移量 | 长度(字节) | 描述 |
|---|---|---|
| 0x00 | 12 | "AcFssFcAJMB\0" 标志串 |
| 0x0C | 4 | 头部地址(通常为 0x00) |
| 0x10 | 4 | 头部大小(0x6C) |
| 0x14 | 4 | 保留字段(通常为 0x04) |
| 0x18 | 4 | 根树节点间隙 |
| 0x1C | 4 | 最左下树节点间隙 |
| 0x20 | 4 | 最右下树节点间隙 |
| 0x24 | 4 | 未知字段 |
| 0x28 | 4 | 最后段 ID |
| 0x2C | 4 | 最后段地址 |
| 0x30 | 4 | 保留字段(全零) |
| 0x34 | 4 | 第二头部地址 |
| 0x38 | 4 | 保留字段(全零) |
| 0x3C | 4 | 间隙总数 |
| 0x40 | 4 | 段总数 |
| 0x44 | 4 | 保留字段(通常为 0x20) |
| 0x48 | 4 | 保留字段(通常为 0x80) |
| 0x4C | 4 | 保留字段(通常为 0x40) |
| 0x50 | 4 | 页映射 ID |
| 0x54 | 4 | 页映射地址(相对于数据部分的偏移) |
| 0x58 | 4 | 保留字段(全零) |
| 0x5C | 4 | 段映射 ID |
| 0x60 | 4 | 段页数组大小 |
| 0x64 | 4 | 间隙数组大小 |
| 0x68 | 4 | CRC32 校验值 |
3. 关键偏移量定位与解析
解密后的数据中有三个关键偏移量对解析整个 DWG 文件至关重要:
- 页映射地址 (Page Map Address):位于 0x54 偏移处
- 段映射 ID (Section Map ID):位于 0x5C 偏移处
- 段页数组大小 (Section Page Array Size):位于 0x60 偏移处
3.1 页映射解析
页映射地址指向的是文件中的数据页索引表。需要注意的是,这个地址是相对于数据部分(即文件头之后)的偏移量,因此实际文件偏移需要加上文件头大小(0x100)。
页映射的结构如下:
typedef struct { uint32_t page_number; // 页编号(从1开始) uint32_t page_size; // 页大小 } PageMapEntry;解析页映射的代码示例:
void parse_page_map(FILE* file, uint32_t page_map_offset) { fseek(file, 0x100 + page_map_offset, SEEK_SET); PageMapEntry entry; uint32_t current_offset = 0x100; // 第一个段从文件头后开始 while (fread(&entry, sizeof(PageMapEntry), 1, file) == 1) { printf("Page %u: size=0x%X, file_offset=0x%X\n", entry.page_number, entry.page_size, current_offset); current_offset += entry.page_size; } }3.2 段映射解析
段映射包含了文件中所有段(section)的详细信息。要找到段映射,需要:
- 通过页映射找到段映射 ID 对应的页
- 解析该页的数据获取段映射信息
段映射页的数据结构如下:
typedef struct { uint32_t num_descriptions; // 描述数量 uint32_t compressed; // 是否压缩(1=否,2=是) uint32_t max_size; // 最大大小(通常为0x7400) uint32_t encrypted; // 是否加密(0=否,1=是) uint32_t num_descriptions_2; // 描述数量(重复) } SectionMapHeader; typedef struct { uint64_t section_size; // 段大小 uint32_t num_pages; // 页数量 uint32_t max_decomp_size; // 最大解压大小 uint32_t unknown; // 未知字段 uint32_t compressed; // 是否压缩(1=否,2=是) uint32_t section_type; // 段类型 uint32_t encrypted; // 是否加密(0=否,1=是) char name[64]; // 段名称 } SectionDescription; typedef struct { uint32_t page_number; // 页编号 uint32_t page_data_size; // 页数据大小 uint64_t start_offset; // 起始偏移 } SectionPageInfo;3.3 完整解析流程
结合上述三个关键偏移量,完整的 DWG 文件解析流程如下:
- 读取并解析文件头(0x100 字节)
- 解密 0x80 处的 0x6C 字节加密数据
- 从解密数据中获取页映射地址、段映射 ID 和段页数组大小
- 通过页映射地址找到页映射表
- 通过段映射 ID 在页映射表中找到段映射页
- 解析段映射页获取所有段的信息
- 根据需要访问特定段的数据
4. 实战代码示例
以下是一个完整的 DWG 2004 文件头解析代码示例,包含结构体定义和解密算法:
#include <stdio.h> #include <stdint.h> #include <string.h> #pragma pack(push, 1) typedef struct { char version[7]; // 0x00: "AC1018\0" char x07_unknown[4]; // 0x07: 4 zeros uint8_t is_maint; // 0x0B uint8_t x0c_0_1_3; // 0x0C: 0 or 1 or 3 uint32_t thumbnail_addr;// 0x0D uint8_t dwg_version; // 0x11 uint8_t dwg_maint_version; // 0x12 uint16_t codepage; // 0x13 uint8_t x15_unknown; // 0x15 uint8_t app_version; // 0x16 uint8_t app_maint_version; // 0x17 uint32_t security_type; // 0x18 uint32_t x1c_unknown; // 0x1C uint32_t summary_info_address; // 0x20 uint32_t vba_proj_address; // 0x24 uint32_t r2004_header_address; // 0x28: 0x80 char x2c_0[0x54]; // 0x2C: 0x54 zeros union { char encrypted_data[0x6C]; // 0x80 struct { char file_id_string[12]; // "AcFssFcAJMB\0" uint32_t header_address; uint32_t header_size; uint32_t x14_unknown; uint32_t root_tree_node_gap; uint32_t lowermost_left_tree_node_gap; uint32_t lowermost_right_tree_node_gap; uint32_t x24_unknown; uint32_t last_section_id; uint64_t last_page_address; uint64_t second_header_address; uint32_t gap_amount; uint32_t page_amount; uint32_t x44_x20; uint32_t x48_x80; uint32_t x4c_x40; uint32_t page_map_id; uint64_t page_map_address; uint32_t section_map_id; uint32_t section_array_size; uint32_t gap_array_size; uint32_t crc32; } r2004; }; char padding[0x14]; // 0xEC: 0x14 bytes } Dwg2004Header; #pragma pack(pop) void decrypt_2004_header(Dwg2004Header* header) { int seed = 1; for (int i = 0; i < 0x6C; i++) { seed = seed * 0x343FD + 0x269EC3; header->encrypted_data[i] ^= (seed >> 0x10); } } void print_header_info(const Dwg2004Header* header) { printf("DWG Version: %.6s\n", header->version); printf("Security Flags: 0x%08X\n", header->security_type); printf("Page Map ID: %u\n", header->r2004.page_map_id); printf("Page Map Address: 0x%08X\n", (uint32_t)header->r2004.page_map_address); printf("Section Map ID: %u\n", header->r2004.section_map_id); printf("Section Array Size: %u\n", header->r2004.section_array_size); } int main(int argc, char** argv) { if (argc < 2) { printf("Usage: %s <dwg_file>\n", argv[0]); return 1; } FILE* file = fopen(argv[1], "rb"); if (!file) { perror("Failed to open file"); return 1; } Dwg2004Header header; if (fread(&header, sizeof(header), 1, file) != 1) { perror("Failed to read header"); fclose(file); return 1; } decrypt_2004_header(&header); print_header_info(&header); fclose(file); return 0; }5. 调试技巧与常见问题
5.1 调试技巧
- 十六进制查看器:使用 HxD 或 010 Editor 等工具直接查看 DWG 文件二进制内容
- CRC 校验:解密后的数据包含 CRC32 校验值,可用于验证解密是否正确
- 逐步验证:先验证文件头基本结构,再逐步验证解密数据和关键偏移量
5.2 常见问题
- 解密失败:检查魔术字节序列生成算法是否正确,特别是种子值和移位操作
- 偏移量计算错误:注意页映射地址是相对于数据部分的偏移,需要加上 0x100
- 字节序问题:DWG 文件使用小端字节序,在某些平台上可能需要转换
提示:在实际项目中,建议先对小型 DWG 文件进行测试,验证解析逻辑正确性后再处理大型文件。