Предисловие

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

  1. Массивы
  2. Указатели
  3. Распределение динамической памяти
  4. Socket Programming (сетевое программирование)
  5. Резьба

Для первой части серии BOLO: Reverse Engineering, пожалуйста, нажмите здесь.

Обратите внимание: хотя в этой статье для дизассемблирования скомпилированного кода используется IDA Pro, многие функции IDA Pro (например, построение графиков, перевод псевдокода и т. д.) можно найти в подключаемых модулях и сборках для других бесплатных дизассемблеров, таких как radare2. Кроме того, при подготовке этой статьи я позволил себе изменить имена некоторых переменных в дизассемблированном коде с предустановок IDA, таких как «v20», на то, что они соответствуют в коде C. Это было сделано для облегчения понимания каждой части. Наконец, обратите внимание, что этот код C был скомпилирован в 64-битный исполняемый файл и дизассемблирован с помощью 64-битной версии IDA Pro. Это особенно заметно при вычислении размеров массива, поскольку 32-битные регистры (т.е. eax) часто удваиваются в размере и преобразуются в 64-битные регистры (например, rax).

Хорошо, приступим!

В то время как Часть 1 раскрывает и описывает основные концепции программирования, такие как циклы и операторы IF, эта статья предназначена для объяснения более сложных тем, которые вам придется расшифровать при обратном проектировании.

Массивы

Начнем с массивов. Во-первых, давайте взглянем на код в целом:

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

Как видите, 12 строк кода превратились в довольно большой блок кода. Но не пугайтесь! Помните, все, что мы здесь делаем, - это настраиваем массивы!

Давайте разберем это по крупицам:

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

РЕДАКТИРОВАТЬ: фотография выше с надписью «Объявление массива с буквальным - дизассемблированным» на самом деле помечена неправильно. Хотя да, при инициализации массива целочисленным литералом компилятор сначала инициализирует длину через локальную переменную, приведенный выше снимок экрана на самом деле является инициализацией канарейки стека. Stack Canaries используются для обнаружения атак переполнения, которые, если их не устранить, могут привести к выполнению вредоносного кода. Во время компиляции компилятор выделил достаточно места для единственного элемента litArray, который будет использоваться, litArray [0] (см. Фото ниже с пометкой «локальные переменные - массивы» - как вы можете видеть, единственным выделенным элементом litArray является litArray [0] ]). Оптимизация компилятора может значительно повысить скорость работы приложений.
Извините за недоразумение!

Сначала длина массива сохраняется в локальной переменной (ArraySize), затем вычисляются максимальное и минимальное значения индекса, а также общая длина массива, которые затем используются для вычисления базового местоположения массива в памяти.

При объявлении массива с предопределенными определениями индекса компилятор просто сохраняет каждый предопределенный объект в свою собственную переменную, которая представляет индекс в массиве (т. Е. objArray4 = objArray [4])

Подобно объявлению массива с предопределенными определениями индекса, при инициализации (или установке) индекса в массиве компилятор создает новую переменную для указанного индекса.

При извлечении элементов из массивов элемент берется из индекса в массиве и устанавливается в нужную переменную.

При создании матрицы сначала устанавливаются размеры строки и столбца для их переменных row и col. Затем вычисляются максимальные и минимальные индексы для строк и столбцов, которые используются для вычисления базового местоположения / общего размера матрицы в памяти.

При вводе в матрицу сначала вычисляется местоположение желаемого индекса с использованием базового местоположения матрицы. Затем содержимое указанной позиции индекса устанавливается на желаемый ввод (т. Е. 1337).

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

Указатели

Теперь, когда мы понимаем, как массивы используются / выглядят в сборке, перейдем к указателям.

Давайте теперь разберем сборку:

Сначала мы устанавливаем int num равным 10.

Затем мы устанавливаем содержимое переменной num (то есть 10) на содержимое переменной-указателя.

Распечатываем переменную num.

Распечатываем переменную указатель.

Мы распечатываем адрес переменной num, используя код операции lea (загрузить эффективный адрес) вместо mov.

Мы печатаем адрес переменной num через переменную pointer.

мы печатаем адрес переменной указателя, используя код операции lea (загрузить эффективный адрес) вместо mov.

Распределение динамической памяти

Следующий пункт в нашем списке - динамическое выделение памяти. В этом уроке я разберу выделение памяти, используя:

  1. маллок
  2. каллок
  3. перераспределить

malloc - распределение динамической памяти

Сначала посмотрим на код:

В этой функции мы выделяем 11 символов с помощью malloc, а затем копируем «Hello World» в выделенное пространство памяти.

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

Обратите внимание: в процессе сборки вы можете видеть инструкции «nop». эти инструкции были специально размещены мной на этапе подготовки этой статьи, чтобы я мог легко перемещаться и комментировать код сборки.

При использовании malloc сначала размер выделенной памяти (0x0B) сначала перемещается в регистр edi. Затем вызывается системная функция _malloc для выделения памяти. Выделенная область памяти затем сохраняется в переменной ptr. Затем строка «Hello World» разбивается на «Hello Wo» и «rld», поскольку она копируется в выделенное пространство памяти. . Наконец, распечатывается вновь скопированная строка «Hello World», и выделенная память освобождается с помощью системной функции _ free.

calloc - распределение динамической памяти

Сначала посмотрим на код:

Как и в методе malloc, выделяется пространство для 11 символов, а затем в указанное пространство копируется строка «Hello World». Затем распечатывается вновь перемещенное «Hello World», и выделенное пространство памяти освобождается.

Распределение динамической памяти с помощью calloc выглядит почти идентично распределению динамической памяти с помощью malloc при разбиении на сборку.

Во-первых, с помощью системной функции _calloc выделяется пространство для 11 символов (0x0B). Затем строка «Hello World» разбивается на «Hello Wo» и «rld» по мере ее копирования во вновь выделенную память. площадь. Затем распечатывается вновь перемещенная строка «Hello World», и выделенная область памяти освобождается с помощью системной функции _free.

realloc - распределение динамической памяти

Сначала посмотрим на код:

В этой функции пространство для 11 символов выделяется с помощью malloc. Затем «Hello World» копируется во вновь выделенное пространство памяти до того, как указанная область памяти перераспределяется для размещения 21 символа с помощью realloc. Наконец, «1337 h4x0r @nonymoose» копируется во вновь перераспределенное пространство. Наконец, после печати память освобождается.

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

Обратите внимание: в процессе сборки вы можете видеть инструкции «nop». эти инструкции были специально размещены мной на этапе подготовки этой статьи, чтобы я мог легко перемещаться и комментировать код сборки.

Во-первых, память выделяется с помощью malloc точно так же, как это было в разделе «malloc - динамическое выделение памяти» выше. Затем, после вывода новой перемещенной строки «Hello World», для ptr (которая представляет переменную mem_alloc в коде), а также передается размер 0x15 (21 в десятичной системе). Затем «1337 h4x0r @nonymoose» разбивается на «1337 h4x», «0r @nony», «moos »и« e »при копировании во вновь выделенное пространство памяти. Наконец, пространство освобождается с помощью системного вызова _free.

Программирование сокетов

Далее мы рассмотрим программирование сокетов, разбив основную систему чата клиент-сервер TCP.

Прежде чем мы начнем разбирать код сервера / клиента, важно указать на следующую строку кода в верхней части файла:

Эта строка определяет переменную PORT как 1337. Эта переменная будет использоваться как на клиенте, так и на сервере в качестве сетевого порта, используемого для создания соединения.

Сервер

Сначала посмотрим на код:

Сначала создается дескриптор файла сокета server с доменом AF_INET, типом SOCK_STREAM и кодом протокола 0 . Далее настраиваются параметры сокета и адрес. Затем сокет привязывается к сетевому адресу / порту, и сервер начинает прослушивать указанный сервер с максимальной длиной очереди 3. Получив соединение, сервер принимает его в переменную sock и считывает переданное значение в переменную value. Наконец, сервер отправляет строку serverhello по соединению перед возвратом функции.

Теперь давайте разберем его на сборку:

Сначала создаются и инициализируются серверные переменные.

Затем создается дескриптор файла сокета «server» путем вызова системной функции _socket с параметрами протокола, типа и домена, передаваемыми через edx , esi и edi соответственно.

Затем вызывается setsockopt для установки параметров сокета в дескрипторе файла сокета «server».

Затем адрес сервера инициализируется через adress.sin_family, address.sin_addr.s_addr и address.sin_port..

После настройки адреса и сокета сервер привязывается к сетевому адресу с помощью системного вызова _bind.

После привязки сервер прослушивает сокет, передавая дескриптор файла сокета «server» и максимальную длину очереди 3.

Как только соединение установлено, сервер принимает соединение сокета в переменную sock.

Затем сервер считывает переданное сообщение в переменную value с помощью системного вызова _read.

Наконец, сервер отправляет сообщение serverhello через переменную s (которая представляет в коде serverhello).

Клиент

Сначала посмотрим на код:

сначала создается дескриптор файла сокета sock с доменом AF_INET, типом SOCK_STREAM и кодом протокола 0 . Затем memset используется для заполнения области памяти server_addr с помощью '0 до того, как информация об адресе будет установлена ​​с помощью server_addr.sin_family и server_addr.sin_port. Затем информация об адресе преобразуется из текстового в двоичный формат с помощью inet_pton до того, как клиент подключится к серверу. После подключения клиент отправляет строку helloclient, а затем считывает ответ сервера в переменную value. Наконец, печатается переменная value, и функция возвращается.

Теперь давайте разберем сборку:

Сначала инициализируются локальные переменные клиента.

Дескриптор файла сокета 'sock' создается путем вызова системной функции _socket и передачи информации о протоколе, типе и домене через edx , esi и edi соответственно.

Затем переменная server_address (представленная как 's' в сборке) заполняется значениями '0 (0x30) с использованием _memset системный вызов.

Затем настраивается адресная информация для сервера.

Затем адрес преобразуется из текстового в двоичный формат с помощью системного вызова _inet_pton. Обратите внимание, что, поскольку адрес не был явно определен в коде, предполагается localhost (127.0.0.1).

Клиент подключается к серверу с помощью системного вызова _connect.

После подключения клиент отправляет на сервер строку helloClient.

Наконец, клиент считывает ответ сервера в переменную value с помощью системного вызова _read.

Резьба

Наконец, мы рассмотрим основы работы с потоками в C.

Сначала посмотрим на код:

Как видите, программа сначала печатает «Это перед потоком», а затем создает новый поток, который указывает на функцию * MyHread, используя pthread_create функция. По завершении функции * Myhread (после сна в течение 1 секунды и печати «Hello from Myhread») новый поток снова присоединяется к основному потоку с помощью pthread_join и напечатано «Это после потока».

Теперь давайте разберем сборку:

Сначала программа печатает «Это перед потоком».

Затем создается новый поток с помощью системного вызова _pthread_create. Эта цепочка указывает на migread как на процедуру запуска.

Как видите, функция migread просто засыпает на одну секунду перед тем, как напечатать «Hello from Myhread».

Обратите внимание: в функции Myhread вы увидите два «нет». Они были специально размещены для облегчения навигации на этапе подготовки этой статьи.

После возврата из функции MyHread новый поток соединяется с основным потоком с помощью функции _pthread_join.

Наконец, «Это после цепочки» распечатывается, и функция возвращается.

Заключительные заявления

Я надеюсь, что эта статья смогла пролить свет на некоторые более сложные концепции программирования и лежащий в их основе ассемблерный код. Теперь, когда мы рассмотрели все основные концепции программирования, следующие несколько статей из серии BOLO: Reverse Engineering будут посвящены различным типам атак и уязвимому коду, чтобы вы могли быстрее выявлять уязвимости и атаки в программах с закрытым исходным кодом с помощью статического анализа.