PNG exécutables: exécutez des images en tant que programmes



Cette image et ce programme simultané



Il y a quelques semaines, j'ai lu sur la PICO-8 , une console de jeu fictive avec de grandes limitations. Un moyen innovant de distribuer ses jeux m'a particulièrement intéressé: encoder leur image PNG. Il comprend tout - le code du jeu, les ressources, tout. L'image peut être n'importe quoi: des captures d'écran du jeu, de l'art sympa ou simplement du texte. Pour charger le jeu, vous devez transférer l'image vers l'entrée du programme PICO-8 et vous pouvez commencer à jouer.



Cela m'a fait penser: serait-ce cool si vous pouviez faire la même chose avec des programmes sous Linux? Non! Je comprends que vous allez dire que c'est une idée stupide, mais je l'ai quand même fait, et ci-dessous se trouve une description de l'un des projets les plus stupides sur lesquels j'ai travaillé cette année.



Codage



Je ne suis pas tout à fait sûr de ce que fait le PICO-8, mais je suppose qu'il utilise probablement des techniques stéganographiques qui cachent les données dans les octets bruts de l'image. Il existe de nombreuses ressources sur Internet expliquant le fonctionnement de la stéganographie, mais son essence même est assez simple: l'image dans laquelle vous souhaitez masquer les données est constituée d'octets et de pixels. Les pixels sont constitués de trois valeurs rouge, verte et bleue (RVB), représentées par trois octets. Pour masquer les données ("payload"), nous "mixons" essentiellement les octets de la charge utile avec les octets de l'image.



Si vous remplacez simplement les octets de l'image par les octets de la charge utile, les zones avec des couleurs déformées apparaîtront dans l'image, car elles ne correspondront pas aux couleurs de l'image d'origine. L'astuce consiste à être aussi discret que possible, à cacher les informations à l'improviste . Cela peut être fait en distribuant les octets de charge utile sur les octets de l'image de couverture, en les cachant dans les bits les moins significatifs . En d'autres termes, apportez de petites modifications aux valeurs d'octet afin que les changements de couleur ne soient pas assez forts pour que l'œil humain puisse les percevoir.



Disons que notre charge utile est une lettre H



représentée en binaire comme 01001000



(72), et l'image contient un ensemble de pixels noirs.





Les bits des octets d'entrée sont répartis sur les 8 octets de sortie en les cachant dans le bit le



moins significatif. En sortie, on obtient quelques pixels qui seront légèrement moins noirs qu'avant, mais peux-tu faire la différence?





Les couleurs des pixels ont été légèrement modifiées.



Peut-être qu'un connaisseur de couleurs extrêmement expérimenté pourra faire la différence, mais dans la vraie vie, de tels changements minuscules ne sont visibles que par la machine. Pour obtenir notre lettre top secrète H



, il vous suffit de lire 8 octets de l'image résultante et de les assembler à nouveau en 1 octet. De toute évidence, cacher une seule lettre est une idée insensée, mais l'échelle de la transmission peut être augmentée librement. Disons que vous envoyez une proposition super-sectorielle, une copie de War and Peace , un lien vers Soundcloud, un compilateur Go - la seule limitation sera le nombre d'octets disponibles dans l'image, car il doit y en avoir au moins 8 fois plus que dans les informations d'entrée.



Masquer les programmes



Revenons donc à notre idée des exécutables Linux dans l'image. Si vous considérez les fichiers exécutables comme de simples octets, il est clair qu'ils peuvent être cachés dans les images, tout comme le fait le PICO-8.



Avant de l'implémenter, j'ai décidé d'écrire ma propre bibliothèque et outil de stéganographie prenant en charge l'encodage et le décodage des données en PNG. Bien sûr, il existe de nombreuses bibliothèques et outils stéganographiques prêts à l'emploi, mais j'apprends mieux quand je fais mon propre truc.



$ 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






Comme tout est écrit en Rust , il n'a pas été du tout difficile de compiler cela dans WASM, vous pouvez donc expérimenter vous- même.



Nous pouvons maintenant intégrer des données en ajoutant des exécutables aux images. Mais comment les gérons-nous?



Exécutez l'image



Le moyen le plus simple serait d'exécuter simplement l'outil ci-dessus, d'exécuter les decode



données dans un nouveau fichier, de modifier les droits avec chmod +x



, puis de l'exécuter. Cela fonctionnera, mais ce sera trop ennuyeux. Je voulais faire quelque chose dans le style PICO-8 - nous transmettons une image PNG à une entité, et elle fait le reste.



Cependant, il s'avère que vous ne pouvez pas simplement charger un ensemble arbitraire d'octets dans la mémoire et dire à Linux d'y accéder ... du moins pas directement. Cependant, vous pouvez utiliser quelques astuces simples pour y parvenir.



memfd_create



Après avoir lu cet article, il est devenu évident que vous pouvez créer un fichier en mémoire et le marquer comme exécutable.



Ne serait-il pas génial de simplement prendre un bloc de mémoire, d'y écrire les données binaires et de les exécuter sans patcher le noyau, réécrire execve (2) dans l'espace utilisateur ou charger la bibliothèque dans un autre processus?


Cette méthode utilise l'appel système memfd_create (2) pour créer un fichier dans l'espace de noms de /proc/self/fd



votre processus et y charger les données dont vous avez besoin en utilisant write



. J'ai passé beaucoup de temps à comprendre les liaisons de la libc avec Rust pour que tout fonctionne, et il était difficile pour moi de comprendre les types de données transmis, la documentation sur ces liaisons de Rust n'a pas beaucoup aidé.



Cependant, j'ai réussi à faire fonctionner quelque chose.



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,
    );
}
      
      





Un appel en /proc/self/fd/<fd>



tant qu'enfant du parent qui l'a créé suffit à exécuter votre binaire.



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();
      
      





Avec ces blocs de construction en main, j'ai écrit un programme pngrun pour exécuter des images. En substance, il fait ce qui suit:



  1. Accepte de l'outil stéganographique une image dans laquelle notre binaire est intégré et des arguments
  2. Le décode (c'est-à-dire récupère et réassemble les octets)
  3. Crée un fichier en mémoire avec memfd_create



  4. Place les octets d'un fichier binaire dans un fichier en mémoire
  5. Appelle le fichier en /proc/self/fd/<fd>



    tant que processus enfant, en transmettant tous les arguments du parent.


Autrement dit, vous pouvez l'exécuter comme ceci:



$ pngrun htop.png

<htop output>

$ pngrun go.png run main.go

Hello world!






Une fois terminé, le pngrun



fichier en mémoire est détruit.



binfmt_misc



Cependant pngrun



, taper est ennuyeux à chaque fois , donc la dernière astuce simple dans ce projet inutile était d'utiliser binfmt_misc - un système qui vous permet "d'exécuter" des fichiers en fonction de leur type de fichier. Je pense que cette fonctionnalité a été principalement conçue pour les interprètes / machines virtuelles comme Java. Au lieu de taper, java -jar my-jar.jar



entrez simplement ./my-jar.jar



et cela appellera le processus java



pour exécuter le JAR. Cependant, le fichier my-jar.jar



doit d'abord être marqué comme exécutable.



Autrement dit, ajoutez une entrée pour binfmt_misc pngrun



pour pouvoir en exécuter n'importe quel png



avec le jeu d'indicateurs x



, vous pouvez aimer ceci:



$ 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>






Quelle est la signification du projet



Eh bien, cela n'a pas vraiment de sens. J'étais tenté par l'idée de créer des images PNG capables d'exécuter des programmes, et je l'ai un peu développé, mais le projet était toujours intéressant. Il y a quelque chose d'étonnant à propos de la possibilité de distribuer des logiciels sous forme d'images - pensez aux boîtes en carton géniales du logiciel PC avec des graphiques à l'avant. Pourquoi ne pas les ramener? (Bien que cela ne vaille pas vraiment la peine.)



Le projet est très stupide et comporte de nombreux défauts qui le rendent complètement dénué de sens et peu pratique. Le principal défaut est qu'il doit y avoir un programme stupide pour qu'il fonctionne sur la machine pngrun



. Cependant, j'ai remarqué quelques bizarreries dans des programmes comme clang



... Je l'ai codé dans ce drôle de logo LLVM, et bien que cela fonctionne bien, il se bloque lors de la compilation.





$ ./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!






Ceci est probablement dû au fait que le fichier est anonyme, et le problème peut être résolu si j'avais un intérêt à l'étudier.



Sinon, pourquoi ce projet est-il stupide



De nombreux fichiers binaires sont assez volumineux, et étant donné qu'ils doivent être écrits sur des images, la taille des graphiques doit être importante et les fichiers résultants sont comiquement énormes.



De plus, la plupart des logiciels ne se composent pas d'un seul fichier exécutable, de sorte que le rêve de distribuer des PNG échouera dans le cas de programmes plus complexes comme les jeux.



Conclusion



Ceci est probablement le projet le plus stupide que j'ai travaillé cette année, mais il était vraiment amusant, j'ai appris stéganographie memfd_create



, binfmt_misc



et joué avec Rust un peu plus.



All Articles