我想实现一个多线程MD5暴力攻击算法(用C ++编写)。我知道彩虹表和词典,但我不会实现最有效的MD5破解程序,只对蛮力算法感兴趣
问题是如何在线程之间分配所有可用长度的所有密码变体。例如,要恢复仅包含4到6个符号的小写字符的密码,我们应该查看N = 26 ^ 4 + 26 ^ 5 + 26 ^ 6 = 321254128组合(根据重复公式的变化,Vnk = n ^ k)的
因此,在例如8个线程之间的等部分中分配所有排列,我们应该知道每个(N / 8)* t变化,其中t =(1..7)。请注意,这些变化具有不同的长度(4,5,6),并且4-5个符号的变化可以被推送到具有一些6个符号变化的相同线程
有谁知道,该算法是如何在"真实世界"蛮力申请?也许某种线程池?
答案 0 :(得分:2)
我发现非常灵活的方法是生成运行以下代码的线程:
void thread_fn() {
PASSWORD_BLOCK block;
while (get_next_password_block(&block) {
for (PASSWORD password in block) {
if (verify_password(password)) set_password_found(password);
}
}
}
通常,如果代码经过充分优化,您将生成与活动核心一样多的线程;但是在某些情况下,启动比核心更多的线程可以提供一些性能提升(这指向次优的代码优化)。
get_next_password_block()
是完成所有锁定和同步的地方。该功能负责跟踪密码列表/范围,增加密码等。
为什么要使用PASSWORD_BLOCK
而不只是一个密码?好吧,MD5是一个非常快速的算法,所以如果我们为每个密码调用get_next_password_block()
,那么锁定/递增的开销将是极端的。此外,SIMD指令允许我们执行批量MD5计算(一次4个密码),因此我们希望快速有效地获取大量密码以减少开销。
块的特定大小取决于CPU速度和算法复杂度;对于MD5,我希望它能达到数百万的密码。
答案 1 :(得分:1)
从两个视图中将任务手动分区到工作线程效率不高:花费的精力和产生的负载平衡。现代处理器和操作系统增加了不平衡,即使最初看起来非常均衡的工作负载也是由于:
现代工作窃取调度算法在甚至不平衡的工作到工作线程的映射和负载平衡方面非常有效:您只需描述您具有潜在并行性的位置,并且任务调度程序将其分配给可用资源。工作窃取是一种分布式方法,不涉及一个共享状态(例如迭代器),因此没有瓶颈。
查看cilk,tbb或ppl,了解有关此类调度算法实施的更多信息。 而且,它们对嵌套和递归的并行结构很友好,如:
void check_from(std::string pass) {
check_password(pass);
if(pass.size() < MAX_SIZE)
cilk_for(int i = 0; i < syms; i++)
check_from(pass+sym[i]);
}
答案 2 :(得分:1)
执行此操作的“正确”方法是拥有一个工作池(等于CPU核心数,不计算超线程核心,或将所有核心计为“一”)和一个无锁FIFO队列你提交了十万个左右的任务组。这在同步开销和负载平衡之间提供了可接受的平衡 诀窍是将工作划分为相对较小的组,因此只有一个线程仍在执行最后一个组的时间不会太长(那里没有并行性!),但同时不会使组太小所以你受到同步/总线争用的约束。 MD5非常快,所以几万到几十万件工作项应该没问题。
然而,鉴于具体问题,这实际上是矫枉过正。太复杂了。
5字母密码比4字母密码多26倍,6字母密码比5字母密码多26倍,依此类推。换句话说,最长的密码长度到目前为止是组合总数中的最大份额。所有4,5,6位数字组合仅占所有7位数组合的3.9%。换句话说,它们完全没有意义。 总运行时间的96%在7位数组合中,无论您如何处理其余部分。如果你考虑字母和数字或大写,那就更加极端了。
因此,您可以简单地启动与CPU核心一样多的线程,并在一个线程中运行所有4位数组合,在另一个线程中运行所有5位数组合,依此类推。这不是很好,但它已经足够好了,因为无论如何没有人会注意到差异
然后简单地将可能的7位组合划分为num_thread
相等大小的范围,并使每个完成其初始范围的线程继续使用该范围。
工作并不总是完美平衡,但它将在96%的运行时间内完成。而且,它适用于任务管理(无)和同步的绝对最小值(仅需要设置一个全局标志,以便在找到匹配时退出)。
由于你不能指望完美的负载均衡,即使你做了完美的,正确的任务调度(因为线程调度掌握在操作系统的手中,而不是你的),这应该非常接近“完美”的方法。
或者,您可以考虑启动一个额外的线程,该线程执行整个全部但最长的组合范围(“无关紧要的3%”)并平均分配其余部分。这将在启动期间引起一些额外的上下文切换,但另一方面使程序逻辑更简单。