核心驅動mmap Handler利用技術(一)

Editor發表於2018-01-16

核心驅動mmap Handler利用技術(一)

1. 核心驅動簡介


在實現Linux核心驅動中,開發者可以註冊一個裝置驅動檔案,該檔案常常在/dev/目錄下完成註冊。該檔案可以支援所有的常規檔案方法,比如opening,reading, writing, mmaping,closing等等。裝置驅動檔案支援的操作由包含了一組函式指標的結構體file_operations描述,每個指標描述一個操作。在4.9版本核心中可以找到如下的定義。


struct file_operations {

struct module *owner;

loff_t(*llseek) (struct file *, loff_t, int);

ssize_t(*read) (struct file *, char __user *, size_t, loff_t *);

ssize_t(*write) (struct file *, const char __user *, size_t,    loff_t *);

ssize_t(*read_iter) (struct kiocb *, struct iov_iter *);

ssize_t(*write_iter) (struct kiocb *, struct iov_iter *);

int(*iterate) (struct file *, struct dir_context *);

int(*iterate_shared) (struct file *, struct dir_context *);

unsigned int(*poll) (struct file *, struct poll_table_struct *);

long(*unlocked_ioctl) (struct file *, unsigned int, unsigned long);

long(*compat_ioctl) (struct file *, unsigned int, unsigned long);

int(*mmap) (struct file *, struct vm_area_struct *);

int(*open) (struct inode *, struct file *);

int(*flush) (struct file *, fl_owner_t id);

int(*release) (struct inode *, struct file *);

int(*fsync) (struct file *, loff_t, loff_t, int datasync);

int(*fasync) (int, struct file *, int);

int(*lock) (struct file *, int, struct file_lock *);

ssize_t(*sendpage) (struct file *, struct page *, int, size_t, loff_t *, int);

unsigned long(*get_unmapped_area)(struct file *, unsigned long, unsigned long, unsigned long, unsigned long);

int(*check_flags)(int); int(*flock) (struct file *, int, struct file_lock *);

ssize_t(*splice_write)(struct pipe_inode_info *, struct file *, loff_t *, size_t, unsigned int);

ssize_t(*splice_read)(struct file *, loff_t *, struct pipe_inode_info *, size_t, unsigned int);

int(*setlease)(struct file *, long, struct file_lock **, void **);

long(*fallocate)(struct file *file, int mode, loff_t offset,loff_t len);

void(*show_fdinfo)(struct seq_file *m, struct file *f);

#ifndef CONFIG_MMU

unsigned(*mmap_capabilities)(struct file *);

#endif

ssize_t(*copy_file_range)(struct file *, loff_t, struct file *, loff_t, size_t, unsigned int);

int(*clone_file_range)(struct file *, loff_t, struct file *, loff_t,u64);

ssize_t(*dedupe_file_range)(struct file *, u64, u64, struct file *, u64);

};


如同上面展示,可以實現非常多的檔案操作,本文的主角是mmap handler的實現。


file_operations結構體的安裝示例以及相關聯的函式可以在下面看到


('/fs/proc/softirqs.c'):

static int show_softirqs(struct seq_file *p, void *v)

{

int i, j;

seq_puts(p, " ");

for_each_possible_cpu(i)

  seq_printf(p, "CPU%-8d", i);

seq_putc(p, '\n');

for (i = 0; i < NR_SOFTIRQS; i++)

{

  seq_printf(p, "%12s:", softirq_to_name[i]);

  for_each_possible_cpu(j)

    seq_printf(p, " %10u", kstat_softirqs_cpu(i, j));

  seq_putc(p, '\n');

}

return 0;

}

static int softirqs_open(struct inode *inode, struct file *file)

{

return single_open(file, show_softirqs, NULL);

}

static const struct file_operations proc_softirqs_operations = {

.open = softirqs_open,

.read = seq_read,

.llseek = seq_lseek,

.release = single_release,

};

static int __init proc_softirqs_init(void)

{

proc_create("softirqs", 0, NULL, &proc_softirqs_operations);

return 0;

}


上述程式碼可以在'proc_softirqs_operations'結構體中看到,它允許呼叫open,read,llseek和close函式。當一個應用程式試圖去開啟一個'softirqs'檔案時就會呼叫'open'系統呼叫,進而會呼叫到指向的'softirqs_open'函式。


2. 核心mmap Handler

2.1 簡單的mmap Handler


如上文提及,核心驅動可以實現自己的mmap handler。主要目的在於mmap handler可以加速使用者空間程式和核心空間的資料交換。核心可以共享一塊核心buffer或者直接共享某些實體記憶體地址範圍給使用者空間。使用者空間程式可以直接修改這塊記憶體而無需呼叫額外的系統呼叫。


一個簡單(並且不安全)的mmap handler實現例子如下:


static struct file_operations fops =

{

.open = dev_open,

.mmap = simple_mmap,

.release = dev_release,

};

int size = 0x10000;

static int dev_open(struct inode *inodep, struct file *filep)

{

printk(KERN_INFO "MWR: Device has been opened\n");

filep->private_data = kzalloc(size, GFP_KERNEL);

if (filep->private_data == NULL)

  return -1;

return 0;

}

static int simple_mmap(struct file *filp, struct vm_area_struct *vma)

{

printk(KERN_INFO "MWR: Device mmap\n");

if (

remap_pfn_range( vma, vma->vm_start,

virt_to_pfn(filp->private_data), vma->vm_end - vma->vm_start,

vma->vm_page_prot ) )

{

  printk(KERN_INFO "MWR: Device mmap failed\n");

  return -EAGAIN;

}

printk(KERN_INFO "MWR: Device mmap OK\n");

return 0;

}


當開啟上面的驅動時,dev_open會被呼叫,它簡單的分配0x10000位元組的buffer並且將其儲存在private_data指標域。此後如果程式在對該檔案描述符呼叫mmap時,就會呼叫到simple_mmap。


該函式簡單的呼叫 remap_pfn_range 函式來建立一個程式地址空間的新對映,將private_data指向的buffer和vma->vm_start開始的尺寸為vma->vm_end-vma->vm_start 大小的地址空間關聯起來。


一個請求對應檔案mmap的使用者空間程式樣例:


int main(int argc, char * const * argv)

{

int fd = open("/dev/MWR_DEVICE", O_RDWR);

if (fd < 0)

{

  printf("[-] Open failed!\n");

  return -1;

}

unsigned long * addr = (unsigned long *)mmap((void*)0x42424000, 0x1000, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0x1000);

if (addr == MAP_FAILED)

{

  perror("Failed to mmap: ");

  close(fd);

  return -1;

}

printf("mmap OK addr: %lx\n", addr);

close(fd);

return 0;

}


上面的程式碼對/dev/MWR_DEVICE驅動檔案呼叫了mmap,大小為0x1000,檔案偏移設定為0x1000,目標地址設定為0x42424000。可以看到一個成功的對映結果:


# cat /proc/23058/maps

42424000-42425000 rw-s 00001000 00:06 68639        /dev/MWR_DEVICE


2.2 空的mmap Handler


到目前為止,我們已經見過了最簡單的mmap操作的實現體,但是如果mmap handler是個空函式的話,會發生什麼?


讓我們考慮這個實現:


static struct file_operations fops =

{

.open = dev_open,

.mmap = empty_mmap,

.release = dev_release,

};

static int empty_mmap(struct file *filp, struct vm_area_struct *vma)

{

printk(KERN_INFO "MWR: empty_mmap\n");

return 0;

}


如我們所見,函式中只有log資訊,這是為了讓我們觀察到函式被呼叫了。


當empty_mmap被呼叫時,我們毫不誇張的可以猜測到什麼都不會發生,mmap會引發失敗,畢竟此時並沒有remap_pfn_range或其他類似的函式。


然而,事實並非如此。讓我們執行一下使用者空間程式碼,看看究竟會發生什麼:


int fd = open("/dev/MWR_DEVICE", O_RDWR);

unsigned long size = 0x1000;

unsigned long *addr = (unsigned long *)mmap((void*)0x42424000, size, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0x1000);

在dmesg log中我們可以看到空的handler被成功呼叫:

[ 1119.393560] MWR: Device has been opened 1 time(s)

[ 1119.393574] MWR: empty_mmap


看看記憶體對映,有沒有什麼異常:


# cat /proc/2386/maps

42424000-42426000 rw-s 00001000 00:06 22305


我們並沒有呼叫remap_pfn_range函式,然而對映卻如同此前情景那樣被建立了。唯一的不同在於對映是無效的,因為我們實際上並沒有對映任何的實體記憶體給這塊虛擬地址。這樣的一個mmap實現中,一旦訪問了對映的地址空間,要麼引起程式崩潰,要麼引起整個核心的崩潰,這取決於具體使用的核心。


讓我們試試訪問這塊記憶體:


int fd = open("/dev/MWR_DEVICE", O_RDWR);

unsigned long size = 0x1000;

unsigned long * addr = (unsigned long *)mmap((void*)0x42424000,

size, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0x1000); printf("addr[0]:

%x\n", addr[0]);


如我們所願,程式崩潰了:


./mwr_client

Bus error


然而在某些3.10 arm/arm64 Android核心中,類似的程式碼會引起kernel panic。


所以說,作為一個開發者,你不應該假定一個空的handler可以按預期表現,在核心中始終使用一個可用的返回碼來控制給定的情形。


一個帶有vm_operations_struct的mmap Handler


在mmap操作中,有辦法在已分配記憶體區間上使用vm_operations_struct結構體來指派多種其他操作的handler(例如控制unmapped memory, page permission changes等)。


vm_operations_struct在kernel 4.9中的定義如下:


struct vm_operations_struct {

void(*open)(struct vm_area_struct * area);

void(*close)(struct vm_area_struct * area);

int(*mremap)(struct vm_area_struct * area);

int(*fault)(struct vm_area_struct *vma, struct vm_fault *vmf);

int(*pmd_fault)(struct vm_area_struct *, unsigned long address, pmd_t *, unsigned int flags);

void(*map_pages)(struct fault_env *fe, pgoff_t start_pgoff, pgoff_t end_pgoff);

int(*page_mkwrite)(struct vm_area_struct *vma, struct vm_fault *vmf);

int(*pfn_mkwrite)(struct vm_area_struct *vma, struct vm_fault *vmf);

int(*access)(struct vm_area_struct *vma, unsigned long addr, void *buf, int len, int write);

const char *(*name)(struct vm_area_struct *vma);

#ifdef CONFIG_NUMA

int(*set_policy)(struct vm_area_struct *vma, struct mempolicy *new);

struct mempolicy *(*get_policy)(struct vm_area_struct *vma, unsigned long addr);

#endif

struct page *(*find_special_page)(struct vm_area_struct *vma, unsigned long addr);

};


如上文描述,這些函式指標可以用於實現特定的handler。關於此的詳細描述在《Linux Device Drivers(Linux裝置驅動)》一書中可以找到。

在實現記憶體分配器時,一個通俗可見的主流的行為是開發者實現了一個'fault' handler。例如,看看這一段:


static struct file_operations fops = {

.open = dev_open,

.mmap = simple_vma_ops_mmap,

.release = dev_release,

};

static struct vm_operations_struct simple_remap_vm_ops = {

.open = simple_vma_open,

.close = simple_vma_close,

.fault = simple_vma_fault,

};

static int simple_vma_ops_mmap(struct file *filp, struct vm_area_struct *vma)

{

printk(KERN_INFO "MWR: Device simple_vma_ops_mmap\n");

vma->vm_private_data = filp->private_data;

vma->vm_ops = &simple_remap_vm_ops;

simple_vma_open(vma);

printk(KERN_INFO "MWR: Device mmap OK\n");

return 0;

}

void simple_vma_open(struct vm_area_struct *vma)

{

printk(KERN_NOTICE "MWR: Simple VMA open, virt %lx, phys %lx\n", vma->vm_start, vma->vm_pgoff << PAGE_SHIFT);

}

void simple_vma_close(struct vm_area_struct *vma)

{

printk(KERN_NOTICE "MWR: Simple VMA close.\n");

}

int simple_vma_fault(struct vm_area_struct *vma, struct vm_fault *vmf)

{

struct page *page = NULL;

unsigned long offset;

printk(KERN_NOTICE "MWR: simple_vma_fault\n");

offset = (((unsigned long)vmf->virtual_address - vma->vm_start) + (vma->vm_pgoff << PAGE_SHIFT));

if (offset > PAGE_SIZE << 4)

  goto nopage_out;

page = virt_to_page(vma->vm_private_data + offset);

vmf->page = page;

get_page(page);

nopage_out:

return 0;

}


上述程式碼中我們可以看到simple_vma_ops_mmap函式用於控制mmap呼叫。它什麼都沒做,除了指派了simple_remap_vm_ops結構體作為虛擬記憶體操作的handler。


讓我們看看下列程式碼在該driver上執行的效果:


int fd = open("/dev/MWR_DEVICE", O_RDWR);

unsigned long size = 0x1000;

unsigned long * addr = (unsigned long *)mmap((void*)0x42424000, size, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0x1000);


dmesg的結果:


[268819.067085] MWR: Device has been opened 2 time(s)

[268819.067121] MWR: Device simple_vma_ops_mmap

[268819.067123] MWR: Simple VMA open, virt 42424000, phys 1000

[268819.067125] MWR: Device mmap OK


程式地址空間的對映:


42424000-42425000 rw-s 00001000 00:06 140215        /dev/MWR_DEVICE


如我們所見,simple_vma_ops_mmap函式被呼叫了,記憶體對映也建立了。例子中simple_vma_fault函式沒有被呼叫。


問題在於,我們有了個地址範圍為0x42424000-0x42425000的地址空間卻不清楚它指向何處。我們沒有為它關聯實體記憶體,因此當程式試圖訪問這段地址的任一部分時,simple_vma_fault都會執行。


所以讓我們看看這段使用者空間程式碼:


int fd = open("/dev/MWR_DEVICE", O_RDWR);

unsigned long size = 0x2000;

unsigned long * addr = (unsigned long *)mmap((void*)0x42424000, size, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0x1000);

printf("addr[0]: %x\n", addr[0]);


程式碼唯一的改變在於使用了printf函式來訪問對映記憶體。因為記憶體區域無效所以我們的simple_vma_fault例程被呼叫了。dmesg的輸出可以看到:


[285305.468520] MWR: Device has been opened 3 time(s)

[285305.468537] MWR: Device simple_vma_ops_mmap

[285305.468538] MWR: Simple VMA open, virt 42424000, phys 1000

[285305.468539] MWR: Device mmap OK

[285305.468546] MWR: simple_vma_fault


在simple_vma_fault函式中,我們可以觀察到offset變數使用了指向一個沒有被對映的地址的vmf->virtual_address進行了計算。我們這裡就是addr[0]的地址。

下一個page結構體由virt_to_page巨集得到,該巨集將新獲取的page賦值給vmf->page變數。


這一賦值意味著當fault handler返回時,addr[0]會指向由simple_vma_fault計算出來的某個實體地址。該記憶體可以被使用者程式所訪問而無需其他任何程式碼。

如果程式試圖訪問addr[513](假定sizeof(unsigned long)為8),fault handler會被再次呼叫,這是由於addr[0]和addr[513]在兩個不同的記憶體頁上,而此前僅有一個記憶體頁被對映過。


這就是原始碼:


int fd = open("/dev/MWR_DEVICE", O_RDWR);

unsigned long size = 0x2000;

unsigned long * addr = (unsigned long *)mmap((void*)0x42424000, size, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0x1000);

printf("addr[0]: %x\n", addr[0]);

printf("addr[513]: %x\n", addr[513]);


生成核心log:


[286873.855849] MWR: Device has been opened 4 time(s)

[286873.855976] MWR: Device simple_vma_ops_mmap

[286873.855979] MWR: Simple VMA open, virt 42424000, phys 1000

[286873.855980] MWR: Device mmap OK

[286873.856046] MWR: simple_vma_fault

[286873.856110] MWR: simple_vma_fault


3. 經典mmap Handler議題

3.1 使用者輸入有效性的不足


讓我們看看前面的mmap handler例子:


static int simple_mmap(struct file *filp, struct vm_area_struct *vma)

{

printk(KERN_INFO "MWR: Device mmap\n");

if (

remap_pfn_range( vma, vma->vm_start,

virt_to_pfn(filp->private_data), vma->vm_end - vma->vm_start,

vma->vm_page_prot ) )

{

  printk(KERN_INFO "MWR: Device mmap failed\n");

  return -EAGAIN;

}

printk(KERN_INFO "MWR: Device mmap OK\n");

return 0;

}


前面展示的程式碼展示了一個通用的實現mmap handler的途徑,相似的程式碼可以在《Linux裝置驅動》一書中找到。示例程式碼主要的議論點在於vma->vm_end和vma->vm_start的值從未檢查有效性。取而代之的,它們被直接傳遞給remap_pfn_range作為尺寸引數。


這意味著一個惡意程式可以用一個不受限的尺寸來呼叫mmap。在我們這裡,允許一個使用者空間程式去mmap所有的在filp->private_databuffer之後的實體記憶體地址空間。這包括所有的核心記憶體。這意味著惡意程式能夠從使用者空間讀寫整個核心記憶體。


另一個流行的用法如下:


static int simple_mmap(struct file *filp, struct vm_area_struct *vma)

{

printk(KERN_INFO "MWR: Device mmap\n");

if ( remap_pfn_range( vma, vma->vm_start, vma->vm_pgoff, vma->vm_end - vma->vm_start, vma->vm_page_prot ) )

{

  printk(KERN_INFO "MWR: Device mmap failed\n");

  return -EAGAIN;

}

printk(KERN_INFO "MWR: Device mmap OK\n");

return 0;

}


上面的程式碼中我們可以看到使用者控制的offset vma->vm_pgoff被直接傳遞給了remap_pfn_range函式作為實體地址。這會使得惡意程式有能力傳遞一個任意實體地址給mmap,也就在使用者空間擁有了整個核心記憶體的訪問許可權。在一些對示例進行微小改動的情景中經常可以看到,要麼offset有了掩碼,要麼使用了另外一個值來計算。


3.2 整數溢位


經常可以看到開發者試圖使用複雜的計算、按位掩碼、位移、尺寸和偏移和等方法去驗證對映的尺寸和偏移(size and offset)。


不幸的是,這常常導致了建立的複雜性以及不尋常的計算和驗證過程晦澀難懂。


在對size和offset值進行少量fuzzing後,找到可以繞過有效性檢查的值並非不可能。


讓我們看看這段程式碼:


static int integer_overflow_mmap(struct file *filp, struct vm_area_struct *vma)

{

unsigned int vma_size = vma->vm_end - vma->vm_start;

unsigned int offset = vma->vm_pgoff << PAGE_SHIFT;

printk(KERN_INFO "MWR: Device integer_overflow_mmap( vma_size: %x, offset: %x)\n", vma_size, offset);

if (vma_size + offset > 0x10000)

{

  printk(KERN_INFO "MWR: mmap failed, requested too large a chunk of memory\n");

  return -EAGAIN;

}

if (remap_pfn_range(vma, vma->vm_start, virt_to_pfn(filp->private_data), vma_size, vma->vm_page_prot))

{

  printk(KERN_INFO "MWR: Device integer_overflow_mmap failed\n");

  return -EAGAIN;

}

printk(KERN_INFO "MWR: Device integer_overflow_mmap OK\n");

return 0;

}


上面的程式碼展示了一個典型的整數溢位漏洞,它發生在一個程式呼叫mmap2系統呼叫時使用了size為0xfffa000以及offset為0xf0006的情況。0xfffa000和0xf0006000的和等於0x100000000。


由於最大的unsigned int值為0xffffffff,最高符號位會被清掉,最終的和會變成0x0。這種情況下,mmap系統呼叫會成功繞過檢查,程式會訪問到預期buffer外的記憶體。


如上文提到的,有兩個獨立的系統呼叫mmap和mmap2。mmap2使得應用程式可以使用一個32位的off_t型別來對映大檔案(最大為2^44位元組),這是通過支援使用一個大數offset引數實現的。


有趣的是mmap2系統呼叫通常在64位核心系統呼叫表中不可用。然而,如果作業系統同時支援32位和64位程式,他就通常在32位程式中可用。這是因為32位和64位程式各使用獨立的系統呼叫表。


3.3 有符號整型型別


另一個老生常談的議題就是size變數的有符號型別。讓我們看看這段程式碼:


static int signed_integer_mmap(

struct file *filp, struct vm_area_struct *vma)

{

int vma_size = vma->vm_end - vma->vm_start;

int offset = vma->vm_pgoff << PAGE_SHIFT;

printk(KERN_INFO "MWR: Device signed_integer_mmap( vma_size: %x, offset: %x)\n", vma_size, offset);

if (vma_size > 0x10000 || offset < 0 || offset > 0x1000 || (vma_size + offset > 0x10000))

{

  printk(KERN_INFO "MWR: mmap failed, requested too large a chunk of memory\n");

  return -EAGAIN;

}

if (remap_pfn_range(vma, vma->vm_start, offset, vma->vm_end - vma->vm_start, vma->vm_page_prot))

{

  printk(KERN_INFO "MWR: Device signed_integer_mmap failed\n");

  return -EAGAIN;

}

printk(KERN_INFO "MWR: Device signed_integer_mmap OK\n");

return 0;

}


上述程式碼中,使用者控制資料儲存在vma_size和offset變數中,它們都是有符號整型。size和offset檢查是這一行:

if (vma_size > 0x10000 || offset < 0 || offset > 0x1000 || (vma_size + offset > 0x10000))


不幸的是,因為vma_size被宣告為有符號整型數,一種攻擊手法是通過使用負數諸如0xf0000000來繞過這個檢查。這回引起0xf0000000位元組被對映到使用者空間地址。



本文由看雪論壇玉涵 編譯,來源exploit-database-papers轉載請註明來自看雪社群

相關文章