kernel
通常一个内核由负责响应中断的中断服务程序,负责管理多个进程从而分享处理器时间的调度程序,负责管理进程地址空间的内存管理程序,进程间通讯等系统服务程序共同组成。 –linux内核设计与实现
一直想学kernel pwn,但迟迟没有开始(懒🤦)。最近又心血来潮,翻开了linux内核设计与实现
这本书,准备看这本来入门kernel。这将会是一个系列,记录下自己的学习过程(读书笔记+kernel_pwn题目复现),希望自己可以坚持下去🎃。
kernel顾名思义是操作的核心,它其实起到了一个承上(用户空间)启下(硬件设备)的作用。应用程序通过系统调用来与内核通信,内核通过处理中断(通过中断号找中断服务程序)来管理系统的硬件设备。
注意:很多操作系统的中断服务程序,包括linux,都不在进程上下文中执行;而是在专门的中断上下文中运行,与所有进程都无关。
kernel_pwn_ROP
题目来自qwd2018_core。
就ctf里kernel pwn而言我们所要做的就是提权(得到一个root权限的进程)。挖掘出存在于kernel模块里的漏洞并通过用户态的相应的系统调用进到内核态中去触发漏洞从而到达提权的目的。
直接撸题✔
解压之后可以看到这些文件:
bzImage:压缩后的内核镜像
vmlinux:编译出的原始内核文件,没有被压缩而且是静态连接的。
.sh:qemu的启动脚本
.cpio:打包后的文件系统
我们首先看下.sh文件:
1 | qemu-system-x86_64 \ |
解释下参数:
1 | -m 指定内存(RAM)大小 |
保护机制:
常见的有kaslr(地址随机化),smep(内核态不可执行用户态的代码),smap(内核态不可访问用户态的数据)。
解题过程:
拿到这些文件后首先要做的就是解包.cpio文件,但不要在共享文件夹下进行此操作,会导致解包的软链接出错。我一般是这样:
1 | cp give_to_player /home/an9ela/kernel |
得到文件系统之后首先要看的就是Init文件了,这个是初始化内核时所进行的操作“
1 | #!/bin/sh |
接下来就要分析core.ko模块了,可以用ida打开。
首先查看init_module函数,该函数会在模块加载时执行。
1 | __int64 init_module() |
该函数调用proc_create创建了名为core的虚拟文件,应用层通过读写改文件实现与内核的交互。
其中第三个参数file_operations存储了内核模块提供的对设备进行各种操作的函数指针,对于用户态而言也可以理解为我们可以通过core文件能进行那些操作。
点进去fop
1 | .data:0000000000000438 dq offset core_write |
可见能执行的操作为write,ioctl,release
接下来是core_ioctl函数:
1 | __int64 __fastcall core_ioctl(__int64 a1, int a2, __int64 a3) |
int ioctl(int fd, ind cmd, …);是设备驱动程序中对设备的io通道进行管理的函数。其中fd是用户程序打开设备时使用open函数返回的文件标识符,cmd是用户程序对设备的控制命令。
ioctl是文件结构中的一个属性分量,如果你的驱动程序提供了对ioctl的支持,用户就可以在用户程序中使用ioctl函数实现对设备io通道的控制。
意思就是如果lkm中提供了ioctl功能(比如上面的这个函数),并且实现了对应指令的操作,那么在用户态中,通过这个驱动程序,我们就可以调用ioctl函数来直接调用模块中的操作。
而这里就提供了ioctl功能,继续分析core_read()。
1 | unsigned __int64 __fastcall core_read(__int64 a1) |
copy_to_user()函数中将v5+off中的0x40长度的数据拷贝到用户空间,其中off是bss上的全局变量。
而case 0x6677889C就给了我们控制off的机会,因此我们利用通过控制off=0x40来leak canary。
core_copy_func函数:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20 __int64 __fastcall core_copy_func(__int64 a1)
{
__int64 result; // rax
__int64 v2; // [rsp+0h] [rbp-50h]
unsigned __int64 v3; // [rsp+40h] [rbp-10h]
v3 = __readgsqword(0x28u);
printk(&unk_215);
if ( a1 > 63 )
{
printk(&unk_2A1);
result = 0xFFFFFFFFLL;
}
else
{
result = 0LL;
qmemcpy(&v2, &name, (unsigned __int16)a1);
}
return result;
}
首先对长度进行了检查,但注意数据类型,a1为int64,而在qmemcpy函数里类型了uint16,因此我们设计a1为0xffffffff00000000|(real_len)使int64为负数,从而绕过检查的同时造成溢出。
这样分析下来利用思路就很明确了:
先利用core_read()泄露canary,之后再利用core_cpoy_func在内核栈里构造rop,使其在内核态执行commit_creds(prepare_kernel_cred(0))使得进程的uidgid=0,然后返回到用户态执行system(“/bin/sh)从而完成提权。
难点在于rop的构造。
对于执行commit_creds(prepare_kernel_cred(0))来说,传参和用户态的一致,但要注意commit_creds()的参数是prepare_kernel_cred(0)函数的返回值。
从内核态返回用户态时要用到条指令swapgs和iretq。在执行iretq之前,执行swapgs指令。该指令通过用一个MSR中的值交换GS寄存器的内容,用来获取指向内核数据结构的指针,然后才能执行系统调用之类的内核空间程序。
iret(特权返回)指令从内核空间返回到用户空间进程。但是iret(64位下为iretq)期望的特定的堆栈布局如下所示:
1 | |----------------------| |
新的用户空间指令指针(RIP),用户空间堆栈指针(RSP),代码和堆栈段选择器(CS和SS)以及具有各种状态信息的EFLAGS寄存器。
一般我们用以下的扩展内联汇编获得所需的寄存器的值:
1 | void save_status(){ |
rop:
1 | pwndbg> stack 50 |
exp:
1 |
|
一点补充:
静态编译exp(内核没有c库):gcc exp.c -static -o exploit
查找gadget指令:ROPgadget --binary vmlinux > ropgadget
调试方法:
.sh脚本里有-s我们可以通过
1 | gdb ./vmlinux |
这样就可以直接根据模块里的函数名下断了。
但又一点很疑惑的时pwngdb调内核空间巨卡,而且si/ni直接跑飞了🤦,只能下断点再c这样调,ruan师傅说gef调起来很快,得再去搞个gef来。
参考博客(感谢):
https://f5.pm/go-26809.html
https://www.cnblogs.com/T1e9u/p/13805760.html
https://www.freebuf.com/articles/system/227357.html