🙌

ASLRとKASLRの概要

2020/09/25に公開

はじめに

本記事はlinuxに存在する[1]Address Space Location Randomization(ASLR)、およびKASLR(Kernel ASLR)という、やたら長い名前のセキュリティ機能を紹介するものです。メモリアドレスの概念やC言語ポインタが理解できれば読める内容だと思います。

ASLR

概要

あるシステムをクラッカーが攻撃する方法はいろいろありますが、そのうちの一つが不正な方法によってプログラムに特定の命令を実行させる、不正なデータを操作させるというものです。攻撃には、(当然ながら)攻撃に使う命令、あるいはデータのアドレスが必要になります。ASLRが無い環境においてはプログラムのコードやデータは[2]固定されたアドレスにロードされるので、動かしているプログラムのバイナリがどんなものかわかっていれば、攻撃者が攻撃に使うコードやデータのアドレスを知るのは簡単です。

この類の攻撃を防ぐために考案されたのがASLRです。ASLRを使うと、プログラムの実行ごとに、そのコードやデータがランダムな位置に配置されます。そのため、クラッカーが攻撃に使うコードやデータのアドレスを割り出すのが困難になります[3]

ASLRは難しいハードウェア機構を使っているわけでなく、コードやデータを参照するときにアドレスを"0x10000000"などと絶対的に決め打ちで指定するのではなく、"所定のベースアドレス+0x10000000"などと、相対的に指定するだけです。

ここで一点注意が必要なのですが、ALSRによってスタック領域やヒープの領域のアドレスはどんなプロセスにおいてもランダム化されるのですが、コード領域や静的に配置されたグローバル変数やstatic変数などのデータ領域についてはそうではありません。以下に例を示します。

$ sleep 10000 &
[1] 7951
$ sleep 10000 &
[2] 7952
$ cat /proc/7951/maps       # sleep(pid=7951)のメモリマップ
00400000-00407000 r-xp 00000000 00:16 207201                             /bin/sleep # コード
00606000-00607000 r--p 00006000 00:16 207201                             /bin/sleep # 読み取り専用データ
00607000-00608000 rw-p 00007000 00:16 207201                             /bin/sleep # 読み書きデータ
009aa000-009cb000 rw-p 00000000 00:00 0                                  [heap]     # ヒープ
...
7ffc60378000-7ffc60399000 rw-p 00000000 00:00 0                          [stack]    # スタック
$ cat /proc/7952/maps       # sleep(pid=7952のメモリマップ)
00400000-00407000 r-xp 00000000 00:16 207201                             /bin/sleep
00606000-00607000 r--p 00006000 00:16 207201                             /bin/sleep
00607000-00608000 rw-p 00007000 00:16 207201                             /bin/sleep
00cf1000-00d12000 rw-p 00000000 00:00 0                                  [heap]
...
7ffd1a406000-7ffd1a427000 rw-p 00000000 00:00 0                          [stack]
...

というのも、上記のようにコード領域と静的に配置されたデータ領域のアドレスを相対的に指定するにはプログラムをPosition Independent Code (PIC) としてビルドされたPosition Independent Executable(PIE)である必要があるからです。gccにおいてはコンパイル時に-fpicオプションを付けると実現できます。こうなっていれば当該バイナリから生成されたプロセスは、任意のコード、データがランダム配置されます。以下に、私の環境ではPIEとして存在するsshを2つ起動した場合の、それぞれのプロセスのアドレスマップを示します。

$ cat /proc/7981/maps
5626d3d9c000-5626d3e45000 r-xp 00000000 00:16 867533                     /usr/bin/ssh
5626d4045000-5626d4048000 r--p 000a9000 00:16 867533                     /usr/bin/ssh
5626d4048000-5626d4049000 rw-p 000ac000 00:16 867533                     /usr/bin/ssh
5626d4049000-5626d404c000 rw-p 00000000 00:00 0
5626d5505000-5626d5544000 rw-p 00000000 00:00 0                          [heap]
...
7ffe2bd73000-7ffe2bd94000 rw-p 00000000 00:00 0                          [stack]
...
$ cat /proc/7985/maps
5616589f2000-561658a9b000 r-xp 00000000 00:16 867533                     /usr/bin/ssh
561658c9b000-561658c9e000 r--p 000a9000 00:16 867533                     /usr/bin/ssh
561658c9e000-561658c9f000 rw-p 000ac000 00:16 867533                     /usr/bin/ssh
561658c9f000-561658ca2000 rw-p 00000000 00:00 0
561658d81000-561658dc0000 rw-p 00000000 00:00 0                          [heap]
...
7ffc75060000-7ffc75081000 rw-p 00000000 00:00 0                          [stack]
...

PIEは通常のバイナリに比べてアドレッシングに余計な手間がかかるため、性能面では劣ります。このため、各種distroによって程度の差はありますが、セキュリティ的に重要なsshなどのバイナリはPIE、そうでないものは非PIEとしてビルドされる傾向にありました。ただし最近では可能な限りPIEをデフォルトにする動きが活発です。例えばUbuntuでは16.10からPIEがデフォルトになりました

あるプログラムがPIEかそうでないかは、たとえばfileコマンドによって確認できます。

$ file /usr/bin/ssh
/usr/bin/ssh: ELF 64-bit LSB shared object, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, for GNU/Linux 2.6.32, BuildID[sha1]=ecf7433a7d26461fc1bc7\
a6b6a4eba868e685839, stripped                # "...share object"となってる実行ファイルはPIE
$ file /bin/bash
/bin/bash: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, for GNU/Linux 2.6.32, BuildID[sha1]=04eca96c5bf3e9a300952a29ef3\218f00487d37b, stripped            # "... executable"となってる実行ファイルは非PIE

有効/無効

ASLRはデフォルトで有効です。ASLRを無効化するにはsysctlのkernel.randomize_va_spaceに0を指定します(有効なときは2です。1についてはあんまり気にしなくていいです)。この設定はシステム全体に対して効果があります。

KASLR (KASLR)

概要

KASLRはASLRの考え方をそのままカーネルに持ってきたものです。KASLRはカーネルをPIEとしてビルドして、ブートのたびに自身を毎回異なるアドレスにロードします。非常に簡素な説明で恐縮ですが、ASLRさえわかっていればKASLRの機能については大して説明することがありません。

有効/無効

KASLRはlinux 3.14で導入されました。設定ファイル(.config)のCONFIG_RANDOMIZE_BASEがyになっていれば有効です。長らくデフォルト設定(defconfigと呼ばれるもの)ではこの機能は無効でした。しかし、本記事執筆時点での最新mainline kernelの4.12からはデフォルトで有効化されました。ただし、リンク先の記事にもある通り、Ubuntuなどのdistroカーネルでは以前から有効化されている例が多かったことより、これはカーネル開発者以外にはあまり影響無い話かもしれません。

KASLRには一つ罠があって、4.7以前のカーネルではhibernationと共存できないという制限があります。CONFIG_HIBERNATION=yの環境下においては、kaslrというカーネルブートオプションを明に指定した場合のみKASLRが有効になります(そのかわりhibernationは無効になります)。それ以外の場合は無効化されます。例えばUbuntuにおいて4.4系のカーネルを使ってシステムを構築している場合はこれに該当するので、デフォルトではKASLRは無効化されているため、注意が必要です。

arch/x86/boot/compressed/aslr.c(linux4.4)
...
unsigned char *choose_kernel_location(struct boot_params *boot_params,                                                                                                                                                             unsigned char *input,                                                                                                                                                                        unsigned long input_size,                                                                                                                                                                    unsigned char *output,                                                                                                                                                                       unsigned long output_size)                                                                                                                             {                                                                                                                                                                                                    unsigned long choice = (unsigned long)output;                                                                                                                                                unsigned long random;                                                                                                                                                                
#ifdef CONFIG_HIBERNATION
        if (!cmdline_find_option_bool("kaslr")) {
                debug_putstr("KASLR disabled by default...\n");
                goto out;
        }
#else
        if (cmdline_find_option_bool("nokaslr")) {
                debug_putstr("KASLR disabled by cmdline...\n");
                goto out;
        }
#endif
...

CONFIG_RANDOMIZE_BASE=yなカーネルで明にKASLRを無効化したい場合はカーネルのブートオプションにnokaslrを指定します。

困ったことにKASLRはdmesgに有効/無効のログを残しません。KASLRが有効かどうかを実行時に確かめるには、次のように適当なシンボルについて、お使いのカーネルのSystem.mapファイル内のアドレスと/proc/kallsymsから読み取れる実行時のアドレスをroot権限の下で[4]比較する必要があります。これらが異なっていればKASLRは有効、そうでなければ無効です。以下に例を示します。

$ sudo grep "T do_page_fault" /boot/System.map-$(uname -r)
ffffffff8106b780 T do_page_fault
$ sudo grep "T do_page_fault" /proc/kallsyms
ffffffffb626b780 T do_page_fault               # 上のアドレスと異なる。つまりKASLR有効

おわりに

この記事を書いた理由は、とある調べものをしているときに、「KASLRを有効にしたカーネルを動かしたはずなのにアドレスが固定だぞ!?」と気づいたのがきっかけです。私の使ってるUbuntuのドキュメントにもしっかり書いてたのでドキュメントちゃんと見てればわかってた話ですね。情けない。

脚注
  1. linux固有機能というわけではなく、他のOSにも同等機能はあります ↩︎

  2. プログラムが動的にロードするライブラリはまた事情が違います ↩︎

  3. 後述のKASLRも含め、困難ではありますが、不可能ではありません。クラッカーと防御側の攻防はいつでもいたちごっこなのです ↩︎

  4. 通常System.mapファイルはrootでないと読めないようになっていますし、/proc/kallsymsのアドレス欄はrootでないと正しい値が出ないように細工されています ↩︎

Discussion