关于模糊哈希(Fuzzy Hashing)算法,目前网上有几篇中文资料介绍,但均不准确。写这篇文章以纠正,并对其原理和应用作详细的介绍。

一、概述

模糊哈希算法又叫基于内容分割的分片分片哈希算法(context triggered piecewise hashing, CTPH),主要用于文件的相似性比较。

2006年,Jesse Kornblum [1] 提出CTPH,并给出一个名为spamsum的算法实例。随后,Jason Sherman开发了ssdeep [2] 工具以实现这一算法。该算法最初用于取证,后来被用于恶意代码检测,最近又有用于开源软件漏洞挖掘等。

模糊哈希的主要原理是,使用一个弱哈希计算文件局部内容,在特定条件下对文件进行分片,然后使用一个强哈希对文件每片计算哈希值,取这些值的一部分并连接起来,与分片条件一起构成一个模糊哈希结果。使用一个字符串相似性对比算法判断两个模糊哈希值的相似度有多少,从而判断两个文件的相似程度。

对文件的部分变化(包括在多处修改、增加、删除部分内容),使用模糊哈希均能发现与源文件的相似关系,是目前判断相似性较好的一种方法。


二、背景

一个文件也许有意或无意地产生变化。例如,有意的情况有作者改动文本内容、恶意代码自动变化;无意的情况有传输出错、磁盘存储出错等。如何有效判断两个文件是否相似,从而是同源的?这个问题在很多领域都有遇到。

最朴素的方法是逐个字节对比,并统计相同字节占文件大小的比例。这存在两个问题:1、对n个文件寻找相似关系,每个文件需要读取和对比n-1次;2、如果在一个文件中插入或删除一个字节,这种方法得到的相似结果也许很糟糕。

先说第一个问题。自然的想法是对每个文件生成较短的“指纹”,然后不一一比较文件本身,而是一一比较指纹。这样来降低比较量,提高效率。显然,传统的哈希算法(尤其是密码学意义上的哈希)正好可以充当这种“指纹”。

传统哈希也会面临前面提到的第二个问题。此外,即便是只修改文件中的一个字节,得到的结果也会大不相同(这是哈希算法的基本要求)。

为了解决后者,Nick Harbour提出所谓的分片哈希(piecewise hashing)算法,并在dcfldd [3] 中进行了实现。其策略非常简单,即每隔一个固定间隔就将文件分片,对每片计算一个哈希值,将这些哈希值一起作相似比较。显然,局部的修改只会影响少量分片的哈希结果,因此从整个文件看还是会有较高的相似性(虽然不是100%)。

这种方法依然面临前面的一个问题,即在一个文件中插入或删除一个字节,就会失效。

如何降低这种局部变化带来的全局影响?Kornblum提出了模糊哈希算法,其思想是:不再固定长度分片,而使用文件局部数据的特点来决定是否分片,使得局部的变化(包括修改、增加、删除等)只影响局部的分片方法,而不会将影响扩散至其他分片点。

三、算法的组成方法

一个模糊哈希算法由以下部分组成:

  1. 一个弱哈希算法,以及一个分片值,用于分片
  2. 一个强哈希算法,用于计算每片的哈希
  3. 一个压缩映射算法,将每片的哈希值映射为一个更短的值
  4. 一个比较算法,用于对两个模糊哈希值计算相似程度

3.1 分片

在文件中读取一部分内容,给弱哈希算法计算,得到一个哈希值。

通常逐字节读取固定长度的内容,就像网络协议中的滑动窗口一样在文件中以固定窗口滑动,每次都对窗口内的内容计算。因此,为了便捷,通常采用滚动哈希算法(rolling hashing)。这里的滚动哈希是指,比如原来已经计算了abcdef的哈希值h1,接下来要计算bcdefg的哈希值,不需要完全重新计算,只需要h1 – X(a) + Y(g)即可。其中X、Y是两个函数,即只需要相应增减差量对哈希值的影响即可。这种哈希大大可以加快分片判断的速度。

例如,在ssdeep中,使用Alder-32 [4] 算法作为弱哈希。它实际是一种用于校验和的弱哈希,类似于CRC32,不能用于密码学算法,但计算快速,生成4字节哈希值,并且是滚动哈希。

除了弱哈希算法,还需要一个分片值,例如n,由它来控制分片条件。在ssdeep中,n的值根据其他条件决定(后面再介绍)。

当Alder-32哈希值除以n的余数恰好等于n-1时,就在当前位置分片;否则,不分片,窗口往后滚动一个字节,然后再次计算Alder-32哈希值并判断,如此继续。

3.2 每片求哈希

将文件分片完以后,就可以对每片分别计算哈希了。可以使用传统的哈希算法,例如MD5。在ssdeep中,使用一个名为Fowler-Noll-Vo hash [5]的哈希算法。

3.3 压缩映射

对每一个文件分片,计算得到一个哈希值以后,可以选择将结果压缩短。例如,在ssdeep中,只取FNV哈希结果的最低6位,并用一个ASCII字符表示出来,作为这个分片的最终哈希结果。

显然,这种压缩映射会损失一部分准确性,引入误报问题。可以选择不做压缩映射,而存储较长的哈希值。不过从实践情况来看,做压缩映射的问题也不大。

3.4 连接哈希值

将每片压缩后的哈希值连接到一起,就得到这个文件的模糊哈希值了。如果分片条件参数n对不同文件可能不同,还应该将n纳入模糊哈希值中。

3.5 比较哈希值

最后的问题是,对两个文件的模糊哈希值,怎么样比较它们是不是相似?可以采用传统的相似性比较算法。

在ssdeep中,采用的如下思路。由于ssdeep对每一片得到的哈希值是一个ASCII字符,最终得到的文件模糊哈希值就是一个字符串了。假设是s1、s2,将s1到s2的“加权编辑距离”(weighted edit distance)作为评价其相似性的依据。

这里的加权编辑距离是指,先判断从s1变为s2,最少需要多少步操作(包括插入、删除、修改、交换),然后对不同操作给出一个权值,将结果加起来,即得是加权编辑距离。

接下来,ssdeep将这个距离除以s1和s2的长度和,以将绝对结果变为相对结果,再映射到0-100的一个整数值上,其中,100表示两个字符串完全一致,而0表示完全不相似。

这样,最后就得到的相似程度的评分,可以用来判断两个文件是否有相似关系。在实践中,一般将ssdeep的结果为1或以上认为有相似性,而将结果为0认为是不相似。

四、解决问题的原理

我们来看一下模糊哈希为什么能有效比较相似性。

如果在一个文件中修改一个字节,有几种情况:

1、这个字节原来不影响分片,改动后也不影响分片,则这次修改只会影响一个分片哈希,对全局的影响微乎其微,最后相似结果极高;

2、这个字节原来不影响分片,改动后影响分片,则这次修改会影响两个分片哈希,并且造成两个文件的模糊哈希值不一样长,但相似性比较算法允许这种差异(无非是改动一个加上插入一个字母),其他大部分结果依然一致,因此相似结果依然很高;

3、这个字节原来影响分片,改动后不影响分片,与情况2类似;

4、这个字节原来影响分片,改动后依然影响分片,与情况1类似。

如果在一个文件中增加一个字节,同样有上述四种情况,但与上述逻辑类似,同样的对两个文件的模糊哈希值影响极小。如果在文件中删除一个字节,也与此类似。

因此,我们可以看到,在模糊哈希中,通过恰当的分片策略,以及相似度比较算法,可以有效地将细节变化对全局结果的影响限制在局部,从而对最终相似性作出有效的判断。事实上,即便是改动连续几个字符,或者作多处改动,模糊哈希算法依然可能作出有效的判断。

最神奇的是,对一篇文章,与它的一个片段(例如一个段落),通过模糊哈希也能判断出它们的相似性。这种独特的性质会非常有用!

五、ssdeep的其他改进

在实际使用中,还是存在一个问题——如果分片数量太小,例如整个文件只有一处触发了分片条件,或者干脆就没有触发分片条件,那最后得到的模糊哈希值干脆只有少数几个字节,这时候退化成了传统的全文哈希了,怎么办?

ssdeep精明地解决了这个问题。它根据文件长度和文件实际内容来决定如何分片。这是通过调整分片条件n的值来实现的。

我们来想一下,即便对弱哈希,都有这样的性质,即产生的结果在其可能空间上是接近于均匀分布的。在ssdeep中,n的值始终取2的整数次方,这样Alder-32哈希值除以n的余数也接近于均匀分布。仅当余数等于n-1时分片,就相当于只有差不多1/n的情况下会分片。也就是说,对一个文件,窗口每移动一次,就有1/n的可能要分片。如果某一次分的片数太小,那就减小n的值,使每次分片的可能性增加,增大片数。而如果觉得分的片太多,就增大n的值,使每次分片的可能性减少,降低片数。在ssdeep中,每次都是将n除以或者乘以2,来调整,使最终的片数尽可能在32到64之间。

实际上,由于分片的可能性差不多是1/n,所以每次ssdeep运行时,第一次尝试的n值就是一个接近于文件长度/64的值。事实表明效果不错。这反过来说明Alder-32的结果分布比较均匀。

因为n的值可能与文件长度、文件内容有关,因此ssdeep中,n的值会作为一个最终结果的一部分出现。

上述策略下,另一个问题出现了。这是一种比较极端的情况。假设一个文件使用的分片值n。在该文件中改动一个字节(修改、插入、删除等),且这个改动影响了分片的数量,使得分片数增加或减少,超出了边界范围,因此程序决定调整一下n,例如把n乘以或者除以2。因此,即便对文件的一个字节改动,也可能导致分片条件n的变化,从而导致分片数相差近一倍,而得到的结果显然也毫无相似性了。怎么办?

ssdeep解决了这种问题。对每一个文件,它同时使用n和n/2作为分片值,算得两个不同的模糊哈希值,而这两个值都使用。因此,最后得到的一个文件的模糊哈希值是:

n:h(n):h(n/2)

而在比较时,如果两个文件的分片值分别为n和m,则判断是否有n==m, n==2m, 2n==m三种情况,如果有之一,则将两者相应的模糊哈希值进行比较。例如,如果n==2m,则比较h(n/2)与h(m)是否相似。这样,在一定程序上解决了分片值变化的问题。

六、应用

模糊哈希最初应用于计算机取证,随即,反病毒领域发现了它的妙处,试图将其用于恶意代码的变种检测,这些工作包括[6] 、[7]和[8]。然而根据[8],直接使用的效果并不佳,应考虑与其他方法配合使用。[9]对模糊哈希用于恶意代码检测有科普性的介绍。

然而,在源码相似性比较上,模糊哈希表现出较好的准确性,因此被用于开源代码漏洞挖掘。在现实中,大量的开源项目使用第三方开源库,而又这些库引入的漏洞并不同步更新,因此存在潜在的安全威胁。于此有关的一些工作[10]使用了模糊哈希来判断开源项目之间的相互引用关系,并取得了杰出的成绩。

七、进一步工作

对模糊哈希的主要问题存在于两个方面,首先就是其准确性。

一是其并非一种精确判断,然而事实上对“相似性”的定义本来就不固定,而且就是一种不精确的概念。在目前,模糊哈希是一种相对较好的通用文件相似性比较方法。

二是对其的专门攻击,在计算机取证中,这种攻击有可能产生一些问题。在[11]中对此有专门的研究。

另一方面则是其性能表现上。ssdeep并不适合于进行大规模计算,例如,为了计算得到一个合适的分片值n,也许要多次读取全文并作大量计算。以ssdeep为代表的模糊哈希算法从设计和实现技巧两方面均存在速度问题。[12]对此做了改进和优化,得到了更好的性能和准确性。


关于模糊哈希(Fuzzy Hashing)算法,目前网上有几篇中文资料介绍,但均不准确。写这篇文章以纠正,并对其原理和应用作详细的介绍。

一、概述

模糊哈希算法又叫基于内容分割的分片分片哈希算法(context triggered piecewise hashing, CTPH),主要用于文件的相似性比较。

2006年,Jesse Kornblum [1] 提出CTPH,并给出一个名为spamsum的算法实例。随后,Jason Sherman开发了ssdeep [2] 工具以实现这一算法。该算法最初用于取证,后来被用于恶意代码检测,最近又有用于开源软件漏洞挖掘等。

模糊哈希的主要原理是,使用一个弱哈希计算文件局部内容,在特定条件下对文件进行分片,然后使用一个强哈希对文件每片计算哈希值,取这些值的一部分并连接起来,与分片条件一起构成一个模糊哈希结果。使用一个字符串相似性对比算法判断两个模糊哈希值的相似度有多少,从而判断两个文件的相似程度。

对文件的部分变化(包括在多处修改、增加、删除部分内容),使用模糊哈希均能发现与源文件的相似关系,是目前判断相似性较好的一种方法。


二、背景

一个文件也许有意或无意地产生变化。例如,有意的情况有作者改动文本内容、恶意代码自动变化;无意的情况有传输出错、磁盘存储出错等。如何有效判断两个文件是否相似,从而是同源的?这个问题在很多领域都有遇到。

最朴素的方法是逐个字节对比,并统计相同字节占文件大小的比例。这存在两个问题:1、对n个文件寻找相似关系,每个文件需要读取和对比n-1次;2、如果在一个文件中插入或删除一个字节,这种方法得到的相似结果也许很糟糕。

先说第一个问题。自然的想法是对每个文件生成较短的“指纹”,然后不一一比较文件本身,而是一一比较指纹。这样来降低比较量,提高效率。显然,传统的哈希算法(尤其是密码学意义上的哈希)正好可以充当这种“指纹”。

传统哈希也会面临前面提到的第二个问题。此外,即便是只修改文件中的一个字节,得到的结果也会大不相同(这是哈希算法的基本要求)。

为了解决后者,Nick Harbour提出所谓的分片哈希(piecewise hashing)算法,并在dcfldd [3] 中进行了实现。其策略非常简单,即每隔一个固定间隔就将文件分片,对每片计算一个哈希值,将这些哈希值一起作相似比较。显然,局部的修改只会影响少量分片的哈希结果,因此从整个文件看还是会有较高的相似性(虽然不是100%)。

这种方法依然面临前面的一个问题,即在一个文件中插入或删除一个字节,就会失效。

如何降低这种局部变化带来的全局影响?Kornblum提出了模糊哈希算法,其思想是:不再固定长度分片,而使用文件局部数据的特点来决定是否分片,使得局部的变化(包括修改、增加、删除等)只影响局部的分片方法,而不会将影响扩散至其他分片点。

三、算法的组成方法

一个模糊哈希算法由以下部分组成:

  1. 一个弱哈希算法,以及一个分片值,用于分片
  2. 一个强哈希算法,用于计算每片的哈希
  3. 一个压缩映射算法,将每片的哈希值映射为一个更短的值
  4. 一个比较算法,用于对两个模糊哈希值计算相似程度

3.1 分片

在文件中读取一部分内容,给弱哈希算法计算,得到一个哈希值。

通常逐字节读取固定长度的内容,就像网络协议中的滑动窗口一样在文件中以固定窗口滑动,每次都对窗口内的内容计算。因此,为了便捷,通常采用滚动哈希算法(rolling hashing)。这里的滚动哈希是指,比如原来已经计算了abcdef的哈希值h1,接下来要计算bcdefg的哈希值,不需要完全重新计算,只需要h1 – X(a) + Y(g)即可。其中X、Y是两个函数,即只需要相应增减差量对哈希值的影响即可。这种哈希大大可以加快分片判断的速度。

例如,在ssdeep中,使用Alder-32 [4] 算法作为弱哈希。它实际是一种用于校验和的弱哈希,类似于CRC32,不能用于密码学算法,但计算快速,生成4字节哈希值,并且是滚动哈希。

除了弱哈希算法,还需要一个分片值,例如n,由它来控制分片条件。在ssdeep中,n的值根据其他条件决定(后面再介绍)。

当Alder-32哈希值除以n的余数恰好等于n-1时,就在当前位置分片;否则,不分片,窗口往后滚动一个字节,然后再次计算Alder-32哈希值并判断,如此继续。

3.2 每片求哈希

将文件分片完以后,就可以对每片分别计算哈希了。可以使用传统的哈希算法,例如MD5。在ssdeep中,使用一个名为Fowler-Noll-Vo hash [5]的哈希算法。

3.3 压缩映射

对每一个文件分片,计算得到一个哈希值以后,可以选择将结果压缩短。例如,在ssdeep中,只取FNV哈希结果的最低6位,并用一个ASCII字符表示出来,作为这个分片的最终哈希结果。

显然,这种压缩映射会损失一部分准确性,引入误报问题。可以选择不做压缩映射,而存储较长的哈希值。不过从实践情况来看,做压缩映射的问题也不大。

3.4 连接哈希值

将每片压缩后的哈希值连接到一起,就得到这个文件的模糊哈希值了。如果分片条件参数n对不同文件可能不同,还应该将n纳入模糊哈希值中。

3.5 比较哈希值

最后的问题是,对两个文件的模糊哈希值,怎么样比较它们是不是相似?可以采用传统的相似性比较算法。

在ssdeep中,采用的如下思路。由于ssdeep对每一片得到的哈希值是一个ASCII字符,最终得到的文件模糊哈希值就是一个字符串了。假设是s1、s2,将s1到s2的“加权编辑距离”(weighted edit distance)作为评价其相似性的依据。

这里的加权编辑距离是指,先判断从s1变为s2,最少需要多少步操作(包括插入、删除、修改、交换),然后对不同操作给出一个权值,将结果加起来,即得是加权编辑距离。

接下来,ssdeep将这个距离除以s1和s2的长度和,以将绝对结果变为相对结果,再映射到0-100的一个整数值上,其中,100表示两个字符串完全一致,而0表示完全不相似。

这样,最后就得到的相似程度的评分,可以用来判断两个文件是否有相似关系。在实践中,一般将ssdeep的结果为1或以上认为有相似性,而将结果为0认为是不相似。

四、解决问题的原理

我们来看一下模糊哈希为什么能有效比较相似性。

如果在一个文件中修改一个字节,有几种情况:

1、这个字节原来不影响分片,改动后也不影响分片,则这次修改只会影响一个分片哈希,对全局的影响微乎其微,最后相似结果极高;

2、这个字节原来不影响分片,改动后影响分片,则这次修改会影响两个分片哈希,并且造成两个文件的模糊哈希值不一样长,但相似性比较算法允许这种差异(无非是改动一个加上插入一个字母),其他大部分结果依然一致,因此相似结果依然很高;

3、这个字节原来影响分片,改动后不影响分片,与情况2类似;

4、这个字节原来影响分片,改动后依然影响分片,与情况1类似。

如果在一个文件中增加一个字节,同样有上述四种情况,但与上述逻辑类似,同样的对两个文件的模糊哈希值影响极小。如果在文件中删除一个字节,也与此类似。

因此,我们可以看到,在模糊哈希中,通过恰当的分片策略,以及相似度比较算法,可以有效地将细节变化对全局结果的影响限制在局部,从而对最终相似性作出有效的判断。事实上,即便是改动连续几个字符,或者作多处改动,模糊哈希算法依然可能作出有效的判断。

最神奇的是,对一篇文章,与它的一个片段(例如一个段落),通过模糊哈希也能判断出它们的相似性。这种独特的性质会非常有用!

五、ssdeep的其他改进

在实际使用中,还是存在一个问题——如果分片数量太小,例如整个文件只有一处触发了分片条件,或者干脆就没有触发分片条件,那最后得到的模糊哈希值干脆只有少数几个字节,这时候退化成了传统的全文哈希了,怎么办?

ssdeep精明地解决了这个问题。它根据文件长度和文件实际内容来决定如何分片。这是通过调整分片条件n的值来实现的。

我们来想一下,即便对弱哈希,都有这样的性质,即产生的结果在其可能空间上是接近于均匀分布的。在ssdeep中,n的值始终取2的整数次方,这样Alder-32哈希值除以n的余数也接近于均匀分布。仅当余数等于n-1时分片,就相当于只有差不多1/n的情况下会分片。也就是说,对一个文件,窗口每移动一次,就有1/n的可能要分片。如果某一次分的片数太小,那就减小n的值,使每次分片的可能性增加,增大片数。而如果觉得分的片太多,就增大n的值,使每次分片的可能性减少,降低片数。在ssdeep中,每次都是将n除以或者乘以2,来调整,使最终的片数尽可能在32到64之间。

实际上,由于分片的可能性差不多是1/n,所以每次ssdeep运行时,第一次尝试的n值就是一个接近于文件长度/64的值。事实表明效果不错。这反过来说明Alder-32的结果分布比较均匀。

因为n的值可能与文件长度、文件内容有关,因此ssdeep中,n的值会作为一个最终结果的一部分出现。

上述策略下,另一个问题出现了。这是一种比较极端的情况。假设一个文件使用的分片值n。在该文件中改动一个字节(修改、插入、删除等),且这个改动影响了分片的数量,使得分片数增加或减少,超出了边界范围,因此程序决定调整一下n,例如把n乘以或者除以2。因此,即便对文件的一个字节改动,也可能导致分片条件n的变化,从而导致分片数相差近一倍,而得到的结果显然也毫无相似性了。怎么办?

ssdeep解决了这种问题。对每一个文件,它同时使用n和n/2作为分片值,算得两个不同的模糊哈希值,而这两个值都使用。因此,最后得到的一个文件的模糊哈希值是:

n:h(n):h(n/2)

而在比较时,如果两个文件的分片值分别为n和m,则判断是否有n==m, n==2m, 2n==m三种情况,如果有之一,则将两者相应的模糊哈希值进行比较。例如,如果n==2m,则比较h(n/2)与h(m)是否相似。这样,在一定程序上解决了分片值变化的问题。

六、应用

模糊哈希最初应用于计算机取证,随即,反病毒领域发现了它的妙处,试图将其用于恶意代码的变种检测,这些工作包括[6] 、[7]和[8]。然而根据[8],直接使用的效果并不佳,应考虑与其他方法配合使用。[9]对模糊哈希用于恶意代码检测有科普性的介绍。

然而,在源码相似性比较上,模糊哈希表现出较好的准确性,因此被用于开源代码漏洞挖掘。在现实中,大量的开源项目使用第三方开源库,而又这些库引入的漏洞并不同步更新,因此存在潜在的安全威胁。于此有关的一些工作[10]使用了模糊哈希来判断开源项目之间的相互引用关系,并取得了杰出的成绩。

七、进一步工作

对模糊哈希的主要问题存在于两个方面,首先就是其准确性。

一是其并非一种精确判断,然而事实上对“相似性”的定义本来就不固定,而且就是一种不精确的概念。在目前,模糊哈希是一种相对较好的通用文件相似性比较方法。

二是对其的专门攻击,在计算机取证中,这种攻击有可能产生一些问题。在[11]中对此有专门的研究。

另一方面则是其性能表现上。ssdeep并不适合于进行大规模计算,例如,为了计算得到一个合适的分片值n,也许要多次读取全文并作大量计算。以ssdeep为代表的模糊哈希算法从设计和实现技巧两方面均存在速度问题。[12]对此做了改进和优化,得到了更好的性能和准确性。




关于模糊哈希(Fuzzy Hashing)算法,目前网上有几篇中文资料介绍,但均不准确。写这篇文章以纠正,并对其原理和应用作详细的介绍。

一、概述

模糊哈希算法又叫基于内容分割的分片分片哈希算法(context triggered piecewise hashing, CTPH),主要用于文件的相似性比较。

2006年,Jesse Kornblum [1] 提出CTPH,并给出一个名为spamsum的算法实例。随后,Jason Sherman开发了ssdeep [2] 工具以实现这一算法。该算法最初用于取证,后来被用于恶意代码检测,最近又有用于开源软件漏洞挖掘等。

模糊哈希的主要原理是,使用一个弱哈希计算文件局部内容,在特定条件下对文件进行分片,然后使用一个强哈希对文件每片计算哈希值,取这些值的一部分并连接起来,与分片条件一起构成一个模糊哈希结果。使用一个字符串相似性对比算法判断两个模糊哈希值的相似度有多少,从而判断两个文件的相似程度。

对文件的部分变化(包括在多处修改、增加、删除部分内容),使用模糊哈希均能发现与源文件的相似关系,是目前判断相似性较好的一种方法。


二、背景

一个文件也许有意或无意地产生变化。例如,有意的情况有作者改动文本内容、恶意代码自动变化;无意的情况有传输出错、磁盘存储出错等。如何有效判断两个文件是否相似,从而是同源的?这个问题在很多领域都有遇到。

最朴素的方法是逐个字节对比,并统计相同字节占文件大小的比例。这存在两个问题:1、对n个文件寻找相似关系,每个文件需要读取和对比n-1次;2、如果在一个文件中插入或删除一个字节,这种方法得到的相似结果也许很糟糕。

先说第一个问题。自然的想法是对每个文件生成较短的“指纹”,然后不一一比较文件本身,而是一一比较指纹。这样来降低比较量,提高效率。显然,传统的哈希算法(尤其是密码学意义上的哈希)正好可以充当这种“指纹”。

传统哈希也会面临前面提到的第二个问题。此外,即便是只修改文件中的一个字节,得到的结果也会大不相同(这是哈希算法的基本要求)。

为了解决后者,Nick Harbour提出所谓的分片哈希(piecewise hashing)算法,并在dcfldd [3] 中进行了实现。其策略非常简单,即每隔一个固定间隔就将文件分片,对每片计算一个哈希值,将这些哈希值一起作相似比较。显然,局部的修改只会影响少量分片的哈希结果,因此从整个文件看还是会有较高的相似性(虽然不是100%)。

这种方法依然面临前面的一个问题,即在一个文件中插入或删除一个字节,就会失效。

如何降低这种局部变化带来的全局影响?Kornblum提出了模糊哈希算法,其思想是:不再固定长度分片,而使用文件局部数据的特点来决定是否分片,使得局部的变化(包括修改、增加、删除等)只影响局部的分片方法,而不会将影响扩散至其他分片点。

三、算法的组成方法

一个模糊哈希算法由以下部分组成:

  1. 一个弱哈希算法,以及一个分片值,用于分片
  2. 一个强哈希算法,用于计算每片的哈希
  3. 一个压缩映射算法,将每片的哈希值映射为一个更短的值
  4. 一个比较算法,用于对两个模糊哈希值计算相似程度

3.1 分片

在文件中读取一部分内容,给弱哈希算法计算,得到一个哈希值。

通常逐字节读取固定长度的内容,就像网络协议中的滑动窗口一样在文件中以固定窗口滑动,每次都对窗口内的内容计算。因此,为了便捷,通常采用滚动哈希算法(rolling hashing)。这里的滚动哈希是指,比如原来已经计算了abcdef的哈希值h1,接下来要计算bcdefg的哈希值,不需要完全重新计算,只需要h1 – X(a) + Y(g)即可。其中X、Y是两个函数,即只需要相应增减差量对哈希值的影响即可。这种哈希大大可以加快分片判断的速度。

例如,在ssdeep中,使用Alder-32 [4] 算法作为弱哈希。它实际是一种用于校验和的弱哈希,类似于CRC32,不能用于密码学算法,但计算快速,生成4字节哈希值,并且是滚动哈希。

除了弱哈希算法,还需要一个分片值,例如n,由它来控制分片条件。在ssdeep中,n的值根据其他条件决定(后面再介绍)。

当Alder-32哈希值除以n的余数恰好等于n-1时,就在当前位置分片;否则,不分片,窗口往后滚动一个字节,然后再次计算Alder-32哈希值并判断,如此继续。

3.2 每片求哈希

将文件分片完以后,就可以对每片分别计算哈希了。可以使用传统的哈希算法,例如MD5。在ssdeep中,使用一个名为Fowler-Noll-Vo hash [5]的哈希算法。

3.3 压缩映射

对每一个文件分片,计算得到一个哈希值以后,可以选择将结果压缩短。例如,在ssdeep中,只取FNV哈希结果的最低6位,并用一个ASCII字符表示出来,作为这个分片的最终哈希结果。

显然,这种压缩映射会损失一部分准确性,引入误报问题。可以选择不做压缩映射,而存储较长的哈希值。不过从实践情况来看,做压缩映射的问题也不大。

3.4 连接哈希值

将每片压缩后的哈希值连接到一起,就得到这个文件的模糊哈希值了。如果分片条件参数n对不同文件可能不同,还应该将n纳入模糊哈希值中。

3.5 比较哈希值

最后的问题是,对两个文件的模糊哈希值,怎么样比较它们是不是相似?可以采用传统的相似性比较算法。

在ssdeep中,采用的如下思路。由于ssdeep对每一片得到的哈希值是一个ASCII字符,最终得到的文件模糊哈希值就是一个字符串了。假设是s1、s2,将s1到s2的“加权编辑距离”(weighted edit distance)作为评价其相似性的依据。

这里的加权编辑距离是指,先判断从s1变为s2,最少需要多少步操作(包括插入、删除、修改、交换),然后对不同操作给出一个权值,将结果加起来,即得是加权编辑距离。

接下来,ssdeep将这个距离除以s1和s2的长度和,以将绝对结果变为相对结果,再映射到0-100的一个整数值上,其中,100表示两个字符串完全一致,而0表示完全不相似。

这样,最后就得到的相似程度的评分,可以用来判断两个文件是否有相似关系。在实践中,一般将ssdeep的结果为1或以上认为有相似性,而将结果为0认为是不相似。

四、解决问题的原理

我们来看一下模糊哈希为什么能有效比较相似性。

如果在一个文件中修改一个字节,有几种情况:

1、这个字节原来不影响分片,改动后也不影响分片,则这次修改只会影响一个分片哈希,对全局的影响微乎其微,最后相似结果极高;

2、这个字节原来不影响分片,改动后影响分片,则这次修改会影响两个分片哈希,并且造成两个文件的模糊哈希值不一样长,但相似性比较算法允许这种差异(无非是改动一个加上插入一个字母),其他大部分结果依然一致,因此相似结果依然很高;

3、这个字节原来影响分片,改动后不影响分片,与情况2类似;

4、这个字节原来影响分片,改动后依然影响分片,与情况1类似。

如果在一个文件中增加一个字节,同样有上述四种情况,但与上述逻辑类似,同样的对两个文件的模糊哈希值影响极小。如果在文件中删除一个字节,也与此类似。

因此,我们可以看到,在模糊哈希中,通过恰当的分片策略,以及相似度比较算法,可以有效地将细节变化对全局结果的影响限制在局部,从而对最终相似性作出有效的判断。事实上,即便是改动连续几个字符,或者作多处改动,模糊哈希算法依然可能作出有效的判断。

最神奇的是,对一篇文章,与它的一个片段(例如一个段落),通过模糊哈希也能判断出它们的相似性。这种独特的性质会非常有用!

五、ssdeep的其他改进

在实际使用中,还是存在一个问题——如果分片数量太小,例如整个文件只有一处触发了分片条件,或者干脆就没有触发分片条件,那最后得到的模糊哈希值干脆只有少数几个字节,这时候退化成了传统的全文哈希了,怎么办?

ssdeep精明地解决了这个问题。它根据文件长度和文件实际内容来决定如何分片。这是通过调整分片条件n的值来实现的。

我们来想一下,即便对弱哈希,都有这样的性质,即产生的结果在其可能空间上是接近于均匀分布的。在ssdeep中,n的值始终取2的整数次方,这样Alder-32哈希值除以n的余数也接近于均匀分布。仅当余数等于n-1时分片,就相当于只有差不多1/n的情况下会分片。也就是说,对一个文件,窗口每移动一次,就有1/n的可能要分片。如果某一次分的片数太小,那就减小n的值,使每次分片的可能性增加,增大片数。而如果觉得分的片太多,就增大n的值,使每次分片的可能性减少,降低片数。在ssdeep中,每次都是将n除以或者乘以2,来调整,使最终的片数尽可能在32到64之间。

实际上,由于分片的可能性差不多是1/n,所以每次ssdeep运行时,第一次尝试的n值就是一个接近于文件长度/64的值。事实表明效果不错。这反过来说明Alder-32的结果分布比较均匀。

因为n的值可能与文件长度、文件内容有关,因此ssdeep中,n的值会作为一个最终结果的一部分出现。

上述策略下,另一个问题出现了。这是一种比较极端的情况。假设一个文件使用的分片值n。在该文件中改动一个字节(修改、插入、删除等),且这个改动影响了分片的数量,使得分片数增加或减少,超出了边界范围,因此程序决定调整一下n,例如把n乘以或者除以2。因此,即便对文件的一个字节改动,也可能导致分片条件n的变化,从而导致分片数相差近一倍,而得到的结果显然也毫无相似性了。怎么办?

ssdeep解决了这种问题。对每一个文件,它同时使用n和n/2作为分片值,算得两个不同的模糊哈希值,而这两个值都使用。因此,最后得到的一个文件的模糊哈希值是:

n:h(n):h(n/2)

而在比较时,如果两个文件的分片值分别为n和m,则判断是否有n==m, n==2m, 2n==m三种情况,如果有之一,则将两者相应的模糊哈希值进行比较。例如,如果n==2m,则比较h(n/2)与h(m)是否相似。这样,在一定程序上解决了分片值变化的问题。

六、应用

模糊哈希最初应用于计算机取证,随即,反病毒领域发现了它的妙处,试图将其用于恶意代码的变种检测,这些工作包括[6] 、[7]和[8]。然而根据[8],直接使用的效果并不佳,应考虑与其他方法配合使用。[9]对模糊哈希用于恶意代码检测有科普性的介绍。

然而,在源码相似性比较上,模糊哈希表现出较好的准确性,因此被用于开源代码漏洞挖掘。在现实中,大量的开源项目使用第三方开源库,而又这些库引入的漏洞并不同步更新,因此存在潜在的安全威胁。于此有关的一些工作[10]使用了模糊哈希来判断开源项目之间的相互引用关系,并取得了杰出的成绩。

七、进一步工作

对模糊哈希的主要问题存在于两个方面,首先就是其准确性。

一是其并非一种精确判断,然而事实上对“相似性”的定义本来就不固定,而且就是一种不精确的概念。在目前,模糊哈希是一种相对较好的通用文件相似性比较方法。

二是对其的专门攻击,在计算机取证中,这种攻击有可能产生一些问题。在[11]中对此有专门的研究。

另一方面则是其性能表现上。ssdeep并不适合于进行大规模计算,例如,为了计算得到一个合适的分片值n,也许要多次读取全文并作大量计算。以ssdeep为代表的模糊哈希算法从设计和实现技巧两方面均存在速度问题。[12]对此做了改进和优化,得到了更好的性能和准确性。



关于模糊哈希(Fuzzy Hashing)算法,目前网上有几篇中文资料介绍,但均不准确。写这篇文章以纠正,并对其原理和应用作详细的介绍。

一、概述

模糊哈希算法又叫基于内容分割的分片分片哈希算法(context triggered piecewise hashing, CTPH),主要用于文件的相似性比较。

2006年,Jesse Kornblum [1] 提出CTPH,并给出一个名为spamsum的算法实例。随后,Jason Sherman开发了ssdeep [2] 工具以实现这一算法。该算法最初用于取证,后来被用于恶意代码检测,最近又有用于开源软件漏洞挖掘等。

模糊哈希的主要原理是,使用一个弱哈希计算文件局部内容,在特定条件下对文件进行分片,然后使用一个强哈希对文件每片计算哈希值,取这些值的一部分并连接起来,与分片条件一起构成一个模糊哈希结果。使用一个字符串相似性对比算法判断两个模糊哈希值的相似度有多少,从而判断两个文件的相似程度。

对文件的部分变化(包括在多处修改、增加、删除部分内容),使用模糊哈希均能发现与源文件的相似关系,是目前判断相似性较好的一种方法。


二、背景

一个文件也许有意或无意地产生变化。例如,有意的情况有作者改动文本内容、恶意代码自动变化;无意的情况有传输出错、磁盘存储出错等。如何有效判断两个文件是否相似,从而是同源的?这个问题在很多领域都有遇到。

最朴素的方法是逐个字节对比,并统计相同字节占文件大小的比例。这存在两个问题:1、对n个文件寻找相似关系,每个文件需要读取和对比n-1次;2、如果在一个文件中插入或删除一个字节,这种方法得到的相似结果也许很糟糕。

先说第一个问题。自然的想法是对每个文件生成较短的“指纹”,然后不一一比较文件本身,而是一一比较指纹。这样来降低比较量,提高效率。显然,传统的哈希算法(尤其是密码学意义上的哈希)正好可以充当这种“指纹”。

传统哈希也会面临前面提到的第二个问题。此外,即便是只修改文件中的一个字节,得到的结果也会大不相同(这是哈希算法的基本要求)。

为了解决后者,Nick Harbour提出所谓的分片哈希(piecewise hashing)算法,并在dcfldd [3] 中进行了实现。其策略非常简单,即每隔一个固定间隔就将文件分片,对每片计算一个哈希值,将这些哈希值一起作相似比较。显然,局部的修改只会影响少量分片的哈希结果,因此从整个文件看还是会有较高的相似性(虽然不是100%)。

这种方法依然面临前面的一个问题,即在一个文件中插入或删除一个字节,就会失效。

如何降低这种局部变化带来的全局影响?Kornblum提出了模糊哈希算法,其思想是:不再固定长度分片,而使用文件局部数据的特点来决定是否分片,使得局部的变化(包括修改、增加、删除等)只影响局部的分片方法,而不会将影响扩散至其他分片点。

三、算法的组成方法

一个模糊哈希算法由以下部分组成:

  1. 一个弱哈希算法,以及一个分片值,用于分片
  2. 一个强哈希算法,用于计算每片的哈希
  3. 一个压缩映射算法,将每片的哈希值映射为一个更短的值
  4. 一个比较算法,用于对两个模糊哈希值计算相似程度

3.1 分片

在文件中读取一部分内容,给弱哈希算法计算,得到一个哈希值。

通常逐字节读取固定长度的内容,就像网络协议中的滑动窗口一样在文件中以固定窗口滑动,每次都对窗口内的内容计算。因此,为了便捷,通常采用滚动哈希算法(rolling hashing)。这里的滚动哈希是指,比如原来已经计算了abcdef的哈希值h1,接下来要计算bcdefg的哈希值,不需要完全重新计算,只需要h1 – X(a) + Y(g)即可。其中X、Y是两个函数,即只需要相应增减差量对哈希值的影响即可。这种哈希大大可以加快分片判断的速度。

例如,在ssdeep中,使用Alder-32 [4] 算法作为弱哈希。它实际是一种用于校验和的弱哈希,类似于CRC32,不能用于密码学算法,但计算快速,生成4字节哈希值,并且是滚动哈希。

除了弱哈希算法,还需要一个分片值,例如n,由它来控制分片条件。在ssdeep中,n的值根据其他条件决定(后面再介绍)。

当Alder-32哈希值除以n的余数恰好等于n-1时,就在当前位置分片;否则,不分片,窗口往后滚动一个字节,然后再次计算Alder-32哈希值并判断,如此继续。

3.2 每片求哈希

将文件分片完以后,就可以对每片分别计算哈希了。可以使用传统的哈希算法,例如MD5。在ssdeep中,使用一个名为Fowler-Noll-Vo hash [5]的哈希算法。

3.3 压缩映射

对每一个文件分片,计算得到一个哈希值以后,可以选择将结果压缩短。例如,在ssdeep中,只取FNV哈希结果的最低6位,并用一个ASCII字符表示出来,作为这个分片的最终哈希结果。

显然,这种压缩映射会损失一部分准确性,引入误报问题。可以选择不做压缩映射,而存储较长的哈希值。不过从实践情况来看,做压缩映射的问题也不大。

3.4 连接哈希值

将每片压缩后的哈希值连接到一起,就得到这个文件的模糊哈希值了。如果分片条件参数n对不同文件可能不同,还应该将n纳入模糊哈希值中。

3.5 比较哈希值

最后的问题是,对两个文件的模糊哈希值,怎么样比较它们是不是相似?可以采用传统的相似性比较算法。

在ssdeep中,采用的如下思路。由于ssdeep对每一片得到的哈希值是一个ASCII字符,最终得到的文件模糊哈希值就是一个字符串了。假设是s1、s2,将s1到s2的“加权编辑距离”(weighted edit distance)作为评价其相似性的依据。

这里的加权编辑距离是指,先判断从s1变为s2,最少需要多少步操作(包括插入、删除、修改、交换),然后对不同操作给出一个权值,将结果加起来,即得是加权编辑距离。

接下来,ssdeep将这个距离除以s1和s2的长度和,以将绝对结果变为相对结果,再映射到0-100的一个整数值上,其中,100表示两个字符串完全一致,而0表示完全不相似。

这样,最后就得到的相似程度的评分,可以用来判断两个文件是否有相似关系。在实践中,一般将ssdeep的结果为1或以上认为有相似性,而将结果为0认为是不相似。

四、解决问题的原理

我们来看一下模糊哈希为什么能有效比较相似性。

如果在一个文件中修改一个字节,有几种情况:

1、这个字节原来不影响分片,改动后也不影响分片,则这次修改只会影响一个分片哈希,对全局的影响微乎其微,最后相似结果极高;

2、这个字节原来不影响分片,改动后影响分片,则这次修改会影响两个分片哈希,并且造成两个文件的模糊哈希值不一样长,但相似性比较算法允许这种差异(无非是改动一个加上插入一个字母),其他大部分结果依然一致,因此相似结果依然很高;

3、这个字节原来影响分片,改动后不影响分片,与情况2类似;

4、这个字节原来影响分片,改动后依然影响分片,与情况1类似。

如果在一个文件中增加一个字节,同样有上述四种情况,但与上述逻辑类似,同样的对两个文件的模糊哈希值影响极小。如果在文件中删除一个字节,也与此类似。

因此,我们可以看到,在模糊哈希中,通过恰当的分片策略,以及相似度比较算法,可以有效地将细节变化对全局结果的影响限制在局部,从而对最终相似性作出有效的判断。事实上,即便是改动连续几个字符,或者作多处改动,模糊哈希算法依然可能作出有效的判断。

最神奇的是,对一篇文章,与它的一个片段(例如一个段落),通过模糊哈希也能判断出它们的相似性。这种独特的性质会非常有用!

五、ssdeep的其他改进

在实际使用中,还是存在一个问题——如果分片数量太小,例如整个文件只有一处触发了分片条件,或者干脆就没有触发分片条件,那最后得到的模糊哈希值干脆只有少数几个字节,这时候退化成了传统的全文哈希了,怎么办?

ssdeep精明地解决了这个问题。它根据文件长度和文件实际内容来决定如何分片。这是通过调整分片条件n的值来实现的。

我们来想一下,即便对弱哈希,都有这样的性质,即产生的结果在其可能空间上是接近于均匀分布的。在ssdeep中,n的值始终取2的整数次方,这样Alder-32哈希值除以n的余数也接近于均匀分布。仅当余数等于n-1时分片,就相当于只有差不多1/n的情况下会分片。也就是说,对一个文件,窗口每移动一次,就有1/n的可能要分片。如果某一次分的片数太小,那就减小n的值,使每次分片的可能性增加,增大片数。而如果觉得分的片太多,就增大n的值,使每次分片的可能性减少,降低片数。在ssdeep中,每次都是将n除以或者乘以2,来调整,使最终的片数尽可能在32到64之间。

实际上,由于分片的可能性差不多是1/n,所以每次ssdeep运行时,第一次尝试的n值就是一个接近于文件长度/64的值。事实表明效果不错。这反过来说明Alder-32的结果分布比较均匀。

因为n的值可能与文件长度、文件内容有关,因此ssdeep中,n的值会作为一个最终结果的一部分出现。

上述策略下,另一个问题出现了。这是一种比较极端的情况。假设一个文件使用的分片值n。在该文件中改动一个字节(修改、插入、删除等),且这个改动影响了分片的数量,使得分片数增加或减少,超出了边界范围,因此程序决定调整一下n,例如把n乘以或者除以2。因此,即便对文件的一个字节改动,也可能导致分片条件n的变化,从而导致分片数相差近一倍,而得到的结果显然也毫无相似性了。怎么办?

ssdeep解决了这种问题。对每一个文件,它同时使用n和n/2作为分片值,算得两个不同的模糊哈希值,而这两个值都使用。因此,最后得到的一个文件的模糊哈希值是:

n:h(n):h(n/2)

而在比较时,如果两个文件的分片值分别为n和m,则判断是否有n==m, n==2m, 2n==m三种情况,如果有之一,则将两者相应的模糊哈希值进行比较。例如,如果n==2m,则比较h(n/2)与h(m)是否相似。这样,在一定程序上解决了分片值变化的问题。

六、应用

模糊哈希最初应用于计算机取证,随即,反病毒领域发现了它的妙处,试图将其用于恶意代码的变种检测,这些工作包括[6] 、[7]和[8]。然而根据[8],直接使用的效果并不佳,应考虑与其他方法配合使用。[9]对模糊哈希用于恶意代码检测有科普性的介绍。

然而,在源码相似性比较上,模糊哈希表现出较好的准确性,因此被用于开源代码漏洞挖掘。在现实中,大量的开源项目使用第三方开源库,而又这些库引入的漏洞并不同步更新,因此存在潜在的安全威胁。于此有关的一些工作[10]使用了模糊哈希来判断开源项目之间的相互引用关系,并取得了杰出的成绩。

七、进一步工作

对模糊哈希的主要问题存在于两个方面,首先就是其准确性。

一是其并非一种精确判断,然而事实上对“相似性”的定义本来就不固定,而且就是一种不精确的概念。在目前,模糊哈希是一种相对较好的通用文件相似性比较方法。

二是对其的专门攻击,在计算机取证中,这种攻击有可能产生一些问题。在[11]中对此有专门的研究。

另一方面则是其性能表现上。ssdeep并不适合于进行大规模计算,例如,为了计算得到一个合适的分片值n,也许要多次读取全文并作大量计算。以ssdeep为代表的模糊哈希算法从设计和实现技巧两方面均存在速度问题。[12]对此做了改进和优化,得到了更好的性能和准确性。