アセンブラを混ぜてコンパイルするとスタックが実行可になってしまう話

  • 8
    いいね
  • 0
    コメント

いきなり結論

  • Q. アセンブラを混ぜてコンパイルするとスタックが実行可になってしまう
  • A. アセンブル時に--noexecstackつけとけ

はじめに

Linuxのユーザランドのプログラムをアセンブラを混ぜてコンパイルし作ると、実行時にスタックのmapの属性に「実行可」がついてしまう。という話を詳細まで追うという趣旨の雑記です。なお、Ubuntu 16.04 (gcc-5.4.0 20160609, Linux-4.4.0-59)くらいで実験しつつ、kernelのソースはLinux-4.10くらいを見ています。

asm.S
dummy_label:
    nop
main.c
#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()より、

binfmt_elf.c
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;
exec.c
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の方がいいのではないかと思う。

asm.S
#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ヘッダが効いているのか確認してみる。

tranp.c
#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より、

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)
elf.h
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のソースコードの該当箇所だとかどのバージョンから入ったかだとかがなく、いろいろ詰めが甘い記事で申し訳ない。

参考情報

どうでもいい関連情報

ARMではspeculative prefetch(投機的プリフェッチ)がXNビットに連動して働くようで、non-RAMな領域で不必要にXNビットを寝かしたままだとプリフェッチでハングを起こしてしまったりする。...という問題を直すcommitがLinuxには5年くらい前に入っているので、よい子のみんなはちゃんとXNビットを立てておこうね、でないとおじさんみたいに不必要に苦労しちゃうよ。