最近做專案遇到一個需求,需要把我們的影像演算法庫提供給客戶使用,為防止演算法庫被對方濫用和逆向破解,需要對演算法庫二進制檔案做加密處理以及加密狗系結,同時防止庫檔案被反除錯跟蹤,演算法庫加密可以借助開源軟體 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.so
3. 間接借助檔案系統的幫助將動態庫匯入行程地址空間
如何不通過檔案系統將動態庫映射到行程的地址空間就是接下來需要面對的問題,顯而易見的想法是硬杠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_HACK
if (!pud_present(*pud)) {
mm_inc_nr_pmds(mm);
pud_populate(mm, pud, new);
} else /* Another has populated it */
pmd_free(mm, new);
#else
if (!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
轉載請註明出處,本文鏈接:https://www.uj5u.com/qita/295564.html
標籤:其他