*CTF babyarm内核题目分析
「主页」:https://unrav31.github.io
本文从漏洞分析、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
函数功能就可以达到目的,代码如下:
这里编译的时候需要使用交叉编译为ARM64的程序。交叉编译环境的安装方式很简单:
编译exp:
重新打包后运行exp,根据泄露的结果得知第3个值是内核代码地址,第13个值是canary
用ARM64的基础加载地址 0xffff800008000000 算出内核基址、commit_creds
和prepare_kernel_cred
的地址:
接下来要考虑如何构造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如下:
调试的时候发现一个问题,因为ARM64的RET
指令并不会使用栈中的数据作为返回地址,而是使用X30寄存器的值,在prepare_kernel_cred
函数结束后,由于X30寄存器还是之前的值,又再次执行了prepare_kernel_cred
,这显然不是想要的结果。这里先看看ARM程序是怎么开辟栈帧的:
这是在内核中随便找的函数,不用考虑这个函数做了什么,重点关注第一条指令和最后两条指令,第一条指令将X29和X30寄存器放入到栈中,最后两条指令平衡栈。如果去掉第一条指令,那么在平衡栈的时候就会将我们构造的内容给X29和X30。这里也看到ARM不像x86那样可以通过加减地址来获得不同的指令,ARM指令必须以四字节对齐为一个指令。所以在执行prepare_kernel_cred
时应该地址加上四字节,执行commit_creds
函数也是同理。调试修改上面的payload为如下:
执行完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就行。内容如下:
观察这两段gadget,这些寄存器我们都可以控制,这就比较简单了,直接拿过来用就可以了,并且在执行完这段gadget后,会自动执行ERET
指令,其实这段函数就是内核返回用户态的代码。指定上面三个关键寄存器的值,用户态栈地址可以随意指定一个,内核只做地址校验,并不会触发panic,ELR_EL1构造为用户态代码地址,最后修改payload如下:
完整PoC如下,最后执行system("/bin/sh")
时,在clone
系统调用时会失败,原因可能是因为某个ARM寄存器未还原,触发了缺页机制,会分配一个新的页,最后PC指针指向这个非法地址,无法获取shell,所以改成了ORW的方式读取flag:
__EOF__

本文链接:https://www.cnblogs.com/unr4v31/p/16165462.html
关于博主:评论和私信会在第一时间回复。或者直接私信我。
版权声明:本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!
声援博主:如果您觉得文章对您有帮助,可以点击文章右下角【推荐】一下。您的鼓励是博主的最大动力!
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· winform 绘制太阳,地球,月球 运作规律
· AI与.NET技术实操系列(五):向量存储与相似性搜索在 .NET 中的实现
· 超详细:普通电脑也行Windows部署deepseek R1训练数据并当服务器共享给他人
· 上周热点回顾(3.3-3.9)
· AI 智能体引爆开源社区「GitHub 热点速览」