はじめに
Linuxのカーネルスタックオーバフローの検知機能の実装について記述します.
Linux ver. 4.20.11のソースコードを見ていきます.
アーキテクチャは,x86_64です.
カーネルスタックオーバフロー検知機能
そもそもカーネルスタックオーバフローとは
カーネルスタック
カーネルスタックとは,プロセスがシステムコールを発行するなどし,カーネル内のコンテキストに切り替わった際に使用するスタックです.この記事を読む人には,いらない説明だと思いますが,ユーザ側では,ユーザ用のスタック領域が別にあります.(Linuxのユーザプロセスのメモリマップについて)を見るとよく分かると思います.
スタックオーバーフロー
スタック用に使用できる領域は,上限があります.
しかし,データをpushして使用している領域が伸長することにより,上限を超えてデータをpushしてしまうことがあります.
これが,スタックオーバーフローであり,システムのクラッシュや権限昇格攻撃を受ける原因となったりします.
実装
カーネルスタック用領域の確保
基本的には,fork(),clone(),vfork()などの新たなタスクを生成する処理の延長で実行されます.
上記の処理の延長でcopy_process() -> dup_task_struct() -> alloc_thread_stack_node()という呼び出し流れにより,alloc_thread_stack_node()でカーネルスタック用の領域が作られます.
余談にはなりますが,以前のカーネル(ver.3.10.0で確認)では,カーネルスタック用の領域確保は,バディシステムから確保していたのですが,ver. 4.20.11だとvmalloc経由の確保も実装されており,カーネルコンフィグで指定できるようですね.(カーネルコンフィグで設定するのではなく,arch依存で自動でconfigされるようです.x86_64の場合は,vmalloc経由の確保のみです.2019/5/17修正)
スタックのサイズも以前のカーネルでは8KBだったのに,16KB(KASAN有効の場合だと32KB)と大きくなっていますね.
検知機能
検知機能について大きく2つ確認したので,説明していきます.
①STACK_END_MAGICを使った方法
以下は,カーネルスタック用領域の確保処理を実施しているソースコードであり,alloc_thread_stack_node()で領域を確保しています.
static struct task_struct *dup_task_struct(struct task_struct *orig, int node)
{
struct task_struct *tsk;
unsigned long *stack;
struct vm_struct *stack_vm_area;
int err;
<----略---->
stack = alloc_thread_stack_node(tsk, node);
if (!stack)
goto free_tsk;
<----略---->
tsk->stack = stack;
<----略---->
setup_thread_stack(tsk, orig);
clear_user_return_notifier(tsk);
clear_tsk_need_resched(tsk);
set_task_stack_end_magic(tsk); <<------★1
<----略---->
return tsk;
free_stack:
free_thread_stack(tsk);
free_tsk:
free_task_struct(tsk);
return NULL;
}
★1を見てください.
set_task_stack_end_magic()を呼び出しています.
void set_task_stack_end_magic(struct task_struct *tsk)
{
unsigned long *stackend;
stackend = end_of_stack(tsk);
*stackend = STACK_END_MAGIC; /* for overflow detection */
}
スタックの伸長限界となる場所にSTACK_END_MAGICで定義された値を書き込んでおき,その値が変わっていれば,スタックの伸長限界を超えて値をpushした,つまりスタックオーバフローが発生しているということを検知するという仕組みです.
では,このチェックはどこで実施されるのかというと,task_stack_end_corrupted()で,実施されます.
この関数は,複数の場所から呼ばれているようですが,何かしらカーネルが本来意図していない挙動をしているときにスタックオーバーフローが原因ではないか?とチェックするための用途で呼び出されているようです.例えば,__bad_area_nosemaphore()やmm_fault_error()の延長で呼ばれています.
②割り込み発生時に実施する定期的なチェック
common_interrupt()という割り込み発生時に共通で呼ばれる関数の延長で割り込み発生時のコンテキストが使用しているカーネルスタックに対してのみチェックを実施します.
すべてのカーネルスタックに対してチェックをするのは,割り込み処理時間が長くなり好ましくないということですね.
処理流れは,common_interrupt() -> do_IRQ() -> handle_irq() -> stack_overflow_check()です.
せっかくなので,stack_overflow_check()の処理を見てみます.
static inline void stack_overflow_check(struct pt_regs *regs)
{
#ifdef CONFIG_DEBUG_STACKOVERFLOW
#define STACK_TOP_MARGIN 128
struct orig_ist *oist;
u64 irq_stack_top, irq_stack_bottom;
u64 estack_top, estack_bottom;
u64 curbase = (u64)task_stack_page(current);
if (user_mode(regs))
return;
if (regs->sp >= curbase + sizeof(struct pt_regs) + STACK_TOP_MARGIN &&
regs->sp <= curbase + THREAD_SIZE)
return;
irq_stack_top = (u64)this_cpu_ptr(irq_stack_union.irq_stack) +
STACK_TOP_MARGIN;
irq_stack_bottom = (u64)__this_cpu_read(irq_stack_ptr);
if (regs->sp >= irq_stack_top && regs->sp <= irq_stack_bottom)
return;
oist = this_cpu_ptr(&orig_ist);
estack_top = (u64)oist->ist[0] - EXCEPTION_STKSZ + STACK_TOP_MARGIN;
estack_bottom = (u64)oist->ist[N_EXCEPTION_STACKS - 1];
if (regs->sp >= estack_top && regs->sp <= estack_bottom)
return;
WARN_ONCE(1, "do_IRQ(): %s has overflown the kernel stack (cur:%Lx,sp:%lx,irq stk top-bottom:%Lx-%Lx,exception stk top-bottom:%Lx-%Lx,ip:%pF)\n",
current->comm, curbase, regs->sp,
irq_stack_top, irq_stack_bottom,
estack_top, estack_bottom, (void *)regs->ip);
if (sysctl_panic_on_stackoverflow)
panic("low stack detected by irq handler - check messages\n");
#endif
}
処理については,特に解説しません.
カーネルスタックオーバフローを検知するとメッセージを出力してくれるようです.
また,/proc/sys/kernel/panic_on_stackoverflowの値を変えることで検知時にパニックするかを設定できるようです.デフォルト値は0なのでパニックしません.
以前のカーネル(ver.3.10.0で確認)では,stack_overflow_check()内でSTACK_END_MAGICの値のチェックも実施していましたが,このチェックはなくなっているようです.