いきなり結論
- Q. アセンブラを混ぜてコンパイルするとスタックが実行可になってしまう
- A. アセンブル時に**--noexecstack**つけとけ
はじめに
Linuxのユーザランドのプログラムをアセンブラを混ぜてコンパイルし作ると、実行時にスタックのmapの属性に「実行可」がついてしまう。という話を詳細まで追うという趣旨の雑記です。なお、Ubuntu 16.04 (gcc-5.4.0 20160609, Linux-4.4.0-59)くらいで実験しつつ、kernelのソースはLinux-4.10くらいを見ています。
dummy_label:
nop
#include <stdio.h>
#include <string.h>
int main(){
FILE *fp;
fp = fopen("/proc/self/maps", "re");
if (fp != NULL) {
char buf[256];
while (fgets(buf, sizeof(buf), fp) != NULL) {
if (strstr(buf, "[stack]") != NULL) {
puts(buf);
}
}
fclose(fp);
}
return 0;
}
[rarul@tina ~]$ gcc -c -o main.o main.c
[rarul@tina ~]$ gcc -c -o asm.o asm.S
[rarul@tina ~]$ gcc -o stack_exec main.o asm.o
[rarul@tina ~]$ ./stack_exec
7ffcd32ad000-7ffcd32ce000 rwxp 00000000 00:00 0 [stack]
実行可属性とNXビット
今時のCPU+LinuxではMMUを介してページごとにメモリにアクセスするが、この時のページの属性の1つに実行可属性がある。xとかPROT_EXECとか。kernelに入るとこれはVM_EXECとして管理されるが、最終的には、CPUごとのARCHに依存したいわゆるNXビットに結びつく。これは、CPUのハードウェアの機能として、このビットが立ったメモリの上ではプログラムを実行できないように保護する。
なんでこんなのができたかというと、C言語にありがちなスタックオーバフローをやらかしたときに、そこをついてコンピュータ命令バイナリを直接書き込みjumpすることで任意のプログラムが実行できちゃう、という古典的セキュリティ問題を起こしにくくするため。これでバッファオーバフローのセキュリティリスクは回避された・・・とはならずに現代的な攻撃につながるわけだけど、そこは今回の趣旨から外れるのでパスで。
ちなみにNXビットはx86(x86_64)の名前で、armではXNビットというようだ。
だれが実行可属性をつけるのか
で、セキュリティリスクなら問答無用でスタック実行不可にしてしまえばよいかというとそうもいかず、目的があってあえてスタック上のプログラムを実行したい人も世の中には少しだけ存在する。そういうプログラムを識別できるようにするために、GNU_STACKというELFのヘッダが作られた。
LinuxはELF形式のプログラムを実行する時に、GNU_STACK(PT_GNU_STACK)ヘッダを探し、そこの実行可属性(PF_X)を確認して、VM_EXECを立てる。
kernel/fs/binfmt_elf.c:load_elf_binary()と、kernel/fs/exec.c:setup_arg_pages()より、
784 case PT_GNU_STACK:
785 if (elf_ppnt->p_flags & PF_X)
786 executable_stack = EXSTACK_ENABLE_X;
787 else
788 executable_stack = EXSTACK_DISABLE_X;
789 break;
713 /*
714 * Adjust stack execute permissions; explicitly enable for
715 * EXSTACK_ENABLE_X, disable for EXSTACK_DISABLE_X and leave alone
716 * (arch default) otherwise.
717 */
718 if (unlikely(executable_stack == EXSTACK_ENABLE_X))
719 vm_flags |= VM_EXEC;
720 else if (executable_stack == EXSTACK_DISABLE_X)
721 vm_flags &= ~VM_EXEC;
たったVM_EXECフラグは...どこで使ってるの?ページテーブルのビットにいくハズだけど具体的な箇所を探せなかった...
.GNU-stackセクション
GNU_STACKヘッダはプログラムのリンク時に作られる。どういうポリシーで作られるのかについてはHardened/GNU stack quickstartにまとまっている。ものすごく雑に書き下すと、
- リンクするときの.oファイルすべてに**.note.GNU-stack**セクションがあると、実行不可とする
- リンクするときの.oファイルに1つでも**.note.GNU-stack**セクションが欠けていると、実行可能とする
- アセンブラは明示的に指定しない限り.note.GNU-stackが入らない
- (.cは後の章で書く)
となる。確かに、asm.oには**.note.GNU-stack**セクションが含まれていない。
[rarul@tina ~]$ readelf -e asm.o
(-----snip-----)
[Nr] Name Type Address Offset
Size EntSize Flags Link Info Align
[ 0] NULL 0000000000000000 00000000
0000000000000000 0000000000000000 0 0 0
[ 1] .text PROGBITS 0000000000000000 00000040
0000000000000001 0000000000000000 AX 0 0 1
[ 2] .data PROGBITS 0000000000000000 00000041
0000000000000000 0000000000000000 WA 0 0 1
[ 3] .bss NOBITS 0000000000000000 00000041
0000000000000000 0000000000000000 WA 0 0 1
[ 4] .shstrtab STRTAB 0000000000000000 000000cd
000000000000002c 0000000000000000 0 0 1
[ 5] .symtab SYMTAB 0000000000000000 00000048
0000000000000078 0000000000000018 6 5 8
[ 6] .strtab STRTAB 0000000000000000 000000c0
000000000000000d 0000000000000000 0 0 1
(-----snip-----)
[rarul@tina ~]$ readelf -e main.o
(-----snip-----)
[Nr] Name Type Address Offset
Size EntSize Flags Link Info Align
[ 0] NULL 0000000000000000 00000000
0000000000000000 0000000000000000 0 0 0
[ 1] .text PROGBITS 0000000000000000 00000040
00000000000000ae 0000000000000000 AX 0 0 1
[ 2] .rela.text RELA 0000000000000000 00000338
00000000000000d8 0000000000000018 I 11 1 8
[ 3] .data PROGBITS 0000000000000000 000000ee
0000000000000000 0000000000000000 WA 0 0 1
[ 4] .bss NOBITS 0000000000000000 000000ee
0000000000000000 0000000000000000 WA 0 0 1
[ 5] .rodata PROGBITS 0000000000000000 000000ee
000000000000001b 0000000000000000 A 0 0 1
[ 6] .comment PROGBITS 0000000000000000 00000109
0000000000000035 0000000000000001 MS 0 0 1
[ 7] .note.GNU-stack PROGBITS 0000000000000000 0000013e
0000000000000000 0000000000000000 0 0 1
[ 8] .eh_frame PROGBITS 0000000000000000 00000140
0000000000000038 0000000000000000 A 0 0 8
[ 9] .rela.eh_frame RELA 0000000000000000 00000410
0000000000000018 0000000000000018 I 11 8 8
[10] .shstrtab STRTAB 0000000000000000 00000428
0000000000000061 0000000000000000 0 0 1
[11] .symtab SYMTAB 0000000000000000 00000178
0000000000000180 0000000000000018 12 9 8
[12] .strtab STRTAB 0000000000000000 000002f8
000000000000003d 0000000000000000 0 0 1
(-----snip-----)
で、最初の結論のところに戻り、--noexecstackをつければ良いとなる。
[rarul@tina ~]$ gcc -c -o asm.o asm.S -Wa,--noexecstac
[rarul@tina ~]$ gcc -o nostack_exec main.o asm.o
[rarul@tina ~]$ ./nostack_exec
7ffca6998000-7ffca69b9000 rw-p 00000000 00:00 0 [stack]
ちなみに、.Sに無理やり.note.GNU-stackを埋め込むやり方でもできるけど、セキュリティ面考えると、1つずつ埋め込まないといけないやり方よりも、一括で行う**--noexecstack**の方がいいのではないかと思う。
#if defined(__linux__) && defined(__ELF__)
.section .note.GNU-stack,"",%progbits
#endif
dummy_label:
nop
[rarul@tina ~]$ gcc -c -o asm.o asm.S
[rarul@tina ~]$ readelf -e asm.o
(-----snip-----)
[Nr] Name Type Address Offset
Size EntSize Flags Link Info Align
[ 0] NULL 0000000000000000 00000000
0000000000000000 0000000000000000 0 0 0
[ 1] .text PROGBITS 0000000000000000 00000040
0000000000000000 0000000000000000 AX 0 0 1
[ 2] .data PROGBITS 0000000000000000 00000040
0000000000000000 0000000000000000 WA 0 0 1
[ 3] .bss NOBITS 0000000000000000 00000040
0000000000000000 0000000000000000 WA 0 0 1
[ 4] .note.GNU-stack PROGBITS 0000000000000000 00000040
0000000000000001 0000000000000000 0 0 1
[ 5] .shstrtab STRTAB 0000000000000000 000000e5
000000000000003c 0000000000000000 0 0 1
[ 6] .symtab SYMTAB 0000000000000000 00000048
0000000000000090 0000000000000018 7 6 8
[ 7] .strtab STRTAB 0000000000000000 000000d8
000000000000000d 0000000000000000 0 0 1
(-----snip-----)
[rarul@tina ~]$ cc -o stack_exec main.o asm.o
[rarul@tina ~]$ ./stack_exec
7fffb7655000-7fffb7676000 rw-p 00000000 00:00 0 [stack]
もちろん、コンパイルしようとしているアセンブラが実行可スタックを必要とするようなものの場合は、こうすると実行時にSIGSEGVするので注意。
実行可スタックはどういうとき使われるのか
先ほど紹介したGentooのページに詳細がのってるんだけど、.cをコンパイルしたときにも**.note.GNU-stack**セクションが作られないことがある。トランポリンと呼ばれる、nested functionで関数ポインタを扱う場合となる。最近はC言語ではそもそもnested functionを使うことが少ないけど、世の中JavaScriptなどでクロージャ大人気だったりするので、これはこれで知っておいたほうがよい知識ではある。詳細はこのへんへ。gccの生成するトランポリンコードについて - memologue
あと詳細知らないんだけど、JITでも必要なんだろうか。
で、実際にGNU_STACKヘッダが効いているのか確認してみる。
#include <stdio.h>
static void caller(void (fp)(void)) {
fp();
}
int main(){
int val;
void inner_func(void) {
printf("%d\n", val);
}
val = 53;
caller(inner_func);
return 0;
}
[rarul@tina ~]$ gcc -o tranp tranp.c
[rarul@tina ~]$ ./tranp
53
普通に実行できることを確認してから、GNU_STACKを無理矢理書き換えてみる。kernel/include/uapi/linux/elf.hより、
33 #define PT_LOOS 0x60000000 /* OS-specific */
34 #define PT_HIOS 0x6fffffff /* OS-specific */
35 #define PT_LOPROC 0x70000000
36 #define PT_HIPROC 0x7fffffff
37 #define PT_GNU_EH_FRAME 0x6474e550
38
39 #define PT_GNU_STACK (PT_LOOS + 0x474e551)
237 /* These constants define the permissions on sections in the program
238 header, p_flags. */
239 #define PF_R 0x4
240 #define PF_W 0x2
241 #define PF_X 0x1
から、ELFのヘッダタイプ「0x6474e551」の箇所を探す。
[rarul@tina ~]$ hexdump -vC tranp |head -n 200
(-----snip-----)
000001b0 44 00 00 00 00 00 00 00 44 00 00 00 00 00 00 00 |D.......D.......|
000001c0 04 00 00 00 00 00 00 00 51 e5 74 64 07 00 00 00 |........Q.td....|
000001d0 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................|
(-----snip-----)
(gdb) vmlinux
Reading symbols from vmlinux...done.
(gdb) ptype struct elf64_phdr
type = struct elf64_phdr {
Elf64_Word p_type;
Elf64_Word p_flags;
Elf64_Off p_offset;
Elf64_Addr p_vaddr;
Elf64_Addr p_paddr;
Elf64_Xword p_filesz;
Elf64_Xword p_memsz;
Elf64_Xword p_align;
}
(gdb) ptype Elf64_Word
type = unsigned int
より、0x1c8からの4バイトがELFタイプ(p_type)で、それに続く0x1ccから4バイトがフラグ(p_flags)とわかる。PF_Xは0x1なので、0x1ccの値0x07をおもむろに0x06に書き換えてみて実験する。
[rarul@tina ~]$./tranp
Segmentation fault
無事にSIGSEGVしてくれた。ちなみに、readelfでも確認できる。
[rarul@tina ~]$ readelf -e tranp
(-----snip-----)
GNU_STACK 0x0000000000000000 0x0000000000000000 0x0000000000000000
0x0000000000000000 0x0000000000000000 RW 10
(-----snip-----)
ちなみに、このトランポリンではI/Dスヌープ問題も発生する。CPUがicache(L1)とdcache(L1)を同期しない設定の場合、dcacheでデータとして書き込んだものを命令として実行するために、**__clear_cache()**関数(libgcc_s.soあたりに含まれる)が呼ばれるよう自動でコードが作られる。
あとがき
gccのソースコードの該当箇所だとかどのバージョンから入ったかだとかがなく、いろいろ詰めが甘い記事で申し訳ない。
参考情報
- 単純なスタックバッファオーバーフロー攻撃をやってみる - ももいろテクノロジー
- gccの生成するトランポリンコードについて - memologue
- Hardened/GNU stack quickstart
どうでもいい関連情報
ARMではspeculative prefetch(投機的プリフェッチ)がXNビットに連動して働くようで、non-RAMな領域で不必要にXNビットを寝かしたままだとプリフェッチでハングを起こしてしまったりする。...という問題を直すcommitがLinuxには5年くらい前に入っているので、よい子のみんなはちゃんとXNビットを立てておこうね、でないとおじさんみたいに不必要に苦労しちゃうよ。