clone systemcallをフックする

概要

Linux でシステムコールをフックする手法はいくつかあります. 例えば,syscall 命令によって参照される MSR レジスタの値を書き換えたり,システムコールテーブルを書き換えるのが代表的です. 前者の手法は前のエントリで紹介しました. 今回は,後者のシステムコールテーブルを書き換える手法で,clone システムコールをフックしようとした時にハマったポイントを書きます.

環境

ホスト OS: Ubuntu16.04 64bit
アーキテクチャ: x86-64

sys_call_table の書き換え

システムコールをフックするには,以下のようにsys_call_tableの特定のエントリの値を書き換えます.

1
2
3
//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 システムコールをフックするにあたって,スタブの考慮だけでなく,他にもハマりそうなポイントがあるので以下にまとめた.

  1. Kernel3 系と 4 系で,sys_cloneのプロトタイプが異なる.

  2. Kernel4 系でも,CONFIG によってプロトタイプが異なる(kernel/fork.c).

    • /boot/config-4.4.0-21-generic で CONFIG を確認可能
  3. sys_call_table[_NR_clone]の値は,sys_cloneではなく,stub_clone

3.の該当コードは以下の通りです.

(2)の stub 処理では,(3)で退避されなかったレジスタの退避を行なっている. 特に,ENTRY(entry_SYSCALL_64)で作られたスタックを破壊しないように,必要な処理だけをし,jmp 命令で,システムコール本体に処理を移している.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
(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
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
(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
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
(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)

解決策

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 に戻り値が保存され,rcx と r11 の値は破壊される可能性がある.

関数呼び出し

system call とは,第4引数が異なる. また,これ以上の引数は,スタックにつむ. 戻り値は rax に保存される