実行可能なPNG:画像をプログラムとして実行



この画像と同時プログラム



数週間前、私はPICO-8について読みました 。これは、大きな制限のある架空のゲームコンソールです。私が特に興味を持っているのは、彼女のゲームを配布する革新的な方法、つまりPNG画像をエンコードすることです。ゲームコード、リソース、すべてが含まれます。画像は何でもかまいません:ゲームのスクリーンショット、クールなアート、または単なるテキスト。ゲームをロードするには、PICO-8プログラムの入力に画像を転送する必要があり、プレイを開始できます。



Linux上のプログラムでも同じことができるとしたら、それは素晴らしいことでしょうか。番号!これはばかげた考えだとおっしゃるでしょうが、とにかくやりました。以下は、私が今年取り組んだ最もばかげたプロジェクトの1つです。



コーディング



PICO-8が何をするのかはよくわかりませんが、おそらく画像の生のバイトのデータを隠すステガノグラフィック技術使用していると思い ます。ステガノグラフィがどのように機能するかを説明するインターネット上のリソースはたくさんありますが、その本質は非常に単純です。データを非表示にする画像は、バイトとピクセルで構成されています。ピクセルは、3つの赤、緑、青(RGB)の値で構成され、3バイトで表されます。データ(「ペイロード」)を非表示にするために、基本的にペイロードバイトとイメージバイトを「混合」します。



画像のバイトをペイロードのバイトに置き換えるだけでは、元の画像の色と一致しないため、歪んだ色の領域が画像に表示されます。秘訣は、情報突然隠すために、できるだけ目立たないようにする ことです。これはペイロードバイトをカバーイメージバイト全体に分散し、最下位ビットで非表示に することで実行でき ます。言い換えれば、バイト値に小さな変更を加えて、色の変化が人間の目で認識できるほど強くないようにします。



ペイロードがH



バイナリで表される 文字であるとしましょう。 01001000



(72)、画像には黒いピクセルのセットが含まれています。





入力バイトのビットは、最下位ビットに非表示にすることで8つの出力バイトに分散されます。



出力では、以前よりもわずかに黒が少なくなるいくつかのピクセルが得られますが、違いがわかりますか?





ピクセルの色が少し調整されました。



おそらく、非常に熟練した色の愛好家が違いを知ることができるでしょうが、実際には、そのような小さな変化は機械によってのみ認識されます。トップシークレットレターを取得するに H



は、結果の画像の8バイトを読み取り、それらを1バイトに再度アセンブルする必要があります。明らかに一文字を隠すのはばかげた考えですが、送信の規模は自由に増やすことができます。スーパーセクターの提案、War and Peaceのコピー Soundcloudへのリンク、Goコンパイラを送信するとします。 唯一の制限は、入力情報よりも少なくとも8倍多い必要があるため、画像で使用できるバイト数です。



プログラムを隠す



だから、イメージ内のLinux実行可能性のアイデアに戻ります。実行可能ファイルを単なるバイトと考えると、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でコンパイルすることはまったく難しくありませんでした。したがって自分で実験することができ ます



これで、実行可能ファイルをイメージに追加してデータを埋め込むことができます。しかし、どのようにそれらを実行しますか?



画像を実行します



最も簡単な方法は、上記のツールを実行し、 decode



データを新しいファイルにchmod +x



実行、で権限を変更して から実行することです。それは機能しますが、退屈すぎます。私はPICO-8スタイルで何かをしたかったのです。PNG画像をあるエンティティに渡し、残りはそれで行います。



ただし、結局のところ、任意のバイトセットをメモリにロードして、Linuxにジャンプするように指示することはできません...少なくとも直接ではありません。ただし、 次のことができ、それを成し遂げるためにいくつかの簡単なトリックを使用しています。



memfd_create



この投稿を読んだ後 、メモリ内にファイルを作成して実行可能としてマークできることが明らかになりました。



メモリのブロックを取得し、そこにバイナリデータを書き込んで、カーネルにパッチを適用したり、ユーザーランドでexecve(2)を書き換えたり、ライブラリを別のプロセスにロードしたりせずに実行するのは良いことではないでしょうか。


このメソッドは、memfd_create(2)システム呼び出し を使用して/proc/self/fd



、プロセスの名前名にファイルを作成し、を使用して必要なデータをそのファイルに ロードします write



Rustとのlibcバインディング理解してすべてを機能させるのにかなりの時間を費やしましたが、 渡されるデータタイプを理解するのは困難でした。これらのRustバインディングに関するドキュメントはあまり役に立ちませんでした。



しかし、私はなんとかうまくいくことができました。



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



Enterキーを押す だけ ./my-jar.jar



で、プロセスが呼び出さ java



れてJARが実行されます。ただし、ファイル my-jar.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画像を作成するというアイデアに誘惑され、少し開発しましたが、プロジェクトはまだ興味深いものでした。ソフトウェアを画像として配布できることには、エキサイティングなことがあります。前面にグラフィックが付いたPCソフトウェアのファンキーな段ボール箱を考えてみてください。それらを持ち帰ってみませんか? (実際には価値がありませんが。)



このプロジェクトは非常に馬鹿げていて、完全に無意味で実用的でない多くの欠陥があります。主な欠陥は、それがマシン上で動作するための愚かなプログラムがなければならないということ 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!






これはおそらくファイルが匿名である結果であり、私がそれを研究することに興味があれば問題は解決することができます。



このプロジェクトが愚かである理由



多くのバイナリは非常に大きく、画像に書き込む必要があることを考えると、グラフィックのサイズを大きくする必要が あり、結果のファイルはコミカルに巨大になります。



さらに、ほとんどのソフトウェアは1つの実行可能ファイルだけで構成されていないため、ゲームなどのより複雑なプログラムの場合、PNGを配布するという夢は失敗します。



結論



これはおそらく、私が今年に働いてきた非常識なプロジェクトですが、それは確かに楽しかった、私はステガノグラフィーについて学び memfd_create



binfmt_misc



そしてもう少し錆で遊ん。



All Articles