Прежде чем перейти к статье, хочу вам представить, экономическую онлайн игру Brave Knights, в которой вы можете играть и зарабатывать. Регистируйтесь, играйте и зарабатывайте!
Дня 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);
}
}