Реверс инжиниринг программ. Реверс-инжиниринг для начинающих. Защита Android-приложений от реверс-инжиниринга. Что такое реверс-инжиниринг программ

  • Tutorial

Этот пост будет интересно действительно тем, кто только начинает интересоваться этой темой. У людей с опытом он, возможно, вызовет только зевки. За исключением разве что, может быть, …
Реверс-инжиниринг в той менее легальной части, где он не касается отладки и оптимизации собственного продукта, касается в том числе и такой задачи: «узнать, а как у них это работает». Иначе говоря, восстановление исходного алгоритма программы, имея на руках ее исполнимый файл.
Для того, чтобы держаться азов и избежать некоторых проблем - «взломаем» не что-нибудь, а… кейген. В 90% он не будет запакован, зашифрован или иным способом защищен - в том числе и нормами международного права…

Вначале было слово. Двойное
Итак, нам нужен кейген и дизассемблер. Что касается второго - то предположим, что это будет Ida Pro. Подопытный безымянный кейген, найденный на просторах Сети:

Открыв файл кейгена в Ida, видим список функций.

Проанализировав этот список, мы видим несколько стандартных функций (WinMain, start, DialogFunc) и кучу вспомогательных-системных. Все это стандартные функции, составляющие каркас.
Пользовательские функции, которые представляют реализацию задач программы, а не ее обертку из API-шных и системных вызовов, дизассемблер не распознает и называет попросту sub_цифры. Учитывая, что такая функция здесь всего одна - она и должна привлечь наше внимание как, скорее всего, содержащая интересующий нас алгоритм или его часть.

Давайте запустим кейген. Он просит ввести две 4-значных строки. Предположим, в функцию расчета ключа отправляются сразу восемь символов. Анализируем код функции sub_401100. Ответ на гипотезу содержится в первых двух строках:

var_4= dword ptr -4
arg_0= dword ptr 8

Вторая строка недвусмысленно намекает нам на получение аргумента функции по смещению 8. Однако размер аргумента - двойное слово, равное 4 байтам, а не 8. Значит, вероятнее всего за один проход функция обрабатывает одну строку из четырех символов, а вызывается она два раза.
Вопрос, который наверняка может возникнуть: почему для получения аргумента функции резервируется смещение в 8 байт, а указывает на 4, ведь аргумент всего один? Как мы помним, стек растет вниз; при добавлении в стек значения стековый указатель уменьшается на соответствующее количество байт. Следовательно, после добавления в стек аргумента функции и до начала ее работы в стек добавляется что-то еще. Это, очевидно, адрес возврата, добавляемый в стек после вызова системной функции call.

Найдем места в программе, где встречаются вызовы функции sub401100. Таковых оказывается действительно два: по адресу DialogFunc+97 и DialogFunc+113. Интересующие нас инструкции начинаются здесь:

Относительно длинный кусок кода

loc_401196: mov esi, mov edi, ds:SendDlgItemMessageA lea ecx, push ecx ; lParam push 0Ah ; wParam push 0Dh ; Msg push 3E8h ; nIDDlgItem push esi ; hDlg call edi ; SendDlgItemMessageA lea edx, push edx ; lParam push 0Ah ; wParam push 0Dh ; Msg push 3E9h ; nIDDlgItem push esi ; hDlg call edi ; SendDlgItemMessageA pusha movsx ecx, byte ptr movsx edx, byte ptr movsx eax, byte ptr shl eax, 8 or eax, ecx movsx ecx, byte ptr shl eax, 8 or eax, edx shl eax, 8 or eax, ecx mov , eax popa mov eax, push eax call sub_401100

Сначала подряд вызываются две функции SendDlgItemMessageA. Эта функция берет хэндл элемента и посылает ему системное сообщение Msg. В нашем случае Msg в обоих случаях равен 0Dh, что является шестнадцатиричным эквивалентом константы WM_GETTEXT. Здесь извлекаются значения двух текстовых полей, в которые пользователь ввел «две 4-символьных строки». Буква А в названии функции указывает, что используется формат ASCII - по одному байту на символ.
Первая строка записывается по смещению lParam, вторая, что очевидно - по смещению var_1C.
Итак, после выполнения функций SendDlgItemMessageA текущее состояние регистров сохраняется в стеке с помощью команды pusha, затем в регистры ecx, edx и eax записывается по одному байту одной из строк. В результате каждый из регистров принимает вид: 000000##. Затем:

  1. Команда SHL сдвигает битовое содержимое регистра eax на 1 байт или, другими словами, умножает арифметическое содержимое на 100 в шестнадцатиричной системе или на 256 в десятичной. В результате еах принимает вид 0000##00 (например, 00001200).
  2. Выполняется операция OR между полученным значением eax и регистром ecx в виде 000000## (пусть это будет 00000034). В результате еах будет выглядеть так: 00001234.
  3. В «освободившийся» есх записывается последний, четвертый байт строки.
  4. Содержимое еах снова сдвигается на байт, освобождая место в младшем байте для следующей команды OR. Теперь еах выглядит так: 00123400.
  5. Инструкция OR выполняется, на этот раз между еах и edx, который содержит, допустим, 00000056. Теперь еах - 00123456.
  6. Повторяются два шага SHL eax,8 и OR, в результате чего новое содержимое ecx (00000078) добавляется в «конец» еах. В итоге, еах хранит значение 12345678.
Затем это значение сохраняется в «переменной» - в области памяти по смещению arg_4. Состояние регистров (их прежние значения), ранее сохраненное в стеке, вытаскивается из стека и раздается регистрам. Затем в регистр еах снова записывается значение по смещению arg_4 и это значение выталкивается из регистра в стек. После этого следует вызов функции sub_401100.

В чем смысл этих операций? Выяснить очень просто даже на практике, без теории. Поставим в отладчике брейкпойнт, например, на инструкции push eax (перед самым вызовом подфункции) и запустим программу на выполнение. Кейген запустится, попросит ввести строки. Введя qwer и tyui и остановившись на брейкпойнте, смотрим значение еах: 72657771. Декодируем в текст: rewq. То есть физический смысл этих операций - инверсия строки.

Теперь мы знаем, что в sub_401100 передается одна из исходных строк, перевернутая задом наперед, в размере двойного слова, целиком умещающаяся в любом из стандартных регистров. Пожалуй, можно взглянуть на инструкции sub_401100.

Еще один относительно длинный кусок кода

sub_401100 proc near var_4= dword ptr -4 arg_0= dword ptr 8 push ebp mov ebp, esp push ecx push ebx push esi push edi pusha mov ecx, mov eax, ecx shl eax, 10h not eax add ecx, eax mov eax, ecx shr eax, 5 xor eax, ecx lea ecx, mov edx, ecx shr edx, 0Dh xor ecx, edx mov eax, ecx shl eax, 9 not eax add ecx, eax mov eax, ecx shr eax, 11h xor eax, ecx mov , eax popa mov eax, pop edi pop esi pop ebx mov esp, ebp pop ebp retn sub_401100 endp


В самом начале здесь ничего интересного - состояния регистров заботливо сохраняются в стеке. А вот первая команда, которая нам интересна - следующая за инструкцией PUSHA. Она записывает в есх аргумент функции, хранящийся по смещению arg_0. Потом это значение перекидывается в еах. И обрезается наполовину: как мы помним, в нашем примере в sub_401100 передается 72657771; логический сдвиг влево на 10h (16 в десятичной) превращает значение регистра в 77710000.
После этого значение регистра инвертируется инструкцией NOT. Это значит, что в двоичном представлении регистра все нули превращаются в единицы, а единицы - в нули. Регистр после выполнения этой инструкции содержит 888ЕFFFF.
Инструкция ADD добавляет (прибавляет, плюсует, и т.д.) получившееся значение к исходному значению аргумента, которое все еще содержится в регистре есх (теперь понятно, зачем было записывать его сначала в есх, а затем в еах?). Результат сохраняется в есх. Проверим, как будет выглядеть есх после выполнения этой операции: FAF47770.
Этот результат копируется из есх в еах, после чего к содержимому еах применяется инструкция SHR. Эта операция противоположна SHL - если последняя сдвигает разряды влево, то первая сдвигает их вправо. Подобно тому, как операция логического сдвига влево эквивалентна умножению на степени двойки, операция логического сдвига вправо эквивалентна такому же делению. Посмотрим, какое значение окажется результатом этой операции: 7D7A3BB.
Теперь совершим еще одно насилие над содержимым еах и есх: инструкция XOR - сложение по модулю 2 или «исключающее ИЛИ». Суть этой операции, грубо говоря, в том, что в результат ее равен единице (истине) только, если операнды ее раЗнозначные. Например, в случае 0 xor 1 результатом будет истина, или единица. В случае 0 xor 0 или 1 xor 1 - результатом будет ложь, или ноль. В нашем случае в результате выполнения этой инструкции применительно к регистрам еах (7D7A3BB) и есх (FAF47770) в регистр еах запишется значение FD23D4CB.

Следующая команда LEA ecx, элегантно и непринужденно умножает еах на 9 и записывает результат в есх. Затем это значение копируется в edx и сдвигается вправо на 13 разрядов: получаем 73213 в еdx и E6427B23 в есх. Затем - снова ксорим есх и edx, записывая в есх E6454930. Копируем это в еах, сдвигаем влево на 9 разрядов: 8А926000, затем инвертируем это, получая 756D9FFF. Прибавляем это значение к регистру есх - имеем 5BB2E92F. Копируем это в еах, сдвигаем вправо аж на 17 разрядов - 2DD9 - и ксорим с есх. Получаем в итоге 5BB2C4F6. Затем… затем… что там у нас? Что, все?..
Итак, мы сохраняем это значение в область памяти по смещению var_4, загружаем из стека состояния регистров, снова берем из памяти итоговое значение и окончательно забираем из стека оставшиеся там состояния регистров, сохраненные в начале. Выходим из функции. Ура!.. впрочем, радоваться еще рано, пока что на выходе из первого вызова функции мы имеем максимум - четыре полупечатных символа, а ведь у нас еще целая необработанная строка есть, да и эту еще к божескому виду привести надо.

Перейдем на более высокий уровень анализа - от дизассемблера к декомпилятору. Представим всю функцию DialogFunc, в которой содержатся вызовы sub_401100, в виде С-подобного псевдокода. Собственно говоря, это дизассемблер называет его «псевдокодом», на деле это практически и есть код на С, только страшненький. Глядим:

Нужно больше кода. Нужно построить зиккурат.

SendDlgItemMessageA(hDlg, 1000, 0xDu, 0xAu, (LPARAM)&lParam); SendDlgItemMessageA(hDlg, 1001, 0xDu, 0xAu, (LPARAM)&v15); v5 = sub_401100((char)lParam | ((SBYTE1(lParam) | ((SBYTE2(lParam) | (SBYTE3(lParam) << 8)) << 8)) << 8)); v6 = 0; do { v21 = v5 % 0x24; v7 = v21; v5 /= 0x24u; if (v7 >= 10) v8 = v7 + 55; else v8 = v7 + 48; v21 = v8; } while (v6 < 4); v22 = 0; v9 = sub_401100(v15 | ((v16 | ((v17 | (v18 << 8)) << 8)) << 8)); v10 = 0; do { v19 = v9 % 0x24; v11 = v19; v9 /= 0x24u; if (v11 >= 10) v12 = v11 + 55; else v12 = v11 + 48; v19 = v12; } while (v10 < 4); v20 = 0; wsprintfA(&v13, "%s-%s-%s-%s", &lParam, &v15, v21, v19); SendDlgItemMessageA(hDlg, 1002, 0xCu, 0, (LPARAM)&v13);

Это уже легче читать, чем ассемблерный листинг. Однако не во всех случаях можно положиться на декомпилятор: нужно быть готовым часами следить за нитью ассемблерной логики, за состояниями регистров и стека в отладчике… а потом давать письменные объяснения сотрудникам ФСБ или ФБР. Под вечер у меня особенно смешные шутки.
Как я уже сказал, читать это легче, но до совершенства еще далеко. Давайте проанализируем код и дадим переменным более удобочитаемые названия. Ключевым переменным дадим понятные и логичные названия, а счетчикам и временным - попроще.

То же самое, только переведенное с китайского на индусский.

SendDlgItemMessageA(hDlg, 1000, 0xDu, 0xAu, (LPARAM)&first_given_string); SendDlgItemMessageA(hDlg, 1001, 0xDu, 0xAu, (LPARAM)&second_given_string); first_given_string_encoded = sub_401100((char)first_given_string | ((SBYTE1(first_given_string) | ((SBYTE2(first_given_string) | (SBYTE3(first_given_string) << 8)) << 8)) << 8)); i = 0; do { first_result_string[i] = first_string_encoded % 0x24; temp_char = first_result_string[i]; first_string_encoded /= 0x24u; if (temp_char >= 10) next_char = temp_char + 55; else next_char = temp_char + 48; first_result_string = next_char; } while (i < 4); some_kind_of_data = 0; second_string_encoded = sub_401100(byte1 | ((byte2 | ((byte3 | (byte4 << 8)) << 8)) << 8)); j = 0; do { second_result_string[j] = second_string_encoded % 0x24; temp_char2 = second_result_string[j]; second_string_encoded /= 0x24u; if (temp_char2 >= 10) next_char2 = temp_char2 + 55; else next_char2 = temp_char2 + 48; second_result_string = next_char2; } while (j < 4); yet_another_some_kind_of_data = 0; wsprintfA(&buffer, "%s-%s-%s-%s", &first_given_string, &second_given_string, first_result_string, second_result_string); SendDlgItemMessageA(hDlg, 1002, 0xCu, 0, (LPARAM)&buffer);

Порой кто-то хочет посмотреть, а какая же начинка в определённой программе? Тогда ему приходится использовать реверс-инжиниринг. Что это такое? Как действует? Каким образом происходит этот процесс? Обо всём этом вы сможете узнать из данной статьи.

Что такое реверс-инжиниринг программ?

Так называют процесс разбора приложения с целью понять, как оно работает, чтобы в будущем воссоздать этот процесс с внесением необходимых изменений. Обычно для этих целей используются отладчик и ассемблер. В зависимости от качества используемого программного обеспечения будут отличаться результат и количество времени, которое необходимо потратить на его приведение в нормальный вид. Объяснять реверс-инжиниринг для начинающих лучше всего на примере. В качестве оного выступит приложение, написанное для Android. А теперь давайте будет узнавать, что и как.

Работа с Android-приложениями

Для начала необходимо прояснить некоторые моменты. В приложениях используется байт-код и LogCat. Это местные аналоги ранее упомянутых отладчика и ассемблера. Также необходимо понимать структуру самих приложений. Так, каждая программа является файлом с расширением apk. Упаковывается он zip’ом. Нас интересует его содержимое - ресурсы приложения, classes.dex и AndroidManifest.xml. Если вы занимаетесь программированием на Android, то вопросов с первыми и последним быть не должно. А вот classes.dex - это байт-код программы, который компилируется специально для виртуальной машины. Выудить из него исходный код java представленными в интернете средствами не получиться. Но возможным является получение dalvik opcodes - специального набора команд, которые используются для виртуальной машины. Для аналогии можно сказать, что это ассемблер местного разлива. Также classes.dex может быть превращен в файл с расширением jar. Уже в таком случае после его декомпиляции можно получить java-код, который будет более-менее читаемым. По такому пути мы и пойдём.

Декомпиляция

Этот процесс будет проводиться с использованием программы Apk Manger. Прежде чем приступать к работе, необходимо убедиться, что есть нужные драйверы для работы девайса, а также функционирует режим USB-debugging. Первоначально нам необходимо будет переместить файл, который будет разбираться в директиву apk_manager\place-apk-here-for-modding. После этого следует запустить Script.bat. Если никаких проблем нет, то запуститься консоль, на которой будут зелёные надписи. Выбираем пункт номер девять - «Декомпилировать». После того как будет запущен процесс, необходимо не закрывать консоль. Потом следует открыть интересующий apk-файл с помощью архиватора и извлечь из него classes.dex, который нужно обработать программой dex2jar. Для необходимого нам результата его надо переместить на объект с расширением.bat. Появится файл, который будет заканчиваться на.jar. Пока что окно не закрываем.

Анализируем данные

Чтобы получить информацию о приложении, необходимо открыть его манифест. По нему определяем, что выступает в роли главной активности. Она-то и представляет для нас сейчас наибольшую важность. Также желательно посмотреть в самый низ программы. Если внизу будет информация о менеджере лицензий, то это существенно усложнит проведение реверсного инжиниринга. Если переключиться на jd-gui и развернуть дерево, то мы увидим несколько пространств имён. Допустим, что их три. В первом находятся файлы, связанные с рекламой. Во втором будут классы менеджера лицензий. В третьем находятся нам необходимые данные. В него-то мы и заходим. Здесь необходимо будет найти и удалить ключ, а потом и остальные строки, которые проверяют, является ли лицензионной работающая версия. Всё это нужно почистить. Потом в нашем Apk Manager ищем место, куда указано размещать байт-код. Теперь сделаем небольшое отступление и закомментируем команды, с которыми потенциально могут возникнуть проблемы. После этого нам осталось только скомпилировать программу.

Сборка приложения

В этом нам поможет всё тот же Apk Manager. В окне консоли, которое нами не закрывалось, выбираем пункт №14. Далее дело техники. Если приложение довольно сложное, то при запуске оно может частично или полностью потерять свою работоспособность. Не огорчайтесь, это значит, что мы только на полпути и есть ещё куда идти. Продолжаем проводить реверс-инжиниринг Android-приложений. Сказать, что нужно делать в конкретном случае, общими словами, увы, невозможно. Поэтому искать проблемное место придётся самостоятельно. Так, если окно приложения блокируется то необходимо просмотреть код и удалить ту его часть, что отвечает за данный диалог. Помочь с этим сможет jd-gui. Как видите, реверсный инжиниринг не является лёгким делом, и он требует наличия значительного багажа знаний. Даже если всё без проблем запустилось, необходимо будет протестировать работоспособность приложения. То есть реверс-инжиниринг является ещё затратным по времени действием. Продолжаем работать, пока не будут выявлены все проблемы.

Безопасность

А что, если нам нужна защита Android-приложений от реверс-инжиниринга? В таком случае есть два варианта: использование специальных программ или создание кодовой конструкции, которая будет мешать совершать разбор написанного. Последний вариант подходит исключительно опытным специалистам, поэтому мы рассмотрим только первый способ защиты. В качестве специализированного программного обеспечения используем ProGuard. Это приложение, которое используется для сокращения, обфускации и оптимизации кода. Если программу «прогнать» через него, то мы получим файл с расширением *.apk меньшего размера, чем он был. В таком случае его будет значительно сложней разобрать. Причем преимуществом данной программы является то, что она даже была внедрена в систему сборки Андроид-приложений с обновления r9. Поэтому воспользоваться ею может любой разработчик, который имеет стандартные инструменты создания и разработки.

Заключение

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

  • Tutorial

Этот пост будет интересно действительно тем, кто только начинает интересоваться этой темой. У людей с опытом он, возможно, вызовет только зевки. За исключением разве что, может быть, …
Реверс-инжиниринг в той менее легальной части, где он не касается отладки и оптимизации собственного продукта, касается в том числе и такой задачи: «узнать, а как у них это работает». Иначе говоря, восстановление исходного алгоритма программы, имея на руках ее исполнимый файл.
Для того, чтобы держаться азов и избежать некоторых проблем - «взломаем» не что-нибудь, а… кейген. В 90% он не будет запакован, зашифрован или иным способом защищен - в том числе и нормами международного права…

Вначале было слово. Двойное
Итак, нам нужен кейген и дизассемблер. Что касается второго - то предположим, что это будет Ida Pro. Подопытный безымянный кейген, найденный на просторах Сети:

Открыв файл кейгена в Ida, видим список функций.

Проанализировав этот список, мы видим несколько стандартных функций (WinMain, start, DialogFunc) и кучу вспомогательных-системных. Все это стандартные функции, составляющие каркас.
Пользовательские функции, которые представляют реализацию задач программы, а не ее обертку из API-шных и системных вызовов, дизассемблер не распознает и называет попросту sub_цифры. Учитывая, что такая функция здесь всего одна - она и должна привлечь наше внимание как, скорее всего, содержащая интересующий нас алгоритм или его часть.

Давайте запустим кейген. Он просит ввести две 4-значных строки. Предположим, в функцию расчета ключа отправляются сразу восемь символов. Анализируем код функции sub_401100. Ответ на гипотезу содержится в первых двух строках:

var_4= dword ptr -4
arg_0= dword ptr 8

Вторая строка недвусмысленно намекает нам на получение аргумента функции по смещению 8. Однако размер аргумента - двойное слово, равное 4 байтам, а не 8. Значит, вероятнее всего за один проход функция обрабатывает одну строку из четырех символов, а вызывается она два раза.
Вопрос, который наверняка может возникнуть: почему для получения аргумента функции резервируется смещение в 8 байт, а указывает на 4, ведь аргумент всего один? Как мы помним, стек растет вниз; при добавлении в стек значения стековый указатель уменьшается на соответствующее количество байт. Следовательно, после добавления в стек аргумента функции и до начала ее работы в стек добавляется что-то еще. Это, очевидно, адрес возврата, добавляемый в стек после вызова системной функции call.

Найдем места в программе, где встречаются вызовы функции sub401100. Таковых оказывается действительно два: по адресу DialogFunc+97 и DialogFunc+113. Интересующие нас инструкции начинаются здесь:

Относительно длинный кусок кода

loc_401196: mov esi, mov edi, ds:SendDlgItemMessageA lea ecx, push ecx ; lParam push 0Ah ; wParam push 0Dh ; Msg push 3E8h ; nIDDlgItem push esi ; hDlg call edi ; SendDlgItemMessageA lea edx, push edx ; lParam push 0Ah ; wParam push 0Dh ; Msg push 3E9h ; nIDDlgItem push esi ; hDlg call edi ; SendDlgItemMessageA pusha movsx ecx, byte ptr movsx edx, byte ptr movsx eax, byte ptr shl eax, 8 or eax, ecx movsx ecx, byte ptr shl eax, 8 or eax, edx shl eax, 8 or eax, ecx mov , eax popa mov eax, push eax call sub_401100

Сначала подряд вызываются две функции SendDlgItemMessageA. Эта функция берет хэндл элемента и посылает ему системное сообщение Msg. В нашем случае Msg в обоих случаях равен 0Dh, что является шестнадцатиричным эквивалентом константы WM_GETTEXT. Здесь извлекаются значения двух текстовых полей, в которые пользователь ввел «две 4-символьных строки». Буква А в названии функции указывает, что используется формат ASCII - по одному байту на символ.
Первая строка записывается по смещению lParam, вторая, что очевидно - по смещению var_1C.
Итак, после выполнения функций SendDlgItemMessageA текущее состояние регистров сохраняется в стеке с помощью команды pusha, затем в регистры ecx, edx и eax записывается по одному байту одной из строк. В результате каждый из регистров принимает вид: 000000##. Затем:

  1. Команда SHL сдвигает битовое содержимое регистра eax на 1 байт или, другими словами, умножает арифметическое содержимое на 100 в шестнадцатиричной системе или на 256 в десятичной. В результате еах принимает вид 0000##00 (например, 00001200).
  2. Выполняется операция OR между полученным значением eax и регистром ecx в виде 000000## (пусть это будет 00000034). В результате еах будет выглядеть так: 00001234.
  3. В «освободившийся» есх записывается последний, четвертый байт строки.
  4. Содержимое еах снова сдвигается на байт, освобождая место в младшем байте для следующей команды OR. Теперь еах выглядит так: 00123400.
  5. Инструкция OR выполняется, на этот раз между еах и edx, который содержит, допустим, 00000056. Теперь еах - 00123456.
  6. Повторяются два шага SHL eax,8 и OR, в результате чего новое содержимое ecx (00000078) добавляется в «конец» еах. В итоге, еах хранит значение 12345678.
Затем это значение сохраняется в «переменной» - в области памяти по смещению arg_4. Состояние регистров (их прежние значения), ранее сохраненное в стеке, вытаскивается из стека и раздается регистрам. Затем в регистр еах снова записывается значение по смещению arg_4 и это значение выталкивается из регистра в стек. После этого следует вызов функции sub_401100.

В чем смысл этих операций? Выяснить очень просто даже на практике, без теории. Поставим в отладчике брейкпойнт, например, на инструкции push eax (перед самым вызовом подфункции) и запустим программу на выполнение. Кейген запустится, попросит ввести строки. Введя qwer и tyui и остановившись на брейкпойнте, смотрим значение еах: 72657771. Декодируем в текст: rewq. То есть физический смысл этих операций - инверсия строки.

Теперь мы знаем, что в sub_401100 передается одна из исходных строк, перевернутая задом наперед, в размере двойного слова, целиком умещающаяся в любом из стандартных регистров. Пожалуй, можно взглянуть на инструкции sub_401100.

Еще один относительно длинный кусок кода

sub_401100 proc near var_4= dword ptr -4 arg_0= dword ptr 8 push ebp mov ebp, esp push ecx push ebx push esi push edi pusha mov ecx, mov eax, ecx shl eax, 10h not eax add ecx, eax mov eax, ecx shr eax, 5 xor eax, ecx lea ecx, mov edx, ecx shr edx, 0Dh xor ecx, edx mov eax, ecx shl eax, 9 not eax add ecx, eax mov eax, ecx shr eax, 11h xor eax, ecx mov , eax popa mov eax, pop edi pop esi pop ebx mov esp, ebp pop ebp retn sub_401100 endp


В самом начале здесь ничего интересного - состояния регистров заботливо сохраняются в стеке. А вот первая команда, которая нам интересна - следующая за инструкцией PUSHA. Она записывает в есх аргумент функции, хранящийся по смещению arg_0. Потом это значение перекидывается в еах. И обрезается наполовину: как мы помним, в нашем примере в sub_401100 передается 72657771; логический сдвиг влево на 10h (16 в десятичной) превращает значение регистра в 77710000.
После этого значение регистра инвертируется инструкцией NOT. Это значит, что в двоичном представлении регистра все нули превращаются в единицы, а единицы - в нули. Регистр после выполнения этой инструкции содержит 888ЕFFFF.
Инструкция ADD добавляет (прибавляет, плюсует, и т.д.) получившееся значение к исходному значению аргумента, которое все еще содержится в регистре есх (теперь понятно, зачем было записывать его сначала в есх, а затем в еах?). Результат сохраняется в есх. Проверим, как будет выглядеть есх после выполнения этой операции: FAF47770.
Этот результат копируется из есх в еах, после чего к содержимому еах применяется инструкция SHR. Эта операция противоположна SHL - если последняя сдвигает разряды влево, то первая сдвигает их вправо. Подобно тому, как операция логического сдвига влево эквивалентна умножению на степени двойки, операция логического сдвига вправо эквивалентна такому же делению. Посмотрим, какое значение окажется результатом этой операции: 7D7A3BB.
Теперь совершим еще одно насилие над содержимым еах и есх: инструкция XOR - сложение по модулю 2 или «исключающее ИЛИ». Суть этой операции, грубо говоря, в том, что в результат ее равен единице (истине) только, если операнды ее раЗнозначные. Например, в случае 0 xor 1 результатом будет истина, или единица. В случае 0 xor 0 или 1 xor 1 - результатом будет ложь, или ноль. В нашем случае в результате выполнения этой инструкции применительно к регистрам еах (7D7A3BB) и есх (FAF47770) в регистр еах запишется значение FD23D4CB.

Следующая команда LEA ecx, элегантно и непринужденно умножает еах на 9 и записывает результат в есх. Затем это значение копируется в edx и сдвигается вправо на 13 разрядов: получаем 73213 в еdx и E6427B23 в есх. Затем - снова ксорим есх и edx, записывая в есх E6454930. Копируем это в еах, сдвигаем влево на 9 разрядов: 8А926000, затем инвертируем это, получая 756D9FFF. Прибавляем это значение к регистру есх - имеем 5BB2E92F. Копируем это в еах, сдвигаем вправо аж на 17 разрядов - 2DD9 - и ксорим с есх. Получаем в итоге 5BB2C4F6. Затем… затем… что там у нас? Что, все?..
Итак, мы сохраняем это значение в область памяти по смещению var_4, загружаем из стека состояния регистров, снова берем из памяти итоговое значение и окончательно забираем из стека оставшиеся там состояния регистров, сохраненные в начале. Выходим из функции. Ура!.. впрочем, радоваться еще рано, пока что на выходе из первого вызова функции мы имеем максимум - четыре полупечатных символа, а ведь у нас еще целая необработанная строка есть, да и эту еще к божескому виду привести надо.

Перейдем на более высокий уровень анализа - от дизассемблера к декомпилятору. Представим всю функцию DialogFunc, в которой содержатся вызовы sub_401100, в виде С-подобного псевдокода. Собственно говоря, это дизассемблер называет его «псевдокодом», на деле это практически и есть код на С, только страшненький. Глядим:

Нужно больше кода. Нужно построить зиккурат.

SendDlgItemMessageA(hDlg, 1000, 0xDu, 0xAu, (LPARAM)&lParam); SendDlgItemMessageA(hDlg, 1001, 0xDu, 0xAu, (LPARAM)&v15); v5 = sub_401100((char)lParam | ((SBYTE1(lParam) | ((SBYTE2(lParam) | (SBYTE3(lParam) << 8)) << 8)) << 8)); v6 = 0; do { v21 = v5 % 0x24; v7 = v21; v5 /= 0x24u; if (v7 >= 10) v8 = v7 + 55; else v8 = v7 + 48; v21 = v8; } while (v6 < 4); v22 = 0; v9 = sub_401100(v15 | ((v16 | ((v17 | (v18 << 8)) << 8)) << 8)); v10 = 0; do { v19 = v9 % 0x24; v11 = v19; v9 /= 0x24u; if (v11 >= 10) v12 = v11 + 55; else v12 = v11 + 48; v19 = v12; } while (v10 < 4); v20 = 0; wsprintfA(&v13, "%s-%s-%s-%s", &lParam, &v15, v21, v19); SendDlgItemMessageA(hDlg, 1002, 0xCu, 0, (LPARAM)&v13);

Это уже легче читать, чем ассемблерный листинг. Однако не во всех случаях можно положиться на декомпилятор: нужно быть готовым часами следить за нитью ассемблерной логики, за состояниями регистров и стека в отладчике… а потом давать письменные объяснения сотрудникам ФСБ или ФБР. Под вечер у меня особенно смешные шутки.
Как я уже сказал, читать это легче, но до совершенства еще далеко. Давайте проанализируем код и дадим переменным более удобочитаемые названия. Ключевым переменным дадим понятные и логичные названия, а счетчикам и временным - попроще.

То же самое, только переведенное с китайского на индусский.

SendDlgItemMessageA(hDlg, 1000, 0xDu, 0xAu, (LPARAM)&first_given_string); SendDlgItemMessageA(hDlg, 1001, 0xDu, 0xAu, (LPARAM)&second_given_string); first_given_string_encoded = sub_401100((char)first_given_string | ((SBYTE1(first_given_string) | ((SBYTE2(first_given_string) | (SBYTE3(first_given_string) << 8)) << 8)) << 8)); i = 0; do { first_result_string[i] = first_string_encoded % 0x24; temp_char = first_result_string[i]; first_string_encoded /= 0x24u; if (temp_char >= 10) next_char = temp_char + 55; else next_char = temp_char + 48; first_result_string = next_char; } while (i < 4); some_kind_of_data = 0; second_string_encoded = sub_401100(byte1 | ((byte2 | ((byte3 | (byte4 << 8)) << 8)) << 8)); j = 0; do { second_result_string[j] = second_string_encoded % 0x24; temp_char2 = second_result_string[j]; second_string_encoded /= 0x24u; if (temp_char2 >= 10) next_char2 = temp_char2 + 55; else next_char2 = temp_char2 + 48; second_result_string = next_char2; } while (j < 4); yet_another_some_kind_of_data = 0; wsprintfA(&buffer, "%s-%s-%s-%s", &first_given_string, &second_given_string, first_result_string, second_result_string); SendDlgItemMessageA(hDlg, 1002, 0xCu, 0, (LPARAM)&buffer);

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

Если нет, то никакие книги не помогут. В этом деле нужна страсть и огромное терпение.

Матан никому не нужен в реверсе. Максимум решение систем линейных уравнений понадобится.
Важно скорее нестандартное мышление, умение брутфорсить в голове множество вариантов, подходов. Для этого нужно знать технологии. Т. е. буквально, знать нужно как можно больше . Чем больше вы знаете, тем быстрее будет решена задача. Это абсолютно различные области: ОС, сети, способы шифрования, компрессии, хеширования, сериализации; знание баз данных и их языков запросов; знание компиляторов в плане, как они генерируют код; знание реализации той же стандартной библиотеки, понимание как компилируется один и тот же код разными компиляторами, понимать, как работают интерпретаторы байткода, виртуальные машины, и т. п.

Это, что касается общих технологий. А есть ещё такая штука, как архитектурные паттерны. Они обычно применяются в прикладных приложениях, малварь редко такое применяет. Т. е. нужно видеть в коде, например, паттерн Event, различные варианты паттерна MVC, и т. п. Например, вы реверсите продукт на Qt. Чтобы его понимать, вам нужно знать... Qt, и уметь на нём разрабатывать, читать его исходный код, знать что такое метаобъекты, как они хранятся, используются, вызываются. А если, внезапно, оно юзает что-то интерпретируемое, типа питона или луа, то вам, мало того, что нужно знать сами языки, так ещё и реализацию их интерпретаторов. А ещё бывает JIT...

Нужно ещё решить, что вы хотите реверсить. Малварь и прикладные приложения немного расходятся. В малвари нужно больше знать нестандартных вещей. Различных вариантов антиотладки, скрытия активности, багов операционной системы, поведения антивирусов. Малварь может быть ботнетом, например. Ботнеты обычно имеют командный сервер, который довольно трудно вычислить, он меняется динамически, как-то не даёт себя обнаружить. Для этого нужно знать как устроен интернет, как работает dns, разбираться в сетевых протоколах.

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

Кстати, чуть не забыл.

Самая лучшая книга по реверсу на русском.

И ещё есть классический курс статей от Рикардо Нарвахи: «Введение в крэкинг с нуля, используя OllyDbg». Погуглите его. Если осилите книгу Юричева и этот курс, то сможете спокойно собеседоваться в Касперский. Хотя, поверьте, есть вещи поинтереснее касперского.