【问题标题】:MD5 brute-force attack - efficient multithreaded implementationMD5蛮力攻击——高效的多线程实现
【发布时间】:2014-08-20 05:27:55
【问题描述】:

我想实现一个多线程 MD5 蛮力攻击算法(在 C++ 中)。我知道 Rainbow 表和字典,但我不会实现最高效的 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 符号变体的同一线程

有人知道,该算法是如何在“现实世界”的蛮力应用中实现的吗?也许某种线程池?

【问题讨论】:

    标签: c++ multithreading md5 brute-force


    【解决方案1】:

    我发现相当灵活的方法是生成运行以下代码的线程:

    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,我希望它大约是数百万个密码。

    【讨论】:

    • 所以你建议为每个线程生成一个密码块?大量分配不会产生高内存消耗吗?
    • 您可以(并且应该)只分配一次内存(每个线程),然后重新使用它。此外,对于蛮力攻击,PASSWORD_BLOCK 实际上可以非常简单和小,例如只保存起始密码和密码计数(即不保存所有密码)。
    • 有道理,会考虑
    • 不管怎样,有什么方法可以检查每个线程的 N 个密码吗?我正在考虑为每个“低阶”变体生成线程,即为 N 个符号密码创建一个单独的线程来查看最后 4 个符号变体。例如:AAAA____:第 1 线程,AAAB____:第 2 线程,...
    • 创建线程是一项昂贵的操作,因此如果性能很重要,那么您应该重新使用现有的线程。请注意,您可以通过让get_next_password_block() 返回“AAAA____”、“AAAB____”、“AAAC____”等来实现与您提议的行为类似的行为。
    【解决方案2】:

    这样做的“正确”方法是拥有一个工作池(等于 CPU 内核的数量,或者不计算超线程内核,或者将它们全部计为“一个”)和一个无锁 FIFO 队列您提交十万左右的任务组。这在同步开销和负载平衡之间提供了可接受的平衡。
    诀窍是将工作分成相对较小的组,因此只有一个线程在做最后一组的时间不会太长(那里没有并行性!),但同时不要让组太小 em> 所以你会受到同步/总线争用的约束。 MD5 相当快,所以几万到十万个工作项应该没问题。

    但是,考虑到具体问题,这实际上是矫枉过正。太复杂了。

    5 个字母的密码是 4 个字母的密码的 26 倍,6 个字母的密码是 5 个字母的密码的 26 倍,以此类推。换句话说,最长的密码长度到目前为止在组合总数中所占的份额最大。所有 4、5、6 位组合加起来仅占所有 7 位组合组合的 3.9% 左右。换句话说,它们完全微不足道。 总运行时间的 96% 都在 7 位数字组合之内,无论您如何处理其余部分。如果你考虑字母和数字或大写,那就更极端了。

    因此,您可以简单地启动与 CPU 内核一样多的线程,并在一个线程中运行所有 4 位组合,在另一个线程中运行所有 5 位组合,依此类推。这不是很好,但已经足够好了,因为无论如何没有人会注意到差异。
    然后简单地将可能的 7 位组合划分为 num_thread 大小相等的范围,并让每个完成其初始范围的线程继续使用该范围。
    工作并不总是完美平衡的,但它会在 96% 的运行时。而且,它与任务管理(无)和同步(只需要设置一个全局标志以在找到匹配项时退出)的绝对最小值一起工作。

    由于即使您进行了完美、正确的任务调度(因为线程调度掌握在操作系统手中,而不是您的手中),您也无法期望完美的负载平衡,因此这应该非常接近“完美”的方法。

    或者,您可以考虑启动一个额外的线程,该线程执行所有但最长的组合范围(“微不足道的 3%”),并将其余部分平均分配。这会在启动期间导致一些额外的上下文切换,但另一方面会使程序逻辑更加简单。

    【讨论】:

      【解决方案3】:

      从两个角度来看,将任务手动分区到工作线程都不是有效的:花费的精力和产生的负载平衡。现代处理器和操作系统增加了不平衡,即使最初看起来非常平衡的工作负载也是由于:

      • 高速缓存未命中:一个线程可能会命中高速缓存,而另一个线程可能会遭受高速缓存未命中,每次内存加载操作花费多达数千个周期,而相同的加载可以在几个周期内执行。
      • 涡轮增压、电源管理、核心停放功能。处理器本身和操作系统都可以管理导致不平衡的计算单元的频率和可用性。
      • 线程抢占:现代多任务操作系统中运行的其他进程可能会暂时中断线程的执行流程。

      现代工作窃取调度算法在将不平衡的工作映射和负载平衡到工作线程方面非常有效:您只需描述您具有潜在并行性的位置,任务调度程序将其分配给可用资源。工作窃取是一种分布式方法,不涉及一个共享状态(例如迭代器),因此没有瓶颈。

      查看,了解有关此类调度算法实现的更多信息。 此外,它们对嵌套和递归并行结构很友好,例如:

      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]);
      }
      

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 2023-03-30
        • 2011-06-10
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2011-06-01
        • 1970-01-01
        • 2013-05-28
        相关资源
        最近更新 更多