Разбираем обфусцированный CrackMe на C#

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

image-loader.svg

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

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

image-loader.svg

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

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

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

image-loader.svg

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

"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

image-loader.svg

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

image-loader.svg

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

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

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

image-loader.svg

Всего нашему паролю предстоит пройти 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));
    }
}

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

image-loader.svg

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

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

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

image-loader.svg

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

image-loader.svg

На этом у меня все, спасибо, что дочитали
PS: Сайт с CrackMe, если решите попрактиковаться: линк

© Habrahabr.ru