Альтернативные методы трассировки приложений — Хакер

Что дальше ?

Это очень упрощенная программа, предназначенная для ознакомления с основами трассировки лучей. Есть много способов улучшить ее и реализовать другие функции. Например:

  • Можно создать классы и выяснить, что является специфическим для сфер, а что нет, определить базовый класс и реализовать другие объекты, такие как плоскости или треугольники;

  • То же самое и со светом. Добавить сюда POO и сделать так, чтобы можно было добавить несколько источников света в сцену;

  • Отделить свойства материала от геометрических свойств, чтобы иметь возможность применять один материал (например, синий матовый) к любому типу объектов;

  • Найти способ правильно расположить экран при любом положении и направлении камеры;

  • Смоделировать свет по-другому. В настоящее время это одна точка, поэтому тени от объектов «жесткие» или четко очерченные. Чтобы получить «мягкие» тени , нужно смоделировать источник света как 2D- или 3D-объект: диск или сферу.

Dbifuzz-фреймворк

Все рассмотренные выше подходы к трассировке я воплотил в DbiFuzz-фреймворке, который демонстрирует, как можно отслеживать работу исполняемого файла альтернативными методами. Как мы уже отмечали, некоторые из известных методов используют инструментацию, которая дает быстрое решение, но при этом предполагает серьезное вмешательство в целевой процесс и не сохраняет целостности бинарного файла.

В отличие от них, DbiFuzz оставляет бинарный файл практически нетронутым, изменяя только PTE, BTF и вставляя флаг TRAP. Другая сторона этого подхода состоит в том, что при интересующем событии включается прерывание: переход ring 3 —ring 0 — ring 3. Так как DbiFuzz подразумевает прямолинейное вмешательство в контекст и поток управления целевого процессора, то его можно использовать для написания собственных инструментов (даже на Python) для доступа к целевому бинарному файлу и его ресурсам.

Show time

Для многих задач, решаемых с помощью трассировки, может оказаться полезной динамическая бинарная инструментация. Что касается DbiFuzz-фреймворка, то его можно использовать в следующих случаях:

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

Нет никаких проблем в запуске DbiFuzz на лету, просто установи ловушку или INT3-перехватчик. Поскольку мы не трогаем бинарный код целевого файла, то не будет никаких проблем с проверкой целостности, а флаг TRAP может быть заменен на MTF. Отслеживание ценных данных тоже не представляет никаких проблем, нужно просто установить соответствующий PTE — и твой монитор готов! Инструменты Python/Ruby/…? Просто создай нужные привязки (bindings) — и вперед!

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

Полезные ссылки

Блоги:

Intel:

Относительно vad:

Виртуализация:

Модули python (дизассемблеры):

To be continued

Как видишь, динамическая бинарная инструментация — не единственный метод трассировки. Альтернатив ей достаточно много, и большинство из них представлены в DbiFuzz-фреймворке. Уже сейчас некоторые возможности этого проекта могут помочь с работой в кодом на уровне ядра, а в дальнейшем я планирую перевести в это пространство весь трейсер. Кстати, уже сейчас ты можешь использовать исходники фреймворка, улучшать концепцию и экспериментировать с новыми идеями…

Алгоритм

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

Рисунок 9
Рисунок 9

Алгоритм трассировки лучей

Трассировка лучей — это метод рендеринга, который имитирует путь света и пересечения с объектами и позволяет создавать изображения с высокой степенью реализма. Более оптимизированные варианты этого алгоритма используются в видеоиграх.

Чтобы объяснить работу этого алгоритма, сначала нужно настроить сцену:

Рисунок 2
Рисунок 2

Экран — это некая определенная вами геометрическая фигура (например, прямоугольник 3×2). Но сами по себе цифры 3 и 2 ни о чем нам не говорят и действительно начинают приобретать какое-то значение только при сравнении их с размерами других объектов.

Напишем алгоритм трассировки лучей с учетом заданной сцены:

для каждого пикселя p(x, y, z) экрана:

   ассоциировать черный цвет с p

   если луч (линия), начинающийся от камеры и идущий к точке p, пересекает любой объект сцены, то:

       вычислить точку пересечения с ближайшим объектом

       если между точкой пересечения и источником света нет объекта, тогда:

           рассчитать цвет точки пересечения

           сопоставить цвет точки пересечения с p

Рисунок 3
Рисунок 3

Обратите внимание, что этот процесс на деле оказывается обратным реальному освещению. Ведь реальный свет выходит из источника во всех направлениях, отражается от объектов и попадает в камеру. Однако, поскольку не все лучи, выходящие из источника света, попадут в камеру, трассировка лучей выполняет обратный процесс для экономии времени вычислений (отслеживает лучи от камеры обратно к источнику света).

Ближайший пересекаемый объект

Пока мы все еще не выполнили инструкцию из псевдокода: если луч (линия), начинающийся от камеры и идущий к точке p, пересекает любой объект сцены, то […]. Теперь нам нужно вычислить точку пересечения с ближайшим объектом.

Напишем функцию, которая использует sphere_intersect() для поиска ближайшего объекта, с которым пересекается луч, если он существует. Просто перебираем все сферы, ищем пересечения и сохраняем ближайшую сферу:

Посмотреть код

При вызове функции, если nearest_object = None, луч не пересекает никакого объекта, иначе его значением является ближайший пересекаемый объект, и мы получаем min_distance — расстояние от начала луча до точки пересечения.

Бонус

Ниже приведена анимация трассировки лучей. По сути это просто несколько раз отрендеренная сцена с камерой в разных положениях:

Код написан на Kotlin (можно оценить, насколько медленный по сравнению с ним Python) и доступен на GitHub.

Взаимодействие с ос

Мониторинг взаимодействия с ОС позволяет отфильтровывать попытки доступа к реестру, контролировать изменения файлов, отслеживать взаимодействие процесса с различными системными ресурсами, а также вызовы определенных API-функций. Как правило, это реализуется через перехват API-функций, путем вставки «трамплинов», inline-хуков, модификацию таблицы импорта, установку брейкпоинтов.

Рис. 2. Нумерация идентификаторов (ID) SYSCALL в Windows 8
Рис. 2. Нумерация идентификаторов (ID) SYSCALL в Windows 8

Механизм SYSCALL представляет собой быстрый способ переключить CPL (Current Privilege Level) из режима пользователя в режим супервайзера, таким образом, приложение режима пользователя может вносить изменения в ОС (рис. 4).

Рис. 4. Обработка операций SYSCALL (по учебнику Intel)
Рис. 4. Обработка операций SYSCALL (по учебнику Intel)

Запускаем код

Увеличим ширину и высоту для получения более высокого разрешения (ценой увеличения времени вычислений).

Рисунок 8
Рисунок 8

Можно заметить две вещи, которые отличают результат от первого изображения, показанного в начале:

Зачем отслеживать?

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

:/>  Пошаговое руководство по использованию диспетчера настройки безопасности. Окончание. » Статьи о Microsoft Windows. Установка, администрирование, ускорение и оптимизация Microsoft Windows 7, Windows 8, Windows XP, и Windows 10

Копаем глубже — идем в vmm!

В предыдущем разделе я предложил некоторые «грязные» методы для режима ядра. Вообще, установка хуков — это неправильный способ, и мне он не нравится, точно так же, как не нравится он и ребятам из Microsoft. Для борьбы с такими методами мелкомягкие и разработали PatchGuard.

Минусы:

  • Виртуализировано не отдельное приложение, а вся система — на уровне ядра ЦП.
  • Оператор switch( VMMExit ) отбирает немного производительности, равно как и код гипервизора, выполняющийся для каждого из вариантов switch’а.

Модель отражения блинна-фонга

Итак, мы знаем, что луч света попал на объект, а отражение луча — прямо в камеру. Вопрос: что увидит камера? На него и пытается ответить модель Блинна-Фонга.

Модель Блинна-Фонга — это приближение к модели Фонга, требующее меньших вычислительных затрат.

Согласно этой модели, любой материал имеет четыре свойства:

Примечание: Все цвета представлены в RGB в диапазоне 0 – 1.

Рисунок 7
Рисунок 7

Добавим эти свойства к сферам:

objects = [
   { 'center': np.array([-0.2, 0, -1]), 'radius': 0.7, 'ambient': np.array([0.1, 0, 0]), 'diffuse': np.array([0.7, 0, 0]), 'specular': np.array([1, 1, 1]), 'shininess': 100 },
   { 'center': np.array([0.1, -0.3, 0]), 'radius': 0.1, 'ambient': np.array([0.1, 0, 0.1]), 'diffuse': np.array([0.7, 0, 0.7]), 'specular': np.array([1, 1, 1]), 'shininess': 100 },
   { 'center': np.array([-0.3, 0, 0]), 'radius': 0.15, 'ambient': np.array([0, 0.1, 0]), 'diffuse': np.array([0, 0.6, 0]), 'specular': np.array([1, 1, 1]), 'shininess': 100 }
]

В нашем примере сферы имеют цвета красный, пурпурный и зеленый, соответственно.

Модель Блинн-Фонга утверждает, что свет также имеет три цветовых свойства: фоновый цвет, рассеянный и зеркальный. Их тоже добавим к модели:

light = { 'position': np.array([5, 5, 5]), 'ambient': np.array([1, 1, 1]), 'diffuse': np.array([1, 1, 1]), 'specular': np.array([1, 1, 1]) }

Учитывая эти свойства, модель Блинна-Фонга рассчитывает освещенность точки следующим образом:

где

Посмотреть код

Обратите внимание, что в конце мы определяем цвет между 0 и 1, чтобы убедиться, что он находится в правильном диапазоне.

Настройка сцены

Перед тем, как начать писать код, нам нужно настроить сцену. В первую очередь определимся, где расположены камера и экран. Для этой цели примем некоторые упрощения, совместив объекты с координатными осями.

Рисунок 4
Рисунок 4

Допустим, камера расположена в точке (x = 0, y = 0, z = 1), а экран является частью плоскости, образованной осями x и y. Теперь мы можем написать скелет нашего кода.

Посмотреть код
import numpy as np
import matplotlib.pyplot as plt
width = 300
height = 200
camera = np.array([0, 0, 1])
ratio = float(width) / height
screen = (-1, 1 / ratio, 1, -1 / ratio) # слева, сверху, справа, снизу
image = np.zeros((height, width, 3))
for i, y in enumerate(np.linspace(screen[1], screen[3], height)):
    for j, x in enumerate(np.linspace(screen[0], screen[2], width)):
        # image[i, j] = ...
        print("progress: %d/%d" % (i   1, height))
plt.imsave('image.png', image)
  • Камера — это просто точка, имеющая три координаты;

  • С другой стороны, экран определяется четырьмя числами (или двумя точками): слева, сверху, справа, снизу. Он находится в диапазоне от -1 до 1 в направлении x и от -1/ratio до 1/ratio в направлении y, где ratio — ширина изображения, деленная на его высоту. Это вытекает из того, что мы хотим, чтобы экран имел такое же соотношение сторон, что и фактическое изображение. При такой настройке экрана будет получено соотношение сторон (ширина к высоте): 2 /(2/ratio) = ratio, которое и является соотношением сторон желаемого изображения 300×200;

  • Цикл состоит из разделения экрана на точки в направлениях x и y, затем вычисляется цвет текущего пикселя;

  • Полученный код создаст — как и ожидалось на данном этапе — черное изображение. 

Окончательный код

Итоговый код состоит из всего порядка сотни строк:

Определение луча

Когда мы говорим «луч», на самом деле мы имеем в виду скорее «линию». Всякий раз при работе с геометрией лучше отдать предпочтение векторам, чем реальным линейным уравнениям: с ними легче работать, и они гораздо менее подвержены ошибкам, таким как деление на ноль.

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

Помните, что камера и пиксель — это 3D-точки. При t = 0 вы окажетесь в положении камеры, но с увеличением t будете удаляться от нее в направлении пикселя. Это параметрическое уравнение, которое даст точку вдоль линии для любого t.

Конечно, точно так же мы можем переписать уравнение и определить луч, который начинается в исходной точке (O) и идет к месту назначения (D) как:

Для удобства определим d как вектор направления.

Теперь мы можем добавить к нашему коду вычисление луча:

Посмотреть код
import numpy as np
import matplotlib.pyplot as plt
def normalize(vector):
    return vector / np.linalg.norm(vector)
width = 300
height = 200
camera = np.array([0, 0, 1])
ratio = float(width) / height
screen = (-1, 1 / ratio, 1, -1 / ratio) # слева, сверху, справа, снизу
image = np.zeros((height, width, 3))
for i, y in enumerate(np.linspace(screen[1], screen[3], height)):
    for j, x in enumerate(np.linspace(screen[0], screen[2], width)):
        pixel = np.array([x, y, 0])
        origin = camera
        direction = normalize(pixel - origin)
        # image[i, j] = ...
    print("progress: %d/%d" % (i   1, height))
plt.imsave('image.png', image)

Теперь перейдем ко второй части, где луч пересекает объект сцены. Для простоты будем использовать только сферы.

Определение сферы

Сфера — довольно простой математический объект. Она определяется как набор точек, находящихся на одинаковом расстоянии r (радиус) от заданной точки (центра).

Следовательно, с учетом центра C сферы и ее радиуса r произвольная точка X лежит на сфере тогда, когда:

Для удобства возведем обе стороны в квадрат, чтобы избавиться от квадратного корня, обусловленного величиной X — C:

После этого мы сможем определить некоторые сферы сразу после объявления экрана:

objects = [
   { 'center': np.array([-0.2, 0, -1]), 'radius': 0.7 },
   { 'center': np.array([0.1, -0.3, 0]), 'radius': 0.1 },
   { 'center': np.array([-0.3, 0, 0]), 'radius': 0.15 }
]

Теперь вычислим пересечение луча и сферы.

Отражение

Сейчас мы рендерим лучи, которые выходят из источника света, ударяются о поверхность объекта и отражаются в камеру. Но что, если луч попадет в несколько объектов, прежде чем попасть в камеру? Появится отражение.

Каждый объект имеет коэффициент отражения в диапазоне от 0 до 1. Здесь 0 означает, что объект матовый, 1 — что объект зеркальный. Добавим свойство отражения ко всем сферам:

{ 'center': np.array([-0.2, 0, -1]), ..., 'reflection': 0.5 }
{ 'center': np.array([0.1, -0.3, 0]), ..., 'reflection': 0.5 }
{ 'center': np.array([-0.3, 0, 0]), ..., 'reflection': 0.5 }
{ 'center': np.array([0, -9000, 0]), ..., 'reflection': 0.5 }

Отраженный луч

Прежде чем мы сможем все это записать в виде кода, нам нужно определить направление отраженного луча. Можно вычислить его следующим образом:

где

Рисунок 10
Рисунок 10

Добавим этот метод в начало кода вместе с функцией normalize():

def reflected(vector, axis):
   return vector — 2 * np.dot(vector, axis) * axis
Посмотреть код
:/>  Logging — библиотека для удобного ведения логов в Python / Хабр

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

Вот и все. Запускаем код и наблюдаем результат:

Рисунок 11
Рисунок 11

Пересечение лучей

Следующий шаг алгоритма: если луч (линия), начинающийся от камеры и проходящий к точке p, пересекает объект сцены, тогда…

Разобьем его на две части. И начнем с определения того, какой луч (линия) начинается от камеры и идет к точке p?

Пересечение со сферой

Мы знаем уравнение лучей и знаем, какому условию должна удовлетворять точка, чтобы она лежала на сфере. Все, что нам нужно сделать, это подставить одно уравнение в другое и решить его относительно t. То есть, найти ответ на вопрос: для какого t точка луча ray(t) окажется на сфере?

Это обычное квадратное уравнение, которое просто решается относительно t. Мы будем вызывать коэффициенты, связанные с t², t¹, t⁰, a, b и c, соответственно. Вычислим дискриминант этого уравнения:

Поскольку направление d является единичным вектором, получим a = 1. После вычисления дискриминанта у нас есть три варианта:

Рисунок 5
Рисунок 5

Для обнаружения пересечений будем использовать только третий случай. Запишем функцию, отвечающую за обнаружение пересечения луча и сферы. Она возвращает расстояние от начала луча до ближайшей точки пересечения, если луч действительно пересекает сферу, иначе возвращает None:

Посмотреть код

Обратите внимание, что мы возвращаем только ближайшее пересечение из двух тогда, когда оба t1 и t2 положительны. Это связано с тем, что ответ уравнения может быть отрицательным, и в таком случае луч, пересекающий сферу, будет иметь не d в качестве вектора направления, а -d (например, если сфера находится за камерой и экраном).

Пересечения света

Итак, мы нашли прямую линию, идущую от камеры к объекту, и знаем, что это за объект, а также на какую часть объекта мы смотрим. Но мы пока не знаем, освещена ли вообще эта конкретная точка. Возможно, свет не попадает конкретно на нее, и нет необходимости производить расчеты дальше, потому что мы ее не видим.

У нас уже есть функция, которая нам может помочь: near_intersected_object(). И мы хотим знать, пересекает ли луч, который начинается в точке пересечения и идет к свету, объект сцены перед тем, как пересечь свет.

light = { 'position': np.array([5, 5, 5]) }

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

# ...
intersection = origin   min_distance * direction
intersection_to_light = normalize(light['position'] — intersection)
_, min_distance = nearest_intersected_object(objects, intersection, intersection_to_light)
intersection_to_light_distance = np.linalg.norm(light['position'] — intersection)
is_shadowed = min_distance < intersection_to_light_distance

Что ж, это так не сработает. Нужно сделать небольшую корректировку. Если мы используем точку пересечения в качестве источника нового луча, мы можем в конечном итоге обнаружить сферу, на которой мы сейчас находимся, как объект между точкой пересечения и источником света.

Рисунок 6
Рисунок 6

Этот трюк используется не только для сфер, но и для любых объектов.

Следовательно, правильный код будет таким:

Посмотреть код

Плюсы:

  • Более высокий уровень прав, чем уровень супервайзера, а также целый набор callback’ов, предоставляемый технологией виртуализации.

При этом сам VMM (Virtual Machine Monitor) может быть минималистичным (микроVMM) и реализовывать только необходимую обработку, занимая при этом минимальный объем кода (пример).

Рис. 6. Некоторые callback’и, предоставляемые Intel VTx
Рис. 6. Некоторые callback’и, предоставляемые Intel VTx

Помимо всего, в данном случае вместо того, чтобы ставить хуки на IDT, можно все обрабатывать напрямую с помощью дебаг-исключения в VMM. То же самое относится и к перехвату ошибок страниц с помощью исключения PageFault в VMM или через реализацию EPT (Extended Page Table).

Рис. 7. Включаем вывод VMX для ловушек и сбоев
Рис. 7. Включаем вывод VMX для ловушек и сбоев

Погружаемся в ядро

Для выполнения упомянутых функций необходимо опуститься на уровень ядра (ring 0). Однако в режиме супервайзера уже появляется доступ к некоторым функциям, предоставляемым самой операционной системой: LoadNotify, ThreadNotify,ProcessNotify.

Вторая группа функций включает в себя дампер памяти, использующий MDL (memory descriptor list — список дескрипторов памяти), монитор памяти процессов, основанный на VAD (Virtual Address Descriptor), монитор взаимодействия с системой, который задействуетnt!KiSystemCall64, перехват доступа к памяти и ловушкам через IDT (Interrupt Descriptor Table).

Монитор памяти использует для своей работы VAD-дерево, которое представляет собой AVL-дерево, используемое для хранения информации об адресном пространстве процесса. Оно же используется, когда необходимо инициализировать PTE (Page Table Entry) для конкретной страницы памяти.

Рис. 3. Пример VAD-дерева
Рис. 3. Пример VAD-дерева

Как я предложил выше, отслеживание доступа к памяти может осуществляться через механизм защиты памяти (такая вот тавтология), но его реализация в режиме пользователя с помощью API-функций может слишком сильно отразиться на производительности. Однако если принять во внимание, что защита памяти основана на механизме MMU — пейджинге, то есть более простой способ: изменять таблицу страниц в режиме ядра, после чего нарушение режима доступа к памяти будет обрабатываться через генерацию процессором исключения PageFault, а управление будет передаваться на обработчик IDT[PageFault]. Установка перехватчика на обработчик PageFault позволит быстро получить сигнал о запросе на доступ к выбранным страницам.

Все потому, что процесс может использовать только страницы памяти, помеченные как Valid (то есть выгруженные в память), в противном же случае будет возникать исключение PageFault, которое и будет перехватываться. Это означает, что если мы намеренно поставили Valid-флаг выбранной страницы памяти в значение invalid(0), то каждая попытка доступа к этой странице будет вызывать обработчик PageFault, что позволяет легко отфильтровать и обработать соответствующий запрос (вызывая callback к трейсеру и выставляя Valid-флаг для конкретного PTE).

Рис. 5. Флаги PTE
Рис. 5. Флаги PTE

Подводные камни vmm

Можно отметить некоторые основные особенности описанного подхода:

  • целевой файл остается практически неизмененным:
    • для отслеживания (как пошагового, так и на уровне ветвлений) внедряется флаг TRAP;
    • адресные брейкпоинты через 0xCC или использование DRx;
    • мониторинг памяти путем изменения таблицы страниц процесса;
    • не нужно патчить бинарный файл;
  • можно использовать как трассировочный модуль из другого приложения;
  • можно отслеживать несколько приложений одновременно;
  • можно отслеживать несколько потоков одного приложения;
  • реализованы быстрые вызовы для переключения CPL.

Выделение трейсера из пространства целевого процесса в другой процесс дает несколько преимуществ: можно использовать его как отдельный модуль, можно сделать биндинги для Python, Ruby и других языков. Однако у этого решения есть и недостаток — очень большой удар по производительности (взаимодействие между процессами: чтение из памяти другого процесса, событийный механизм ожидания).

Для ускорения трассировки необходимо перенести логику в адресное пространство целевого процесса, чтобы можно было быстро получать доступ к его ресурсам (памяти, стеку, содержимому регистров), а также опционально отказаться от VMM из-за негативного влияния обработки VMMExit на производительность и вернуться обратно к установке хуков для ловушек и обработчиков PageFault.

:/>  Руководство по Windows PowerShell для начинающих

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

Поток данных

В этом сценарии трассировка применяется для распаковки кода, а также для наблюдения за обработкой ценной информации — во время его можно обнаружить неправильное использование объектов, переполнения и прочие ошибки. Кроме того, оно также может использоваться для сохранения и восстановления контекста в процессе трассировки.

Обычно это делается так: исследуемая библиотека полностью дизассемблируется, после этого в ней локализуются все инструкции чтения/записи, а затем в процессе выполнения кода происходит их парсинг и определяется адрес назначения. Есть и другой вариант — с помощью соответствующей API-функции устанавливается защита виртуальной памяти, после чего отслеживаются все нарушения доступа к ней. Реже используется метод, когда в памяти изменяется таблица страниц.

Рис. 1. Трансляция виртуальных адресов в физические
Рис. 1. Трансляция виртуальных адресов в физические

Поток управления

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

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

Другой метод состоит в том, чтобы просто приаттачить отладчик к исследуемой программе и обрабатывать ловушки и точки останова. Однако есть еще один не очень распространенный способ — задействовать функции центрального процессора. Одна из интересных возможностей процессоров Intel — флаг MSR-BTF, который позволяет отслеживать выполнение программы на уровне базовых блоков — на ветвлениях (бранчах). Вот что говорится по поводу данного флага в документации:

«Когда ПО устанавливает флаг BTF в MSR-регистре MSR_DEBUGCTLA и устанавливает флаг TF в регистре EFLAGS, процессор будет генерировать отладочное прерывание только после встречи с ветвлением или исключением.»

Предпосылки

Нам понадобится только самая базовая векторная геометрия:

  • Пусть у нас есть две точки A и B — независимо от размерности: 1, 2, 3,…, n, — тогда вектор, идущий от A к B, может быть найден путем вычисления B — A (поэлементно);

  • Длину вектора — независимо от его размерности — можно найти, вычислив квадратный корень из суммы возведенных в квадрат компонентов. Длина вектора v обозначается ||v||;

  • Единичный вектор — это вектор длины 1: ||v|| = 1;

  • Для данного вектора другой вектор, идущий в том же направлении, но имеющий длину 1, может быть найден путем деления каждого компонента первого вектора на его длину — это называется нормализацией: u = v/||v||;

  • Скалярное произведение для векторов вычисляется как: <v, v> = ||v||².

Расчет цвета

Чтобы получить цвет пикселя, нужно просуммировать вклад каждой точки пересечения луча:

где

Когда прекратить вычисление этой суммы (и отслеживание отраженных лучей, соответственно), решаете вы сами.

Точка пересечения

Чтобы вычислить точку пересечения, используем предыдущую функцию:

nearest_object, distance = nearest_intersected_object(objects, o, d)
if nearest_object:
   intersection_point = o   d * distance

В результате получаем следующий код:

Посмотреть код
import numpy as np
import matplotlib.pyplot as plt
def normalize(vector):
   return vector / np.linalg.norm(vector)
def sphere_intersect(center, radius, ray_origin, ray_direction):
   b = 2 * np.dot(ray_direction, ray_origin — center)
   c = np.linalg.norm(ray_origin — center) ** 2 — radius ** 2
   delta = b ** 2 — 4 * c
   if delta > 0:
       t1 = (-b   np.sqrt(delta)) / 2
       t2 = (-b — np.sqrt(delta)) / 2
       if t1 > 0 and t2 > 0:
           return min(t1, t2)
   return None
def nearest_intersected_object(objects, ray_origin, ray_direction):
   distances = [sphere_intersect(obj['center'], obj['radius'], ray_origin, ray_direction) for obj in objects]
   nearest_object = None
   min_distance = np.inf
   for index, distance in enumerate(distances):
       if distance and distance < min_distance:
           min_distance = distance
           nearest_object = objects[index]
   return nearest_object, min_distance
width = 300
height = 200
camera = np.array([0, 0, 1])
ratio = float(width) / height
screen = (-1, 1 / ratio, 1, -1 / ratio) # слева, сверху, справа, снизу
objects = [
   { 'center': np.array([-0.2, 0, -1]), 'radius': 0.7 },
   { 'center': np.array([0.1, -0.3, 0]), 'radius': 0.1 },
   { 'center': np.array([-0.3, 0, 0]), 'radius': 0.15 }
]
image = np.zeros((height, width, 3))
for i, y in enumerate(np.linspace(screen[1], screen[3], height)):
   for j, x in enumerate(np.linspace(screen[0], screen[2], width)):
       pixel = np.array([x, y, 0])
       origin = camera
       direction = normalize(pixel — origin)
       # проверка пересечений
       nearest_object, min_distance = nearest_intersected_object(objects, origin, direction)
       if nearest_object is None:
           continue
       # вычисления пересечений между лучом и ближайшим объектом
       intersection = origin   min_distance * direction
       # image[i, j] = ...
       print("%d/%d" % (i   1, height))
plt.imsave('image.png', image)

Трейсер для ядра

Что касается трассировщика для ядра, то здесь действуют все те же принципы:

  • отслеживание через ловушки (TRAP);
  • мониторинг памяти через изменение таблицы страниц;
  • callback’и трейсера передаются в приложения уровня пользователя;
  • не нужно патчить бинарные файлы целевого приложения.

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

С другой стороны, за все эти возможности тоже приходится расплачиваться:

  • адресное пространство драйвера принадлежит не ему;
  • фаззинг в памяти — не такое уж простое дело;
  • неверное значение RIP, регистров, памяти… манипулирование ими может очень плохо закончиться;
  • необходимо четко представлять себе, что именно ты отслеживаешь или проверяешь;
  • необходимо в течение всего процесса трассировки помнить о многочисленных IRQL;
  • обработка исключений.

Отделение от целевого процесса, а также инкапсуляция в модуль дают нам высокую масштабируемость и возможность совместной работы с другими модулями для создания более сложного инструмента. Таким образом, в случае реализации трейсера, например, на Python, можно будет использовать IDA Python, привязки LLVM, Dbghelp для отладочных символов, дизассемблеры (движки capstone и bea) и многое другое.

Фейковая плоскость

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

Добавим эту сферу в список объектов и снова выполним рендеринг:

{ 'center': np.array([0, -9000, 0]), 'radius': 9000 — 0.7, 'ambient': np.array([0.1, 0.1, 0.1]), 'diffuse': np.array([0.6, 0.6, 0.6]), 'specular': np.array([1, 1, 1]), 'shininess': 100 }

Оставьте комментарий

Adblock
detector