gcc で 0文字でセグフォらせる


はじめに

流行ってるネタへの便乗です。

手っ取り早く実演

動作確認環境として x86-64 Ubuntu 19.04 + gcc 8.3.0 を使用しています。

ビルドと実行
$ gcc -xc /dev/null -nostdlib -Wl,-e0xfa -Wl,-Ttext=0x2504890000 -zexecstack && ./a.out
Segmentation fault (core dumped)
$ 

解説

まずコンパイルして警告が出ない程度の内容の最小の C プログラムをコンパイルしてみます。

int main(){}

ソースコードを用意するのは面倒なので echo コマンドを使用して | でコンパイラと繋ぎ、入力を標準入力とする - として直接入力することとします。コンパイラ側で入力ファイルの拡張子で入力ファイルの種別が判定できないため、C のソースであることを通知するコマンドラインオプション `-xc' を使用します。

$ echo 'int main(){}' | gcc -xc - && ls -l a.out
-rwxr-xr-x 1 fujita fujita 16344 Jul  8 22:31 a.out
$ 

16kB 程の実行ファイルができました。生成された a.out を readelf コマンドに食わせてみると

$ readelf -S a.out
There are 28 section headers, starting at offset 0x38d8:

Section Headers:
  [Nr] Name              Type             Address           Offset
       Size              EntSize          Flags  Link  Info  Align
  [ 0]                   NULL             0000000000000000  00000000
       0000000000000000  0000000000000000           0     0     0
  [ 1] .interp           PROGBITS         00000000000002a8  000002a8
       000000000000001c  0000000000000000   A       0     0     1
  [ 2] .note.gnu.build-i NOTE             00000000000002c4  000002c4
       0000000000000024  0000000000000000   A       0     0     4
  [ 3] .note.ABI-tag     NOTE             00000000000002e8  000002e8
       0000000000000020  0000000000000000   A       0     0     4
  [ 4] .gnu.hash         GNU_HASH         0000000000000308  00000308
       0000000000000024  0000000000000000   A       5     0     8
  [ 5] .dynsym           DYNSYM           0000000000000330  00000330
       0000000000000090  0000000000000018   A       6     1     8
  [ 6] .dynstr           STRTAB           00000000000003c0  000003c0
       000000000000007d  0000000000000000   A       0     0     1
  [ 7] .gnu.version      VERSYM           000000000000043e  0000043e
       000000000000000c  0000000000000002   A       5     0     2
  [ 8] .gnu.version_r    VERNEED          0000000000000450  00000450
       0000000000000020  0000000000000000   A       6     1     8
  [ 9] .rela.dyn         RELA             0000000000000470  00000470
       00000000000000c0  0000000000000018   A       5     0     8
  [10] .init             PROGBITS         0000000000001000  00001000
       0000000000000017  0000000000000000  AX       0     0     4
  [11] .plt              PROGBITS         0000000000001020  00001020
       0000000000000010  0000000000000010  AX       0     0     16
  [12] .plt.got          PROGBITS         0000000000001030  00001030
       0000000000000008  0000000000000008  AX       0     0     8
  [13] .text             PROGBITS         0000000000001040  00001040
       0000000000000151  0000000000000000  AX       0     0     16
  [14] .fini             PROGBITS         0000000000001194  00001194
       0000000000000009  0000000000000000  AX       0     0     4
  [15] .rodata           PROGBITS         0000000000002000  00002000
       0000000000000004  0000000000000004  AM       0     0     4
  [16] .eh_frame_hdr     PROGBITS         0000000000002004  00002004
       000000000000003c  0000000000000000   A       0     0     4
  [17] .eh_frame         PROGBITS         0000000000002040  00002040
       0000000000000108  0000000000000000   A       0     0     8
  [18] .init_array       INIT_ARRAY       0000000000003df0  00002df0
       0000000000000008  0000000000000008  WA       0     0     8
  [19] .fini_array       FINI_ARRAY       0000000000003df8  00002df8
       0000000000000008  0000000000000008  WA       0     0     8
  [20] .dynamic          DYNAMIC          0000000000003e00  00002e00
       00000000000001c0  0000000000000010  WA       6     0     8
  [21] .got              PROGBITS         0000000000003fc0  00002fc0
       0000000000000040  0000000000000008  WA       0     0     8
  [22] .data             PROGBITS         0000000000004000  00003000
       0000000000000010  0000000000000000  WA       0     0     8
  [23] .bss              NOBITS           0000000000004010  00003010
       0000000000000008  0000000000000000  WA       0     0     1
  [24] .comment          PROGBITS         0000000000000000  00003010
       0000000000000023  0000000000000001  MS       0     0     1
  [25] .symtab           SYMTAB           0000000000000000  00003038
       00000000000005b8  0000000000000018          26    43     8
  [26] .strtab           STRTAB           0000000000000000  000035f0
       00000000000001e9  0000000000000000           0     0     1
  [27] .shstrtab         STRTAB           0000000000000000  000037d9
       00000000000000f9  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)
$ 

多くのセクションが含まれていることがわかります。

続いて、0文字のソースをコンパイルすることを試してみます。読み出すと常に EOF を返す /dev/null をソースファイル代わりに使用します。main を呼び出すスタートアップも不要なので `-nostdlib' オプションを指定します。これを使用するとデフォルトのプログラムの実行開始番地である _start が見つからなくなりリンカが警告を出すので、仮の実行開始番地として 0 番地を指定する `-Wl,-e0' オプションを使用します。

$ gcc -xc /dev/null -nostdlib -Wl,-e0 && ls -l a.out
-rwxr-xr-x 1 fujita fujita 9512 Jul  8 22:35 a.out
$ 

9.5kB ほどの実行ファイルができました。これを readelf コマンドに食わせてみると

$ readelf -S a.out
There are 12 section headers, starting at offset 0x2228:

Section Headers:
  [Nr] Name              Type             Address           Offset
       Size              EntSize          Flags  Link  Info  Align
  [ 0]                   NULL             0000000000000000  00000000
       0000000000000000  0000000000000000           0     0     0
  [ 1] .interp           PROGBITS         0000000000000200  00000200
       000000000000001c  0000000000000000   A       0     0     1
  [ 2] .note.gnu.build-i NOTE             000000000000021c  0000021c
       0000000000000024  0000000000000000   A       0     0     4
  [ 3] .gnu.hash         GNU_HASH         0000000000000240  00000240
       000000000000001c  0000000000000000   A       4     0     8
  [ 4] .dynsym           DYNSYM           0000000000000260  00000260
       0000000000000018  0000000000000018   A       5     1     8
  [ 5] .dynstr           STRTAB           0000000000000278  00000278
       0000000000000001  0000000000000000   A       0     0     1
  [ 6] .eh_frame         PROGBITS         0000000000001000  00001000
       0000000000000000  0000000000000000   A       0     0     8
  [ 7] .dynamic          DYNAMIC          0000000000001f20  00001f20
       00000000000000e0  0000000000000010  WA       5     0     8
  [ 8] .comment          PROGBITS         0000000000000000  00002000
       0000000000000023  0000000000000001  MS       0     0     1
  [ 9] .symtab           SYMTAB           0000000000000000  00002028
       0000000000000168  0000000000000018          10    12     8
  [10] .strtab           STRTAB           0000000000000000  00002190
       0000000000000027  0000000000000000           0     0     1
  [11] .shstrtab         STRTAB           0000000000000000  000021b7
       000000000000006c  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)
$ 

最小の main の例よりも少ないものの、まだ複数のセクションが実行ファイルに含まれることが分かります。

リンカへのコマンドラインオプションにはセクションの開始アドレスを指定するものがあります。試しに上のセクションのリストの中には含まれていない .text セクションの開始アドレスとして 0x123456789abcdef0 という値を指定してみましょう。下記の `-Wl,-Ttext=0x123456789abcdef0' というコマンドラインオプションがそれに該当します。

$ gcc -xc /dev/null -nostdlib -Wl,-e0 -Wl,-Ttext=0x123456789abcdef0 && ls -l a.out
-rwxr-xr-x 1 fujita fujita 9512 Jul  8 22:41 a.out
$ readelf -S a.out
There are 12 section headers, starting at offset 0x2228:

Section Headers:
  [Nr] Name              Type             Address           Offset
       Size              EntSize          Flags  Link  Info  Align
  [ 0]                   NULL             0000000000000000  00000000
       0000000000000000  0000000000000000           0     0     0
  [ 1] .interp           PROGBITS         0000000000000238  00000238
       000000000000001c  0000000000000000   A       0     0     1
  [ 2] .note.gnu.build-i NOTE             0000000000000254  00000254
       0000000000000024  0000000000000000   A       0     0     4
  [ 3] .gnu.hash         GNU_HASH         0000000000000278  00000278
       000000000000001c  0000000000000000   A       4     0     8
  [ 4] .dynsym           DYNSYM           0000000000000298  00000298
       0000000000000018  0000000000000018   A       5     1     8
  [ 5] .dynstr           STRTAB           00000000000002b0  000002b0
       0000000000000001  0000000000000000   A       0     0     1
  [ 6] .eh_frame         PROGBITS         123456789abce000  00001000
       0000000000000000  0000000000000000   A       0     0     8
  [ 7] .dynamic          DYNAMIC          123456789abcef20  00001f20
       00000000000000e0  0000000000000010  WA       5     0     8
  [ 8] .comment          PROGBITS         0000000000000000  00002000
       0000000000000023  0000000000000001  MS       0     0     1
  [ 9] .symtab           SYMTAB           0000000000000000  00002028
       0000000000000168  0000000000000018          10    12     8
  [10] .strtab           STRTAB           0000000000000000  00002190
       0000000000000027  0000000000000000           0     0     1
  [11] .shstrtab         STRTAB           0000000000000000  000021b7
       000000000000006c  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)
$ 

何のセクションか分かりませんが.eh_frameというセクションが影響を受けてそれらしいアドレスになっていることがわかります。Offset の項目の値でかアドレスの下位 16bit がdef0からe000に整列されているようです。
これらセクション情報は実行ファイル a.out に含まれている筈のものです。試しに hexdump コマンドで 16進ダンプしてみると

$ hexdump -C a.out | head -24
00000000  7f 45 4c 46 02 01 01 00  00 00 00 00 00 00 00 00  |.ELF............|
00000010  03 00 3e 00 01 00 00 00  00 00 00 00 00 00 00 00  |..>.............|
00000020  40 00 00 00 00 00 00 00  28 22 00 00 00 00 00 00  |@.......("......|
00000030  00 00 00 00 40 00 38 00  09 00 40 00 0c 00 0b 00  |[email protected]...@.....|
00000040  06 00 00 00 04 00 00 00  40 00 00 00 00 00 00 00  |........@.......|
00000050  40 00 00 00 00 00 00 00  40 00 00 00 00 00 00 00  |@.......@.......|
00000060  f8 01 00 00 00 00 00 00  f8 01 00 00 00 00 00 00  |................|
00000070  08 00 00 00 00 00 00 00  03 00 00 00 04 00 00 00  |................|
00000080  38 02 00 00 00 00 00 00  38 02 00 00 00 00 00 00  |8.......8.......|
00000090  38 02 00 00 00 00 00 00  1c 00 00 00 00 00 00 00  |8...............|
000000a0  1c 00 00 00 00 00 00 00  01 00 00 00 00 00 00 00  |................|
000000b0  01 00 00 00 04 00 00 00  00 00 00 00 00 00 00 00  |................|
000000c0  00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  |................|
000000d0  b1 02 00 00 00 00 00 00  b1 02 00 00 00 00 00 00  |................|
000000e0  00 10 00 00 00 00 00 00  01 00 00 00 04 00 00 00  |................|
000000f0  00 10 00 00 00 00 00 00  00 e0 bc 9a 78 56 34 12  |............xV4.|
00000100  00 e0 bc 9a 78 56 34 12  00 00 00 00 00 00 00 00  |....xV4.........|
00000110  00 00 00 00 00 00 00 00  00 10 00 00 00 00 00 00  |................|
00000120  01 00 00 00 06 00 00 00  20 1f 00 00 00 00 00 00  |........ .......|
00000130  20 ef bc 9a 78 56 34 12  20 ef bc 9a 78 56 34 12  | ...xV4. ...xV4.|
00000140  e0 00 00 00 00 00 00 00  e0 00 00 00 00 00 00 00  |................|
00000150  00 10 00 00 00 00 00 00  02 00 00 00 06 00 00 00  |................|
00000160  20 1f 00 00 00 00 00 00  20 ef bc 9a 78 56 34 12  | ....... ...xV4.|
00000170  20 ef bc 9a 78 56 34 12  e0 00 00 00 00 00 00 00  | ...xV4.........|
$ 

000000f8 番地から 8バイト、.eh_frameの開始アドレスらしきものがリトルエンディアンで格納されていることが分かります。
アドレスが整列されるのを避けて下位 16bit は無視するとして、`-Wl,-Ttext=0xXXXXXXXXXXXX0000'を使用することで 000000fa 番地から任意のバイト列を.eh_frameのアドレスとして実行ファイルに埋め込むことができそうです。試しとして、`-Wl,-Ttext=0x2504890000'を指定して 000000fa 番地から 89 04 25 を配置してみます。実行番地として 000000fa 番地を`-e0xfa'にて指定し、コード領域以外も実行可とする `-zexecstack' を併せて指定します。

$ gcc -xc /dev/null -nostdlib -Wl,-e0xfa -Wl,-Ttext=0x2504890000 -zexecstack && ls -l a.out
-rwxr-xr-x 1 fujita fujita 9512 Jul  8 22:49 a.out
$ ./a.out
Segmentation fault (core dumped)
$ 

生成された a.out を実行することでセグフォが発生することを確認できました。000000fa 番地から実行ファイルに埋め込んだ命令を objdump コマンドで見てみましょう。

$ objdump -D -bbinary -mi386 -Mintel,x86-64 --start-address=0xfa a.out | head -8

a.out:     file format binary


Disassembly of section .data:

000000fa <.data+0xfa>:
      fa:   89 04 25 00 00 00 00    mov    DWORD PTR ds:0x0,eax
$ 

EAX レジスタの値を 0番地へ書き込む内容となっています。これを実行しようとすることで OS の保護機能によりセグフォで終了したことが分かります。

応用編

以上で表題の『gcc で 0文字でセグフォらせる』を達成することができました。他にも、`-Wl,-Ttext=0xXXXXXXXXXXXX0000' の内容を工夫することでセグフォ以外のエラーを出すことも可能です。

デバグ用のソフトウェア割り込みINT3を呼び出す
$ gcc -xc /dev/null -nostdlib -Wl,-e0xfa -Wl,-Ttext=0xcc0000 -zexecstack && ls -l a.out
-rwxr-xr-x 1 fujita fujita 9512 Jul  8 22:55 a.out
$ objdump -D -bbinary -mi386 -Mintel,x86-64 --start-address=0xfa a.out | head -8

a.out:     file format binary


Disassembly of section .data:

000000fa <.data+0xfa>:
      fa:   cc                      int3   
$ ./a.out
Trace/breakpoint trap (core dumped)
$ 
不正命令として予約されているUD2を実行する
$ gcc -xc /dev/null -nostdlib -Wl,-e0xfa -Wl,-Ttext=0x0b0f0000 -zexecstack && ls -l a.out
-rwxr-xr-x 1 fujita fujita 9512 Jul  8 22:56 a.out
$ objdump -D -bbinary -mi386 -Mintel,x86-64 --start-address=0xfa a.out | head -8

a.out:     file format binary


Disassembly of section .data:

000000fa <.data+0xfa>:
      fa:   0f 0b                   ud2    
$ ./a.out
Illegal instruction (core dumped)
$ 
AXレジスタの値を0が格納されている0x100番地の内容で割り、0除算エラーを発生させる
$ gcc -xc /dev/null -nostdlib -Wl,-e0xfa -Wl,-Ttext=0x35f60000 -zexecstack && ls -l a.out
-rwxr-xr-x 1 fujita fujita 9512 Jul  8 22:58 a.out
$ objdump -D -bbinary -mi386 -Mintel,x86-64 --start-address=0xfa a.out | head -9

a.out:     file format binary


Disassembly of section .data:

000000fa <.data+0xfa>:
      fa:   f6 35 00 00 00 00       div    BYTE PTR [rip+0x0]        # 0x100
     100:   00 00                   add    BYTE PTR [rax],al
$ ./a.out
Floating point exception (core dumped)
$ 

いろいろ工夫してみるのも楽しいですね。

おわりに

おわりです。