はじめに
久しぶりの更新です,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しています.
このLKMをビルドして実行するとカーネルメッセージにIA32_LSTARの中身が表示されるというわけです.
それではこのLKMをおもむろに何度も実行してみましょう!(なぜ)
dmesg | grep MOLY
するとどうでしょう.
何故かアドレスが異なっています!! (結果はめんどいのでのせない)
syscallハンドラが気まぐれでメモリ空間を散歩しているのでしょうか?
いえ,違います.
この挙動はマルチコアプロセッサのKPTI(Kernel Page-Table Isolation)が有効な環境においては正常な動作なのです!!
syscall_init関数
さて,IA32_LSTAR
はLinuxの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_LSTAR
にSYSCALL64_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復活しましたのでフォローおねがいします.
チームHyperVillage
また,仮想化技術関連のお話ができるslack(Hypervillage)をつくりました.
私一人しかいなくて寂しいのでだれか入ってください.
あといろいろと相談とか乗ってください……
とゆーか,仮想化技術関連とかで盛り上がっているコミュニティがあったら誘ってください……. よろしくおねがいします.
Dentoo.LT #21に登壇します
2018/11/11(日)に電通大で最もハッキーなサークルMMAが主催するDentoo.LTに登壇します! ハイパーバイザーとかBareflankな話をしようと思ってるのでぜひきてくれよな!!