带通配符的二进制特征码扫描效率优化技术问询
我现在需要把二进制文件转换成十六进制字符串,用来匹配用户提供的带通配符的特征码(?匹配任意单字符),逻辑和杀毒软件的特征码扫描一致,匹配成功返回true。但现在遇到两个大问题:一是通配符的高效处理,二是扫描速度太慢——用户提供的特征码有数千条,每条长度甚至能到200字符以上。比如下面这个识别C++编译文件的特征码:
55 8B EC 53 8B 5D 08 56 8B 75 0C 85 F6 57 8B 7D 10 ?? ?? ?? ?? ?? ?? ?? ?? ?? ?? ?? ?? ?? 01
这些不同长度的特征码都存在文件里。我当前用的代码功能正常,但和ExeInfoPE、Die这类工具比起来速度差太多:
static bool compare(string[] mask, byte[] buffer, int position) { var i = 0; // index foreach (string x in mask) // loop through the mask { if (x.Contains("?")) // is current mask position a wildcard? { // if so skip comparison } else if (byte.Parse(x, System.Globalization.NumberStyles.HexNumber) == buffer[position + i]) // else try to compare { // succeeded, move onto next byte } else return false; // failed, pattern not found ++i; // increment the index. } return true; // pattern was found }
请问怎么在保留通配符支持的前提下大幅提升扫描速度,让工具能用起来?
我来给你梳理几个核心优化方向,都是能直接落地提升速度的,毕竟ExeInfoPE这类工具也是靠这些思路堆出来的性能:
1. 先给特征码做「预处理」,砍掉重复的字符串解析开销
你的当前代码每次匹配都要做byte.Parse和x.Contains("?"),这在几千条特征码、循环几十万次的场景下开销极大。提前把所有特征码转换成**[字节值+匹配掩码]**的结构化数据:
- 把
"55"转成(0x55, true)(true表示需要精确匹配),把"??"转成(0x00, false)(false表示通配符,跳过匹配) - 一次性解析所有特征码到内存里,后续匹配直接用字节和掩码判断,完全避免字符串操作和重复解析
示例预处理代码(C#):
public struct PatternByte { public byte Value; public bool IsExactMatch; // false = 通配符 } public List<PatternByte[]> PreprocessPatterns(List<string> rawPatterns) { var processed = new List<PatternByte[]>(); foreach (var patternStr in rawPatterns) { var parts = patternStr.Split(' ', StringSplitOptions.RemoveEmptyEntries); var patternBytes = new PatternByte[parts.Length]; for (int i = 0; i < parts.Length; i++) { if (parts[i] == "??") { patternBytes[i] = new PatternByte { IsExactMatch = false }; } else { patternBytes[i] = new PatternByte { Value = byte.Parse(parts[i], System.Globalization.NumberStyles.HexNumber), IsExactMatch = true }; } } processed.Add(patternBytes); } return processed; }
2. 用多模式匹配算法替代单模式遍历,把复杂度砍到线性
你现在的逻辑应该是遍历二进制文件的每个位置,然后对每个特征码调用compare——这是O(NMK)的复杂度(N是文件长度,M是特征码数量,K是特征码平均长度),几千条特征码肯定慢。
Aho-Corasick算法专门解决多模式匹配问题:先把所有特征码构建成一个自动机,然后只需要遍历一次二进制文件,就能找出所有匹配的特征码,复杂度降到O(N + Z)(Z是匹配结果数量)。而且可以很容易适配通配符:在构建自动机的时候,把通配符节点标记为可跳过匹配,或者在状态转移时允许通配符匹配任意字节。
如果不想自己实现Aho-Corasick,也可以给每个特征码预处理KMP的失败函数,但Aho-Corasick在多模式场景下效率碾压单模式算法。
3. 优化匹配循环的底层细节,减少不必要的开销
- 把
foreach改成for循环:C#里foreach会产生枚举器的开销,对于高频循环,for的速度快很多 - 用
Span<byte>代替byte[]访问:Span<byte>是栈分配的,访问速度比堆分配的byte[]更快,而且可以直接映射二进制文件的内存区域,避免数组拷贝 - 提前计算特征码的最大长度:扫描二进制文件时,只需要遍历到
文件长度 - 最长特征码长度的位置,避免无效的越界检查和匹配
优化后的匹配函数示例:
static bool ComparePattern(PatternByte[] pattern, Span<byte> buffer, int position) { for (int i = 0; i < pattern.Length; i++) { if (pattern[i].IsExactMatch && pattern[i].Value != buffer[position + i]) { return false; } } return true; }
4. 并行化处理,充分利用多核CPU
- 可以按特征码长度分组,用多线程分别处理不同组的特征码扫描
- 或者把二进制文件分成多个块,每个块分配一个线程扫描,最后合并结果
- 注意:并行化要控制好粒度,小文件没必要并行,避免线程切换开销大于收益
5. 其他小技巧
- 跳过不可能匹配的位置:比如二进制文件里的连续0字节,直接跳过和特征码开头非0字节的匹配
- 缓存无通配符特征码的哈希值:先对比哈希再精确匹配,快速排除不匹配的情况
内容的提问来源于stack exchange,提问作者Karizan

