linux内存布局和ASLR下的可分配地址空间

导语

64位下的linux地址空间虽然看起来虽然庞大2^64 但是实际上进行内核与用户空间的划分后, 包括ASLR以及PIE等机制的启用, 实际留给mmap和brk的可分配区域远远小于这个值, 大约是42T的可用地址空间. 本文根据内核代码的默认宏定义进行了X86-64下的布局分析, 给基于共享内存的用户空间选址给予一定的参考.

目录

LINUX内存布局

针对X86-64内核代码的分析

..1.1. 基本布局

- -
内核地址空间范围 [0XFFFF 0000 0000 0000, 0XFFFF FFFF FFFF FFFF]
用户地址空间 [0X0000 7FFF FFFF F000, 0X0000 0000 0000 0000]
不规范地址空间 不属于内核或者用户的地址空间属于不规范地址空间

用户空间的大小由宏定义TASK_SIZE决定, 在X86-64下这个大小默认为2^47-4096(128T) 对应十六进制数为: 0X0000 7FFF FFFF F000 .

..1.2. 用户空间布局

- 用户空间布局 -
0x0
保留区
代码段(PLT代码表部分)
代码段
数据段(GOT) 只读
数据段(.got.plt) 惰性加载机制
数据段(Data)
数据段(BSS)
堆空间(Heap)
未分配区域
内存映射区域(mmap)
栈空间(进程栈)
TASK_SIZE

ASLR地址空间随机化

地址空间配置随机加载(英语:Address space layout randomization,缩写ASLR,又称地址空间配置随机化、地址空间布局随机化)是一种防范内存损坏漏洞被利用的计算机安全技术。
ASLR通过随机放置进程关键数据区域的地址空间来防止攻击者能可靠地跳转到内存的特定位置来利用函数。现代操作系统一般都加设这一机制,以防范恶意程序对已知地址进行Return-to-libc攻击。
这些数据区域一般包括代码段 数据段 堆区 栈区 mmap 动态库等, 其中涉及代码段的随机一般需要代码位置无关化的支持(PIC PIE机制)

不同版本的操作系统和内核版本, 在ASLR的实现上会有细节的不同, 这里主要是根据目前的生产环境做的分析, 用于确认ASLR的在地址空间中对内存布局带来的扰动范围.

ASLR的设置与关闭

LINUX下常见的设置或关闭有方式:

设置randomize_va_space的等级

随机化虚拟地址空间的配置说明如下:

1
2
3
4
5
6
7
8
9
10
11
12
/proc/sys/kernel/randomize_va_space
0 = Disabled
1 = Conservative Randomization
2 = Full Randomization
```
总共分了三档, 不同版本其内容随着支持程度和等级划分略有不同, 默认启用的等级也不同, 以更具体的生产环境为准 大致如下:
0 = 关闭
1 = 保守随机化: 共享库 栈 mmap vdso随机化
2 = 完全随机化: 包括brk分配的内存
2.1 = 代码段和数据段的随机化需要PIE位置无关可执行程序的支持 编译链接时添加 -fpie -pie

* 修改/关闭系统配置方式如下

echo 0 > /proc/sys/kernel/randomize_va_space

1
2

* 或者通过sysctl修改

sysctl -w kernel.randomize_va_space=0

1
2
3
4
5
6
7
8
9



##### 进程个性化设置: 进程描述符的成员personality设置 ADDR_NO_RANDOMIZE
* setarch $(uname -m) -R [--addr-no-randomize] [target exe]
* 例如 ldd ./benchmark_fast 在aslr环境下会看到每次so的内存位置都在变化
* setarch $(uname -m) -R ldd ./benchmark_fast 这样去查看则是固定不变的

##### GDB调试中打开或者关闭ASLR(默认会禁用)

关闭ASLR:
set disable-randomization on
开启ASLR:
set disable-randomization off
查看:
show disable-randomization

1
2
3
4
5
6
7


### 内核代码中相关的宏定义和随机值计算

#### TASK_SIZE 定义 用户地址空间的大小
在X86的内核代码中, 默认的TASK_SIZE为 (1UL << 47) - 4096)

2^47-4096 => 0x7fff ffff f000 约为128T

1
gdb调试程序默认会关闭aslr, 我们通过gdb运行一个程序, 然后对齐pmap可以得到如下内存分布:

00007ffff7ffc000 4K r—- ld-2.27.so
00007ffff7ffd000 4K rw— ld-2.27.so
00007ffff7ffe000 4K rw— [ anon ]
00007ffffffde000 132K rw— [ stack ]
ffffffffff600000 4K r-x– [ anon ]

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

00007ffffffde000 + 132K = ‭0x7FFFFFFFF000‬ 和内核代码中的宏定义一致



```C++
#ifdef CONFIG_X86_32
/*
* User space process size: 3GB (default).
*/
#define IA32_PAGE_OFFSET PAGE_OFFSET
#define TASK_SIZE PAGE_OFFSET
#define TASK_SIZE_LOW TASK_SIZE
#define TASK_SIZE_MAX TASK_SIZE
#define DEFAULT_MAP_WINDOW TASK_SIZE
#define STACK_TOP TASK_SIZE
#define STACK_TOP_MAX STACK_TOP

#else

#ifdef CONFIG_X86_5LEVEL
#define __VIRTUAL_MASK_SHIFT (pgtable_l5_enabled() ? 56 : 47)
#else
#define __VIRTUAL_MASK_SHIFT 47
#endif

#define TASK_SIZE_MAX ((1UL << __VIRTUAL_MASK_SHIFT) - PAGE_SIZE)

#define DEFAULT_MAP_WINDOW ((1UL << 47) - PAGE_SIZE)

/* This decides where the kernel will search for a free chunk of vm
* space during mmap's.
*/
#define IA32_PAGE_OFFSET ((current->personality & ADDR_LIMIT_3GB) ? \
0xc0000000 : 0xFFFFe000)

#define TASK_SIZE_LOW (test_thread_flag(TIF_ADDR32) ? \
IA32_PAGE_OFFSET : DEFAULT_MAP_WINDOW)
#define TASK_SIZE (test_thread_flag(TIF_ADDR32) ? \
IA32_PAGE_OFFSET : TASK_SIZE_MAX)
#define TASK_SIZE_OF(child) ((test_tsk_thread_flag(child, TIF_ADDR32)) ? \
IA32_PAGE_OFFSET : TASK_SIZE_MAX)

#define STACK_TOP TASK_SIZE_LOW
#define STACK_TOP_MAX TASK_SIZE_MAX

#define INIT_THREAD { \
.addr_limit = KERNEL_DS, \
}

extern unsigned long KSTK_ESP(struct task_struct *task);

#endif /* CONFIG_X86_64 */

栈地址

随机值大小为17G = 0x3fffff000

代码位置如下:
load_elf_binary -> setup_arg_page

1
2
retval = setup_arg_pages(bprm, randomize_stack_top(STACK_TOP),
executable_stack);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
#define __STACK_RND_MASK(is32bit) ((is32bit) ? 0x7ff : 0x3fffff)
#define STACK_RND_MASK __STACK_RND_MASK(mmap_is_ia32())

unsigned long randomize_stack_top(unsigned long stack_top)
{
unsigned long random_variable = 0;

if (current->flags & PF_RANDOMIZE) {
random_variable = get_random_long();
random_variable &= STACK_RND_MASK;
random_variable <<= PAGE_SHIFT;
}
return PAGE_ALIGN(stack_top) - random_variable;
}

MMAP映射区起始地址

1
load_elf_binary -> setup_new_exec -> arch_pick_mmap_layout ->arch_pick_mmap_base

mmap的起始计算为:
STACK_TOP - 栈最大长度 - 间隙 - 随机值
栈最小长度为128M
随机位数配置在/proc/sys/vm/mmap_rnd_bits default=28
默认随机最大值为 0xFFFFFFF000 大约为1T
用户空间起始地址0x7FFFFFFFF000

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

#define TASK_SIZE_MAX ((1UL << 47) - PAGE_SIZE) //0x7FFFFFFFF000
#define TASK_SIZE TASK_SIZE_MAX

#define STACK_TOP TASK_SIZE
#define STACK_TOP_MAX TASK_SIZE_MAX

/* 1GB for 64bit, 8MB for 32bit */
#define __STACK_RND_MASK(is32bit) ((is32bit) ? 0x7ff : 0x3fffff)
#define STACK_RND_MASK __STACK_RND_MASK(mmap_is_ia32())


static unsigned long stack_maxrandom_size(unsigned long task_size)
{
unsigned long max = 0;
if (current->flags & PF_RANDOMIZE) {
max = (-1UL) & __STACK_RND_MASK(task_size == task_size_32bit());
max <<= PAGE_SHIFT;
}

return max;
}

//task size为0x7FFFFFFFF000
//随机值为 random()& ((1UL << 28) -1) 个页面, 即0x00FFFFFFF000
void arch_pick_mmap_layout(struct mm_struct *mm, struct rlimit *rlim_stack)
{

mm->get_unmapped_area = arch_get_unmapped_area_topdown;

arch_pick_mmap_base(&mm->mmap_base, &mm->mmap_legacy_base,
arch_rnd(mmap64_rnd_bits), task_size_64bit(0),
rlim_stack); //proc/sys/vm/mmap_rnd_bits default=28

}

//
static unsigned long arch_rnd(unsigned int rndbits)
{
if (!(current->flags & PF_RANDOMIZE))
return 0;
return (get_random_long() & ((1UL << rndbits) - 1)) << PAGE_SHIFT;
}

/*
* 新布局直接调用*base = mmap_base(random_factor, task_size, rlim_stack);
*/
static void arch_pick_mmap_base(unsigned long *base, unsigned long *legacy_base,
unsigned long random_factor, unsigned long task_size,
struct rlimit *rlim_stack)
{
*legacy_base = mmap_legacy_base(random_factor, task_size);
if (mmap_is_legacy())
*base = *legacy_base;
else
*base = mmap_base(random_factor, task_size, rlim_stack);
}

//这是是随机值的上下限保护 栈至少要有128M
//task size减去随机值, 再减去栈的大小, 栈的最小值为128M
static unsigned long mmap_base(unsigned long rnd, unsigned long task_size,
struct rlimit *rlim_stack)
{
unsigned long gap = rlim_stack->rlim_cur;
unsigned long pad = stack_maxrandom_size(task_size) + stack_guard_gap;
unsigned long gap_min, gap_max;

/* Values close to RLIM_INFINITY can overflow. */
if (gap + pad > gap)
gap += pad;

/*
* Top of mmap area (just below the process stack).
* Leave an at least ~128 MB hole with possible stack randomization.
*/
gap_min = SIZE_128M;
gap_max = (task_size / 6) * 5;

if (gap < gap_min)
gap = gap_min;
else if (gap > gap_max)
gap = gap_max;

return PAGE_ALIGN(task_size - gap - rnd);
}

//max = (0x3fffff <<= 12) = 0x3fffff000
static unsigned long stack_maxrandom_size(unsigned long task_size)
{
unsigned long max = 0;
if (current->flags & PF_RANDOMIZE) {
max = (-1UL) & __STACK_RND_MASK(task_size == task_size_32bit());
max <<= PAGE_SHIFT;
}

return max;
}

代码段开始地址

ELF文件如果是普通的EXEC类型则会使用指定的入口地址下面讨论PIE编译出的DYN可执行文件
其加载地址为 DEFAULT_MAP_WINDOW /32上增加一个arch_mmap_rnd随机值
DEFAULT_MAP_WINDOW /3
2 = 0x555555554AAA
同mmap一样为 0x00FFFFFFF000 约1个T大小
代码段起始位置约为84T 随机值 1T

代码段数据段等整体随机

1
2
3
4
5
6
7
if (interpreter) {
load_bias = ELF_ET_DYN_BASE;
if (current->flags & PF_RANDOMIZE)
load_bias += arch_mmap_rnd();
elf_flags |= MAP_FIXED;
} else
load_bias = 0;
1
2
3
4
5
6
7
loc->elf_ex.e_entry += load_bias;
elf_bss += load_bias;
elf_brk += load_bias;
start_code += load_bias;
end_code += load_bias;
start_data += load_bias;
end_data += load_bias;

HEAP区开始地址

brk从BSS结束地址开始, 会有一个额外的随机arch_randomize_brk
为固定的大小范围0x02000000, 大约为33M

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
if ((current->flags & PF_RANDOMIZE) && (randomize_va_space > 1)) {
/*
* For architectures with ELF randomization, when executing
* a loader directly (i.e. no interpreter listed in ELF
* headers), move the brk area out of the mmap region
* (since it grows up, and may collide early with the stack
* growing down), and into the unused ELF_ET_DYN_BASE region.
*/
if (IS_ENABLED(CONFIG_ARCH_HAS_ELF_RANDOMIZE) &&
loc->elf_ex.e_type == ET_DYN && !interpreter)
current->mm->brk = current->mm->start_brk =
ELF_ET_DYN_BASE;

current->mm->brk = current->mm->start_brk =
arch_randomize_brk(current->mm);
#ifdef compat_brk_randomized
current->brk_randomized = 1;
#endif
}
1
2
3
4
unsigned long arch_randomize_brk(struct mm_struct *mm)
{
return randomize_page(mm->brk, 0x02000000);
}

结论部分

假定编译出来的是PIE类型的ELF并且全开ASLR设置
那么在mmap和heap之间的地址空间大小大约是
128T - 17G - 1T - 84T - 1T -33M = 42T

mmap最小起始地址略小于0x0000 7F00 0000 0000
brk起始地址最大略大于 0x0000 5655 5555 5555

这个是以T为单位的粗略计算, 考虑到计算时忽略了一些小的单位(GB级别或者更小), 包括间隙 小的随机值, 以及动态库 数据段代码段本身占用的空间, 这里可以做一个进一步的保守计算来使用这个空间.

计算这个空间的意义在于, 例如我们对共享内存使用一个固定的地址时, 需要避免和系统本身的动态分配的地址空间相冲突, 而计算出来的地址空间的确定可以保证这一点, 例如可以用这两个地址做一个中值计算, 把这个中值作为安全的绝对地址使用.

结论部分补充验证数据

  • 没有开启PIE和ASLR
    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
    0000000000400000      4K r-x-- a.out (deleted)
    0000000000600000 4K r---- a.out (deleted)
    0000000000601000 40964K rw--- a.out (deleted)
    0000000002e02000 132K rw--- [ anon ]
    00007ffff70f2000 1732K r-x-- libc-2.27.so
    00007ffff72a3000 2044K ----- libc-2.27.so
    00007ffff74a2000 16K r---- libc-2.27.so
    00007ffff74a6000 8K rw--- libc-2.27.so
    00007ffff74a8000 16K rw--- [ anon ]
    00007ffff74ac000 92K r-x-- libgcc_s.so.1
    00007ffff74c3000 2044K ----- libgcc_s.so.1
    00007ffff76c2000 4K r---- libgcc_s.so.1
    00007ffff76c3000 4K rw--- libgcc_s.so.1
    00007ffff76c4000 1608K r-x-- libm-2.27.so
    00007ffff7856000 2044K ----- libm-2.27.so
    00007ffff7a55000 4K r---- libm-2.27.so
    00007ffff7a56000 4K rw--- libm-2.27.so
    00007ffff7a57000 1480K r-x-- libstdc++.so.6.0.25
    00007ffff7bc9000 2048K ----- libstdc++.so.6.0.25
    00007ffff7dc9000 40K r---- libstdc++.so.6.0.25
    00007ffff7dd3000 8K rw--- libstdc++.so.6.0.25
    00007ffff7dd5000 12K rw--- [ anon ]
    00007ffff7dd8000 148K r-x-- ld-2.27.so
    00007ffff7fd7000 24K rw--- [ anon ]
    00007ffff7ff8000 8K r---- [ anon ]
    00007ffff7ffa000 8K r-x-- [ anon ]
    00007ffff7ffc000 4K r---- ld-2.27.so
    00007ffff7ffd000 4K rw--- ld-2.27.so
    00007ffff7ffe000 4K rw--- [ anon ]
    00007ffffffde000 132K rw--- [ stack ]
    ffffffffff600000 4K r-x-- [ anon ]
  • 开启PIE和ASLR后多次测试得到的一个接近最小可分配空间的内存分布结果如下:
    代码段从0x0000 5640开始
    mmap的则从0x 0000 7eff开始而不是概率更大的0x 0000 7f** 这样的地址
    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
    00005640b5092000      4K r-x-- a.out
    00005640b5292000 4K r---- a.out
    00005640b5293000 40964K rw--- a.out
    00005640b99f2000 132K rw--- [ anon ]
    00007effd7734000 20484K rw--- [ anon ]
    00007effd8b35000 1732K r-x-- libc-2.27.so
    00007effd8ce6000 2044K ----- libc-2.27.so
    00007effd8ee5000 16K r---- libc-2.27.so
    00007effd8ee9000 8K rw--- libc-2.27.so
    00007effd8eeb000 16K rw--- [ anon ]
    00007effd8eef000 92K r-x-- libgcc_s.so.1
    00007effd8f06000 2044K ----- libgcc_s.so.1
    00007effd9105000 4K r---- libgcc_s.so.1
    00007effd9106000 4K rw--- libgcc_s.so.1
    00007effd9107000 1608K r-x-- libm-2.27.so
    00007effd9299000 2044K ----- libm-2.27.so
    00007effd9498000 4K r---- libm-2.27.so
    00007effd9499000 4K rw--- libm-2.27.so
    00007effd949a000 1480K r-x-- libstdc++.so.6.0.25
    00007effd960c000 2048K ----- libstdc++.so.6.0.25
    00007effd980c000 40K r---- libstdc++.so.6.0.25
    00007effd9816000 8K rw--- libstdc++.so.6.0.25
    00007effd9818000 12K rw--- [ anon ]
    00007effd981b000 148K r-x-- ld-2.27.so
    00007effd9a1e000 24K rw--- [ anon ]
    00007effd9a3f000 4K r---- ld-2.27.so
    00007effd9a40000 4K rw--- ld-2.27.so
    00007effd9a41000 4K rw--- [ anon ]
    00007ffd922c7000 132K rw--- [ stack ]
    00007ffd92385000 8K r---- [ anon ]
    00007ffd92387000 8K r-x-- [ anon ]
    ffffffffff600000 4K r-x-- [ anon ]