August 11, 2018

Win

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

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

Когда меня поставили дежурным по части, я отдал рацию помощнику, а сам засел за отладчик, чувствуя себя идиотом. В 10 вечера провёл построение и вечернюю проверку, и снова нырнул в код. В половину двенадцатого я расшифровал первые 130 байт, а к половине первого у меня была готова программа - расшифровщик всей базы. Отвечая на вопрос Ivan Stoikin - реверс инженирингом я не заработал пока ни копейки, но этот азарт охотника и наслаждение победителя в конце - бесценно :)

Открытие номер один: Sergey Kataev был прав. Лучший способ взломать программу - сидеть в отладчике. Читать дизассемблированный код, строить гипотезы о его работе, проверять эти гипотезы. Находить нестыковки. Строить гипотезы о нестыковках. Проверять. Снова находить. Снова строить. Снова ошибаться. Читать исходник. Учить заново ассемблер. Возненавидеть отладчик. Возненавидеть программу и её автора. Возненавидеть свою жизнь и весь мир. А потом приходит просветление. Мир снова обретает утраченное равновесие, жизнь снова обретает смысл. Катарсис. Нирвана.

Открытие номер два: у Делфи есть промежуточный уровень API. Раньше я считал, что при работе с языками программирования высокого уровня компилятор превращает текст программы сразу в машинный код. Оказывается, высокоуровневые функции компилятор переводит в набор функций среднего уровня, которые делают разные странные вещи. В частности, простые высокоуровневые операции со строками вида str1 := str2 + str3 в делфи превращаются в запутанную серию вызовов UniqueString, _SetLength, _LStrFromPChar, _LStrCat и _LStrClr (полный список в system.pas). Причем, этот набор будет отличаться от случая к случаю, в зависимости от уровня оптимизации.

Открытие номер три - для контроля над памятью каждая делфи строка содержит счетчик использования. Функция при работе со строкой прибавляет к счётчику единичку с помощью InterlockedIncrement, при завершении вычитает InterlockedDecrement, и пока счётчик не нулевой, сборщик мусора память не трогает. Вообще, структура делфи строк сложнее, чем я думал. Я ломал голову над фрагментом return *(_DWORD *)(result - 4), не понимая, зачем программе лезть в область памяти _перед_ началом строки, пока не вспомнил, что там паскаль хранит длину строки в байтах. Потом оказалось, что перед длиной есть еще 4 байта с идентификатором строки, и где-то еще должен быть счётчик интерлоков.

Открытие номер четыре - Ida Pro может ошибаться. Она успешно переводит машинный код в ассемблер, ищет точку входа, и если угадает (или мы ей подскажем) исходный язык программирования, пытается интерпретировать ассемблерные блоки. Вот это она делает не всегда успешно. Нужно не забывать, что первичен именно ассемблер, а псевдокод - вторичен. Проверяя по байтам работу каждой строчки, я обнаружил, что мой ключ, сгенерированный из псевдокода, отличается от того, что видит дебаггер.

Псевдокод генератора ключа состоит всего из двух строчек. Первая: a3 = 134775813 * a3 + 1 - здесь явно видно "магическое" число 134775813 - основа шифра. Вторая строчка - запись полученного значения в буфер: *(_BYTE *)(result + i) = BYTE3(a3) - здесь result - адрес начала строки в памяти, i - смещение в байтах. Директиву BYTE3 я понимал как обрезку четырехбайтного целого а3 до размеров байта. В попытках найти, почему мой разультат отличается от дебаггера, я опустился на уровень машинного кода и после imul, inc и mov я обнаружил неожиданную ассемблерную комманду "побитовое смещение вправо": shr ebx, 18h. Я сделал такую ассемблерную вставку в свою программу, и почувствовал, как по спине поползли мурашки. Цифровой мусор на экране впервые сложился в осмысленную фразу: "Тактика: Бойові дії взводу в обороні". Потом еще я чистил код и писал модуль расшифровки, но главное дело было сделано.

Итак, чтобы расшифровать базу, выполняем следующую последовательность действий. Получим размер файла в байтах. Сформируем ключ длиной 100 байт, двигаясь от хвоста к началу, по формуле: buf[i-1] = 134775813 * buf[i] + 1, смещая результат на 24 бита вправо. В качестве начального значания берем размер файла в байтах (именно из-за этой хитрости первые несколько байт базы у разных файлов никогда не совпадают). Потом проходим по всему файлу, последовательно вычитая из каждого байта значение ключа.

Функция формирования ключа:
void subKey(char * buf, unsigned int a3){
for (int i = 100-1; i >= 0; --i){
a3 = 134775813 * a3 + 1;
buf[i] = a3 >> 24;
}
}

Функция дешифрования:
void subMix(int len, char * in, char * key, char * out){
int k = 0;
for(int i = 0; i < len; i++){ out[i] = in[i] - key[k]; if(++k>=100)k = 0;
}
}

Задача на будущее: пока не удалось найти, как в Иде можно увидеть фрагмент памяти, только байт за байтом или регистры.



Вы тоже хотите испытать бурную радость после чёрных дней тоски и отчаяния? Откройте для себя реверс инжениринг.

No comments: