前言
在安全研發(fā)的過程中,難免會(huì)使用內(nèi)存分配函數(shù) malloc、重載的運(yùn)算符 new 開啟堆內(nèi)存用于長時(shí)間駐留一些數(shù)據(jù),但這些數(shù)據(jù)可能對于防御者來說比較敏感,比如有時(shí)候,這些堆內(nèi)存中可能會(huì)出現(xiàn)回連地址等。所以有必要對這些保留在堆內(nèi)存中的敏感數(shù)據(jù)進(jìn)行加密。
在進(jìn)入堆分配內(nèi)存加密之前,首先了解以下程序中堆棧的概念;
堆和棧是程序運(yùn)行時(shí)保存數(shù)據(jù)的方式。
棧:通常來說棧用于存儲(chǔ)臨時(shí)數(shù)據(jù),比如局部變量和函數(shù)調(diào)用的上下文信息,棧上的數(shù)據(jù)的生命周期隨著函數(shù)的調(diào)用和返回而自動(dòng)管理,一般在作用域結(jié)束后被自動(dòng)釋放;
堆:堆通常用于存儲(chǔ)想長期駐留在內(nèi)存中的數(shù)據(jù),比如一些需要長期存在的配置信息和一些需要共享的數(shù)據(jù),堆上的數(shù)據(jù)的生命周期不受作用域的限制,一般在整個(gè)程序運(yùn)行期間都存在,直到顯示地釋放它們。
由于堆上的數(shù)據(jù)的生命周期不受限制,除非顯式地釋放它們,否則它們將一直駐留在內(nèi)存中,如果我們想保護(hù)這些數(shù)據(jù),可以在內(nèi)存中將存儲(chǔ)這些數(shù)據(jù)的堆內(nèi)存進(jìn)行加密,防止數(shù)據(jù)暴露。
堆分配內(nèi)存加密
枚舉堆
在對堆進(jìn)行操作之前,首先需要先枚舉進(jìn)程內(nèi)存中堆的信息,可以通過 HeapWalk 函數(shù)枚舉指定堆中的內(nèi)存塊,其函數(shù)簽名如下:
BOOL HeapWalk( [in] HANDLE hHeap, [in, out] LPPROCESS_HEAP_ENTRY lpEntry );
可以使用以下代碼枚舉進(jìn)程內(nèi)存中堆的信息
void EnumHeaps() { PROCESS_HEAP_ENTRY entry; SecureZeroMemory(&entry, sizeof(entry)); while (HeapWalk(GetProcessHeap(), &entry)) { printf("heap addr: %p size: %d ", (char*)entry.lpData, entry.cbData); } }
獲取了進(jìn)程內(nèi)存中的堆信息,就可以對指定類型的堆進(jìn)行操作了,一般加密已分配的堆,需要注意的是,由于堆中的數(shù)據(jù)是線程共享的,所以,在加密堆數(shù)據(jù)之前,需要掛起所有線程(除了當(dāng)前執(zhí)行的線程),待操作結(jié)束后,恢復(fù)所有線程的執(zhí)行。
掛起線程
可以使用 SuspendThread 掛起指定線程,需要注意的是,需要排除當(dāng)前執(zhí)行的線程。
void DoSuspendThreads(DWORD targetProcessId, DWORD targetThreadId) { HANDLE h = CreateToolhelp32Snapshot(TH32CS_SNAPTHREAD, 0); if (h != INVALID_HANDLE_VALUE) { THREADENTRY32 te; te.dwSize = sizeof(te); if (Thread32First(h, &te)) { do { if (te.dwSize >= FIELD_OFFSET(THREADENTRY32, th32OwnerProcessID) + sizeof(te.th32OwnerProcessID)) { // Suspend all threads EXCEPT the one we want to keep running if (te.th32ThreadID != targetThreadId && te.th32OwnerProcessID == targetProcessId) { HANDLE thread = ::OpenThread(THREAD_ALL_ACCESS, FALSE, te.th32ThreadID); if (thread != NULL) { SuspendThread(thread); CloseHandle(thread); } } } te.dwSize = sizeof(te); } while (Thread32Next(h, &te)); } CloseHandle(h); } }
恢復(fù)線程
使用 ResumeThread 函數(shù),可以讓掛起的線程恢復(fù)。
void DoResumeThreads(DWORD targetProcessId, DWORD targetThreadId) { HANDLE h = CreateToolhelp32Snapshot(TH32CS_SNAPTHREAD, 0); if (h != INVALID_HANDLE_VALUE) { THREADENTRY32 te; te.dwSize = sizeof(te); if (Thread32First(h, &te)) { do { if (te.dwSize >= FIELD_OFFSET(THREADENTRY32, th32OwnerProcessID) + sizeof(te.th32OwnerProcessID)) { // Suspend all threads EXCEPT the one we want to keep running if (te.th32ThreadID != targetThreadId && te.th32OwnerProcessID == targetProcessId) { HANDLE thread = ::OpenThread(THREAD_ALL_ACCESS, FALSE, te.th32ThreadID); if (thread != NULL) { ResumeThread(thread); CloseHandle(thread); } } } te.dwSize = sizeof(te); } while (Thread32Next(h, &te)); } CloseHandle(h); } }
在安全研發(fā)的過程中,為了降低自動(dòng)化分析對代碼的敏感度,通常會(huì)使用延遲代碼的執(zhí)行速度,比如使用Sleep函數(shù)。
根據(jù)以上,進(jìn)程中堆分配內(nèi)存的步驟如下:
掛鉤 Sleep 函數(shù)
在 HookedSleep 函數(shù)內(nèi)部:
掛起當(dāng)前進(jìn)程內(nèi)所有線程(排除當(dāng)前執(zhí)行線程)
對已分配的堆進(jìn)行加密
調(diào)用原始 Sleep 進(jìn)行睡眠操作
恢復(fù)所有線程執(zhí)行
void WINAPI HookedSleep(DWORD dwMiliseconds) { DWORD time = dwMiliseconds; if (time > 1000) { printf("HookedSleep: suspend threads, strat encrypt heaps. "); DoSuspendThreads(GetCurrentProcessId(), GetCurrentThreadId()); HeapEncryptDecrypt(); OldSleep(dwMiliseconds); HeapEncryptDecrypt(); DoResumeThreads(GetCurrentProcessId(), GetCurrentThreadId()); printf("HookedSleep: decrypt heaps success, resume threads run. "); } else { OldSleep(time); } }
當(dāng)代碼中存在使用堆相關(guān)函數(shù),比如 HeapAlloc 分配內(nèi)存時(shí),再比如使用 CRT 函數(shù) malloc 和 重載的運(yùn)算符 new 開辟堆空間時(shí)(其最終也是通過HeapAlloc函數(shù)分配),都可通過堆分配內(nèi)存加密保護(hù)我們的數(shù)據(jù)。
下圖是在掛鉤了 Sleep 后,程序進(jìn)入HookedSleep 函數(shù)未進(jìn)行對加密之前我們在程序中分配的堆空間的原始數(shù)據(jù)。
在進(jìn)行堆加密后,效果如下,可以看到,我們堆分配的內(nèi)存已被加密。
目標(biāo)線程堆分配空間加密
上文可以看到通過 HeapWalk 遍歷了進(jìn)程中所有的堆信息,并在堆加密之前比如掛起所有的線程,這樣有一個(gè)缺點(diǎn),就是只能針對自己寫的程序。
有時(shí)候我們的需要執(zhí)行的功能代碼是通過注入到其它進(jìn)程實(shí)現(xiàn)的,如果這樣冒然的將所有線程掛起,這有太多的不可預(yù)料性,可能會(huì)導(dǎo)致宿主程序崩潰。
所以需要有一種方式來實(shí)現(xiàn)只加密我們執(zhí)行功能線程代碼中分配的堆內(nèi)存。
由于通過注入 shellcode/dll 的方式執(zhí)行功能,所以首要目標(biāo)是獲取執(zhí)行功能的線程ID,之后就是在代碼中 Hook 堆分配/釋放相關(guān)的函數(shù),RtlAllocateHeap,RtlReAllocateHeap,RtlFreeHeap 函數(shù),這樣就可以跟蹤線程代碼中堆分配釋放的狀態(tài)了。
獲取了線程堆分配釋放的情況,目標(biāo)線程堆分配內(nèi)存加密的代碼思路如下:
獲取當(dāng)前執(zhí)行線程ID
Hook RtlAllocateHeap,RtlReAllocateHeap,RtlFreeHeap
在 HookedRtlAllocateHeap、HookedRtlReAllocateHeap 函數(shù)內(nèi)部
執(zhí)行原始 OldRtlAllocateHeap、OldRtlReAllocateHeap,并記錄堆分配的地址和大小
篩選出我們的線程分配的堆內(nèi)存:將堆分配信息添加進(jìn)維護(hù)的堆分配信息數(shù)組中
HookedRtlFreeHeap 函數(shù)內(nèi)部
得到需要釋放的地址
執(zhí)行原始 OldRtlFreeHeap
篩選出我們的線程釋放的堆內(nèi)存:將其移除維護(hù)的堆分配信息信息數(shù)組
當(dāng)篩選出目標(biāo)線程堆分配的內(nèi)存信息時(shí),就可以通過在 HookedSleep 函數(shù)中對填充好堆分配信息數(shù)組進(jìn)行操作了。
審核編輯:劉清
-
crt
+關(guān)注
關(guān)注
2文章
80瀏覽量
35776 -
加解密
+關(guān)注
關(guān)注
0文章
17瀏覽量
6500
原文標(biāo)題:安全開發(fā)之堆分配內(nèi)存加密
文章出處:【微信號:蛇矛實(shí)驗(yàn)室,微信公眾號:蛇矛實(shí)驗(yàn)室】歡迎添加關(guān)注!文章轉(zhuǎn)載請注明出處。
發(fā)布評論請先 登錄
相關(guān)推薦
評論