На русском языке довольно мало информации про то, как работать с 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
ELF Header:
Magic: 7f 45 4c 46 02 01 01 00 00 00 00 00 00 00 00 00
Class: ELF64
Data: 2's complement, little endian
Version: 1 (current)
OS/ABI: UNIX - System V
ABI Version: 0
Type: DYN (Shared object file)
Machine: Advanced Micro Devices X86-64
Version: 0x1
Entry point address: 0x1070
Start of program headers: 64 (bytes into file)
Start of section headers: 14800 (bytes into file)
Flags: 0x0
Size of this header: 64 (bytes)
Size of program headers: 56 (bytes)
Number of program headers: 11
Size of section headers: 64 (bytes)
Number of section headers: 30
Section header string table index: 29
- Прочитать информацию о сегментах и секциях:
$ readelf -l -W simple
Для удобства чтения адреса приведены к 32-битному формату:
Elf file type is DYN (Shared object file)
Entry point 0x1070
There are 11 program headers, starting at offset 64
Program Headers:
Type Offset VirtAddr PhysAddr FileSiz MemSiz Flg Align
PHDR 0x000040 0x00000040 0x00000040 0x000268 0x000268 R 0x8
INTERP 0x0002a8 0x000002a8 0x000002a8 0x00001c 0x00001c R 0x1
[Requesting program interpreter: /lib64/ld-linux-x86-64.so.2]
LOAD 0x000000 0x00000000 0x00000000 0x0005f8 0x0005f8 R 0x1000
LOAD 0x001000 0x00001000 0x00001000 0x00026d 0x00026d R E 0x1000
LOAD 0x002000 0x00002000 0x00002000 0x0001b8 0x0001b8 R 0x1000
LOAD 0x002de8 0x00003de8 0x00003de8 0x000258 0x000260 RW 0x1000
DYNAMIC 0x002df8 0x00003df8 0x00003df8 0x0001e0 0x0001e0 RW 0x8
NOTE 0x0002c4 0x000002c4 0x000002c4 0x000044 0x000044 R 0x4
GNU_EH_FRAME 0x002070 0x00002070 0x00002070 0x00003c 0x00003c R 0x4
GNU_STACK 0x000000 0x00000000 0x00000000 0x000000 0x000000 RW 0x10
GNU_RELRO 0x002de8 0x00003de8 0x00003de8 0x000218 0x000218 R 0x1
Section to Segment mapping:
Segment Sections...
00
01 .interp
02 .interp .note.ABI-tag .note.gnu.build-id .gnu.hash .dynsym .dynstr .gnu.version .gnu.version_r .rela.dyn .rela.plt
03 .init .plt .plt.got .text .fini
04 .rodata .eh_frame_hdr .eh_frame
05 .init_array .fini_array .dynamic .got .got.plt .data .bss
06 .dynamic
07 .note.ABI-tag .note.gnu.build-id
08 .eh_frame_hdr
09
10 .init_array .fini_array .dynamic .got
- Прочитать информацию о секциях:
$ readelf -S -W simple
Для удобства чтения адреса приведены к 32-битному формату:
There are 30 section headers, starting at offset 0x39d0:
Section Headers:
[Nr] Name Type Address Off Size ES Flg Lk Inf Al
[ 0] NULL 00000000 000000 000000 00 0 0 0
[ 1] .interp PROGBITS 000002a8 0002a8 00001c 00 A 0 0 1
[ 2] .note.ABI-tag NOTE 000002c4 0002c4 000020 00 A 0 0 4
[ 3] .note.gnu.build-id NOTE 000002e4 0002e4 000024 00 A 0 0 4
[ 4] .gnu.hash GNU_HASH 00000308 000308 000024 00 A 5 0 8
[ 5] .dynsym DYNSYM 00000330 000330 0000d8 18 A 6 1 8
[ 6] .dynstr STRTAB 00000408 000408 0000a2 00 A 0 0 1
[ 7] .gnu.version VERSYM 000004aa 0004aa 000012 02 A 5 0 2
[ 8] .gnu.version_r VERNEED 000004c0 0004c0 000030 00 A 6 1 8
[ 9] .rela.dyn RELA 000004f0 0004f0 0000c0 18 A 5 0 8
[10] .rela.plt RELA 000005b0 0005b0 000048 18 AI 5 23 8
[11] .init PROGBITS 00001000 001000 000017 00 AX 0 0 4
[12] .plt PROGBITS 00001020 001020 000040 10 AX 0 0 16
[13] .plt.got PROGBITS 00001060 001060 000008 08 AX 0 0 8
[14] .text PROGBITS 00001070 001070 0001f2 00 AX 0 0 16
[15] .fini PROGBITS 00001264 001264 000009 00 AX 0 0 4
[16] .rodata PROGBITS 00002000 002000 000070 00 A 0 0 8
[17] .eh_frame_hdr PROGBITS 00002070 002070 00003c 00 A 0 0 4
[18] .eh_frame PROGBITS 000020b0 0020b0 000108 00 A 0 0 8
[19] .init_array INIT_ARRAY 00003de8 002de8 000008 08 WA 0 0 8
[20] .fini_array FINI_ARRAY 00003df0 002df0 000008 08 WA 0 0 8
[21] .dynamic DYNAMIC 00003df8 002df8 0001e0 10 WA 6 0 8
[22] .got PROGBITS 00003fd8 002fd8 000028 08 WA 0 0 8
[23] .got.plt PROGBITS 00004000 003000 000030 08 WA 0 0 8
[24] .data PROGBITS 00004030 003030 000010 00 WA 0 0 8
[25] .bss NOBITS 00004040 003040 000008 00 WA 0 0 1
[26] .comment PROGBITS 00000000 003040 00001c 01 MS 0 0 1
[27] .symtab SYMTAB 00000000 003060 000630 18 28 44 8
[28] .strtab STRTAB 00000000 003690 000232 00 0 0 1
[29] .shstrtab STRTAB 00000000 0038c2 000107 00 0 0 1
Key to Flags:
W (write), A (alloc), X (execute), M (merge), S (strings), l (large)
I (info), L (link order), G (group), T (TLS), E (exclude), x (unknown)
O (extra OS processing required) o (OS specific), p (processor specific)
- Прочитать информацию о символах:
$ readelf -s -W simple
Вывод сокращён для удобства чтения:
Symbol table '.dynsym' contains 9 entries:
Num: Value Size Type Bind Vis Ndx Name
0: 00000000 0 NOTYPE LOCAL DEFAULT UND
1: 00000000 0 NOTYPE WEAK DEFAULT UND _ITM_deregisterTMCloneTable
2: 00000000 0 FUNC GLOBAL DEFAULT UND puts@GLIBC_2.2.5 (2)
3: 00000000 0 FUNC GLOBAL DEFAULT UND printf@GLIBC_2.2.5 (2)
4: 00000000 0 FUNC GLOBAL DEFAULT UND __libc_start_main@GLIBC_2.2.5 (2)
5: 00000000 0 NOTYPE WEAK DEFAULT UND __gmon_start__
6: 00000000 0 FUNC GLOBAL DEFAULT UND __isoc99_scanf@GLIBC_2.7 (3)
7: 00000000 0 NOTYPE WEAK DEFAULT UND _ITM_registerTMCloneTable
8: 00000000 0 FUNC WEAK DEFAULT UND __cxa_finalize@GLIBC_2.2.5 (2)
Symbol table '.symtab' contains 66 entries:
Num: Value Size Type Bind Vis Ndx Name
0: 00000000 0 NOTYPE LOCAL DEFAULT UND
1: 000002a8 0 SECTION LOCAL DEFAULT 1
2: 000002c4 0 SECTION LOCAL DEFAULT 2
3: 000002e4 0 SECTION LOCAL DEFAULT 3
4: 00000308 0 SECTION LOCAL DEFAULT 4
5: 00000330 0 SECTION LOCAL DEFAULT 5
6: 00000408 0 SECTION LOCAL DEFAULT 6
7: 000004aa 0 SECTION LOCAL DEFAULT 7
....
26: 00000000 0 SECTION LOCAL DEFAULT 26
27: 00000000 0 FILE LOCAL DEFAULT ABS crtstuff.c
28: 000010a0 0 FUNC LOCAL DEFAULT 14 deregister_tm_clones
29: 000010d0 0 FUNC LOCAL DEFAULT 14 register_tm_clones
30: 00001110 0 FUNC LOCAL DEFAULT 14 __do_global_dtors_aux
31: 00004040 1 OBJECT LOCAL DEFAULT 25 completed.7389
....
Опция -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
simple: file format elf64-x86-64
Contents of section .comment:
0000 4743433a 20284465 6269616e 20382e32 GCC: (Debian 8.2
0010 2e302d39 2920382e 322e3000 .0-9) 8.2.0.
readelf
$ readelf -p .comment simple
String dump of section '.comment':
[ 0] GCC: (Debian 8.2.0-9) 8.2.0
$ readelf -n simple
Displaying notes found at file offset 0x000002c4 with length 0x00000020:
Owner Data size Description
GNU 0x00000010 NT_GNU_ABI_TAG (ABI version tag)
OS: Linux, ABI: 3.2.0
Displaying notes found at file offset 0x000002e4 with length 0x00000024:
Owner Data size Description
GNU 0x00000014 NT_GNU_BUILD_ID (unique build ID bitstring)
Build ID: dae0509e4edb79719a65af37962b74e4cf2a8c2e
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
Для удобства чтения результат команды сокращён:
Dynamic section at offset 0x2dd8 contains 28 entries:
Tag Type Name/Value
0x0000000000000001 (NEEDED) Shared library: [libbubble.so]
0x0000000000000001 (NEEDED) Shared library: [libc.so.6]
0x000000000000000f (RPATH) Library rpath: [/run/media/pablo/disk1/projects/cheat_sheets/ELF/lib/]
0x000000000000000c (INIT) 0x1000
0x000000000000000d (FINI) 0x11c8
....
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.
LIEF
Можно сделать свой аналог на коленке 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')
Radare2
Спасибо dukebarman за дополнение по использованию Radare2 для вывода информации аналогично checksec:
> r2 -c i~pic,canary,nx,crypto,stripped,static,relocs test_stack_proteck
«Сырой код» из эльфа (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