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

    image


    На русском языке довольно мало информации про то, как работать с 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


    Спасибо 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:



    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-файлами с использованием разных инструментов.


    Наверняка есть или нужны сценарии, которые не были упомянуты здесь — напишите об этом в комментариях.


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


    ИНФОРИОН
    31.44
    Решения ИТ-инфраструктуры и защита информации
    Share post

    Comments 19

      +3

      Спасибо большое, в закладки!

        0
        В большинстве случаев примеры можно выполнить как на Linux, так и на Windows

        А MacOS нет?)
        Пока в маках столкнулся с тем что strip требует другие опции. Ну и идеология у утилиты немного другая.
        objcopy позволяет добавить секцию, но эта секция не будет относиться ни к одному сегменту и не будет загружаться в ОЗУ при запуске приложения

        А есть ли простые способы из загружаемого кода библиотеки добраться до этой секции? Может быть можно включить загрузку этой секции?
          +1

          С маками все печально, это одна из причин по которой я пересел на линукс. В брю либо старье, либо нет, либо просто не ставится. А добраться до секции элементарно, просто парсингом самого файла.

            0
            А добраться до секции элементарно, просто парсингом самого файла.

            Это если знаешь где файл находится. А библиотека может лежать где угодно, да и бинарь приложения тоже. Поэтому интересны способы загрузки по умолчанию доп секций.
            +1
            А MacOS нет?)

            А слона-то я и не заметил. Объективно — не было под рукой Мака, чтобы всё полностью проверить.


            И как уже отметили, в Маках бывают какие-то странности в binutils.


            Из простых способов — воспользоваться LIEF — ей ничего кроме Python не надо.
            К сожалению, objcopy "не обладает коровьей суперсилой" и даже если указать флаги --set-section-flags .section=alloc,load, сегмент от этого не отрастёт.
            Ну или писать что-то своё с использованием libelf.

            0
            Интересно, насколько сложно реализовать (хотя бы спеки) ELF для новой (старой) архитектуры? Представьте, есть 32 архитектура (заброшенная) для которой был a.out (было в binutils), был COFF (в binutils даже не было запилено) но нету ELF. То есть не удалось найти даже следов операционки с поддержкой ELF. Причем под нее даже номер архитектуры EMxxx был выделен.
              0
              Если она заброшена, что вы хотите от нее получить?)
                +1
                Как что? Олдфажное техно-порно конечно. Для DEC VAX есть ELF, для Motorola 68000 есть. А этой нет.
                0

                Правильно понимаю, что ОСь есть, но elf-loader'а нет? То есть задача — написать elf-loader под эту ОСь?
                По идее, не должно быть запредельно сложно — взять спеку по ELF, посмотреть "кишки" ОСи, объединить их (понимаю, что это звучит как ответ на вопрос "как нарисовать сову?").

                  +1
                  Нету даже полной спеки на ELF для этой архитектуры. И нету ни одного примера ELF файла для нее.

                  Архитектура: ns32k. Есть бесплатная реализация на HDL/FPGA, вполне бодрая и рабочая, 25K LE. Так что процессоры доступны. Последняя живая ОС под нее была NetBSD 1.5x, формат exe-шника a.out. Следующая NetBSD 1.6 уже выпилила эту архитектуру. Параллельно существовала также фирменная System V R3 под NS32000, с exe-шниками COFF. Как мы знаем, ELF (как механизм ОС) появился в System V R4 (влившись туда из работ SUN над прототипом Solaris c динамически загружаемыми библиотеками DSO). Системы Systtem V R4 in wild под NS32000 обнаружить не удалось… Хотя, кто знает, может где-то в архивах и лежит ленточка…

                  Познакомится можно тут:
                  www.cpu-ns32k.net
                –2

                Детский сад какой-то. Лучше бы начали с (неполного) описания ELF и соответствующих опций ld, потом механизма загрузки (включая ifunc) и затем dwarf3.


                Отсылка к "библиотеке Oracle" тоже доставляет )

                  +1

                  Вы в теме больше чем автор. Пилите статью ;)

                    +3
                    Лучше бы для кого, конкретно для вас? Вся информация, приведенная в статье бесполезна или неправильна? Исходя из вашей логики, любой cheat-sheet или учебник бесполезен. Более того, неполных (и полных) описаний эльфов достаточно много, нужно yet another one описание? Таких же cheat-sheet, как данная статья я не видел, чем она и примечательна. Автор, свою крутость явно показать не хотел. Если есть что рассказать о глубинах эльфов со всеми возможными вариантами динамической релокации для популярных процессорных архитектур (ARM, MIPS, x86, x86, PPC) со всеми их тонкостями и best-practice, будет очень интересно почитать. Автор же привел обзор тулов, которые работают с эльфами, чтобы для тех кому встретится такая задача, мог дальше выбрать тот инструмент, который подходит для его задачи. Кстати, сказать, многие программные библиотеки крайне плохо работают с эльфами и могут пропускать секции, что-то не поддерживать, в итоге работа превращается в сплошной ад. Хардкор есть в статьях по реверс-инжинирнигу, там и железки, и РТОС, и ВПО и немного эльфов)))
                      +3
                      Ценность статьи в систематизации знаний, а не в их уникальности.
                      Давайте будем грамотно критиковать и давать обратную связь.
                        0

                        Мне обычно неловко в ситуации, когда у оппонента есть "своё правильное мнение". Выше немного опередели с комментариями — если понимаете ЧТО действительно нужно, напишите статью.


                        Рад, что Вам "доставила" библиотека Oracle. А можно уточнить, что с ней не так? Можете порекомендовать хороший ресурс?

                          +1
                          Статья называется «Рецепты для ELFов» и вполне соответствует содержимому.
                          Мне лично было интересно узнать несколько новых инструментов.

                          Что касается описания ELF — это тема тяжелая и там надо мини-книжку писать.
                          У меня впечатление, что ELF на порядок сложнее современного PE.
                          Минимальная работоспособная ручная загрузка PE (VirtualAlloс, релоки, импорты) доступна даже студенту.
                          А вот ELF…
                            0
                            Так PE это для исполняемых файлов. А эльф более универсальный формат
                          +1

                          Небольшое добавление к статье. Возможности checksec (и даже больше) уже есть в упомянутом инструменте radare2 — https://github.com/radareorg/radare2/issues/818. Информация о файле (в том числе mitigations) выводится командой i. Или можно так i~pic,canary,nx,crypto,stripped,static,relocs

                            0

                            Большое спасибо!
                            Дополнил статью этим рецептом!

                          Only users with full accounts can post comments. Log in, please.