內核pwn基礎


前言

之前有說到這學期打算在學習操作系統這門課的時候,去簡單了解一下Linux內核的內容。那時候以為學習內核(簡單瀏覽一下Linux內核的一些源碼以及大概了解一些板塊的內容)和內核pwn關系很密切,但是自己學了一下之后,其實感覺差異挺大的。因為學習內核的系統,肯定是大部分板塊都牽涉到,但是內核pwn的話更多是一些固定方向的漏洞以及利用姿勢。所以現在打算將學習內核以及學習內核pwn分開來搞。今天開始了解一下內核pwn的內容,下面先記錄一下內核pwn入門的一些基礎知識。若有錯誤之處,懇請師傅們斧正

​ kernel

​ kernel 也是一個程序,用來管理軟件發出的數據 I/O 要求,將這些要求轉義為指令,交給 CPU 和計算機中的其他組件處理,kernel 是現代操作系統最基本的部分。

kernel 最主要的功能有兩點:
控制並與硬件進行交互
提供 application 能運行的環境
包括 I/O,權限控制,系統調用,進程管理,內存管理等多項功能都可以歸結到上邊兩點中。
需要注意的是,kernel 的 crash 通常會引起重啟。

Ring Model

intel CPU 將 CPU 的特權級別分為 4 個級別:Ring 0, Ring 1, Ring 2, Ring 3。
Ring0 只給 OS 使用,Ring 3 所有程序都可以使用,內層 Ring 可以隨便使用外層 Ring 的資源。
使用 Ring Model 是為了提升系統安全性,例如某個間諜軟件作為一個在 Ring 3 運行的用戶程序,在不通知用戶的時候打開攝像頭會被阻止,因為訪問硬件需要使用 being 驅動程序保留的 Ring 1 的方法。
大多數的現代操作系統只使用了 Ring 0 和 Ring 3。

user space to kernel space

當發生 系統調用,產生異常,外設產生中斷等事件時,會發生用戶態到內核態的切換,具體的過程為:

  • 通過 swapgs 切換 GS 段寄存器,將 GS 寄存器值和一個特定位置的值進行交換,目的是保存 GS 值,同時將該位置的值作為內核執行時的 GS 值使用。
  • 將當前棧頂(用戶空間棧頂)記錄在 CPU 獨占變量區域里,將 CPU 獨占區域里記錄的內核棧頂放入RSP/ESP。
  • 通過 push 保存各寄存器值
    具體的 代碼 如下:
ENTRY(entry_SYSCALL_64)
/* SWAPGS_UNSAFE_STACK是一個宏,x86直接定義為swapgs指令 */
SWAPGS_UNSAFE_STACK

/* 保存棧值,並設置內核棧 */
movq %rsp, PER_CPU_VAR(rsp_scratch)
movq PER_CPU_VAR(cpu_current_top_of_stack), %rsp

/* 通過push保存寄存器值,形成一個pt_regs結構 */
/* Construct struct pt_regs on stack */
pushq  $__USER_DS                /* pt_regs->ss */
pushq  PER_CPU_VAR(rsp_scratch)  /* pt_regs->sp */
pushq  %r11                      /* pt_regs->flags */
pushq  $__USER_CS                /* pt_regs->cs */
pushq  %rcx                      /* pt_regs->ip */
pushq  %rax                      /* pt_regs->orig_ax */
pushq  %rdi                      /* pt_regs->di */
pushq  %rsi                      /* pt_regs->si */
pushq  %rdx                      /* pt_regs->dx */
pushq  %rcx tuichu               /* pt_regs->cx */
pushq  $-ENOSYS                  /* pt_regs->ax */
pushq  %r8                       /* pt_regs->r8 */
pushq  %r9                       /* pt_regs->r9 */
pushq  %r10                      /* pt_regs->r10 */
pushq  %r11                      /* pt_regs->r11 */
sub $(6*8), %rsp                 /* pt_regs->bp, bx, r12-15 not saved */

通過匯編指令判斷是否為 x32_abi。
通過系統調用號,跳到全局變量 sys_call_table 相應位置繼續執行系統調用。

kernel space to user space

退出時,流程如下:

  • 通過 swapgs 恢復 GS 值
  • 通過 sysretq 或者 iretq 恢復到用戶控件繼續執行。如果使用 iretq 還需要給出用戶空間的一些信息(CS, eflags/rflags, esp/rsp 等)

Loadable Kernel Modules(LKMs)

LKMs(Loadable Kernel Modules)稱為可加載核心模塊(內核模塊),其可以看作是運行在內核空間的可執行程序,包括:

  • 驅動程序(Device drivers)設備驅動文件系統驅動…
  • 內核擴展模塊 (modules)

LKMs 的文件格式和用戶態的可執行程序相同,Linux 下為 ELF,Windows 下為 exe/dll,mac 下為 MACH-O,因此我們可以用 IDA 等工具來分析內核模塊。
模塊可以被單獨編譯,但不能單獨運行。它在運行時被鏈接到內核作為內核的一部分在內核空間運行,這與運行在用戶控件的進程不同。
模塊通常用來實現一種文件系統、一個驅動程序或者其他內核上層的功能。

Linux 內核之所以提供模塊機制,是因為它本身是一個單內核 (monolithic kernel)。單內核的優點是效率高,因為所有的內容都集合在一起,但缺點是可擴展性和可維護性相對較差,模塊機制就是為了彌補這一缺陷。
通常情況下,Kernel漏洞的發生也常見於加載的LKMs出現問題。

內核中的模塊相關指令:

  • insmod: 將指定模塊加載到內核中。
  • rmmod: 從內核中卸載指定模塊。
  • lsmod: 列出已經加載的模塊。
  • modprobe: 添加或刪除模塊,modprobe 在加載模塊時會查找依賴關系。

syscall

系統調用,指的是用戶空間的程序向操作系統內核請求需要更高權限的服務,比如 IO 操作或者進程間通信。系統調用提供用戶程序與操作系統間的接口,部分庫函數(如 scanf,puts 等 IO 相關的函數實際上是對系統調用的封裝 (read 和 write))。
在 /usr/include/x86_64-linux-gnu/asm/unistd_64.h 和 /usr/include/x86_64-linux-gnu/asm/unistd_32.h 分別可以查看 64 位和 32 位的系統調用號。

ioctl

ioctl 也是一個系統調用,用於與設備通信。
int ioctl(int fd, unsigned long request, ...) 的第一個參數為打開設備 (open) 返回的 文件描述符,第二個參數為用戶程序對設備的控制命令,再后邊的參數則是一些補充參數,與設備有關。
使用 ioctl 進行通信的原因:
操作系統提供了內核訪問標准外部設備的系統調用,因為大多數硬件設備只能夠在內核空間內直接尋址, 但是當訪問非標准硬件設備這些系統調用顯得不合適, 有時候用戶模式可能需要直接訪問設備。
比如,一個系統管理員可能要修改網卡的配置。現代操作系統提供了各種各樣設備的支持,有一些設備可能沒有被內核設計者考慮到,如此一來提供一個這樣的系統調用來使用設備就變得不可能了。
為了解決這個問題,內核被設計成可擴展的,可以加入一個稱為設備驅動的模塊,驅動的代碼允許在內核空間運行而且可以對設備直接尋址。一個 Ioctl 接口是一個獨立的系統調用,通過它用戶空間可以跟設備驅動溝通。對設備驅動的請求是一個以設備和請求號碼為參數的 Ioctl 調用,如此內核就允許用戶空間訪問設備驅動進而訪問設備而不需要了解具體的設備細節,同時也不需要一大堆針對不同設備的系統調用。

struct cred

kernel 記錄了進程的權限,更具體的,是用 cred 結構體記錄的,每個進程中都有一個 cred 結構,這個結構保存了該進程的權限等信息(uid,gid 等),如果能修改某個進程的 cred,那么也就修改了這個進程的權限。
源碼可以參考一下我的這篇博文

內核態函數

相比用戶態庫函數,內核態的函數有了一些變化

printf() -> printk(),但需要注意的是 printk() 不一定會把內容顯示到終端上,但一定在內核緩沖區里,可以通過 dmesg 查看效果
memcpy() -> copy_from_user()/copy_to_user()
copy_from_user() 實現了將用戶空間的數據傳送到內核空間
copy_to_user() 實現了將內核空間的數據傳送到用戶空間
malloc() -> kmalloc(),內核態的內存分配函數,和 malloc() 相似,但使用的是 slab/slub 分配器
free() -> kfree(),同 kmalloc()
另外要注意的是,kernel 管理進程,因此 kernel 也記錄了進程的權限。kernel 中有兩個可以方便的改變權限的函數:
int commit_creds(struct cred *new)
struct cred* prepare_kernel_cred(struct task_struct* daemon)
從函數名也可以看出,執行 commit_creds(prepare_kernel_cred(0)) 即可獲得 root 權限,0 表示 以 0 號進程作為參考准備新的 credentials。
執行 commit_creds(prepare_kernel_cred(0)) 也是最常用的提權手段,兩個函數的地址都可以在 /proc/kallsyms 中查看(較老的內核版本中是 /proc/ksyms)。

內核中的保護機制

canary, NX, PIE, RELRO 等保護與用戶態原理和作用相同,也就是跟我們普通的Linux平台下的保護機制一致,這里就不解釋。

KPTI

KPTI,Kernel PageTable Isolation,內核頁表隔離。進程地址空間被分成了內核地址空間和用戶地址空間,其中內核地址空間映射到了整個物理地址空間,而用戶地址空間只能映射到指定的物理地址空間。內核地址空間和用戶地址空間共用一個頁全局目錄表。為了徹底防止用戶程序獲取內核數據,可以令內核地址空間和用戶地址空間使用兩組頁表集

KASLR

KASLR中的K指kernel,也就是內核地址空間布局隨機化。可以在內核命令行中加入nokaslr關閉KASLR。

SMAP/SMEP

SMAP(Supervisor Mode Access Prevention,管理模式訪問保護)和SMEP(Supervisor Mode Execution Prevention,管理模式執行保護)的作用分別是禁止內核訪問用戶空間的數據和禁止內核執行用戶空間的代碼。arm里面叫 PXN(Privilege Execute Never) 和PAN(Privileged Access Never)。
SMEP類似於用戶態下的NX,不過一個是在內核態中,一個是在用戶態中。和NX一樣SMAP/SMEP需要處理器支持,可以通過cat /proc/cpuinfo查看,在內核命令行中添加nosmap和nosmep禁用。

Stack Protector

Stack Protector,當然在內核中也是有這種防護的,編譯內核時設置CONFIG_CC_STACKPROTECTOR 選項即可,該補丁是Tejun Heo在09年給主線kernel提交的。

address protection

address protection,由於內核空間和用戶空間共享虛擬內存地址,因此需要防止用戶空間mmap的內存從0開始,從而緩解NULL解引用攻擊。windows系統從win8開始禁止在零頁分配內存。

pwn題相關

不同於用戶態的pwn,Kernel-Pwn不再是用python遠程鏈接打payload拿shell,而是給你一個環境包,下載后qemu本地起系統。對於一個Kernel-Pwn來說,題目通常會給定以下文件:

  • boot.sh: 一個用於啟動 kernel 的 shell 的腳本,多用 qemu,保護措施與 qemu 不同的啟動參數有關
  • bzImage: kernel binary
  • rootfs.cpio: 文件系統映像

本地寫好 exploit 后,可以通過 base64 編碼等方式把編譯好的二進制文件保存到遠程目錄下,進而拿到 flag。同時可以使用 musl, uclibc 等方法減小 exploit 的體積方便傳輸。

參考

https://ctf-wiki.github.io/ctf-wiki/pwn/linux/kernel/basic_knowledge-zh/#_1
https://zhuanlan.zhihu.com/p/140338884


免責聲明!

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



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