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