我将如何获取与返回的哈希值匹配的任何可能的字符串值?
我不想获得使用过的确切键,只是获得传递给函数的任何键,都将返回未知键的相同哈希值。
uint32_t jenkins_one_at_a_time_hash(const uint8_t* key, size_t length) {
size_t i = 0;
uint32_t hash = 0;
while (i != length) {
hash += key[i++];
hash += hash << 10;
hash ^= hash >> 6;
}
hash += hash << 3;
hash ^= hash >> 11;
hash += hash << 15;
return hash;
}
例如我将密钥作为“ keynumber1”传递,该函数返回0xA7AF2FFE。 我将如何找到也可以哈希为0xA7AF2FFE的任何字符串。
答案 0 :(得分:3)
如果哈希函数很好,只需尝试许多键组合,然后查看哈希是否匹配。这就是一个很好的哈希值。很难逆转。
我估计大约有2 ^ 32次尝试,您将有50%的机会找到一个。下面花了几秒钟。
使用此哈希,可能适用捷径。
int main() {
const char *key1 = "keynumber1";
uint32_t match = jenkins_one_at_a_time_hash(key1, strlen(key1));
printf("Target 0x%lX\n", (unsigned long) match);
uint32_t i = 0;
do {
uint32_t hash = jenkins_one_at_a_time_hash(&i, sizeof i);
if (hash == match) {
printf("0x%lX: 0x%lX\n", (unsigned long) i, (unsigned long) hash);
fflush(stdout);
}
} while (++i);
const char *key2 = "\x3C\xA0\x94\xB9";
uint32_t match2 = jenkins_one_at_a_time_hash(key2, strlen(key2));
printf("Match 0x%lX\n", (unsigned long) match2);
}
输出
Target 0xA7AF2FFE
0xB994A03C: 0xA7AF2FFE
Match 0xA7AF2FFE
答案 1 :(得分:2)
虽然蛮力方法suggested by chux可以正常工作,但实际上我们可以将其提高多达256倍左右(实际上,如果使用所有优化,则可以提高很多)如下所述。)
此处的关键实现是,用于计算哈希的所有操作都是可逆的。 (这是设计使然,因为它确保例如将相同的后缀附加到所有输入字符串中不会增加哈希冲突的次数。)特别是:
操作hash += hash << n
当然等效于hash *= (1 << n) + 1
。我们正在使用32位无符号整数,因此所有这些计算都是modulo 2 32 完成的。要撤消该操作,我们要做的就是找到(1 << n) + 1
= 2 n +1模2 32 并乘以hash
。
我们可以很容易地做到这一点,例如基于this Python script的this answer here on SO。事实证明,2 10 + 1,2 3 + 1和2 15 +1的乘法逆以十六进制表示为0xC00FFC01 ,分别为0x38E38E39和0x3FFF8001。
要找到某个常数hash ^= hash >> n
的{{1}}的逆,首先请注意,此操作将n
的最高n
位保留为完全不变。接下来的较低的hash
位将与最高的n
位进行异或运算,因此,对于这些位,只需重复操作即可将其撤消。到目前为止看起来很简单,对吧?
要恢复最高{em {3} 位n
位组的原始值,我们需要将它们与第二最高n
位的原始值进行异或当然,可以通过如上所述对n
位的两个最高位组进行异或运算来计算。依此类推。
这全部归结为n
的反运算是:
hash ^= hash >> n
当然,一旦移位量等于或大于所处理整数的位数(在本例中为32),我们就可以中断该序列。或者,我们可以分多个步骤来实现相同的结果,每次将移位量加倍,直到超出我们正在处理的数字的位长为止,如下所示:
hash ^= (hash >> n) ^ (hash >> 2*n) ^ (hash >> 3*n) ^ (hash >> 4*n) ^ ...
(hash ^= hash >> n;
hash ^= hash >> 2*n;
hash ^= hash >> 4*n;
hash ^= hash >> 8*n;
// etc.
小于整数时,多步方法的缩放效果更好,但是对于n
较大的情况,单步方法可能会遇到较少的现代CPU流水线停顿的情况。这很难再说哪一种实际上在任何给定的情况下都更有效,而没有对它们进行基准测试,结果在编译器和CPU模型之间可能会有所不同。在任何情况下,这种微优化都不值得担心。)
最后,当然,n
的倒数就是hash += key[i++]
。
所有这些意味着,如果我们愿意,我们可以像这样反向运行哈希:
hash -= key[--i]
然后调用uint32_t reverse_one_at_a_time_hash(const uint8_t* key, size_t length, uint32_t hash) {
hash *= 0x3FFF8001; // inverse of hash += hash << 15;
hash ^= (hash >> 11) ^ (hash >> 22);
hash *= 0x38E38E39; // inverse of hash += hash << 3;
size_t i = length;
while (i > 0) {
hash ^= (hash >> 6) ^ (hash >> 12) ^ (hash >> 18) ^ (hash >> 24) ^ (hash >> 30);
hash *= 0xC00FFC01; // inverse of hash += hash << 10;
hash -= key[--i];
}
return hash; // this should return 0 if the original hash was correct
}
应该返回零,as indeed it does。
好的,很酷。但这对于找到原像有什么好处?
好吧,一方面,如果我们猜测输入的除了第一个字节之外的所有内容,那么我们可以将第一个字节设置为零,并对该输入进行反向哈希处理。此时,有两种可能的结果:
如果像这样向后运行哈希,则产生的输出是有效输入字节(即不大于255,并且如果您希望所有输入字节都是可打印的ASCII,则可能有其他限制),然后我们可以将输入的第一个字节设置为该值,我们就完成了!
这里是an example,它查找与chux代码相同的输入(但将其打印为带引号的字符串,而不是小端整数):
reverse_one_at_a_time_hash("keynumber1", 10, 0xA7AF2FFE)
然后here's a version that restricts the input to printable ASCII(并输出五字节字符串#define TARGET_HASH 0xA7AF2FFE
#define INPUT_LEN 4
int main() {
uint8_t buf[INPUT_LEN+1]; // buffer for guessed input (and one more null byte at the end)
for (int i = 0; i <= INPUT_LEN; i++) buf[i] = 0;
do {
uint32_t ch = reverse_one_at_a_time_hash(buf, INPUT_LEN, TARGET_HASH);
if (ch <= 255) {
buf[0] = ch;
// print the input with unprintable chars nicely quoted
printf("hash(\"");
for (int i = 0; i < INPUT_LEN; i++) {
if (buf[i] < 32 || buf[i] > 126 || buf[i] == '"' || buf[i] == '\\') printf("\\x%02X", buf[i]);
else putchar(buf[i]);
}
printf("\") = 0x%08X\n", TARGET_HASH);
return 0;
}
// increment buffer, starting from second byte
for (int i = 1; ++buf[i] == 0; i++) /* nothing */;
} while (buf[INPUT_LEN] == 0);
printf("No matching input of %d bytes found for hash 0x%08X. :(", INPUT_LEN, TARGET_HASH);
return 1;
}
):
^U_N.
当然,很容易修改此代码,以更加严格地限制要接受的输入字节。例如,using the following settings:
#define TARGET_HASH 0xA7AF2FFE
#define MIN_INPUT_CHAR ' '
#define MAX_INPUT_CHAR '~'
#define INPUT_LEN 5
int main() {
uint8_t buf[INPUT_LEN+1]; // buffer for guessed input (and one more null byte at the end)
buf[0] = buf[INPUT_LEN] = 0;
for (int i = 1; i < INPUT_LEN; i++) buf[i] = MIN_INPUT_CHAR;
do {
uint32_t ch = reverse_one_at_a_time_hash(buf, INPUT_LEN, TARGET_HASH);
if (ch >= MIN_INPUT_CHAR && ch <= MAX_INPUT_CHAR) {
buf[0] = ch;
printf("hash(\"%s\") = 0x%08X\n", buf, TARGET_HASH);
return 0;
}
// increment buffer, starting from second byte, while keeping bytes within the valid range
int i = 1;
while (buf[i] >= MAX_INPUT_CHAR) buf[i++] = MIN_INPUT_CHAR;
buf[i]++;
} while (buf[INPUT_LEN] == 0);
printf("No matching input of %d bytes found for hash 0x%08X. :(", INPUT_LEN, TARGET_HASH);
return 1;
}
(在几秒钟的计算后)产生原像#define TARGET_HASH 0xA7AF2FFE
#define MIN_INPUT_CHAR 'A'
#define MAX_INPUT_CHAR 'Z'
#define INPUT_LEN 7
。
限制输入范围确实会使代码运行变慢,因为向后哈希计算结果为有效输入字节的概率当然与可能的有效字节数成比例。
可以通过多种方式使此代码运行得更快。例如,我们可以combine the backwards hashing with a recursive search,这样就不必重复对整个输入字符串进行哈希处理,即使其中只有一个字节发生变化也是如此:
KQEJZVS
但是等等,我们还没有完成!查看一次一次哈希的原始代码,我们可以看到在循环的第一次迭代之后,#define TARGET_HASH 0xA7AF2FFE
#define MIN_INPUT_CHAR 'A'
#define MAX_INPUT_CHAR 'Z'
#define INPUT_LEN 7
static bool find_preimage(uint32_t hash, uint8_t *buf, int depth) {
// first invert the hash mixing step
hash ^= (hash >> 6) ^ (hash >> 12) ^ (hash >> 18) ^ (hash >> 24) ^ (hash >> 30);
hash *= 0xC00FFC01; // inverse of hash += hash << 10;
// then check if we're down to the first byte
if (depth == 0) {
bool found = (hash >= MIN_INPUT_CHAR && hash <= MAX_INPUT_CHAR);
if (found) buf[0] = hash;
return found;
}
// otherwise try all possible values for this byte
for (uint32_t ch = MIN_INPUT_CHAR; ch <= MAX_INPUT_CHAR; ch++) {
bool found = find_preimage(hash - ch, buf, depth - 1);
if (found) { buf[depth] = ch; return true; }
}
return false;
}
int main() {
uint8_t buf[INPUT_LEN+1]; // buffer for results
for (int i = 0; i <= INPUT_LEN; i++) buf[INPUT_LEN] = 0;
// first undo the finalization step
uint32_t hash = TARGET_HASH;
hash *= 0x3FFF8001; // inverse of hash += hash << 15;
hash ^= (hash >> 11) ^ (hash >> 22);
hash *= 0x38E38E39; // inverse of hash += hash << 3;
// then search recursively until we find a matching input
bool found = find_preimage(hash, buf, INPUT_LEN - 1);
if (found) {
printf("hash(\"%s\") = 0x%08X\n", buf, TARGET_HASH);
} else {
printf("No matching input of %d bytes found for hash 0x%08X. :(", INPUT_LEN, TARGET_HASH);
}
return !found;
}
的值为hash
,其中((c << 10) + c) ^ ((c << 4) + (c >> 6))
是输入的第一个字节。由于c
是一个八位字节,因此这意味着c
的最低18字节只能在第一次迭代后设置。
如果实际上,如果我们calculate the value of hash
after the first iteration为第一个字节hash
的每个可能值,我们可以看到c
永远不会超过hash
。 (实际上,比率1042 * c
的最大值仅为1041.015625 = 1041 + 2 -6 。)这意味着,如果hash / c
是有效值的最大可能值输入字节,第一次迭代后的M
的值不能超过hash
。并且添加下一个输入字节只会使1042 * M
最多增加hash
。
因此我们可以通过在M
中添加以下快捷方式检查来speed up the code above significantly:
find_preimage()
实际上,可以使用类似的参数来表明,在处理了前两个 个字节之后,最多可以设置 // optimization: return early if no first two bytes can possibly match
if (depth == 1 && hash > MAX_INPUT_CHAR * 1043) return false;
的最低28个字节(并且{ 3}},hash
与最大输入字节值之比最大为1084744.46667。因此,我们可以more precisely覆盖hash
来覆盖搜索的最后三个阶段,如下所示:
find_preimage()
对于示例搜索哈希0xA7AF2FFE的七字节全大写原像的示例,此进一步的优化将运行时间缩短至仅0.075秒(而static bool find_preimage(uint32_t hash, uint8_t *buf, int depth) {
// first invert the hash mixing step
hash ^= (hash >> 6) ^ (hash >> 12) ^ (hash >> 18) ^ (hash >> 24) ^ (hash >> 30);
hash *= 0xC00FFC01; // inverse of hash += hash << 10;
// for the lowest three levels, abort early if no solution is possible
switch (depth) {
case 0:
if (hash < MIN_INPUT_CHAR || hash > MAX_INPUT_CHAR) return false;
buf[0] = hash;
return true;
case 1:
if (hash > MAX_INPUT_CHAR * 1043) return false;
else break;
case 2:
if (hash > MAX_INPUT_CHAR * 1084746) return false;
else break;
}
// otherwise try all possible values for this byte
for (uint32_t ch = MIN_INPUT_CHAR; ch <= MAX_INPUT_CHAR; ch++) {
bool found = find_preimage(hash - ch, buf, depth - 1);
if (found) { buf[depth] = ch; return true; }
}
return false;
}
快捷方式的运行时间仅为0.148秒,仅为2.456秒对于没有快捷方式的递归搜索,则为15.489秒,对于非递归搜索,则由TIO计时)。