行业资讯
📅 2026/7/30 5:30:18
【系列:CCG Crypto CrackMe 逆向全解析 · 第 1 篇】
导读2001 年CCGChina Cracking Group的 Blowfish 放出了一个 CrackMe声称本题多解。这个系列将完整记录破解它的全过程——从最基础的字节解析到自定义壳的算法复原到最后跑出一个能通过真实程序验证的 Keygen。系列的第一条原则很朴素任何结论都必须能用代码复现本系列都是围绕破解过程中一步步推导过程来写的。第一篇我们先不谈算法只把手里这个 123,904 字节的文件用最笨的方法过一遍。逆向分析的第一步不是猜壳猜算法而是把文件当成一堆确定的字节。这篇文章不做任何推测只用 Python 逐字段解析一个 crackme 样本的 PE 结构每个数字都附带可运行的代码看完你也能自己验证一遍。很多逆向教程一上来就说这是 UPX 壳“这是自定义加密”。但这些判断的依据是什么大多数时候答案是经验和直觉不是当场验证的事实。这篇文章想做一件更基础的事打开crackme_crypto.exe只用struct和open把能确定的东西一条条读出来。不下结论只呈现数字。文件大小从来不是大概网上流传的说法是这个样本大约124KB。用代码量一下就知道这种四舍五入的说法从一开始就是错的。importos pathcrackme_crypto.exesizeos.path.getsize(path)print(size)# 123904123904 字节不是 124000也不是 126976124×1024。逆向工作里大约没有意义只有精确的字节数才能拿来做后续比对。PE 头的位置不用猜文件里写着DOS 头的第 0x3C 字节处存的就是 PE 头的偏移量e_lfanew。这是 PE 格式规定死的不需要经验判断。importstructwithopen(path,rb)asf:dataf.read()e_lfanewstruct.unpack(I,data[0x3C:0x40])[0]print(hex(e_lfanew))# 0xe0pe_sigdata[e_lfanew:e_lfanew4]print(pe_sig)# bPE\x00\x00e_lfanew 0xE0跳到这个偏移四个字节正好是PE\0\0。这两个值互相印证偏移对了签名也对了说明这是一个结构完整的 PE 文件而不是拼接出来的伪装文件。COFF 头里的三个数字决定了后面怎么读PE 签名之后紧跟的 20 字节是 COFF 文件头里面的Machine、NumberOfSections、SizeOfOptionalHeader三个字段直接决定了后续解析要怎么走。coff_offsete_lfanew4machine,num_sectionsstruct.unpack(HH,data[coff_offset:coff_offset4])size_opt_headerstruct.unpack(H,data[coff_offset16:coff_offset18])[0]print(hex(machine))# 0x14cprint(num_sections)# 6print(hex(size_opt_header))# 0xe0Machine 0x14C对应 32 位 x86 架构。NumberOfSections 6告诉我们后面要读几个节区表条目。SizeOfOptionalHeader 0xE0告诉我们可选头占多少字节从而算出节区表从哪里开始。这三个数字不是背景信息是接下来每一步计算的输入参数。入口点从 RVA 到实际虚拟地址,只差一次加法可选头里最关心的是入口点。这里涉及两个数字AddressOfEntryPoint相对虚拟地址和ImageBase镶像基址两者相加才是程序运行时真正跳转到的地址。opt_offsetcoff_offset20magicstruct.unpack(H,data[opt_offset:opt_offset2])[0]entry_rvastruct.unpack(I,data[opt_offset16:opt_offset20])[0]image_basestruct.unpack(I,data[opt_offset28:opt_offset32])[0]entry_vaimage_baseentry_rvaprint(hex(magic))# 0x10bprint(hex(entry_rva))# 0x44bd6print(hex(image_base))# 0x400000print(hex(entry_va))# 0x444bd6Magic 0x10B说明这是 PE3232位格式不是 PE32。ImageBase 0x400000是默认加载基址。两者相加AddressOfEntryPoint的 RVA0x44BD6换算成实际虚拟地址就是0x444BD6。这个地址意味着什么现在先不下结论。它只是一个确定的数字后续调试器里下断点会用到。六个节区六组数字,先只记录节区表紧跟在可选头之后起始偏移是opt_offset size_opt_header。每个节区头固定 40 字节按顺序读就行。sec_table_offsetopt_offsetsize_opt_headerforiinrange(num_sections):offsec_table_offseti*40namedata[off:off8].rstrip(b\x00)virtual_size,virtual_addressstruct.unpack(II,data[off8:off16])size_raw,ptr_rawstruct.unpack(II,data[off16:off24])print(name,hex(virtual_address),hex(virtual_size),hex(size_raw),hex(ptr_raw))跑出来是六组固定的数字节区名称VirtualAddressVirtualSizeSizeOfRawDataPointerToRawData0UPX!0x10000x120000xA4000x10001UPX!0x130000x10000x8000xB4002UPX!0x140000xA0000x6000xBC003UPX!0x1E0000x20000x6000xC2004.rsrc0x200000x230000xF2000xC8005UPX!0x430000x30000x2A000x1BA00注意一个细节五个节区的名字字面就是UPX!四个字符第 4 个节区叫.rsrc。同时VirtualSize和SizeOfRawData之间的差距在几个节区里相当大。这些名字和数字本身不能证明任何事——名字字段可以被任意改写大小差异也可能有多种原因。它们只是文件里当场读出来的字符串和整数。这组数字要说明什么留到下一篇。文件末尾的六个字节节区数据之外文件还有个容易被忽略的角落——最后几个字节。taildata[-6:]print(tail)# bBF2000print(tail.decode())# BF2000文件末尾的最后 6 个字节解码成 ASCII 正好是BF2000这六个字符。它出现在文件的最末尾不属于任何节区的正常数据范围。这六个字符是什么含义为什么会出现在这个位置这篇不做推测。它是下一篇要处理的第一个问题。先把地基打稳回顾一下这次拿到的确定信息文件大小 123904 字节PE 头偏移0xE0架构0x14C6 个节区入口点实际地址0x444BD6。这些都是当场用代码验证过的事实不依赖任何转述。节区表里五个UPX!名称和一个.rsrc还有文件末尾那六个字符BF2000这两组线索先记下来,不着急下结论。逆向的第一原则很简单先把能用代码确认的东西全部确认完再去谈假设和推理。你在分析文件时通常是先看结构还是先跑一遍再说小结本文是CCG Crypto CrackMe 逆向全解析系列的开篇只做了一件事把crackme_crypto.exe的 PE 结构逐字段解析出来不下任何算法结论。文件大小精确为123,904 字节而不是网上常见的约124KBPE 头、COFF 头、Optional 头的关键字段全部现场验证Machine0x14C、NumberOfSections6、入口点实际地址0x444BD66 个节区表条目全部逐字段读出5 个命名为UPX!1 个为.rsrc文件末尾 6 字节解码为BF2000这些都只是事实还不是结论。5 个UPX!节区是否真的是 UPX 压缩BF2000这 6 个字符意味着什么下一篇开始回答。下一篇预告《字符串侦察从 find() 命中到编译器身份识别》——在不碰任何反汇编工具的前提下我们将展示如何通过字符串搜索和上下文分析从这个文件里挖出编译器遗留的源文件路径直接锁定程序使用的密码学算法组件。参考文献与引用Microsoft PE/COFF 格式官方规范learn.microsoft.com/windows/win32/debug/pe-format——本文所有字段偏移量和结构定义均依据此规范pefile — Python PE 解析库[github.com/erocarrera/pefile](https://github.com/erocarrera/pefile觉得有用点个关注持续获取优质内容。