Esta imagen y programa simultáneo
Hace unas semanas leí sobre la PICO-8 , una consola de juegos ficticia con grandes limitaciones. De particular interés para mí fue una forma innovadora de distribuir sus juegos: codificar su imagen PNG. Incluye todo: el código del juego, los recursos, todo. La imagen puede ser cualquier cosa: capturas de pantalla del juego, arte genial o simplemente texto. Para cargar el juego, debe transferir la imagen a la entrada del programa PICO-8 y puede comenzar a jugar.
Me hizo pensar: ¿Sería genial si pudieras hacer lo mismo con programas en Linux? ¡No! Entiendo que dirás que esta es una idea tonta, pero lo hice de todos modos, y debajo hay una descripción de uno de los proyectos más tontos en los que trabajé este año.
Codificación
No estoy muy seguro de lo que hace el PICO-8, pero supongo que probablemente use técnicas esteganográficas que ocultan datos en los bytes sin procesar de la imagen. Hay muchos recursos en Internet que explican cómo funciona la esteganografía, pero su esencia misma es bastante simple: la imagen en la que desea ocultar los datos consta de bytes y píxeles. Los píxeles se componen de tres valores rojo, verde y azul (RGB), representados como tres bytes. Para ocultar los datos ("carga útil"), esencialmente "mezclamos" los bytes de carga útil con los bytes de la imagen.
Si simplemente reemplaza los bytes de la imagen con los bytes de la carga útil, aparecerán áreas con colores distorsionados en la imagen, porque no coincidirán con los colores de la imagen original. El truco consiste en ser lo más discreto posible para ocultar información de la nada . Esto se puede hacer extendiendo los bytes de carga útil a través de los bytes de la imagen de portada, ocultándolos en los bits menos significativos . En otras palabras, realice pequeños cambios en los valores de bytes para que los cambios de color no sean lo suficientemente fuertes para que el ojo humano los perciba.
Digamos que nuestra carga útil es una letra
H
representada en binario como
01001000
(72) y la imagen contiene un conjunto de píxeles negros.
Los bits de los bytes de entrada se distribuyen entre los 8 bytes de salida ocultándolos en el bit
menos significativo. En la salida, obtenemos algunos píxeles que serán un poco menos negros que antes, pero ¿puedes notar la diferencia?
Los colores de los píxeles se han modificado ligeramente.
Tal vez un conocedor del color con mucha experiencia podrá notar la diferencia, pero en la vida real, estos pequeños cambios solo son visibles para la máquina. Para obtener nuestra carta ultrasecreta
H
, basta con leer 8 bytes de la imagen resultante y ensamblarlos nuevamente en 1 byte. Obviamente, ocultar una sola letra es una idea tonta, pero la escala de la transmisión se puede aumentar libremente. Digamos que transmite una propuesta súper-sectorial, una copia de Guerra y paz , un enlace a Soundcloud, un compilador de Go; la única limitación será la cantidad de bytes disponibles en la imagen, porque debe haber al menos 8 veces más de ellos que en la información de entrada.
Ocultar programas
Entonces, volvamos a nuestra idea de los ejecutables de Linux en la imagen. Si piensa que los archivos ejecutables son simplemente bytes, entonces está claro que se pueden ocultar en imágenes, al igual que lo hace el PICO-8.
Antes de implementar esto, decidí escribir mi propia biblioteca y herramienta de esteganografía que admita la codificación y decodificación de datos en PNG. Por supuesto, hay muchas bibliotecas y herramientas esteganográficas listas para usar, pero aprendo mejor cuando hago mis propias cosas.
$ 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
Dado que todo está escrito en Rust , no fue nada difícil compilar esto en WASM, por lo que puede experimentarlo usted mismo.
Entonces ahora podemos incrustar datos agregando ejecutables a las imágenes. ¿Pero cómo los manejamos?
Ejecutar la imagen
La forma más fácil sería simplemente ejecutar la herramienta anterior, ejecutar los
decode
datos en un nuevo archivo, cambiar los derechos con
chmod +x
y luego ejecutarlo. Funcionará, pero será demasiado aburrido. Quería hacer algo con el estilo PICO-8: pasamos una imagen PNG a alguna entidad y ella hace el resto.
Sin embargo, resulta que no puedes simplemente cargar un conjunto arbitrario de bytes en la memoria y decirle a Linux que salte a él ... al menos no directamente. Sin embargo, puede usar algunos trucos simples para hacerlo.
memfd_create
Después de leer esta publicación, se hizo evidente que puede crear un archivo en la memoria y marcarlo como ejecutable.
¿No sería bueno simplemente tomar un bloque de memoria, escribir los datos binarios allí y ejecutarlo sin parchear el kernel, reescribir execve (2) en el área de usuario o cargar la biblioteca en otro proceso?
Este método usa la llamada al sistema memfd_create (2) para crear un archivo en el espacio de nombres de
/proc/self/fd
su proceso y cargar los datos que necesita en él usando
write
. Pasé bastante tiempo averiguando los enlaces libc con Rust para que todo funcionara, y fue difícil para mí entender los tipos de datos que se pasaban, la documentación sobre estos enlaces Rust no ayudó mucho.
Sin embargo, logré que algo funcionara.
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,
);
}
Una llamada
/proc/self/fd/<fd>
como hijo del padre que lo creó es suficiente para ejecutar su binario.
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();
Con estos bloques de construcción en la mano, escribí un programa pngrun para ejecutar imágenes. En esencia, hace lo siguiente:
- Toma una imagen de una herramienta esteganográfica que tiene nuestro binario incrustado y los argumentos
- Decodifica (es decir, recupera y vuelve a ensamblar bytes)
- Crea un archivo en memoria con
memfd_create
- Coloca los bytes de un archivo binario en un archivo en la memoria
- Llama al archivo
/proc/self/fd/<fd>
como un proceso hijo, pasando todos los argumentos del padre.
Es decir, puede ejecutarlo así:
$ pngrun htop.png
<htop output>
$ pngrun go.png run main.go
Hello world!
Al finalizar,
pngrun
se destruye el archivo en la memoria.
binfmt_misc
Sin embargo
pngrun
, escribir es molesto cada vez , por lo que el último truco simple en este proyecto sin sentido fue usar binfmt_misc , un sistema que le permite "ejecutar" archivos en función de su tipo de archivo. Creo que esta característica fue diseñada principalmente para intérpretes / máquinas virtuales como Java. En lugar de escribir,
java -jar my-jar.jar
simplemente ingrese
./my-jar.jar
y se llamará
java
al proceso para ejecutar el JAR. Sin embargo, el archivo
my-jar.jar
primero debe marcarse como ejecutable.
Es decir, agregue una entrada para binfmt_misc
pngrun
para poder ejecutar cualquiera
png
con la bandera establecida
x
, te puede gustar esto:
$ 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>
Cual es el significado del proyecto
Bueno, realmente no tiene mucho sentido. Me tentó la idea de crear imágenes PNG que pudieran ejecutar programas y lo desarrollé un poco, pero el proyecto seguía siendo interesante. Hay algo sorprendente en la capacidad de distribuir software como imágenes: piense en las originales cajas de cartón del software de PC con gráficos en la parte frontal. ¿Por qué no traerlos de vuelta? (Aunque en realidad no vale la pena).
El proyecto es muy tonto y tiene muchas fallas que lo hacen completamente sin sentido y poco práctico. El principal defecto es que debe haber un programa estúpido en la máquina para que funcione
pngrun
. Sin embargo, noté algunas rarezas en programas como
clang
... Lo codifiqué en este divertido logotipo de LLVM y, aunque funciona bien, se bloquea al intentar compilar.
$ ./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!
Esto probablemente se deba a que el archivo es anónimo y el problema se puede resolver si tuviera interés en estudiarlo.
¿Por qué más es estúpido este proyecto?
Muchos binarios son bastante grandes y dado que deben escribirse en imágenes, el tamaño de los gráficos debe ser grande y los archivos resultantes son cómicamente enormes.
Además, la mayoría del software no consta de un solo archivo ejecutable, por lo que el sueño de distribuir PNG fallará para programas más complejos como los juegos.
Conclusión
Este es probablemente el proyecto más tonta que he trabajado en este año, pero fue sin duda divertido, aprendí sobre la esteganografía
memfd_create
,
binfmt_misc
y jugado un poco con un poco más de Rust.