main 蹂躙の説明を試みる

少し不粋ではありますが、何やってるかわかりにくい部分も多いので、解説を試みてみます。昨日だらだら書いてたんですが、途中 yupo5656 さんがご自分で解説書かれてたりしたのでかぶったりしてますがまぁ気にしない方針で。

基本的に、なぜか、拡張機能をたくさん使うとか、危っかしいコードを書く、とかに価値があるとされています。また、なぜか、普段高慢ちきな main を汚すことに快感を覚えていることも重要です。

  • 最初の私のやつ
#include <stdio.h>
__attribute__((constructor)) int main() {
    static int i = 0;
    if (i) puts("world!");
    else i = puts("hello");
}

GCC 拡張の constructor は main の前に呼ばれることになってます。現在の GCC 環境ではエントリポイントは main ではなく _start で、 main の前後に処理をはさむことができます。 .ctors や .dtors セクションを objdump -s -j .ctors などしてみるといいかもしれません。

  • 次の yupo5656 さんの
#include <stdio.h>
#include <stdlib.h>

int main;
__attribute__((constructor, destructor))
static void x() {
  if (main) puts("world!");
  else exit(main = puts("hello"));
}

2つの意味でシャレが強くなっています。まず、 world の出力は main の後に呼ばれる、 destructor に任されてます。また、 main はもはや関数ではなく、 static int i のかわりに、単なるフラグとして使われてしまってます。このままでは main で落ちてしまうので、 constructor で早くも exit しまっています。

  • 次の私の
main;
__attribute__((constructor, destructor))
static x() {
    if (main) puts("world!");
    else puts("hello", main = 195);
}

上記のヤツを少しだけ改変したのです。やっぱり main は呼んであげようよ、という感じのテーマ。 main = 195; は x86 の ret を埋めてるだけです。要は valid なコードが入ってれば、 int 変数だろうが関数呼び出しすることはできるんです、と。 puts の第二引数は非常識さを少しだけ増やしてみた感じです。 Short Code で学んだホビーノウハウです。

ちなみに main=195; と #include は私の記憶が確かなら、私が感動したコード上位2位だと思います。とても重要です。

  • 配列になった main
main[30];
__attribute__((constructor))
static x() {
    if (main[0]) {
        main[0] = 0x6c726f77;
        main[1] = 0x0a2164;
        __asm__("movl $1, %%ebx \n"
                "movl %0, %%ecx \n"
                "movl $7, %%edx \n"
                "movl $4, %%eax \n"
                "int $0x80 \n"
                :: "r"(main));
    }
    else {
        puts("hello");
        memcpy(main, x, 120);
    }
}

配列になった main が色んな用途に使われますが、最初は何も入ってません。 memcpy で関数 x だったものが main になります。そして main が呼び出される。ここからは再配置の関係で標準ライブラリが呼べません。 main の第二の用途は if (main[0]) で、既に書き換えられてるかをチェックしてます。第三の用途は数値代入で、これは world! をムリにつっこんでします。で、標準ライブラリが呼べないので __asm__ 拡張で write システムコールを呼んで表示。

  • main の自己書き換え
__attribute__((section (".data"))) main() {
    unsigned char *p = main;
    while (*p / 2 != 116) p++;
    *p++ = 0xb8; *p++ = 1; *p++ = 0; *p++ = 0; *p++ = 0;
    *p++ = 0xcd; *p++ = 0x80;
    puts("hello world!");
}

.text は (GCCが余計なことをしやが略) 書き込みが禁止されています。だから main を別のセクションに入れてやればいいだろう、という話。 section 拡張でセクションを変えています。 p は puts を呼ぶ時に call を探しています。コメントいただいた通り、条件がいいかげんなので環境に強く依存していたようです。

ちなみにこういうのは一見すごそうに見えますが objdump してからせっせと目コピしてるだけなのでたいして難しくは無いです。

  • 空気のように扱われる main

yupo5656 さんがセクション移動を上記と逆の意味で使うという話。

__attribute((section(".text"))) main = 2425393296;
_() { __attribute((constructor)) _() { puts("hello"); } puts("world!"); }

まず inner function に constructor がついているのが一味違う感じ。 main の内容は 0x90909090 、つまり nop nop nop nop です。よってそのまま下に置いてある関数に落ちて行きます。 main = 2425393296 だけだと、 .bss セクションに置かれてしまうので、下にある関数なんて無いので落ちてしまうことに注意。

  • puts をそのまま呼ぶ main

woさんのヤツです。

http://d.hatena.ne.jp/w_o/20060829#p2

main() { puts(); }

#include <sys/mman.h>
__attribute__((constructor)) init() {
  mmap( 0, 4096, PROT_READ|PROT_WRITE, MAP_FIXED|MAP_ANONYMOUS|MAP_PRIVATE, 0, 0
  strcpy( 1, "Hello World");
}

main の argc==1 がそのまま渡って、アーキテクチャの ABI によっては、 NULL 近傍の文字列を吐いてくれる、という話。 x86 だと 1 じゃなくて 0 がいってるぽいですね。次のヤツは自分のファイル自体を mmap してるわけですね。

なにかあれば下記メールアドレスへ。
shinichiro.hamaji _at_ gmail.com
shinichiro.h