本文主要参考ctf-wiki,记录一下自己的学习历程
内核态与用户态
Linux内核把系统分成两个空间:用户空间和内核空间。CPU既可以运行在用户空间(这时候就是我们所说的用户态),也可以运行在内核空间(内核态)。一些架构的实现还有多种执行模式,intel x86体系结构有ring0~ring3 四种执行级别。但Linux内核只使用了ring0 和 ring3两种级别来实现内核态和用户态。
内核态与用户态之间的状态转换
Linux内核为内核态和用户态之间的切换设置了软件抽象层,叫做系统调用层。通过一些指令即可实现内核态和用户态之间的切换。
用户态切换到内核态
当发生 系统调用
,产生异常
,外设产生中断
等事件时,会发生用户态到内核态的切换,具体的过程为:
- 通过
swapgs
切换 GS 段寄存器,将 GS 寄存器值和一个特定位置的值进行交换,目的是保存 GS 值,同时将该位置的值作为内核执行时的 GS 值使用。
- 将当前栈顶(用户空间栈顶)记录在 CPU 独占变量区域里,将 CPU 独占区域里记录的内核栈顶放入 rsp/esp。
- 通过 push 保存各寄存器值(具体代码在5下面)。
- 通过汇编指令判断是否为
x32_abi
。
- 通过系统调用号,跳到全局变量
sys_call_table
相应位置继续执行系统调用。
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 */
|
内核态切换到用户态
该过程也是我们在做题中经常用到的
通过 swapgs
恢复 GS 值。
- 通常使用
swapgs; popfq; ret
这条gadget
通过 sysretq
或者 iretq
恢复到用户控件继续执行。如果使用 iretq
还需要给出用户空间的一些信息(CS, eflags/rflags, esp/rsp , SS等)
- payload一般这么构造
1 2 3 4 5 6
| rop[i++] = iretq_ret_addr; rop[i++] = rip; rop[i++] = user_cs; rop[i++] = user_rflags; rop[i++] = user_sp; rop[i++] = user_ss;
|
因为iretq
需要寄存器信息,所以需要在exp的开头保存一下寄存器信息:
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
| size_t user_cs, user_ss, user_rflags, user_sp; void save_status() { __asm__("mov user_cs, cs;" "mov user_ss, ss;" "mov user_sp, rsp;" "pushf;" "pop user_rflags;" ); puts("[*]status has been saved."); }
void save_stats() { asm( "movq %%cs, %0\n" "movq %%ss, %1\n" "movq %%rsp, %3\n" "pushfq\n" "popq %2\n" :"=r"(user_cs), "=r"(user_ss), "=r"(user_eflags),"=r"(user_sp) : : "memory" ); }
|
内核模块(LKM)
Linux内核在发展过程中很早就引入了内核模块这个机制,内核模块全称是Loadable Kernel Module (LKM)。在内核运行时加载一组目标代码来实现某个特定的功能,这样在使用Linux的时候不需要重新编译内核代码来实现动态的扩展(文件系统、驱动等)。
模块可以被单独编译,但不能单独运行。它在运行时被链接到内核作为内核的一部分在内核空间运行,这与运行在用户控件的进程不同。
Linux操作LKM主要有下面几条指令:
lsmod
: 列出已经加载的模块
insmod
: 将指定模块加载到内核中
rmmod
: 从内核中卸载指定模块
ioctl
ioctl 也是一个系统调用,用于与设备通信。
1 2
| #include <sys/ioctl.h> int ioctl(int fd, unsigned long request, ...);
|
第一个参数为打开设备 (open) 返回的文件描述符,第二个参数为用户程序对设备的控制命令,再后边的参数则是一些补充参数,与设备有关。调用过程大概如下:
1 2 3 4 5 6 7 8
| | | +-----------+ | +--------------+ | +-------------------+ | | | | | | | | | ioctl() +------+-----> | sys_ioctl() | +-----+------> | babydriver_ioctl()| | | | | | | | | +-----------+ | +--------------+ | +-------------------+ | | user space + vfs + driver
|
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; void *put_addr; unsigned magic; #define CRED_MAGIC 0x43736564 #define CRED_MAGIC_DEAD 0x44656144 #endif kuid_t uid; kgid_t gid; kuid_t suid; kgid_t sgid; kuid_t euid; kgid_t egid; kuid_t fsuid; kgid_t fsgid; unsigned securebits; kernel_cap_t cap_inheritable; kernel_cap_t cap_permitted; kernel_cap_t cap_effective; kernel_cap_t cap_bset; kernel_cap_t cap_ambient; #ifdef CONFIG_KEYS unsigned char jit_keyring;
struct key __rcu *session_keyring; struct key *process_keyring; struct key *thread_keyring; struct key *request_key_auth; #endif #ifdef CONFIG_SECURITY void *security; #endif struct user_struct *user; struct user_namespace *user_ns; struct group_info *group_info; struct rcu_head rcu; } __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。
执行 commit_creds(prepare_kernel_cred(0))
也是最常用的提权手段,两个函数的地址都可以在 /proc/kallsyms
中查看,需要root权限
参考
ctf-wiki