滚动哈希和卡哈希的数学原理
文章目录
translate from The Mathematics Behind Rolling Hashes and Anti-hash Tests
设计难卡的滚动哈希
滚动哈希
我们使用二元组来描述滚动哈希。其中是模数(modulo),是基数(base)。
对于长度为的字符串,它的滚动哈希函数为
对于等长的两个字符串,若且,则称这是等长冲突(equal-length collision)。
随机基数
考虑我们固定一个质数模数,在中随机一个整数作为基数来做滚动哈希。
对于两个长度为的串,考虑计算他们等长冲突的概率:
在这里是一个关于的度的多项式。那么的概率就是的概率,即是的根的概率。
由于是质数,因此这构成一个域。在域中,任何度的多项式最多有个根。因此
对于,这个概率是级别的。
的范围其实比较紧的。如果。考虑,则。根据一些群论知识,这个方程有个根。
随机模数
考虑固定基数,然后在的范围内随机选择一个整数作为模数。
同样地,对于两个长度为的串,考虑计算他们等长冲突的概率:
则可以通过一些数学知识得到
对于,这个概率是级别的。
如何随机
以下三种可以考虑:
//by Sshwy
//#define DEBUGGER
#include<chrono>
#include<iostream>
using namespace std;
int main(){
cout<<chrono::duration_cast<chrono::nanoseconds>(chrono::high_resolution_clock::now().time_since_epoch()).count()<<endl;
cout<<chrono::duration_cast<chrono::nanoseconds>(chrono::steady_clock::now().time_since_epoch()).count()<<endl;
cout<<__builtin_ia32_rdtsc()<<endl;
return 0;
}
多重哈希
我们可以使用多重的随机滚动哈希。如果重哈希的冲突概率分别是,则你哈希碰撞的概率就是。
更大的模数
你哈希的模数越大,冲突的概率就越小。但是更大的模数在计算过程中会很吃力。使用__int128
会降低计算速度。不过有一个质数例外,那就是梅森质数,我们可以通过位运算来计算:
constexpr uint64_t mod = (1ull<<61) - 1;
uint64_t mul(uint64_t a, uint64_t b){
uint64_t l1 = (uint32_t)a, h1 = a>>32, l2 = (uint32_t)b, h2 = b>>32;
uint64_t l = l1*l2, m = l1*h2 + l2*h1, h = h1*h2;
uint64_t ret = (l&mod) + (l>>61) + (h << 3) + (m >> 29) + (m << 35 >> 3) + 1;
ret = (ret & mod) + (ret>>61);
ret = (ret & mod) + (ret>>61);
return ret-1;
}
卡哈希
接下来就是关于如何卡哈希的教程了。
如何卡哈希
在介绍具体的方法之前,先思考一下,面对一道哈希问题,如果去卡它?
本质上,分为两个步骤:
- 对于该哈希方式找一个等长冲突;
- 利用这个等长冲突来造数据卡他。
单哈希
Thue–Morse 序列攻击:ULL 自然溢出
考虑,任意的情况。我们可以使用 Thue–Morse 序列来卡。它的生成方式如下:
const int Q = 10;
const int N = 1 << Q;
std::string S(N, ' '), T(N, ' ');
for (int i = 0; i < N; i++)
S[i] = 'A' + __builtin_popcount(i) % 2;
for (int i = 0; i < N; i++)
T[i] = 'B' - __builtin_popcount(i) % 2;
这时就发生了等长冲突(不论的值)。具体参见 这篇文章。
生日攻击:32 位模数,固定模数和基数
考虑使用生日攻击。固定长度,令。并且等概率随机(uniformly at random)生成个长度为的字符串。
如果的值不是特别小,则这个串的哈希值可以近似看作等概率随机分布,使用生日悖论,则这个数全部两两不同的概率是
因此我们重复这个过程,可以在的时间内找到等长冲突。
生日攻击不依赖于哈希函数(异或哈希也可以)。但它不能卡回文串。
树攻击:更大模数,固定模数和基数
对于更大的模数,生日攻击将会变得很慢。
考虑树攻击(tree-attack)。
对于两个长度为的的字符串,我们知道
不妨设。显然。树攻击会尝试寻找一个的序列使得
考虑维护个集合。定义。
如果我们合并两个集合为,则我们可以:
- 直接合并,则;
- 把都乘再合并,则;
- 类似 2,可以令;
- 把都变成,则;
- 类似 4,可以令。
一开始我们有个集合,设每个集合的都是。不妨设。则我们每个阶段,都把集合按排序,然后使用 2 或者 3 方法来合并相邻两个集合(要保证合并完后非负)。一轮完成后,集合数量减半。如果出现了一个的集合,那么我们把其他集合的都置为即可。这样会最多做轮。如果没有找到,那么就对更大的继续这个过程。
如果一开始个集合的是等概率随机分布于,则期望为时可以找到。那么我们就可以地构造长度为的等长冲突的串了。
注意,树攻击是依赖于哈希函数的,即你的哈希函数必须是多项式函数。
卡回文串
树攻击可以卡回文串。以偶回文串为例,具体地说,我们要构造一个长度为偶数的串,使得。表示反串。
那么这等价于
因此我们设,得到
那么我们对这个做一次正常的树攻击,就可以构造出一组的值。
那么用字符和来构造串即可。
多重树攻击
尽管树攻击速度很快,生成的字符串可能会过长(对于,通常)。实际上我们可以花更多的计算时间来生成一个尽量短的等长冲突。对于每个集合,我们可以维护个最小的可能的和(单树攻击是的情况)。合并两个集合可以使用堆在的时间内完成。
一通鬼畜分析后,我们得到。
这个东西 Dls 也没写过,所以不知道好不好使。
多重哈希
卡多重哈希的方式有两种。
逐个击破
以双哈希为例,考虑双哈希。首先我们使用生日攻击或者树攻击找到的一个等长冲突。然后我们以作为字符集对求出等长冲突(以作为字符集的意思是,我们用和拼接出一个更大的串)。这样就可以把两个哈希都冲突掉。
使用这个方法,时间复杂度是每次攻击的复杂度之和。但生成的串长会随着哈希的重数而指数级增长。
不过这个方法不用考虑模数的大小。
中国剩余定理(CRT)
对于树攻击,我们可以使用中国剩余定理来卡哈希。本质上,我们要求
设。然后我们使用 CRT 求出一个,使得
那么我们就只需要
即可。 那么我们对此做一次树攻击即可。
这个方法的要求模数的 LCM 不能太大。不过它的优点是,双哈希的回文串它也能卡。
修订记录
- 2020年3月22日 创建文章