Дня 3 назад заглянул на сайт crackmes.one попробовать силы во взломе защит. Просто наугад взялся за "hitTman's Kolay One!": просто по оценке Difficulty: 2.0 и Quality: 4.0. Не примитивно, но и не слишком сложно.

Оказалось, форма ввода пароля с подсказкой: текст кнопки "submit password" после нажатия меняется на число. Если попробовать разные символы пароля, заметно, что для одних и тех же символов число не меняется. Очевидно, пароль подается в хеш-функцию, а ее результат попадает на кнопку. Пробуя пары символов, легко узнать что число на кнопке - сумма чисел для символов пароля.

Логично, что программа сравнивает хеш с эталоном прежде чем выдает сообщение "Try harder!". Ищем в сообщение строках бинарника. Повезло: оно даже не зашифровано и на него единственная ссылка в коде.

Очевидно, рядом же и эталон хеша: 12Eh. Легко проверить: из известных хешей символов составить пароль, например "AaAaAaAaAaAaAaAaAaAaAaAaAaAaAa//" и предложить программе, на что она выдает "Great Job".

Автор просил: "find a Valid pass!", но это оказалось просто, да и верный пароль - не один. Куда интересней написать генератор ключей. Пригодится таблица хеш-значений для всего алфавита. Руками вводить символы пароля и записывать на бумаге их хеши долго и скучно, ведь можно заставить программу саму перебрать алфавит.

У OllyDbg есть коллега - Immunity Debugger, он умеет выполнять скрипты на Python. Отыщем хеш-функцию и заставим ее обработать алфавит.

В обработчике ButtonClick (sub_45385C) прямо перед сравнением хеша с эталоном находится вызов sub_4537C4. Похоже, это и есть хеш-функция: она принимает строку пароля и возвращает хеш в EAX. Привычных push для передачи параметров не видно, вероятно, они передаются через регистры. Если так, то var_C содержит указатель на строку-пароль.

Выше вызов sub_432A4C принимает два аргумента: адрес var_C и еще какой-то указатель по смещению от EBX. Ищем что записано в EBX: выше по коду ButtonClick в него записывается содержимое EAX, но прежде нет записей в EAX, значит там тоже находится параметр процедуры. Заглянув в документацию по Delphi видим, что обработчику ButtonClick передается адрес TObject sender. Можно догадаться, что второй аргумент sub_432A4C - это адрес поля ввода на форме, с которого она читает пароль.

Таких вызовов в ButtonClick два: если запустить программу в отладчике, увидим, что sub_00432A4C действительно записывает указатель на строку-пароль в переменную-аргумент.

Если дальше выполнять ButtonClick пошагово в отладчике, можно заметить, что в переменной на стеке появится и текст кнопки - число. Легко догадаться, что sub_407CE0 - это int to string, [ebx+2FCh] - это указатель на кнопку, а sub_00432A7C - это setButtonText.

Теперь подменим пароль перед хешированием: ставим breakpoint по адресу 0x004538B3 - прямо перед вызовом hashFn она же sub_4537C4. Вводим пароль 1234 и средствами отладчика отредактируем содержимое памяти var_C - введем букву A и символ конца строки \0. Сейчас хеш-функция должна вернуть в EAX значение 0x13 (19). Шагаем вперед и... Первая неудача: в EAX - 0x56 (86). Долго думал, пробовал, грешил на Юникод или еще какие скрытые хитрости программы. Оказалось, программа где-то запоминает длину строки и Вместо хеша "A\0" вычисляет хеш "A\034". Хеш \0 равен 0, поэтому если ввести пароль A34 получим хеш 0x56. Значит введем один символ пароля и повторим правки в отладчике - теперь получилось!

Теперь нужно повторить действия для всего алфавита:

  • остановиться перед вызовом hashFn

  • сменить символ пароля на следующий по алфавиту

  • остановиться после вызова hashFn и запомнить хеш символа

  • вернуть программу к вызову hashFn

Вот скрипт на Python, который это сделает:

import immlib

imm = immlib.Debugger()

def main(args):
    table = {}
	# break после hashFn
    imm.setBreakpoint(0x004538B8)
    mem_password = imm.getRegs()["EAX"]
	# цикл по печатным символам ASCII 
    for c in range(0x20, 0x7F):
        b = bytearray()
        b.append(c)
        b.append(b'\x00')
        if imm.writeMemory(mem_password, bytes(b)) <= 0:
            imm.log("Failed to write password '{0}'".format(chr(c)))
            return "ERROR"
        
        imm.run()
		# запоминаем хеш
        table[c] = imm.getRegs()["EAX"]
        
		# Возвращаемся к вызову hashFn
        imm.setReg("EIP", 0x004538B3)
		# снова передали указатель на строку-пароль в EAX
        imm.setReg("EAX", mem_password)
        imm.run()
    
	# запишем результаты в файл
    with open("C:/a.csv", "w") as out:
        for c in table:
            out.write("{0} ".format(chr(c)))
            out.write("0x{:02X}\n".format(table[c]))
        
    return "Kolay done"

Как вариант можно вводить пароль 1234 и вызывать imm.writeLong(mem_password, с), чтобы записать байт символа и три нуля следом.

Алфавитом овладели, правило хеширования пароля знаем. Как генерировать пароли?

Наивное решение: перебор всех ключей и проверка каждого. Вот код, что найдет все ключи длины 2:

const string alphabet = //...
const unordered_map<char, int> weights = //...
const int N = 2;
const int S = 0x12E;
string key;

void generateKeys(ostream& out) {
    if (N <= key.length()) {
        if (S == sum) {
            out << key << "\n";
        }
    } else {
        for (char c : alphabet) {
            const int w = weights.at(c);
            if (sum + w <= S) {
                key.push_back(c);
                sum += w;
                generateKeys(out);
                key.pop_back();
                sum -= w;
            }
        }
    }
}

Работает, но захлебывается уже на ключах длиной больше 5.

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

bool nextCombination(vector<int>& nums, int n) {
    const int k = nums.size();
    for (int i = k - 1; 0 <= i; --i) {
        if (nums[i] < n - k + i) {
            ++nums[i];
            for (int j = i + 1; j < k; ++j) {
                nums[j] = nums[j - 1] + 1;
            }
            return true;
        }
    }
    return false;
}

bool TestKey(const vector<int>& nums) {
    int sum = 0;
    for (int i : nums) {
        sum += weights.at(alphabet[i]);
    }
    return S == sum;
}

string MakeKey(const vector<int>& nums) {
    string key;
    for (int i : nums) {
        key.push_back(alphabet[i]);
    }
    return key;
}

void generateKeys(ostream& out) {
    vector<int> nums(N);
    for (int i = 0; i < N; ++i) {
        nums[i] = i;
    }

    do {
        if (TestKey(nums)) {
            ++cnt;
            out << MakeKey(nums) << '\n';
        }
    } while (nextCombination(nums, alphabet.size()));
}

Теперь можно дождаться, пока он найдет все сочетания длины 7.

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

Можно ли найти лучшее решение, чем перебор всех сочетаний? Например, если известен один верный ключ, как перейти к следующему?
vx -> ?

Аналогия: чтобы получить следующее число, добавляем 1 к младшему разряду и, если необходимо, выполняем перенос:
18, 19, 20, ...

Можно заменять символы пароля так, чтобы его хеш не менялся. Беда в том, что для 'x' нет равноценной замены.

0x01 001: %)+/5;=CGIOSYaegkmq
0x08 008: 1
0x0c 00c: y
0x0d 00d: #
0x0f 00f: !
0x11 011: '7
0x13 013: AM
0x14 014: "
0x15 015: 3[
0x16 016: &
0x17 017: 9U
0x19 019: _w
0x1a 01a: .
0x1b 01b: E
0x1d 01d: s
0x1f 01f:  }
0x20 020: :
0x21 021: -W
0x22 022: >
0x23 023: ]
0x28 028: ,JQ
0x29 029: ?o
0x2b 02b: 2
0x2c 02c: R
0x2d 02d: {
0x2e 02e: 4V
0x31 031: K
0x32 032: (^
0x36 036: *
0x37 037: $
0x38 038: j
0x39 039: c
0x3a 03a: D
0x3e 03e: v
0x3f 03f: @
0x40 040: 8Lz
0x41 041: u
0x42 042: 6
0x49 049: b
0x4a 04a: F
0x4c 04c: 0\
0x4e 04e: B
0x57 057: i
0x5a 05a: N
0x5c 05c: X
0x5e 05e: t
0x64 064: |
0x6a 06a: Phn
0x6c 06c: <
0x72 072: f
0x75 075: d
0x7b 07b: H
0x7e 07e: r
0x88 088: p
0x8c 08c: T
0x90 090: Z
0x9c 09c: `
0xac 0ac: l
0xba 0ba: ~
0xf0 0f0: x

Возьмем другой ключ из 3х символов: Zr%. Заменяя % можно пройтись по нескольким ключам:
Zr% -> Zr) -> Zr+ -> ... -> Zrq
Попробуем найти замену для rq. Пришлось обойти все пары символов алфавита и вычислить их хеши.
Zrq -> Zt- -> ZtW -> Zuv

Что делать дальше - непонятно. Сменить Z на другой символ? У Z нет равноценных замен, придется перебором снова найти первый подходящий ключ.

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

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

// Длина ключа N - степень двойки
void generateKeys(ostream& out) {
    for (int n = 1; n < N; n <<= 1) {
        unordered_map<string, int> next_weights;
        for (auto i = weights.begin(); i != weights.end(); ++i) {
            for (auto j = next(i); j != weights.end(); ++j) {
                const string& s1 = i->first;
                int w1 = i->second;
                const string& s2 = j->first;
                int w2 = j->second;
                int len = s1.length() + s2.length();
                if (len == n * 2) {
                    string s3(len, 0);
                    int w3 = w1 + w2;
                    merge(s1.begin(), s1.end(), s2.begin(), s2.end(), s3.begin());
                    if (len < N) {
                        next_weights[s3] = w3;
                    }

                    if (N == len && S == w3) {
                        out << s3 << '\n';
                        ++cnt;
                    }
                }
            }
        }
        weights.swap(next_weights);
    }
}

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


  1. orcy
    15.01.2023 05:24
    +10

    Если хоть раз мечтал написать crack или keygen

    Было такое. Когда-то давно поставил SoftICE и сделал Keygen для плагина Winamp. Achievement unlocked: hax0r


    1. xenon
      15.01.2023 13:43
      +2

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


  1. perfect_genius
    15.01.2023 16:11
    +3

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


    1. JuSay
      15.01.2023 18:06
      +2

      Прошло столько времени с «того Хабра», что люди забыли и снова начали любить такие статьи ????


    1. adante
      15.01.2023 20:53
      +1

      Я в этом реверсе ноль просто абсолютный. Максимум что делал с кряками - искал их в гугле.

      Но тут зачитался и даже 90% понял. Все бы так писали)

      UPD даже на сайт по ссылке зашел, пока ничо не понятно, но копаюсь)


  1. IvaYan
    15.01.2023 18:05
    +7

    Если хоть раз мечтал написать crack или keygen

    А как написать и добавить в keygen прилипчивую музыку, раздел будет?


    1. steanlab
      15.01.2023 19:24
      +1

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


  1. Maccimo
    15.01.2023 19:03
    +2

    Нет, ну так не интересно!
    Стоило хотя бы попытаться хеш-функцию отреверсить.


  1. redsh0927
    15.01.2023 20:04
    +2

    Сотню лет назад играл в San Andreas с читами. Ещё удивляло чому к предыдущим версиям коды составлены из обычных слов, а тут какая-то тарабарщина. Потом поковырял игрушку и понял что игрушка проверяет какой-то хэш от введённого с клавы. Вытащил алгоритм и написал брутилку, было интересно удастся ли найти более удобные коды…


  1. Daar
    16.01.2023 08:37
    +1

    Эхх... молодость... еще на ZX-Spectrum этим занимался. Но в наше время онлайн-сервисов данное направление не сильно актуально. Оплатил подписку, сервис работает, не оплатил, не работает.
    А так судя по иконке прога написана на Delphi 7, а она дебажится просто на ура. Нет никакого полиморфизма или другой "приятной" хрени от которой в дебагере можно залипнуть на несколько дней.


  1. voted
    16.01.2023 14:36
    +1

    Я себя впервые хакером почувствовал в 99м, когда был какой то сборник пасьянсов, 7 дней бесплатно потом без ключа не запускался, очень хотелось продолжить, интернета ещё толком не было (у меня лично, модем появился, BBS, сайты некоторые были доступны, но о поисковиках я не знал). И вот я решил стать хакером - открыл папку с игрой и принялся изучать "код", из инструментов был блокнот, только научился открывать любые файлы в нём.
    В итоге в файле buy_now.url была найдена интересная строка http://(тут сайт производителя, я уже не помню)/buy_now.cgi?key=KWEWRQOZLJG
    Как можно догадаться ключ "KWEWRQOZLJG" оказался правильным и активировал пасьянсы на неограниченное время, и меня воодушевил посвятить свою жизнь компьютерам, потому как я стал Хакером.