Android O 内核加固与缓解机制

0x00

本文主要结合代码介绍Android O 引入的新加固与缓解机制的原理与影响,Android官方的介绍请戳这里

这次Android引入的内核安全机制主要有:

  • PAN
  • Hardened usercopy
  • Kalsr
  • Post-init read-only memory

0x01

1.PAN

PAN(Privileged Access Never)的主要作用是防止内核任意读取用户态数据,在arm平台引入PXN以后,内核态不能直接执行用户态的代码,这让攻击方利用的漏洞难度变大。目前出现了多种PXN bypass的技术,其中一种就是在用户态伪造内核结构体的方法,在我前面的分析的CVE-2016-2434 poc中就利用了这种方法。可以学习360的spinlock2014,pjf等在mosec上的topic—Android Root利用技术漫谈:绕过PXN.
这种方法的关键是在用户态创建伪造的很和结构体,并将内核结构体指针指向这个伪造的结构体,内核不需要执行用户态代码,但需要读取用户态数据。PAN则会限制这种行为。

由于要armv8.1指令集才会支持PAN,所以Android在kernel代码中对armv8.1以下的cpu做了PAN Emulation.

1.1 PAN (armv8.1)

在armv8.1的硬件支持中,系统不需要做什么特别的改动,原理是通过在CPSR寄存其中放一个PAN标志位实现的。在armv8.1的文档中搜索PAN就可看到实现。
armv8.1_pan.png

1.2 PAN Emulation

ARM64:
新增了CONFIG_ARM64_SW_TTBR0_PAN选项来配置是否开启PAN,通过uaccess_ttbr0_disable禁止内核态向用户态的访问,原理是将ttbr0寄存器置为0,ttbr0实际上保存的是一级页表的地址,所以ttbr0被置零以后,内存页寻址失败,PAN生效。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
#ifdef CONFIG_ARM64_SW_TTBR0_PAN
/*
* Set the TTBR0 PAN bit in SPSR. When the exception is taken from
* EL0, there is no need to check the state of TTBR0_EL1 since
* accesses are always enabled.
* Note that the meaning of this bit differs from the ARMv8.1 PAN
* feature as all TTBR0_EL1 accesses are disabled, not just those to
* user mappings.
*/
alternative_if_not ARM64_HAS_PAN
nop
alternative_else
b 1f // skip TTBR0 PAN
alternative_endif
.if \el != 0
mrs x21, ttbr0_el1
tst x21, #0xffff << 48 // Check for the reserved ASID
orr x23, x23, #PSR_PAN_BIT // Set the emulated PAN in the saved SPSR
b.eq 1f // TTBR0 access already disabled
and x23, x23, #~PSR_PAN_BIT // Clear the emulated PAN in the saved SPSR
.endif
uaccess_ttbr0_disable x21

ARM32:
arm32的PAN emulation实现方式不同,是通过设置访问属性实现,这种方法更简单,但是在arm64上性能较差,所以arm64未采用

1
2
3
4
5
6
#ifdef CONFIG_CPU_SW_DOMAIN_PAN
#define DACR_INIT \
(domain_val(DOMAIN_USER, DOMAIN_NOACCESS) | \
domain_val(DOMAIN_KERNEL, DOMAIN_MANAGER) | \
domain_val(DOMAIN_IO, DOMAIN_CLIENT) | \
domain_val(DOMAIN_VECTORS, DOMAIN_CLIENT))

2.Hardened usercopy

许多漏洞的原因是程序员书写代码时在copy_*_user时,边界检查不严,比如之前分析的CVE-2016-5342 poc.Hardened usercopy 在copy_*_user函数内部对拷贝的位置和长度进行检测.检测函数check_object_size.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
static inline unsigned long __must_check copy_from_user(void *to, const void __user *from, unsigned long n)
{
if (access_ok(VERIFY_READ, from, n)) {
check_object_size(to, n, false);
n = __arch_copy_from_user(to, from, n);
} else /* security hole - plug it */
memset(to, 0, n);
return n;
}
static inline unsigned long __must_check copy_to_user(void __user *to, const void *from, unsigned long n)
{
if (access_ok(VERIFY_WRITE, to, n)) {
check_object_size(from, n, true);
n = __arch_copy_to_user(to, from, n);
}
return n;
}

check_object_size中最终调用__check_object_size

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
void __check_object_size(const void *ptr, unsigned long n, bool to_user)
{
const char *err;
/* Skip all tests if size is zero. */
if (!n)
return;
/* Check for invalid addresses. */
err = check_bogus_address(ptr, n); //检查目的地址是否有效
if (err)
goto report;
/* Check for bad heap object. */
err = check_heap_object(ptr, n, to_user); //检查是否堆越界
if (err)
goto report;
/* Check for bad stack object. */
switch (check_stack_object(ptr, n)) { //检查是否栈越界
case NOT_STACK:
/* Object is not touching the current process stack. */
break;
case GOOD_FRAME:
case GOOD_STACK:
/*
* Object is either in the correct frame (when it
* is possible to check) or just generally on the
* process stack (when frame checking not available).
*/
return;
default:
err = "<process stack>";
goto report;
}
/* Check for object in kernel to avoid text exposure. */
err = check_kernel_text_object(ptr, n);
if (!err)
return;
report:
report_usercopy(ptr, n, to_user, err);
}
EXPORT_SYMBOL(__check_object_size);

3.Kalsr

由于目前kernel没有进行随机化,所以在kernel编译好之后所有的内核符号地址都已经固定,之前也分析过如何从kernel中提取符号地址.Android kernel 4.4 版本中加入kalsr.使内核地址随机化.
在bootloader启动kernel的时候,会通过FDT向kernel传入一个seed,在kenrel启动的过程中在kaslr_early_init中利用这个seed计算一个random size
用这个值来对kernel进行随机化(也就是每次加载的基址会在默认基址上偏移random size)

1
2
3
4
5
6
7
8
9
10
11
12
13
#ifdef CONFIG_RANDOMIZE_BASE
tst x23, ~(MIN_KIMG_ALIGN - 1) // already running randomized?
b.ne 0f
mov x0, x21 // pass FDT address in x0
mov x1, x23 // pass modulo offset in x1
bl kaslr_early_init // parse FDT for KASLR options
cbz x0, 0f // KASLR disabled? just proceed
orr x23, x23, x0 // record KASLR offset
ret x28 // we must enable KASLR, return
// to __enable_mmu()
0:
#endif
b start_kernel

在kaslr_early_init中,获取fdt中的seed,并用seed计算随机化的offset

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
u64 __init kaslr_early_init(u64 dt_phys, u64 modulo_offset)
{
void *fdt;
u64 seed, offset, mask, module_range;
const u8 *cmdline, *str;
int size;
/*
* Set a reasonable default for module_alloc_base in case
* we end up running with module randomization disabled.
*/
module_alloc_base = (u64)_etext - MODULES_VSIZE;
/*
* Try to map the FDT early. If this fails, we simply bail,
* and proceed with KASLR disabled. We will make another
* attempt at mapping the FDT in setup_machine()
*/
early_fixmap_init();
fdt = __fixmap_remap_fdt(dt_phys, &size, PAGE_KERNEL);
if (!fdt)
return 0;
/*
* Retrieve (and wipe) the seed from the FDT
*/
seed = get_kaslr_seed(fdt); // 从fdt 中获取seed
if (!seed)
return 0;
/*
* Check if 'nokaslr' appears on the command line, and
* return 0 if that is the case.
*/
cmdline = get_cmdline(fdt);
str = strstr(cmdline, "nokaslr");
if (str == cmdline || (str > cmdline && *(str - 1) == ' '))
return 0;
/*
* OK, so we are proceeding with KASLR enabled. Calculate a suitable
* kernel image offset from the seed. Let's place the kernel in the
* lower half of the VMALLOC area (VA_BITS - 2).
* Even if we could randomize at page granularity for 16k and 64k pages,
* let's always round to 2 MB so we don't interfere with the ability to
* map using contiguous PTEs
*/
/* 用高16位进行随机化,并且限制随机化在2M空间以内*/
mask = ((1UL << (VA_BITS - 2)) - 1) & ~(SZ_2M - 1);
offset = seed & mask;
/* use the top 16 bits to randomize the linear region */
memstart_offset_seed = seed >> 48;
/*
* The kernel Image should not extend across a 1GB/32MB/512MB alignment
* boundary (for 4KB/16KB/64KB granule kernels, respectively). If this
* happens, increase the KASLR offset by the size of the kernel image.
*/
if ((((u64)_text + offset + modulo_offset) >> SWAPPER_TABLE_SHIFT) !=
(((u64)_end + offset + modulo_offset) >> SWAPPER_TABLE_SHIFT))
offset = (offset + (u64)(_end - _text)) & mask;
if (IS_ENABLED(CONFIG_KASAN))
/*
* KASAN does not expect the module region to intersect the
* vmalloc region, since shadow memory is allocated for each
* module at load time, whereas the vmalloc region is shadowed
* by KASAN zero pages. So keep modules out of the vmalloc
* region if KASAN is enabled.
*/
return offset;
if (IS_ENABLED(CONFIG_RANDOMIZE_MODULE_REGION_FULL)) {
/*
* Randomize the module region independently from the core
* kernel. This prevents modules from leaking any information
* about the address of the kernel itself, but results in
* branches between modules and the core kernel that are
* resolved via PLTs. (Branches between modules will be
* resolved normally.)
*/
module_range = VMALLOC_END - VMALLOC_START - MODULES_VSIZE;
module_alloc_base = VMALLOC_START;
} else {
/*
* Randomize the module region by setting module_alloc_base to
* a PAGE_SIZE multiple in the range [_etext - MODULES_VSIZE,
* _stext) . This guarantees that the resulting region still
* covers [_stext, _etext], and that all relative branches can
* be resolved without veneers.
*/
module_range = MODULES_VSIZE - (u64)(_etext - _stext);
module_alloc_base = (u64)_etext + offset - MODULES_VSIZE;
}
/* use the lower 21 bits to randomize the base of the module region */
module_alloc_base += (module_range * (seed & ((1 << 21) - 1))) >> 21;
module_alloc_base &= PAGE_MASK;
return offset;
}

ps:在kaslr_early_init中,还对内核模块加载的基址进行随机化,防止在模块中泄露内核的地址。并且提供一个CONFIG_RANDOMIZE_MODULE_REGION_FULL选项,用来保证内核模块加载地址与内核加载地址完全无关

4 Post-init read-only

Post-init read-only 是用来限制利用vdso区域部署shellcode的措施,VDSO(Virtual Dynamically-lined Shared Object)是由内核提供的虚拟so,所有程序共享,用于支持快速系统调用,如getcpu,time等时间要求较高的系统调用. 由于这段空间可写,有的exploit在漏洞利用的过程中,通过patch这段区域中的代码,将自己的shellcode在其中部署并执行.Android上的有一个dirtycow的poc就是利用这种方法提权.更多vdso的利用细节可以查看给shellcode找块福地- 通过VDSO绕过PXN.
post-init read-only机制在这段区域初始化之后将其设置为只读。

1
2
3
4
5
6
7
8
9
10
11
12
#include <linux/init.h>
#include <linux/linkage.h>
#include <linux/const.h>
#include <asm/page.h>
.globl vdso_start, vdso_end
.section .rodata //设为只读
.balign PAGE_SIZE
vdso_start:
.incbin "arch/arm64/kernel/vdso/vdso.so"
.balign PAGE_SIZE
vdso_end: