バイナリ操作奮闘記 前編(アセンブリ/手書き編)
最近以下のテキストを読みながら C コンパイラを書いています
書いてるうちにふと疑問がわいてきました
9cc はアセンブリを出力しているけども実際の実行ファイルにするために gcc 挟んでいるし、アセンブリを ELF 形式の実行ファイルにするプロセスはどうすればいいんだ?
// そもそもテキストでも触れられていたのですが、気付いたのは試行錯誤の後...
というわけで、色々調べながら試行錯誤したのでそのログを残します
アセンブリと機械語は 1:1 で対応しているので変換表があれば手で変換できます
なのであとやるべきことは ELF 形式の実行ファイルの作り方さえ分かれば Linux 上で実行可能なファイルを作成できます
ELF 形式
そもそも ELF 形式、名前は知っているけども中身はよくわからんという状態だったので調べます
Linux kernel が実行ファイルを実行するために必要な情報をヘッダに入れて、実際の機械語は .text に入っています
ヘッダに何を指定すればいいのかを理解するためにはさらに色々調べる必要がありそうだったので、ヘッダは適当に何か簡単なアセンブリをアセンブルして手に入れることにしました
ヘッダにはセクションの長さを保持する部分があるので、まずは機械語の長さは変えないようにバイナリエディタで書き換えてみます
最低限のアセンブリを用意する
とりあえずテキストで最初のほうにある
tmp.s
.intel_syntax noprefix .global main main: mov rax, 42 ret
これを基にしていきます
これは実行すると exit code 42 が返ってくるだけのコードです
とりあえず、テキストでそれまでやっていたように gcc を使ってアセンブルします
$ gcc -o tmp tmp.s $ ./tmp $ echo $? 42
ELF 形式のヘッダを見てみます
$ readelf -a tmp ... Symbol table '.symtab' contains 61 entries: ... 26: 0000000000000000 0 FILE LOCAL DEFAULT ABS crtstuff.c 27: 0000000000001070 0 FUNC LOCAL DEFAULT 13 deregister_tm_clones 28: 00000000000010a0 0 FUNC LOCAL DEFAULT 13 register_tm_clones 29: 00000000000010e0 0 FUNC LOCAL DEFAULT 13 __do_global_dtors_aux 30: 0000000000004028 1 OBJECT LOCAL DEFAULT 24 completed.7447 31: 0000000000003e20 0 OBJECT LOCAL DEFAULT 19 __do_global_dtors_aux_fin 32: 0000000000001120 0 FUNC LOCAL DEFAULT 13 frame_dummy 33: 0000000000003e18 0 OBJECT LOCAL DEFAULT 18 __frame_dummy_init_array_ 34: 0000000000000000 0 FILE LOCAL DEFAULT ABS crtstuff.c 35: 000000000000211c 0 OBJECT LOCAL DEFAULT 17 __FRAME_END__ 36: 0000000000000000 0 FILE LOCAL DEFAULT ABS 37: 0000000000003e20 0 NOTYPE LOCAL DEFAULT 18 __init_array_end 38: 0000000000003e28 0 OBJECT LOCAL DEFAULT 20 _DYNAMIC 39: 0000000000003e18 0 NOTYPE LOCAL DEFAULT 18 __init_array_start 40: 0000000000002004 0 NOTYPE LOCAL DEFAULT 16 __GNU_EH_FRAME_HDR 41: 0000000000004000 0 OBJECT LOCAL DEFAULT 22 _GLOBAL_OFFSET_TABLE_ 42: 0000000000001000 0 FUNC LOCAL DEFAULT 10 _init 43: 0000000000001190 1 FUNC GLOBAL DEFAULT 13 __libc_csu_fini 44: 0000000000000000 0 NOTYPE WEAK DEFAULT UND _ITM_deregisterTMCloneTab 45: 0000000000004018 0 NOTYPE WEAK DEFAULT 23 data_start 46: 0000000000004028 0 NOTYPE GLOBAL DEFAULT 23 _edata 47: 0000000000001194 0 FUNC GLOBAL HIDDEN 14 _fini 48: 0000000000000000 0 FUNC GLOBAL DEFAULT UND __libc_start_main@@GLIBC_ 49: 0000000000004018 0 NOTYPE GLOBAL DEFAULT 23 __data_start 50: 0000000000000000 0 NOTYPE WEAK DEFAULT UND __gmon_start__ 51: 0000000000004020 0 OBJECT GLOBAL HIDDEN 23 __dso_handle 52: 0000000000002000 4 OBJECT GLOBAL DEFAULT 15 _IO_stdin_used 53: 0000000000001130 93 FUNC GLOBAL DEFAULT 13 __libc_csu_init 54: 0000000000004030 0 NOTYPE GLOBAL DEFAULT 24 _end 55: 0000000000001040 43 FUNC GLOBAL DEFAULT 13 _start 56: 0000000000004028 0 NOTYPE GLOBAL DEFAULT 24 __bss_start 57: 0000000000001125 0 NOTYPE GLOBAL DEFAULT 13 main 58: 0000000000004028 0 OBJECT GLOBAL HIDDEN 23 __TMC_END__ 59: 0000000000000000 0 NOTYPE WEAK DEFAULT UND _ITM_registerTMCloneTable 60: 0000000000000000 0 FUNC WEAK DEFAULT UND __cxa_finalize@@GLIBC_2.2 ...
ん? なんか libc の云々が呼ばれてない?
これでは該当の部分を抽出して操作し辛いので、他の方法を調べる
Gas(GNU assembler) を使う
C と関係ない純粋な(?)アセンブラに投げればいいのでは、と思ったので調べると GNU assembler というのがあったので使ってみる
$ as -o tmp tmp.s $ chmod 755 tmp $ ./tmp Failed to execute process './tmp'. Reason: exec: Exec format error The file './tmp' is marked as an executable but could not be run by the operating system.
なるほど、これだけではだめなのか...
適当に使い方をググっている内に ld
も使ってやらないといけなそうであることが分かった
// どこでその記述を見付けたのか忘れてしまった...
$ as -o tmp.o tmp.s $ ld -o tmp tmp.o $ ./tmp Segmentation fault
なるほどわからん
とりあえずヘッダ見てみる
ELF Header: Magic: 7f 45 4c 46 02 01 01 00 00 00 00 00 00 00 00 00 Class: ELF64 Data: 2's complement, little endian Version: 1 (current) OS/ABI: UNIX - System V ABI Version: 0 Type: EXEC (Executable file) Machine: Advanced Micro Devices X86-64 Version: 0x1 Entry point address: 0x401000 Start of program headers: 64 (bytes into file) Start of section headers: 4336 (bytes into file) Flags: 0x0 Size of this header: 64 (bytes) Size of program headers: 56 (bytes) Number of program headers: 2 Size of section headers: 64 (bytes) Number of section headers: 5 Section header string table index: 4 Section Headers: [Nr] Name Type Address Offset Size EntSize Flags Link Info Align [ 0] NULL 0000000000000000 00000000 0000000000000000 0000000000000000 0 0 0 [ 1] .text PROGBITS 0000000000401000 00001000 0000000000000008 0000000000000000 AX 0 0 1 [ 2] .symtab SYMTAB 0000000000000000 00001008 00000000000000a8 0000000000000018 3 2 8 [ 3] .strtab STRTAB 0000000000000000 000010b0 000000000000001e 0000000000000000 0 0 1 [ 4] .shstrtab STRTAB 0000000000000000 000010ce 0000000000000021 0000000000000000 0 0 1 Key to Flags: W (write), A (alloc), X (execute), M (merge), S (strings), I (info), L (link order), O (extra OS processing required), G (group), T (TLS), C (compressed), x (unknown), o (OS specific), E (exclude), l (large), p (processor specific) There are no section groups in this file. Program Headers: Type Offset VirtAddr PhysAddr FileSiz MemSiz Flags Align LOAD 0x0000000000000000 0x0000000000400000 0x0000000000400000 0x00000000000000b0 0x00000000000000b0 R 0x1000 LOAD 0x0000000000001000 0x0000000000401000 0x0000000000401000 0x0000000000000008 0x0000000000000008 R E 0x1000 Section to Segment mapping: Segment Sections... 00 01 .text There is no dynamic section in this file. There are no relocations in this file. The decoding of unwind sections for machine type Advanced Micro Devices X86-64 is not currently supported. Symbol table '.symtab' contains 7 entries: Num: Value Size Type Bind Vis Ndx Name 0: 0000000000000000 0 NOTYPE LOCAL DEFAULT UND 1: 0000000000401000 0 SECTION LOCAL DEFAULT 1 2: 0000000000000000 0 NOTYPE GLOBAL DEFAULT UND _start 3: 0000000000402000 0 NOTYPE GLOBAL DEFAULT 1 __bss_start 4: 0000000000401000 0 NOTYPE GLOBAL DEFAULT 1 main 5: 0000000000402000 0 NOTYPE GLOBAL DEFAULT 1 _edata 6: 0000000000402000 0 NOTYPE GLOBAL DEFAULT 1 _end No version information found in this file.
シンボルテーブルにまだ何か付いてるのは見えるけど、だいぶましになった
けど Segmentation fault になるのは何でかよくわからん
さらに調べていると以下の資料に行きついた
http://ankokudan.org/d/dl/pdf/pdf-linuxasm.pdf
ret
は pop
してそのアドレスに飛ぶだけだから別にそれそのものが exit code になる訳ではない
Linux kernel に exit code として伝えるためには exit システムコールを叩いてやらないといけなくて、gcc によるアセンブルで前後にくっついていた諸々はその辺りの処理もしてくれてたんだろうなぁ
という気付きを得たので、元のアセンブリを修正します
.intel_syntax noprefix .global main main: mov eax, 1 mov ebx, 42 int 0x80
先の資料では ebx
に 0 を入れて正常終了と書いていたので、多分 ebx
の値が exit code になるだろうということで入れてみました
というわけでアセンブルする
$ as -o tmp.o tmp.s $ ld -o tmp tmp.o $ ./tmp $ echo $? 42
やったーできた
ヘッダも見ていきます
$ readelf -a tmp ELF Header: Magic: 7f 45 4c 46 02 01 01 00 00 00 00 00 00 00 00 00 Class: ELF64 Data: 2's complement, little endian Version: 1 (current) OS/ABI: UNIX - System V ABI Version: 0 Type: EXEC (Executable file) Machine: Advanced Micro Devices X86-64 Version: 0x1 Entry point address: 0x401000 Start of program headers: 64 (bytes into file) Start of section headers: 4344 (bytes into file) Flags: 0x0 Size of this header: 64 (bytes) Size of program headers: 56 (bytes) Number of program headers: 2 Size of section headers: 64 (bytes) Number of section headers: 5 Section header string table index: 4 Section Headers: [Nr] Name Type Address Offset Size EntSize Flags Link Info Align [ 0] NULL 0000000000000000 00000000 0000000000000000 0000000000000000 0 0 0 [ 1] .text PROGBITS 0000000000401000 00001000 000000000000000c 0000000000000000 AX 0 0 1 [ 2] .symtab SYMTAB 0000000000000000 00001010 00000000000000a8 0000000000000018 3 2 8 [ 3] .strtab STRTAB 0000000000000000 000010b8 000000000000001e 0000000000000000 0 0 1 [ 4] .shstrtab STRTAB 0000000000000000 000010d6 0000000000000021 0000000000000000 0 0 1 Key to Flags: W (write), A (alloc), X (execute), M (merge), S (strings), I (info), L (link order), O (extra OS processing required), G (group), T (TLS), C (compressed), x (unknown), o (OS specific), E (exclude), l (large), p (processor specific) There are no section groups in this file. Program Headers: Type Offset VirtAddr PhysAddr FileSiz MemSiz Flags Align LOAD 0x0000000000000000 0x0000000000400000 0x0000000000400000 0x00000000000000b0 0x00000000000000b0 R 0x1000 LOAD 0x0000000000001000 0x0000000000401000 0x0000000000401000 0x000000000000000c 0x000000000000000c R E 0x1000 Section to Segment mapping: Segment Sections... 00 01 .text There is no dynamic section in this file. There are no relocations in this file. The decoding of unwind sections for machine type Advanced Micro Devices X86-64 is not currently supported. Symbol table '.symtab' contains 7 entries: Num: Value Size Type Bind Vis Ndx Name 0: 0000000000000000 0 NOTYPE LOCAL DEFAULT UND 1: 0000000000401000 0 SECTION LOCAL DEFAULT 1 2: 0000000000000000 0 NOTYPE GLOBAL DEFAULT UND _start 3: 0000000000402000 0 NOTYPE GLOBAL DEFAULT 1 __bss_start 4: 0000000000401000 0 NOTYPE GLOBAL DEFAULT 1 main 5: 0000000000402000 0 NOTYPE GLOBAL DEFAULT 1 _edata 6: 0000000000402000 0 NOTYPE GLOBAL DEFAULT 1 _end No version information found in this file.
なんかよさそう
とりあえず、作ったバイナリを手でいじっていきます
.text セクションに機械語が入っているということは、このコード上の特徴的な数字である 42 (== 0x2a) があってかつその付近に != 0 なバイト列が固まっているところを探せばいいはずです
ここで著名なバイナリエディタである vim を開いて操作します
$ vim -b tmp :%!xxd
バッファを xxd コマンドに食わせて人間が読めるように変換します
00001000: b801 0000 00bb 2a00 0000 cd80 0000 0000 ......*......... 00001010: 0000 0000 0000 0000 0000 0000 0000 0000 ................ 00001020: 0000 0000 0000 0000 0000 0000 0300 0100 ................ ...
ありましたね
とりあえず 0x2b
に変更してみます
00001000: b801 0000 00bb 2b00 0000 cd80 0000 0000 ......*......... 00001010: 0000 0000 0000 0000 0000 0000 0000 0000 ................ 00001020: 0000 0000 0000 0000 0000 0000 0300 0100 ................
あとはバイナリに書き戻します
:%!xxd -r
保存して再度実行してみます
$ ./tmp $ echo $? 43
ちゃんと 0x2b (== 43) に変更されましたね
同じことを Go からやってみます
Go には encode/binary と debug/elf package があり、これらを使うことでバイナリを Go から編集できます
以下の資料を読めばどうやって触ればよいかが分かります
www.slideshare.net
ありがたいことに実際バイナリを触っているところのコードも公開されていますので参考にしていきます
というところで、永遠に書き終わらない気がしてきたので一旦中断
まだ readelf
の出力内容などは全然理解できてないけども、とりあえず色々いじってみる
後編へつづく(かも...)