Рецепты для ELFов
На русском языке довольно мало информации про то, как работать с ELF-файлами (Executable and Linkable Format — основной формат исполняемых файлов Linux и многих Unix-систем). Не претендуем на полное покрытие всех возможных сценариев работы с эльфами, но надеемся, что информация будет полезна в виде справочника и сборника рецептов для программистов и реверс-инженеров.
Подразумевается, что читатель на базовом уровне знаком с форматом ELF (в противном случае рекомендуем цикл статей Executable and Linkable Format 101).
Под катом будут перечислены инструменты для работы, описаны приемы для чтения метаинформации, модификации, проверки и размножения создания эльфов, а также приведены ссылки на полезные материалы.
— Я тоже эльф… Синий в красный… Эльфы очень терпеливы… Синий в красный… А мы эльфы!.. Синий в красный… От магии одни беды…
(с) Маленькое королевство Бена и Холли
Инструменты
В большинстве случаев примеры можно выполнить как на Linux, так и на Windows.
В рецептах мы будем использовать следующие инструменты:
- утилиты из набора binutils (objcopy, objdump, readelf, strip);
- фреймворк radare2;
- hex-редактор с поддержкой шаблонов файлов (в примерах показан 010Editor, но можно использовать, например, свободный Veles);
- Python и библиотеку LIEF;
- другие утилиты (ссылки указаны в рецепте).
Тестовые эльфы
В качестве «подопытного» будем использовать ELF-файл simple из таска nutcake's PieIsMyFav на crackmes.one, но подойдёт любой представитель «эльфийского» семейства. Если готовый файл с требуемыми характеристиками не был найден в свободном доступе, то будет приведён способ создания такого эльфа.
«Свободных» эльфов можно также найти по ссылкам:
- Примеры эльфов для разных платформ;
- Тестовые эльфы для radare2;
- Проект ElfHacks на Github — подборка небольших эльфов с разными настройками;
- Crackme для Unix/Linux — но стоит учитывать, что тут могут попадаться хитрые образцы.
Чтение, получение информации
Тип файла, заголовок, секции
В зависимости от задачи интерес могут представлять:
- тип файла (DYN — библиотека, EXEC — исполняемый, RELOC — линкуемый);
- целевая архитектура (E_MACHINE — x86_64, x86, ARM и т.д.);
- точка входа в приложение (Entry Point);
- информация о секциях.
010Editor
HEX-редактор 010Editor предоставляет систему шаблонов. Для ELF-файлов шаблон называется, как ни странно, ELF.bt и находится в категории Executable (меню Templates — Executable).
Интерес может представлять, например, точка входа в исполняемый файл (entry point) (записана в заголовке файла).
readelf
Утилиту readelf можно считать стандартом де-факто для получения сведений об ELF-файле.
- Прочитать заголовок файла:
$ readelf -h simple
Результат команды
- Прочитать информацию о сегментах и секциях:
$ readelf -l -W simple
Результат команды
- Прочитать информацию о секциях:
$ readelf -S -W simple
Результат команды
- Прочитать информацию о символах:
$ readelf -s -W simple
Результат команды
Опция -W
нужна для увеличения ширины консольного вывода (по умолчанию, 80 символов).
LIEF
Прочитать заголовок и информацию о секциях можно с использованием кода на Python и библиотеки LIEF (предоставляет API не только для Python):
import lief binary = lief.parse("simple.elf") header = binary.header print("Entry point: %08x" % header.entrypoint) print("Architecture: ", header.machine_type) for section in binary.sections: print("Section %s - size: %s bytes" % (section.name, section.size)
Информация о компиляторе
Для получения информации о компиляторе и сборке следует смотреть секции .comment
и .note
.
objdump
$ objdump -s --section .comment simple
Результат команды
readelf
$ readelf -p .comment simple
Результат команды
$ readelf -n simple
Результат команды
LIEF
import lief binary = lief.parse("simple") comment = binary.get_section(".comment") print("Comment: ", bytes(comment.content))
Я вычислю тебя по… RPATH
Эльфы могут сохранять пути для поиска динамически подключаемых библиотек. Чтобы не задавать системную переменную LD_LIBRARY_PATH
перед запуском приложения, можно просто «вшить» этот путь в ELF-файл.
Для этого используется запись в секции .dynamic
с типом DT_RPATH
или DT_RUNPATH
(см. главу Directories Searched by the Runtime Linker в документации).
И будь осторожен, юный разработчик, не «спали» свою директорию проекта!
Как появляется RPATH?
Основная причина появления RPATH-записи в эльфе — опция -rpath
линковщика для поиска динамической библиотеки. Примерно так:
$ gcc -L./lib -Wall -Wl,-rpath=/run/media/pablo/disk1/projects/cheat_sheets/ELF/lib/ -o test_rpath.elf bubble_main.c -lbubble
Такая команда создаст в секции .dynamic
RPATH-запись со значением /run/media/pablo/disk1/projects/cheat_sheets/ELF/lib/
.
readelf
Посмотреть элементы из секции .dynamic
(среди которых есть и RPATH) можно так:
$ readelf -d test_rpath.elf
Результат команды
LIEF
С помощью библиотеки LIEF также можно прочитать RPATH-запись в эльфе:
import lief from lief.ELF import DYNAMIC_TAGS elf = lief.parse("test_rpath.elf") if elf.has(DYNAMIC_TAGS.RPATH): rpath = next(filter(lambda x: x.tag == DYNAMIC_TAGS.RPATH, elf.dynamic_entries)) for path in rpath.paths: print(path) else: print("No RPATH in ELF")
Проверка эльфа на безопасность
Скрипт проверки безопасности checksec.sh от исследователя Tobias Klein (автора книги A Bug Hunter's Diary) не обновлялся с 2011 года. Данный скрипт для ELF-файлов выполняет проверку наличия опций RelRO (Read Only Relocations), NX (Non-Executable Stack), Stack Canaries, PIE (Position Independent Executables) и для своей работы использует утилиту readelf.
Можно сделать свой аналог на коленке Python и LIEF (чуть короче прародителя и с дополнительной проверкой опции separate-code):
import lief from lief.ELF import DYNAMIC_TAGS, SEGMENT_TYPES def filecheck(filename): binary = lief.parse(filename) # check RELRO if binary.has(SEGMENT_TYPES.GNU_RELRO): print("+ Full RELRO") if binary.has(DYNAMIC_TAGS.BIND_NOW) else print("~ Partial RELRO") else: print("- No RELRO") # check for stack canary support print("+ Canary found") if binary.has_symbol("__stack_chk_fail") else print("- No canary found") # check for NX support (check X-flag for GNU_STACK-segment) print("+ NX enabled") if binary.has_nx else print("- NX disabled") # check for PIE support print("+ PIE enabled") if binary.is_pie else print("- No PIE") # check for rpath / run path print("+ RPATH") if binary.has(DYNAMIC_TAGS.RPATH) else print("- No RPATH") print("+ RUNPATH")if binary.has(DYNAMIC_TAGS.RUNPATH) else print("- No RUNPATH") # check separate-code option if set(binary.get_section('.text').segments) == set(binary.get_section('.rodata').segments): print("- Not Separated Code Sections") else: print("+ Separated Code Sections") filecheck('test_rpath.elf')
«Сырой код» из эльфа (binary from ELF)
Бывают ситуации, когда «эльфийские одёжи» в виде ELF-структуры не нужны, а нужен только «голый» исполняемый код приложения.
objcopy
Использование objcopy вероятно знакомо тем, кто пишет прошивки:
$ objcopy -O binary -S -g simple.elf simple.bin
-S
— для удаления символьной информации;-g
— для удаления отладочной информации.
LIEF
Никакой магии. Просто взять содержимое загружаемых секций и слепить из них бинарь:
import lief from lief.ELF import SECTION_FLAGS, SECTION_TYPES binary = lief.parse("test") end_addr = 0 data = [] for section in filter(lambda x: x.has(SECTION_FLAGS.ALLOC) and x.type != SECTION_TYPES.NOBITS, binary.sections): if 0 < end_addr < section.virtual_address: align_bytes = b'\x00' * (section.virtual_address - end_addr) data.append(align_bytes) data.append(bytes(section.content)) end_addr = section.virtual_address + section.size with open('test.lief.bin', 'wb') as f: for d_bytes in data: f.write(d_bytes)
Mangled — demangled имена функций
В ELF-ах, созданных из С++ кода, имена функций декорированы (манглированы) для упрощения поиска соответствующей функции класса. Однако читать такие имена при анализе не очень удобно.
nm
Для представления имён в удобочитаемом виде можно использовать утилиту nm из набора binutils:
# Тут имена функций выводятся в манглированном виде $ nm -D demangle-test-cpp ... U _Unwind_Resume U _ZdlPv U _Znwm U _ZSt17__throw_bad_allocv U _ZSt20__throw_length_errorPKc # Тут имена функций выводятся в читаемом виде $ nm -D --demangle demangle-test-cpp ... U _Unwind_Resume U operator delete(void*) U operator new(unsigned long) U std::__throw_bad_alloc() U std::__throw_length_error(char const*)
LIEF
Вывод имён символов в деманглированном виде с использованием библиотеки LIEF:
import lief binary = lief.parse("demangle-test-cpp") for symb in binary.symbols: print(symb.name, symb.demangled_name)
Сборка, запись, модификация эльфа
Эльф без метаинформации
После того как приложение отлажено и выпускается в дикий мир, имеет смысл удалить метаинформацию:
- отладочные секции — бесполезны в большинстве случаев;
- имена переменных и функций — совершенно ни на что не влияют для конечного пользователя (чуть усложняет реверс);
- таблица секций — совершенно не нужна для запуска приложения (её отсутсвие чуть усложнит реверс).
Удаление символьной информации
Символьная информация — это имена объектов и функций. Без неё реверс приложения немного усложняется.
strip
В самом простом случае можно воспользоваться утилитой strip из набора binutils. Для удаления всей символьной информации достаточно выполнить команду:
- для исполняемого файла:
$ strip -s simple
- для динамической библиотеки:
$ strip --strip-unneeded libsimple.so
sstrip
Для тщательного удаления символьной информации (в том числе ненужных нулевых байтов в конце файла) можно воспользоваться утилитой sstrip из набора ELFkickers. Для удаления всей символьной информации достаточно выполнить команду:
$ sstrip -z simple
LIEF
C использованием библиотеки LIEF также можно сделать быстрый strip (удаляется таблица символов — секция .symtab
):
import lief binary = lief.parse("simple") binary.strip() binary.write("simple.stripped")
Удаление таблицы секций
Как упоминалось выше, наличие/отсутствие таблицы секций не оказывает влияния на работу приложения. Но при этом без таблицы секций реверс приложения становится чуть сложнее.
Воспользуемся библиотекой LIEF под Python и примером удаления таблицы секций:
import lief binary = lief.parse("simple") binary.header.numberof_sections = 0 binary.header.section_header_offset = 0 binary.write("simple.modified")
Изменение и удаление RPATH
chrpath, PatchELF
Для изменения RPATH под Linux можно воспользоваться утилитами chrpath (доступна в большинстве дистрибутивов) или PatchELF.
- Изменить RPATH:
$ chrpath -r /opt/my-libs/lib:/foo/lib test_rpath.elf
или
$ patchelf --set-rpath /opt/my-libs/lib:/foo/lib test_rpath.elf
- Удалить RPATH:
$ chrpath -d test_rpath.elf
или
$ patchelf --shrink-rpath test_rpath.elf
LIEF
Библиотека LIEF также позволяет как изменить, так и удалить RPATH-запись.
- Изменить RPATH:
import lief binary = lief.parse("test_rpath.elf") rpath = next(filter(lambda x: x.tag == lief.ELF.DYNAMIC_TAGS.RPATH, binary.dynamic_entries)) rpath.paths = ["/opt/my-lib/here"] binary.write("test_rpath.patched")
- Удалить RPATH:
import lief binary = lief.parse("test_rpath.elf") binary.remove(lief.ELF.DYNAMIC_TAGS.RPATH) binary.write("test_rpath.patched")
Обфускация символьной информации
Для усложнения реверса приложения можно сохранить символьную информацию, но запутать имена объектов. В качестве подопытного используем эльф crackme01_32bit из crackme01 by seveb.
Упрощенный вариант примера из библиотеки LIEF может выглядеть так:
import lief binary = lief.parse("crackme01_32bit") for i, symb in enumerate(binary.static_symbols): symb.name = "zzz_%d" % i binary.write("crackme01_32bit.obfuscated")
В результате получим:
$ readelf -s crackme01_32bit.obfuscated ... Symbol table '.symtab' contains 78 entries: Num: Value Size Type Bind Vis Ndx Name 0: 00000000 0 NOTYPE LOCAL DEFAULT UND zzz_0 1: 08048154 0 SECTION LOCAL DEFAULT 1 zzz_1 2: 08048168 0 SECTION LOCAL DEFAULT 2 zzz_2 3: 08048188 0 SECTION LOCAL DEFAULT 3 zzz_3 4: 080481ac 0 SECTION LOCAL DEFAULT 4 zzz_4 5: 080481d0 0 SECTION LOCAL DEFAULT 5 zzz_5 6: 080482b0 0 SECTION LOCAL DEFAULT 6 zzz_6 7: 0804835a 0 SECTION LOCAL DEFAULT 7 zzz_7 8: 08048378 0 SECTION LOCAL DEFAULT 8 zzz_8 9: 080483b8 0 SECTION LOCAL DEFAULT 9 zzz_9 10: 080483c8 0 SECTION LOCAL DEFAULT 10 zzz_10 ...
Подмена функций через PLT/GOT
Также известная как ELF PLT INFECTION.
Дабы не копипастить, просто оставим ссылки по теме:
Изменить точку входа
Может быть полезно при создании патчей, установке хуков и прочей динамической инструментации, ну или для вызова скрытых функций. В качестве подопытного используем эльфа crackme01_32bit из crackme01 by seveb
radare2
radare2 запускается в режиме записи (опция -w
) — изменения будут внесены в оригинальный файл:
$ ./crackme01_32bit Please enter the secret number: ^C $ r2 -w -nn crackme01_32bit [0x00000000]> .pf.elf_header.entry=0x0804860D [0x00000000]> q $ ./crackme01_32bit Nope.
LIEF
import lief binary = lief.parse("crackme01_32bit") header = binary.header header.entrypoint = 0x0804860D binary.write("crackme01_32bit.patched")
Патчинг кода
В качестве простого подопытного возьмём крякми novn91's crackmepal. При запуске без параметров программка выводит:
$ ./crackmeMario usage <password>
При запуске с параметром-произвольной строкой выдаётся:
./crackmeMario qwerty try again pal.
Сделаем патч, чтобы программа сразу при запуске выводила сообщение «good job! now keygen me!»
radare2
radare2 умеет патчить любые форматы, которые сам поддерживает. При этом имеется возможность описывать патчи в текстовом формате:
# Rapatch for https://crackmes.one/crackme/5ccecc7e33c5d4419da559b3 !echo Patching crackme 0x115D : jmp 0x1226
Применить такой патч можно командой:
$ r2 -P patch.txt crackmeMario
Почитать про патчинг кода через radare2:
- Binary Patching Using Radare2 by wolfshirtz
- Radare2 Explorations. Tutorial 1 — Simple Patch
- Ground Zero: Part 3-2 – Reverse Engineering – Patching Binaries with Radare2 – ARM64
LIEF
LIEF позволяет патчить эльф (перезаписать байты) по указанному виртуальному адресу. Патч может быть в виде массива байт или в виде целочисленного значения:
import lief binary = lief.parse("crackmeMario") binary.patch_address(0x115D, bytearray(b"\xe9\xc4\x00\x00\x00")) binary.write("crackmeMario.patched")
После применения патча программа будет выводить:
$ ./crackmeMario.patched good job! now keygen me!
Добавить секцию в ELF
objcopy
objcopy позволяет добавить секцию, но эта секция не будет относиться ни к одному сегменту и не будет загружаться в ОЗУ при запуске приложения:
$ objcopy --add-section .testme=data.zip \ --set-section-flags .testme=alloc,contents,load,readonly \ --change-section-address .testme=0x08777777 \ simple simple.patched.elf
LIEF
Библиотека LIEF позволяет добавить новую секцию и соответствующий ей сегмент (флаг loaded=True
) в имеющийся ELF:
import lief binary = lief.parse("simple") data = bytearray(b"\xFF" * 16) section = lief.ELF.Section(".testme", lief.ELF.SECTION_TYPES.PROGBITS) section += lief.ELF.SECTION_FLAGS.EXECINSTR section += lief.ELF.SECTION_FLAGS.ALLOC section.content = data binary.add(section, loaded=True) binary.write("simple.testme.lief")
Изменить секцию
objcopy
objcopy позволяет заменить содержимое секции данными из файла, а также изменить виртуальный адрес секции и флаги:
$ objcopy --update-section .testme=patch.bin \ --change-section-address .testme=0x08999999 simple simple.testme.elf
LIEF
import lief binary = lief.parse("simple") data = bytearray(b"\xFF" * 17) section = binary.get_section(".text") section.content = data binary.write("simple.patched")
Удалить секцию
objcopy
objcopy позволяет удалить определённую секцию по имени:
$ objcopy --remove-section .testme simple.testme.elf simple.no_testme.elf
LIEF
Удаление секции с использованием библиотеки LIEF выглядит так:
import lief binary = lief.parse("simple.testme.elf") binary.remove_section(".testme") binary.write("simple.no_testme")
Эльф-контейнер
Рецепт навеян статьёй Гремлины и ELFийская магия: а что, если ELF-файл — это контейнер?. Встречаются также man’ы про утилиту elfwrap родом из Solaris, которая позволяет создавать ELF-файл из произвольных данных, а формат ELF используется просто как контейнер.
Попробуем сделать то же самое на Python и LIEF.
К сожалению, на данный момент библиотека LIEF не умеет создавать эльф-файл c нуля, поэтому нужно ей помочь — создать пустой ELF-шаблон:
$ echo "" | gcc -m32 -fpic -o empty.o -c -xc - $ gcc -m32 -shared -o libempty.so empty.o
Теперь можно использовать этот шаблон для наполнения данными:
import lief binary = lief.parse("libempty.so") filename = "crackme.zip" data = open(filename, 'rb').read() # Add section with zip-archive as content section = lief.ELF.Section() section.content = data section.name = ".%s"%filename binary.add(section, loaded=True) # Add symbol as a reference to zip-archive symb = lief.ELF.Symbol() symb.type = lief.ELF.SYMBOL_TYPES.OBJECT symb.binding = lief.ELF.SYMBOL_BINDINGS.GLOBAL symb.size = len(data) symb.name = filename symb.value = section.virtual_address binary.add_static_symbol(symb) binary.write("libdata.crackme.container")
Эльф «с прицепом»
ELF-формат не накладывает ограничений на данные, которые есть в файле, но не входят ни в один сегмент. Таким образом, можно создать исполняемый файл, у которого после ELF-структуры будет храниться что-то. Это что-то не будет загружаться в ОЗУ при исполнении, но оно будет записано на диске, и в любой момент это что-то можно с диска прочитать.
- IDA Pro не будет учитывать эти данные при анализе
Пример структуры файла «с прицепом»
radare2
Наличие «прицепа» можно установить, если сравнить реальный и вычисленный размер файла:
$ radare2 test.elf [0x00001040]> ?v $s 0x40c1 [0x00001040]> iZ 14699
readelf
readelf не показывает информацию о наличии «прицепа», но можно вычислить вручную:
$ ls -l test.elf # Размер файла 16577 байт $ readelf -h test.elf Start of section headers e_shoff 14704 Size of section headers e_shentsize 64 Number of section headers e_shnum 29 # Размер ELF-структуры: e_shoff + ( e_shentsize * e_shnum ) = 16560
LIEF
Библиотека LIEF позволяет как проверить наличие «прицепа», так и добавить его. С использованием LIEF всё выглядит достаточно лаконично:
import lief binary = lief.parse("test") # check if overlay exists print('ELF has overlay data') if binary.has_overlay else print("No overlay data") # add overlay data to ELF data = bytearray(b'\xFF'*17) binary.overlay = data binary.write('test.overlay')
Эльф из пустоты (ELF from scratch)
На просторах интернета можно найти проекты по созданию ELF-файла «вручную» — без использования компилятора и линковщика под общим названием «ELF from scratch»:
- Проект на Github
- Статья Elf from scratch
- Ветка elf_from_scratch в репозитории библиотеки LIEF
Знакомство с этими проектами благотворно влияет на впитывание в себя формата ELF.
Самый маленький эльф
Интересные эксперименты с минимизацией размера эльфа описаны в статьях:
- A Whirlwind Tutorial on Creating Really Teensy ELF Executables for Linux
- A Whirlwind Tutorial on Creating Somewhat Teensy ELF Executables for Linux
- Минималистичная программа в формате ELF
Если кратко, загрузчик эльфа в ОС использует далеко не все поля заголовка и таблицы сегментов, при этом некоторый минимальный исполняемый код можно поместить прямо в структуру заголовка ELF’а (код взят из первой статьи):
; tiny.asm BITS 32 org 0x00010000 db 0x7F, "ELF" ; e_ident dd 1 ; p_type dd 0 ; p_offset dd $ ; p_vaddr dw 2 ; e_type ; p_paddr dw 3 ; e_machine dd _start ; e_version ; p_filesz dd _start ; e_entry ; p_memsz dd 4 ; e_phoff ; p_flags _start: mov bl, 42 ; e_shoff ; p_align xor eax, eax inc eax ; e_flags int 0x80 db 0 dw 0x34 ; e_ehsize dw 0x20 ; e_phentsize db 1 ; e_phnum ; e_shentsize ; e_shnum ; e_shstrndx filesize equ $ - $
Ассемблируем и получаем ELF размером… 45 байт:
$ nasm -f bin -o a.out tiny.asm $ chmod +x a.out $ ./a.out ; echo $? 42 $ wc -c a.out 45 a.out
Эльф по шаблону
Для создания эльфа с использованием библиотеки LIEF можно сделать следующие шаги (см. рецепт «Эльф-контейнер»):
- взять простой ELF-файл в качестве шаблона;
- заменить содержимое секций, добавить новые секции;
- настроить необходимые параметры (точка входа, флаги).
Вместо заключения
Дописывая статью, обнаружили, что получилось что-то вроде оды библиотеке LIEF. Но так не было запланировано — хотелось показать способы работы с ELF-файлами с использованием разных инструментов.
Наверняка есть или нужны сценарии, которые не были упомянуты здесь — напишите об этом в комментариях.
Ссылки и литература
- Спецификация формата ELF
- Ещё спецификация формата в библиотеке Oracle
- Работа с ELF’ами с использованием radare2
- Документация библиотеки LIEF
- Примеры использования библиотеки LIEF
- Книга «PRACTICAL BINARY ANALYSIS», Dennis Andriesse
- Книга «Learning Linux Binary Analysis», Ryan "elfmaster" O'Neill
Автор: https://habr.com/ru/users/prusanov/