Реверс-инжиниринг для начинающих: основные концепции программирования
В этой статье рассмотрены основы реверс инжиниринга и основные способы дизассемблирования и изучения кода приложений с целью защиты от взлома
54К открытий55К показов
В этой статье мы заглянем под капот программного обеспечения. Новички в реверс-инжиниринге получат общее представление о самом процессе исследования ПО, общих принципах построения программного кода и о том, как читать ассемблерный код.
Примечание Программный код для этой статьи компилируется с помощью Microsoft Visual Studio 2015, так что некоторые функции в новых версиях могут использоваться по-другому. В качестве дизассемблера используется IDA Pro.
- Инициализация переменных
- Стандартная функция вывода
- Математические операции
- Вызов функций
- Циклы
- Условный оператор
- Оператор выбора
- Пользовательский ввод
Инициализация переменных
Переменные — одна из основных составляющих программирования. Они делятся на несколько видов, вот некоторые из них:
- строка;
- целое число;
- логическая переменная;
- символ;
- вещественное число с двойной точностью;
- вещественное число;
- массив символов.
Стандартные переменные:
Примечание в С++ строка — не примитивная переменная, но важно понять, как она будет выглядеть в машинном коде.
Давайте посмотрим на ассемблерный код:
Здесь можно увидеть как IDA показывает распределение пространства для переменных. Сначала под каждую переменную выделяется пространство, а потом уже она инициализируется.
Как только пространство выделено, в него помещается значение, которое мы хотим присвоить переменной. Инициализация большинства переменных представлена на картинке выше, но как инициализируется строка, показано ниже.
Для инициализации строки требуется вызов встроенной функции.
Стандартная функция вывода
Примечание Здесь речь пойдёт о том, что переменные помещаются в стек и затем используются в качестве параметров для функции вывода. Концепт функции с параметрами будет рассмотрен позднее.
Для вывода данных было решено использовать printf()
, а не cout
.
Стандартный вывод:
Теперь посмотрим на машинный код. Сначала строковый литерал:
Как видите, строковый литерал сначала помещается в стек для вызова в качестве параметра функции printf()
.
Теперь посмотрим на вывод одной из переменных:
Как можно заметить, сначала переменная intvar
помещается в регистр EAX, который в свою очередь записывается в стек вместе со строковым литералом %i
, используемым для обозначения целочисленного вывода. Эти переменные затем берутся из стека и используются в качестве параметров при вызове функции printf()
.
Математические операции
Сейчас мы поговорим о следующих математических операциях:
- Сложение.
- Вычитание.
- Умножение.
- Деление.
- Поразрядная конъюнкция (И).
- Поразрядная дизъюнкция (ИЛИ).
- Поразрядное исключающее ИЛИ.
- Поразрядное отрицание.
- Битовый сдвиг вправо.
- Битовый сдвиг влево.
Переведём каждую операцию в ассемблерный код:
сначала присвоим переменной A
значение 0A
в шестнадцатеричной системе счисления или 10 в десятичной. Переменной B
— 0F
, что равно 15 в десятичной.
Для сложения мы используем инструкцию add
:
При вычитании используется инструкция sub
:
При умножении — imul
:
Для деления используется инструкция idiv
. Также мы используем оператор cdq
, чтобы удвоить размер EAX и результат деления уместился в регистре.
При поразрядной конъюнкции используется инструкция and
:
При поразрядной дизъюнкции — or
:
При поразрядном исключающем ИЛИ — xor
:
При поразрядном отрицании — not
:
При битовом сдвиге вправо — sar
:
При битовом сдвиге влево — shl
:
Вызов функций
Мы рассмотрим три вида функций:
- Функция, не возвращающая значение (void).
- Функция, возвращающая целое число.
- Функция с параметрами.
Вызов функций:
Сначала посмотрим, как происходит вызов функций 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 запускается, у него есть 2 варианта:
- он может перейти к блоку справа (зелёная стрелка) и вернуться в основную программу;
- он может перейти к блоку слева (красная стрелка) и вернуться к началу цикла 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
.
Сначала программа устанавливает содержимое переменной sentence в EAX, затем помещает EAX в стек, откуда значение переменной будет использоваться в качестве параметра для потока cin
, затем вызывается оператор потока >>. Его вывод помещается в ECX, который затем помещается в стек для оператора printf()
:
Мы рассмотрели лишь основные принципы работы программного обеспечения на низком уровне. Без этих основ невозможно понимать работу ПО и, соответственно, заниматься его исследованием.
Смотрите также: Эксплуатация уязвимостей исполняемых файлов для новичков
54К открытий55К показов