バイナリ操作奮闘記 前編(アセンブリ/手書き編)

最近以下のテキストを読みながら C コンパイラを書いています

www.sigbus.info

書いてるうちにふと疑問がわいてきました

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

retpop してそのアドレスに飛ぶだけだから別にそれそのものが 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

github.com

ありがたいことに実際バイナリを触っているところのコードも公開されていますので参考にしていきます

というところで、永遠に書き終わらない気がしてきたので一旦中断
まだ readelf の出力内容などは全然理解できてないけども、とりあえず色々いじってみる

後編へつづく(かも...)