No Image

Реверс инжиниринг для начинающих

СОДЕРЖАНИЕ
11 просмотров
10 марта 2020
    Переводы, 13 февраля 2019 в 14:00

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

Примечание Программный код для этой статьи компилируется с помощью Microsoft Visual Studio 2015, так что некоторые функции в новых версиях могут использоваться по-другому. В качестве дизассемблера используется IDA Pro.

Инициализация переменных

Переменные — одна из основных составляющих программирования. Они делятся на несколько видов, вот некоторые из них:

  • строка;
  • целое число;
  • логическая переменная;
  • символ;
  • вещественное число с двойной точностью;
  • вещественное число;
  • массив символов.

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

Давайте посмотрим на ассемблерный код:

Здесь можно увидеть как IDA показывает распределение пространства для переменных. Сначала под каждую переменную выделяется пространство, а потом уже она инициализируется.

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

Инициализация строковой переменной в C++

Для инициализации строки требуется вызов встроенной функции.

Стандартная функция вывода

Примечание Здесь речь пойдёт о том, что переменные помещаются в стек и затем используются в качестве параметров для функции вывода. Концепт функции с параметрами будет рассмотрен позднее.

Для вывода данных было решено использовать printf() , а не cout .

19 ноября 2019 – 10 января 2020, Гусев и онлайн, беcплатно

Теперь посмотрим на машинный код. Сначала строковый литерал:

Вывод строкового литерала

Как видите, строковый литерал сначала помещается в стек для вызова в качестве параметра функции printf() .

Теперь посмотрим на вывод одной из переменных:

Как можно заметить, сначала переменная intvar помещается в регистр EAX, который в свою очередь записывается в стек вместе со строковым литералом %i , используемым для обозначения целочисленного вывода. Эти переменные затем берутся из стека и используются в качестве параметров при вызове функции printf() .

Математические операции

Сейчас мы поговорим о следующих математических операциях:

  1. Сложение.
  2. Вычитание.
  3. Умножение.
  4. Деление.
  5. Поразрядная конъюнкция (И).
  6. Поразрядная дизъюнкция (ИЛИ).
  7. Поразрядное исключающее ИЛИ.
  8. Поразрядное отрицание.
  9. Битовый сдвиг вправо.
  10. Битовый сдвиг влево.

Переведём каждую операцию в ассемблерный код:

сначала присвоим переменной A значение 0A в шестнадцатеричной системе счисления или 10 в десятичной. Переменной B — 0F , что равно 15 в десятичной.

Для сложения мы используем инструкцию add :

При вычитании используется инструкция sub :

При умножении — imul :

Для деления используется инструкция idiv . Также мы используем оператор cdq , чтобы удвоить размер EAX и результат деления уместился в регистре.

При поразрядной конъюнкции используется инструкция and :

При поразрядной дизъюнкции — or :

При поразрядном исключающем ИЛИ — xor :

Поразрядное исключающее ИЛИ

При поразрядном отрицании — not :

При битовом сдвиге вправо — sar :

Битовый сдвиг вправо

При битовом сдвиге влево — shl :

Битовый сдвиг влево

Вызов функций

Мы рассмотрим три вида функций:

  1. Функция, не возвращающая значение (void).
  2. Функция, возвращающая целое число.
  3. Функция с параметрами.

Сначала посмотрим, как происходит вызов функций newfunc() и newfuncret() , которые вызываются без параметров.

Вызов функций без параметров

Функция newfunc() просто выводит сообщение «Hello! I’m a new function!»:

Эта функция использует инструкцию retn , но только для возврата к предыдущему местоположению (чтобы программа могла продолжить свою работу после завершения функции). Посмотрим на функцию newfuncret() , которая генерирует случайное целое число с помощью функции С++ rand() и затем его возвращает.

Сначала выделяется место под переменную A . Затем вызывается функция rand() , результат которой помещается в регистр EAX. Затем значение EAX помещается в место, выделенное под переменную A , фактически присваивая переменной A результат функции rand() . Наконец, переменная A помещается в регистр EAX, чтобы функция могла его использовать в качестве возвращаемого параметра. Теперь, когда мы разобрались, как происходит вызов функций без параметров и что происходит при возврате значения из функции, поговорим о вызове функции с параметрами.

Вызов такой функции выглядит следующим образом:

Вызов функции с параметрами

Строки в С++ требуют вызова функции basic_string , но концепция вызова функции с параметрами не зависит от типа данных. Сначала переменная помещается в регистр, затем оттуда в стек, а потом происходит вызов функции.

Посмотрим на код функции:

Эта функция берёт строку, целое число и символ и печатает их с помощью функции printf() . Как видите, сначала переменные размещаются в начале функции, затем они помещаются в стек для вызова в качестве параметров функции printf() . Очень просто.

Циклы

Теперь, когда мы изучили вызов функции, вывод, переменные и математику, перейдём к контролю порядка выполнения кода (flow control). Сначала мы изучим цикл for:

Графический обзор цикла for

Прежде чем разбить ассемблерный код на более мелкие части, посмотрим на общий вариант. Как вы можете видеть, когда цикл for запускается, у него есть 2 варианта:

  • он может перейти к блоку справа (зелёная стрелка) и вернуться в основную программу;
  • он может перейти к блоку слева (красная стрелка) и вернуться к началу цикла for.
Читайте также:  1С сортировка таблицы на форме

Цикл for подробно

Сначала сравниваются переменные i и max , чтобы проверить, достигла ли переменная максимального значения. Если переменная i не больше или не равна переменной max , то подпрограмма пойдёт по красной стрелке (вниз влево) и выведет переменную i , затем i увеличится на 1 и произойдёт возврат к началу цикла. Если переменная i больше или равна max , то подпрограмма пойдёт по зелёной стрелке, то есть выйдет из цикла for и вернётся в основную программу.

Теперь давайте взглянем на цикл while :

В этом цикле генерируется случайное число от 0 до 20. Если число больше 10, то произойдёт выход из цикла со словами «I’m out!», в противном случае продолжится работа в цикле.

В машинном коде переменная А сначала инициализируется и приравнивается к нулю, а затем инициализируется цикл, A сравнивается с шестнадцатеричным числом 0A , которое равно 10 в десятичной системе счисления. Если А не больше и не равно 10, то генерируется новое случайное число, которое записывается в А , и снова происходит сравнение. Если А больше или равно 10, то происходит выход из цикла и возврат в основную программу.

Условный оператор

Теперь поговорим об условных операторах. Для начала посмотрим код:

Эта функция генерирует случайное число от 0 до 20 и сохраняет получившееся значение в переменной А . Если А больше 15, то программа выведет «greater than 15». Если А меньше 15, но больше 10 — «less than 15, greater than 10». Если меньше 5 — «less than 5».

Посмотрим на ассемблерный граф:

Ассемблерный граф для условного оператора

Граф структурирован аналогично фактическому коду, потому что условный оператор выглядит просто: «Если X, то Y, иначе Z». Если посмотреть на первую сверху пару стрелок, то оператору предшествует сравнение А с 0F , что равно 15 в десятичной системе счисления. Если А больше или равно 15, то подпрограмма выведет «greater than 15» и вернётся в основную программу. В другом случае произойдёт сравнение А с 0A (1010). Так будет продолжаться, пока программа не выведет что-нибудь на экран и не вернётся.

Оператор выбора

Оператор выбора очень похож на оператор условия, только в операторе выбора одна переменная или выражение сравнивается с несколькими «случаями» (возможными эквивалентностями). Посмотрим код:

В этой функции переменная А получает случайное значение от 0 до 10. Затем А сравнивается с несколькими случаями, используя switch . Если значение А равно одному из случаев, то на экране появится соответствующее число, а затем произойдёт выход из оператора выбора и возврат в основную программу.

Оператор выбора не следует правилу «Если X, то Y, иначе Z» в отличии от условного оператора. Вместо этого программа сравнивает входное значение с существующими случаями и выполняет только тот случай, который соответствует входному значению. Рассмотрим два первых блока подробней.

Два первых блока оператора выбора

Сначала генерируется случайное число и записывается в А . Теперь программа инициализирует оператор выбора, приравняв временную переменную var_D0 к А , затем проверяет, что она равна хотя бы одному из случаев. Если var_D0 требуется значение по умолчанию, то программа пойдёт по зелёной стрелке в секцию окончательного возврата из подпрограммы. Иначе программа совершит переход в нужный case .

Если var_D0 (A) равно 5, то код перейдёт в секцию, которая показана выше, выведет «5» и затем перейдёт в секцию возврата.

Пользовательский ввод

В этом разделе мы рассмотрим ввод пользователя с помощью потока сin из C++. Во-первых, посмотрим на код:

В этой функции мы просто записываем строку в переменную sentence с помощью функции C++ cin и затем выводим предложение с помощью оператора printf() .

Разберём это в машинном коде. Во-первых, функция cin :

Сначала происходит инициализация строковой переменной sentence , затем вызов cin и запись введённых данных в sentence .

Функция C++ cin детальнее

Сначала программа устанавливает содержимое переменной sentence в EAX, затем помещает EAX в стек, откуда значение переменной будет использоваться в качестве параметра для потока cin , затем вызывается оператор потока >>. Его вывод помещается в ECX, который затем помещается в стек для оператора printf() :

Мы рассмотрели лишь основные принципы работы программного обеспечения на низком уровне. Без этих основ невозможно понимать работу ПО и, соответственно, заниматься его исследованием.

Вначале было слово. Двойное

Открыв файл кейгена в 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.

Читайте также:  Как выключить автоматическую перезагрузку windows 10

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

Сначала подряд вызываются две функции 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.

В самом начале здесь ничего интересного — состояния регистров заботливо сохраняются в стеке. А вот первая команда, которая нам интересна — следующая за инструкцией 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.

Читайте также:  Почему не устанавливается автокад на виндовс 10

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

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

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

Эпилог

Level complete. Cледующая (и заключительная) цель — это написание своего кейгена по этому алгоритму. Писать я, по привычке, буду на языке скриптов командной оболочки Linux bash. test $ <#reg1>-gt && reg1=`echo "$"` — это обрезка строки, содержащей эмулированное значение регистра, до 8 младших символов. При выполнении операций туда добавлялись лишние старшие разряды. Все остальное — кропотливая эмуляция ассемблерного листинга. Я же указал вверху хаб «Ненормальное программирование», да.

bash-реализация пресловутой sub_401100:

Основная функция кейгена:

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

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

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

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

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

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

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

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

Комментировать
11 просмотров
Комментариев нет, будьте первым кто его оставит

Это интересно
No Image Компьютеры
0 комментариев
No Image Компьютеры
0 комментариев
No Image Компьютеры
0 комментариев
No Image Компьютеры
0 комментариев
Adblock detector