_ .-') _     ('-.   ('-.     _ .-') _        .-. .-')               .-') _     ('-.    .-')
( (  OO) )  _(  OO) ( OO ).-.( (  OO) )       \  ( OO )             (  OO) )  _(  OO)  ( OO ).
 \     .'_ (,------./ . --. / \     .'_        ;-----.\  ,--.   ,--./     '._(,------.(_)---\_)
 ,`'--..._) |  .---'| \-.  \  ,`'--..._)       | .-.  |   \  `.'  / |'--...__)|  .---'/    _ |
 |  |  \  ' |  |  .-'-'  |  | |  |  \  '       | '-' /_).-')     /  '--.  .--'|  |    \  :` `.
 |  |   ' |(|  '--.\| |_.'  | |  |   ' |       | .-. `.(OO  \   /      |  |  (|  '--.  '..`''.)
 |  |   / : |  .--' |  .-.  | |  |   / :       | |  \  ||   /  /\_     |  |   |  .--' .-._)   \
 |  '--'  / |  `---.|  | |  | |  '--'  /       | '--'  /`-./  /.__)    |  |   |  `---.\       /
 `-------'  `------'`--' `--' `-------'        `------'   `--'         `--'   `------' `-----'
                                                                                  ~ xcellerator

[ переклад @IamAlwaysAngry Captain ]

Привіт, це стаття про ELF! У цій статті, ви побачите невелику бібліотеку,
над якою я працював, ім'я її LibGolf. Я хотів розібратися з ELF і заголовками, а вийшов
практичний інструмент. Це дозволяє дуже легко згенерувати двійковий файл, який складається
з заголовка ELF, за ним йде один заголовок програми, і за яким слідкує ще один завантажуваний
сегмент. Ще раз, дуже легко згенерувати двійковий файл, що складається з заголовка ELF, за яким
слідує заголовок простої програми, де далі завантажується сегмент. За замовчуванням всі файли
значення заголовка містять цілі числа, але є простий спосіб змінити їх! Я збираюся
продемонструвати, як я використовую LibGolf, щоб точно вказати, які байти необхідні, а які
ігноруються загрузчиком Linux. На щастя, виявляється, що завантажувач - один з найменш розбірливих
парсеров серед стандартного набору інструментів Linux. Перш ніж ми закінчимо, ми побачимо кілька
популярних статичних інструментів аналізу, які розсипаються перед нашим зіпсованим ELF, в той
час як завантажувач продовжує із задоволенням завантажувати і читати обрані нами байти.

+----------------------+
|--[ Привіт LibGolf ]--|
+----------------------+

Нещодавно я писав ELF в NASM. Хоча якийсь час це і було забавно (і ще, звичайно,
мало свої переваги). Я зрозумів, що мені не вистачало всієї повноти, яка структурувала б
Сі-структури які б пропонувалися. Зокрема, як я впевнений, багато читачів знають,
<Linux / elf.h>, містить цікаві речі, такі як «Elf64_Ehdr» і «Elf32_Phdr»,
готові до оголошення.

Аби не допустити, щоб такі корисні заголовки пропадали задарма, я вирішив узяти їх і 
використовувати з користю. Завдяки цим зусиллям народився libgolf.h, це бібліотека,
яка спрощує додавання шеллкода в функціонуючий виконуваний файл (в функцію). Я знаю,
про що ви думаєте - "це просто жах!", і, можливо, ви маєте рацію. Однак що тут добре,
так це те, що ви можете легко змінити заголовки *перед* побудовою виконуваного файлу.

Давайте подивимося, як це працює. Якщо ви хочете пограти вдома, ви можете знайти вихідний код
для всього цього на [0]. Ви можете знайти код в цій статті в розділі «examples / 01_dead_bytes».
Для базової установки потрібні два файли; вихідний файл на мові C і файл shellcode.h.
Що стосується шелл-коду, то це старий добрий 'b0 3c 48 31 ff 0f 05', який розбирається на:

        mov al, 0x3c    @ b0 3c
        xor rdi, rdi    @ 48 31 ff
        syscall         @ 0f 05

(Так - виконання цього коду робить пуш!)

Це просто викликає exit (0). Це нормально, тому що дозволяє нам легко перевірити, що
ці байти були успішно виконані з розширенням оболонки $ ?.

Використовуй цей або який-небудь інший шеллкод (але переконайтеся, що це PIC - поки немає
підтримки переміщуються символів!) в буфер з ім'ям buf [] в shellcode.h і поверніться до
файлу C. Якщо ти просто хотів отримати двійковий файл, який виконує ваш шеллкод, то це все,
що вам потрібно:

        #include "libgolf.h"
        #include "shellcode.h"

        int main(int argc, char **argv)
        {
            INIT_ELF(X86_64,64);

            GEN_ELF();
            return 0;
        }

Компіляція цього і запуск отриманого файлу згенерує вам файл .bin - це
твій новий блискучий ELF! Досить просто, чи не так? Але простота зазвичай супроводжується тим, що
нудно, так що давайте займемося чимось цікавішим!

Перш ніж продовжити, варто пояснити, що ці два макроси роблять за кулісами. Перший
INIT_ELF () приймає два аргументи: набір інструкцій ISA і архітектуру. В даний момент,
LibGolf підтримує X86_64, ARM32 і AARCH64 як дійсних наборів інструкцій ISA і
архітектури 32 або 64. Спочатку налаштуйте деякі структури внутрішнього контролю і вирішите,
використовувати об'єкт Elf32_ * або Elf64_ * для заголовків. Він також автоматично призначає
покажчики на ELF і заголовки програм з іменами ehdr і phdr відповідно. Саме їх ми будемо
використовувати, щоб легко змінювати поля. Крім цього, це також копіює буфер шеллкода і
заповнює заголовки ELF і програми перед обчисленням інтегральної точки входу. Тепер йде
GEN_ELF (), який просто друкує деяку статистику в stdout, а потім записує
відповідні структури в .bin файлі. Ім'я .bin визначається argv [0].

Отже, після використання макросу INIT_ELF () у нас є ehdr і phdr, доступні для перейменування.
Припустимо, ми хочемо змінити поле e_version заголовка ELF.
Що нам потрібно зробити, так це додати один рядок:

        #include "libgolf.h"
        #include "shellcode.h"

        int main(int argc, char **argv)
        {
            INIT_ELF(X86_64);

            // Встановіть для e_version значення 12345678
            ehdr->e_version = 0x78563412;

            GEN_ELF();
            return 0;
        }

Ще одна швидка компіляція, і вас чекає ще один .bin-файл. Поглянувши
в цьому файлі в xxd, hexyl або в вашому улюбленому маніпуляторі bin ви побачите досить маленький
'12 34 56 78 ' яке визирає на початку 0x14. Хіба це не було просто?

Щоб прискорити роботу, я використовую наступний Makefile:

        .PHONY golf clean

        CC=gcc
        CFLAGS=-I.
        PROG=golf

        golf:
        	@$(CC) -o $(PROG) $(PROG).c
        	@./$(PROG)
        	@chmod +x $(PROG).bin

        	@rm $(PROG) $(PROG).bin

(Це Makefile, який ви знайдете в репозиторії [0])

+------------------------------------+
|--[ Падіння при першій перешкоді ]--|
+------------------------------------+

Як багато хто з вас знає, парсери файлів - жахлива річ. Хоча специфікації
зазвичай мають серйозні цілі, їх рідко поважають ті, хто нібито знає більше, ніж звичайно.
Головний серед цих бандитів є сам завантажувач ELF в Linux. LibGolf спрощує пошук
масштабів цих злочинів проти elf.h.

Гарне місце для початку - це початок, тобто заголовок ELF. На початку будь-якого файлу ELF варто
зрозуміти, знайомий нам 0x7f, за яким слідує ELF, відомий своїм друзям як EI_MAG0 через EI_MAG3.
Не дивно, що зміна будь-якого з цих чотирьох байтів призводить до того, що завантажувач Linux
відхиляє файл. Дякуватимо Богові за це!

А як щодо байта 0x5? Наша надійна специфікація каже нам, що це байт EI_CLASS і позначає
цільову архітектуру. Можна вибрати зі значень 0x01 і 0x02 для 32- і 64-розрядних версій відповідно.
Ще раз: допустимі значення 0x01 і 0x02. Що, якщо ми встановимо його в 0x58
(Або в «X» для любителів ASCII)? Ми можемо зробити це, додавши:

        (ehdr->e_ident)[EI_CLASS] = 0x58;

в наш згенерований файл C. (Чому 0x58? Тому що він чітко ідентифікується на виході
з xxd / hexyl!)

Спершу використовуй парсери для пошуку проблеми ділянок, перш ніж намагатися відразу запустити .bin,
давай спробуємо ще пару знайомих інструментів синтаксичного аналізу ELF в пошуках подальших
ілюзій. Першим в списку йде gdb. Бачите, що відбувається?

        "not in executable format: file format not recognized"
		"Не в виконуваному форматі: формат файлу не розпізнано"
	
Точно так же objdump дасть вам аналогічну відповідь. Здається, ці парсери роблять свою справу
належним чином. Тепер давайте спробуємо запустити двійковий файл як зазвичай:

        <spoiler>It works perfectly.</spoiler>
		<spoiler>Працює відмінно.</spoiler>
	
Якщо ви використовуєте мій приклад шеллкода, то проконсультуйтеся з $? з жалем повідомить вам, що
двійковий файл успішно завершився. Ті ж злочини скоюються при установці EI_DATA і EI_VERSION до
неприпустимих значень теж.

+-------------------------+
|--[ Перетворення в 11 ]--|
+-------------------------+

Отже, як далеко ми можемо зайти? Яку частину заголовків ELF і програм завантажувач Linux проігнорує?
Ми вже розглянули EI_CLASS, EI_DATA і EI_VERSION, але виявилося, що EI_OSABI також
безпечно ігнорувати. Це підводить нас до зміщення 0x8. Згідно зі специфікацією, наступними йдуть
EI_ABIVERSION і EI_PAD, які разом ведуть нас до байту 0xf. Здається, ніхто не дбає про них,
тому ми може без страху встановити для всіх 0x58.

Просуваючись далі, ми стикаємося з областю, яка, здається, не схильна до ніякому впливу:
e_type. Зрозуміло, що завантажувачу Linux не подобається, якщо ми не повідомляємо йому, який
це тип файлу ELF ми надаємо йому (приємно знати, що у нього є * якісь * стандарти!).
Нам потрібно, щоб ці два байта залишалися 0x0002 (або ET_EXEC для elf.h дій).
Нижче ще один метушливий байт у відомому зміщенні 0x12: e_machine, який позначає набір
цільових інструкцій ISA. наскільки нам відомо, в специфікації X86_64 це відповідає
до першого аргументу INIT_ELF (), цей байт вже був заповнений 0x3e за допомогою LibGolf.

Раптово з'явилася e_version! Перед нами ще один дисидент, який нібито повинен
завжди бути байтами 0x00000001. Однак на практиці це, схоже, нікому не цікаво,
тому давайте встановимо його на 0x58585858.

Слідом за цим ланцюжком єретиків у нас є пара важливих полів, які чинят опір, щоб їх не
зіпсували; e_entry і e_phoff. Я впевнений, що мені не потрібно вдаватися в подробиці про e_entry;
Це точка входу в двійковий файл, де виконання в кінцевому підсумку передається одного разу до
завантажуваних розділах знаходяться у пам'яті. Хоча можна сподіватися, що завантажувач(лоадер)
зможе впораться, не знаючи, яке зміщення заголовків програм, здається що він недостатньо розумний,
щоб вирішити всі самому тому потрібен помічник. Краще залиш цих двох у спокої.

LibGolf ще не підтримує заголовки розділів (і, з огляду на його орієнтацію на створення *невеликих*
двоічних файлів, чи буде підтримка їх в майбутньому ніхто не зна). Це означає, що, зіткнувшись
з заголовками, що відносяться до них ми можемо грати скільки душі завгодно. Сюди входять e_shoff,
e_shentsize, eh_shnum і навіть e_shstrndx. Якщо у нас немає заголовків розділів, ми не можемо
пошкодити того чого у нас немає.

Решта полів, які, мабуть, мають якесь значення для завантажувача Linux, це e_ehsize,
e_phentsize, і e_phnum. Знову ж таки, це не дуже дивно, оскільки вони стурбовані завантаженням
єдиного loadabale сегмента в пам'ять перед передачею керування. Якщо вам потрібно нагадати,
e_ehsize - це розмір заголовка ELF (0x34 або 0x40 для 32- і 64-бітних відповідно), eh_phentsize
- розмір заголовка майбутньої програми (знову ж, жорстко заданий 0x20 або 0x38 для 32- і
64-бітові архітектури). Якби завантажувач був більш вимогливий до EI_CLASS, йому б не треба було
ці два поля. Нарешті, e_phnum - це просто кількість записів в заголовку програми - для нас це
завжди 0x1. Без сумніву, це використовується для деякого циклу в процедурах завантаження пам'яті,
але я ще не досліджував її досконально.

У заголовку ELF залишилося одне поле, яке я не торкнувся, - це e_flags. Причина
досить проста, справа в тому, що все залежить від архітектури. Для x86_64 це взагалі не має значення,
тому що він не визначений (хоча це *важливо* для деяких платформ ARM! Погляньте на приклад arm32 в [0]).

Ось так ми отримуємо заголовок ELF. Для тих, хто не веде рахунок, трохи більше 50% ELF
ігнорується загрузчиком. А як щодо заголовків програм? Виявляється,
в заголовках програм менше місця, але не з тієї причини про яку ви подумали.
Дійсно, *будь-яка* зміна в заголовок програми не відіб'ється на завантажувачі Linux.
Ми могли б заповнити всі це нашим вірним 0x58, і завантажувач не буде заперечувати. Але будь
обережний, відважний шукач пригод, маніпулюючи не тим байтом і ти заплутав у помилкову сегментацію!

Отже, чи є що-небудь в заголовку програми, схильне до примусу? З'ясовується, є два
поля, які в даний час неактуальні: p_paddr і p_align. Перше було важливо в ті прекрасні
дні, коли ще не було віртуальної пам'яті, коли 4 ГБ ОЗУ були не більше ніж мрією, тому було важливо
повідомити завантажувачу, куди в фізичну паямть варто завантажувати сегмент.

Вирівнювання пам'яті - кумедна річ. Імовірно, p_vaddr повинен бути рівним p_offset по модулю p_align.
«Правильні» файли ELF (ті, які скомпільовані за допомогою GCC), схоже, просто
встановлюють p_offset рівним p_vaddr і рухаються далі. Це те ж саме, що робить LibGolf за
замовчуванням, роблячи p_align абсолютно непотрібним!

З огляду на всі обставини, як заголовок ELF, але все ж є невелика вигода. Бінарний генерує файл C
тепер виглядає так:

        #include "libgolf.h"
        #include "shellcode.h"

        int main(int argc, char **argv)
        {
            INIT_ELF(X86_64,64);

            /*
             * Ламає звичайні інструменти статичного аналізу, такі як gdb і objdump.
             */
            (ehdr->e_ident)[EI_CLASS]    = 0x58;  // Архітектура
            (ehdr->e_ident)[EI_DATA]     = 0x58;  // Порядок байтів
            (ehdr->e_ident)[EI_VERSION]  = 0x58;  // Імовірно завжди 0x01
            (ehdr->e_ident)[EI_OSABI]    = 0x58;  // Цільова операційна система

            // Цикл для решти e_indent
            int i;
            for ( i = 0 ; i < 0x10 ; i++ )
                (ehdr->e_ident)[i] = 0x58;

            ehdr->e_version = 0x58585858;       // Імовірно завжди 0x00000001

            // Заголовки розділів? Нам не потрібні смердючі заголовки розділів!
            ehdr->e_shoff = 0x5858585858585858;
            ehdr->e_shentsize = 0x5858;
            ehdr->e_shnum = 0x5858;
            ehdr->e_shstrndx = 0x5858;

            ehdr->e_flags = 0x58585858;         // x86_64 не має певних прапорів

            phdr->p_paddr = 0x5858585858585858; // Фізична адреса ігнорується
            phdr->p_align = 0x5858585858585858; // p_vaddr = p_offset, тому це не має значення

            GEN_ELF();
            return 0;
        }

Якщо ви скомпілюєте і запустіть програму, ви отримаєте наступний двійковий файл:

        00000000: 7f45 4c46 5858 5858 5858 5858 5858 5858  .ELFXXXXXXXXXXXX
        00000010: 0200 3e00 5858 5858 7800 4000 0000 0000  ..>.XXXXx.@.....
        00000020: 4000 0000 0000 0000 5858 5858 5858 5858  @.......XXXXXXXX
        00000030: 5858 5858 4000 3800 0100 5858 5858 5858  XXXX@.8...XXXXXX
        00000040: 0100 0000 0500 0000 0000 0000 0000 0000  ................
        00000050: 0000 4000 0000 0000 5858 5858 5858 5858  ..@.....XXXXXXXX
        00000060: 0700 0000 0000 0000 0700 0000 0000 0000  ................
        00000070: 5858 5858 5858 5858 b03c 4831 ff0f 05    XXXXXXXX.<H1...

Цей файл має розмір 127 байт, але ми змогли замінити в цілому 50 байтів на 'X',
що означає трохи менше 40% цього двійкового файлу ігнорується загрузчиком Linux ELF!
Хто знає, що ви могли б зробити з 50 байтами?

Виявляється - досить багато. Кілька років тому неймовірне розслідування netspooky
продемонструвало, як можна складати частині заголовка програми в заголовок ELF.
Скомбінований з вашому шелл-кодом в одній з цих областей мертвих байтів і декількома
іншими фокусами, може зменшити ELF до 84 байтів.
Я рекомендую почитати "ELF Mangling" на [1].

Інший цікавий аспект цих методів легко випустити з уваги. Завантажувач
Linux, мало піклується про структуру ELF, йому просто потрібно отримати
машинний код, інші інструменти набагато більш перебірливі. Ми вже подивилися на objdump і gdb,
але багато антивірусних рішень не справляються з пошкодженим ELF. У моєму дослідженні
єдиний продукт, який (більш-менш) досягає цього, - ClamAV, з позитивним
результатом для «Heuristics.Broken.Executable». Звичайно, все роблять ставки на динамічний аналіз.

+----------------------+
|--[ Рухаємося далі ]--|
+----------------------+

x86_64 - не єдина ISA, підтримувана LibGolf! Ви також можете використовувати його для
створення крихітних виконуваних файлів для платформи ARM32 і AARCH64 теж. У репозиторії на
GitHub [0] ви знайдете кілька прикладів для ARM платформи (включаючи мертві байти з цієї статті). 

Але до біса приклади! Сподіваюся, більшість з вас, які зайшли так далеко, захочуть поглянути на
libgolf.h сам. Як я вже згадував на початку, все це починалося як навчальне вправу,
тому я приділяв особливу увагу тому, щоб коментувати речі як можна більш детально.

+----------------+
|--[ Примітка ]--|
+----------------+

В ході цього дослідження я в основному тестував Ubuntu 20.04 з ядром 5.4.0-65-generic, але також
перевірив, що такі ж результати можуть бути отримані на 5.11.11-arch1-1. Я чув
що в ядрах WSL можуть відбуватися дивні речі, але я не досліджував це - може бути, ви зможете!


+--------------+
|--[ Подяки ]--|
+--------------+

Особливе "ahoy" усім Thugcrowd, Symbolcrash, та the Mental ELF Support Group!

+-----------------+
|--[ Посилання ]--|
+-----------------+

[0] https://www.github.com/xcellerator/libgolf
[1] https://n0.lol/ebm/1.html