使用LiME收集主機實體記憶體的內容時發生宕機

2023-10-20 06:00:25

作者

[email protected]

現象

在一臺ARM64的Centos7虛擬機器器裡載入 https://github.com/504ensicsLabs/LiME 編譯出的核心模組時發生宕機:

insmod lime.ko path=/root/allmem.dump format=raw

上面的目的是把機器實體記憶體的內容全部dump到檔案中,大致的實現過程是,遍歷系統中所有的"System RAM",然後處理每一個物理頁:根據物理頁幀獲取對應的page,然後呼叫kmap_atomic得到虛擬地址,最後將這個虛擬頁的資料讀取出來存放到檔案中。

分析

宕機的呼叫棧如下:

如果對ARM64的頁表屬性很熟悉的話,應該可以看出PTE的bit0是0,說明這是一個無效的PTE,雖然其他的bit看上去很正常。

如果對頁表熟悉不熟的話,當然也可以分析,就是麻煩一些,下面按不熟的方法來。

在原始碼中加偵錯語句,把每次存取的物理也的資訊列印出來:

反覆幾次,發現每次都是這個地址出錯0xffff80009fe80000,對應的實體地址是0xdfe80000。

為了測試這個問題,我單獨寫了一個demo模組,單獨去存取這個地址,發現確實會宕機。
檢視程式碼,發現驅動中使用kmap_atomic獲取page對應的虛擬地址:

看上去直接返回的是這個page對應的64KB實體記憶體在直接對映區的虛擬地址,而且是在開機時就對映好的,沒有道理不能存取呀:

ESR的內容記錄了發生異常的原因,在讀的時候發生了DARA ABORT異常。

檢視這段實體地址空間在crash kernel的範圍內:(/proc/iomem)

難道跟crash kernel有關?暫時放下這個。

那是不是可以把之前可以存取的物理頁的對映資訊也打出來比較一下呢?

那麼如何將某個虛擬地址的頁表對映資訊輸出呢?

核心提供了show_pte這個函數:arch/arm64/mm/fault.c

void show_pte(unsigned long addr)
{
	struct mm_struct *mm;
	pgd_t *pgdp;
	pgd_t pgd;

	if (is_ttbr0_addr(addr)) {
		/* TTBR0 */
		mm = current->active_mm;
		if (mm == &init_mm) {
			pr_alert("[%016lx] user address but active_mm is swapper\n",
				 addr);
			return;
		}
	} else if (is_ttbr1_addr(addr)) {
		/* TTBR1 */
		mm = &init_mm;
	} else {
		pr_alert("[%016lx] address between user and kernel address ranges\n",
			 addr);
		return;
	}

	pr_alert("%s pgtable: %luk pages, %llu-bit VAs, pgdp=%016lx\n",
		 mm == &init_mm ? "swapper" : "user", PAGE_SIZE / SZ_1K,
		 vabits_actual, (unsigned long)virt_to_phys(mm->pgd));
	pgdp = pgd_offset(mm, addr);
	pgd = READ_ONCE(*pgdp);
	pr_alert("[%016lx] pgd=%016llx", addr, pgd_val(pgd));

	do {
		pud_t *pudp, pud;
		pmd_t *pmdp, pmd;
		pte_t *ptep, pte;

		if (pgd_none(pgd) || pgd_bad(pgd))
			break;

		pudp = pud_offset(pgdp, addr);
		pud = READ_ONCE(*pudp);
		pr_cont(", pud=%016llx", pud_val(pud));
		if (pud_none(pud) || pud_bad(pud))
			break;

		pmdp = pmd_offset(pudp, addr);
		pmd = READ_ONCE(*pmdp);
		pr_cont(", pmd=%016llx", pmd_val(pmd));
		if (pmd_none(pmd) || pmd_bad(pmd))
			break;

		ptep = pte_offset_map(pmdp, addr);
		pte = READ_ONCE(*ptep);
		pr_cont(", pte=%016llx", pte_val(pte));
		pte_unmap(ptep);
	} while(0);

	pr_cont("\n");
}

但是函數並沒有呼叫EXPORT_SYMBOL_GPL匯出給模組用,怎麼辦呢?

可以使用核心提供的kallsyms_lookup_name來獲取這個函數的地址:

void (*func)(unsigned long addr);
func = kallsyms_lookup_name("show_pte");
func(addr);

如果核心連kallsyms_lookup_name都沒有匯出怎麼辦?

可以使用kprobe。在呼叫register_kprobe註冊kprobe的時候,會根據設定的函數名稱得到函數地址,然後存放到kprobe->addr中,那麼我們可以先只設定kprobe->symbol_name,當註冊成功可以存取kprobe->addr得到函數的地址。目前在最新的6.5版本的核心裡,register_kprobe也是匯出的。

有了show_pte,那麼可以輸出之前幾個地址的PTE的內容:

對比發現PTE的值排除實體地址佔用的bit外,屬性部分只有bit0的內容不同。

既然kmap_atomic直接返回了物理頁的線性地址,那麼可不可以通過ioremap把這個有問題的實體地址重新對映一下呢? 我測試了一下,不行,在ioremap時會檢查要對映的實體地址是否是合法的系統實體記憶體地址,更明確地說是DDR記憶體,這裡要跟裝置記憶體地址區別開來。如果是系統實體記憶體,那麼直接返回0. 這麼處理也好理解,既然是ioremap,當然應該針對的是io memory,如暫存器地址。下面是ARM64上ioreamp的定義:

#define ioremap(addr, size)		__ioremap((addr), (size), __pgprot(PROT_DEVICE_nGnRE))
#define ioremap_nocache(addr, size)	__ioremap((addr), (size), __pgprot(PROT_DEVICE_nGnRE))
#define ioremap_wc(addr, size)		__ioremap((addr), (size), __pgprot(PROT_NORMAL_NC))
#define ioremap_wt(addr, size)		__ioremap((addr), (size), __pgprot(PROT_DEVICE_nGnRE)

void __iomem *__ioremap(phys_addr_t phys_addr, size_t size, pgprot_t prot)
{
	return __ioremap_caller(phys_addr, size, prot,
				__builtin_return_address(0));
}

static void __iomem *__ioremap_caller(phys_addr_t phys_addr, size_t size,
				      pgprot_t prot, void *caller)
{
	unsigned long last_addr;
	unsigned long offset = phys_addr & ~PAGE_MASK;
	int err;
	unsigned long addr;
	struct vm_struct *area;

	/*
	 * Page align the mapping address and size, taking account of any
	 * offset.
	 */
	phys_addr &= PAGE_MASK;
	size = PAGE_ALIGN(size + offset);

	/*
	 * Don't allow wraparound, zero size or outside PHYS_MASK.
	 */
	last_addr = phys_addr + size - 1;
	if (!size || last_addr < phys_addr || (last_addr & ~PHYS_MASK))
		return NULL;

	/*
	 * Don't allow RAM to be mapped.
	 */
	if (WARN_ON(pfn_valid(__phys_to_pfn(phys_addr))))
		return NULL;

	area = get_vm_area_caller(size, VM_IOREMAP, caller);
	if (!area)
		return NULL;
	addr = (unsigned long)area->addr;
	area->phys_addr = phys_addr;

	err = ioremap_page_range(addr, addr + size, phys_addr, prot);
	if (err) {
		vunmap((void *)addr);
		return NULL;
	}

	return (void __iomem *)(offset + addr);
}

可以看到,上面的記憶體屬性都是DEVICE MEMORY,其中pfn_valid(__phys_to_pfn(phys_addr))就是用來判斷是否是系統實體記憶體的,如果是的話,返回true,那麼ioremap就會直接返回0.

下面分析PTE是怎麼構造的呢?

下面分析缺頁異常中中構造PTE的部分:

handle_pte_fault
	|- do_anonymous_page
		|- entry = mk_pte(page, vma->vm_page_prot);

這裡vm_page_prot存放的就是PTE中屬性部分,這些屬性是通過vm_get_page_prot根據vm_flags轉換而來:

/* description of effects of mapping type and prot in current implementation.
 * this is due to the limited x86 page protection hardware.  The expected
 * behavior is in parens:
 *
 * map_type	prot
 *		PROT_NONE	PROT_READ	PROT_WRITE	PROT_EXEC
 * MAP_SHARED	r: (no) no	r: (yes) yes	r: (no) yes	r: (no) yes
 *		w: (no) no	w: (no) no	w: (yes) yes	w: (no) no
 *		x: (no) no	x: (no) yes	x: (no) yes	x: (yes) yes
 *
 * MAP_PRIVATE	r: (no) no	r: (yes) yes	r: (no) yes	r: (no) yes
 *		w: (no) no	w: (no) no	w: (copy) copy	w: (no) no
 *		x: (no) no	x: (no) yes	x: (no) yes	x: (yes) yes
 */
pgprot_t protection_map[16] __ro_after_init = {
	__P000, __P001, __P010, __P011, __P100, __P101, __P110, __P111,
	__S000, __S001, __S010, __S011, __S100, __S101, __S110, __S111
};

pgprot_t vm_get_page_prot(unsigned long vm_flags)
{
	pgprot_t ret = __pgprot(pgprot_val(protection_map[vm_flags &
				(VM_READ|VM_WRITE|VM_EXEC|VM_SHARED)]) |
			pgprot_val(arch_vm_get_page_prot(vm_flags)));

	return arch_filter_pgprot(ret);
}
EXPORT_SYMBOL(vm_get_page_prot);

上面這些宏定義在arch/arm64/include/asm/pgtable-prot.h中,

#define PAGE_NONE		__pgprot(((_PAGE_DEFAULT) & ~PTE_VALID) | PTE_PROT_NONE | PTE_RDONLY | PTE_NG | PTE_PXN | PTE_UXN)
#define PAGE_SHARED		__pgprot(_PAGE_DEFAULT | PTE_USER | PTE_NG | PTE_PXN | PTE_UXN | PTE_WRITE)
#define PAGE_SHARED_EXEC	__pgprot(_PAGE_DEFAULT | PTE_USER | PTE_NG | PTE_PXN | PTE_WRITE)
#define PAGE_READONLY		__pgprot(_PAGE_DEFAULT | PTE_USER | PTE_RDONLY | PTE_NG | PTE_PXN | PTE_UXN)
#define PAGE_READONLY_EXEC	__pgprot(_PAGE_DEFAULT | PTE_USER | PTE_RDONLY | PTE_NG | PTE_PXN)

#define __P000  PAGE_NONE
#define __P001  PAGE_READONLY
#define __P010  PAGE_READONLY
#define __P011  PAGE_READONLY
#define __P100  PAGE_READONLY_EXEC
#define __P101  PAGE_READONLY_EXEC
#define __P110  PAGE_READONLY_EXEC
#define __P111  PAGE_READONLY_EXEC

#define __S000  PAGE_NONE
#define __S001  PAGE_READONLY
#define __S010  PAGE_SHARED
#define __S011  PAGE_SHARED
#define __S100  PAGE_READONLY_EXEC
#define __S101  PAGE_READONLY_EXEC
#define __S110  PAGE_SHARED_EXEC
#define __S111  PAGE_SHARED_EXEC

其中BIT0對應的是宏是PTE_VALID,有問題的PTE的BIT0確實是0.

然後搜尋一下這個宏在核心中的用法,發現使用這個宏的函數還不少:

int set_memory_valid(unsigned long addr, int numpages, int enable)
{
	if (enable)
		return __change_memory_common(addr, PAGE_SIZE * numpages,
					__pgprot(PTE_VALID),
					__pgprot(0));
	else
		return __change_memory_common(addr, PAGE_SIZE * numpages,
					__pgprot(0),
					__pgprot(PTE_VALID));
}

/*
 * This function is used to determine if a linear map page has been marked as
 * not-valid. Walk the page table and check the PTE_VALID bit. This is based
 * on kern_addr_valid(), which almost does what we need.
 *
 * Because this is only called on the kernel linear map,  p?d_sect() implies
 * p?d_present(). When debug_pagealloc is enabled, sections mappings are
 * disabled.
 */
bool kernel_page_present(struct page *page);

static inline pte_t pte_mkpresent(pte_t pte)
{
	return set_pte_bit(pte, __pgprot(PTE_VALID));
}

static inline int pte_protnone(pte_t pte)
{
	return (pte_val(pte) & (PTE_VALID | PTE_PROT_NONE)) == PTE_PROT_NONE;
}

接著看到arch_kexec_protect_crashkres呼叫了set_memory_valid,這個函數是給crash_kernel所在的記憶體設定屬性的,將那段記憶體對映的屬性設定為無效,防止被破壞。

void arch_kexec_protect_crashkres(void)
{
	int i;

	kexec_segment_flush(kexec_crash_image);

	for (i = 0; i < kexec_crash_image->nr_segments; i++)
		set_memory_valid(
			__phys_to_virt(kexec_crash_image->segment[i].mem),
			kexec_crash_image->segment[i].memsz >> PAGE_SHIFT, 0);
}

結合之前看到的iomem的內容,基本可以確認就是這導致的。

下面驗證了一下,將/etc/default/grub中設定的crashkernel=auto刪除,然後重新生成grub.cfg,重啟後再次載入lime模組就可以正常執行了。

最後補充一點ARM64的頁表屬性的只是,參考ARMv8手冊。

  • 中間級和BLOCK級的頁表項的格式

可以看到,BIT0如果是0,那麼就是無效的。

  • PTE級的頁表項格式

其中bit0如果是0,表示invalid,存取的話會異常。