kernel pwn 初识随笔

To begin with

在冰城 xman 冬令营跟着华为的老师了解了安卓内核安全的内容,本想着自己对 pwn 的了解很浅,借此机会学一些堆的内容,却没想到内核层也有很多基础内容需要了解,这也导致为期一周的冬令营一直在划水,只是在思路上了解了一些简单的内容。

最后结束时,老师 push 了一篇总结,本觉得自己划了这么久,没有什么可以写的,gdb 调试也很水,那就总结一下这期间的收获吧,最终还是斗胆将此总结放到博客上。不过从大佬的视角来看,充其量本篇内容也只是简单的 guide,糅合了很多的参考内容,并没有太多营养,且因为水平有限,如有纰漏请指出。

1 Linux内核驱动

1.1 Linux内核驱动简要结构

内核模块驱动一般以.ko文件方式存在,这里以一个最简化虚拟设备源代码为例介绍其结构。

#include <linux/init.h>         
#include <linux/module.h>      
static int  __init  hello_init(void)      /*模块加载函数,通过insmod命令加载模块时,被自动执行*/
{ 
  printk(KERN_INFO " Hello World enter\n"); 
return 0; 
} 
static void  __exit  hello_exit(void)    /*模块卸载函数,当通过rmmod命令卸载时,会被自动执行*/
{ 
  printk(KERN_INFO " Hello World exit\n "); 
} 
module_init(hello_init); 
module_exit(hello_exit); 
MODULE_LICENSE("Dual BSD/GPL");     /*模块许可证明,描述内核模块的许可权限*/

对于该设备来说,使用module_init注册初始化函数,使用module_exit注册销毁函数,在源码中易看到对用的函数逻辑。其中printk为打印内核日志。

稍复杂一些,对一个字符设备而言,会增加如下结构体。

struct file_operations d_fops = {
	.owner = THIS_MODULE,
	.open = d_open,
	.read = d_read,
	.write = d_write,
	.ioctl = d_ioctl,
	.release = d_release,
	};

该结构体示例展示了部分文件操作对应的函数指针。如读该设备时会调用d_open函数。从该结构体我们可以看出其实现了用户与内核驱动交互的接口,同时也成为了内核攻击的入口点之一。

1.2 Tzdriver驱动简要结构

对于华为手机终端内核而言,代码总量庞大,以其中tzdriver作为例子进行简要说明。

根据上一节介绍的的结构,我们分别关注初始化函数、销毁函数、文件操作函数的声明。

//Code_Opensource/kernel/drivers/hisi/tzdriver/tc_client_driver.c

fs_initcall_sync(tc_init);// 函数实现中调用了module_init
module_exit(tc_exit);

static const struct file_operations TC_NS_ClientFops = {
	.owner = THIS_MODULE,
	.open = tc_client_open,
	.release = tc_client_close,
	.unlocked_ioctl = tc_client_ioctl,
	.mmap = tc_client_mmap,
	.compat_ioctl = tc_compat_client_ioctl,
};

可以看到实现了compat_ioctl的支持64位内核的ioctl函数,对于内核的漏洞分析可关注该函数的实现以及调用过程。细节内容因本人水平原因未深入了解,不再说明。

2. Arm Kernel PWN

2.1 Backgroud

我们要研究的权限分为EL0、EL1、EL2、EL3四层*,其中我们需要关注的是EL0和EL1,分别对应用户态和内核态,我们的目标即从EL0层提权到EL1层,我们要利用的内容是内核驱动模块的漏洞。

具体的理论内容由于水平有限不能详细介绍,利用过程可能存在纰漏。

驱动处理预期流程是:

  1. 用户态调用驱动
  2. 触发状态切换,进入内核态
  3. 内核态响应用户请求,处理数据
  4. 返回结果,切换回用户态

其中,若要提升权限,则需要修改权限信息。kernel记录了进程的权限,更具体的,是用 cred 结构体记录的,每个进程中都有一个cred结构,这个结构保存了该进程的权限等信息(uid,gid等),如果能修改某个进程的cred,那么也就修改了这个进程的权限。

* In ARMv8, the four privilege levels extend this to provide support for virtualization and security. The levels behave like this:
PL0 - This is unprivileged and is used for executing user code under an OS or hypervisor
PL1 - This is privileged and is used for running an OS like Linux
PL2 - This has a higher level of privilege and can be used to run a hypervisor which takes control of the system and can host multiple "guest" operating systems
PL3 - This is the highest level off privilege and is used to control (and protect) access to the Secure world supported by TrustZone

FhUDUxW3NhZQgypA1uWdA3Xkum3J

2.2 Exmaple

这里以一个kernel pwn练习平台中一道简单的栈溢出进行简要说明,我们关闭了内核中所有保护。
内核驱动模块代码摘要:

int proc_entry_write(struct file *file, const char __user *ubuf, unsigned long count, void *data)
{
    char buf[MAX_LENGTH];
    if (copy_from_user(&buf, ubuf, count)) {
        printk(KERN_INFO "stackBufferProcEntry: error copying data from userspacen");
        return -EFAULT;
    }
    return count;
}
static int __init stack_buffer_proc_init(void)
{
    stack_buffer_proc_entry = create_proc_entry("stack_buffer_overflow", 0666, NULL);
    stack_buffer_proc_entry->write_proc = proc_entry_write;
    printk(KERN_INFO "created /proc/stack_buffer_overflown");
    return 0;
}
module_init(stack_buffer_proc_init);
module_exit(stack_buffer_proc_exit);

上述驱动会创建/proc/stack_buffer_overflow设备文件,当向该设备文件调用 write 系统调用时会调用proc_entry_write函数进行处理,在proc_entry_write函数中定义了一个64字节大小的栈缓冲区,copy_from_user函数(实现了将用户空间的数据传送到内核空间)在处理数据时并未检测数据长度,直接拷贝至内核空间(此处可通俗理解memcpy的内存拷贝),当我们输入超过64字节时我们能够覆盖其他的数据,比如返回地址等,进而劫持程序执行流到我们的shellcode中进行提权。在linux stack overflow中,使用ROP形式传入超长数据覆盖返回地址,跳转至可控内容即可获得shell控制权,如ret2libc。而在内核中稍有区别。

2.3 Privilege escalation

为了提权我们需要修改cred结构的内容,2.6.29以后引入了cred结构,所以提权payload可为:

void __attribute__((regparm(3))) payload() {
    commit_creds(prepare_kernel_cred(0);//创建新的凭证结构体,且uid/gid为0,为当前任务设置新的权限凭据
}

但是要在栈地址中跳转调用该函数我们需要知道函数地址,在/proc/kallsyms文件中保存着所有的内核符号的名称和它在内存中的位置。不过在新内核版本中,为了使利用内核漏洞变得更加困难,linux内核目前禁止一般用户获取符号。

当启用 kptr_restrict 是我们不能获取内核符号地址的。

root@generic:/ # cat /proc/kallsyms | grep commit_creds                        
00000000 T commit_creds

在本文中,把它禁用掉,不管他。

root@generic:/ # echo 0 > /proc/sys/kernel/kptr_restrict                       
root@generic:/ # cat /proc/kallsyms | grep commit_creds                        
c0039834 T commit_creds
root@generic:/ # cat /proc/kallsyms | grep prepare_kernel_cred                 
c0039d34 T prepare_kernel_cred

禁用掉之后,我们通过/proc/kallsyms获取了commit_creds和prepare_kernel_cre的地址。

至此我们解决了提权问题,还剩下一个重要的部分,执行用户态/system/bin/sh。在linux kernel中,可以使用ROP,通过内核空间的rop链达到执行commit_creds(prepare_kernel_cred(0))来达到提权目的,之后返回到用户态,执行用户空间的 system("/bin/sh") 获取 shell,其中一个重要环节就是状态切换。

2.4 Back to user mode

在x86平台有iret指令可以回到用户态(利用swapgs; iretq等返回到用户态),在arm下 cpsr 寄存器的M[4:0]位用来表示处理器的运行模式,将cpsr寄存器的M[4:0]位设置为10000后就表示处理器进入了用户模式。

在本例中,使用了ret2usr做法提权。

2.5 Ret2usr

ret2usr攻击利用了用户空间的进程不能访问内核空间,但内核空间能访问用户空间这个特性来定向内核代码或数据流指向用户控件,以EL1特权执行用户空间代码完成提权等操作。

ret2usr做法中,直接返回到用户空间构造的commit_creds(prepare_kernel_cred(0))(通过函数指针实现)来提权,虽然这两个函数位于内核空间,但此时我们是EL1特权,因此可以正常运行。之后也是通过改变cpsr寄存器返回到用户态来执行用户空间的 system("/bin/sh")。

可以看出ret2usr避免了在内核空间构造特定ROP链,降低了利用难度。

最终,我们的利用思路是:

  1. 调用commit_creds(prepare_kernel_cred(0))提升权限
  2. 设置 cpsr 寄存器,使cpu进入用户模式
  3. 然后执行 execl("/system/bin/sh", "sh", NULL); 起一个内核态root权限的shell

2.6 Arm缓释机制

在实际的环境中,内核是开启保护机制,由于时间和能力原因,对缓释机制了解很浅。简单说一下自己的理解(搜到的资料)。

1.mmap_min_addr

指定用户进程通过mmap可使用的最小虚拟内存地址,以避免其在低地址空间产生映射导致安全问题。

2.kptr_restrict / dmesg_restrict

在linux内核漏洞利用中常常使用commit_creds和prepare_kernel_cred来完成提权,它们的地址可以从/proc/kallsyms中读取。/proc/sys/kernel/kptr_restrict被默认设置为1以阻止通过这种方式泄露内核地址。
dmesg_restrict限制非特权读dmesg(Restrict unprivileged access to kernel syslog)

3.PXN / PAN

arm里面的PXN(Privilege Execute Never)和PAN(Privileged Access Never)分别对应x86下的SMEP(Supervisor Mode Execution Prevention,管理模式执行保护)和SMAP(Supervisor Mode Access Prevention,管理模式访问保护),其作用分别是禁止内核执行用户空间的代码和禁止内核访问用户空间的数据。

PXN的研究与绕过,即 kernel rop。

4.KASLR

内核地址空间布局随机化(Kernel address space layout randomization),类似aslr。

5.KCFI

内核控制流完整性是一种安全机制,它不允许更改已编译二进制文件的原始控制流图,因而执行此类攻击变得异常困难。

6.HKIP / RKP

HKIP,华为内核完整性保护。为了更好地保护内核的完整性,EMUI使用硬件管理程序虚拟化技术来实现实时的内核完整性保护,通过防止内核代码段与重要系统寄存器被篡改、防止恶意代码在特权模式下注入等方式来保护关键位置。
RKP,三星手机包含了一个安全管理程序RKP,是Knox的一部分,基本功能就是防止内核代码被修改,防止内核的结构体被修改,防止内存控制流程

Ref

1.https://consumer.huawei.com/en/opensource/detail/
2.https://source.android.google.cn/security/overview/kernel-security
3.https://ctf-wiki.github.io/ctf-wiki/pwn/linux/kernel/basic_knowledge
4.https://ctf-wiki.github.io/ctf-wiki/pwn/linux/kernel/ret2usr/
5.https://www.anquanke.com/post/id/86617
6.https://www.cnblogs.com/Joe-Z/p/5651812.html
7.https://consumer-img.huawei.com/content/dam/huawei-cbg-site/en/mkt/legal/privacy-policy/EMUI 8.0 Security Technology White Paper.pdf

Updated At: Author:xmsec
Chief Water dispenser Manager of Lancet, delivering but striving.
Github
comments powered by Disqus