Вспоминаем
Дизассемблерએ — это процесс, обратный сборке, байты данных анализируются и преобразуются в инструкции сборки (которые более удобочитаемы для пользователей).
Различные архитектуры процессоров могут иметь разные наборы инструкций, и один процессор может выполнять инструкции ассемблера только в своем собственном наборе инструкций для запуска кода, предназначенного для разных архитектур, нам нужно использовать эмулятор, который представляет собой программу, которая переводит код для неподдерживаемой архитектуры в код, который может выполняться в хост-системе.
Есть много сценариев, в которых может быть полезна сборка, дизассемблирование или эмуляция кода для разных архитектур, один из основных интересов — обучение (большинство университетов преподают сборку MIPS) для запуска и тестирования программ, написанных для различных устройств, таких как маршрутизаторы (фаззинг и т. д.), а также для обратного проектирования.
В этом руководстве мы соберем, дизассемблируем и эмулируем ассемблерный код, написанный для ARM с использованием движка
Сначала установим эти три фреймворка:
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.
Респект и уважуха