В этой статье мы заглянем под капот программного обеспечения. Новички в реверс-инжиниринге получат общее представление о самом процессе исследования ПО, общих принципах построения программного кода и о том, как читать ассемблерный код.
Примечание Программный код для этой статьи компилируется с помощью Microsoft Visual Studio 2015, так что некоторые функции в новых версиях могут использоваться по-другому. В качестве дизассемблера используется IDA Pro.- Инициализация переменных
- Стандартная функция вывода
- Математические операции
- Вызов функций
- Циклы
- Условный оператор
- Оператор выбора
- Пользовательский ввод
Инициализация переменных
Переменные — одна из основных составляющих программирования. Они делятся на несколько видов, вот некоторые из них:
- строка;
- целое число;
- логическая переменная;
- символ;
- вещественное число с двойной точностью;
- вещественное число;
- массив символов.
Стандартные переменные:
string stringvar = "Hello World";
int intvar = 100;
bool boolvar = false;
char charvar = 'B';
double doublevar = 3.1415;
float floatvar = 3.14159265;
char carray[] = { 'a', 'b', 'c', 'd', 'e' };
Примечание в С++ строка — не примитивная переменная, но важно понять, как она будет выглядеть в машинном коде.
Давайте посмотрим на ассемблерный код:
Здесь можно увидеть как IDA показывает распределение пространства для переменных. Сначала под каждую переменную выделяется пространство, а потом уже она инициализируется.

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

Инициализация строковой переменной в C++
Для инициализации строки требуется вызов встроенной функции.
Стандартная функция вывода
Примечание Здесь речь пойдёт о том, что переменные помещаются в стек и затем используются в качестве параметров для функции вывода. Концепт функции с параметрами будет рассмотрен позднее.Для вывода данных было решено использовать printf()
, а не cout
.
Стандартный вывод:
printf("Hello String Literal");
printf("%s", stringvar);
printf("%i", intvar);
printf("%c", charvar);
printf("%f", doublevar);
printf("%f", floatvar);
printf("%c", carray[3]);
Теперь посмотрим на машинный код. Сначала строковый литерал:

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

Вывод переменной
Как можно заметить, сначала переменная intvar
помещается в регистр EAX, который в свою очередь записывается в стек вместе со строковым литералом %i
, используемым для обозначения целочисленного вывода. Эти переменные затем берутся из стека и используются в качестве параметров при вызове функции printf()
.
Математические операции
Сейчас мы поговорим о следующих математических операциях:
- Сложение.
- Вычитание.
- Умножение.
- Деление.
- Поразрядная конъюнкция (И).
- Поразрядная дизъюнкция (ИЛИ).
- Поразрядное исключающее ИЛИ.
- Поразрядное отрицание.
- Битовый сдвиг вправо.
- Битовый сдвиг влево.
void mathfunctions() { // математические операции
int A = 10;
int B = 15;
int add = A + B;
int sub = A - B;
int mult = A * B;
int div = A / B;
int and = A & B;
int or = A | B;
int xor = A ^ B;
int not = ~A;
int rshift = A >> B;
int lshift = A << B;
}
Переведём каждую операцию в ассемблерный код:
сначала присвоим переменной A
значение 0A
в шестнадцатеричной системе счисления или 10 в десятичной. Переменной B
— 0F
, что равно 15 в десятичной.

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

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

Вычитание
При умножении — imul
:

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

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

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

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

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

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

Битовый сдвиг вправо
При битовом сдвиге влево — shl
:

Битовый сдвиг влево
Вызов функций
Мы рассмотрим три вида функций:
- Функция, не возвращающая значение (void).
- Функция, возвращающая целое число.
- Функция с параметрами.
Вызов функций:
newfunc();
newfuncret();
funcparams(intvar, stringvar, charvar);
Сначала посмотрим, как происходит вызов функций newfunc()
и newfuncret()
, которые вызываются без параметров.

Вызов функций без параметров
Функция newfunc()
просто выводит сообщение «Hello! I’m a new function!»:
void newfunc() { // новая функция без параметров
printf("Hello! I'm a new function"!);
}
Эта функция использует инструкцию retn
, но только для возврата к предыдущему местоположению (чтобы программа могла продолжить свою работу после завершения функции). Посмотрим на функцию newfuncret()
, которая генерирует случайное целое число с помощью функции С++ rand()
и затем его возвращает.
int newfuncret() { // новая функция, которая что-то возвращает
int A = rand();
return A;
}

Функция newfuncret()
Сначала выделяется место под переменную A
. Затем вызывается функция rand()
, результат которой помещается в регистр EAX. Затем значение EAX помещается в место, выделенное под переменную A
, фактически присваивая переменной A
результат функции rand()
. Наконец, переменная A помещается в регистр EAX, чтобы функция могла его использовать в качестве возвращаемого параметра. Теперь, когда мы разобрались, как происходит вызов функций без параметров и что происходит при возврате значения из функции, поговорим о вызове функции с параметрами.
Вызов такой функции выглядит следующим образом:
funcparams(intvar, stringvar, charvar);
Строки в С++ требуют вызова функции basic_string
, но концепция вызова функции с параметрами не зависит от типа данных. Сначала переменная помещается в регистр, затем оттуда в стек, а потом происходит вызов функции.
Посмотрим на код функции:
void funcparams (int iparam, string sparam, char cparam) { // функция с параметрами
printf("%i \n", iparam);
printf("%s \n", sparam);
printf("%c \n", cparam);
}
Эта функция берёт строку, целое число и символ и печатает их с помощью функции printf()
. Как видите, сначала переменные размещаются в начале функции, затем они помещаются в стек для вызова в качестве параметров функции printf()
. Очень просто.
Циклы
Теперь, когда мы изучили вызов функции, вывод, переменные и математику, перейдём к контролю порядка выполнения кода (flow control). Сначала мы изучим цикл for:
void forloop (int max) { // обычный цикл for
for (int i = 0; i < max; ++i){
printf("%i \n", i);
}
}
Прежде чем разбить ассемблерный код на более мелкие части, посмотрим на общий вариант. Как вы можете видеть, когда цикл for запускается, у него есть 2 варианта:
- он может перейти к блоку справа (зелёная стрелка) и вернуться в основную программу;
- он может перейти к блоку слева (красная стрелка) и вернуться к началу цикла for.
Сначала сравниваются переменные i
и max
, чтобы проверить, достигла ли переменная максимального значения. Если переменная i
не больше или не равна переменной max
, то подпрограмма пойдёт по красной стрелке (вниз влево) и выведет переменную i
, затем i
увеличится на 1 и произойдёт возврат к началу цикла. Если переменная i
больше или равна max
, то подпрограмма пойдёт по зелёной стрелке, то есть выйдет из цикла for
и вернётся в основную программу.
Теперь давайте взглянем на цикл while
:
void whileloop() { // цикл while
int A = 0;
while (A<10) {
A = 0 + (rand()%(int)(20-0+1))
}
printf("I'm out!");
}
В этом цикле генерируется случайное число от 0 до 20. Если число больше 10, то произойдёт выход из цикла со словами «I’m out!», в противном случае продолжится работа в цикле.
В машинном коде переменная А
сначала инициализируется и приравнивается к нулю, а затем инициализируется цикл, A
сравнивается с шестнадцатеричным числом 0A
, которое равно 10 в десятичной системе счисления. Если А
не больше и не равно 10, то генерируется новое случайное число, которое записывается в А
, и снова происходит сравнение. Если А
больше или равно 10, то происходит выход из цикла и возврат в основную программу.
Условный оператор
Теперь поговорим об условных операторах. Для начала посмотрим код:
void ifstatement() { // условные операторы
int A = 0 + (rand()%(int)(20-0+1));
if (A < 15) {
if (A < 10) {
if (A < 5) {
printf("less than 5");
}
else {
printf("less than 10, greater than 5");
}
}
else {
printf("less than 15, greater than 10");
}
}
else {
printf("greater than 15");
}
}
Эта функция генерирует случайное число от 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). Так будет продолжаться, пока программа не выведет что-нибудь на экран и не вернётся.
Оператор выбора
Оператор выбора очень похож на оператор условия, только в операторе выбора одна переменная или выражение сравнивается с несколькими «случаями» (возможными эквивалентностями). Посмотрим код:
void switchcase() { // оператор выбора
int A = 0 + (rand()%(int)(10-0+1));
switch (A) {
case 0:
printf("0");
break;
case 1:
printf("1");
break;
case 2:
printf("2");
break;
case 3:
printf("3");
break;
case 4:
printf("4");
break;
case 5:
printf("5");
break;
case 6:
printf("6");
break;
case 7:
printf("7");
break;
case 8:
printf("8");
break;
case 9:
printf("9");
break;
case 10:
printf("10");
break;
}
}
В этой функции переменная А
получает случайное значение от 0 до 10. Затем А
сравнивается с несколькими случаями, используя switch
. Если значение А
равно одному из случаев, то на экране появится соответствующее число, а затем произойдёт выход из оператора выбора и возврат в основную программу.
Оператор выбора не следует правилу «Если X, то Y, иначе Z» в отличии от условного оператора. Вместо этого программа сравнивает входное значение с существующими случаями и выполняет только тот случай, который соответствует входному значению. Рассмотрим два первых блока подробней.
Сначала генерируется случайное число и записывается в А
. Теперь программа инициализирует оператор выбора, приравняв временную переменную var_D0
к А
, затем проверяет, что она равна хотя бы одному из случаев. Если var_D0
требуется значение по умолчанию, то программа пойдёт по зелёной стрелке в секцию окончательного возврата из подпрограммы. Иначе программа совершит переход в нужный case
.
Если var_D0 (A)
равно 5, то код перейдёт в секцию, которая показана выше, выведет «5» и затем перейдёт в секцию возврата.
Пользовательский ввод
В этом разделе мы рассмотрим ввод пользователя с помощью потока сin
из C++. Во-первых, посмотрим на код:
void userinput() { // ввод с клавиатуры
string sentence;
cin >> sentence;
printf("%s", sentence);
}
В этой функции мы просто записываем строку в переменную sentence с помощью функции C++ cin и затем выводим предложение с помощью оператора printf()
.
Разберём это в машинном коде. Во-первых, функция cin
:
Сначала происходит инициализация строковой переменной sentence
, затем вызов cin
и запись введённых данных в sentence
.
Сначала программа устанавливает содержимое переменной sentence в EAX, затем помещает EAX в стек, откуда значение переменной будет использоваться в качестве параметра для потока cin
, затем вызывается оператор потока >>. Его вывод помещается в ECX, который затем помещается в стек для оператора printf()
:
Мы рассмотрели лишь основные принципы работы программного обеспечения на низком уровне. Без этих основ невозможно понимать работу ПО и, соответственно, заниматься его исследованием.
Смотрите также: Эксплуатация уязвимостей исполняемых файлов для новичков
Перевод статьи «BOLO: Reverse Engineering — Part 1 (Basic Programming Concepts)»
Варвара Николаева