R0 CREW

Анализ keygenme от TPoDT #2

Источник: habrahabr.ru

Всем доброго дня!

Это снова я (Veliant), и снова несу темы reverse engineering в широкие массы. Так как, по некоторым причинам, я не могу освещать в своих статьях анализ коммерческих протекторов или программ, поэтому на сегодня нашим подопытным кроликом будет keygenme от группы T.P.o.D.T. Не сказать, что сложный, но пару часов не жалко было потратить на него.

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

Грузим в отладчик, и устанавливаем точки останова на стандартные функции WinAPI, используемые для получения текста из полей ввода: GetDlgItem, GetDlgItemTextA, GetWindowTextA. Отпускаем программу на выполнение, вбиваем любое имя, ключ и жмем кнопку ‘Check’.

Никаких неприятных сюрпризов нет, и мы сразу останавливаемся на GetDlgItemTextA. Убираем бряки, и выходим из API — стоим на адресе 0x0040195B

Как видим идет чтение текста из полей ввода, и если они пустые, то показываются MessageBox с ошибками. Ниже можно увидеть вход в функцию CheckCode, которая возвращает 0 или 1 в зависимости от результата проверки. И соответственно ниже пара MessageBox на истинный и ложный вариант проверки. Стоит заметить, что, не смотря на всю нелепость такой защиты, она встречается почти в половине приложений. И если целью является просто получение работоспособного приложения, а не ключа, то можно заменить call на последовательность

xor al, al
inc al

Но нам-то интересен сам алгоритм, поэтому трейсим и заходим в CheckCode.

Код довольно чистый и читается легко. Первым делом идет вызов подпроцедуры, названной мной GetOnlyNumeric, ей передается, как параметр, указатель на введеный код.

Для любителей C я специально сделал комментарии на C-подобном псевдокоде. Код в цикле читает введенную строку и если символ не лежит в диапазоне ‘A’-‘F’ или ‘0’-‘9’, то пропускает символ. Если же символ удовлетворяет условию, то склеивает его с новой строкой. Это сделано для того, чтобы можно было вводить ключ в «красивом» виде, с разделителями.

Вернемся обратно к коду функции CheckCode. По адресу 0x00401457 можно увидеть подсчет длины нормализованной строки, и она должна быть равна 24. (У меня тут опечатка на скриншоте). Почему 24? REP SCASB ищет в строке символ из AL, при этом уменьшая ECX. Изначально ECX указывается 0xFFFFFFFF как максимальное значение. Обычно после этого выполняют команды NOT ECX и DEC ECX, чтоб получить в нормальном виде длину строки. Проделаем и мы

-1Ah = FFFFFFE6h
not FFFFFFE6hh = 19h
19h - 1 = 24dec

Последующие вызовы функций sscanf переводят нормализованную строку в три бинарных DWORD’а подряд. И в самой нижней части скриншота имеется небольшой цикл, который считает хэш от введенного имени, путем последовательного XOR десяти DWORD’ов буфера. (Буфер длиной 40 символов) На этом первичные приготовления закончены и начинаются проверки.

Лично я, при исследовании программ, сначала смотрю поверхностно что она делает, трейся код, а потом анализирую необходимые участки в обратную сторону от проверок.
В данном случае верным будет решение при котором EDI=ECX.

Начнем с ECX — прямо перед сравнением выполняется команда LEA ECX, [ECX4+ECX]. Вообще, LEA — это команда для подсчета адреса, но в данном случае она выполняет выражение: ECX = ECX4 + ECX или проще говоря ECX5. Идем вверх по коду, ECX теперь встречается по адресу 0x0040154F LEA ECX, [EDX2+EDX]. Как могли уже догадаться это опять, простое умножение. Выходит, что конечное ECX = EDX35. Но что же тогда есть EDX? Если потрейсить код, то понимаем, что он равен операцииXOR между старшей и младшей частью последнего блока ключа. В дальнейшем полученное число буду называть SUM, а поксоренные два байта последнего блока — CRC. Поясню почему блока — после перевода введенной строки в бинарный вид, все операции над ними выполняются либо с DWORD либо с WORD значениями. Поэтому вид ключа может иметь следующий вид:

1111-2222-3333-4444-5555-6666

Это моя вариация, можно и по другому расставить дефисы.

Итак, правую часть условия мы узнали, теперь перейдем к левой — к расчету EDI.

Опишу вкратце что происходит после SUB EAX, 798134. Полученное значение, далее KEY1, сохраняется в переменную, и из него достается 4 полу-байта (4 бита), находящимся в позициях 2, 3, 6, 7 (нумерация с 0) и перемножаются между собой. Результирующее значение и должно равняться ECX. Первое что приходит на ум — просчитать такие значения, при которых и crc и KEY1 равнялись бы 0. Вроде все хорошо, но это было бы слишком просто, при таком варианте проходятся все проверки, кроме последней. Но об этом позже.

Теперь посмотрим, что происходит с EDX, до того как он становится KEY1. Из комментариев можно понять, что сначала оно считается из младшей части хэша от имени и блоков block1 и block2, опять же используя XOR. Дальше выполняются две операции

XOR EAX, 9F827364
SUB EAX, 798134

которые, для получение исходного числа из KEY1, можно обратить следующим образом:

ADD EAX, 798134
XOR EAX, 9F827364

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

На этом скриншоте их сразу три. Первая почти повторяет предыдущую. Из отличий можно выделить изменение алгоритма расчета KEY2

ADD ECX, 871623
XOR ECX, 4637A819

Которые так же легко обращаются в

XOR ECX, 4637A819
SUB ECX, 871623

А так же, изменились индексы полу-байтов. Теперь берутся 5 и 6 из KEY2 и (!) 1, 2 из KEY1. Вот почему я говорил, что не стоит подбирать числа, так как они еще используются и дальше по алгоритму. В дальнейших проверках больше не генерируется ключевых чисел, а используются два полученных KEY1 и KEY2.

Третья проверка перемножает 0,1,4 и 5 полу-байты KEY1.

В самом низу скриншота, по адресу 0x00401654, стоит совсем небольшая проверка CMP SI, BX. Она не сложная, и опять же путем трейса можно увидеть, что сравнивается младшая часть хэша от имени с пятым блоком введенного кода. Оставшиеся три проверки так же небольшие:

Первые две из них однотипные, и просто выбирают из ключевых чисел свои полу-байты для перемножения. Поэтому не буду повторяться. Большего внимания заслуживает проверка, располагающаяся по адресу 0x004016BF. Вот она-то и является самой коварной — она сравнивает crc, или проще говоря поксоренные байты последнего блока, от которых зависят все предыдущие проверки. И которая собственно не позволяет нам сделать KEY1 и KEY2 нулевыми.

Команда SHL EDX, 1 делает сдвиг на 1 бит влево, что равноценно умножению числа на 2. Итого получаем, что поксоренное число является константой для любого кода и должно равняться 0xB6/2 = 0x5B.

Теперь немного математики. В самом начале мы видели, что число, с которым идет сравнение в проверках равно CRC15. А так как мы выше нашли, что CRC=0x5B, то не трудно получить SUM=CRC15=0x555=1365(10).

В каждой проверке, при перемножении полу-байтов всегда используется 4 операнда, а это значит, что мы должны разложить число 1365 на 4 множителя. Если делимость на 3 и 5 видно на глаз, то вот оставшиеся 7 и 13 уже не такие очевидные. Но в целом задачка не сложная. В коммерческом варианте можно было взять большее число множителей и соответственно гораздо большее число, тогда разложение заняло больше бы времени. С множителями разобрались, теперь посмотрим на карту выборки полу-байтов на разных итерациях.

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

При этом я заметил некоторую симметрию, что позволяет нам генерировать не все, а только половину значений, а остальные заполнять зеркальным отражением.

Разберу небольшой пример, для своего ника.

Хэш от него будет равен 0x6930622F. Старшая часть не участвует в алгоритме.
KEY1 и KEY2 выбираем например 5d7337d5 d537735d.

Тогда для получения block1-block4 нужно выполнить следующие операции:

(5d7337d5 + 798134) ^ 9F827364 = C26ECA6D
block1 = C26E ^ 622F = A041
block2 = CA6D ^ 622F = A842
(d537735d ^ 4637A819) - 871623 = 9279C521
block3 = 9279 ^ 622F = F056
block4 = C521 ^ 622F = A70E

Пятым блоком ключа является младшая часть хэша. В моем случае — 622F. Ну а в последнем, шестом блоке, у нас больше вариантов для выбора — общая формула будет (c << 8) | (c ^ 0x5B). В самом простом случае 005B (00^5B = 5B).
Итоговый ключ будет таким:

A041-A842-F056-A70E-622F-005B

Если все нашли правильно, то должно отобразиться следующее окошко:

С моим вариантом генератора ключей можно ознакомиться ниже:

    // Key generator for keygen tPODt
     
    #include <stdio.h>
    #include <stdlib.h>
    #include <string.h>
    #include <time.h>
     
    typedef unsigned long ULONG;
    typedef unsigned short USHORT;
     
    char szEnterName[40];
     
    ULONG keys[2];
     
    ULONG HashName(ULONG *puName)
    {
            size_t i;
            ULONG hash = 0;
     
            for (i = 0; i < 10; i++)
                    hash ^= puName[i];
            return (0x00280000 | (hash >> 16)) ^ hash;
    }
     
    int MulTable[] = { 3, 5, 7, 13 };
     
    void GenConst(ULONG puKeys[2], ULONG uHash)
    {
            size_t x, i;
            puKeys[0] = puKeys[1] = 0;
            ULONG uMask[3] = { 0, 0, 0 };
            // Генерируем спаренные позиции
            i = 0;
            while (uMask[2] != 0xF)
            {
                    x = rand() % 4;
                    if (!((uMask[2] >> x) & 1))
                    {
                            switch (i)
                            {
                                    case 0: puKeys[0] |= MulTable[x] << 4;  uMask[0] |= (1 << x); break;
                                    case 1: puKeys[0] |= MulTable[x] << 8;  uMask[0] |= (1 << x); break;
                                    case 2: puKeys[1] |= MulTable[x] << 20; uMask[1] |= (1 << x); break;
                                    case 3: puKeys[1] |= MulTable[x] << 24; uMask[1] |= (1 << x); break;
                            }
                            i++;
                            uMask[2] |= (1 << x);
                    }
            }
            // Генерируем оставшиеся множители
            // Первый ключ
            i = 0;
            while (uMask[0] != 0xF)
            {
                    x = rand() % 4;
                    if (!((uMask[0] >> x) & 1))
                    {
                            switch (i)
                            {
                                    case 0: puKeys[0] |= MulTable[x];       break;
                                    case 1: puKeys[0] |= MulTable[x] << 12; break;
                            }
                            i++;
                            uMask[0] |= (1 << x);
                    }
            }
            // Старшая часть = зеркальное отражение младшей
            puKeys[0] |=  puKeys[0]              << 28;
            puKeys[0] |= (puKeys[0] >> 4  & 0xF) << 24;
            puKeys[0] |= (puKeys[0] >> 8  & 0xF) << 20;
            puKeys[0] |= (puKeys[0] >> 12 & 0xF) << 16;
     
            // Второй ключ
            i = 0;
            while (uMask[1] != 0xF)
            {
                    x = rand() % 4;
                    if (!((uMask[1] >> x) & 1))
                    {
                            switch (i)
                            {
                                    case 0: puKeys[1] |= MulTable[x] << 16; break;
                                    case 1: puKeys[1] |= MulTable[x] << 28; break;
                            }
                            i++;
                            uMask[1] |= (1 << x);
                    }
            }
            // Младшая часть = зеркальное отражение старшей
            puKeys[1] |= (puKeys[1] >> 16 & 0xF) << 12;
            puKeys[1] |= (puKeys[1] >> 20 & 0xF) << 8;
            puKeys[1] |= (puKeys[1] >> 24 & 0xF) << 4;
            puKeys[1] |=  puKeys[1] >> 28;
    }
     
    int main(int argc, char *argv[])
    {
            ULONG hash;
            USHORT crc, code[6];
            int i;
            memset(&szEnterName, 0, 40);
            srand(time(NULL));
            printf("Enter your name: ");
            scanf("%s", (char *)&szEnterName);
            hash = HashName((ULONG *) & szEnterName);
            // Выводим десяток ключей
            for (i =0; i< 10; i++)
            {
                    // Генерируем необходимые KEY1 и KEY2
                    GenConst(keys, hash);
                    // Обращаем их по найденному алгоритму
                    keys[0] = (keys[0] + 0x798134)   ^ 0x9F827364;
                    keys[1] = (keys[1] ^ 0x4637A819) - 0x871623;
                    // Расчитываем блоки ключа
                    crc = rand() % 255;
                    code[0] = (keys[0] >> 16 ^ hash) & 0xFFFF;
                    code[1] = (keys[0] ^ hash) & 0xFFFF;
                    code[2] = (keys[1] >> 16 ^ hash) & 0xFFFF;
                    code[3] = (keys[1] ^ hash) & 0xFFFF;
                    code[4] = hash & 0xFFFF;
                    code[5] = ((crc & 0xFF) << 8) | (crc ^ 0x5B);
                    printf("Your key: %04x-%04x-%04x-%04x-%04x-%04x\n", code[0], code[1], code[2], code[3], code[4], code[5]);
            }
            return 0;
    }

Ссылка на keygenme не работала, поэтому связался с автором. Он дал исходники и сам keygenme. Всё хозяйство прикрепил к теме =)