Gambar dan Program Simultan
Ini Beberapa minggu yang lalu, saya membaca tentang PICO-8 , konsol game fiksi dengan keterbatasan yang besar. Yang menarik bagi saya adalah cara inovatif mendistribusikan gimnya - menyandikan gambar PNG mereka. Ini mencakup segalanya - kode permainan, sumber daya, semuanya. Gambarnya bisa apa saja: screenshot dari game, seni keren, atau hanya teks. Untuk memuat game, Anda perlu mentransfer gambar ke input program PICO-8, dan Anda dapat mulai bermain.
Itu membuat saya berpikir: Akankah keren jika Anda dapat melakukan hal yang sama dengan program di Linux? Tidak! Saya mengerti Anda akan mengatakan bahwa ini adalah ide yang bodoh, tetapi saya tetap melakukannya, dan di bawah ini adalah deskripsi salah satu proyek paling bodoh yang saya kerjakan tahun ini.
Pengodean
Saya tidak begitu yakin apa tepatnya yang dilakukan PICO-8, tapi saya rasa itu mungkin menggunakan teknik steganografi untuk menyembunyikan data dalam byte mentah gambar. Ada banyak sumber di Internet yang menjelaskan cara kerja steganografi, tetapi intinya cukup sederhana: gambar yang ingin Anda sembunyikan datanya terdiri dari byte dan piksel. Piksel terdiri dari tiga nilai merah, hijau dan biru (RGB), yang direpresentasikan sebagai tiga byte. Untuk menyembunyikan data ("payload"), pada dasarnya kita "mencampur" byte payload dengan byte gambar.
Jika Anda hanya mengganti byte gambar dengan byte payload, maka area dengan warna yang terdistorsi akan muncul di gambar, karena tidak akan cocok dengan warna gambar aslinya. Triknya adalah menjadi tidak mencolok mungkin, untuk menyembunyikan informasi secara tiba-tiba . Ini dapat dilakukan dengan mendistribusikan byte payload ke seluruh byte gambar sampul, menyembunyikannya dalam bit yang paling tidak signifikan . Dengan kata lain, buat perubahan kecil pada nilai byte sehingga perubahan warna tidak cukup kuat untuk dilihat oleh mata manusia.
Misalkan payload kita adalah huruf yang
H
direpresentasikan dalam biner sebagai
01001000
(72), dan gambar berisi satu set piksel hitam.
Bit-bit byte input didistribusikan ke 8 byte output dengan menyembunyikannya di bit yang
paling tidak signifikan. Pada output, kita mendapatkan beberapa piksel yang akan sedikit kurang hitam dari sebelumnya, tetapi dapatkah Anda membedakannya?
Warna piksel telah sedikit diubah.
Mungkin seorang ahli warna yang sangat terampil akan dapat membedakannya, tetapi dalam kehidupan nyata, perubahan kecil seperti itu hanya dapat dilihat oleh mesin. Untuk mendapatkan surat rahasia kami
H
, Anda hanya perlu membaca 8 byte dari gambar yang dihasilkan dan merangkainya lagi menjadi 1 byte. Jelas, menyembunyikan satu huruf adalah ide yang bodoh, tetapi skala transmisi dapat ditingkatkan dengan bebas. Katakanlah Anda mengirimkan proposal super-sektoral, salinan War and Peace , tautan ke Soundcloud, kompiler Go - satu-satunya batasan adalah jumlah byte yang tersedia dalam gambar, karena harus ada setidaknya 8 kali lebih banyak daripada di informasi masukan.
Menyembunyikan program
Jadi, kembali ke ide kami tentang Linux yang dapat dieksekusi pada gambar. Jika Anda menganggap file yang dapat dieksekusi hanya sebagai byte, maka jelas bahwa mereka dapat disembunyikan dalam gambar, seperti yang dilakukan PICO-8.
Sebelum menerapkan ini, saya memutuskan untuk menulis pustaka steganografi saya sendiri dan alat yang mendukung pengkodean dan dekode data di PNG. Tentu saja, ada banyak pustaka dan alat steganografi siap pakai di luar sana, tetapi saya belajar paling baik ketika saya melakukan hal saya sendiri.
$ 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
Karena semuanya ditulis dalam Rust , tidaklah sulit untuk mengkompilasi ini di WASM, jadi Anda dapat bereksperimen sendiri.
Jadi sekarang kita bisa menyematkan data dengan menambahkan file yang dapat dieksekusi ke gambar. Tapi bagaimana kita menjalankannya?
Jalankan gambarnya
Cara termudah adalah dengan menjalankan alat di atas, mengeksekusi
decode
data menjadi file baru, mengubah hak akses dengan
chmod +x
, dan kemudian menjalankannya. Ini akan berhasil, tetapi akan terlalu membosankan. Saya ingin melakukan sesuatu dengan gaya PICO-8 - kami meneruskan gambar PNG ke beberapa entitas, dan sisanya akan dikerjakan.
Namun, ternyata, Anda tidak bisa hanya memuat sekumpulan byte yang berubah-ubah ke dalam memori dan memberi tahu Linux untuk melompat ke sana ... setidaknya tidak secara langsung. Namun, Anda dapat menggunakan beberapa trik sederhana untuk menyelesaikannya.
memfd_create
Setelah membaca posting ini, menjadi jelas bahwa Anda dapat membuat file di memori dan menandainya sebagai dapat dieksekusi.
Bukankah lebih baik mengambil satu blok memori, menulis data biner di sana dan menjalankannya tanpa menambal kernel, menulis ulang execve (2) di userland, atau memuat pustaka ke proses lain?
Metode ini menggunakan panggilan sistem memfd_create (2) untuk membuat file di namespace
/proc/self/fd
proses Anda dan memuat data yang Anda perlukan ke dalamnya menggunakan
write
. Saya menghabiskan cukup banyak waktu untuk mencari tahu binding libc dengan Rust agar semuanya berfungsi, dan sulit bagi saya untuk memahami tipe data yang diteruskan, dokumentasi tentang binding Rust ini tidak banyak membantu.
Namun, saya berhasil mendapatkan sesuatu yang berfungsi.
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,
);
}
Panggilan
/proc/self/fd/<fd>
sebagai anak dari induk yang membuatnya sudah cukup untuk menjalankan biner Anda.
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();
Dengan blok bangunan ini di tangan, saya menulis program pngrun untuk menjalankan gambar. Pada dasarnya, ini melakukan hal berikut:
- Menerima dari alat steganografi gambar tempat biner kita disematkan, dan argumen
- Mendekodekannya (yaitu mengambil dan memasang kembali byte)
- Membuat file di memori dengan
memfd_create
- Menempatkan byte dari file biner ke dalam file di memori
- Memanggil file
/proc/self/fd/<fd>
sebagai proses anak, meneruskan semua argumen dari induknya.
Artinya, Anda bisa menjalankannya seperti ini:
$ pngrun htop.png
<htop output>
$ pngrun go.png run main.go
Hello world!
Setelah selesai,
pngrun
file di memori dihancurkan.
binfmt_misc
Namun
pngrun
, mengetik selalu mengganggu , jadi trik sederhana terakhir dalam proyek tidak berguna ini adalah menggunakan binfmt_misc - sistem yang memungkinkan Anda untuk "mengeksekusi" file berdasarkan jenis file mereka. Saya rasa fitur ini dirancang terutama untuk interpreter / mesin virtual seperti Java. Alih-alih mengetik,
java -jar my-jar.jar
cukup masuk
./my-jar.jar
dan ini akan memanggil proses
java
untuk menjalankan JAR. Namun, file tersebut
my-jar.jar
harus terlebih dahulu ditandai dapat dieksekusi.
Artinya, tambahkan entri untuk binfmt_misc
pngrun
agar dapat menjalankan apa pun
png
dengan set bendera
x
, Anda bisa menyukai ini:
$ 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>
Apa arti dari proyek tersebut
Yah, itu tidak masuk akal. Saya tergoda dengan ide membuat gambar PNG yang dapat menjalankan program, dan saya mengembangkannya sedikit, tetapi proyek tersebut tetap menarik. Ada sesuatu yang menarik tentang kemampuan mendistribusikan perangkat lunak sebagai gambar - bayangkan kotak kardus yang funky dari perangkat lunak PC dengan grafik di bagian depan. Mengapa tidak membawanya kembali? (Meskipun tidak terlalu berharga.)
Proyek ini sangat bodoh dan memiliki banyak kekurangan yang membuatnya benar-benar tidak berarti dan tidak praktis. Kelemahan utamanya adalah harus ada program bodoh di mesin agar bisa bekerja
pngrun
. Namun, saya melihat beberapa keanehan dalam program seperti
clang
... Saya mengkodekannya ke dalam logo LLVM yang lucu ini, dan meskipun berfungsi dengan baik, ia macet saat mencoba mengkompilasi.
$ ./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!
Ini mungkin hasil dari file yang menjadi anonim, dan masalah dapat diselesaikan jika saya tertarik untuk mempelajarinya.
Mengapa lagi proyek ini bodoh
Banyak binari berukuran cukup besar, dan mengingat bahwa mereka perlu ditulis ke gambar, ukuran grafik harus besar dan file yang dihasilkan sangat besar.
Selain itu, sebagian besar perangkat lunak tidak hanya terdiri dari satu file yang dapat dieksekusi, sehingga impian untuk mendistribusikan PNG akan gagal dalam kasus program yang lebih kompleks seperti game.
Kesimpulan
Ini mungkin proyek terbodoh yang pernah saya kerjakan tahun ini, tapi pasti menyenangkan, saya belajar tentang steganografi
memfd_create
,
binfmt_misc
dan bermain-main dengan Rust lebih banyak.