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

Статья Рецепты для ELFов

baykal

(L2) cache
Пользователь
Регистрация
16.03.2021
Сообщения
370
Реакции
838
На русском языке довольно мало информации про то, как работать с 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, но подойдёт любой представитель «эльфийского» семейства. Если готовый файл с требуемыми характеристиками не был найден в свободном доступе, то будет приведён способ создания такого эльфа.

«Свободных» эльфов можно также найти по ссылкам:

Чтение, получение информации​

Тип файла, заголовок, секции​

В зависимости от задачи интерес могут представлять:
  • тип файла (DYN — библиотека, EXEC — исполняемый, RELOC — линкуемый);
  • целевая архитектура (E_MACHINE — x86_64, x86, ARM и т.д.);
  • точка входа в приложение (Entry Point);
  • информация о секциях

010Editor​

HEX-редактор 010Editor предоставляет систему шаблонов. Для ELF-файлов шаблон называется, как ни странно, ELF.bt и находится в категории Executable (меню Templates — Executable).
Интерес может представлять, например, точка входа в исполняемый файл (entry point) (записана в заголовке файла).
image

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")
Почитать про секцию .dynamic

Проверка эльфа на безопасность​


Скрипт проверки безопасности 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​

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 liefbinary = 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:

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 не будет учитывать эти данные при анализе
Пример структуры файла «с прицепом»
image


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»:
Знакомство с этими проектами благотворно влияет на впитывание в себя формата 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-файлами с использованием разных инструментов.

Ссылки и литература​


Автор Павел Русанов @prusanov
 


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