kernel初探

kernel

kernel 也是一个程序,用来管理软件发出的数据 I/O 要求,将这些要求转义为指令,交给 CPU 和计算机中的其他组件处理,kernel 是现代操作系统最基本的部分。
kernel 最主要的功能有两点:

  1. 控制并与硬件进行交互
  2. 提供 application 能运行的环境

包括 I/O,权限控制,系统调用,进程管理,内存管理等多项功能都可以归结到上边两点中。
需要注意的是,kernel 的 crash 通常会引起重启

在pwn中的内核题通常都是提权?通过执行commit_creds(&init_cred)或commit_creds(prepare_kernel_cred(NULL)) 并且最后返回用户态的shell,就取得了root权限。

Ring Model

intel CPU 将 CPU 的特权级别分为 4 个级别:Ring 0, Ring 1, Ring 2, Ring 3。
Ring0 只给 OS 使用,Ring 3 所有程序都可以使用,内层 Ring 可以随便使用外层 Ring 的资源。
使用 Ring Model 是为了提升系统安全性,例如某个间谍软件作为一个在 Ring 3 运行的用户程序,在不通知用户的时候打开摄像头会被阻止,因为访问硬件需要使用 being 驱动程序保留的 Ring 1 的方法。
大多数的现代操作系统只使用了 Ring 0 和 Ring 3。

Loadable Kernel Modules(LKMs)

可加载核心模块 (或直接称为内核模块) 就像运行在内核空间的可执行程序,包括:

  • 驱动程序(Device drivers)
    • 设备驱动
    • 文件系统驱动
  • 内核扩展模块 (modules)

LKMs 的文件格式和用户态的可执行程序相同,Linux 下为 ELF,Windows 下为 exe/dll,mac 下为 MACH-O,因此我们可以用 IDA 等工具来分析内核模块。
模块可以被单独编译,但不能单独运行。它在运行时被链接到内核作为内核的一部分在内核空间运行,这与运行在用户控件的进程不同。
模块通常用来实现一种文件系统、一个驱动程序或者其他内核上层的功能。

在pwn中的kernel题,通常漏洞位于驱动文件即一个xxx.ko文件,这个可以在文件系统中的init文件中详细分析漏洞文件是哪个,以及更改init文件来使自己在qemu模拟时获得root权限来方便后续调试

相关指令

  • insmod: 讲指定模块加载到内核中
  • rmmod: 从内核中卸载指定模块
  • lsmod: 列出已经加载的模块
  • modprobe: 添加或删除模块,modprobe 在加载模块时会查找依赖关系

syscall

系统调用,指的是用户空间的程序向操作系统内核请求需要更高权限的服务,比如 IO 操作或者进程间通信。系统调用提供用户程序与操作系统间的接口,部分库函数(如 scanf,puts 等 IO 相关的函数实际上是对系统调用的封装(read 和 write))。

ioctl

直接查看 man 手册

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
NAME
      ioctl - control device

SYNOPSIS
      #include <sys/ioctl.h>

      int ioctl(int fd, unsigned long request, ...);

DESCRIPTION
      The ioctl() system call manipulates the underlying device parameters of special
      files. In particular, many operating characteristics of character special
      files (e.g., terminals) may be controlled with ioctl() requests. The argument
      fd must be an open file descriptor.

      The second argument is a device-dependent request code. The third argument is
      an untyped pointer to memory. It's traditionally char *argp (from the days
      before void * was valid C), and will be so named for this discussion.

      An ioctl() request has encoded in it whether the argument is an in parameter or
      out parameter, and the size of the argument argp in bytes. Macros and defines
      used in specifying an ioctl() request are located in the file <sys/ioctl.h>.

可以看出 ioctl 也是一个系统调用,用于与设备通信。
int ioctl(int fd, unsigned long request, …) 的第一个参数为打开设备 (open) 返回的 文件描述符,第二个参数为用户程序对设备的控制命令,再后边的参数则是一些补充参数,与设备有关。

状态切换

user space to kernel space

当发生 系统调用,产生异常,外设产生中断等事件时,会发生用户态到内核态的切换,具体的过程为:

  1. 通过 swapgs 切换 GS 段寄存器,将 GS 寄存器值和一个特定位置的值进行交换,目的是保存 GS 值,同时将该位置的值作为内核执行时的 GS 值使用。
  2. 将当前栈顶(用户空间栈顶)记录在 CPU 独占变量区域里,将 CPU 独占区域里记录的内核栈顶放入 rsp/esp。
  3. 通过 push 保存各寄存器值,具体的 代码 如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
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 */
  1. 通过汇编指令判断是否为 x32_abi。
  2. 通过系统调用号,跳到全局变量 sys_call_table 相应位置继续执行系统调用。

kernel space to user space

退出时,流程如下:

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

struct cred

之前提到 kernel 记录了进程的权限,更具体的,是用 cred 结构体记录的,每个进程中都有一个 cred 结构,这个结构保存了该进程的权限等信息(uid,gid 等),如果能修改某个进程的 cred,那么也就修改了这个进程的权限。
源码 如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
struct cred {
  atomic_t   usage;
#ifdef CONFIG_DEBUG_CREDENTIALS
  atomic_t   subscribers;   /* number of processes subscribed */
  void       *put_addr;
  unsigned   magic;
#define CRED_MAGIC 0x43736564
#define CRED_MAGIC_DEAD 0x44656144
#endif
  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 */
#ifdef CONFIG_KEYS
  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 */
#endif
#ifdef CONFIG_SECURITY
  void       *security; /* subjective LSM security */
#endif
  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;

内核态函数

相比用户态库函数,内核态的函数有了一些变化

  • printf() -> **printk()**,但需要注意的是 printk() 不一定会把内容显示到终端上,但一定在内核缓冲区里,可以通过 dmesg 查看效果
  • memcpy() -> copy_from_user()/copy_to_user()
    • copy_from_user() 实现了将用户空间的数据传送到内核空间
    • copy_to_user() 实现了将内核空间的数据传送到用户空间
  • malloc() -> **kmalloc()**,内核态的内存分配函数,和 malloc() 相似,但使用的是 slab/slub 分配器
  • free() -> **kfree()**,同 kmalloc()

另外要注意的是,kernel 管理进程,因此 kernel 也记录了进程的权限。kernel 中有两个可以方便的改变权限的函数:

  • *int commit_creds(struct cred new)
  • struct cred prepare_kernel_cred(struct task_struct daemon)**

从函数名也可以看出,执行 commit_creds(prepare_kernel_cred(0)) 即可获得 root 权限,0 表示 以 0 号进程作为参考准备新的 credentials。
更多关于 prepare_kernel_cred 的信息可以参考 源码
执行 commit_creds(prepare_kernel_cred(0)) 也是最常用的提权手段,两个函数的地址都可以在 /proc/kallsyms 中查看(较老的内核版本中是 /proc/ksyms)。

1
2
3
4
5
6
7
8
9
post sudo grep commit_creds /proc/kallsyms
[sudo] m4x 的密码:
ffffffffbb6af9e0 T commit_creds
ffffffffbc7cb3d0 r __ksymtab_commit_creds
ffffffffbc7f06fe r __kstrtab_commit_creds
post sudo grep prepare_kernel_cred /proc/kallsyms
ffffffffbb6afd90 T prepare_kernel_cred
ffffffffbc7d4f20 r __ksymtab_prepare_kernel_cred
ffffffffbc7f06b7 r __kstrtab_prepare_kernel_cred

一般情况下,/proc/kallsyms 的内容需要 root 权限才能查看
所以在更改init文件时,需要更改权限为root以便调试获取基址等内容

CTF kernel pwn 相关

一般会给以下三个文件

  1. boot.sh: 一个用于启动 kernel 的 shell 的脚本,多用 qemu,保护措施与 qemu 不同的启动参数有关
  2. bzImage: kernel binary
  3. rootfs.cpio: 文件系统映像

比如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
youlin@youlin-virtual-machine:~/kernel/qwb2018-core/give_to_player$ ls
bzImage core core.cpio core.ko exp exp.c g1 g2 start.sh vmlinux
youlin@youlin-virtual-machine:~/kernel/qwb2018-core/give_to_player$ file vmlinux
vmlinux: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), statically linked, BuildID[sha1]=1d8344e71a82bc43821029796ef65bebfe8e65c3, not stripped
youlin@youlin-virtual-machine:~/kernel/qwb2018-core/give_to_player$ file bzImage
bzImage: Linux kernel x86 boot executable bzImage, version 4.15.8 (simple@vps-simple) #19 SMP Mon Mar 19 18:50:28 CST 2018, RO-rootFS, swap_dev 0X6, Normal VGA
youlin@youlin-virtual-machine:~/kernel/qwb2018-core/give_to_player$ cat start.sh
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 \

  1. 解释一下 qemu 启动的参数:
    • initrd rootfs.cpio,使用 rootfs.cpio 作为内核启动的文件系统
    • kernel ./bzImage,使用 bzImage 作为 kernel 映像
    • cpu kvm64,+smep,设置 CPU 的安全选项,这里开启了 smep
    • m 64M,设置虚拟 RAM 为 64M,默认为 128M 其他的选项可以通过 –help 查看。
  2. 本地写好 exploit 后,可以通过 base64 编码等方式把编译好的二进制文件保存到远程目录下,进而拿到 flag。同时可以使用 musl, uclibc 等方法减小 exploit 的体积方便传输。(这里注意kernel的exp不再是拿python写了,而是c语言编写exp)

kernel初探
http://blogyoulin.top/2023/09/25/kernel初探/
Author
John Doe
Posted on
September 25, 2023
Licensed under