人生は勉強ブログ

https://github.com/dooooooooinggggg

KASLRの実装と挙動の確認

この記事はSFC-RG Advent Calendar 2019の16日目の記事です. 今年も遅くなって申し訳ないです.

この記事では,KASLRkernel address space layout randomizationについて記述する.

KASLRは,カーネル領域における仮想アドレス空間のランダム化である.この基本的な仕組みは,カーネルに限らずASLRという仕組みで実現されているため,まずは,ASLRの目的や仕組みついて記述する.

ASLR

ASLRに関しては,こちらのQiitaの記事にて概要が書かれている. (ちなみにKASLRについても触れられている)

ここで述べられているシチュエーションは,

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

となっている.実行ファイル内のコードセグメントと呼ばれる領域にはプログラムの命令自身が格納されている.そのほかにもスタック領域やヒープ領域と呼ばれる領域も存在する. ASLRが無効な状態で実行可能ファイルを実行すると,これらの領域は常に決まった仮想アドレス空間にロードされる.

すると,仮想アドレスの推測が攻撃者にとって容易になってしまう.そこで登場したのが,ASLRである.この機能を使用することで,スタック領域やヒープ領域は常にランダム化されるが,コードセグメントに関してはコンパイル時に時にgccオプションで-fpicをつけることが必要となっている.

これらの条件を満たした状態でプログラムの実行を行うと,実行のたびにロードされる仮想アドレス空間が変動する,というのがASLRである.

KASLR

上述のASLRに対して,KASLRは名前の通り,カーネルの実行時の仮想アドレス空間を,起動のたびにランダム化するためのものである. こちらに関しても,先ほどの記事で概要が述べられている.

カーネルは,vmlinuxからbzImageになる際,stripされシンボル情報がバイナリから削除されるため,外から仮想アドレスを知ることはできない. そこで,カーネルモジュールなどからカーネルに実装されている関数を参照する際は,System.map(/boot/System.map-$(uname -r))を参照し,その仮想アドレスを知る.

System.mapには,それぞれのシンボルに対する仮想アドレスが記述されているが,KASLRにおいてはこれはただの目安となっている.つまり,実際の仮想アドレスは起動時にランダム化されていて,/proc/kallsymsの値とSystem.mapの値は異なる.

先ほどの記事から例を示す.

sudo cat /boot/System.map-4.19.0-6-amd64 | grep "T printk"
# ffffffff810e085e T printk
sudo cat /proc/kallsyms | grep "T printk"
# ffffffffad0e085e T printk

このように,異なるのがわかる.

この機能を無効にするには,nokaslrカーネルコマンドラインに追加する. 具体的には,/etc/default/grubのうち,GRUB_CMDLINE_LINUX=""となっている行を,GRUB_CMDLINE_LINUX="nokaslr"とする.

実装

KASLRの実装は,x86アーキテクチャにおいては,arch/x86/mm/kaslr.cに実装されている.(執筆時点でv5.4.3)

Randomization is done on PGD & P4D/PUD page table levels to increase possible addresses.

とあるように,ページテーブルレベルでランダム化を行う.Linuxのページング機構に関しては,以下の記事に書いた.

blog.ishikawa.tech

このファイルの主要な関数は,kernel_randomize_memory()であるが,これは/arch/x86/kernel/setup.cの中のsetup_arch()で呼ばれている.

  1. 5段階ページングが有効かどうかを確認
  2. メモリレイアウトがvaddr_start / vaddr_end変数と一致しているかどうかを確認

などの前処理を経てから最終的にここでランダム化をする.

この処理では,kernel_memory_regionに対して,

static __initdata struct kaslr_memory_region {
    unsigned long *base;
    unsigned long size_tb;
} kaslr_regions[] = {
    { &page_offset_base, 0 },
    { &vmalloc_base, 0 },
    { &vmemmap_base, 0 },
};

下にあるようなコードで

/*
* Select a random virtual address using the extra entropy
* available.
*/
entropy = remain_entropy / (ARRAY_SIZE(kaslr_regions) - i);
prandom_bytes_state(&rand_state, &rand, sizeof(rand));
entropy = (rand % (entropy + 1)) & PUD_MASK;
vaddr += entropy;
*kaslr_regions[i].base = vaddr;

page_offset_base, vmalloc_base, vmemmap_baseの値を上書きしている.

実際に,この関数のentropy加算後の値をKASLRの有効/無効で試してみたところ,

無効なマシンでは,page_offset_baseの値は,

page_offset_base: ffff888000000000

ソースで定義されているPAGE_OFFSETの値に等しいことが確認できるが,

有効なマシンでは,ffff9ee680000000となったり,再起動後は,ffff9d1100000000と変化していることが確認できた.

page_offset_base: ffff9ee680000000
page_offset_base: ffff9d1100000000

参考文献