Вспоминаем , который является языком программирования низкого уровня, напрямую использующий регистры и память внутри собственного исполняемого файла. В собранном виде ассемблерный код хранится в виде двоичных данных и для каждого процессора есть своё руководство, в котором описано, как каждая инструкция закодирована в байты данных.
Дизассемблерએ — это процесс, обратный сборке, байты данных анализируются и преобразуются в инструкции сборки (которые более удобочитаемы для пользователей).
Различные архитектуры процессоров могут иметь разные наборы инструкций, и один процессор может выполнять инструкции ассемблера только в своем собственном наборе инструкций для запуска кода, предназначенного для разных архитектур, нам нужно использовать эмулятор, который представляет собой программу, которая переводит код для неподдерживаемой архитектуры в код, который может выполняться в хост-системе.
Есть много сценариев, в которых может быть полезна сборка, дизассемблирование или эмуляция кода для разных архитектур, один из основных интересов — обучение (большинство университетов преподают сборку MIPS) для запуска и тестирования программ, написанных для различных устройств, таких как маршрутизаторы (фаззинг и т. д.), а также для обратного проектирования.
В этом руководстве мы соберем, дизассемблируем и эмулируем ассемблерный код, написанный для ARM с использованием движка , и , которые представляют собой фреймворки, удобные привязки Python для управления кодом сборки, они поддерживают различные архитектуры (x86એ, ARM (архитектура)એ, MIPS (архитектура)એ, SPARCએ и другие) и имеют встроенную поддержку основных операционных систем (включая Linuxએ, Windowsએ и MacOSએ. ).
Сначала установим эти три фреймворка:
pip3 install keystone-engine capstone unicorn
В качестве примера этого урока используем функцию вычисления факториала, реализованную в архитектуре ARM, соберем код и имитируем его.
Мы также дизассемблируем функцию x86 (чтобы показать, как легко можно обрабатывать несколько архитектур).
Ассемблер для ARM
Начнем с импорта того, что нам понадобится для сборки ARM:
# Нам нужно эмулировать ARM from unicorn import Uc, UC_ARCH_ARM, UC_MODE_ARM, UcError # для доступа к регистрам R0 и R1 from unicorn.arm_const import UC_ARM_REG_R0, UC_ARM_REG_R1 # Нам нужно собрать код ARM from keystone import Ks, KS_ARCH_ARM, KS_MODE_ARM, KsError
Напишем наш ассемблерный код ARM, который вычисляет factorial(r0), где r0 — входной регистр:
ARM_CODE = """
// n равно r0, мы передадим его из python, ans - это r1
mov r1, 1 // ans = 1
loop:
cmp r0, 0 // while n >= 0:
mulgt r1, r1, r0 // ans *= n
subgt r0, r0, 1 // n = n - 1
bgt loop //
// ответ в r1
"""
Соберем вышеуказанный ассемблерный код (преобразуем его в байт-код):
print("Assembling the ARM code")
try:
# инициализируем объект keystone с архитектурой ARM
ks = Ks(KS_ARCH_ARM, KS_MODE_ARM)
# Собираем код ARM
ARM_BYTECODE, _ = ks.asm(ARM_CODE)
# преобразовываем массив целых чисел в байты
ARM_BYTECODE = bytes(ARM_BYTECODE)
print(f"Code successfully assembled (length = {len(ARM_BYTECODE)})")
print("ARM bytecode:", ARM_BYTECODE)
except KsError as e:
print("Keystone Error: %s" % e)
exit(1)
Функция Ks возвращает ассемблер в режиме ARM, метод asm() собирает код и возвращает байты и количество собранных инструкций.
Теперь байт-код можно записать в область памяти и выполнить с помощью процессора ARM (или, в нашем случае, эмулировать):
# адрес памяти, с которого начинается эмуляция
ADDRESS = 0x1000000
print("Emulating the ARM code")
try:
# Инициализировать эмулятор в режиме ARM
mu = Uc(UC_ARCH_ARM, UC_MODE_ARM)
# отведём 2 МБ памяти для этой эмуляции
mu.mem_map(ADDRESS, 2 * 1024 * 1024)
# записываем машинный код для эмуляции в память
mu.mem_write(ADDRESS, ARM_BYTECODE)
# Устанавливаем в коде регистр r0, вычисляем factorial(5)
mu.reg_write(UC_ARM_REG_R0, 5)
# эмулировать код за бесконечное время и неограниченное количество инструкций
mu.emu_start(ADDRESS, ADDRESS + len(ARM_BYTECODE))
# теперь распечатать регистр R0
print("Эмуляция сделана. Посмотрите")
# получаем результат из регистра R1
r1 = mu.reg_read(UC_ARM_REG_R1)
print(">> R1 = %u" % r1)
except UcError as e:
print("Unicorn Error: %s" % e)
В приведенном выше коде инициализируем эмулятор в режиме ARM, ограничиваем 2 МБ памяти по указанному адресу (2 * 1024 * 1024 байта), записываем результат нашей сборки в отображаемую область памяти, устанавливаем регистр r0 в 5, и начинаем эмулировать наш код.
Метод emu_start() принимает необязательный аргумент тайм-аута и необязательное максимальное количество инструкций для эмуляции, которое может быть полезно для изолирования кода или ограничения эмуляции определенной частью кода.
После завершения эмуляции читаем содержимое регистра r1, который должен содержать результат эмуляции. Запустив кода, получим следующие результаты:
Assembling the ARM code Code successfully assembled (length = 20) ARM bytecode: b'\x01\x10\xa0\xe3\x00\x00P\xe3\x91\x00\x01\xc0\x01\x00@\xc2\xfb\xff\xff\xca' Emulating the ARM code Эмуляция сделана. Посмотрите >> R1 = 120
И действительно получаем ожидаемый результат, факториал 5 равен 120.
Дизассемблирование кода x86-64
А что, если у нас есть машинный код x86 и мы хотим его дизассемблировать. Для этого запишем следующий код:
# We need to emulate ARM and x86 code
from unicorn import Uc, UC_ARCH_X86, UC_MODE_64, UcError
# для доступа к регистрам RAX и RDI
from unicorn.x86_const import UC_X86_REG_RDI, UC_X86_REG_RAX
# We need to disassemble x86_64 code
from capstone import Cs, CS_ARCH_X86, CS_MODE_64, CsError
X86_MACHINE_CODE = b"\x48\x31\xc0\x48\xff\xc0\x48\x85\xff\x0f\x84\x0d\x00\x00\x00\x48\x99\x48\xf7\xe7\x48\xff\xcf\xe9\xea\xff\xff\xff"
# адрес памяти, с которого начинается эмуляция
ADDRESS = 0x1000000
try:
# Инициализировать дизассемблер в режиме x86
md = Cs(CS_ARCH_X86, CS_MODE_64)
# перебрать каждую инструкцию и распечатать ее
for instruction in md.disasm(X86_MACHINE_CODE, 0x1000):
print("0x%x:\t%s\t%s" % (instruction.address, instruction.mnemonic, instruction.op_str))
except CsError as e:
print("Capstone Error: %s" % e)
Мы инициализируем дизассемблер в режиме x86-64, дизассемблируем предоставленный машинный код, перебираем инструкции в результате дизассемблирования и для каждой из них печатаем инструкцию и адрес, где она встречается.
Вот что получим:
0x1000: xor rax, rax 0x1003: inc rax 0x1006: test rdi, rdi 0x1009: je 0x101c 0x100f: cqo 0x1011: mul rdi 0x1014: dec rdi 0x1017: jmp 0x1006
Теперь попробуем сымитировать это с помощью движка Unicorn:
try:
# Инициализировать эмулятор в режиме x86_64
mu = Uc(UC_ARCH_X86, UC_MODE_64)
# отдадим 2 МБ памяти для этой эмуляции
mu.mem_map(ADDRESS, 2 * 1024 * 1024)
# записываем машинный код для эмуляции в память
mu.mem_write(ADDRESS, X86_MACHINE_CODE)
# Установить регистр r0 в коде на число 7
mu.reg_write(UC_X86_REG_RDI, 7)
# эмулировать код за бесконечное время и неограниченное количество инструкций
mu.emu_start(ADDRESS, ADDRESS + len(X86_MACHINE_CODE))
# теперь распечатайте регистр R0
print("Эмуляция сделана. Посмотрите")
rax = mu.reg_read(UC_X86_REG_RAX)
print(">>> RAX = %u" % rax)
except UcError as e:
print("Unicorn Error: %s" % e)
Получите, распишитесь:
Эмуляция сделана. Посмотрите >>> RAX = 5040
Мы получаем результат 5040 для 7. Если мы внимательно посмотрим на этот ассемблерный код x86, то заметим, что этот код вычисляет факториал регистра rdi (5040 — факториал 7).
И-го-го
Три фреймворка одинаковым нообразом манипулируют кодом сборки, как можно видеть в коде, имитирующем сборку x86-64 и, который действительно похож на версию, эмулирующую ARM. Дизассемблирование и сборка кода также выполняется таким же образом с любой поддерживаемой архитектурой.
Следует иметь в виду, что эмулятор Unicorn эмулирует необработанный машинный код, он не эмулирует вызовы Windows APIએ, а также не анализирует и не эмулирует такие форматы файлов, как Portable Executableએ и Executable and Linkable Formatએ.
В некоторых сценариях полезно эмулировать всю операционную систему или программу в виде драйвера ядра или двоичный файл, предназначенный для другой операционной системы. Есть отличная платформа, построенная на основе Unicorn, которая обрабатывает эти ограничения, а также позволяет работаь из Python, то есть , она также позволяет использовать двоичные инструменты (например, поддельный системный вызов возвращаемые значения, файловые дескрипторы и т. д.).
После тестирования трех фреймворков Python мы пришли к выводу, что манипулировать кодом сборки с помощью Python очень легко, простота Python в сочетании с удобными и единообразными интерфейсами Python, предлагаемыми Keystone, Capstone и Unicorn, упрощают сборку даже для новичков, дизассемблировать и эмулировать ассемблерный код для разных архитектур.
Использованы маериалы
Сборка, разборка и эмуляция с использованием Python, опубликовано К ВВ, лицензия — Creative Commons Attribution-NonCommercial 4.0 International.
Респект и уважуха

