Путь становления Реверс инженера? Реверс-инжиниринг для начинающих. Защита Android-приложений от реверс-инжиниринга Вначале было слово

29.05.2021 Устройства

Собрали несколько отличных книг по реверс-инжинирингу, которые подойдут и новичкам, и желающим попробовать что-то новое, будь то iOS или Xbox.

Reverse Engineering для начинающих

«Теперь, когда Денис Юричев сделал эту книгу бесплатной, она стала вкладом в мир свободных знаний и бесплатного образования» – Ричард Столман (Richard Stallman), основатель GNU, активист в области свободного ПО.

«Reverse Engineering для начинающих» – не только учебник по реверс-инжинирингу, но и отличный учебник по основам программирования, который подойдет как для изучения глубин C++ и Java, так и для лучшего понимания того, как работает компьютер.

BIOS DISASSEMBLY NINJUTSU UNCOVERED

Многие годы среди компьютерных энтузиастов и практиков бытует миф о том, что модификация BIOS (Basic Input Output System) – своего рода черная магия и лишь немногие на это способны или что только производитель материнской платы может выполнить такую ​​задачу. Эта книга показывает, что при правильных инструментах и ​​системном подходе к реверс-инжинирингу каждый может понять и модифицировать BIOS в соответствии с их потребностями без наличия исходного кода.

iOS App Reverse Engineering

Книга написана «слоями» – теория, практика, теория и снова практика. Она состоит из 4 частей:

— Понятия
— Инструменты
— Теория
— Практика

В первой части разбираются базовые концепции iOS, иерархия файловой системы, типы файлов, скрытые от разработчиков приложений, но необходимые для исследователей системы. Во второй части рассматриваются основные инструменты для реверс-инжиниринга системы, такие как Theos, Cycript, Reveal, IDA и LLDB. Далее рассматривается теория реверс-инжиниринга iOS на Objective-C, объясняются методологии. А в последней части рассматриваются 4 практики реверс-инжиниринга системы, разработанные на основе теории и практики из прошлых частей книги.

Hacking the Xbox: An Introduction to Reverse Engineering

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

  • 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);

  • 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». Погуглите его. Если осилите книгу Юричева и этот курс, то сможете спокойно собеседоваться в Касперский. Хотя, поверьте, есть вещи поинтереснее касперского.