Имеется файл CrackMe, при запуске которого предлагается ввести ключ лицензии:

Исходя из подсказок после неправильного ввода ключа, можно понять, что в пароле должно быть обязательно 8 цифр, то есть от 00000000 до 99999999, на данном этапе можно переходить к коду программы, точнее тому что получится достать из .exe

Если просто открыть программу в .net Reflector, мы увидим следующее:

Это последствия обфускции, работать с таким кодом можно, но достаточно проблематично, поэтому воспользуемся небезызвестным de4dot, который имеет встроенные алгоритмы деобфускации (основанные на известных обфускаторах, от кастомных это дело не поможет)

Ну и сменим инструмент просмотра кода на бесплатный dotPeek, он несколько удобней чем Reflector, но не позволяет изменять код программы. Впрочем на данном этапе, мы хотим узнать алгоритм верификации пароля, а не обойти его, но в конце статьи я покажу, как можно изменить проверку, и входить с практически любым паролем.

Открываем почищенный de4dot файл CrackMe в dotPeek, и видим следующее

Методы конечно не носят оригинальных названий, как и классы, но уже гораздо более читабельно, а главное можно увидеть строковые переменные, по которым быстро можно найти нужное место.
Как видно строка,

"Nope. A license key consists of 8 numbers, e.g. 31628594 or 71439532!"

которую мы видели при первом запуске программы находится в открытом виде, по ней мы и нашли нужное место кода с проверками пароля.

Логика работы:

В бесконечном цикле while, консоль CrackMe принимает данные от пользователя, передает в функцию smethod_0, возвращаемым значением которой является bool, который равен true если пароль верен, и false если нет.

При этом if(! инвертирует это значение, и в случае с возвращенным false мы получаем вывод о неверном пароле,
Console.WriteLine("Nope. A license key consists of 8 numbers, e.g. 31628594 or 71439532!");
А в случае true переходим в else, где посредствам оператора break выходим из бесконечного цикла, и переходим к удивленному вопросу автора CrackMe

Осталось разобраться, что же происходит в функции smethod_0, для последующего воссоздания алгоритма и получения списка возможных паролей (KeyGen)

В целом никакой сложной математики тут нет, пробежимся по коду:

В функцию передается строковое значение полученное из пользовательского ввода консоли, проверяется на длину, и если она не равна 8 символам, то сразу же возвращает false, ибо такой пароль точно не подходит вне зависимости от его содержания.

Далее, в try выполняется преобразование строки в числовое значение int, а если же оно содержит любые символы кроме цифр, то выполняются действия в операторе catch, которые так же возвращают false, честно сказать не знаю, зачем использовался goto, вместо return false, может автор так хотел запутать пользователя, либо как-то так код деобфусцируется, но имеем, то что имеем.

Далее если это число, и оно длинной в 8 цифр, переходим к основным проверкам, которые выглядят так:

Всего нашему паролю предстоит пройти 4 проверки, каждая из них использует smethod_1, в котором происходит выборка подстроки из строки, со сдвигом указанным в int_0, и возвращается из этого метода, всегда одна цифра, так как вторым аргументом Substring, всегда будет 1.

Теперь, понимая как это работает, можно составить условия на все 4 проверки:

Все число должно делится на первую цифру числа без остатка
✦ Сумма значений 3 цифры и 6 цифры числа = значению 2 цифры
4 цифра * на 8 цифру должна делится на 2 без остатка
7 цифра - 6 цифра = 1 цифре версии установленного .net framework (В нашем случае это будет 4)

Ну а теперь надо лишь заставить эти методы работать в нашу пользу.
Немного модифицируем изначальные методы, для работы с Paraller.For, ибо предстоит 90 миллионов комбинаций паролей перебрать, и это будет в разы быстрее, на многоядерном компьютере. (13 секунд, против 89 при использовании обычного For, на моем ноутбуке)

И я намеренно упускаю, первые 10 миллионов паролей от 00000000 до 09999999, дабы не подставлять нули вперед значения строки, нашу задачу это никоим образом не обесценит, просто полученное количество возможных паролей, будет немного меньше возможного. Но нам то нужен всего 1, не так ли?

Вот код, который у меня получился, он запишет все возможные пароли в txt файл.
Я не профессиональный программист, это просто мое хобби, так же как и реверс инжиниринг, так что за реализацию сразу прощу прощения, если есть что добавить, прошу об этом написать, мне и самому будет интересно узнать.

using System;
using System.Diagnostics;
using System.IO;
using System.Threading.Tasks;

namespace ConsoleApp10
{
    internal class Program
    {
        internal static int nfmV;
        internal static string allPwd;

        private static void Main(string[] args)
        {
            nfmW = smethod_1(Environment.Version.ToString(), 0);
            Console.WriteLine("Начат парсинг подходящих паролей");
            Stopwatch stw = new Stopwatch();
            stw.Start();
            Parallel.For(10000000, 99999999, smethod_0);
            stw.Stop();
            Console.WriteLine($"Закончено за: {stw.ElapsedMilliseconds / 1000.0} сек.");

            using (StreamWriter sw = new StreamWriter("paswds.txt"))
            {
                sw.Write(allPwd);
            }
            Console.ReadLine();

        }
        private static void smethod_0(int x, ParallelLoopState pls)
        {
            string string_0 = Convert.ToString(x);
            bool flag = false;

            int num = -1;
            try { num = int.Parse(string_0); }
            catch { flag = false; }

            try { flag = num % smethod_1(string_0, 0) == 0 &&
                    (smethod_1(string_0, 2) + smethod_1(string_0, 5) == smethod_1(string_0, 1) &&
                    (smethod_1(string_0, 3) * smethod_1(string_0, 7) % 2 == 0 &&
                     smethod_1(string_0, 6) - smethod_1(string_0, 5) == nfmW)); }
            catch { flag = false; }

            if (flag)
            {
                allPwd += $"{x}\n"; 
            }
        }
        private static int smethod_1(string string_0, int int_0) => int.Parse(string_0.Substring(int_0, 1));
    }
}

В результате получаем долгожданный результат, и удивление автора, как же мы все таки это сделали?!

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

Для этого мы будем использовать уже знакомый нам .net Reflector с плагином Reflexil, который позволяет делать изменения в IL коде, с последующей пересборокой .exe

Нужно найти в коде строку How did you got that?! и идущий перед ней опкод изменить с brtrue.s на brfalse.s

Тем самым изменить поведения оператора IF, теперь он будет считать все неправильные пароли верными и наоборот

На этом у меня все, спасибо, что дочитали.

P.S.: Сайт с CrackMe, если решите попрактиковаться: линк

Комментарии (24)


  1. KvanTTT
    04.08.2021 00:08
    +1

    А каким обфускатором программа была запакована, с какими настройками?


    Если просто открыть программу в .net Reflector, мы увидим следующее:

    Есть dnSpy — он тоже позволяет менять код, но еще бесплатный и опенсорсный.


    Нужно найти в коде строку How did you got that?! и идущий перед ней опкод изменить с brtrue.s на brfalse.s

    В теории можно и этому помешать: проверять хеш-код всей программы или даже отдельных методов. Это, конечно, тоже можно взломать, но потребуется больше времени (но в этом и смысл обфускации — затратить как можно больше времени у взломщика).


    1. Scrypto Автор
      04.08.2021 00:11
      +1

      Обфускатор .net Recator.
      За dnSpy спасибо, посмотрю что за зверёк.
      По поводу проверки хеша программы, да, но он не спасет если на руках алгоритм проверки пароля.


    1. Slav2
      04.08.2021 03:29
      +1

      А как вы бы предложили реализовать такую проверку? Я сейчас так и делаю, но приходится компилировать защищаемый файл многократно чтобы в контрольной сумме файла появилась нужная комбинация чисел.


      1. maaGames
        04.08.2021 06:25
        +3

        Хранить контрольную сумму файла не в самом ехе файле, а в другой библиотеке. Конрольную сумму библиотеки в другой библиотеке. И так далее. И проверку делать не сразу при запуске программы, а через некоторое время. Чем больше времени от запуска до срабатывания произойдёт - тем лучше. И проверять не все конрольные суммы разом. В разных местах вызовы проверки сумм из разных библиотек выполнять. И это должна быть не одна и та же функция, а для каждой проверки отдельная фукнция, чтобы каждую отдельно взламывать пришлось. Даже одну и ту же проверку можно делать не каждый раз, а с какой-то вероятностью или только после нескольких включений программы, чтобы радостный хакер опубликовал недоломанную сборку. Ну и т.д и т.п.


    1. fedorro
      04.08.2021 22:26

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


  1. rroyter
    04.08.2021 07:52
    +4

    Писать крэкми на с# довольно бестолково, как раз потому что получить исходник на довольно высоком уровне (а не в ассемблерных коммандах) раз плюнуть. Ну только если сама валидация не вынесена в отдельную сишную, ламповую дллку. Ну а имея исходник зареверсить логику тривиальная задача.

    Толи дело поставить бряк в SoftICE на какой нибудь DialogBoxW и раскручивать стек вверх анализируя условные и безусловные переходы... Было дело.


    1. freeExec
      04.08.2021 09:48
      +1

      Если не отлаживать пошагово, то сишноподобный код так же хорошо понятен, потому что стандарные методы декомпилятор за тебя определил и обозвал. И ты погружен только в логику что написал сам программист.


    1. qw1
      04.08.2021 12:01
      +3

      Со времён SoftICE появился HexRays в IDA, и он по сишному бинарнику восстанавливает более-менее понятный сишный код.

      С другой стороны, в c# можно так же навернуть виртуальную машину с самомодифицирующимся байт-кодом, и разобраться будет очень сложно, придётся начинать с написания инструментария.


    1. KvanTTT
      04.08.2021 12:45
      +1

      Писать крэкми на с# довольно бестолково, как раз потому что получить исходник на довольно высоком уровне (а не в ассемблерных коммандах) раз плюнуть.

      Может крэкми и бесполезно, но в целом обфускация имеет смысл, особенно если нужно извлечь логику из программы, а не просто взломать ее. Злоумышленнику нужно убирать всяческие защиты и восстанавливать идентификаторы. Последнее придется делать заново для каждой следующей версии, т.к. идентификаторы меняются рандомно и это происходит с потерей информации.


    1. Alexey2005
      04.08.2021 21:22

      Ну а имея исходник зареверсить логику тривиальная задача.
      Но только не в случае C#, где у вас без отладчика попросту нет способа определить, при каких условиях вызовется тот или иной кусок кода.
      Потому что динамическое связывание, потому что неявные вызовы, потому что код активируется по событию внутри события внутри события.
      Добавьте многопоточность, и в этой каше сам чёрт ногу сломит. Вся логика приложения нарезана на мелкие кусочки, каждый из которых вы конечно можете отреверсить, но собрать из них цельный паззл на одном только декомпиляторе у вас не выйдет — нет явно прописанных условий перехода от одного фрагмента к другому.
      ИМХО, даже сильно обфусцированные C++ приложения (например, замусоренные чем-то вроде ExeCryptor) изучать легче, чем эту невнятную кашу. Восстановить логику работы чего-то сложнее Hello World можно только в отладчике, да и то усилий нужно приложить очень много.


  1. Lirix_vladimir
    04.08.2021 09:52
    +2

    а крэклаб (exelab) еще жив? раньше там море таких статей было и обсуждения интересные


  1. xi-tauw
    04.08.2021 10:41
    +3

    Автор , если вы раскопали логику правил проверки, то логичнее было бы подбирать числа не перебором всех, а подбирать конкретные цифры.

    Смотрим на последнее правило (разница в 4) - пары простые 04, 15, 26, 37, 48, 59 Для каждой из этих пар определена шестая цифра, значит легко подобрать все пары к ним для второго правила (перебираем третью цифру, вычисляем вторую).

    Так за ~60 итераций мы узнали все возможные цифры на четырех позициях. Третье правило дает, что 4 и 8 цифра не должны быть одновременно нечетными. Проверим 100 комбинаций, получим 75 пар.

    За 60*75 итераций мы получаем все цифры на шести позициях. Для каждой добавляем еще проверку перебором пятой позиции (она в правилах не фигурирует) и первой, проверяя делимость. Итого ~450000 итераций против ~100000000, а это два порядка.


    1. Scrypto Автор
      04.08.2021 10:53

      Спасибо, интересно, учту.


      1. qw1
        04.08.2021 12:04

        Если стоит задача оптимизировать, то лучше от строк избавиться.
        Чтобы получить n-ую цифру в числе, не обязательно конвертировать его в строку, вырезать цифру и конвертировать обратно в число.


        1. Scrypto Автор
          04.08.2021 12:28

          Нет, конкретно такой задачи не стояло.
          Но да, можно через процент от деления нужный разряд выбрать.


          1. Scrypto Автор
            04.08.2021 12:28

            Уже голова перестала сооражать, остаток от деления


    1. Scrypto Автор
      04.08.2021 21:54
      +1

         private static void smethod_0(int x, ParallelLoopState pls)
              {
                  int num = x;
      
                  int x1 = num / 10000000;
                  if (num % x1 != 0) return;
                  num = num % 10000000;
                  int x2 = num / 1000000;
                  num = num % 1000000;
                  int x3 = num / 100000;
                  num = num % 100000;
                  int x4 = num / 10000;
                  num = num % 10000;
                  int x5 = num / 1000;
                  num = num % 1000;
                  int x6 = num / 100;
                  if (x3 + x6 != x2) return;
                  num = num % 100;
                  int x7 = num / 10;
                  if (x7 - x6 != nfmV) return;
                  num = num % 10;
                  int x8 = num;
                  if (x4 * x8 % 2 != 0) return;
      
                  allPwd += $"{x}\n";
              }

      Убрал строки, ну и проверки вывел в порядки их возможного использования.
      Скорость выполнения увеличилась с 13 секунд, до 3.5 сек.
      Спасибо за идею.


      1. KvanTTT
        04.08.2021 22:42
        +2

        Вместо паттерна


        int a = b % x;
        int c = d / x;

        Рекомендую использовать


        int c = Math.DivRem(b, x, out a);

        Это быстрей работает (через умножение и вычитание, а в будущем вообще будет через одну операцию), и семантически ясней выглядит. А будет еще наглядней, когда версия с туплами подъедет (может уже есть в net 6):


        (a, c) = Math.DivRem(b, x);


        1. Scrypto Автор
          05.08.2021 06:57
          +1

          Что интересно, время выполнения выросло на 0.6 секунды в среднем, с 3.5 до 4.1, при использовании DivRem
          .net используется 4.7.1


          1. KvanTTT
            05.08.2021 15:30

            Это странно — возможно проблема в чем-то другом. А на коровских версиях как?


            1. Scrypto Автор
              05.08.2021 17:41
              +1

              На Core 5, быстрее в два раза.
              За 2.2 сек.
              Определенно прогресс по сравнению .net 4 есть


        1. KvanTTT
          05.08.2021 15:31

          Ой, ошибся — в первом фрагменте должно быть:


          int a = b % x;
          int c = b / x;


  1. srv7
    05.08.2021 18:00
    +1

    "И я намеренно упускаю, первые 10 миллионов паролей от 00000000 до 09999999" - все равно на 0 делить нельзя, первое условие.


    1. Scrypto Автор
      05.08.2021 18:01

      Да, вы правы. Моя невнимательность сыграла