资讯动态

不靠特征库的查壳新思路:熵值、节区权限与入口点统计识别未知壳

发布时间:2026/10/16 1:43:59 来源:尧图企业网站定制
简介PEiD侦测未知壳加强版是面向逆向工程师与恶意代码分析人员的查壳工具用于识别可执行文件所用的壳类型、定位OEP入口并应对未知或新型加壳保护。压缩包共45个文件核心为PEiD.exe主程序配套大量DLL插件覆盖特征扫描、脱壳辅助、资源编辑、OEP查找、校验修复等功能、Borland运行时BPL组件、txt签名与用户自定义数据库以及以h/c形式提供的插件开发接口文件整体约2.48MB目录与插件结构清晰便于直接运行和后续扩展。已有234人学习下载。该加强版在原版基础上扩充未知壳特征库与高级扫描策略内含多种功能插件和SDK示例既可快速完成日常查壳分析也能为研究PEiD插件机制、编写私有签名规则提供可参考的实现样例对恶意样本识别与软件保护机制研究具有实用价值。1. 老工具的新活法PEiD侦测未知壳加强版在解决什么问题把样本拖进 PEiD界面回你一句 Nothing found后面跟着一个从没见过的节区名。PEiD侦测未知壳加强版要解决的不是某个新版本的壳库更新而是老工具根子上的盲区传统 PEiD 的“侦测”本质是拿已知壳的特征码去比对匹配不上就是未知壳再老的版本也拿它没办法。加强版的思路是把检测信号从“特征码”扩展到熵值、节区权限、入口点统计特征这些不依赖已知库的维度让“查不出”变成“能画像”。这条路线适合三类人手上常过陌生样本的逆向分析者、要给内部系统做加壳基线扫描的安全工程师以及想搞懂查壳原理而不是只会点按钮的初学者。接下来从 PEiD 的老逻辑讲起再给一套能跑的加强检测实现。2. 传统 PEiD 的查壳逻辑入口点特征码、Deep Scan 与结构盲区2.1 入口点特征码PEiD 最依赖的“指纹比对”PEiD 读 PE 文件时第一步是拿到 PE 头里的 AddressOfEntryPoint也就是程序入口点的 RVA。然后它把这个入口点附近的几十个字节抠出来和特征库 userdb.txt 里记录的签名逐字节比对。特征库里的每一条记录大致是“壳名 偏移位置 一串字节序列”的结构字节序列里用??表示通配示意如下[UPX 2.9x示意] EP0: 60 BE ?? ?? ?? ?? 8D BE ?? ?? ?? ?? 8D 9E ?? ?? ?? ?? 57 83 CD FF这条规则的含义是在入口点偏移 0 的位置第一个字节必须是 0x60对应 32 位下的 pushad 指令后面若干字节允许任意值但整体骨架固定。为什么 UPX、ASPack 这类壳能被秒查因为加壳工具生成的入口胶水代码几乎是模板化生产几代版本都改不掉开头的 pushad、取址、跳转套路。特征码匹配的优势是快、准、占用资源少缺点是脆弱——只要壳作者在入口处插入一个花指令字节整条特征码就全部失配界面立刻回到 Nothing found。这也是老 PEiD 使用者最常遇到的情况拿变种壳样本去查得到的是一个干净得让人心里发毛的结果。2.2 未知壳为什么会让老版本“失声甚至误报”特征库里没有签名PEiD 就无话可说。所谓未知壳不单指新发布的商业壳还包括三种很常见的变体第一种是壳作者在模板上做了字节级改动比如在入口处插花指令、调换两条指令的顺序第二种是自动化加壳工具每次生成时随机化入口代码第三种是那些主动反查壳工具的壳它们会检测自身是否被调试器或查壳软件打开故意把入口特征伪装成普通程序的样子。更麻烦的是老版本 PEiD 的特征库停更了很多年新出现的壳根本不在库内库里留下的老签名还会造成另一种翻车——匹配到旧特征的一个子串于是报出一个完全错误的名字。老 PEiD 能给出的信号获取方式面对未知壳时的表现已知壳特征签名userdb.txt 逐字节匹配变种一个字节不匹配就失声入口点代码片段入口点扫描花指令、VM 化后完全失真节区名UPX0、.aspack 等读取节表节名可伪造一句话绕过Overlay 附加数据文件尾部检测数字签名也会触发只能当参考这张表基本就是老 PEiD 的全部家底特征库、入口点代码、节区名、附加数据。前两个是“已知知识”驱动遇到新壳就归零后两个只是提示信号没法单独下结论。曾经有经验的分析者不只看壳名而是看入口点落在哪个节区、节区权限是否可写可执行这些从 PE 头里直接读出来的结构信息反而比特征库更耐用。这也就是加强版真正要补的位置把结构信息变成可量化的检测信号。2.3 Deep Scan 与其他辅助信号老版本仅有的立体手段PEiD 界面里其实还有一个 Deep Scan 模式中文习惯叫深扫。它和普通扫描的区别在于普通扫描只看入口点附近的特征Deep Scan 会在整个文件范围内分段扫描能照顾到入口点不在 PE 头标注位置的少数壳。但深扫的本质还是拿特征库去匹配扫的区间变大库还是那个库。除此之外PEiD 会在结果面板里展示入口点所在节区名、入口点文件偏移、Overlay 长度。这些原始信息基本没人细看但恰恰是后面做增强检测的起点。老 PEiD 给的是“已知特征库 少量结构提示”加强版补的是“结构统计 信息熵 指令分布”后者不依赖任何已知壳的签名理论上能覆盖所有变种和未知壳。3. 给未知壳画像熵值、节区权限与入口点统计特征3.1 区段熵值压缩壳的“高熵”痕迹信息熵衡量一段数据的混乱程度计算时以字节为单位统计每个字节值出现的概率套用香农熵公式H -Σ p(x)·log2(p(x))。8 位字节的理论最大熵是 8数据越接近随机分布熵值越高。正常编译出来的 C/C 代码操作码分布有明显的偏向性区段熵通常在 5.0 到 6.5 之间而 UPX、ASPack 这类压缩壳会把原始代码压缩后再加一层解压逻辑压缩后的数据近似随机熵值经常冲到 7.0 以上。这个信号之所以可靠是因为加壳的本质就是把原始代码变成“不可直接执行”的压缩或加密数据这个变换过程必然抬高熵值。区段类型常见熵值范围备注普通 C/C 代码节区5.0 ~ 6.5编译器生成的指令分布压缩壳数据区段7.0 ~ 7.8UPX 等压缩后近似随机加密或 VM 字节码区段7.5 ~ 8.0接近均匀分布导入表、资源数据4.0 ~ 6.0原始字节流较规则要注意一点熵值只看代码节区别把资源节、签名段、Overlay 拉进来算。很多加壳程序会把资源单独存放资源数据的熵本来就不低混在一起算会把信号抹平。实操里我只看 EP入口点所在节区的熵或者所有带代码标志的节区分别算哪个高取哪个做参考。3.2 入口点所在节区与节区权限加壳逃不掉的结构改动加壳必然要改动 PE 结构因为壳需要给自己腾出空间来存放解压代码和压缩后的原始程序。这种改动在节表层面会留下几个很难消除的痕迹。第一EP 通常被指向新加的节区而且这个节区往往是文件末尾的最后一个节正常程序入口应该落在 .text 或等价的代码节里很少出现在最末节。第二壳为了在运行时解压数据节区权限经常会设置成可读可写可执行同时存在也就是 RWX正常编译出来的代码节一般是 R-X数据节是 R-WR 和 W 和 X 三项同时亮起是强信号。第三节区名会变得可疑比如 UPX0、UPX1、.aspack但聪明的壳会伪造节名所以节名只能当辅助不能当主证据。这三个结构信号的特征码无法覆盖因为特征码只匹配字节序列不去管节区权限和 EP 位置。而且壳无论怎么变种总要给自己保留一个可写可执行的内存区域否则解压代码无处安放。就算壳作者把节区名改成 .text把权限伪装成 R-X运行时一旦要写内存就会露馅。所以结构信号在对抗变种壳时比特征码稳定得多。3.3 入口点代码统计pushad、call 与花指令的软证据特征码匹配要求“逐字节精确命中”变种壳只要插入一条花指令就全盘失配。换个思路不去匹配精确序列而是统计入口点附近指令的出现频次。加壳工具的入口胶水代码再怎么变核心动作是固定的——保存现场、取当前地址、跳转到解压循环、填充花指令。对应到字节上就是 pushad0x60、pushfd/pushfq0x9C、call0xE8、短跳 jmp0xEB、nop0x90这类操作码的高频出现。写一小段代码就能算def ep_opcode_hits(raw: bytes, ep_offset: int, window: int 128) - dict: chunk raw[ep_offset:ep_offset window] targets { pushad(0x60): bytes([0x60]), pushfd(0x9C): bytes([0x9C]), call(0xE8): bytes([0xE8]), jmp(0xEB): bytes([0xEB]), nop(0x90): bytes([0x90]), } return {name: chunk.count(op) for name, op in targets.items()}这段代码的思路是把 EP 附近的窗口切成固定长度然后数每个操作码在这个窗口里出现了几次。窗口默认 128 字节太小了统计波动大太大了会被无关代码稀释。逻辑上要注意统计的是“频次”而不是“序列”因为变种壳会调整指令顺序但操作码的整体分布很难改干净。一个正常程序的入口点call 和 nop 可能有一些但 pushad、pushfd 同时高频出现的情况极少而 UPX 这类 32 位壳的入口几乎必然有 pushad 开场的固定套路。另外有一个额外信号如果 EP 窗口里的字节本身熵值就高于 7.0说明入口代码是压缩或加密过的这比任何指令统计都更直接——壳的入口都藏起来了当然可疑。4. 落一个能跑的加强版检测器解析 PE、算熵、组合打分4.1 用 pefile 解析 PE 结构拿到 EP、节区与权限前面铺垫了三个信号现在做成一个可复用的 Python 检测器依赖只有一个第三方库 pefile。先写加载函数把 PE 头、入口点、节表、节区权限一次取全import math import pefile def section_entropy(data: bytes) - float: if not data: return 0.0 freq [0] * 256 for b in data: freq[b] 1 total len(data) h 0.0 for c in freq: if c: p c / total h - p * math.log2(p) return h def load_pe_info(path: str): pe pefile.PE(path, fast_loadTrue) ep_rva pe.OPTIONAL_HEADER.AddressOfEntryPoint ep_offset pe.get_offset_from_rva(ep_rva) bitness pe.OPTIONAL_HEADER.Magic # 0x10b 表示 32 位0x20b 表示 64 位 sections [] for sec in pe.sections: name sec.Name.rstrip(b\x00).decode(utf-8, replace) chars sec.Characteristics sections.append({ name: name or (无名节区), entropy: section_entropy(sec.get_data()), is_code: bool(chars 0x00000020), readable: bool(chars 0x40000000), writable: bool(chars 0x80000000), executable: bool(chars 0x20000000), last: False, }) sections[-1][last] True return pe, ep_rva, ep_offset, bitness, sections这段代码的要点在pe.get_offset_from_rva(ep_rva)它把入口点从 RVA 转成文件偏移这一步是后面所有字节级分析的前提。很多人手写转换时直接用节表的 PointerToRawData 加偏移遇到 PE 头里有 SectionAlignment 和 FileAlignment 不一致的情况就会算错统一交给 pefile 处理最稳。fast_loadTrue只解析头信息不加载区段数据性能开销小。sec.get_data()直接返回该节区的原始字节正好喂给熵值计算。最后把last标记打在最后一个节区上用来判断 EP 是否落在文件末尾节区。调用方记得在finally里执行pe.close()避免文件句柄泄漏。4.2 区分 32/64 位入口点指令窗口统计的完整版上一章的统计代码是 32 位视角直接用在 64 位程序上会出问题0x60 在 64 位模式下并不是有效的 pushad 指令统计它没有意义。完整版需要根据 PE 头里的 Magic 字段分流def ep_opcode_hits_full(raw: bytes, ep_offset: int, bitness: int, window: int 128) - dict: chunk raw[ep_offset:ep_offset window] if bitness 0x10b: # 32 位 PE targets { pushad(0x60): bytes([0x60]), pushfd(0x9C): bytes([0x9C]), call(0xE8): bytes([0xE8]), jmp(0xEB): bytes([0xEB]), nop(0x90): bytes([0x90]), } else: # 64 位 PE targets { pushfq(0x9C): bytes([0x9C]), call(0xE8): bytes([0xE8]), jmp(0xEB): bytes([0xEB]), nop(0x90): bytes([0x90]), } return {name: chunk.count(op) for name, op in targets.items()}参数说明window决定统计窗口长度128 字节是默认值对大多数壳足够如果样本入口处有大量花指令填充可以调大到 256让统计更平滑。bitness直接取Magic值比较不需要额外解析。逻辑说明64 位下不再统计 pushad因为 0x60 在 x64 下是无效指令正常程序入口不该出现它如果 64 位样本的 EP 窗口里出现了大量 0x60那这本身就是异常信号可以直接拉高嫌疑分。另外补充一个判断如果chunk本身的熵值大于 7.0说明入口代码处于加密或压缩状态这种“入口被藏起来”的情况比任何指令统计都更有说服力。4.3 组合打分与报告输出把三个信号合成一个结论三个信号单独看都有误差组合起来才能用。我的做法是给每个信号分配权重算出总分后按阈值判定。默认权重如下信号默认权重打分规则EP 所在节区熵值0.35熵 7.0 给 1.05.5 ~ 7.0 按比例低于 5.5 给 0EP 落在末尾节区0.30是给 1.0否则 0节区 RWX 权限0.20EP 节区同时可写可执行给 1.0入口点指令统计0.15命中 2 类以上指令才给 1.0def find_ep_section(sections, ep_offset): for sec in sections: start sec[raw] if raw in sec else 0 # 简化判断上面 load_pe_info 未保存 raw 范围这里补一个 return None def analyze(path: str): raw open(path, rb).read() pe, ep_rva, ep_offset, bitness, sections load_pe_info(path) try: ep_sec None for sec in pe.sections: start sec.PointerToRawData size max(sec.Misc_VirtualSize, sec.SizeOfRawData) if start ep_offset start size: ep_sec sections[pe.sections.index(sec)] break sig {entropy: 0.0, ep_last: 0.0, rwx: 0.0, opcodes: 0.0} if ep_sec: ent ep_sec[entropy] sig[entropy] 1.0 if ent 7.0 else max(0.0, (ent - 5.5) / 1.5) sig[ep_last] 1.0 if ep_sec[last] else 0.0 sig[rwx] 1.0 if ep_sec[writable] and ep_sec[executable] else 0.0 hits ep_opcode_hits_full(raw, ep_offset, bitness) hot [v for v in hits.values() if v 2] sig[opcodes] min(1.0, len(hot) / 3.0) weights {entropy: 0.35, ep_last: 0.30, rwx: 0.20, opcodes: 0.15} total sum(weights[k] * sig[k] for k in weights) return total, sig, ep_sec, hits finally: pe.close()这段组合代码里ep_sec的定位用的是文件偏移落在节区原始数据范围内这个规则避免引入复杂的 RVA 换算。sig字典存四个信号的归一化分值每个都压到 0 到 1 之间。权重总和是 1.0总分也自然落在 0 到 1 之间。判定经验值总分大于 0.6 判定为高嫌疑0.4 到 0.6 之间是中嫌疑低于 0.4 暂不处理。输出时把 EP 节区名、熵值、RWX 状态、指令统计一起打出来方便人工二次确认。这一步做完一个不依赖任何已知壳签名的“未知壳侦测器”就能用了。5. 避坑指南阈值调优与误报排查的五个常见问题5.1 高熵误报Qt 和 Electron 程序为什么总是“高嫌疑”现象把内部开发的一个 Qt 桌面程序或 Electron 打包的程序扔进检测器几乎所有样本都报“加壳嫌疑高”得分普遍在 0.7 以上。原因Qt 程序的大量逻辑集中在编译后的代码段里部分代码段经过编译器优化后字节分布比较散熵值本身就接近 7.0Electron 程序体内嵌了 asar 打包数据和 V8 引擎快照这些数据段的熵值天然很高。如果检测器对所有节区都算熵或者只看全文件平均熵必然误报。解决第一熵只看 EP 所在节区不要看全文件第二排除明显的资源节、数据节只保留带代码标志IMAGE_SCN_CNT_CODE的节区参与判断第三把熵信号的高阈值从 7.0 提到 7.3。我实际用下来正常程序 EP 代码节的熵很少超过 7.27.3 这条线能把绝大多数误报挡掉。5.2 变种壳漏报改一个字节就让旧特征彻底失声现象拿 UPX 加壳的样本手动修改入口点的一个字节后老 PEiD 立刻显示 Nothing found加强版检测器的得分也掉到 0.3 左右明明壳还在。原因特征码匹配要求逐字节精确命中一个字节不匹配就整体失败。加强版的指令统计虽然不依赖精确序列但如果你把统计窗口缩得太小或者只统计少数几个操作码变种样本一样能绕过。解决统计窗口不能低于 128 字节最好同时对 256 字节窗口做一次交叉验证操作码列表里加入 0xEB短跳和 0x90nop这类花指令常用的字节变种壳特别喜欢用它们填充入口。另外坚持“多个信号同时看”的原则EP 节区熵和 RWX 权限这两个结构信号不会因为入口改动而消失只要 EP 还落在末尾节区总分就不会太低。5.3 低熵漏报VM 壳和混淆壳的熵值并不总是很高现象某商业壳加壳的样本全文件熵只有 6.2EP 节区熵 6.8检测器判定为“低嫌疑”但实际是个保护强度很高的 VM 壳。原因VM 壳把原始指令虚拟化成自定义字节码这些字节码的分布和普通代码差不多不会像压缩壳那样出现 7.5 以上的高熵混淆壳则会把代码展开成大量等概率的分支块熵值同样被拉低。把熵当唯一指标就会漏掉这类壳。解决对熵值偏低的情况补充两个替代信号——导入表异常和 EP 代码风格。VM 壳通常会抽空导入表把 API 调用集中到自己维护的一张表里导致导入函数数量少得可疑EP 处如果直接跳到一段类似 dispatcher 的分发循环也是强信号。我在权重表里给熵值降权到 0.25给 EP 节区位置升权到 0.35对 VM 壳的检出率明显更好。5.4 偏移换算错误把 RVA 当文件偏移用的经典翻车现象用第 4 章的脚本检测一个壳样本EP 明明显示 0x1000打印出来要分析的字节却落在完全无关的数据区指令统计全是 0。原因PE 头里的 AddressOfEntryPoint 是 RVA不是文件偏移。RVA 是相对内存基址的虚拟地址必须减去所在节区的 VirtualAddress再加上 PointerToRawData才能得到文件里的真实偏移。这里换算错一位后面所有分析全歪。解决不要手算统一用pe.get_offset_from_rva(ep_rva)。如果你实在要手算也要先确认节区的 VirtualAddress 和 PointerToRawData 之间的差值再用ep_rva - VirtualAddress PointerToRawData计算。调试时先打印 EP 所在节区的名字如果 EP 显示落在 .text 但换算后偏移指向了 .rsrc那一定是换算写错了。5.5 运行时库误报带运行时库的开发工具程序被当成壳现象某种带运行时库的开发工具编译出来的程序本身没加壳但检测器报“加壳嫌疑高”查看节区发现有一个高熵的运行时数据段指令统计里也出现了疑似壳特征的操作码。原因这类程序会把运行时库一并打包进可执行文件运行时库的初始化代码有自己的入口套路和大量间接跳转从统计特征上看和壳的初始化代码高度相似再加上运行时数据段熵值偏高三个信号里中了两个就被误报了。解决对这类样本做“双特征”判断——只有同时满足“EP 节区熵高”和“EP 落在末尾节区”才给高嫌疑单纯的高熵加指令统计最多给到中嫌疑。另外建立一份白名单节区名比如运行时库自带的固定节区名直接跳过熵计算。这种误报很难彻底消除因为信号层面确实和壳太像只能靠阈值收紧和人工复核兜底。6. 验证与进阶技巧用样本集校准阈值用 OEP 熵变化确认检测器写完不算完阈值定多少直接决定好不好用。我的习惯是先收集一套小样本集10 个已知加壳样本覆盖 UPX、ASPack、某种商业壳的变种加上 10 个干净程序跑一遍打分再根据结果调权重和阈值。比如把结果导出成 JSON写几行代码统计漏报和误报import json results json.load(open(results.json)) packed [r for r in results if r[label] packed] clean [r for r in results if r[label] clean] tp sum(1 for r in packed if r[score] 0.6) fp sum(1 for r in clean if r[score] 0.6) print(f召回率 {tp}/{len(packed)}, 误报 {fp}/{len(clean)})这一步很值得做我最早把阈值拍脑袋定在 0.6结果拿一套真实样本一跑误报率三成被打脸打得很疼。后来养成的习惯是每次调完阈值都要把样本集重新跑一遍保证召回率不掉到 90% 以下才敢给别人用。还有一个手工确认的进阶技巧加壳程序运行时壳会先解压或解密然后跳转到原始入口点 OEP。用调试器在 OEP 处下断把内存 dump 下来再把这堆内存数据扔进检测器你会发现原来的高熵节区熵值从 7.x 回落到 5.xRWX 权限也消失。这个“熵值回落”现象是加壳最硬的证据动态分析时屡试不爽。没条件动态分析时就退而求其次对比 EP 节区熵和所有代码节区的平均熵前者明显高于后者也值得手动跟进。这套加强版方案从一开始就不是为了替代老 PEiD而是给老工具补上“特征库之外”的视角。特征码负责快准狠地识别已知壳熵值和结构信号负责兜住未知壳和变种壳。两条腿走路查壳这件事才算真正落地。希望帮到你。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑