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

от автора

Дня 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);     } }


ссылка на оригинал статьи https://habr.com/ru/articles/710616/