PNGs executáveis: execute imagens como programas



Esta Imagem e o Programa Simultâneo



Há algumas semanas li sobre o PICO-8 , um videogame fictício com grandes limitações. Meu interesse particular era a maneira inovadora de distribuir seus jogos - codificando sua imagem PNG. Inclui tudo - o código do jogo, recursos, tudo. A imagem pode ser qualquer coisa: screenshots do jogo, arte legal ou apenas texto. Para carregar o jogo, você precisa transferir a imagem para a entrada do programa PICO-8 e pode começar a jogar.



Isso me fez pensar: seria legal se você pudesse fazer o mesmo com programas no Linux? Não! Eu entendo que você vai dizer que esta é uma ideia idiota, mas eu fiz assim mesmo, e abaixo está uma descrição de um dos projetos mais idiotas em que trabalhei este ano.



Codificação



Não tenho certeza do que o PICO-8 faz exatamente, mas acho que provavelmente usa técnicas esteganográficas que ocultam dados nos bytes brutos da imagem. Existem muitos recursos na Internet que explicam como funciona a esteganografia, mas sua própria essência é bastante simples: a imagem na qual você deseja ocultar os dados consiste em bytes e pixels. Os pixels são compostos por três valores de vermelho, verde e azul (RGB), representados como três bytes. Para ocultar os dados ("carga útil"), essencialmente "misturamos" os bytes da carga útil com os bytes da imagem.



Se você simplesmente substituir os bytes da imagem pelos bytes da carga útil, áreas com cores distorcidas aparecerão na imagem, pois não corresponderão às cores da imagem original. O truque é ser o mais discreto possível, para ocultar informações do nada . Isso pode ser feito distribuindo os bytes de carga útil pelos bytes da imagem de capa, ocultando-os nos bits menos significativos . Em outras palavras, faça pequenas mudanças nos valores dos bytes para que as mudanças de cor não sejam fortes o suficiente para serem percebidas pelo olho humano.



Digamos que nossa carga útil seja uma letra H



representada em binário como 01001000



(72), e a imagem contém um conjunto de pixels pretos.





Os bits dos bytes de entrada são distribuídos pelos 8 bytes de saída, ocultando-os no bit



menos significativo. Na saída, obtemos alguns pixels que ficarão um pouco menos pretos do que antes, mas você pode notar a diferença?





As cores dos pixels foram ligeiramente ajustadas.



Talvez um conhecedor de cores extremamente habilidoso seja capaz de perceber a diferença, mas na vida real essas pequenas mudanças são visíveis apenas para a máquina. Para obter nossa carta ultrassecreta H



, você só precisa ler 8 bytes da imagem resultante e montá-los novamente em 1 byte. Obviamente, esconder uma única letra é uma ideia estúpida, mas a escala da transmissão pode ser aumentada livremente. Digamos que você envie uma proposta supersetorial, uma cópia de Guerra e Paz , um link para o Soundcloud, um compilador Go - a única limitação será o número de bytes disponíveis na imagem, porque deve haver pelo menos 8 vezes mais bytes do que nas informações de entrada.



Escondendo programas



Então, de volta à nossa ideia de executáveis ​​Linux na imagem. Se você pensa nos arquivos executáveis ​​simplesmente como bytes, então é claro que eles podem ser ocultados nas imagens, assim como o PICO-8 faz.



Antes de implementar isso, decidi escrever minha própria biblioteca e ferramenta de esteganografia que oferece suporte à codificação e decodificação de dados em PNG. Claro, existem muitas bibliotecas e ferramentas esteganográficas já prontas, mas aprendo melhor quando faço minhas próprias coisas.



$ 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






Como tudo é escrito em Rust , não foi nada difícil compilar isso no WASM, então você pode experimentar por conta própria.



Portanto, agora podemos incorporar dados adicionando executáveis ​​às imagens. Mas como os gerimos?



Execute a imagem



A maneira mais fácil seria simplesmente executar a ferramenta acima, executar os decode



dados em um novo arquivo, alterar os direitos com chmod +x



e executá-lo. Vai funcionar, mas será muito chato. Eu queria fazer algo no estilo PICO-8 - passamos uma imagem PNG para alguma entidade e ela faz o resto.



No entanto, como se constatou, você não pode simplesmente carregar um conjunto arbitrário de bytes na memória e dizer ao Linux para pular para ele ... pelo menos não diretamente. No entanto, você pode usar alguns truques simples para fazer isso.



memfd_create



Depois de ler este post, ficou claro que você pode criar um arquivo na memória e marcá-lo como executável.



Não seria bom apenas pegar um bloco de memória, gravar os dados binários lá e executá-los sem corrigir o kernel, reescrever execve (2) no domínio do usuário ou carregar a biblioteca em outro processo?


Este método usa a chamada de sistema memfd_create (2) para criar um arquivo no namespace do /proc/self/fd



seu processo e carregar os dados que você precisa nele usando write



. Eu gastei muito tempo descobrindo as ligações libc com Rust para fazer tudo funcionar, e era difícil para mim entender os tipos de dados que estavam sendo passados, a documentação sobre essas ligações Rust não ajudou muito.



No entanto, consegui fazer algo funcionar.



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





Uma chamada /proc/self/fd/<fd>



como filho do pai que o criou é suficiente para executar seu binário.



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





Com esses blocos de construção em mãos, escrevi um programa pngrun para executar imagens. Basicamente, ele faz o seguinte:



  1. Obtém uma imagem de uma ferramenta esteganográfica que contém nosso arquivo binário e argumentos
  2. Decodifica-o (ou seja, recupera e remonta bytes)
  3. Cria um arquivo na memória com memfd_create



  4. Coloca os bytes de um arquivo binário em um arquivo na memória
  5. Chama o arquivo /proc/self/fd/<fd>



    como um processo filho, passando todos os argumentos do pai.


Ou seja, você pode executá-lo assim:



$ pngrun htop.png

<htop output>

$ pngrun go.png run main.go

Hello world!






Após a conclusão, o pngrun



arquivo na memória é destruído.



binfmt_misc



No entanto pngrun



, digitar é sempre chato , então o último truque simples neste projeto inútil foi usar binfmt_misc - um sistema que permite "executar" arquivos com base em seu tipo de arquivo. Acho que esse recurso foi projetado principalmente para intérpretes / máquinas virtuais como Java. Em vez de digitar, java -jar my-jar.jar



basta digitar ./my-jar.jar



e isso irá chamar o processo java



para executar o JAR. No entanto, o arquivo my-jar.jar



deve primeiro ser marcado como executável.



Ou seja, adicione uma entrada para binfmt_misc pngrun



para poder executar qualquer um png



com o sinalizador definido x



, você pode gostar disso:



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






Qual é o significado do projeto



Bem, isso realmente não faz muito sentido. Fiquei tentado com a ideia de criar imagens PNG que pudessem rodar programas, e desenvolvi um pouco, mas o projeto ainda era interessante. Há algo incrível em ser capaz de distribuir software como imagens - pense nas caixas de papelão descoladas de software para PC com gráficos na frente. Por que não trazê-los de volta? (Embora não valha a pena.)



O projeto é muito burro e tem muitas falhas que o tornam completamente sem sentido e impraticável. A principal falha é que deve haver um programa estúpido para que funcione na máquina pngrun



. No entanto, notei algumas estranhezas em programas como clang



... Eu codifiquei neste logotipo engraçado do LLVM e, embora funcione bem, ele trava ao tentar 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!






Isso provavelmente é resultado do anonimato do arquivo, e o problema pode ser resolvido se eu tiver interesse em estudá-lo.



Por que mais este projeto é estúpido



Muitos binários são muito grandes e, como precisam ser gravados em imagens, o tamanho dos gráficos deve ser grande e os arquivos resultantes são comicamente grandes.



Além disso, a maioria dos softwares não consiste em apenas um arquivo executável, então o sonho de distribuir PNGs irá falhar no caso de programas mais complexos como jogos.



Conclusão



Este é provavelmente o projeto mais estúpida que já trabalhou neste ano, mas foi definitivamente divertido, eu aprendi sobre esteganografia memfd_create



, binfmt_misc



e brinquei com Rust um pouco mais.



All Articles