hello worldなELFバイナリを出力するCのプログラム(の一番単純な奴)

こちらの記事(Binary HacksのHack #25の軽い補足)は、「インラインアセンブラをちょっとだけ使って、gccに小さなhello worldバイナリを出力させる」というお話でした。一方、小さいHello Worldが欲しかったら、gccにELF実行バイナリを出力させるのではなく、「自力でELFを吐くCのコードを書いてしまう」手もあります。ご利用のCPUのアセンブリ言語がわかるのでしたら、こっちの方法でHello Worldするのも悪くないですね。こちらの方法でしたら、「急にbrainf*ckのコンパイラが書きたくなった*1」などの非常によくあるシチュエーションにも応用が効きますしー。


ELF直書きって、なんかすごく難しいように思われていると思うんですが(いや、DSO吐いたりするのは実際面倒ですが)、hello worldくらいだったらなんてことないです。次の4つの処理を順に行えばいいだけです。

  1. ELFヘッダをwrite
  2. プログラムヘッダをwrite
  3. 文字列 "Hello World!\n" をwrite
  4. システムコールを使って文字列を出力するコードをwrite

以下にELFを吐くCプログラムの雛型的なものを示しますので、ELFゴルファーになる前の学習ネタなんかに使っていただければと。必ずしもC言語で書かなくてもいいんですが(そのほうがタイプ数は少ない)、最初はelf.hに沿ってコードを書く方がわかりやすい(ひともいる)かと。

基本方針


標準出力にELFなバイナリをwriteします。できあがったバイナリの全体をそのままメモリにロードします(カーネルにロードしてもらいます)。本当は、上記の3.と4.だけロードすればよいのですが、いろいろ面倒なんで全体を。ロードするアドレスは、LOAD_ADDRESS とします。単純に 0x00000000 でもいいですし、お好きな場所に城を建ててもいいです。

1. ELFヘッダ書き


/usr/include/elf.h に、Elf32_Ehdr という構造体があります。この構造体のメンバに適当な値をセットして、そのままwriteすればOKです。e_entryメンバ以外は、決まり文句です。e_entryには、上記4.の(ファイルオフセットではなくメモリ上での)アドレスを指定します。埋めてある定数についての詳細は、elf(5)のmanか、Binary Hacksか、Linkers&Loaders あたりを...。あーもちろん、BinaryHacks のukaiさんの記事が一番お薦めです(宣伝)。

void out_elf_header() {
  Elf32_Ehdr ehdr = {
    .e_ident     = { ELFMAG0, ELFMAG1, ELFMAG2 ,ELFMAG3,
                     ELFCLASS32, ELFDATA2LSB, EV_CURRENT, ELFOSABI_SYSV },
    .e_type      = ET_EXEC,
    .e_machine   = EM_386,
    .e_version   = EV_CURRENT,
    .e_entry     = LOAD_ADDRESS + sizeof(Elf32_Ehdr) + sizeof(Elf32_Phdr) + STRING_LEN,
    .e_phoff     = sizeof(Elf32_Ehdr),
    .e_shoff     = 0, // dummy
    .e_flags     = 0x0,
    .e_ehsize    = sizeof(Elf32_Ehdr),
    .e_phentsize = sizeof(Elf32_Phdr),
    .e_phnum     = 1,
    .e_shentsize = 0, // dummy
    .e_shnum     = 0,
    .e_shstrndx  = 0, // dummy
  };

  write(1, &ehdr, sizeof(Elf32_Ehdr));
}

2. プログラムヘッダ書き


同じく Elf32_Phdr をwriteするだけです。最低限、PT_LOAD なヘッダを一つ書けばOKです*2。p_offsetは、ファイルのどこからメモリに貼るかのオフセット値です。全体を貼るので、0x0にします。埋める値の計算が面倒なのは、p_fileszとp_memszくらいでしょうか。1.〜4.の合計サイズを書きます。p_vaddrには、ファイルをメモリのどこにロードして欲しいかを書きます。カーネル様への指示ですね。それ以外は定型文です。なお、p_offset % p_align == 0 かつ p_vaddr % p_align == 0 でないとまずかったような気がするんですが詳しくはゴルファーの皆様のページでも見てください。

void out_program_header() {
  uintptr_t code_len = (uintptr_t)&end_ - (uintptr_t)&start_;
  Elf32_Phdr phdr = {
    .p_type   = PT_LOAD,
    .p_offset = 0x0,
    .p_vaddr  = LOAD_ADDRESS,
    .p_paddr  = 0, // dummy
    .p_filesz = sizeof(Elf32_Ehdr) + sizeof(Elf32_Phdr) + STRING_LEN + code_len,
    .p_memsz  = sizeof(Elf32_Ehdr) + sizeof(Elf32_Phdr) + STRING_LEN + code_len, /* BSSが欲しいならここを増やす */
    .p_flags  = PF_R | PF_X,
    .p_align  = 0x1000,
  };

  write(1, &phdr, sizeof(Elf32_Phdr));
}

3. 文字列書き


単に、write(1, "Hello World!\n", 13); するだけです。下記コードのように、4.のコードの中に.byte疑似命令で埋め込んでもいいです。.string という疑似命令で埋めてもいいです。ということは、1.〜3. 全部を、.byte や .string で埋めてもいいですが、そういう方向に行くならgcc(gas)でなくnasmを使う方が良いと思われます。

__asm__ ("rodata_:            \r\n"
         ".byte 'h'           \r\n"
         ".byte 'e'           \r\n"
(略)
         "start_:              \r\n"
         "movl $4,  %eax      \r\n" // eax: system call number (__NR_write)
         "movl $1,  %ebx      \r\n" // ebx: fd (stdout)

4. コード書き


インラインアセンブラでいきなりコードを書き、GCCコンパイルさせたあとのコードをwriteするコードもいっしょに書きます。GCC拡張である「&&でラベル参照」技を使うとラクにそういうことができます。

__asm__ ("start_:              \r\n"
         "movl $4,  %eax      \r\n" // eax: system call number (__NR_write)
         "movl $1,  %ebx      \r\n" // ebx: fd (stdout)
         "movl $" ECX ", %ecx \r\n" // ecx: addr
         "movl $13, %edx      \r\n" // edx: len
         "int  $0x80          \r\n"
         "movl $1,  %eax      \r\n" // eax: system call number (__NR_exit)
         "movl $0,  %ebx      \r\n" // ebx: exit code
         "int  $0x80          \r\n"
         "end_:                   ");
extern char *start_, *end_;

これを用意して、

void write_code() {
  const uintptr_t code_len = (uintptr_t)&end_ - (uintptr_t)&start_;
  write(1, &rodata_, code_len);
}

これだけです*3。文字列を出力して(write)、exitします。なお、すこしでも短いバイナリが欲しい場合には、movlとかアリエえませんので、適当に改変してください。まずは116? 111? バイトくらいに改造するのがよいと思います。

ソースコード全体


131バイトのバイナリを生成するコードです。あれー、さっきより1バイト少ないな...まぁいいか。

#include <elf.h>
#include <unistd.h>  // write

#define PAGE_ALIGN(adr) ((adr) & ~(0x1000 - 1)) // 16進下3桁を切り捨てるだけ
#define LOAD_ADDRESS    PAGE_ALIGN(0x12345678)  // 0x12345000にロード
#define STRING_LEN 13

#define TO_STR(s)  TO_STR_(s)
#define TO_STR_(s) #s

#define ECX \
  TO_STR(LOAD_ADDRESS + 52 + 32) // LOAD_ADDRESS + sizeof(Elf32_Ehdr) + sizeof(Elf32_Phdr)

__asm__ ("start_:              \r\n"
         "movl $4,  %eax      \r\n" // eax: system call number (__NR_write)
         "movl $1,  %ebx      \r\n" // ebx: fd (stdout)
         "movl $" ECX ", %ecx \r\n" // ecx: addr
         "movl $13, %edx      \r\n" // edx: len
         "int  $0x80          \r\n"
         "movl $1,  %eax      \r\n" // eax: system call number (__NR_exit)
         "movl $0,  %ebx      \r\n" // ebx: exit code
         "int  $0x80          \r\n"
         "end_:                   ");
extern char *start_, *end_;

void out_elf_header() {
  Elf32_Ehdr ehdr = {
    .e_ident     = { ELFMAG0, ELFMAG1, ELFMAG2 ,ELFMAG3,
                     ELFCLASS32, ELFDATA2LSB, EV_CURRENT, ELFOSABI_SYSV },
    .e_type      = ET_EXEC,
    .e_machine   = EM_386,
    .e_version   = EV_CURRENT,
    .e_entry     = LOAD_ADDRESS + sizeof(Elf32_Ehdr) + sizeof(Elf32_Phdr) + STRING_LEN,
    .e_phoff     = sizeof(Elf32_Ehdr),
    .e_shoff     = 0, // dummy
    .e_flags     = 0x0,
    .e_ehsize    = sizeof(Elf32_Ehdr),
    .e_phentsize = sizeof(Elf32_Phdr),
    .e_phnum     = 1,
    .e_shentsize = 0, // dummy
    .e_shnum     = 0,
    .e_shstrndx  = 0, // dummy
  };

  write(1, &ehdr, sizeof(Elf32_Ehdr));
}

void out_program_header() {
  uintptr_t code_len = (uintptr_t)&end_ - (uintptr_t)&start_;
  Elf32_Phdr phdr = {
    .p_type   = PT_LOAD,
    .p_offset = 0x0,
    .p_vaddr  = LOAD_ADDRESS,
    .p_paddr  = 0, // dummy
    .p_filesz = sizeof(Elf32_Ehdr) + sizeof(Elf32_Phdr) + STRING_LEN + code_len,
    .p_memsz  = sizeof(Elf32_Ehdr) + sizeof(Elf32_Phdr) + STRING_LEN + code_len,
    .p_flags  = PF_R | PF_X,
    .p_align  = 0x1000,
  };

  write(1, &phdr, sizeof(Elf32_Phdr));
}

void out_code() {
  uintptr_t code_len = (uintptr_t)&end_ - (uintptr_t)&start_;
  write(1, &start_, code_len);
}

int main() {
  out_elf_header();
  out_program_header();
  write(1, "hello world!\n", 13);
  out_code();
  return 0;
}

実行例

% gcc -m32 -Wall hello_world_elfout.c 
% ./a.out > elf

% wc -c elf
131 elf
% chmod +x elf

% ./elf
hello world!

*1:もちろん、bfのソースを喰って直接ELFのバイナリを吐くような奴ですよ

*2:politeな(?)バイナリにしたい人はPT_GNU_STACK なヘッダをもう一つという感じになりますが、説明は省略

*3:ECXという変なマクロが使われている件について。拡張構文を使って、"c"(ほげ) で%%ecxに値をいれてしまうと、その値のセットって start_: ラベルより前で行われてしまうので、具合がよろしくないのですよ