error_entry处理分析

假设我们此时进入了error_entry的处理逻辑,它在/source/arch/x86/entry/entry_64.S#L1287处实现:

/*
 * Save all registers in pt_regs, and switch GS if needed.
 */
SYM_CODE_START_LOCAL(error_entry)
    UNWIND_HINT_FUNC
    cld
    PUSH_AND_CLEAR_REGS save_ret=1
    ENCODE_FRAME_POINTER 8
    testb   $3, CS+8(%rsp)
    jz  .Lerror_kernelspace

    /*
     * We entered from user mode or we're pretending to have entered
     * from user mode due to an IRET fault.
     */
    SWAPGS
    FENCE_SWAPGS_USER_ENTRY
    /* We have user CR3.  Change to kernel CR3. */
    SWITCH_TO_KERNEL_CR3 scratch_reg=%rax

.Lerror_entry_from_usermode_after_swapgs:
    /* Put us onto the real thread stack. */
    popq    %r12                /* save return addr in %12 */
    movq    %rsp, %rdi          /* arg0 = pt_regs pointer */
    call    sync_regs
    movq    %rax, %rsp          /* switch stack */
    ENCODE_FRAME_POINTER
    pushq   %r12
    ret

.Lerror_entry_done_lfence:
    FENCE_SWAPGS_KERNEL_ENTRY
.Lerror_entry_done:
    ret

    /*
     * There are two places in the kernel that can potentially fault with
     * usergs. Handle them here.  B stepping K8s sometimes report a
     * truncated RIP for IRET exceptions returning to compat mode. Check
     * for these here too.
     */
.Lerror_kernelspace:
    leaq    native_irq_return_iret(%rip), %rcx
    cmpq    %rcx, RIP+8(%rsp)
    je  .Lerror_bad_iret
    movl    %ecx, %eax          /* zero extend */
    cmpq    %rax, RIP+8(%rsp)
    je  .Lbstep_iret
    cmpq    $.Lgs_change, RIP+8(%rsp)
    jne .Lerror_entry_done_lfence

    /*
     * hack: .Lgs_change can fail with user gsbase.  If this happens, fix up
     * gsbase and proceed.  We'll fix up the exception and land in
     * .Lgs_change's error handler with kernel gsbase.
     */
    SWAPGS
    FENCE_SWAPGS_USER_ENTRY
    SWITCH_TO_KERNEL_CR3 scratch_reg=%rax
    jmp .Lerror_entry_done

.Lbstep_iret:
    /* Fix truncated RIP */
    movq    %rcx, RIP+8(%rsp)
    /* fall through */

.Lerror_bad_iret:
    /*
     * We came from an IRET to user mode, so we have user
     * gsbase and CR3.  Switch to kernel gsbase and CR3:
     */
    SWAPGS
    FENCE_SWAPGS_USER_ENTRY
    SWITCH_TO_KERNEL_CR3 scratch_reg=%rax

    /*
     * Pretend that the exception came from user mode: set up pt_regs
     * as if we faulted immediately after IRET.
     */
    mov %rsp, %rdi
    call    fixup_bad_iret
    mov %rax, %rsp
    jmp .Lerror_entry_from_usermode_after_swapgs
SYM_CODE_END(error_entry)
保存现场(储存所有通用寄存器)

首先内核会把返回地址保存在R12寄存器中,随即会调用PUSH_AND_CLEAR_REGS将通用寄存器的值存储在中断栈上:

首先内核会调用PUSH_AND_CLEAR_REGS将通用寄存器的值存储在中断栈上:

.macro PUSH_AND_CLEAR_REGS rdx=%rdx rax=%rax save_ret=0
    /*
     * Push registers and sanitize registers of values that a
     * speculation attack might otherwise want to exploit. The
     * lower registers are likely clobbered well before they
     * could be put to use in a speculative execution gadget.
     * Interleave XOR with PUSH for better uop scheduling:
     */
    .if \save_ret
    pushq   %rsi        /* pt_regs->si */
    movq    8(%rsp), %rsi   /* temporarily store the return address in %rsi */
    movq    %rdi, 8(%rsp)   /* pt_regs->di (overwriting original return address) */
    .else
    pushq   %rdi        /* pt_regs->di */
    pushq   %rsi        /* pt_regs->si */
    .endif
    pushq   \rdx        /* pt_regs->dx */
    xorl    %edx, %edx  /* nospec   dx */
    pushq   %rcx        /* pt_regs->cx */
    xorl    %ecx, %ecx  /* nospec   cx */
    pushq   \rax        /* pt_regs->ax */
    pushq   %r8     /* pt_regs->r8 */
    xorl    %r8d, %r8d  /* nospec   r8 */
    pushq   %r9     /* pt_regs->r9 */
    xorl    %r9d, %r9d  /* nospec   r9 */
    pushq   %r10        /* pt_regs->r10 */
    xorl    %r10d, %r10d    /* nospec   r10 */
    pushq   %r11        /* pt_regs->r11 */
    xorl    %r11d, %r11d    /* nospec   r11*/
    pushq   %rbx        /* pt_regs->rbx */
    xorl    %ebx, %ebx  /* nospec   rbx*/
    pushq   %rbp        /* pt_regs->rbp */
    xorl    %ebp, %ebp  /* nospec   rbp*/
    pushq   %r12        /* pt_regs->r12 */
    xorl    %r12d, %r12d    /* nospec   r12*/
    pushq   %r13        /* pt_regs->r13 */
    xorl    %r13d, %r13d    /* nospec   r13*/
    pushq   %r14        /* pt_regs->r14 */
    xorl    %r14d, %r14d    /* nospec   r14*/
    pushq   %r15        /* pt_regs->r15 */
    xorl    %r15d, %r15d    /* nospec   r15*/
    UNWIND_HINT_REGS
    .if \save_ret
    pushq   %rsi        /* return address on top of stack */
    .endif
.endm

执行后,堆栈将如下所示:

     +------------+
+160 | %SS        |
+152 | %RSP       |
+144 | %RFLAGS    |
+136 | %CS        |
+128 | %RIP       |
+120 | ERROR CODE |
     |------------|
+112 | %RDI       |
+104 | %RSI       |
 +96 | %RDX       |
 +88 | %RCX       |
 +80 | %RAX       |
 +72 | %R8        |
 +64 | %R9        |
 +56 | %R10       |
 +48 | %R11       |
 +40 | %RBX       |
 +32 | %RBP       |
 +24 | %R12       |
 +16 | %R13       |
  +8 | %R14       |
  +0 | %R15       | <- %RSP
     +------------+
再次检查CPL

内核将通用寄存器保存在堆栈中之后,因为正如官方文档中描述的那样,一旦发生%RIP中断,则有可能发生错误,我们应该使用以下命令再次检查是否来自用户空间空间:

testb  $3, CS+8(%rsp)
jz  .Lerror_kernelspace
初始化GS寄存器

接下来将执行SWAPGS指令,这将会交换MSR_KERNEL_GS_BASEMSR_GS_BASE中的值。从这一刻起,%gs寄存器将指向内核结构的基址。

获取运行栈的栈指针(sync_regs函数分析)

接下来将会进入.Lerror_entry_from_usermode_after_swapgs:中:

movq    %rsp, %rdi
call    sync_regs

在这里,我们将堆栈的基址指针置入%rdi寄存器这将作为sync_regs函数的参数。

接下来我们来分析sync_regs函数:(在/source/arch/x86/kernel/traps.c#L613中实现)

/*
 * Help handler running on a per-cpu (IST or entry trampoline) stack
 * to switch to the normal thread stack if the interrupted code was in
 * user mode. The actual stack switch is done in entry_64.S
 */
asmlinkage __visible notrace struct pt_regs *sync_regs(struct pt_regs *eregs)
{
    struct pt_regs *regs = (struct pt_regs *)this_cpu_read(cpu_current_top_of_stack) - 1;
    if (regs != eregs)
        *regs = *eregs;
    return regs;
}
NOKPROBE_SYMBOL(sync_regs);

// In /source/include/linux/percpu-defs.h#L507

#define this_cpu_read(pcp)     __pcpu_size_call_return(this_cpu_read_, pcp)

这将会获取运行栈的栈指针将其存储在中断栈中并返回,这意味着异常处理程序将在实际流程上下文中运行。

栈切换

接下来我们进行栈切换操作

正如我们来自用户空间一样,这意味着异常处理程序将在实际流程上下文中运行。从堆栈指针中获取堆栈指针后,sync_regs我们切换堆栈:

movq    %rax, %rsp

然后内核从R12中取出返回地址,返回上级函数

可选参数逻辑分析

在用户空间发生异常的处理逻辑下,接下来只需要处理以下三个选项\has_error_code, \read_cr2, paranoid=0

    .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
        ......(代码省略)
    .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
        ......(代码省略)
    .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
read_cr2被设置

read_cr2相关的逻辑有两处,第一处是

.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

# In /source/arch/x86/entry/calling.h#L365

#define GET_CR2_INTO(reg) GET_CR2_INTO_AX ; _ASM_MOV %_ASM_AX, reg

作用是存储CR2寄存器的值到R12寄存器。

第二处逻辑是

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

作用是存储R12寄存器的值到RDX寄存器,也就是把CR2寄存器的值存储到RDX寄存器作为接下来调用函数的第三个参数。

分类: CTF

0 条评论

发表评论

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