• XSS.stack #1 – первый литературный журнал от юзеров форума

Статья BOLO: Reverse Engineering

Azrv3l

win32kfull
Эксперт
Регистрация
30.03.2019
Сообщения
215
Реакции
539
1.jpeg


В процессе обучения реверсингу я обнаружил, что мне нужно простое руководство о том, на что обращать внимание при просмотре кода. Хотя я понимаю необходимость чтения исходного кода и руководств для получения информации, я также понимаю желание иметь краткую, легкую для понимания информацию в одном месте. Серия «BOLO: Reverse Engineering» - это именно то, что вам нужно! В этой серии статей я буду показывать вам, на что следует обратить внимание при реверс-инжиниринге кода. В идеале, эта серия статей упростит начинающим реверс-инженерам понимание множества различных концепций!

Предисловие
В этой статье вы увидите скриншоты кода C ++ и ассемблера, а также некоторые объяснения того, что вы видите, и почему вещи выглядят так, как они выглядят. Кроме того, в этой серии статей не будут рассмотрены основы ассемблера, в ней будут представлены только шаблоны и декомпилированный код, чтобы вы могли получить общее представление о том, что искать / как интерпретировать ассемблер.

В этой статье мы рассмотрим:
  1. Инициирование переменной
  2. Базовый вывод
  3. Математические операции
  4. Функции
  5. Циклы (цикл for / цикл while)
  6. Условные операторы (оператор if / оператор switch)
  7. Пользовательский ввод
Обратите внимание: это руководство было создано с помощью Visual C ++ в Microsoft Visual Studio 2015 (я знаю, устаревшая версия). Некоторая часть ассемблера (например, ввод пользователя с помощью cin) будет отражать это. Кроме того, я использую IDA Pro в качестве дизассемблера.

Инициирование переменной
Переменные чрезвычайно важны при программировании, здесь мы видим несколько важных переменных:
  1. string
  2. int
  3. boolean
  4. char
  5. double
  6. float
  7. char массив
2.jpeg


Обратите внимание: в C++ «string» не является примитивной переменной, но я счел важным все равно показать вам её

Теперь посмотрим на ассемблер:

3.jpeg


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

4.jpeg


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

5.jpeg


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

Базовый вывод
предисловие: в этом разделе я буду говорить об элементах, помещаемых в стек и используемых в качестве параметров для функции printf. Концепция параметров функции будет более подробно объяснена позже в этой статье.

Хотя это руководство было построено на Visual C++, я решил использовать для вывода printf, а не cout.

6.jpeg


Теперь посмотрим на ассемблер:

Во-первых, строковый литерал:

7.jpeg


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

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

8.jpeg


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

Математические операции
В этом разделе мы рассмотрим следующие математические функции:
  1. Сложение
  2. Вычетание
  3. Умножение
  4. Деление
  5. Побитовое AND
  6. Побитовое OR
  7. Побитовое XOR
  8. Побитовое NOT
  9. Побитовый сдвиг вправо
  10. Побитовый сдвиг влево
9.jpeg


Разберем каждую функцию:

Во-первых, мы устанавливаем A как шестнадцатеричный 0A, который представляет десятичное 10, и B в шестнадцатеричный 0F, который представляет десятичный 15.

10.jpeg


Мы складываем с помощью опкода «add»:

11.jpeg


Мы вычитаем, используя опкод «sub»:

12.jpeg


Мы умножаем, используя опкод «imul»:

13.jpeg


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

14.jpeg


Мы выполняем побитовое AND, используя опкод «and»:

15.jpeg


Мы выполняем побитовое OR, используя опкод «or»:

16.jpeg


Мы выполняем побитовое XOR, используя опкод «xor»:

17.jpeg


Мы выполняем побитовое NOT, используя опкод «not»:

18.jpeg


Мы выполняем побитовый сдвиг вправо, используя опкод «sar»:

19.jpeg


Мы выполняем побитовый сдвиг влево, используя опкод «shl»:

20.jpeg


Функции
В этом разделе мы рассмотрим 3 различных типа функций:
  1. Базовая функция void
  2. Функция, которая возвращает int
  3. Функция, которая принимает параметры
21.jpeg


Во-первых, давайте взглянем на вызовы newfunc() и newfuncret(), потому что ни один из них на самом деле не принимает никаких параметров.

22.jpeg


Если мы проследим за вызовом функции newfunc(), мы увидим, что все, что она действительно делает, это выводит «Hello! I’m a new function!»:

23.jpeg


24.jpeg


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

25.jpeg


26.jpeg


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

Теперь, когда у нас есть понимание того, как вызывать функцию и как это выглядит, когда функция что-то возвращает, давайте поговорим о вызове функций с параметрами:

Во-первых, давайте еще раз посмотрим на оператор вызова:

27.jpeg


28.jpeg


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

Давайте посмотрим на код функции:

29.jpeg


30.jpeg


Все, что делает эта функция, - это принимает строку, целое число и символ и распечатывает их с помощью printf. Как видите, сначала в верхней части функции выделяются 3 переменные, затем эти переменные помещаются в стек как параметры для функции printf. Очень просто.

Циклы
Теперь, когда у нас есть вызов функций, вывод, переменные и математика, давайте перейдем к управлению потоком. Сначала мы начнем с цикла for:

31.jpeg


32.jpeg


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

33.jpeg


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

Теперь давайте посмотрим на цикл while:

34.jpeg


35.jpeg


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

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

Оператор If
Далее мы поговорим о операторах if. Сначала посмотрим на код:

36.jpeg


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

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

37.jpeg


Как видите, сборка структурирована аналогично фактическому коду. Это потому, что операторы IF - это просто «Если X, то Y Else Z». IF мы посмотрим на первый набор стрелок, выходящих из верхней части, мы можем увидеть сравнение между переменной A и шестнадцатеричным 0F, которое представляет десятичное число 15. Если A больше или равно 15, программа распечатает «greater than 15», а затем вернитесь. В противном случае программа сравнит A с шестнадцатеричным 0A, которое представляет десятичное число 10. Этот шаблон будет продолжаться до тех пор, пока программа не напечатает и не вернется.

Оператор Switch
Операторы Switch во многом похожи на операторы IF, за исключением того, что в операторе Switch одна переменная или оператор сравнивается с рядом «вариантов» (или возможных эквивалентов). Давайте посмотрим на наш код:

38.jpeg


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

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

39.jpeg


В отличие от операторов IF, операторы switch не подчиняются правилу «If X Then Y Else Z», вместо этого программа просто сравнивает условный оператор с case и выполняет case только в том случае, если этот case является эквивалентом условного оператора. Давайте сначала взглянем на первые 2 блока:

40.jpeg


Сначала программа генерирует случайное число и устанавливает его в A. Затем программа инициализирует оператор switch, сначала устанавливая для временной переменной (var_D0) значение A, а затем проверяя, что var_D0 соответствует хотя бы одному из возможных случаев. Если var_D0 необходимо установить по умолчанию, программа следует по зеленой стрелке вниз до последней секции возврата (см. Ниже). В противном случае программа инициирует переход к эквивалентному разделу case:

41.jpeg


В случае, если var_D0 (A) равно 5, код перейдет к указанному выше разделу case, распечатает «5» и затем перейдет к разделу возврата.

Пользовательский ввод
В этом разделе мы рассмотрим ввод данных пользователем с помощью функции cin C++. Сначала посмотрим на код:

42.jpeg


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

Давай разберем ассемблер. Во-первых, часть C++ cin:

43.jpeg


Этот код просто инициализирует строковое предложение, затем вызывает функцию cin и устанавливает ввод переменной предложения. Давайте подробнее рассмотрим вызов cin:

44.jpeg


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

45.jpeg


Благодарности
Надеюсь, эта статья дала вам хорошее представление о том, как основные концепции программирования представлены на ассемблере. Следите за следующей частью этой серии, BOLO: Reverse Engineering - Part 2 (Advanced Programming Concepts)!

Код:
lea eax, [ebp+Reading] ; “Reading”
push eax
lea ecx, [ebp+For] ; “For”
push ecx
mov edx, [ebp+Thanks] ; “Thanks”
push edx
push offset _Format ; “%s %s %s”
call j_printf

От ТС
Постараюсь в скором веремени выпустить вторую часть цикла
В статие могут быть ошибки. Пишите - исправлю
За оригиналом сюда

Перевод:
Azrv3l cпециально для xss.pro
 


Напишите ответ...
  • Вставить:
Прикрепить файлы
Верх