clone systemcallをフックする
概要
Linux でシステムコールをフックする手法はいくつかあります. 例えば,syscall 命令によって参照される MSR レジスタの値を書き換えたり,システムコールテーブルを書き換えるのが代表的です. 前者の手法は前のエントリで紹介しました. 今回は,後者のシステムコールテーブルを書き換える手法で,clone システムコールをフックしようとした時にハマったポイントを書きます.
環境
ホスト OS: Ubuntu16.04 64bit
アーキテクチャ: x86-64
sys_call_table の書き換え
システムコールをフックするには,以下のようにsys_call_table
の特定のエントリの値を書き換えます.
//readシステムコールをフックする例
sys_call_table[__NR_read] = (uint64_t) fake_sys_read;
clone システムコールをフックする場合も上記と同じように,
sys_call_table[__NR_clone]
の値を fake_sys_clone に書き換えると、ユーザ空間で SIGSEGV の嵐でシステムがクラッシュしました(新たにプロセスを生成できなくなった).
結論から言うと,sys_call_table[__NR__clone]
の値は,clone システムコールの実態であるsys_clone
ではなく,stub_clone
です.このstub_clone
内でsys_clone
が呼び出される実装です.
clone の他にも,fork,execve などのプロセスの生成に関わる特別なシステムコールは,スタブを経由して呼び出されます.
よって,横取りしてやるには,このスタブを考慮してシステムコールをフックする必要があります.
嵌りそうなポイントは以下の 3 つ
clone システムコールをフックするにあたって,スタブの考慮だけでなく,他にもハマりそうなポイントがあるので以下にまとめた.
Kernel3 系と 4 系で,
sys_clone
のプロトタイプが異なる.Kernel4 系でも,CONFIG によってプロトタイプが異なる(kernel/fork.c).
- /boot/config-4.4.0-21-generic で CONFIG を確認可能
sys_call_table[_NR_clone]
の値は,sys_clone
ではなく,stub_clone
.
3.の該当コードは以下の通りです.
arch/x86/entry/entry_64.S に
stub_clone
==>sys_clone
呼び出しを行うマクロがある (1)arch/x86/entry/calling.h に
SAVE_EXTRA_REGSマクロ
がある (2)arch/x86/entry/entry_64.S に
sys_call_table
からシステムコールを呼び出すエントリポイントENTRY(entry_SYSCALL_64)
がある (3)- コメントに bp, bx, r12-15 not saved とあり,sub して領域を確保するだけで,レジスタの退避を行なっていない
(2)の stub 処理では,(3)で退避されなかったレジスタの退避を行なっている.
特に,ENTRY(entry_SYSCALL_64)
で作られたスタックを破壊しないように,必要な処理だけをし,jmp 命令で,システムコール本体に処理を移している.
(1)
.macro FORK_LIKE func
ENTRY(stub_\func)
SAVE_EXTRA_REGS 8
jmp sys_\func
END(stub_\func)
.endm
FORK_LIKE clone
FORK_LIKE fork
FORK_LIKE vfork
(2)
.macro SAVE_EXTRA_REGS offset=0
movq %r15, 0*8+\offset(%rsp)
movq %r14, 1*8+\offset(%rsp)
movq %r13, 2*8+\offset(%rsp)
movq %r12, 3*8+\offset(%rsp)
movq %rbp, 4*8+\offset(%rsp)
movq %rbx, 5*8+\offset(%rsp)
.endm
(3)
(snip)
GLOBAL(entry_SYSCALL_64_after_swapgs)
movq %rsp, PER_CPU_VAR(rsp_scratch)
movq PER_CPU_VAR(cpu_current_top_of_stack), %rsp
/* Construct struct pt_regs on stack */
pushq $__USER_DS /* pt_regs->ss */
pushq PER_CPU_VAR(rsp_scratch) /* pt_regs->sp */
/*
* Re-enable interrupts.
* We use 'rsp_scratch' as a scratch space, hence irq-off block above
* must execute atomically in the face of possible interrupt-driven
* task preemption. We must enable interrupts only after we're done
* with using rsp_scratch:
*/
ENABLE_INTERRUPTS(CLBR_NONE)
pushq %r11 /* pt_regs->flags */
pushq $__USER_CS /* pt_regs->cs */
pushq %rcx /* pt_regs->ip */
pushq %rax /* pt_regs->orig_ax */
pushq %rdi /* pt_regs->di */
pushq %rsi /* pt_regs->si */
pushq %rdx /* pt_regs->dx */
pushq %rcx /* pt_regs->cx */
pushq $-ENOSYS /* pt_regs->ax */
pushq %r8 /* pt_regs->r8 */
pushq %r9 /* pt_regs->r9 */
pushq %r10 /* pt_regs->r10 */
pushq %r11 /* pt_regs->r11 */
sub $(6*8), %rsp /* pt_regs->bp, bx, r12-15 not saved */
testl $_TIF_WORK_SYSCALL_ENTRY, ASM_THREAD_INFO(TI_flags, %rsp, SIZEOF_PTREGS)
jnz tracesys
entry_SYSCALL_64_fastpath:
#if __SYSCALL_MASK == ~0
cmpq $__NR_syscall_max, %rax
#else
andl $__SYSCALL_MASK, %eax
cmpl $__NR_syscall_max, %eax
#endif
ja 1f /* return -ENOSYS (already in pt_regs->ax) */
movq %r10, %rcx
call *sys_call_table(, %rax, 8)
movq %rax, RAX(%rsp)
(snip)
- その他、
movq %r10, %rcx
でシステムコールにおける第 4 引数レジスタの r10 を,関数呼び出しにおける第4引数レジスタの rcx に代入している. つまり,システムコール呼び出しを関数呼び出しに変換している.
解決策
stub_cloneからsys_clone
が呼ばれる実装なので,sys_call_table[__NR_clone]
のstub_clone
の値をfake_sys_clone
にしてはいけない(正規のスタックレイアウトにならないので).
そこで,sys_call_table[__NR_clone]
のstub_clone
の値をfake_stub_clone
に設定し,fake_stub_clone
から,fake_sys_clone
を呼び出すようにする.
他にも,fork 系や,execve にスタブがあるので,ABI に注意が必要.
ただし,fork,vfork は,引数がないので,sys_call_table[__NR_(v)fork]
のstub_(v)fork
の値をfake_sys_(v)fork
に変更しても,問題ないと考えられる.
まとめ
clone システムコールなどでは,rbx - r15 が重要.
参考: x64 の引数
system call
- rax にシステムコール番号を設定
- 第 1 引数 を rdi に設定
- 第 2 引数 を rsi に設定
- 第 3 引数 を rdx に設定
- 第 4 引数 を r10 に設定
- 第 5 引数 を r8 に設定
- 第 6 引数 を r9 に設定
- システムコール命令( syscall ) を実行
なお,rax に戻り値が保存され,rcx と r11 の値は破壊される可能性がある.
関数呼び出し
- 第 1 引数 を rdi に設定
- 第 2 引数 を rsi に設定
- 第 3 引数 を rdx に設定
- 第 4 引数 を rcx に設定
- 第 5 引数 を r8 に設定
- 第 6 引数 を r9 に設定
system call とは,第4引数が異なる. また,これ以上の引数は,スタックにつむ. 戻り値は rax に保存される