-
-
[原创]PE分析小工具开发流水账(二)
-
发表于: 2025-12-7 14:15 890
-
最近随便看了一下其他类似工具的处理,我修改了一个程序的MZ头然后把它放到不管是CFF Explorer也好还是PEview也好,感觉都是采用的保守式分析。众所周知,MZ签名在最开头,当MZ签名被修改后,后面就不进行分析了,但是依然还是合法文件,而不是直接报错不是可执行文件不能分析。然后又试了一下改magic字段看看咋样,也是只分析到前面一些字段,后面保守式仅显示十六进制数据。
emmm,之所以想看看其他工具怎么处理,就是因为开发其实也遇到一点问题,就是在极端情况下假设一些关键字段被修改(虽然其实现实中可能没什么意义),但还是要考虑一些奇奇怪怪的情况。下面还是梳理一下最近又遇到的一些问题。
问题一:可选头分析的依赖问题
可选头之所以叫可选头我想就是因为它是分类讨论的,大概有32位、64位还有ROM架构的这个问题。目前我想先不考虑ROM架构,只做一个最小可执行版的。但是加载一个文件时怎么知道选择哪个结构呢,或者说,我在静态分析时怎么知道我要按照哪个结构分析呢。magic字段是最有说服力的,但是也不排除它的错误,归根结底就是,不能因为一个关键字段的修改而放弃后续的分析。但是其实这个架构问题,并不仅仅只是涉及到这一个字段,像是machine值啊,在合法情况下也要和这个东西匹配。所以我想留一个启发式分析的空给这种反推式分析结构。但是当多个字段出现错配时,这种分析方式也无能为力。所以就引发了下一个问题。
问题二:联合检验和反推式验证平衡点
除了在可选头中出现这种联合性检验问题,还有就是节区头的检验问题。一个健壮的解析器大概是不能仅仅依赖于关键字段的,比如NumberOfSections这样的字段。现实的恶意软件中,可能出现跳转执行这样的情况吧,万一后面还有节区咋办。然后我现在想的就是自己检验这个节区是不是节区,但是怎么检验呢,大概是根据里面字段的合法性检验。首先一个问题就是数量,我想的就是每40个字节一读取,然后看看它是不是,但是万一哪个什么奇怪的文件它搞个1000个节区咋搞,虽然它的NumberOfSections可能只写了几个。然后我设定了一个最大值128,如果它真搞1000个那这也解析不了,就报错算了。可现实就是某些壳啊什么的本来就不会改的所有值都合法,比如Name字段啊搞一点奇奇怪怪的值进去。既然知道现实中这个情况,那又怎么保证检验的成功性呢,可能需要置信度来判断吧,这个还在解决中。
问题三:节区属性判断(Characteristic字段处理)问题
关于属性问题,其实作为解析工具重点肯定不是放在像加载器一样搞清楚它到底是什么属性,但是对于某些节区,比如text段不应该有可执行属性还是需要诊断的。除了常见值,Characteristic字段问题就在于它有很多组合,按位计算,出现常见值容易解决,但是非常见值应该怎么处理。就类似于一个莫名其妙的值,可能被解析为可执行,怎么判断呢,这个部分也在设计中。目前想到的是分析以下几个关键属性。
struct section_imformation {
bool mem_execute_ = false; // 内存可执行
bool mem_read_ = false; // 内存可读
bool mem_write_ = false; // 内存可写
bool mem_shared_ = false; // 内存共享
bool cnt_code_ = false; // 包含可执行代码
bool cnt_initialized_data_ = false; // 包含已初始化数据
bool cnt_uninitialized_data_ = false; // 零初始化
……
}----------------------------------------分割线 以下内容为2026/2/27更新
最近发现之前的结构太乱了,所以把诊断的中文输出全部以固定格式存储起来了。过去设计的数据结构大概是,一个结构体记录每一块区域的内容,分类成偏移、错误信息、普通信息这一类的东西,然后直接以字符串形式存储。现在诊断部分差不多能把头部关键的地方诊断了,想加个界面先做个最小可执行的程序,但是在加界面时第一个问题就是编码问题,以字符串存储占位置不说还很麻烦。在修改中,新的设计想法是,把原本的一句话拆成几个内容,以模板形式存储,少量字符串也以英文形式存起来,这样在未来加界面时不仅可以直接根据整个结构来进行翻译,而且还将导出功能变成了可能。可以做到界面和程序本体分离。下面就是我想到的新结构。
namespace Core {
// 分析对象
enum class Object : uint8_t {
SIGNATURE, // 签名
FIELD, // 字段本身
ADDRESS_IN_FIELD, // 根据字段计算出的地址(适用于简单情况)
STRUCTURE, // 结构
IMFORMATION_IN_FIELD, // 字段所示的信息
STRUCTURE_ADDRESS // 结构地址(适用于复杂情况)
};
// 严重程度
enum class Severity : uint8_t {
INFO_LOW, // 普通信息
SUSPICIOUS, // 可疑
WARNING_MED, // 警告
ERROR_HIGH // 错误
};
// 包含诊断信息
enum class DiagCategory : uint8_t {
/* 签名(SIGNATURE)、字段本身(FIELD) */
VALUE_MISMATCH, // 【{severity}】{description} -> {field_name}字段异常,期望/阈值/参考值:{expected},实际值:{actual}
INVALID_VALUE, // 【{severity}】{description} -> {field_name}字段值无效,实际值:{actual}
/* 根据字段计算出的地址(ADDRESS_IN_FIELD) */
EXCURSION_ANOMALY, // 【{severity}】{description} -> {field_name}所示地址异常,值:0x{address}
ADDRESS_OUT_OF_RANGE, // 【{severity}】{description} -> {field_name}地址超过文件/内存范围,值:0x{address}
/* 结构(STRUCTURE) */
ABNORMAL_LENGTH, // 【{severity}】{description}长度异常,实际长度:{actual}字节
STRUCTURE_MISSING, // 【{severity}】{description}区域缺失
/* 字段所示的信息(IMFORMATION_IN_FIELD) */
DETAILED_INFORMATION, // 【{severity}】{description} -> {field_name}:{info1}
/* 结构地址(STRUCTURE_ADDRESS) */
REGULAR_ISSUE, // 【{severity}】{description}{info1}
INDEXED_ISSUE, // 【{severity}】{description}[{index}]{info1}
/* 其他问题 */
RELATIONSHIP_ISSUE, // 【{severity}】{description} -> {field_name}与{compared_description} -> {compared_field_name}:{info1}
ADDITIONAL_INFORMATION // {info2}
};
// 对象诊断结果所在背景以及包含的诊断信息
struct Diagnostic {
Object object; // 诊断对象
Severity severity; // 严重程度
DiagCategory category; // 诊断类别(用哪种模板)
/* 提示信息 */
std::string info1; // 提示信息1
std::string info2; // 提示信息2
/* 基础参数 */
std::string field_name; // 字段名,如"PE Signature"
std::string description; // 结构名,如"File Header"
uint64_t offset; // 文件偏移位置
/* 字段值问题 */
uint64_t expected_value; // 期望值/阈值/参考值,如"0x00004550"
uint64_t actual_value; // 实际值,如"0x12345678"
/* 地址问题 */
uint64_t address; // 地址值,如"0x00400000"
/* 其他信息 */
uint64_t index; // 索引,如节区头索引0、1、2...
std::string compared_field_name; // 相关字段名,如"SizeOfHeaders"
std::string compared_description; // 相关结构名,如"Optional Header"
};
}
一点个人的歪点子,欢迎讨论交流。
[培训]Windows内核深度攻防:从Hook技术到Rootkit实战!