はじめに
流行ってるネタへの便乗です。
手っ取り早く実演
動作確認環境として 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 |....@.8...@.....|
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' の内容を工夫することでセグフォ以外のエラーを出すことも可能です。
$ 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)
$
$ 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)
$
$ 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)
$
いろいろ工夫してみるのも楽しいですね。
おわりに
おわりです。