到此我们理解了如何去实现一个可以获取任意内存地址(通常是内核内存)访问权的mmap handler。现在的问题是:我们如何用现有的知识来获取root权限?我们考虑两种基本情景:
我们知道物理内存布局(通常通过/proc/iomem)
黑盒模型 - 我们只是有一个非常大的mmap
当我们了解了物理内存布局后,我们可以轻易地查看我们映射了内存的那个区域,也可以试图去把想要的内存区域与虚拟地址进行关联。
这允许我们对信令(creds)/函数指针执行精准的覆写。
更有意思的在于完成黑盒模型的情景。它可以工作在多版本内核和CPU架构,且一旦写成了exploit,它对不同的驱动来说都会更为的可靠。
为了写这样的exp,我么需要找出内存中的一些pattern,这些pattern可以直接告诉我们找到的东西是否有用。
当我们开始考虑我们可以搜索到什么时,我们就迅速的找到了实现方法:“有一些我们可以搜索的明显pattern,至少16字节,既然是全部内存我们应该可以几乎找到任何东西”。
如果我们看一下credential结构体(struct cred)的话,就可以看到一些有意思的数据:
struct cred {
atomic_t usage;
#ifdef CONFIG_DEBUG_CREDENTIALS
atomic_t subscribers; /* number of processes subscribed */
void *put_addr;
unsigned magic;
#define CRED_MAGIC 0x43736564
#define CRED_MAGIC_DEAD 0x44656144
#endif
kuid_t uid; /* real UID of the task */
kgid_t gid; /* real GID of the task */
kuid_t suid; /* saved UID of the task */
kgid_t sgid; /* saved GID of the task */
kuid_t euid; /* effective UID of the task */
kgid_t egid; /* effective GID of the task */
kuid_t fsuid; /* UID for VFS ops */
kgid_t fsgid; /* GID for VFS ops */
unsigned securebits; /* SUID-less security management */
kernel_cap_t cap_inheritable; /* caps our children can inherit */
kernel_cap_t cap_permitted; /* caps we're permitted */
kernel_cap_t cap_effective; /* caps we can actually use */
kernel_cap_t cap_bset; /* capability bounding set */
kernel_cap_t cap_ambient; /* Ambient capability set */
#ifdef CONFIG_KEYS
unsigned char jit_keyring; /* default keyring to attach requested * keys to */
struct key __rcu *session_keyring; /* keyring inherited over fork */
struct key *process_keyring; /* keyring private to this process */
struct key *thread_keyring; /* keyring private to this thread */
struct key *request_key_auth; /* assumed request_key authority */
#endif
#ifdef CONFIG_SECURITY
void *security; /* subjective LSM security */
#endif
struct user_struct *user; /* real user ID subscription */
struct user_namespace *user_ns; /* user_ns the caps and keyrings are relative to. */
struct group_info *group_info; /* supplementary groups for euid/fsgid */
struct rcu_head rcu; /* RCU deletion hook */
};
cred结构体用于控制我们线程的信令。这意味着我们可以掌握此结构体的大部分值,可以通过简单的读/proc/
查看结构体定义可以观察到有8个连续的整型变量,我们对此很熟悉(uid,gid,suid,sgid等)。紧随其后是一个4字节的securebits变量,再后面是4或5个(实际数量取决于内核版本)long long int(cap_inheritable等)。
我们获取root权限的计划是:
获取我们的credentials
扫描内存去查找这样的一组跟随4-5个long long int型capabilities变量的8个int型变量。在capabilities和uids/gids之间还应该有4个字节的留空。
将uids/gids改为值0
调用getuid(),检查我们是否已经是root用户
如果是,则将capabilities修改为值0xffffffffffffffff
如果不是,则恢复uids/gids的旧值,继续查找;重复步骤2
我们现在是root,跳出循环
在某些情况下,这一方案不奏效,例如:
如果内核是坚固的,一些组建对提权进行了监视(例如,一些三星手机设备上的Knox)。
如果我们已经有了值为0的uid。这种情况下我们好像可以修改内核的一些东西因为内核包含了大量的0值在内存中而我们的pattern没什么用。
如果一些安全模块被使能(SELinux, Smack等),我们可能完成的是部分提权,安全模块需要通过后面的步骤来绕过。
在安全模块的情况下,cred结构体的security域拥有一个指向内核使用的特殊安全模块定义的结构体。例如,对SELinux来说他是指向一个包含下列结构体的内存区域:
struct task_security_struct {
u32 osid; /* SID prior to last execve */
u32 sid; /* current SID */
u32 exec_sid; /* exec SID */
u32 create_sid; /* fscreate SID */
u32 keycreate_sid; /* keycreate SID */
u32 sockcreate_sid; /* fscreate SID */
};
我们可以替换security域的指针为一个我们已经控制的地址(如果给定架构(如arm, aarch64)允许我们在内核中直接访问用户空间映射的话,我们可以提供用户空间映射),然后brute force sid值。进程应该相对快速因为大部分权限标签例如内核或初始化时会将该值设置为0到512之间。
为了绕过SELinux我们需要尝试下列步骤:
准备一个新的SELinux策略,该策略将当前SELinux的上下文设置成宽松
固定伪造的包含全0值的security结构
尝试去重载SELinux策略
恢复旧的安全指针
尝试去执行一个恶意行为,该行为此前被SELinux禁止
如果他工作的话,我们就绕过了SELinux
如果不行的话,在我们伪造的security结构中递增sid值,重试
这一部分我们将会尝试开发一个完整root权限的exp,针对下面的代码:
static int simple_mmap(struct file *filp, struct vm_area_struct *vma)
{
printk(KERN_INFO "MWR: Device mmap\n");
printk(KERN_INFO "MWR: Device simple_mmap( size: %lx, offset: %lx)\n", vma->vm_end - vma->vm_start, vma->vm_pgoff);
if (remap_pfn_range(vma, vma->vm_start, vma->vm_pgoff, vma->vm_end - vma->vm_start, vma->vm_page_prot))
{
printk(KERN_INFO "MWR: Device mmap failed\n");
return -EAGAIN;
}
printk(KERN_INFO "MWR: Device mmap OK\n");
return 0;
}
代码有2个漏洞:
vma->vm_pgoff在remap_pfn_range中被作为一个物理地址直接使用而没有进行安检。
传递给remap_pfn_range的映射尺寸没有做安检。
我们exp开发的第一步就是,创建触发漏洞的