1. 程式人生 > >(轉載)/dev/mem可沒那麽簡單

(轉載)/dev/mem可沒那麽簡單

add 助理 ops vat 了解 sign bar 映射 redirect

remap_pfn_range()校驗漏洞的利用過程中,熟悉Linux內核地址空間布局非常重要,這篇文章幫助理解這個問題。

參考CVE-2013-2506的PoC:https://github.com/hiikezoe/libfb_mem_exploit

參考材料:

  1. http://unix.stackexchange.com/questions/5124/what-does-the-virtual-kernel-memory-layout-in-dmesg-imply?noredirect=1&lq=1

  2. http://unix.stackexchange.com/questions/4929/what-are-high-memory-and-low-memory-on-linux?rq=1

  3. http://duartes.org/gustavo/blog/post/anatomy-of-a-program-in-memory/

  4. http://unix.stackexchange.com/questions/218507/kernel-address-space-layout

  5. http://www.cnblogs.com/bizhu/archive/2012/10/09/2717303.html

PoC中的kernel_phys_address是由讀取/proc/iomem設備得到,相關資料如下:

  1. http://superuser.com/questions/480451/what-kind-of-memory-addresses-are-the-ones-shown-by-proc-ioports-and-proc-iomem

  2. http://stackoverflow.com/questions/14632771/is-the-system-ram-displayed-by-proc-iomem-is-like-unallocated-memory?rq=1

/proc/iomem lists ranges of physical memory addresses.


以下轉載原文

這幾天研究了下/dev/mem,發現功能很神奇,通過mmap可以將物理地址映射到用戶空間的虛擬地址上,在用戶空間完成對設備寄存器的操作,於是上網搜了一些/dev/mem的資料。網上的說法也很統一,/dev/mem是物理內存的全映像,可以用來訪問物理內存,一般用法是open(“/dev/mem”,O_RDWR|O_SYNC),接著就可以用mmap來訪問物理內存以及外設的IO資源,這就是實現用戶空間驅動的一種方法。

用戶空間驅動聽起來很酷,但是對於/dev/mem,我覺得沒那麽簡單,有2個地方引起我的懷疑:

(1)網上資料都說/dev/mem是物理內存的全鏡像,這個概念很含糊,/dev/mem到底可以完成哪些地址的虛實映射?

(2)/dev/mem看似很強大,但是這也太危險了,黑客完全可以利用/dev/mem對kernel代碼以及IO進行一系列的非法操作,後果不可預測,難道內核開發者們沒有意識到這點嗎?

網上資料說法都很泛泛,只對mem設備的使用進行說明,沒有對這些問題進行深究。要搞清這一點,我覺得還是從/dev/mem驅動開始下手。

參考內核版本:3.4.55

參考平臺:powerpc/arm

mem驅動在drivers/char/mem.c,mmap是系統調用,產生軟中斷進入內核後調用sys_mmap,最終會調用到mem驅動的mmap實現函數。

來看下mem.c中的mmap實現:

static int mmap_mem(struct file *file, struct vm_area_struct *vma)  
{  
    size_t size = vma->vm_end - vma->vm_start;  
  
    if (!valid_mmap_phys_addr_range(vma->vm_pgoff, size))  
        return -EINVAL;  
  
    if (!private_mapping_ok(vma))  
        return -ENOSYS;  
  
    if (!range_is_allowed(vma->vm_pgoff, size))  
        return -EPERM;  
  
    if (!phys_mem_access_prot_allowed(file, vma->vm_pgoff, size,  
                        &vma->vm_page_prot))  
        return -EINVAL;  
  
    vma->vm_page_prot = phys_mem_access_prot(file, vma->vm_pgoff,  
                         size,  
                         vma->vm_page_prot);  
  
    vma->vm_ops = &mmap_mem_ops;  
  
    /* Remap-pfn-range will mark the range VM_IO and VM_RESERVED */  
    if (remap_pfn_range(vma,  
                vma->vm_start,  
                vma->vm_pgoff,  
                size,  
                vma->vm_page_prot)) {  
        return -EAGAIN;  
    }  
    return 0;  
}  

vma是內核內存管理很重要的一個結構體,
其結構成員中start end代表要映射到的用戶空間虛擬地址範圍,用戶空間的動態映射是以PAGE_SIZE也就是4K為一頁,
vma_pgoff是要映射的物理地址,vma_page_prot代表該頁的權限。

這些成員的賦值是在調用具體驅動的mmap實現函數之前,在sys_mmap中進行的。

在mmap_mem最後調用remap_pfn_range,該函數完成指定物理地址與用戶空間虛擬地址頁表的建立。

remap_pfn_range參數中vma->vm_pgoff即代表要映射的物理地址,並沒有範圍限制僅能夠操作內存。

mmap系統調用的函數定義如下:


void *mmap(void *addr, size_t length, int prot, int flags, int fd, off_t offset);


addr指定要映射到的虛擬地址,寫NULL則有sys_mmap來分配該虛擬地址。

mmap參數與mem_mmap參數對應關系如下:


prot ===> vma->vma_page_prot offset ===> vma->vma_pgoff length ===> size


從剛才分析的mem_mmap流程來看,可以得出一個簡單的結論:

mem_mmap可以映射整個處理器的地址空間,而不單單是內存。這裏要說明的是,地址空間不等於內存空間。站在處理器角度看,地址空間指處理器總線上的所有可尋址空間,除了內存,還有外設的IO空間,以及其他總線映射過來的mem(如PCI)

我的理解,mem_mmap完全可以映射0-0xffffffff的所有物理地址(填TLB頁表完成映射),但前提是保證該物理地址是真實有效的,也就是處理器訪問該總線物理地址可以獲取有效數據。

所以現在看來mmap /dev/mem,只要確定我們處理器的地址空間分布,就可以將我們需要的地址映射到用戶空間進行操作。

如果地址不是一個有效物理地址(處理器地址空間分布中該地址沒用),mmap建立該物理地址與用戶空間虛擬地址的映射,填TLB,CPU經過TLB翻譯後去訪問該不存在的物理地址訪問就有可能導致CPU掛掉。

這也就解釋了我第一個疑問,但是kernel的安全機制不會允許用戶這麽肆無忌憚的操作。接著來看remap_pfn_range之前mmap_mem如何進行防護。

首先是valid_mmap_phys_addr_range,檢查該物理地址是否是一個有效的mmap地址,如果平臺定義了ARCH_HAS_VALID_PHYS_ADDR_RANGE則會實現該函數,
arm中定義並實現了該函數,在arch/arm/mm/mmap.c中,如下:

/* 
 * We don‘t use supersection mappings for mmap() on /dev/mem, which 
 * means that we can‘t map the memory area above the 4G barrier into 
 * userspace. 
 */  
int valid_mmap_phys_addr_range(unsigned long pfn, size_t size)  
{  
    return !(pfn + (size >> PAGE_SHIFT) > 0x00100000);  
}  

該函數確定mmap的範圍是否超過4G,超過4G則為無效物理地址,這種情況用戶空間一般不會出現。
而對於powerpc,平臺沒有定義ARCH_HAS_VALID_PHYS_ADDR_RANGE,所以valid_mmap_phys_addr_range在mem.c中定義為空函數,返回1 表示該物理地址一直有效。

物理地址有效,不會返回-EINVAL,繼續往下走。

接下來是private_mapping_ok,對於有MMU的CPU,實現如下:


static inline int private_mapping_ok(struct vm_area_struct *vma) { return 1; }


MMU的權限管理可以支持私有映射,所以該函數一直成功。

接下來是一個最為關鍵的檢查函數range_is_allowed,定義如下:


#ifdef CONFIG_STRICT_DEVMEM static inline int range_is_allowed(unsigned long pfn, unsigned long size) { u64 from = ((u64)pfn) << PAGE_SHIFT; u64 to = from + size; u64 cursor = from; while (cursor < to) { if (!devmem_is_allowed(pfn)) { printk(KERN_INFO "Program %s tried to access /dev/mem between %Lx->%Lx.\n", current->comm, from, to); return 0; } cursor += PAGE_SIZE; pfn++; } return 1; } #else static inline int range_is_allowed(unsigned long pfn, unsigned long size) { return 1; } #endif


可以看出如果不打開CONFIG_STRICT_DEVMEM,range_is_allowed是返回1,表示該物理地址範圍是被允許的。查看kconfig文件(在相應平臺目錄下,如arch/arm/Kconfig.debug中)找到CONFIG_STRICT_DEVMEM說明如下

config STRICT_DEVMEM  
    def_bool y  
    prompt "Filter access to /dev/mem"  
    help  
      This option restricts access to /dev/mem.  If this option is  
      disabled, you allow userspace access to all memory, including  
      kernel and userspace memory. Accidental memory access is likely  
      to be disastrous.  
      Memory access is required for experts who want to debug the kernel.  
  
      If you are unsure, say Y.  

該選項menuconfig時在kernel hacking目錄下。

根據說明可以理解,CONFIG_STRICT_DEVMEM是嚴格的對/dev/mem訪問檢查,如果關掉該選項,用戶就可以通過mem設備訪問所有地址空間(根據對我提出的第一個問題理解,這裏memory應該理解為地址空間)。該選項對於調試內核有幫助。

如果打開該選項,內核就會對mem設備訪問加以檢查,檢查函數就是range_is_allowed。

range_is_allowed函數對要檢查的物理地址範圍以4K頁為單位,一頁一頁的調用devmem_is_allowed,如果不允許,則會進行打印提示,並返回0,表示該物理地址範圍不被允許。

來看devmem_is_allowed.該函數是平臺相關函數,不過arm跟powerpc的實現相差不大,以arm的實現為例。在arch/arm/mm/mmap.c中。


/* * devmem_is_allowed() checks to see if /dev/mem access to a certain * address is valid. The argument is a physical page number. * We mimic x86 here by disallowing access to system RAM as well as * device-exclusive MMIO regions. This effectively disable read()/write() * on /dev/mem. */ int devmem_is_allowed(unsigned long pfn) { if (iomem_is_exclusive(pfn << PAGE_SHIFT)) return 0; if (!page_is_ram(pfn)) return 1; return 0; }


首先iomem_is_exclusive檢查該物理地址是否被獨占保留,實現如下:


#ifdef CONFIG_STRICT_DEVMEM static int strict_iomem_checks = 1; #else static int strict_iomem_checks; #endif /* * check if an address is reserved in the iomem resource tree * returns 1 if reserved, 0 if not reserved. */ int iomem_is_exclusive(u64 addr) { struct resource *p = &iomem_resource; int err = 0; loff_t l; int size = PAGE_SIZE; if (!strict_iomem_checks) return 0; addr = addr & PAGE_MASK; read_lock(&resource_lock); for (p = p->child; p ; p = r_next(NULL, p, &l)) { /* * We can probably skip the resources without * IORESOURCE_IO attribute? */ if (p->start >= addr + size) break; if (p->end < addr) continue; if (p->flags & IORESOURCE_BUSY && p->flags & IORESOURCE_EXCLUSIVE) { err = 1; break; } } read_unlock(&resource_lock); return err; }


如果打開了CONFIG_STRICT_DEVMEM,iomem_is_exclusive遍歷iomem_resource鏈表,查看要檢查的物理地址所在resource的flags,如果是bug或者exclusive,則返回1,表明該物理地址是獨占保留的。

據我了解,iomem_resource是來表征內核iomem資源的鏈表。

對於外設的IO資源,kernel中使用platform device機制來註冊平臺設備(platform_device_register)時調用insert_resource將該設備相應的io資源插入到iomem_resource鏈表中。

如果我要對某外設的IO資源進行保護,防止用戶空間訪問,可以將其resource的flags置位exclusive即可。
不過我查看我平臺支持包裏的所有platform device的resource,flags都沒有置位exclusive或者busy。如果我映射的物理地址範圍是外設的IO,檢查可以通過。

對於內存的mem資源,如何註冊到iomem_resource鏈表中,內核代碼中我還沒找到具體的位置,不過iomem在proc下有相應的表征文件,可以cat /proc/iomem。

根據我的實際操作測試,內存資源也都沒有exclusive,所以如果我映射地址是內存,檢查也可以通過。

所以這裏iomem_is_exclusive檢查一般是通過的,接下來看page_is_ram,看devmem_is_range的邏輯,如果地址是ram地址,則該地址不被允許。page_is_ram也是平臺函數,查看powerpc的實現如下。

int page_is_ram(unsigned long pfn)  
{  
#ifndef CONFIG_PPC64    /* XXX for now */  
    return pfn < max_pfn;  
#else  
    unsigned long paddr = (pfn << PAGE_SHIFT);  
    struct memblock_region *reg;  
  
    for_each_memblock(memory, reg)  
        if (paddr >= reg->base && paddr < (reg->base + reg->size))  
            return 1;  
    return 0;  
#endif  
}  
max_pfn賦值在在do_init_bootmem中,如下.  
void __init do_init_bootmem(void)  
{  
    unsigned long start, bootmap_pages;  
    unsigned long total_pages;  
    struct memblock_region *reg;  
    int boot_mapsize;  
  
    max_low_pfn = max_pfn = memblock_end_of_DRAM() >> PAGE_SHIFT;  
    total_pages = (memblock_end_of_DRAM() - memstart_addr) >> PAGE_SHIFT;  

max_pfn代表了內核lowmem的頁個數,lowmem在內核下靜態線性映射,系統啟動之初完成映射之後不會改動,讀寫效率高,內核代碼都是跑在lowmem。

lowmem大小我們可以通過cmdline的“mem=”來指定。

這裏就明白了如果要映射的物理地址在lowmem範圍內,也是不允許被映射的。

這樣range_is_allowed就分析完了,exclusive的iomem以及lowmem範圍內的物理地址是不允許被映射的。

接下來phys_mem_access_prot_allowed實現為空返回1,沒有影響。

phys_mem_access_prot確定我們映射頁的權限,該函數也是平臺函數,以powerpc實現為例,如下:


pgprot_t phys_mem_access_prot(struct file *file, unsigned long pfn, unsigned long size, pgprot_t vma_prot) { if (ppc_md.phys_mem_access_prot) return ppc_md.phys_mem_access_prot(file, pfn, size, vma_prot); if (!page_is_ram(pfn)) vma_prot = pgprot_noncached(vma_prot); return vma_prot; }


如果有平臺實現的phys_mem_access_prot,則調用之。如果沒有,對於不是lowmem範圍內的物理地址,權限設置為uncached。

以上的檢查完畢,最後調用remap_pfn_range完成頁表設置。

所以如果打開CONFIG_STRICT_DEVMEM,mem驅動會對mmap要映射的物理地址進行範圍和位置的檢查然後才進行映射,檢查條件如下:

(1)映射範圍不能超過4G。

(2)該物理地址所在iomem不能exclusive.

(3)該物理地址不能處在lowmem中。

所以說對於網上給出的各種利用/dev/mem來操作內存以及寄存器的文章,如果操作範圍在上述3個條件內,內核必須關閉CONFIG_STRICT_DEVMEM才行。

這樣對於mem設備我的2個疑問算是解決了。查看mem.c時我還看到了另外一個有趣的設備kmem,這個設備mmap的是哪裏的地址,網上的說法是內核虛擬地址,這個說法我不以為然,這裏記錄下我的想法。

如果內核打開CONFIG_KMEM,則會創建kmem設備,它與mem設備主要差別在mmap的實現上,kmem的mmap實現如下:


#ifdef CONFIG_DEVKMEM static int mmap_kmem(struct file *file, struct vm_area_struct *vma) { unsigned long pfn; /* Turn a kernel-virtual address into a physical page frame */ pfn = __pa((u64)vma->vm_pgoff << PAGE_SHIFT) >> PAGE_SHIFT; /* * RED-PEN: on some architectures there is more mapped memory than * available in mem_map which pfn_valid checks for. Perhaps should add a * new macro here. * * RED-PEN: vmalloc is not supported right now. */ if (!pfn_valid(pfn)) return -EIO; vma->vm_pgoff = pfn; return mmap_mem(file, vma); } #endif


引起我註意的是__pa,完成內核虛擬地址到物理地址的轉換,最後調用mmap_mem,簡單一看kmem的確是映射的內核虛擬地址。

但是搞清楚__pa的實現,我就不這麽認為了。以powerpc為例,在arch/powerpc/include/asm/page.h,定義如下:

#define __va(x) ((void *)(unsigned long)((phys_addr_t)(x) + VIRT_PHYS_OFFSET))  
#define __pa(x) ((unsigned long)(x) - VIRT_PHYS_OFFSET)  
....  
#define VIRT_PHYS_OFFSET (KERNELBASE - PHYSICAL_START)  

內核中定義了4個變量來表示內核一些基本的物理地址和虛擬地址,如下:

KERNELBASE     內核的起始虛擬地址,我的是0xc0000000
PAGE_OFFSET    低端內存的起始虛擬地址,一般是0xc0000000
PHYSICAL_START 內核的起始物理地址,我的是0x80000000
MEMORY_START   低端內存的起始物理地址,我的是0x80000000

內核在啟動過程中對於lowmem的靜態映射,就是以上述的物理地址和虛擬地址的差值進行線性映射的。
所以__pa __va轉換的是線性映射的內存部分,也就是lowmem。

所以kmem映射的是lowmem,如果我的cmdline參數中mem=512M,這就意味著通過kmem的mmap我最多可以訪問內核地址空間開始的512M內存。

對於超過lowmem範圍,訪問highmem,如果使用__pa訪問,由於highmem是動態映射的,其映射關系不是線性的那麽簡單了,根據__pa獲取的物理地址與我們想要的內核虛擬地址是不對應的。

(轉載)/dev/mem可沒那麽簡單