Зображення-програми: експеримент з запуском бінарників у PNG

03.10.2025 2 хвилин Автор: Lady Liberty

Виконувані PNG — це експериментальна технологія, яка поєднує стеганографію та можливості Linux, дозволяючи перетворити звичайне зображення на програму. За допомогою спеціальних інструментів можна вбудувати бінарний код у PNG-файл і запустити його без створення окремого виконуваного файлу. Такий підхід заснований на використанні системних викликів memfd_create, файлових дескрипторів і механізму binfmt_misc. Попри обмеження та громіздкість, цей метод показує, як приховування інформації у зображеннях може переходити на новий рівень — від простих «секретних» повідомлень до реального запуску програм. Матеріал буде цікавий програмістам, фахівцям із безпеки та всім, хто хоче глибше зрозуміти, як ОС працює з файлами та процесами.

Коли зображення стають виконуваними: трюки з memfd і binfmt_misc

Це зображення і водночас програма

Кілька років тому з’явилася публікація про PICO-8 — вигадану ігрову консоль із жорсткими обмеженнями. Особливий інтерес викликає новаторський спосіб поширення ігор для неї — кодування у форматі PNG. У такому зображенні міститься все: код гри, ресурси та додаткові дані. Картинка може бути будь-якою — скріншот із гри, оригінальний арт чи навіть текст. Щоб завантажити гру, достатньо передати цей PNG-файл на вхід програми PICO-8, і вона одразу запуститься.

Ця ідея наштовхує на думку: а що, якщо подібний підхід застосувати до програм у Linux? На перший погляд, задум здається абсурдним, проте саме він став основою одного з найдивніших експериментів року.

Кодування

PICO-8 імовірно використовує техніки стеганографії, що дозволяють приховувати дані у «сирих» байтах зображення. Суть підходу доволі проста: будь-яке зображення складається з пікселів, а кожен піксель визначається трьома значеннями кольору — червоним, зеленим та синім (RGB), які зберігаються у вигляді трьох байтів. Щоб додати до зображення приховані дані (так зване «корисне навантаження»), байти цього навантаження «змішуються» з байтами зображення.

Якщо записати дані напряму, замінивши вихідні байти, на картинці одразу з’являться спотворені ділянки — кольори не збігатимуться з початковими. Завдання полягає в іншому: зробити зміни максимально непомітними. Для цього байти корисного навантаження розподіляються серед байтів зображення-прикриття, при цьому зміни вносяться у молодші біти. Такий підхід дозволяє міняти значення кольорів настільки незначно, що людське око не здатне зафіксувати відмінності.

Наприклад, якщо потрібно вбудувати символ H, який у двійковій формі подається як 01001000 (72), його біти розкладаються по найменш значущих бітах чорних пікселів, що утворюють фон зображення.

На виході ми отримаємо кілька пікселів, які будуть трохи менш чорними, ніж раніше, але чи можна помітити різницю ?

Кольори пікселів трохи підкориговані.

Розрізнити такі зміни зможе хіба що дуже досвідчене око кольориста — на практиці незначні зрушення помітні тільки при машинному аналізі. Щоб відновити прихований символ H, досить прочитати по одному молодшому біту з восьми послідовних байтів зображення та зібрати з них один байт. Хоча ховати лише одну літеру — безглуздо, масштаб передачі можна збільшувати майже необмежено: у зображенні можна помістити коротке повідомлення, повний роман, посилання на аудіо або навіть скомпільований додаток. Єдине реальне обмеження — кількість доступних байтів у зображенні: для надійного приховування потрібно принаймні вісім разів більше байтів-масиву у зображенні, ніж байтів даних, які планується ввести.

Приховування програм

Повертаючись до ідеї виконуваних файлів Linux у зображенні, можна відзначити, що будь-який файл складається з байтів, а отже його можливо приховати всередині PNG аналогічно до того, як це реалізовано в PICO-8.

Для цього було створено власну бібліотеку стеганографії та спеціальний інструмент, здатний кодувати й декодувати дані у форматі PNG. Хоча готових рішень для стеганографії існує чимало, метою було глибше зрозуміти принципи роботи методу через розробку власної реалізації.

$ stegtool encode \

--cover-image htop-logo.png \

--input-data /usr/bin/htop \

--output-image htop.png

$

$ echo "Super secret hidden message" | stegtool encode \ 

--cover-image image.png \

--output-image image-with-hidden-message.png

$ stegtool decode --image image-with-hidden-message.png

Super secret hidden message

Оскільки все написано на Rust, було зовсім не складно скомпілювати це на WASM, тому можете поекспериментувати самостійно. Отже, тепер ми можемо вбудовувати дані, додаючи виконувані файли зображення. Але як нам їх запускати?

Запускаємо зображення

Найпростіший підхід полягав би в тому, щоб скористатися інструментом для декодування даних у новий файл, змінити права доступу за допомогою chmod +x, а потім запустити його. Такий спосіб цілком працездатний, але занадто прямолінійний. Більш цікавим завданням було створити механізм у стилі PICO-8: передати на вхід PNG-зображення й дозволити системі виконати всі подальші дії автоматично.

Проте завантажити довільний набір байтів у пам’ять і одразу передати управління Linux неможливо — принаймні безпосередньо. Водночас існують нескладні прийоми, які дозволяють реалізувати подібний задум обхідними шляхами.

memfd_create

Після ознайомлення з матеріалом стало ясно: можливе створення виконуваного файлу, що знаходиться в пам’яті. Ідея полягає в тому, щоб виділити блок пам’яті, записати в нього бінарні дані й запустити їх без потреби патчити ядро, перезаписувати execve(2) у userland або інжектити бібліотеки в інші процеси.

Для реалізації використовується системний виклик memfd_create(2), який створює анонімний файл у просторі імен /proc/self/fd поточного процесу; у цей файл записуються потрібні дані за допомогою write. Під час реалізації в Rust виникають складнощі з прив’язками до libc і типами даних у цих прив’язках, оскільки офіційна документація по них часто виявляється недостатньо інформативною.

Втім, вдалося отримати працездатну реалізацію цього підходу:

unsafe {
    let write_mode = 119; // w
    // create executable in-memory file
    let fd = syscall(libc::SYS_memfd_create, &write_mode, 1);
    if fd == -1 {
        return Err(String::from("memfd_create failed"));
    }

    let file = libc::fdopen(fd, &write_mode); 

    // write contents of our binary
    libc::fwrite(
        data.as_ptr() as *mut libc::c_void, 
        8 as usize,
        data.len() as usize,
        file,
    );
}

Виклику /proc/self/fd/<fd> як дочірнього процесу від батька, що його створив, достатньо для запуску вашого двійкового файлу:

let output = Command::new(format!("/proc/self/fd/{}", fd))
    .args(args)
    .stdin(std::process::Stdio::inherit())
    .stdout(std::process::Stdio::inherit())
    .stderr(std::process::Stdio::inherit())
    .spawn();

Спираючись на ці базові механізми, була створена програма pngrun, призначена для запуску зображень. Її робота зводиться до наступного:

  1. Приймає від інструменту стеганографічного зображення, в яке вбудований наш двійковий файл, і аргументи

  2. Декодує його (тобто витягує і збирає заново байти)

  3. Створює файл у пам’яті за допомогою memfd_create

  4. Поміщає байти двійкового файлу у файл у пам’яті

  5. Викликає файл /proc/self/fd/<fd>як дочірній процес, передаючи всі аргументи батьківського.

Тобто можна запустити це так:

$ pngrun htop.png

<htop output>

$ pngrun go.png run main.go

Hello world!

Після завершення pngrun файл у пам’яті знищується.

binfmt_misc

Постійно вводити команду pngrun незручно, тому останнім кроком у цьому експерименті стало застосування механізму binfmt_misc. Ця система дозволяє виконувати файли на основі їхнього типу. Первинне призначення — підтримка інтерпретаторів і віртуальних машин, таких як Java: замість виконання java -jar my-jar.jar можна просто запустити ./my-jar.jar, і система автоматично викличе процес java для запуску JAR-файла. Важливою умовою при цьому є встановлений прапор виконання у самого файлу.

Аналогічно можна додати в binfmt_misc запис для pngrun, що дасть можливість запускати будь-які PNG-файли з установленим прапором x безпосередньо:

$ cat /etc/binfmt.d/pngrun.conf

:ExecutablePNG:E::png::/home/me/bin/pngrun:

$ sudo systemctl restart binfmt.d

$ chmod +x htop.png

$ ./htop.png

<output>

У чому сенс проекту

З практичної точки зору цей підхід не має особливого сенсу. Ідея створення PNG-зображень, здатних запускати програми, виглядає привабливою й оригінальною, проте на практиці вона виявляється скоріше експериментом. У самому факті поширення програм у вигляді картинок є певна ностальгійна привабливість — на зразок коробкових версій ПЗ із барвистим оформленням, які колись продавалися для персональних комп’ютерів. Але у сучасних умовах подібний формат навряд чи вартий застосування.

Проект має багато недоліків, що роблять його непрактичним. Основний серед них — необхідність використання окремої програми pngrun, без якої вся схема не працює. Додатково виявляються дивні ефекти з деякими застосунками. Наприклад, після кодування clang у логотип LLVM, сама програма запускається коректно, проте під час компіляції відбувається збій.

$ ./clang.png --version

clang version 11.0.0 (Fedora 11.0.0-2.fc33)

Target: x86_64-unknown-linux-gnu

Thread model: posix

InstalledDir: /proc/self/fd

$ ./clang.png main.c

error: unable to execute command: Executable "" doesn't exist!

Чому цей проєкт виявився нежиттєздатним

Основною проблемою є великий розмір бінарних файлів. Оскільки їх необхідно повністю записати в PNG, підсумкові зображення стають непропорційно великими, що виглядає комічно. Додатково більшість програмного забезпечення складається не лише з одного виконуваного файлу, тож ідея поширювати складні додатки чи ігри у вигляді PNG-файлів втрачає будь-яку практичність.

Висновок

Цей експеримент можна вважати одним із найбільш абсурдних, проте він виявився корисним у навчальному плані. У процесі вдалося краще розібратися у принципах стеганографії, системному виклику memfd_create та механізмі binfmt_misc, а також попрактикуватися з використанням Rust. Попри всі недоліки, проєкт залишився цікавим досвідом і своєрідною вправою для розуміння внутрішніх можливостей Linux.

Підписатися
Сповістити про
0 Коментарі
Найстаріші
Найновіше Найбільше голосів
Знайшли помилку?
Якщо ви знайшли помилку, зробіть скріншот і надішліть його боту.