我才刚开始进行递归编程-既然我已经听说过它对于解决问题有多么强大,我想尝试一下我几天前编写的简单解密算法。< / p>
我知道可能很难计算出每个迭代在做什么,但是递归可以使这个循环更“优雅”和“算法”吗?
for (int e = 0; e < length; e++)
{
for (int d = 0; d < length; d++)
{
for (int c = 0; c < length; c++)
{
for (int b = 0; b < length; b++)
{
for (int a = 1; a < length; a++)
{
key[0] = letters[a];
key[1] = letters[b];
key[2] = letters[c];
key[3] = letters[d];
key[4] = letters[e];
if (strcmp(crypt(key, salt), hash) == 0)
{
printf("%s\n", key);
return 0;
}
}
}
}
}
}
答案 0 :(得分:6)
如果无需递归就可以完成任务,那么以这种方式解决它是一个好主意。如果您想了解递归,请查看一些问题,例如阶乘或斐波那契。这些还具有迭代解决方案,但比您在此处遇到的问题更适合于递归。在这种情况下,很清楚您的算法在做什么,递归将使它变得不必要地难以理解。这是您可以做出的一项改进
for (int e = 0; e < length; e++)
{
key[4] = letters[e];
for (int d = 0; d < length; d++)
{
key[3] = letters[d];
for (int c = 0; c < length; c++)
{
key[2] = letters[c];
for (int b = 0; b < length; b++)
{
key[1] = letters[b];
for (int a = 1; a < length; a++)
{
key[0] = letters[a];
if (strcmp(crypt(key, salt), hash) == 0)
{
printf("%s\n", key);
return 0;
}
}
}
}
}
}
答案 1 :(得分:1)
尽管我不同意在本示例中不鼓励您使用递归的每个人,但由于我认为这是一个合理的问题,因此我想递归地编写它。
这是我递归编写的尝试。这样,我只需要编写一次循环,因为外部循环是由递归处理的。我采取了一些自由措施,因此它并不完全等同于您的代码,但我认为原则上是相同的(针对hash
测试所有组合),并展示了如何递归编写此代码的基本思想。我假设您有一种知道strcmp
检查是安全的方法。
int recur(int cur, int klength, char *key, char *letters, int length, char *salt, char *hash)
{
if (cur == klength)
{
if (strcmp(crypt(key, salt), hash))
{
return 1;
}
else
{
return 0;
}
}
else
{
for (int i = 0; i < length; i++)
{
key[cur] = letters[i];
int j = recur(cur+1, klength, key, letters, length, salt, hash);
if (!j)
{
return 0;
}
}
return 1;
}
}
然后我会用
来称呼它recur(5, 0, ...)
执行您编写的5个循环。这不是很优雅,但我认为很清楚为什么如果将密钥扩展为需要10个循环(为什么它对于10000个循环的堆栈来说会很糟糕),可能会更优雅?
话虽如此,我首先想到的不是查看代码,而是“那些外循环看起来很相似,因此我想摆脱其中的一些循环”。我下面的代码不是很漂亮(嘿,它很晚了!),但是我认为原则上,如果您认为可能需要将要测试的字符数增加到10个(或10000个),这将是一个更好的方法。我想做的是维护一个等于key
中idx
的整数。如果我递增idx[0]
且它是== length
,我知道我需要重设idx[0] = 0
并尝试递增idx[1]
,依此类推。每次更改idx[i]
时,我都会对key[i]
的等效更改。每次我对idx / key进行新的排列时,我都会进行strcmp
测试,以查看是否找到了正确的密钥。
int ksize = 5;
int idx[ksize];
for (int i = 0; i < ksize; ++i)
{
idx[i] = 0;
key[i] = letters[0];
}
for (int done = 0; !done; )
{
if (strcmp(crypt(key, salt), hash) == 0)
{
printf("%s\n", key);
return 0;
}
for (int i = 0; ; i++)
{
if (++idx[i] == length)
{
idx[i] = 0;
}
key[i] = letters[idx[i]];
if (idx[i]) // We incremented idx[i] and it wasn't reset to 0, so this is a new combination to try
{
break;
}
else if (i == ksize-1) // We incremented idx[ksize-1] and it was reset to 0, so we've tried all possibilities without returning
{
done++;
break;
}
}
}
答案 2 :(得分:0)
在您不限制嵌套循环深度的情况下,即在查找带有一组字母的任意单词时,递归效果很好。
因此,以下功能对“字母”集中的单词进行了递归搜索,返回
这是一个模拟循环的递归函数:
int recursion(int keyIndex, char* key, char letters[], int length, const char *word) {
int depth = strlen(word);
int i;
for (i = 0; i < length; i++) {
key[keyIndex] = letters[i];
if (keyIndex == depth-1) {
if (strncmp(key, word, depth) == 0) {//crypt(key, salt), hash) == 0){
key[depth] = 0;
printf("found: %s\n", key);
return 0;
}
}
else {
int recStatus = recursion(keyIndex+1, key, letters, length, word);
if (recStatus == 0)
return 0;
}
}
return 1;
}
这是相同功能的更好实现。在您的情况下,它可能不起作用,因为您需要为'crypt'使用完整的最终字符串,但是它可以用于查找简单的单词。
int betterRecursion(int keyIndex, char *letters, int length, const char *word) {
int depth = strlen(word);
int i;
for (i = 0; i < length; i++) {
if (word[keyIndex] == letters[i]) {
if (keyIndex == depth-1) {
printf("found: %s\n", word);
return 0;
}
else
return betterRecursion(keyIndex+1, letters, length, word);
}
}
return 1;
}
,当然还有调用它们的主要功能:
int main() {
char key[256];
char *letters = "salt or not";
if(recursion(0, key, letters, strlen(letters), "salt") != 0)
printf("not found\n");
if (betterRecursion(0, letters, strlen(letters), "or not") != 0)
printf("not found\n");
return 0;
}
答案 3 :(得分:-1)
好的,让我们尝试将其重写为功能程序。我会继续使用Haskell,因为它是一种更好的工具,不仅适用于所有工作,而且不适用于所有特定工作。 C并非真正旨在优雅地完成这样的示例。
让我们从内而外开始。循环的内部内容如下:
key[0] = letters[a];
key[1] = letters[b];
key[2] = letters[c];
key[3] = letters[d];
key[4] = letters[e];
if (strcmp(crypt(key, salt), hash) == 0)
{
printf("%s\n", key);
return 0;
}
这取决于名为letters
,循环索引a
,b
,c
,d
和e
的数组,变量{ {1}},key
和salt
,以及库调用hash
。
我注意到存在终止条件。如果密文等于您要通过蛮力搜索解密的哈希,程序将打印当前密钥并退出。这些都是无法在纯函数中出现的副作用,尽管我们可以合并它们,但实际上我将做的是如果没有匹配项,则返回crypt
或Just key
。如果Nothing
的类型命名为key
,则返回类型为Key
。
参数Maybe Key
至a
分别从0到e
枚举。在一个功能程序中,我们可以设置这五个单独的参数,但是,我将声明length - 1
的类型别名Key
(五个字符)。
接下来,我们可以依次定义从(Char, Char, Char, Char, Char)
到('A','A','A','A','A')
到('A','A','A','A','B')
的整个键空间的列表。不幸的是,使('Z','Z','Z','Z','Z')
这样的范围可以工作is a little too complicated to use as an example of how nice functional code can be,的样板,但至少我可以直观地将其作为列表理解来编写。
[firstKey..lastKey]
请注意,由于Haskell是一种延迟评估的语言,因此它仅计算其实际使用的值。整个列表不是预先生成的。实际上,GHC完全可以避免创建链接列表对象。
外部函数的参数在循环的迭代之间不变,应该是我们外部函数的参数,我们将其称为allKeys = [(a, b, c, d, e) | a <- ['A'..'Z'],
b <- ['A'..'Z'],
c <- ['A'..'Z'],
d <- ['A'..'Z'],
e <- ['A'..'Z'] ]
。为了简单起见,我们假定一个无符号的16位盐和一个无符号的64位哈希。为简单起见,它将蛮力搜索整个密钥空间,而不是对其进行分区。
bruteForce
有多种写import Data.Word (Word16, Word64)
type Salt = Word16
type Hash = Word64
bruteForce :: Salt -> Hash -> Maybe Key
的方法,但是您要求递归解决方案,所以让我们编写一个辅助函数。递归助手的常规名称为bruteForce
或go
。我会选择bruteForce'
,因为它更短。由于它是嵌套的局部函数,因此可以引用参数go
和salt
。稍后,我将在使用它的函数中移动整个键空间的列表的定义:
hash
您可能已经注意到,遗失了一块。该尾部递归函数调用bruteForce :: Salt -> Hash -> Maybe Key
bruteForce salt hash = go allKeys
where
go :: [Key] -> Maybe Key
-- Terminating case: we exhausted the key space without finding a match.
go [] = Nothing
-- Terminating case: we found a match.
go (x:_) | crypt x salt == hash = Just x
-- Tail-recursive case: no match so far.
go (x:xs) = go xs
并将结果与crypt x salt
进行比较,如果相等,则返回值hash
。在这种情况下,Just x
是x
,而Key
是salt
,因此必须有一些Salt
函数使用crypt
且Key
并返回Salt
。
出于演示目的,我将对从(AAAAA,0x0000)→0到(ZZZZZ,0xFFFF)→778 657 857 535的每个可能的键/盐对进行简单的枚举。
将它们放在一起,我们得到:
Hash
当您记住只有module Crack (bruteForce, crypt) where
import Data.Word (Word16, Word64)
type Key = (Char, Char, Char, Char, Char)
type Salt = Word16
type Hash = Word64
bruteForce :: Salt -> Hash -> Maybe Key
bruteForce salt hash = go allKeys
where
allKeys = [(a, b, c, d, e) | a <- ['A'..'Z'],
b <- ['A'..'Z'],
c <- ['A'..'Z'],
d <- ['A'..'Z'],
e <- ['A'..'Z'] ]
go :: [Key] -> Maybe Key
-- Terminating case: we exhausted the key space without finding a match.
go [] = Nothing
-- Terminating case: we found a match.
go (x:_) | crypt x salt == hash = Just x
-- Tail-recursive case: no match so far.
go (x:xs) = go xs
crypt :: Key -> Salt -> Hash
crypt (a, b, c, d, e) salt = let
a' = fromLetter a
b' = fromLetter b
c' = fromLetter c
d' = fromLetter d
e' = fromLetter e
fromLetter x = fromIntegral ( fromEnum x -
fromEnum 'A' )
in
(((((a'*26 + b')*26 + c')*26 + d')*26) + e')*65536 +
(fromIntegral salt)
下的内容与您编写的代码示例相对应时,我们会发现该代码非常简单,而且速度相当快。
因此,进行了一些快速测试。如果我们的哈希为0x48080,则最后16位0x8080就是我们的盐。 (切勿编写这样的加密哈希函数!)其余的位0x4表示密钥编号4,其中零为AAAAA,即AAAAE。在REPL中进行测试:
bruteForce
检查往返转换:
*Crack> bruteForce 0x8080 0x48080
Just ('A','A','A','A','E')
一条评论反对我使用的语言不是C。现在,我喜欢C,但是坦率地说,它不是从事这项工作的正确工具。您的尾部递归函数将具有七个或八个参数以及许多特殊情况(或者通过使用全局和循环来“欺骗”)。
如果我想高效且简洁地在类似C的语言中使用这种功能习惯用法,我将使用LINQ和*Crack> crypt ('N','I','F','T','Y') 0xCEED
398799326957
*Crack> bruteForce 0xCEED 398799326957
Just ('N','I','F','T','Y')
用C#编写。这是C中的迭代代码到C#中的准功能代码的an example of a translation I’ve posted previously。还有here are the benchmarks的运行速度。
C#的实现可能非常相似:将整个键空间枚举为异步序列,然后对其进行扫描以查找与给定的盐匹配哈希值的第一个键。
您可以更改上面的Haskell程序来做到这一点,并将yield return
减少为一行,即高阶函数调用:
bruteForce