Я в ваших .zips взламывает ваши пароли

Как попытка узнать многопоточность лучше, я написал программу для взлома пароля ZIP-файла. Это медленный процесс, обрабатывающий трехзначный пароль из 95 печатных символов ASCII примерно через 1:45 минут. Это мой класс, который фактически обрабатывает треск:

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 s, в виде строки.

Я создаю свои задачи таким образом, оставляя 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;
}

Далее в моем списке нужно создать больше задач, особенно при работе с большими паролями, поэтому основные указатели на то, как разделить пароль на группы для каждой задачи более эффективно, будут приветствоваться, но, пожалуйста, не предоставляйте полное решение - я нужно думать об этом через меня.

44 голоса | спросил Hosch250 11 FriEurope/Moscow2015-12-11T06:06:09+03:00Europe/Moscow12bEurope/MoscowFri, 11 Dec 2015 06:06:09 +0300 2015, 06:06:09

3 ответа


13

Передача CancellationToken в конструктор задачи позволяет 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 для примера асинхронного дизайна без атак.

Наконец, отличные статьи Стивена Клири о Task.Run Etiquette действительно помогли мне понять, как создавать и потреблять задачи, и должны быть прочитаны для всех, кто интересуется этой темой.

ответил piedar 12 SatEurope/Moscow2015-12-12T01:54:22+03:00Europe/Moscow12bEurope/MoscowSat, 12 Dec 2015 01:54:22 +0300 2015, 01:54:22
35

Вы не видите 95 потоков из-за того, как работают задачи. документация для класса Task говорит, что «задачи обычно выполняются асинхронно в потоке пула потоков». Пул потоков ограничит количество задач, которые будут выполняться за один раз, поэтому, когда вы вызываете task.Start(), когда пул достиг своего предела, эта задача не будет запускаться до тех пор, пока один из потоков пула заканчивается и становится доступным.

Так как эти потоки с интенсивным вычислением, имеющие больше потоков, чем у вас есть аппаратные потоки, на самом деле немного замедляют работу из-за дополнительных переключений задач и связанных с ними промахов кэша.

ответил 1201ProgramAlarm 11 FriEurope/Moscow2015-12-11T09:46:10+03:00Europe/Moscow12bEurope/MoscowFri, 11 Dec 2015 09:46:10 +0300 2015, 09:46:10
25

Предположение о возможной ошибке

Я предполагаю, что вы только протестировали это с паролем, длина которого совпадает с Math.Max(startPassword.Length, endPassword.Length);, и если это правда, я думаю, t, если вы не будете знать длину пароля.

Массив 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-файл в память, чтобы избежать узкого места ввода-вывода
  • , чтобы использовать char[] вместо массива int[], это устранит необходимость вызова Select().
ответил Heslacher 11 FriEurope/Moscow2015-12-11T10:49:39+03:00Europe/Moscow12bEurope/MoscowFri, 11 Dec 2015 10:49:39 +0300 2015, 10:49:39

Похожие вопросы

Популярные теги

security × 330linux × 316macos × 2827 × 268performance × 244command-line × 241sql-server × 235joomla-3.x × 222java × 189c++ × 186windows × 180cisco × 168bash × 158c# × 142gmail × 139arduino-uno × 139javascript × 134ssh × 133seo × 132mysql × 132