elf.hを読んで実行可能ファイルを直書きする
Linux で動く極小 ELF 実行ファイルをつくる怒涛のチュートリアルを読んで
僕の環境はx86_64なのでx86_64ネイティブな実行可能ファイルを作る.もちろんlinux環境.
リンカ・ローダ実践開発テクニック
Linkers & Loaders
などを読めばここに書いてあることが更に深く書いてあるので買いましょう.
最終的にこれができれば,static linkされた実行可能ファイルが作り放題なので,自作コンパイラでバイナリをelf形式で吐いて,それを実行することが理論上可能になります.
ELF とは
ELF Formatについてやman elfを読みましょう.
簡単に言うと,linuxで使われている実行ファイル形式です.
elf header(必須),program header(任意), section header(任意), 中身(任意),で構成されます.
program headerが必要になるのは実行ファイルをロードしてOSがそのファイルを実行するとき,
section headerが必要になるのはリンカが外部シンボルの解決や複数のオブジェクトファイルを1つにまとめるとき,
に使用されます.
重要なのはsection headerはファイルを実行するときには必要ないということです.
なので, elf header, program header, 実行する機械語,の3つを書いていきます.
elf header
man elf すれば書いてあります.
typedef struct
{
unsigned char e_ident[EI_NIDENT]; /* Magic number and other info */
Elf64_Half e_type; /* Object file type */
Elf64_Half e_machine; /* Architecture */
Elf64_Word e_version; /* Object file version */
Elf64_Addr e_entry; /* Entry point virtual address */
Elf64_Off e_phoff; /* Program header table file offset */
Elf64_Off e_shoff; /* Section header table file offset */
Elf64_Word e_flags; /* Processor-specific flags */
Elf64_Half e_ehsize; /* ELF header size in bytes */
Elf64_Half e_phentsize; /* Program header table entry size */
Elf64_Half e_phnum; /* Program header table entry count */
Elf64_Half e_shentsize; /* Section header table entry size */
Elf64_Half e_shnum; /* Section header table entry count */
Elf64_Half e_shstrndx; /* Section header string table index */
} Elf64_Ehdr;
では,バイナリエディタを開いて最初からelf headerを書いていきます.僕はxxdとvimを使いました.
これらのメンバをelf.hを読みながらひたすら埋めていきます.型はelf.hのtypedefを見ればintN_tがtypedefされてるとわかるので,適宜補完してください.
長いので→のリンクから飛ぶと結論だけ見られます.
完成形はこれになります
+-----------------------------------------------+
|e_ident[EI_NIDENT] |
+-----------------------------------------------+
|7f 45 4c 46 02 01 01 00 00 00 00 00 00 00 00 00|
+-----+-----+-----------+-----------------------+
|type |mach |ver |e_entry |
+-----+-----+-----------+-----------------------+
|02 00|3e 00|01 00 00 00|78 00 40 00 00 00 00 00|
+-----+-----+-----------+-----------------------+
|e_phoff |e_shoff |
+-----------------------+-----------------------+
|40 00 00 00 00 00 00 00|00 00 00 00 00 00 00 00|
+-----------+-----+-----+-----+-----+-----+-----+
|e_flags |ehsiz|phesi|phnum|shesi|shnum|shstr|
+-----------+-----+-----+-----+-----+-----+-----+
|00 00 00 00|40 00|38 00|01 00|40 00|00 00|00 00|
+-----------+-----+-----+-----+-----+-----+-----+
e_ident
7f45 4c46 0201 0100 0000 0000 0000 0000
elf.hにEI_NIDENTが16と定義されていたのでe_identは16byte.
/* Fields in the e_ident array. The EI_* macros are indices into the
array. The macros under each EI_* macro are the values the byte
may have. */
と書かれていたので,EI_*を見つつ適切な数値を書き込みます.
#define EI_MAG0 0 /* File identification byte 0 index */
#define ELFMAG0 0x7f /* Magic number byte 0 */
#define EI_MAG1 1 /* File identification byte 1 index */
#define ELFMAG1 'E' /* Magic number byte 1 */
#define EI_MAG2 2 /* File identification byte 2 index */
#define ELFMAG2 'L' /* Magic number byte 2 */
#define EI_MAG3 3 /* File identification byte 3 index */
#define ELFMAG3 'F' /* Magic number byte 3 */
とあるので,最初の4byteは7f 45 4c 46
. いわゆるマジックナンバー.
#define EI_CLASS 4 /* File class byte index */
#define ELFCLASSNONE 0 /* Invalid class */
#define ELFCLASS32 1 /* 32-bit objects */
#define ELFCLASS64 2 /* 64-bit objects */
僕の環境は64bitなので02
.
#define EI_DATA 5 /* Data encoding byte index */
#define ELFDATANONE 0 /* Invalid data encoding */
#define ELFDATA2LSB 1 /* 2's complement, little endian */
#define ELFDATA2MSB 2 /* 2's complement, big endian */
x86_64は01
.
#define EI_VERSION 6 /* File version byte index */
/* Value must be EV_CURRENT */
EV_CURRENTは1なので01
.
#define EI_OSABI 7 /* OS ABI identification */
#define ELFOSABI_NONE 0 /* UNIX System V ABI */
(snip)
#define ELFOSABI_GNU 3 /* Object uses GNU ELF extensions. */
(snip)
なので00
.03
でもいいかもしれない.
#define EI_ABIVERSION 8 /* ABI version */
#define EI_PAD 9 /* Byte index of padding bytes */
ここからは使われていないので,00 00 00 00 00 00 00 00 00
で埋めます.
よって,最初の7f45 4c46 0201 0100 0000 0000 0000 0000
が得られます.
e_type
02 00
/* Legal values for e_type (object file type). */
#define ET_NONE 0 /* No file type */
#define ET_REL 1 /* Relocatable file */
#define ET_EXEC 2 /* Executable file */
#define ET_DYN 3 /* Shared object file */
#define ET_CORE 4 /* Core file */
とあるので,実行形式の2を選びます.
e_machine
3e 00
/* Legal values for e_machine (architecture). */
#define EM_NONE 0 /* No machine */
(snip)
#define EM_X86_64 62 /* AMD x86-64 architecture *
(snip)
とあるので,3e 00
.つーかアーキテクチャ多すぎでしょ.
e_version
01 00 00 00
/* Legal values for e_version (version). */
#define EV_NONE 0 /* Invalid ELF version */
#define EV_CURRENT 1 /* Current version */
なので01 00 00 00
.簡単.
e_entry
78 00 40 00 00 00 00 00
理由は普通の実行形式(gccが普通に吐く奴)のe_entryを見たら全部0x400000付近だったからと,後に出てくるp_alignとの整合性を取るため.
なんで0x400000なのか理由を知ってる人がいたら教えて下さい.(リンカスクリプトでtext_segmentが0x400000から始まるのは知ってる)
e_phoff
40 00 00 00 00 00 00 00
program headerはelf headerの直後にくっつけるので,sizeof(Elf64_Ehdr)==0x40.
e_shoff
00 00 00 00 00 00 00 00
使わないので0
e_flags
とりあえず00 00 00 00
理由は普通の実行形式(gccが普通に吐く奴)のe_flagsを見たら全部0だったから.
e_ehsize
40 00
sizeof(Elf64_Ehdr)==0x40だから
e_phentsize
38 00
sizeof(Elf64_Phdr)==0x38だから
e_phnum
01 00 00 00
とりあえず,read,write,exec フラグの付いた領域を1つだけ確保するので1にします.
将来的にdata領域とtext領域を分けたいという時は,ここを2にして追加のprogram headerを書きましょう.
e_shentsize
40 00
sizeof(Elf64_Shdr)==0x40だから
e_shnum, e_shstrndx
00 00
使わないので0
program header
typedef struct
{
Elf64_Word p_type; /* Segment type */
Elf64_Word p_flags; /* Segment flags */
Elf64_Off p_offset; /* Segment file offset */
Elf64_Addr p_vaddr; /* Segment virtual address */
Elf64_Addr p_paddr; /* Segment physical address */
Elf64_Xword p_filesz; /* Segment size in file */
Elf64_Xword p_memsz; /* Segment size in memory */
Elf64_Xword p_align; /* Segment alignment */
} Elf64_Phdr;
完成形はこちら
+-----------+-----------+-----------------------+
|p_type |p_flags |p_offset |
+-----------+-----------+-----------------------+
|01 00 00 00|07 00 00 00|00 00 00 00 00 00 00 00|
+-----------+-----------+-----------------------+
|p_vaddr |p_paddr |
+-----------------------+-----------------------+
|00 00 40 00 00 00 00 00|00 00 40 00 00 00 00 00|
+-----------------------+-----------------------+
|p_filesz |p_memsz |
+-----------------------+-----------------------+
|90 00 00 00 00 00 00 00|90 00 00 00 00 00 00 00|
+-----------------------+-----------------------+
|p_align |
+-----------------------+
|00 00 20 00 00 00 00 00|
+-----------------------+
p_type
01 00 00 00
/* Legal values for p_type (segment type). */
#define PT_NULL 0 /* Program header table entry unused */
#define PT_LOAD 1 /* Loadable program segment */
#define PT_DYNAMIC 2 /* Dynamic linking information */
#define PT_INTERP 3 /* Program interpreter */
#define PT_NOTE 4 /* Auxiliary information */
#define PT_SHLIB 5 /* Reserved */
#define PT_PHDR 6 /* Entry for header table itself */
#define PT_TLS 7 /* Thread-local storage segment */
#define PT_NUM 8 /* Number of defined types */
と書いてあるので,1を書きます.
p_flags
07 00 00 00
/* Legal values for p_flags (segment flags). */
#define PF_X (1 << 0) /* Segment is executable */
#define PF_W (1 << 1) /* Segment is writable */
#define PF_R (1 << 2) /* Segment is readable */
と書いてるので,RWXが欲しいので,1+2+4=7を書きます.
p_offset
00 00 00 00 00 00 00 00
とりあえず0.理由はp_vaddrとうまく合わせるためです.こうするとelf headerの最初からセグメントが始まってしまいます.これを正しいオフセットにするにはp_alignの説明を見るとどうすればいいか書いてあります.
p_vaddr, p_paddr
00 00 40 00 00 00 00 00
理由は普通の実行形式が0x400000だったからです.普通の実行形式なので物理アドレスと仮想アドレスは一致します.ココらへんが詳しく知りたかったら,上記の参考書籍と読むと良いでしょう.
こうするとp_alignの所で言及されている式と一致します.
p_filesz, p_memsz
90 00 00 00 00 00 00 00
bss領域などはfilesz<memszですが,今回はさらなるメモリ領域は使わないので,とりあえず0x90にします.後で実際のファイルサイズが確定したら変更します.
p_align
00 00 20 00 00 00 00 00
理由は普通の実行形式のp_alignが0x200000だったからです.
man elf によると,
p_align
このメンバは、セグメントがメモリーおよびファイルにおいて配置 (align) される値を保持する。 ロード可能プロセスセグメントは、ページサイズを法として p_vaddr と p_offset と合同でなければならない (訳注:「p_vaddr mod ページサイズ = p_offset mod ページサイズ」 でなければならない)。。 0 と 1 という値は配置が必要ないことを意味する。 それ以外の場合、 p_align は正で 2 の整数乗でなければならず、 p_vaddr は p_align を法として p_offset と合同でなければならない (訳注:「p_vaddr mod p_align = p_offset mod p_align」でなければならない)。
とあるので,要するにp_vaddr==p_offsetにするのが一番簡単なので,一致させます.
このため,本当はp_offsetはsizeof(Elf64_Ehdr)+sizeof(Elf64_Phdr)=0x78から始まってもいいのですが,こうすると,p_vaddrも0x400078にする必要があります.
まあ結局エントリポイントの値はこのoffsetぶんずれてしまうので,p_fileszを本当の機械語が占めるサイズにするか実行ファイルのサイズにするかは好みの問題だ思います.
中身
b8 01 00 00 00 bb 01 00 00 00 cd 80
linuxシステムコールのexitを呼び出す命令を実行させましょう.
システムコールの呼び出し方は,%eaxに1, %ebxに返り値を入れて,int $0x80するだけです.
movl $1, %eax
movl $1, %ebx
int $0x80
としてasコマンドでアセンブルして,objdump -dでディスアセンブルしたものをprogram headerの後に書き込みます.
ディスアセンブル結果は
0: b8 01 00 00 00 mov $0x1,%eax
5: bb 01 00 00 00 mov $0x1,%ebx
a: cd 80 int $0x80
らしいので,書き込むバイト列はb8 01 00 00 00 bb 01 00 00 00 cd 80
になります.
全体像
elf header とprogram header と中身を全部くっつける.合計0x84バイトなのでp_fileszと
p_memszを0x84にします.
+-----------------------------------------------+
|e_ident[EI_NIDENT] |
+-----------------------------------------------+
|7f 45 4c 46 02 01 01 00 00 00 00 00 00 00 00 00|
+-----+-----+-----------+-----------------------+
|type |mach |ver |e_entry |
+-----+-----+-----------+-----------------------+
|02 00|3e 00|01 00 00 00|78 00 40 00 00 00 00 00|
+-----+-----+-----------+-----------------------+
|e_phoff |e_shoff |
+-----------------------+-----------------------+
|40 00 00 00 00 00 00 00|00 00 00 00 00 00 00 00|
+-----------+-----+-----+-----+-----+-----+-----+
|e_flags |ehsiz|phesi|phnum|shesi|shnum|shstr|
+-----------+-----+-----+-----+-----+-----+-----+
|00 00 00 00|40 00|38 00|01 00|40 00|00 00|00 00|
+-----------+-----+-----+-----+-----+-----+-----+
|p_type |p_flags |p_offset |
+-----------+-----------+-----------------------+
|01 00 00 00|07 00 00 00|00 00 00 00 00 00 00 00|
+-----------+-----------+-----------------------+
|p_vaddr |p_paddr |
+-----------------------+-----------------------+
|00 00 40 00 00 00 00 00|00 00 40 00 00 00 00 00|
+-----------------------+-----------------------+
|p_filesz |p_memsz |
+-----------------------+-----------------------+
|84 00 00 00 00 00 00 00|84 00 00 00 00 00 00 00|
+-----------------------+-----------------------+
|p_align |mov $1, %eax |mov $1, |
+-----------------------+--------------+--------+
|00 00 20 00 00 00 00 00|b8 01 00 00 00|bb 01 00|
+---------------+-------+--------------+--------+
|%ebx |int $0x80|
+-----+-----+---+
|00 00|cd 80|
+-----+-----+
xxdの出力
00000000: 7f45 4c46 0201 0100 0000 0000 0000 0000 .ELF............
00000010: 0200 3e00 0100 0000 7800 4000 0000 0000 ..>.....x.@.....
00000020: 4000 0000 0000 0000 0000 0000 0000 0000 @...............
00000030: 0000 0000 4000 3800 0100 4000 0000 0000 ....@.8...@.....
00000040: 0100 0000 0700 0000 0000 0000 0000 0000 ................
00000050: 0000 4000 0000 0000 0000 4000 0000 0000 ..@.......@.....
00000060: 8400 0000 0000 0000 8400 0000 0000 0000 ................
00000070: 0000 2000 0000 0000 b801 0000 00bb 0100 .. .............
00000080: 0000 cd80 ....
これを./a.outなどの名前で保存し,シェルで実行するとエラーが起きずに実行できます.
また,mov $1, %ebx
の1を適当に変えて実行し,echo $?
として返り値を見てみると変更した値になっているので,正しく終了していることもわかります.
これができれば,static linkされた実行可能ファイルが作り放題なので,自作コンパイラでバイナリをelf形式で吐いて,それを実行することが可能になります.