异常处理程序调用前准备

#DB#BP的异常处理程序位于/source/arch/x86/include/asm/traps.h#L13

asmlinkage void divide_error(void);
asmlinkage void debug(void);
asmlinkage void nmi(void);
asmlinkage void int3(void);
asmlinkage void overflow(void);
asmlinkage void bounds(void);
asmlinkage void invalid_op(void);
asmlinkage void device_not_available(void);

asmlinkagegcc特殊说明符。实际上,对于C从汇编码中调用的函数,我们需要显式声明函数调用约定。如果函数使用asmlinkage描述符创建,gcc将从堆栈中检索参数以编译该函数。

因此,两个处理程序都在带有idtentry宏的/arch/x86/entry/entry_64.S中定义:

idtentry debug do_debug has_error_code=0 paranoid=1 shift_ist=IST_INDEX_DB ist_offset=DB_STACK_OFFSET
idtentry int3 do_int3 has_error_code=0 create_gap=1

每个异常处理程序可以由两部分组成:

  • 第一部分是通用部分,所有异常处理程序都相同。异常处理程序应将通用寄存器保存在堆栈上,如果异常来自用户空间,则应切换到内核堆栈,并将控制权转移到异常处理程序的第二部分。
  • 异常处理程序的第二部分完成的工作取决于具体的异常。例如,页面错误异常处理程序应找到给定地址的虚拟页面,无效的操作码异常处理程序应发送SIGILL 信号等。

现在来分析idtentry宏的实现。如我们所见,该宏采用七个参数:

  • sym – 定义全局符号,该符号.globl name将作为异常处理程序的入口。
  • do_sym – 符号名称,这表示异常处理程序的辅助条目。
  • has_error_code – 异常是否存在错误代码。

最后四个参数是可选的:

  • paranoid – 非零表示可以使用用户GSBASE和/或用户CR3从内核模式调用此中断向量。
  • shift_ist – 如果内核模式下的中断条目使用IST堆栈,以便使得嵌套的中断条目获得新的中断栈,则置位。 (这是针对#DB的,它具有递归的逻辑。(这很糟糕!))
  • create_gap – 从内核模式进入此中断处理程序时,创建一个6字大小的堆栈间隙。
  • read_cr2 – 在调用任何C代码之前,将CR2加载到第3个参数中

.idtentry宏的定义:(实现在/source/arch/x86/entry/entry_64.S#L970)

/**
 * idtentry - Generate an IDT entry stub
 * @sym:        Name of the generated entry point
 * @do_sym:     C function to be called
 * @has_error_code: True if this IDT vector has an error code on the stack
 * @paranoid:       non-zero means that this vector may be invoked from
 *          kernel mode with user GSBASE and/or user CR3.
 *          2 is special -- see below.
 * @shift_ist:      Set to an IST index if entries from kernel mode should
 *          decrement the IST stack so that nested entries get a
 *          fresh stack.  (This is for #DB, which has a nasty habit
 *          of recursing.)
 * @create_gap:     create a 6-word stack gap when coming from kernel mode.
 * @read_cr2:       load CR2 into the 3rd argument; done before calling any C code
 *
 * idtentry generates an IDT stub that sets up a usable kernel context,
 * creates struct pt_regs, and calls @do_sym.  The stub has the following
 * special behaviors:
 *
 * On an entry from user mode, the stub switches from the trampoline or
 * IST stack to the normal thread stack.  On an exit to user mode, the
 * normal exit-to-usermode path is invoked.
 *
 * On an exit to kernel mode, if @paranoid == 0, we check for preemption,
 * whereas we omit the preemption check if @paranoid != 0.  This is purely
 * because the implementation is simpler this way.  The kernel only needs
 * to check for asynchronous kernel preemption when IRQ handlers return.
 *
 * If @paranoid == 0, then the stub will handle IRET faults by pretending
 * that the fault came from user mode.  It will handle gs_change faults by
 * pretending that the fault happened with kernel GSBASE.  Since this handling
 * is omitted for @paranoid != 0, the #GP, #SS, and #NP stubs must have
 * @paranoid == 0.  This special handling will do the wrong thing for
 * espfix-induced #DF on IRET, so #DF must not use @paranoid == 0.
 *
 * @paranoid == 2 is special: the stub will never switch stacks.  This is for
 * #DF: if the thread stack is somehow unusable, we'll still get a useful OOPS.
 */
.macro idtentry sym do_sym has_error_code:req paranoid=0 shift_ist=-1 ist_offset=0 create_gap=0 read_cr2=0
SYM_CODE_START(\sym)
    UNWIND_HINT_IRET_REGS offset=\has_error_code*8

    /* Sanity check */
    .if \shift_ist != -1 && \paranoid != 1
    .error "using shift_ist requires paranoid=1"
    .endif

    .if \create_gap && \paranoid
    .error "using create_gap requires paranoid=0"
    .endif

    ASM_CLAC

    .if \has_error_code == 0
    pushq   $-1             /* ORIG_RAX: no syscall to restart */
    .endif

    .if \paranoid == 1
    testb   $3, CS-ORIG_RAX(%rsp)       /* If coming from userspace, switch stacks */
    jnz .Lfrom_usermode_switch_stack_\@
    .endif

    .if \create_gap == 1
    /*
     * If coming from kernel space, create a 6-word gap to allow the
     * int3 handler to emulate a call instruction.
     */
    testb   $3, CS-ORIG_RAX(%rsp)
    jnz .Lfrom_usermode_no_gap_\@
    .rept   6
    pushq   5*8(%rsp)
    .endr
    UNWIND_HINT_IRET_REGS offset=8
.Lfrom_usermode_no_gap_\@:
    .endif

    idtentry_part \do_sym, \has_error_code, \read_cr2, \paranoid, \shift_ist, \ist_offset

    .if \paranoid == 1
    /*
     * Entry from userspace.  Switch stacks and treat it
     * as a normal entry.  This means that paranoid handlers
     * run in real process context if user_mode(regs).
     */
.Lfrom_usermode_switch_stack_\@:
    idtentry_part \do_sym, \has_error_code, \read_cr2, paranoid=0
    .endif

_ASM_NOKPROBE(\sym)
SYM_CODE_END(\sym)
.endm

在分析idtentry宏的内部实现之前,首先明确,这是发生异常时的堆栈状态:

    +------------+
+40 | %SS        |
+32 | %RSP       |
+24 | %RFLAGS    |
+16 | %CS        |
 +8 | %RIP       |
  0 | ERROR CODE | <-- %RSP
    +------------+

然后结合#DB#BP的异常处理程序定义来看idtentry宏的内部实现:

idtentry debug do_debug has_error_code=0 paranoid=1 shift_ist=IST_INDEX_DB ist_offset=DB_STACK_OFFSET
idtentry int3 do_int3 has_error_code=0 create_gap=1
  • 编译器将生成带有debugint3名称的两个例程,并且经过一些准备后,这两个异常处理程序将分别调用do_debugdo_int3辅助处理程序。第三个参数定义了错误代码是否存在,此处的两个异常都没有错误代码。如上面的堆栈结构所示,如果有异常,处理器会将错误代码压入堆栈。

    那么我们可以很直观的看出,对于提供错误代码的异常和未提供错误代码的异常,堆栈的外观会有所不同。这就是为什么idtentry宏的实现中,在异常未提供错误代码的情况下将会把”伪造”的错误代码放入堆栈:

    .if \has_error_code == 0
    pushq $-1             /* ORIG_RAX: no syscall to restart */
    .endif
    

    但这不仅仅是一个”伪造”的错误代码,-1还会代表无效的系统调用号,因此这不会触发系统调用的重新启动逻辑。

  • 接下来的第一个可选参数 – shift_ist参数将表征异常处理程序是否使用了IST栈。系统中的每个内核线程都有自己的堆栈。除了这些堆栈外,还有一些专用堆栈与系统中的每个处理器相关联,异常栈就是这类专用堆栈之一。x86_64架构提供了一个新机制,它被称为Interrupt Stack Table(IST机制)。此机制允许在发生指定事件时(例如double fault之类的原子异常等)切换到新堆栈。shift_ist参数就用来标识是否需要使用IST机制为异常处理程序创建一个新的堆栈。

  • 第二个可选参数 – paranoid定义了一种方法,可以帮助我们知道服务程序的调用是来自用户空间还是来自异常处理程序。确定这一点的最简单方法是通过在CS段寄存器中的CPL(Current Privilege Level)。如果等于3,则来自用户空间,如果为零,则来自内核空间。

    “`asm
    .if \paranoid == 1
    testb $3, CS-ORIG_RAX(%rsp) /* If coming from userspace, switch stacks */
    jnz .Lfrom_usermode_switch_stack_\@
    .endif
    “`

    但是不幸的是,这种方法不能提供100%的保证。如内核文档中所述:

    如果我们处于 NMI/MCE/DEBUG 以及其他任何 super-atomic 入口上下文中,那么在正常入口将CS写入堆栈之后,执行SWAPGS之前可能已经触发异常,那么检查GS的唯一安全方法是一种速度较慢的方法:RDMSR。

    换言之,例如NMI(不可屏蔽中断)发生在swapgs指令的内部。这样的话,我们应该检查MSR_GS_BASE的值,该寄存器存储指向每个cpu区域开始的指针。因此,要检查我们是否来自用户空间,我们应该检查MSR_GS_BASE,如果它是负数,则我们来自内核空间,否则我们来自用户空间:

    movl $MSR_GS_BASE,%ecx
    rdmsr
    testl %edx,%edx
    js 1f
    

    在前两行代码中,我们将MSR_GS_BASE的值按edx:eax成对读取,我们不能为用户空间中的gs寄存器设置负值。但是从另一方面说,我们知道物理内存的直接映射是从0xffff880000000000虚拟地址开始的。这样,MSR_GS_BASE将包含从0xffff880000000000到的地址0xffffc7ffffffffff。而后rdmsr指令将被执行,%edx寄存器中可能的最小值将会是0xffff8800也就是-30720(unsigned 4 bytes)。这就是gs指向per-cpu区域开始的内核空间包含负值的原因。

  • 在为通用寄存器分配空间之后,我们进行一些检查以了解异常是否来自用户空间,如果是,则应移回中断的进程堆栈或保留在异常堆栈上:

.if \paranoid
    .if \paranoid == 1
        testb    $3, CS(%rsp)
        jnz    1f
    .endif
    call    paranoid_entry
.else
    call    error_entry
.endif

让我们考虑一下所有这些情况。

当用户空间中发生异常时

可以看到,当用户空间中发生异常时,内核会执行如下处理逻辑:

.if \paranoid == 1
    testb   $3, CS-ORIG_RAX(%rsp)       /* If coming from userspace, switch stacks */
    jnz .Lfrom_usermode_switch_stack_\@
.endif
.if \paranoid == 1
    /*
     * Entry from userspace.  Switch stacks and treat it
     * as a normal entry.  This means that paranoid handlers
     * run in real process context if user_mode(regs).
     */
.Lfrom_usermode_switch_stack_\@:
    idtentry_part \do_sym, \has_error_code, \read_cr2, paranoid=0
.endif

也就是核心是执行idtentry_part \do_sym, \has_error_code, \read_cr2, paranoid=0

那么关于idtentry_part/source/arch/x86/entry/entry_64.S#L868处实现

/*
 * Exception entry points.
 */
#define CPU_TSS_IST(x) PER_CPU_VAR(cpu_tss_rw) + (TSS_ist + (x) * 8)

.macro idtentry_part do_sym, has_error_code:req, read_cr2:req, paranoid:req, shift_ist=-1, ist_offset=0

    .if \paranoid
        call    paranoid_entry
    /* returned flag: ebx=0: need swapgs on exit, ebx=1: don't need it */
    .else
        call    error_entry
    .endif
    UNWIND_HINT_REGS

    .if \read_cr2
    /*
     * Store CR2 early so subsequent faults cannot clobber it. Use R12 as
     * intermediate storage as RDX can be clobbered in enter_from_user_mode().
     * GET_CR2_INTO can clobber RAX.
     */
    GET_CR2_INTO(%r12);
    .endif

    .if \shift_ist != -1
    TRACE_IRQS_OFF_DEBUG            /* reload IDT in case of recursion */
    .else
    TRACE_IRQS_OFF
    .endif

    .if \paranoid == 0
    testb   $3, CS(%rsp)
    jz  .Lfrom_kernel_no_context_tracking_\@
    CALL_enter_from_user_mode
.Lfrom_kernel_no_context_tracking_\@:
    .endif

    movq    %rsp, %rdi          /* pt_regs pointer */

    .if \has_error_code
    movq    ORIG_RAX(%rsp), %rsi        /* get error code */
    movq    $-1, ORIG_RAX(%rsp)     /* no syscall to restart */
    .else
    xorl    %esi, %esi          /* no error code */
    .endif

    .if \shift_ist != -1
    subq    $\ist_offset, CPU_TSS_IST(\shift_ist)
    .endif

    .if \read_cr2
    movq    %r12, %rdx          /* Move CR2 into 3rd argument */
    .endif

    call    \do_sym

    .if \shift_ist != -1
    addq    $\ist_offset, CPU_TSS_IST(\shift_ist)
    .endif

    .if \paranoid
    /* this procedure expect "no swapgs" flag in ebx */
    jmp paranoid_exit
    .else
    jmp error_exit
    .endif

.endm
分类: CTF

0 条评论

发表评论

电子邮件地址不会被公开。 必填项已用*标注