Linuxのsyscallハンドラのエントリポイントがたくさんあるんだが -> 完全に理解した

はじめに

久しぶりの更新です,morimolymolyです.

最近,私はRing-1に興味を持ちいろいろなhypervisorを触って遊んでいます研究しています.

さて,皆さんはsyscallにはかなり馴染み深いですよね?

システムソフトウェアを書くときはもちろんのこと,exploitするときにガジェットをあーでもないこーでもないとつないでROPして呼び出したりしますよね?

私も最近syscallをRing-1から監視したりしているのですが,そこで気になったことがあったのです.

というわけでこの記事では,x86_64のマルチコアプロセッサLinux環境でのsyscall命令についてみていきます.

syscall命令

x86_64ではシステムコールを行う際には,引数をレジスタに設定して,syscall命令を呼び出します.

syscall命令はMSR(Model-specific Register)のIA32_LSTAR(0xC0000082)からシステムコールのエントリポイントのアドレスを読み出し,RIPにセットします.

神秘的なRing3からRing0の移動が,この一つの命令でできるわけです.

驚きですね.

ではIA32_LSTARの中身を読み出してみましょう.

以下のLKM(Loadable Kernel Module)はRDMSR命令を用いてIA32_LSTARを読み出してprintkしています.

gist.github.com

このLKMをビルドして実行するとカーネルメッセージにIA32_LSTARの中身が表示されるというわけです.

それではこのLKMをおもむろに何度も実行してみましょう!(なぜ)

dmesg | grep MOLY

するとどうでしょう.

何故かアドレスが異なっています!! (結果はめんどいのでのせない)

syscallハンドラが気まぐれでメモリ空間を散歩しているのでしょうか?

いえ,違います.

この挙動はマルチコアプロセッサのKPTI(Kernel Page-Table Isolation)が有効な環境においては正常な動作なのです!!

syscall_init関数

さて,IA32_LSTARLinuxのboot時,syscall_init関数によって設定されます.

/* May not be marked __init: used by software suspend */
void syscall_init(void)
{
    extern char _entry_trampoline[];
    extern char entry_SYSCALL_64_trampoline[];

    int cpu = smp_processor_id();
    unsigned long SYSCALL64_entry_trampoline =
        (unsigned long)get_cpu_entry_area(cpu)->entry_trampoline +
        (entry_SYSCALL_64_trampoline - _entry_trampoline);

    wrmsr(MSR_STAR, 0, (__USER32_CS << 16) | __KERNEL_CS);
    if (static_cpu_has(X86_FEATURE_PTI))
        wrmsrl(MSR_LSTAR, SYSCALL64_entry_trampoline);
    else
        wrmsrl(MSR_LSTAR, (unsigned long)entry_SYSCALL_64);

ここの関数で行っている処理は,X86_FEATURE_PTIが有効になっている場合,つまりKPTIが有効になっている場合に,wrmsr命令でIA32_LSTARSYSCALL64_entry_trampolineを書き込んでいます.

SYSCALL64_entry_trampolineは,ご覧の通り,cpuのidによってアドレスが異なるようです.

つまりcpuのコアごとにIA32_LSTARの値が異なることは正解なわけです.

また,先程のLKMでアドレスが異なっていたのも,実行するコアが異なっていたからですね.

entry_SYSCALL_64_trampoline

さて,entry_SYSCALL_64_trampolineでは何をしているのでしょうか.

 .pushsection .entry_trampoline, "ax"

/*
 * The code in here gets remapped into cpu_entry_area's trampoline.  This means
 * that the assembler and linker have the wrong idea as to where this code
 * lives (and, in fact, it's mapped more than once, so it's not even at a
 * fixed address).  So we can't reference any symbols outside the entry
 * trampoline and expect it to work.
 *
 * Instead, we carefully abuse %rip-relative addressing.
 * _entry_trampoline(%rip) refers to the start of the remapped) entry
 * trampoline.  We can thus find cpu_entry_area with this macro:
 */

#define CPU_ENTRY_AREA \
    _entry_trampoline - CPU_ENTRY_AREA_entry_trampoline(%rip)

/* The top word of the SYSENTER stack is hot and is usable as scratch space. */
#define RSP_SCRATCH    CPU_ENTRY_AREA_entry_stack + \
            SIZEOF_entry_stack - 8 + CPU_ENTRY_AREA

ENTRY(entry_SYSCALL_64_trampoline)
    UNWIND_HINT_EMPTY
    swapgs

    /* Stash the user RSP. */
    movq %rsp, RSP_SCRATCH

    /* Note: using %rsp as a scratch reg. */
    SWITCH_TO_KERNEL_CR3 scratch_reg=%rsp

    /* Load the top of the task stack into RSP */
    movq CPU_ENTRY_AREA_tss + TSS_sp1 + CPU_ENTRY_AREA, %rsp

    /* Start building the simulated IRET frame. */
    pushq    $__USER_DS           /* pt_regs->ss */
    pushq    RSP_SCRATCH          /* pt_regs->sp */
    pushq    %r11             /* pt_regs->flags */
    pushq    $__USER_CS           /* pt_regs->cs */
    pushq    %rcx             /* pt_regs->ip */

    /*
    * x86 lacks a near absolute jump, and we can't jump to the real
    * entry text with a relative jump.  We could push the target
    * address and then use retq, but this destroys the pipeline on
    * many CPUs (wasting over 20 cycles on Sandy Bridge).  Instead,
    * spill RDI and restore it in a second-stage trampoline.
    */
    pushq    %rdi
    movq $entry_SYSCALL_64_stage2, %rdi
    JMP_NOSPEC %rdi
END(entry_SYSCALL_64_trampoline)

    .popsection

コメントにもある通り,このcpu_entry_area's trampolineはコアごとにremapされるようです.

そして,syscall_init関数でIA32_LSTARに書き込んでいたのは,RIPベースの相対アドレスの計算を行って導き出した,実際にマップされたコードのアドレスというわけです.

またentry_SYSCALL_64_trampolineはその名の通り,いくつかの段階をへて本来のシステムコールを行う前のトランポリンのようなコードです.

まあ今回は各段階のコードは読みませんが.

さて,ではなぜこのような複雑なマッピングを行っているのでしょうか?

コアごとにハンドラを配置する必要性がわかりません.

コアごとにハンドラがマッピングされるワケ

ぐぐると,以下のようなLKMLのパッチ情報にたどり着きました. LKML: Thomas Gleixner: [patch 19/60] x86/entry/64: Create a per-CPU SYSCALL entry trampoline

syscallが行われる際,FLAGS以外のすべてのレジスタとともにハンドラへ突入します.

当然,ユーザ空間のRSPをどこかに保存してカーネルのスタックのアドレスを示してやる必要がありますね.

通常,per-CPUdataセクション(コアごと)と呼ばれる空間にユーザ空間のRSPは格納されことになっています.

その後にentryの際にSWAPGSをしてGSレジスタベースでper-CPUdataセクションへアクセスします.

しかし,KPTIが導入された場合大きな問題が生じてきます.

KPTIによりユーザ空間とカーネル空間は別のページテーブルにマップされるからです.

つまり,CR3レジスタカーネル用とユーザ空間用とで書き換えなければならないのです!!(CR3レジスタに入るのはページディレクトリのベースアドレス.ページングに詳しくない人はアドレス解決に必要な基本のアドレスと思ってもらえれば良き!)

従来どおりGSレジスタベースでアクセスすればいいと思いますが,GSベースのアクセスの場合,ユーザ空間用のページに重要なperCPUdataがマッピングされていなければならないので,Meltdownなどの脆弱性によっていろいろと情報が盗み出されてしまう恐れがあります!!!!(CR3をカーネル用に書き換えたいのに特権がないとアクセスできないデータ……しんどい……)

CR3レジスタは即値で代入できず,レジスタを介する必要がるのだが,syscallハンドラで自由に使えるレジスタは大変少ないのです.

ではエントリ時のレジスタをみてみよう.

  • rax system call number
  • rcx return address
  • r11 saved rflags(これはばんばん変動する)
  • rdi arg0
  • rsi arg1
  • rdx arg2
  • r10 arg3 (CのABIによるとRCXに移す必要がある)
  • r8 arg4
  • r9 arg5
  • (note: r12-r15, rbp, rbx はABIで呼び出し先で自由に使えるアドレスと定義されている)

少なすぎィッ!! ということでどうにかしてperCPUdataセクションへユーザ空間のRSPを保存しつつ,CR3を書き換えなければならない.

さて,ここでcpu_entry_area's trampoline(syscallはんどら)をCPUごとに一定間隔でマッピングしてみると……なんとハンドラのアドレスから相対的にperCPUdataセクションにアクセスすることができるのです!!

つまりGSレジスタベースでアクセスする必要がなくなった!!やったあ! 実際はRSPを介してCR3のスイッチングを行っているみたいですね. いやあ複雑なマッピングをしてまでセキュリティを向上させなければ行けないのは大変ですねえ.

まとめ

syscallハンドラがKPTI環境でCPUコアごとにマッピングされているのは,重要なデータをユーザ空間とおなじページにマッピングされないようにしながら,CR3(ページディレクトリのベースアドレス)をスイッチングする必要があったからです!! セキュリティのコストってすごいねえ……

告知

twitter復活しました!

twitter復活しましたのでフォローおねがいします.

twitter.com

チームHyperVillage

また,仮想化技術関連のお話ができるslack(Hypervillage)をつくりました.
私一人しかいなくて寂しいのでだれか入ってください.
あといろいろと相談とか乗ってください……

Slack

とゆーか,仮想化技術関連とかで盛り上がっているコミュニティがあったら誘ってください……. よろしくおねがいします.

Dentoo.LT #21に登壇します

2018/11/11(日)に電通大で最もハッキーなサークルMMAが主催するDentoo.LTに登壇します! ハイパーバイザーとかBareflankな話をしようと思ってるのでぜひきてくれよな!!