为了更好地学习多线程,我编写了一个程序来破解ZIP文件的密码。这有点慢,在大约1:45分钟内处理95个可打印ASCII字符的三位数密码。这是我的实际处理破解的类:

class DecryptPassword
{
    private readonly List<char> _charList;
    private readonly int[] _currentPassword;
    private readonly string _endPassword;

    public DecryptPassword(List<char> charList, string startPassword, string endPassword)
    {
        _charList = charList;
        _endPassword = endPassword;

        var passwordLength = Math.Max(startPassword.Length, endPassword.Length);

        _currentPassword = startPassword.Select(c => _charList.IndexOf(c)).ToArray();

        while (_currentPassword.Length != passwordLength)
        {
            _currentPassword = _currentPassword.Concat(new [] {0}).ToArray();
        }
    }

    public string CalculatePassword()
    {
        while (true)
        {
            var password = GetPasswordAsString();

            try
            {
                if (ZipFile.CheckZipPassword(Environment.GetFolderPath(Environment.SpecialFolder.Desktop) + "\CrackMe3.zip", password))
                {
                    return password;
                }
            }
            catch (BadCrcException)
            {
                // For some reason, sometimes a BadCRCException is thrown.
                // I have never had it thrown on the real password,
                // but this may be an issue for that.
                // My best guess is that the speed of access the file,
                // or perhaps accessing it from multiple threads, is the issue
            }

            if (password == _endPassword) { break; }

            CalculateNextPassword();
        }

        return null;
    }

    private void CalculateNextPassword()
    {
        for (var index = _currentPassword.Length - 1; index >= 0; index--)
        {
            if (_currentPassword[index] == _charList.Count - 1)
            {
                _currentPassword[index] = 0;
                continue;
            }

            _currentPassword[index]++;
            break;
        }
    }

    private string GetPasswordAsString()
    {
        return new string(_currentPassword.Select(i => _charList[i]).ToArray());
    }
}


构造函数采用要使用的字符集以及密码的开始和结束范围。现在,它假定两个密码的长度相同,这在知道密码中的字符数时很有用。

CalculatePassword()遍历密码范围,并在找到密码时返回密码。如果未找到密码,则返回null

CalculateNextPassword()将以与base-N数学中的加法类似的方式计算下一个密码。

GetPasswordAsString()将返回当前密码。密码,以字符串形式存储在int的数组中。

我生成这样的任务,让Windows来管理线程。基于在调试模式下的观察,它确实会创建多个线程,但不会创建95个线程:

// characters sorted by ASCII code
private static readonly List<char> CharList = new List<char>
{
    ' ',
    '!',
    '"',
    '#',
    '$',
    '%',
    '&',
    '\'',
    '(',
    ')',
    '*',
    '+',
    ',',
    '-',
    '.',
    '/',
    '0',
    '1',
    '2',
    '3',
    '4',
    '5',
    '6',
    '7',
    '8',
    '9',
    ':',
    ';',
    '<',
    '=',
    '>',
    '?',
    '@',
    'A',
    'B',
    'C',
    'D',
    'E',
    'F',
    'G',
    'H',
    'I',
    'J',
    'K',
    'L',
    'M',
    'N',
    'O',
    'P',
    'Q',
    'R',
    'S',
    'T',
    'U',
    'V',
    'W',
    'X',
    'Y',
    'Z',
    '[',
    '\',
    ']',
    '^',
    '_',
    '`',
    'a',
    'b',
    'c',
    'd',
    'e',
    'f',
    'g',
    'h',
    'i',
    'j',
    'k',
    'l',
    'm',
    'n',
    'o',
    'p',
    'q',
    'r',
    's',
    't',
    'u',
    'v',
    'w',
    'x',
    'y',
    'z',
    '{',
    '|',
    '}',
    '~',
};

static void Main()
{
    List<Task<string>> tasks;
    StartTasks(out tasks);

    Console.WriteLine(tasks.First(t => t.Result != null).Result);
}

private static void StartTasks(out List<Task<string>> tasks)
{
    var source = new CancellationTokenSource();
    var token = source.Token;

    // split problem into 95 tasks, each group calculates as follows:
    // "c  ", "c !" "c "", ... "c! ", "c!!", ... "c~}", "c~~"
    tasks = CharList.Select(c => StartTask(c + "  ", c + "~~", token)).ToList();

    while (true)
    {
        Task.WaitAny(tasks.ToArray());

        if (tasks.Any(t => t.Result != null))
        {
            Console.WriteLine("Cancelling");
            source.Cancel();
            break;
        }
    }
}

static async Task<string> StartTask(string start, string end, CancellationToken token)
{
    var decryptor = new DecryptPassword(CharList, start, end);
    var task = new Task<string>(() => decryptor.CalculatePassword(), token);
    task.Start();
    return await task;
}


我的清单下一步是生成更多任务,尤其是在处理更大的任务时密码,因此欢迎您提供有关如何更有效地将每个任务的密码分为几组的基本指示,但是请不要提供完整的解决方案-我需要自己考虑一下。

评论

只是一个问题...这显然是对“我在我们的基地杀死曼人”的重复。由于这是一个有趣的标题,是否应将其编辑为使用“ ur”而不是“ your”?我知道我们经常对此不满意,但是“您的”与以后对该问题的搜索没有任何关系,应该通过关键字“ crack”和“ password”以及这些标记来获取。

就我的喜好而言,语法太混乱了。

好!只是一个想法。 :)

@Almo:knowyourmeme.com/memes/in-ur-base,它是“杀了你的帅哥”,您可以自由决定使用et语拼写。

这是我指的版本:blogcdn.com/massively.joystiq.com/media/2008/03/…我一直都这样看,从未见过拼写更好的版本。 :)

#1 楼

将CancellationToken传递给Task构造函数使Cancel()仅在任务尚未启动时才能工作。还将令牌传递给CalculatePassword()并在每次循环迭代中对其进行检查:

while (true)
{
    token.ThrowIfCancellationRequested();
    // ...
}


访问Task.Result最终会调用Task.Wait()。因此,您正在等待任何任务完成,然后再次等待第一个任务。通常,仅在任务已经离开主线程时才从Task.Wait()调用Main()。这是一种简化任务代码的方法:

static void Main()
{
    // Start with a background task to avoiding deadlocking the main thread.
    string result = Task.Run(async () => await FindResultAsync()).Result;
    Console.WriteLine(result);
}

private static async Task<string> FindResultAsync()
{
    var source = new CancellationTokenSource();
    var token = source.Token;

    // split problem into 95 tasks, each group calculates as follows:
    // "c  ", "c !" "c "", ... "c! ", "c!!", ... "c~}", "c~~"
    List<Task<string>> tasks = CharList.Select(
        (c) =>
        {
            var decryptor = new DecryptPassword(CharList, c + "  ", c + "~~");
            return Task.Run(() => decryptor.CalculatePassword(token), token);
        }).ToList();

    foreach (Task<string> task in tasks)
    {
        string result = await task;
        if (result != null)
        {
            Console.WriteLine("Cancelling");
            source.Cancel();
            return result;
        }
    }

    return null; // No result.
}


这里仍然有优化的机会。例如,您可以弄清楚如何在完成任务时而不是按顺序评估任务。

考虑将DecryptPassword重构为静态类,并将类成员转换为参数。无状态设计通常更易于并行化,因为您不必担心共享状态。有关无状态异步设计的示例,请参见Hushpuppy.Http.HttpServer。

最后,Stephen Cleary在Task.Run Etiquette上的出色文章确实帮助我了解了如何创建和使用任务,这是必需的。为对此主题感兴趣的人阅读。

#2 楼

由于任务的工作原理,您看不到95个线程。 Task类的文档说:“任务通常在线程池线程上异步运行”。线程池将限制一次执行多少个任务,因此当池达到其限制时调用task.Start()时,直到其中一个池线程完成运行并可用后,任务才开始执行。

由于这些线程是计算密集型线程,因此运行的线程多于硬件线程实际上会由于额外的任务切换和相关的高速缓存未命中而使速度稍微放慢。

评论


\ $ \ begingroup \ $
是的,我知道一个Task!=一个线程,而且我听说过最后一个问题。不过,好的答案是+1。
\ $ \ endgroup \ $
–user34073
2015年12月11日15:12

#3 楼

假设存在可能的错误

我假设您仅使用长度与Math.Max(startPassword.Length, endPassword.Length);相同的密码对它进行了测试,如果是的话,我认为如果您不知道该密码,它将无法正常工作。密码的长度。

int[]数组的长度与Max()调用的结果相同,每个重叠部分将被当作0放入数组,该数组等于" "中的charList

假设文件的密码为"aaa",并且传递了startPassword = "aaa"endPassword = "aaaa",那么对GetPasswordAsString()的首次调用将返回"aaa ",而显然不是"aaa"

命名
>
类名应由名词或名词短语组成,但DecryptPassword的命名更像是一种方法。实际上,这将是一种方法的好名字。

构造函数



_currentPassword可以像这样更惯用地创建

_currentPassword = startPassword.Select(c => _charList.IndexOf(c))
                   .Concat(Enumerable.Repeat(0, passwordLength - startPassword.Length))
                   .ToArray();


>由于既不更改charList,也不使用任何List<T>的方法,应将其更改为IList<char>


CalculatePassword()


Environment.GetFolderPath(Environment.SpecialFolder.Desktop) + "\CrackMe3.zip"  



因为这永远不会改变,所以您不应该重新创建随时循环。将完整的文件名传递给构造函数或此方法。


考虑



预计算密码并将其传递给CalculatePassword()方法
创建该zip文件的副本,以便每个任务都可以执行在单独的文件上以避免文件锁定
将zip文件加载(如果可能)到内存中,以避免I / O瓶颈
使用char[]而不是int[]数组,这将消除调用的需要Select()


评论


\ $ \ begingroup \ $
关于将文件加载到内存中:在Linux上,我将使用vmtouch将其放入缓存中,这样我就不必担心它是否真正适合内存,并获得最高速度如果有的话。我不知道如何在C#上做到这一点。
\ $ \ endgroup \ $
– Davidmh
2015年12月11日14:12在

\ $ \ begingroup \ $
该错误是已知的,但目前仍被视为边缘情况。我从未打算将其真正用于破解密码,而只是作为异步解决耗时任务的研究。
\ $ \ endgroup \ $
–user34073
2015年12月11日15:46

\ $ \ begingroup \ $
@Davidmh:您的意思是hoytech.com/vmtouch?是的,但是您不需要vmtouch就可以了。任何操作系统的LRU分页算法都会将重复使用的页面保留在核心中。担心手动固定任何页面听起来是个坏主意。最佳:在生成线程之前,先映射zip,以便它们都已映射。 (比每个线程分别打开/映射更容易,但是它们仍然共享相同的内存页)。不过,感谢您提到vmtouch:它对于查询页面缓存很有用。
\ $ \ endgroup \ $
– Peter Cordes
2015年12月12日19:41



\ $ \ begingroup \ $
@PeterCordes vmtouch将它们放入缓存中,但让操作系统在认为合适时将其逐出。就我个人而言,我在知道自己要比操作系统做得更好时会使用它(例如,在我开始处理它们的同时预加载一堆文件,隐藏IO延迟或在出现以下情况时明确驱逐它们)我知道我已经做好了腾出空间的准备。对我而言,最大的好处就是该接口仍然是一个文件,因为我使用的许多库都要求该接口作为输入。
\ $ \ endgroup \ $
– Davidmh
2015年12月12日23:42

\ $ \ begingroup \ $
@Davidmh:我知道您不是在建议使用-l mlock选项。我的观点是,对于此任务,仅需要将zip文件中用于检查密码的部分进行分页。并且,密码检查程序将使这种情况单独发生。没有非线性访问模式可以提高预缓存文件范围的效率。在这种情况下,将立即进行第一次密码检查,因此没有任何窗口可以隐藏I / O延迟。如果仅需查看标题即可检查zip密码,则在整个文件中进行分页是不好的。
\ $ \ endgroup \ $
– Peter Cordes
15年12月13日在1:43