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

Статья Метаморфозы. Изучаем возможности фреймворка Metasm

weaver

31 c0 bb ea 1b e6 77 66 b8 88 13 50 ff d3
Забанен
Регистрация
19.12.2018
Сообщения
3 301
Решения
11
Реакции
4 622
Депозит
0.0001
Пожалуйста, обратите внимание, что пользователь заблокирован
Манипуляция с машинным кодом дает большие возможности как для программистов, так и для хакеров. Единственное, что является камнем преткновения для тех и других, — это динамический разбор структур, анализ и систематизация кода для эффективной работы с ним. Автоматизацию этой рутинной работы мы сегодня и рассмотрим.

ВВЕДЕНИЕ В ДИНАМИЧЕСКУЮ РЕКОМПИЛЯЦИЮ

Что же такое динамическая рекомпиляция и зачем она нужна? С точки зрения науки реверс-инжиниринга вопрос непростой, потому что включает в себя разбор больших структур бинарного кода с последующим анализом (отделением мух от котлет) и определенными действиями уже над подготовленным кодом. Компиляция — процесс однонаправленный, и в отличие от интерпретаторов и виртуальных машин компиляторы имеют на борту оптимизирующие бэкенды, которые наглухо уничтожают всю «полезную» информацию для полноценного восстановления исходного кода скомпилированной программы. Конечно же, есть возможность компиляции с драгоценной с точки зрения реверсера отладочной информацией (например, «gcc -g ./prog1.c -o /prog»), но на практике в release-версиях (то есть конечных вариантах) это маловероятно. Ты должен понимать, что отладочная информация — это палка о двух концах, которая также может послужить кратчайшим путем для крекинга проприетарного ПО.

Суть динамической рекомпиляции, которую также часто называют двоичной трансляцией (binary translation), заключается в эмуляции одного набора инструкций на другом за счет трансляции машинного кода. Последовательности инструкций переводятся из исходного набора (source) в целевой (target) набор инструкций. Двоичная трансляция позволяет выполнять приложения одной архитектуры при работе на второй, причем для оптимизирующих двоичных компиляторов скорость выполнения кода зачастую получается выше оригинала. Динамические рекомпиляторы бинарного кода могут быть реализованы как программно, так и аппаратно, причем программно они реализуются в двух вариантах:

a) в качестве транслятора фронтенд-кода в пользовательском уровне (ring 3);
б) в виде гипервизора, работающего в режиме ядра (ring 0) и транслирующего код с архитектурой процессоров одной платформы для другой.

Здесь нельзя не привести в пример разработанный для Apple корпорацией Transitive программный пакет Rosetta для динамической трансляции между платформами на основе архитектур SPARC, PowerPC, MIPS, Itanium и x86. Уровень трансляции Rosetta был включен в выпуски Mac OS 10.4 для Intel-ориентированных Mac — это было необходимо для упрощения перехода от PPC к x86.

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

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

• динамическая декомпиляция и манипуляция кодом на уровне ассемблера конкретной машины: морфинг, крипт, структурная обфускация инструкций, которые так часто используются вирусописателями;
• динамическая декомпиляция с переводом ассемблеровских инструкций в псевдокод на любом из High Level Language (далее просто HLL);
• динамическая инструментализация бинарного кода для поиска узких мест (вопросы оптимизации), утечки памяти (memory leaking);
• динамическая инструментализация бинарного кода с последующим анализом покрытия кода для поиска типичных ошибок (buffer overflow, stack overflow и так далее);
• динамическая рекомпиляция бинарного кода с возможностью трейса и гибкой манипуляции кодом, которая может использоваться для автоматических распаковщиков различного рода пакеров, крипторов, виртуализаторов исполняемых файлов.

На ум сразу приходят динамические рекомпиляторы типа Valgrind, PIN Toolkit, DynamoRIO, которые часто используются как средства для анализа покрытия кода.

g2.png


Некоторые исследователи используют в качестве инструмента известную ныне связку реверс-инженера: IDA Pro + IDAPython. В большинстве своем первые инструменты решают очень узкие задачи, то есть об универсальном подходе речи тут и не может быть. Для второго инструмента, как минимум, придется раскошелиться на платную версию Иды, ну и, соответственно, ознакомиться с API IDAPython. Казалось бы, универсального и бесплатного инструмента для вышеперечисленных направлений не существует, но чудо, как оказалось, есть, и имя ему Metasm.

ШВЕЙЦАРСКИЙ НОЖ РЕВЕРС-ИНЖЕНЕРА

Metasm — довольно крутой фреймворк с открытым исходным кодом, позволяющий взаимодействовать с машинным кодом в различных форматах (шестнадцатеричным байт-кодом, кодом на Cи и ассемблером конкретной машины). Поддерживает подавляющее большинство архитектур процессоров, форматов исполняемых файлов и работает на практически всех известных операционных системах. Впервые данная разработка была представлена Йоанном Гийо (Yoann Guillot) из ESEC на конференции SSTIC 2007, и позднее на ней практиковались на нескольких конференциях Hack.lu. Кстати, Metasm является сердцем такого известного и любимого инструмента пентестеров и хакеров всех мастей, как Metasploit Framework.

Основная часть фреймворка, которая именуется движком, написана на Ruby. Установка довольно проста, единственное, что для нее требуется сделать, — это прописать пути в переменные окружения пользовательского интерпретатора. Если ты так же, как и я, используешь в качестве интерпретатора bash, тебе после распаковки библиотеки нужно будет добавить следующую строку в ~/.bash_profile:

Код:
export RUBYLIB=$RUBYLIB:/<путь до библиотеки>/metasm

Для пользователей Windows вопрос решается аналогичным образом, если они работают с cygwin (эмулятор UNIX-среды). В любом другом случае идем в переменные окружения OS Windows: свойства «Моего компьютера», на вкладке «Дополнительно» находим «Переменные окружения». Если у тебя уже стоит Руби, то там должна быть переменная RUBYLIB (если ее нет, то создаем вручную), в которую нужно будет как раз таки дописать путь до самой библиотеки.

Чтобы проверить, работает ли наша библиотека, достаточно вписать в консоль следующую команду:

Код:
ruby -r metasm -e 'p Metasm::VERSION'

С установкой разобрались, теперь рассмотрим краткий перечень возможностей фреймворка:

• интерактивная работа и гибкая манипуляция исполняемыми файлами: PE COFF, ELF, Mach-O, Raw Shellcode;
• компиляция с нуля без использования компиляторов, линкеров и тому подобного инструментария;
• дизассемблирование и базовая декомпиляция в псевдокод С-like HLL;
• манипуляция структурой файлов.

ПОСТАНОВКА ПРОСТЫХ ЗАДАЧ И БЫСТРОЕ РЕШЕНИЕ

Выше мы рассмотрели перспективные направления динамической рекомпиляции, теперь пришло время продемонстрировать возможности фреймворка на деле. Итак, есть простая задача компиляции сырого Linux-шелл-кода на asm. Ниже приведен пример сборки шелл-кода без использования компилятора.

Подключаем нашу библиотеку:

Код:
require 'metasm'

Вызываем модуль ассемблирования, тип исполняемого файла a.out, архитектура процессора IA-32 (то есть x86), выхлопной файл test1.out, в переменную RAW_SHELLCODE заносим буфер с кодом нашего шелл-кода:

Код:
Metasm::AOut.assemble(Metasm::Ia32.new,<<RAW_SHELLCODE).encode_file('test1.out')
.text
.entrypoint
mov eax, 4
mov ebx, 1
.data
str db "test\\n"
strend:
.text
mov ecx, str
mov edx, strend - str
int 80h // linux sys_write
mov eax, 1
mov ebx, 42
int 80h // linux sys_exit
ret
RAW_SHELLCODE

Отлично, проверяем наш сырой шелл-код и видим, что он работает, дизассемблировав встроенным дизассемблером (который идет в папке с примерами /samples):

Код:
$ ruby ./test1.rb
$ ruby ./disassemble.rb --no-data --cpu Ia32 ./test1.out
entrypoint_0:
 add [eax], al ; @0 0000
 add fs:[eax], al ; @2 640000
 adc [eax], al ; @5 1000
 ...
 mov eax, 1 ; @36h b801000000
 mov ebx, 2ah ; @3bh bb2a000000
 int 80h ; @40h cd80
 ret ; @42h c3 endsub entrypoint_0

Собственно, ты можешь сравнить этот код с дизассемблерным листингом, полученным с помощью ndisasm из пакета NASM.

Код:
$ ndisasm -b32 ./test1.out

Такой дизассемблерный дамп легко будет распарсить тем же Metasm/Ruby и в дальнейшем манипулировать этим кодом как душе угодно — оптимизировать узкие места, транслировать в опкоды других машин, разбивать и переводить в микод (Mutable Independent Code) и так далее.

А как насчет декомпиляции и перевода в псевдокод HLL, например, реального работающего исполняемого файла библиотеки libc? После некоторой возни интерпретатора Руби и шуршания процессора на выходе мы получим:

Код:
$ ruby ./disassemble.rb --no-data --cpu Ia32 --exe ELF
/lib/libc.so.6 --decompile

libc.png


Качество кода не сказать что хорошее, но и не особо плохое, к примеру, для морфинга на уровне исходных кодов или в качестве легитимного стаба для исполняемых файлов полученный код очень даже хорош. Если немного посидеть и поработать над модулем декомпиляции, естественно, можно добиться куда лучших результатов. Для более качественной декомпиляции можно изменить подход, например на ступени генерации IR (Itermediate Representation) разбивать код на специальные блоки (узлы графа), далее на основе сигнатур методом сопоставления восстанавливать структуры кода. Реализация идеи довольно проста, по сути, представляет собой специальный конструктор конечного автомата на основе множества свитчкейсов Ruby-кода: case something when «1» ... when «2» ... и так далее. Если прикрутить сюда модули генерации правдоподобных имен переменных, функций, процедур, можно получить более-менее полноценный декомпилятор. Подняв же веб-панель на Ruby on Rails, можно за короткое время организовать и неплохой сервис по декомпиляции ПО.

entropy.png


Текущий псевдокод больше поход на выхлоп фронтендовой части компилятора gcc, на этапе генерации синтаксического дерева. Такой код легко перегнать в PI-код (Position Independent Code, не путать с p-кодом виртуальных машин). Скрипт disassemble. rb принимает намного больше параметров и, соответственно, предоставляет большие возможности для манипуляции процессом дизассемблирования. В папке samples ты также можешь найти сценарий disassemble-gui.rb, представляющий собой графическую прослойку над консольным дизассемблером.

arch.png



СБОРКА ИСПОЛНЯЕМЫХ ФАЙЛОВ НА HLL
Выше мы рассмотрели компиляцию сырого шелл-кода, в котором обошлись без использования компиляторов. Это уже впечатляет, а что ты скажешь, если мы решим компилировать Си-код с использованием WIN32API? Веришь или нет, но Metasm может и не такое!

Подключаем нашу библиотеку:

Код:
require 'metasm'

Дальше описываем параметры для сборки бинаря: первый параметр execlass, в который попадает тип исполняемого файла Metasm::PE, можно выбрать и другие типы ELF/MachO, с помощью второго параметра srctype_data указываем, что наш код на С (понятно, что можно указать и asm, если у нас код на ассемблере):

Код:
$opts = { :execlass => Metasm::PE, :srctype_data => 'c' }

Склеиваем исходники нашего файла и сэмпла exeencode.rb, представленного как раз для компиляции HLL-исходников:

Код:
load File.join(File.dirname(__FILE__), 'exeencode.rb')
__END__

Ниже представлен код на Си. Если внимательно рассмотреть этот код, можно заметить, что WinAPI-функции тут представлены декораторами, и их в обязательном порядке нужно вначале объявлять в виде прототипов. Ты также должен понимать, что любые другие функции из ряда CRT/RTL/LIBC и прочих библиотек также должны описываться в виде прототипов прежде, чем ты сможешь использовать их в своем коде.

Код:
__stdcall int MessageBox(int, char*, char*, int);
__stdcall void ExitProcess(int);
void main(void)
{
 MessageBox(0, "Sanjar Satsura", "hello", 0);
 ExitProcess(0);
}

Высокоуровневый код тут можно мешать также и с ассемблером, как отдельно в виде связки C + ASM кода, так и в виде инлайновых ассемблерных вставок прямо в Си-коде. Тот же самый код на чистом ассемблере x86 будет выглядеть так:

Код:
require 'metasm'

Создаем переменную pe в качестве хендла для манипуляции кодом. Указываем тип исполняемого файла, в данном случае — PE, наш код на ассемблере, поэтому применяем метод assemble, так как код мы пишем для x86, ОС указываем в качестве архитектуры Ia32. В переменную EOS методом HEREDOC помещается наш код на ассемблере:

Код:
pe = Metasm::PE.assemble Metasm::Ia32.new, <<EOS
.entrypoint
push 0
push title
push message
push 0
call MessageBoxA
xor eax, eax
ret
.data
message db 'Sanjar Satsura', 0
title db 'hello', 0
EOS

Собираем наш исполняемый файл:

Код:
pe.encode_file 'testpe.exe'

В исходниках, приложенных к данной статье, ты можешь найти демку, цель которой — продемонстрировать ход трансляции и кодогенерации рубинового компилятора Metasm. Думаю, не нужно быть гением, чтобы понять, какой профит можно сорвать на этой теме. Если посмотреть на внутренности предложений черного рынка, к примеру услуги «уникальных» крипторов/протекторов, предоставляющих доступ через веб-интерфейс, можно понять, насколько они убоги в реализациях. И неудивительно, что все сервисы подобного рода оказываются построены на PHP (вебинтерфейс, шлюз) и исполняемом бинарном движке, работающем в подавляющем большинстве (99,8%) на VPS с ОС Windows. Добавим сюда криворуких кодеров подобных веб-сервисов с конструкциями кода типа:

Код:
...
system_execute('C:\\SXCryptor\engine.exe
--input_file '.$_GET["file_id"].'
--output_file '.rand_fl($fname).'
--dir '.$wrk_dir);
...

Все, что мы рассмотрели выше, еще цветочки по сравнению с тем, что этот волшебный фреймворк нам позволяет написать. Так вот, он позволяет создавать не только ring 3 приложения, но и модули ядра, драйверы для известных пользовательских операционных систем, работоспособность которых тут же можно проверить при помощи встроенных VM. Звучит круто? Давай попробуем создать полноценный драйвер с возможностью загрузки и выгрузки. Ниже я приведу фрагменты кода с комментариями, полный исходник драйвера ты можешь найти на нашем диске.

Код:
require 'metasm'

Обрати внимание: ниже мы не инклудим библиотеку, в данном случае слово include необходимо для перевода имени класса Metasm в глобальную область видимости. В C++/C# за это ответственна конструкция «using namespace ***». До этого мы делали так: Metasm::PE.assemble, Metasm::Ia32.new. В дальнейшем конструкцию Metasm:: можно будет не писать.


Код:
include Metasm
# Имя нашего драйвера
$drv = 'drv_test.sys'
# Размер буфера обычно используется для трейса драйвера,
здесь мы его рассматривать не будем
BUF_SZ = 0
# Проверяем, существует ли файл нашего драйвера
if not File.exist? $drv

Собираем драйвер, в переменной DRV_CODE содержится код нашего 32-битного драйвера для Windows. Опция kmod в encode_file указывает, что драйвер работает на уровне ядра:

Код:
PE.assemble(Ia32.new, <<DRV_CODE).encode_file
($drv, 'kmod')
#define bufsz #{BUF_SZ}
.data
oldi1 dd 0,0
oldi15 dd 0,0
buf dd bufsz dup(?)
.text
.entrypoint
mov eax, [esp+4]
mov dword ptr [eax+0x34], unload
call setup_idt
xor eax, eax
// Размер используемого буфера
mov [buf], eax
ret

Для запуска и останова напишем Cи-обертку. DynLdr — специальный каркасный модуль Metasm, позволяющий интерпретатору Ruby манипулировать экспортируемыми API-функциями динамически разделяемых библиотек (*dll, *so).

Код:
DynLdr.new_api_c <<DRV_CODE
// Определяем типы и структуры данных
typedef int BOOL;
typedef char CHAR;
typedef unsigned long DWORD;
...

Здесь, как и описывалось ранее, задаем прототипы WinAPIфункций:

Код:
__stdcall BOOL CloseServiceHandle
(SC_HANDLE hSCObject __attribute__((in)));
__stdcall SC_HANDLE
CreateServiceA(SC_HANDLE hSCManager
__attribute__((in)), LPCSTR lpServiceName
__attribute__((in)), LPCSTR lpDisplayName
__attribute__((in)),DWORD dwDesiredAccess
__attribute__((in)), DWORD
...

Константы, используемые импортируемыми выше API функциями

Код:
#define STANDARD_RIGHTS_REQUIRED (0x000F0000L)
#define SC_MANAGER_CONNECT 0x0001
#define SC_MANAGER_CREATE_SERVICE 0x0002
...

Ну и, собственно, код для загрузки/выгрузки драйвера:

Код:
# Создаем функцию loadmod, где переменной mod
присваивается указатель на наш драйвер
def loadmod(mod=$drv)

После того как модуль DynLdr подгрузил все нужные API из прототипов, имена функций можно писать в нижнем регистре. Переменная sh является хендлом для открытия менеджера управления сервисами. Без старта OpenSCManagerA() мы не можем стартовать сервис нашего драйвера CreateServiceA().

Код:
sh = DynLdr.openscmanagera
(0, 0, DynLdr::SC_MANAGER_ALL_ACCESS)
# Выплевываем ошибку при возникновении исключения
 raise "cannot openscm" if (sh == 0)

Переменная rh является хендлом функции старта сервиса, функцией CreateServiceA() запускаем сервис, передав параметры для запуска, так, параметр SERVICE_KERNEL_DRIVER указывает на то, что это драйвер режима ядра.

Код:
 rh = DynLdr.createservicea(sh, mod, mod,
 DynLdr::SERVICE_ALL_ACCESS,
 DynLdr::SERVICE_KERNEL_DRIVER,
 DynLdr::SERVICE_DEMAND_START,
 DynLdr::SERVICE_ERROR_NORMAL,
 File.expand_path(mod), 0, 0, 0, 0, 0)
 # Стартуем сервис
 if (DynLdr.startservicea(rh, 0, 0) == 0)
 raise "cannot start service"
 end
 # Закрытие хендлов в обратном порядке,
 # по типу LIFO (Last Input First Out)
 DynLdr.CloseServiceHandle(rh)
 DynLdr.CloseServiceHandle(sh)
end
# Функция выгрузки драйвера
def unloadmod(mod=$drv)
 sh = DynLdr.openscmanagera
 (0, 0, DynLdr::SC_MANAGER_ALL_ACCESS)
 raise "cannot openscm" if (sh == 0)
 rh = DynLdr.openservicea
 (sh, mod, DynLdr::SERVICE_ALL_ACCESS)
Функция ControlService() позволяет нам управлять сервисами.
В качестве параметров принимает хендл rh и параметр SERVICE_
CONTROL_STOP, который останавливает сервис.
 DynLdr.controlservice
 (rh, DynLdr::SERVICE_CONTROL_STOP, 0.chr*4*32)
 # Удаляем сервис функцией DeleteService()
 DynLdr.deleteservice(rh)
 DynLdr.CloseServiceHandle(rh)
 DynLdr.CloseServiceHandle(sh)
end

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

ПОТРОШЕНИЕ ПАКЕРА
Напоследок мы займемся распаковкой известного пакера UPX. В задачу нашего скрипта будет входить загрузка упакованного исполняемого файла в память, поиск оригинальной точки входа по дизассемблированному UPX стабу в памяти, установка прерываний на OEP, ну и наконец, дамп распакованного образа на жесткий диск.

Код:
def find_oep(pe)
# Дизассемблируем стаб образа UPX для поиска
# cross-section jump’ов для нахождения оригинальной точки
# входа (OEP)
dasm = pe.disassemble_fast_deep 'entrypoint'
 return if not jmp = dasm.decoded.find { |addr, di|
 # Проверяем каждый блок данных узлов графа
 next if not di.block_head?
 b = di.block
 next if b.to_subfuncret.to_a.length != 0 or
 b.to_normal.to_a.length != 1
 to = b.to_normal.first
 # Игнорируем прыжки в несуществующие адреса
 next if not s = dasm.get_section_at(to)
 # Игнорируем прыжки в данной секции
 next if dasm.get_section_at(di.address) == s
 true
}

Теперь мы имеем нормальный jump [<адрес>, di], благодаря которому появляется возможность восстановить оригинальную точку входа:

Код:
dasm.normalize(jmp[1].block.to_normal.first)
end

В качестве прерывания будем ставить hardware breakpoint на OEP:

Код:
def debugloop
# Функция debugloop истинна, пока нет адреса точки входа
 @dbg.hwbp(@oep, :x, 1, true) { breakpoint_callback }
 @dbg.run_forever
 puts 'done'
end
def breakpoint_callback
 puts 'breakpoint hit !'
 # Снимаем дамп процесса из памяти, создавая при этом
 # уникальный исполняемый PE-файл
 dump = LoadedPE.memdump @dbg.memory, @baseaddr, @oep,
@iat
 # Загрузчик упаковщика UPX распаковывает все данные
 # из секции помеченных для чтения (Read Only), сменив
 # маркер RO на RW (Read Write) в заголовке PE-файла
 dump.sections.each { |s| s.characteristics |=
 ['MEM_WRITE'] }
 # Пишем распакованный образ на жесткий диск
 dump.encode_file @dumpfile
...

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

Код:
def initialize(file, dumpfile, iat_rva=nil)
 @dumpfile = dumpfile || 'upx-dumped.exe'
 @iat = iat_rva
 puts 'disassembling UPX loader...'
 # Считываем данные запакованного файла
 pe = PE.decode_file(file)
 # Ищем точку входа
 @oep = find_oep(pe)
 raise 'cant find oep...' if not @oep
 puts "oep found at #{Expression[@oep]}"
 @baseaddr = pe.optheader.image_base
 @iat -= @baseaddr if @iat > @baseaddr
 # Запускаем отладчик для установки хардварных
 # брейкпоинтов и трейса (функция debugloop)
 # инструкций
 @dbg = OS.current.create_process(file).debugger
 puts 'running...'
 debugloop
end

graph.png


Единственное, что остается выполнить для восстановления полноценной работоспособности, — это восстановить таблицу импорта распакованного файла, например утилитой ImpRec. Упаковщик UPX мы выбрали в качестве протектора не случайно. В приведенном примере выполняются практически все стандартные функции распаковки, которые могут использоваться и в других упаковщиках/крипторах. По сути, эти принципы распространяются на 95% существующих протекторов, которые как раз таки в большинстве своем черпали вдохновение именно из UPX. Вторым немаловажным моментом является мультиплатформенность этого упаковщика, сжатие поддерживается как для PE COFF, так и для ELF-форматов, а вместе с тем распространяются и методы распаковки на *nix-системы. Последние 5% существующих протекторов, которые немного отличаются методами упаковки данных, — это, наверное, виртуализаторы кода и крипторы с пермутирующим движком. Для первых существуют таблицы с опкодами VM-движков, для вторых можно использовать VM нашего фреймворка.

ПРОЧИЕ ВОЗМОЖНОСТИ
Metasm предоставляет среду для воплощения большого количества идей, связанных не только со сферой инфобезопасности. Существует ряд нерешенных алгоритмических задач в теории компиляторов. Написанный на Ruby, фреймворк предоставляет возможность решения этих самых задач не самым человечным и ООП-шным образом. Дерзай!

WWW

Источник: https://xakep.ru/issues/xa/168/
 

Вложения

  • Метаморфозы.zip
    317 КБ · Просмотры: 14


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