一种linux平台下算法库二进制文件加密方法探讨
? ? ? ? 最近做項目遇到一個需求,需要把我們的圖像算法庫提供給客戶使用,為防止算法庫被對方濫用和逆向破解,需要對算法庫二進制文件做加密處理以及加密狗綁定,同時防止庫文件被反調試跟蹤。算法庫加密可以借助開源軟件 openssl實現,加密狗的使用也很簡單,從加密狗官方渠道即可拿到arm平臺下的支持庫和簡單的示例程序。接下來就是如何構建對二進制庫文件的加解密以及如何安全的映射至進程地址空間中,一個最容易想到的思路是對算法庫文件套上一層外殼,由外殼程序完成算法庫的自動解密以及完全脫離文件系統的進程內存映射,整個過程中加密后的算法由外殼進程直接解密到內存中,并由驅動程序直接映射至調用進程的地址空間中,完全不會暴露給文件系統,配合一定的反調試技術,使得用戶很難直接拿到算法庫的二進制代碼,從而大大提高對算法庫文件的保護,防止惡意破解。
1. 借助內核編譯initramfs的方式將加密后的算法庫二進制文件嵌入到外殼中
? ? ? ? 利用開源的openssl開發例程很容易實現對算法庫文件的加密和解密,接下來的問題是如何將加密后的算法庫文件套上外殼,多年的內核編譯經驗讓我想到內核的一些奇技淫巧,早些年簡單研究過內核使用cpio打包initramfs的一些原理,內核也是將打包后的文件系統嵌套進vmlinux中的,于是借鑒一下代碼,套殼的第一步就完成了,代碼如下:
.section .init.data,"a" .globl __idata_start __idata_start: .incbin "../deplib/encrypt.so" .globl __idata_end __idata_end:? ? ? ? 代碼里的關鍵指令是incbin匯編命令,該命令可以用來包含可執行文件及其他任意數據,文件內容將按字節逐一添加到當前 ELF 節中,原樣包含不進行任何匯編,同時代碼中定義必要的全局變量定位該段位置。這樣就可以將加密后的算法文件encrypt.so嵌入到外殼程序的ELF段中,并在外殼程序初始化時將其解密至外殼進程的內存中。
2. 常規的動態庫載入方法和問題
? ? ? ? 解密后的算法庫文件位于內存中,如何將其作為動態庫文件導入到進程地址空間中是接下來要處理的問題。常規的動態庫加載方案主要有2種,第一種是最常用的方法,程序編譯時由gcc指定依賴的動態庫信息,該信息會記錄到ELF文件中,并可由readelf -d獲取,應用程序加載時ld.so會根據指定的路徑加載相應的庫。第二種方法是利用libdl庫的dlopen/dlclose/dlsym等接口,在程序執行過程中動態載入庫文件并解析其內部符號等。這2種方法有一個共同的問題是動態庫的載入必須通過文件系統的方式導入進來,而一旦將動態庫暴露給文件系統,技術人員就很容易通過文件系統拿到算法庫的二進制文件進行反編譯,且這2種方式都可以通過cat /proc/{pid}/maps直接定位到導入的動態庫的具體位置,如下所示:
cat /proc/3639/maps 00400000-00402000 r-xp 00000000 b3:02 797096 /root/test/shtest 00411000-00412000 rw-p 00001000 b3:02 797096 /root/test/shtest 00412000-00433000 rw-p 00000000 00:00 0 [heap] ... 7f9b56d000-7f9b57c000 ---p 00017000 b3:02 393672 /lib/aarch64-linux-gnu/libpthread-2.23.so 7f9b57c000-7f9b57d000 r--p 00016000 b3:02 393672 /lib/aarch64-linux-gnu/libpthread-2.23.so 7f9b57d000-7f9b57e000 rw-p 00017000 b3:02 393672 /lib/aarch64-linux-gnu/libpthread-2.23.so3. 間接借助文件系統的幫助將動態庫導入進程地址空間
? ? ? ? 如何不通過文件系統將動態庫映射到進程的地址空間就是接下來需要面對的問題,顯而易見的想法是硬杠glibc,直接改寫dlopen等接口的實現,將其動態庫的載入脫離文件系統,后來發現自己迷失在glibc盤根錯節的層層套用和復雜的符號解析引用中。之后還考慮借鑒內核的vdso及uselib機制,但對ELF文件的符號解析并不是一件短時間內容易做到并可以確保無誤的事情。最好的方法就是仍然沿用dlopen那一套機制,由glibc來處理動態庫的符號解析和引用問題。躊躇之際突然想到驅動程序的設備節點也是一種文件,也有自己的file_operations操作,也支持read/write/mmap等基本的文件操作接口,把它用作動態庫的代理入口交由dlopen處理,由驅動程序配合完成dlopen對動態庫的所有操作,用這種斗轉星移的方法,將處于用戶態內存中的算法庫映射到用戶進程地址空間中,間接脫離文件系統的支持。
3.1 將算法庫二進制文件導入到內核空間
? ? ? ? 首先通過ioctl將用戶態下解密后的算法庫文件導入到驅動程序申請的一段內存空間中,如果內存足夠,使用dma_alloc_coherent直接從CMA區域拿到一段連續內存即可。更加普適的方法是使用alloc_page申請足夠的物理頁幀(成功概率高于連續內存段),將用戶態算法庫數據分頁拷貝至內核空間中,然后使用vmap機制將page頁面數組對應的物理內存映射到vmalloc地址空間中,核心代碼摘錄如下:
wxcoder_dev->pages = kmalloc(sizeof(struct page *) * npages, GFP_KERNEL);if (!wxcoder_dev->pages)goto oom;for (i = 0; i < npages; i++) {struct page *p;p = alloc_page(GFP_KERNEL);if (!p){goto oom;}wxcoder_dev->pages[i] = p;if (copy_from_user(page_address(p), (const void __user *)usr_data->algo_start+i*PAGE_SIZE, (usr_data->algo_len - i*PAGE_SIZE) > PAGE_SIZE ? PAGE_SIZE : (usr_data->algo_len - i*PAGE_SIZE))){debug("err copy %d pages, left: %d pages\n", i, npages - i);ret = -EFAULT;goto oom;}}wxcoder_dev->vbase = vmap(wxcoder_dev->pages, npages, 0, PAGE_KERNEL);if (!wxcoder_dev->vbase){ret = -EFAULT;goto oom;}? ? ? ? 算法庫二進制文件拷貝到內核空間后,接下來驅動程序需要支撐dlopen的實現,首先需要了解dlopen的具體執行過程,涉及到哪些系統調用,最簡單的方法是使用strace調試工具追蹤dlopen的調用過程,其中dlopen使用RTLD_NOW參數調用,結果如下:
... openat(AT_FDCWD, "/dev/wxcoder", O_RDONLY|O_CLOEXEC) = 3 read(3, "\177ELF\2\1\1\0\0\0\0\0\0\0\0\0\3\0\267\0\1\0\0\0\340\351\0\0\0\0\0\0"..., 832) = 832 fstat(3, {st_mode=S_IFCHR|0600, st_rdev=makedev(10, 41), ...}) = 0 mmap(NULL, 515904, PROT_READ|PROT_EXEC, MAP_PRIVATE|MAP_DENYWRITE, 3, 0) = 0x7f7ca5e000 mprotect(0x7f7cac8000, 65536, PROT_NONE) = 0 mmap(0x7f7cad8000, 12288, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_FIXED|MAP_DENYWRITE, 3, 0x6a000) = 0x7f7cad8000 mmap(0x7f7cadb000, 3904, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_FIXED|MAP_ANONYMOUS, -1, 0) = 0x7f7cadb000 close(3) = 0 ...? ? ? ? 可以看出dlopen先后調用了openat、read、mmap、mprotect和close等系統調用,看起來dlopen讀取了動態庫的程序頭表等信息,分析其內部段信息后將其映射到進程地址空間中。顯而易見,我們只需在驅動里實現read/mmap接口即可支撐dlopen的功能。read的實現比較簡單,只要利用copy_to_user函數配合vmap返回的內核虛擬地址即可將dlopen需要的數據返回給用戶態空間,要注意的是dlopen完成后需關閉驅動的read和mmap通道,防止數據通過設備節點外泄給惡意用戶,另外還可以在外殼程序和驅動程序間使用ioctl時加上簡易的口令,以進一步保護數據。
3.2 自定義mmap實現算法庫文件到進程地址空間的映射
? ? ? ? mmap的實現有多種方式,如果先前用的是dma_alloc_coherent申請的一段連續物理內存,mmap實現最簡單,只要使用remap_pfn_range函數將相應的物理頁幀映射到用戶進程地址空間的vma段即可,該函數的底層實現即建立物理頁幀至用戶空間vma段的頁表實例,代碼如下:
static int wzalgo_mmap(struct file *filp, struct vm_area_struct *vma) {unsigned long size = PAGE_ALIGN(vma->vm_end - vma->vm_start);debug("wzalgo_mmap - start: 0x%lx, end: 0x%lx, size: %ld\n\tpgoff: %lu, flags: %lu\n", \vma->vm_start, vma->vm_end, size, vma->vm_pgoff, vma->vm_flags);if (false == readable || false == loadok){return -EINVAL;}...vma->vm_pgoff += (wxcoder_dev->rxdma_addr >> PAGE_SHIFT);return remap_pfn_range(vma, vma->vm_start, vma->vm_pgoff, size, vma->vm_page_prot); }? ? ? ? 如果先前采用的是alloc_page申請的一組物理不連續頁幀保存的算法庫二進制文件,除了采用分頁單獨remap_pfn_range映射的方法,還可以借助缺頁中斷來實現,代碼如下:
static vm_fault_t wzcoder_mapping_fault(struct vm_fault *vmf) {struct vm_area_struct *vma = vmf->vma;pgoff_t pgoff;struct page **pages;debug("wzcoder_mapping_fault vmf->pgoff: %lu\n", vmf->pgoff);pages = vma->vm_private_data;for (pgoff = vmf->pgoff; pgoff && *pages; ++pages)pgoff--;if (*pages) {struct page *page = *pages;get_page(page);vmf->page = page;return 0;}return VM_FAULT_SIGBUS; }static const struct vm_operations_struct wzcoder_mapping_vmops = {.close = wzcoder_mapping_close,.fault = wzcoder_mapping_fault, };static int wzalgo_mmap(struct file *filp, struct vm_area_struct *vma) {unsigned long size = PAGE_ALIGN(vma->vm_end - vma->vm_start);if (false == readable || false == loadok || !wxcoder_dev->vbase){return -EINVAL;}debug("wzalgo_mmap - start: 0x%lx, end: 0x%lx, size: %ld\n\tpgoff: %lu, flags: %lu\n", vma->vm_start, vma->vm_end, size, \vma->vm_pgoff, vma->vm_flags);vma->vm_ops = &wzcoder_mapping_vmops;vma->vm_private_data = (void *)wxcoder_dev->pages;return 0; }? ? ? ? 在用戶態進程mmap該段vma區域時,注冊vm_operations_struct結構,缺頁中斷處理函數wzcoder_mapping_fault把vmf->pgoff對應的物理頁幀返回給vmf->page,其中vmf->pgoff指示當前頁在vma區域中邏輯頁的偏移量,由于該段vma的邏輯頁和實際的物理頁幀是一一對應的,所以很容易找到對應的page實例。
? ? ? ? 更深入一點的方法可以參考remap_pfn_range的底層實現,自己建立所需的頁表結構,強化對內核建立頁表過程的理解,代碼摘錄如下:
#define pte_alloc_wz(mm, pmd, address) \(unlikely(pmd_none(*(pmd))) && __pte_alloc_wz(mm, pmd, address))#define pte_alloc_map_lock_wz(mm, pmd, address, ptlp) \(pte_alloc_wz(mm, pmd, address) ? NULL : pte_offset_map_lock(mm, pmd, address, ptlp))int __pte_alloc_wz(struct mm_struct *mm, pmd_t *pmd, unsigned long address) {spinlock_t *ptl;pgtable_t new = pte_alloc_one(mm, address);if (!new)return -ENOMEM;smp_wmb(); /* Could be smp_wmb__xxx(before|after)_spin_lock */ptl = pmd_lock(mm, pmd);if (likely(pmd_none(*pmd))) { /* Has another populated it ? */mm_inc_nr_ptes(mm);pmd_populate(mm, pmd, new);new = NULL;}spin_unlock(ptl);if (new)pte_free(mm, new);return 0; }int __pmd_alloc_wz(struct mm_struct *mm, pud_t *pud, unsigned long address) {spinlock_t *ptl;pmd_t *new = pmd_alloc_one(mm, address);if (!new)return -ENOMEM;smp_wmb(); /* See comment in __pte_alloc */ptl = pud_lock(mm, pud); #ifndef __ARCH_HAS_4LEVEL_HACKif (!pud_present(*pud)) {mm_inc_nr_pmds(mm);pud_populate(mm, pud, new);} else /* Another has populated it */pmd_free(mm, new); #elseif (!pgd_present(*pud)) {mm_inc_nr_pmds(mm);pgd_populate(mm, pud, new);} else /* Another has populated it */pmd_free(mm, new); #endif /* __ARCH_HAS_4LEVEL_HACK */spin_unlock(ptl);return 0; }static inline pmd_t *pmd_alloc_wz(struct mm_struct *mm, pud_t *pud, unsigned long address) {return (unlikely(pud_none(*pud)) && __pmd_alloc_wz(mm, pud, address))? NULL: pmd_offset(pud, address); }static int remap_pte_range(struct mm_struct *mm, pmd_t *pmd,unsigned long addr, unsigned long end,unsigned long pfn, pgprot_t prot) {pte_t *pte;spinlock_t *ptl;int err = 0;pte = pte_alloc_map_lock_wz(mm, pmd, addr, &ptl);if (!pte)return -ENOMEM;arch_enter_lazy_mmu_mode();do {BUG_ON(!pte_none(*pte));if (!pfn_modify_allowed(pfn, prot)) {err = -EACCES;break;}set_pte_at(mm, addr, pte, pte_mkspecial(pfn_pte(pfn, prot)));pfn++;} while (pte++, addr += PAGE_SIZE, addr != end);arch_leave_lazy_mmu_mode();pte_unmap_unlock(pte - 1, ptl);return err; }static inline int remap_pmd_range(struct mm_struct *mm, pud_t *pud,unsigned long addr, unsigned long end,unsigned long pfn, pgprot_t prot) {pmd_t *pmd;unsigned long next;int err;pfn -= addr >> PAGE_SHIFT;pmd = pmd_alloc_wz(mm, pud, addr);if (!pmd)return -ENOMEM;VM_BUG_ON(pmd_trans_huge(*pmd));do {next = pmd_addr_end(addr, end);err = remap_pte_range(mm, pmd, addr, next,pfn + (addr >> PAGE_SHIFT), prot);if (err)return err;} while (pmd++, addr = next, addr != end);return 0; }static inline int remap_pud_range(struct mm_struct *mm, p4d_t *p4d,unsigned long addr, unsigned long end,unsigned long pfn, pgprot_t prot) {pud_t *pud;unsigned long next;int err;pfn -= addr >> PAGE_SHIFT;pud = pud_alloc(mm, p4d, addr);if (!pud)return -ENOMEM;do {next = pud_addr_end(addr, end);err = remap_pmd_range(mm, pud, addr, next,pfn + (addr >> PAGE_SHIFT), prot);if (err)return err;} while (pud++, addr = next, addr != end);return 0; }static inline int remap_p4d_range(struct mm_struct *mm, pgd_t *pgd,unsigned long addr, unsigned long end,unsigned long pfn, pgprot_t prot) {p4d_t *p4d;unsigned long next;int err;pfn -= addr >> PAGE_SHIFT;p4d = p4d_alloc(mm, pgd, addr);if (!p4d)return -ENOMEM;do {next = p4d_addr_end(addr, end);err = remap_pud_range(mm, p4d, addr, next,pfn + (addr >> PAGE_SHIFT), prot);if (err)return err;} while (p4d++, addr = next, addr != end);return 0; }int wx_remap_pfn_range(struct vm_area_struct *vma, unsigned long addr,unsigned long pgoff, unsigned long size, pgprot_t prot) {pgd_t *pgd;unsigned long end = addr + PAGE_ALIGN(size);struct mm_struct *mm = vma->vm_mm;int err, i = 0;if (is_cow_mapping(vma->vm_flags)) {if (addr != vma->vm_start || end != vma->vm_end){return -EINVAL;}vma->vm_pgoff = page_to_pfn(wxcoder_dev->pages[pgoff]);}vma->vm_flags |= VM_IO | VM_PFNMAP | VM_DONTEXPAND | VM_DONTDUMP;BUG_ON(addr >= end);pgd = pgd_offset(mm, addr);flush_cache_range(vma, addr, end);for (i = 0; i < (PAGE_ALIGN(size) >> PAGE_SHIFT); i++){err = remap_p4d_range(mm, pgd, addr, addr + PAGE_SIZE, page_to_pfn(wxcoder_dev->pages[i+pgoff]), prot);if (err){printk("remap_p4d_range: %d\n", err);break;}addr += PAGE_SIZE;}return err; }static int wzalgo_mmap(struct file *filp, struct vm_area_struct *vma) {unsigned long size = PAGE_ALIGN(vma->vm_end - vma->vm_start);if (false == readable || false == loadok){return -EINVAL;}debug("wzalgo_mmap - start: 0x%lx, end: 0x%lx, size: %ld\n\tpgoff: %lu, flags: %lu\n", vma->vm_start, vma->vm_end, size, \vma->vm_pgoff, vma->vm_flags);if (!wxcoder_dev->vbase){return -EINVAL;}vma->vm_ops = &wzcoder_mapping_vmops;vma->vm_private_data = (void *)wxcoder_dev->pages;return wx_remap_pfn_range(vma, vma->vm_start, vma->vm_pgoff, size, vma->vm_page_prot); }? ? ? ? 函數wx_remap_pfn_range完成進程虛擬地址區域vma到物理頁幀間的映射,即一一建立頁表項,pgd_offset根據傳入的addr拿到當前進程的全局頁目錄,考慮到我們需要映射的物理頁幀是非連續的,因此分頁調用remap_p4d_range建立第5級頁表,remap_p4d_range函數根據待映射的地址范圍分段調用remap_pud_range建立第四級頁表,依次下去直至set_pte_at為每個虛擬內存頁建立頁表項。需要注意的是較新的linux內核采用了5級頁表的模式,實際使用的頁表級數依賴于cpu平臺的定義,不同cpu平臺下各級頁表的頁表處理宏的實現是不一樣的,內核使用這種方式將頁表的建立過程統一到5級模式之下。
4. 借助內核ptrace機制設計初步的反調試手段
? ? ? ? 以上工作完成后,設計的內核驅動程序就足以支撐用戶態進程使用dlopen/dlsym等libdl庫中的函數導入動態庫并解析其符號等。加密后的算法庫二進制文件嵌入到外殼程序中,并配合內核驅動程序完成其內存映射的方法大大提高了對算法庫文件的保護,為進一步提高安全性,防止用戶使用strace等調試工具追蹤其執行過程,我們可以借助內核的ptrace機制建立初步的反調試技術,ptrace是linux內核支持的一種進程調試手段,值得慶幸的是即使是常用的gdb的實現也完全依賴于ptrace機制,為此可以采用在檢測到用戶使用ptrace追蹤外殼進程時返回錯誤碼等反制手段。
? ? ? ? 檢測外殼程序是否被ptrace跟蹤調試有2種簡易方法,一種是在驅動程序中添加獲取當前進程task_struct->ptrace值的功能,當用戶態進程被采用PTRACE_ATTACH調試時,內核會修改該值為一個非0值,指示當前進程的調試狀態,外殼程序可以據此判定自己是否被跟蹤調試。另一種方法可以在用戶態監測/proc/self/status,檢查其TracerPid項是否非0,如果非0值則表示當前進程被監控跟蹤了,示例如下:
lyfan@MV:/home/lyfan/shtest$ cat /proc/3629/status Name: shtest State: t (tracing stop) Tgid: 3629 Pid: 3629 PPid: 3627 TracerPid: 3627 ...? ? ? ? TracerPid為3627,可以看到進程3629被其父進程3627跟蹤調試了。
5. 不足和待研究的地方
? ? ? ? 采用這種外殼加固的方法雖然可以將原SO文件完全抹去,但外殼程序在動態加載算法庫文件后,仍然會將解密后的部分動態庫內容暴露到進程地址空間,需要進一步配合反dump技術的使用,加強外殼在內存安全強度方面的不足,prctl(PR_SET_DUMPABLE, 0)可以關閉進程的coredump功能,但仍需結合其他方面的內存安全技術來提升外殼程序的防御能力。另外針對ELF段分別加解密也是一種思路,前提是需要深入了解ELF文件的詳細組織方式,及其內部符號的解析方法等。
? ? ? ? 至此,對算法庫二進制文件的加密和導入的研究算是全部完成了,文中討論的相關技術的示例工程已上傳到gitee上,考慮到安全性,示例工程中的具體實現以及使用的密鑰口令等均做了較大調整。測試采用的內核版本是Linux 4.19.0,cpu是arm64平臺,gcc版本8.2.0。
附上項目地址:https://gitee.com/liangyuf/linux_so_encrypt
總結
以上是生活随笔為你收集整理的一种linux平台下算法库二进制文件加密方法探讨的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: plupload上传文件碰到的问题
- 下一篇: mysql sysbench 教程_my