何為dll注入
DLL注入技術,一般來講是向一個正在運行的進程插入/注入代碼的過程。我們注入的代碼以動態(tài)鏈接庫(DLL)的形式存在。DLL文件在運行時將按需加載(類似于UNIX系統(tǒng)中的共享庫(share object,擴展名為.so))。然而實際上,我們可以以其他的多種形式注入代碼(正如惡意軟件中所常見的,任意PE文件,shellcode代碼/程序集等)。
全局鉤子注入
在Windows大部分應用都是基于消息機制,他們都擁有一個消息過程函數(shù),根據(jù)不同消息完成不同功能,windows通過鉤子機制來截獲和監(jiān)視系統(tǒng)中的這些消息。一般鉤子分局部鉤子與全局鉤子,局部鉤子一般用于某個線程,而全局鉤子一般通過dll文件實現(xiàn)相應的鉤子函數(shù)。
核心函數(shù)
SetWindowsHookEx

通過設定鉤子類型與回調(diào)函數(shù)的地址,將定義的鉤子函數(shù)安裝到掛鉤鏈中。如果函數(shù)成功返回鉤子的句柄,如果函數(shù)失敗,則返回NULL
實現(xiàn)原理
由上述介紹可以知道如果創(chuàng)建的是全局鉤子,那么鉤子函數(shù)必須在一個DLL中。這是因為進程的地址空間是獨立的,發(fā)生對應事件的進程不能調(diào)用其他進程地址空間的鉤子函數(shù)。如果鉤子函數(shù)的實現(xiàn)代碼在DLL中,則在對應事件發(fā)生時,系統(tǒng)會把這個DLL加較到發(fā)生事體的進程地址空間中,使它能夠調(diào)用鉤子函數(shù)進行處理。
在操作系統(tǒng)中安裝全局鉤子后,只要進程接收到可以發(fā)出鉤子的消息,全局鉤子的DLL文件就會由操作系統(tǒng)自動或強行地加載到該進程中。因此,設置全局鉤子可以達到DLL注入的目的。創(chuàng)建一個全局鉤子后,在對應事件發(fā)生的時候,系統(tǒng)就會把 DLL加載到發(fā)生事件的進程中,這樣,便實現(xiàn)了DLL注入。
為了能夠讓DLL注入到所有的進程中,程序設置WH_GETMESSAGE消息的全局鉤子。因為WH_GETMESSAGE類型的鉤子會監(jiān)視消息隊列,并且 Windows系統(tǒng)是基于消息驅(qū)動的,所以所有進程都會有自己的一個消息隊列,都會加載?WH_GETMESSAGE類型的全局鉤子DLL。
那么設置WH_GETMESSAGE就可以通過以下代碼實現(xiàn),記得加上判斷是否設置成功

這里第二個參數(shù)是回調(diào)函數(shù),那么我們還需要寫一個回調(diào)函數(shù)的實現(xiàn),這里就需要用到CallNextHookEx這個api,主要是第一個參數(shù),這里傳入鉤子的句柄的話,就會把當前鉤子傳遞給下一個鉤子,若參數(shù)傳入0則對鉤子進行攔截

既然我們寫入了鉤子,如果不使用的情況下就需要將鉤子卸載掉,那么這里使用到UnhookWindowsHookEx這個api來卸載鉤子

既然我們使用到了SetWindowsHookEx這個api,就需要進行進程間的通信,進程通信的方法有很多,比如自定義消息、管道、dll共享節(jié)、共享內(nèi)存等等,這里就用共享內(nèi)存來實現(xiàn)進程通信

實現(xiàn)過程
首先新建一個dll

在pch.h頭文件里面聲明這幾個我們定義的函數(shù)都是裸函數(shù),由我們自己平衡堆棧


然后在pch.cpp里面寫入三個函數(shù)并創(chuàng)建共享內(nèi)存


然后再在dllmain.cpp設置DLL_PROCESS_ATTACH,然后編譯生成Golbal.dll


再創(chuàng)建一個控制臺項目

使用LoadLibrabryW加載dll,生成GolbalInjectDll.cpp文件


執(zhí)行即可注入GolbalDll.dll


遠程線程注入
遠程線程函數(shù)顧名思義,指一個進程在另一個進程中創(chuàng)建線程。
核心函數(shù)
CreateRemoteThread

lpStartAddress:A pointer to the application-defined function of type?LPTHREAD_START_ROUTINE?to be executed by the thread and represents the starting address of the thread in the remote process. The function must exist in the remote process. For more information, see?ThreadProc).
lpParameter:A pointer to a variable to be passed to the thread function.
lpStartAddress即線程函數(shù),使用LoadLibrary的地址作為線程函數(shù)地址;lpParameter為線程函數(shù)參數(shù),使用dll路徑作為參數(shù)
VirtualAllocEx
是在指定進程的虛擬空間保留或提交內(nèi)存區(qū)域,除非指定MEM_RESET參數(shù),否則將該內(nèi)存區(qū)域置0。

hProcess:申請內(nèi)存所在的進程句柄
lpAddress:保留頁面的內(nèi)存地址;一般用NULL自動分配 。
dwSize:欲分配的內(nèi)存大小,字節(jié)單位;注意實際分 配的內(nèi)存大小是頁內(nèi)存大小的整數(shù)倍。
flAllocationType
可取下列值:
MEM_COMMIT:為特定的頁面區(qū)域分配內(nèi)存中或磁盤的頁面文件中的物理存儲
MEM_PHYSICAL :分配物理內(nèi)存(僅用于地址窗口擴展內(nèi)存)
MEM_RESERVE:保留進程的虛擬地址空間,而不分配任何物理存儲。保留頁面可通過繼續(xù)調(diào)用VirtualAlloc()而被占用
MEM_RESET :指明在內(nèi)存中由參數(shù)lpAddress和dwSize指定的數(shù)據(jù)無效
MEM_TOP_DOWN:在盡可能高的地址上分配內(nèi)存(Windows 98忽略此標志)
MEM_WRITE_WATCH:必須與MEM_RESERVE一起指定,使系統(tǒng)跟蹤那些被寫入分配區(qū)域的頁面(僅針對Windows 98)
flProtect
可取下列值:
PAGE_READONLY: 該區(qū)域為只讀。如果應用程序試圖訪問區(qū)域中的頁的時候,將會被拒絕訪
PAGE_READWRITE 區(qū)域可被應用程序讀寫
PAGE_EXECUTE: 區(qū)域包含可被系統(tǒng)執(zhí)行的代碼。試圖讀寫該區(qū)域的操作將被拒絕。
PAGE_EXECUTE_READ :區(qū)域包含可執(zhí)行代碼,應用程序可以讀該區(qū)域。
PAGE_EXECUTE_READWRITE: 區(qū)域包含可執(zhí)行代碼,應用程序可以讀寫該區(qū)域。
PAGE_GUARD: 區(qū)域第一次被訪問時進入一個STATUS_GUARD_PAGE異常,這個標志要和其他保護標志合并使用,表明區(qū)域被第一次訪問的權限
PAGE_NOACCESS: 任何訪問該區(qū)域的操作將被拒絕
PAGE_NOCACHE: RAM中的頁映射到該區(qū)域時將不會被微處理器緩存(cached)
注:PAGE_GUARD和PAGE_NOCHACHE標志可以和其他標志合并使用以進一步指定頁的特征。PAGE_GUARD標志指定了一個防護頁(guard page),即當一個頁被提交時會因第一次被訪問而產(chǎn)生一個one-shot異常,接著取得指定的訪問權限。PAGE_NOCACHE防止當它映射到虛擬頁的時候被微處理器緩存。這個標志方便設備驅(qū)動使用直接內(nèi)存訪問方式(DMA)來共享內(nèi)存塊。
WriteProcessMemory
此函數(shù)能寫入某一進程的內(nèi)存區(qū)域(直接寫入會出Access Violation錯誤),故需此函數(shù)入口區(qū)必須可以訪問,否則操作將失敗。

實現(xiàn)原理
使用CreateRemoteThread這個API,首先使用CreateToolhelp32Snapshot拍攝快照獲取pid,然后使用Openprocess打開進程,使用VirtualAllocEx
遠程申請空間,使用WriteProcessMemory寫入數(shù)據(jù),再用GetProcAddress獲取LoadLibraryW的地址(由于Windows引入了基址隨機化ASLR安全機制,所以導致每次開機啟動時系統(tǒng)DLL加載基址都不一樣,有些系統(tǒng)dll(kernel,ntdll)的加載地址,允許每次啟動基址可以改變,但是啟動之后必須固定,也就是說兩個不同進程在相互的虛擬內(nèi)存中,這樣的系統(tǒng)dll地址總是一樣的),在注入進程中創(chuàng)建線程(CreateRemoteThread)
實現(xiàn)過程
首先生成一個dll文件,實現(xiàn)簡單的彈窗即可

我們要想進行遠程線程注入,那么就需要得到進程的pid,這里使用到的是CreateToolhelp32Snapshot這個api拍攝快照來進行獲取,注意我這里定義了#include "tchar.h",所有函數(shù)都是使用的寬字符

首先使用OpenProcess打開進程

然后使用VirtualAllocEx遠程申請空間

然后寫入內(nèi)存,使用WriteProcessMemory

然后創(chuàng)建線程并等待線程函數(shù)結束,這里WaitForSingleObject的第二個參數(shù)要設置為-1才能夠一直等待

綜上完整代碼如下



然后這里生成一個test.exe來做測試

編譯并運行,實現(xiàn)效果如下

突破session 0的遠程線程注入
首先提一提session0的概念:
Intel的CPU將特權級別分為4個級別:RING0,RING1,RING2,RING3。Windows只使用其中的兩個級別RING0和RING3,RING0只給操作系統(tǒng)用,RING3誰都能用。如果普通應用程序企圖執(zhí)行RING0指令,則Windows會顯示“非法指令”錯誤信息。
ring0是指CPU的運行級別,ring0是最高級別,ring1次之,ring2更次之…… 拿Linux+x86來說, 操作系統(tǒng)(內(nèi)核)的代碼運行在最高運行級別ring0上,可以使用特權指令,控制中斷、修改頁表、訪問設備等等。 應用程序的代碼運行在最低運行級別上ring3上,不能做受控操作。如果要做,比如要訪問磁盤,寫文件,那就要通過執(zhí)行系統(tǒng)調(diào)用(函數(shù)),執(zhí)行系統(tǒng)調(diào)用的時候,CPU的運行級別會發(fā)生從ring3到ring0的切換,并跳轉(zhuǎn)到系統(tǒng)調(diào)用對應的內(nèi)核代碼位置執(zhí)行,這樣內(nèi)核就為你完成了設備訪問,完成之后再從ring0返回ring3。這個過程也稱作用戶態(tài)和內(nèi)核態(tài)的切換。
RING設計的初衷是將系統(tǒng)權限與程序分離出來,使之能夠讓OS更好的管理當前系統(tǒng)資源,也使得系統(tǒng)更加穩(wěn)定。舉個RING權限的最簡單的例子:一個停止響應的應用程式,它運行在比RING0更低的指令環(huán)上,你不必大費周章的想著如何使系統(tǒng)回復運作,這期間,只需要啟動任務管理器便能輕松終止它,因為它運行在比程式更低的RING0指令環(huán)中,擁有更高的權限,可以直接影響到RING0以上運行的程序,當然有利就有弊,RING保證了系統(tǒng)穩(wěn)定運行的同時,也產(chǎn)生了一些十分麻煩的問題。比如一些OS虛擬化技術,在處理RING指令環(huán)時便遇到了麻煩,系統(tǒng)是運行在RING0指令環(huán)上的,但是虛擬的OS畢竟也是一個系統(tǒng),也需要與系統(tǒng)相匹配的權限。而RING0不允許出現(xiàn)多個OS同時運行在上面,最早的解決辦法便是使用虛擬機,把OS當成一個程序來運行。
核心函數(shù)
ZwCreateThreadEx
注意一下這個地方ZwCreateThreadEx這個函數(shù)在32位和64位中的定義不同
在32位的情況下

在64位的情況下

這里因為我們要進到session 0那么就勢必要到system權限,所以這里還有幾個提權需要用到的函數(shù)
OpenProcessToken

LookupPrivilegeValueA

AdjustTokenPrivileges

實現(xiàn)原理
ZwCreateThreadEx比?CreateRemoteThread函數(shù)更為底層,CreateRemoteThread函數(shù)最終是通過調(diào)用ZwCreateThreadEx函數(shù)實現(xiàn)遠線程創(chuàng)建的。
通過調(diào)用CreateRemoteThread函數(shù)創(chuàng)建遠線程的方式在內(nèi)核6.0(Windows VISTA、7、8等)以前是完全沒有問題的,但是在內(nèi)核6.0 以后引入了會話隔離機制。它在創(chuàng)建一個進程之后并不立即運行,而是先掛起進程,在查看要運行的進程所在的會話層之后再決定是否恢復進程運行。
在Windows XP、Windows Server 2003,以及更老版本的Windows操作系統(tǒng)中,服務和應用程序使用相同的會話(Session)運行,而這個會話是由第一個登錄到控制臺的用戶啟動的。該會話就叫做Session 0,如下圖所示,在Windows Vista之前,Session 0不僅包含服務,也包含標準用戶應用程序。

將服務和用戶應用程序一起在Session 0中運行會導致安全風險,因為服務會使用提升后的權限運行,而用戶應用程序使用用戶特權(大部分都是非管理員用戶)運行,這會使得惡意軟件以某個服務為攻擊目標,通過“劫持”該服務,達到提升自己權限級別的目的。
從Windows Vista開始,只有服務可以托管到Session 0中,用戶應用程序和服務之間會被隔離,并需要運行在用戶登錄到系統(tǒng)時創(chuàng)建的后續(xù)會話中。例如第一個登錄的用戶創(chuàng)建 Session 1,第二個登錄的用戶創(chuàng)建Session 2,以此類推,如下圖所示。

使用CreateRemoteThread注入失敗DLL失敗的關鍵在第七個參數(shù)CreateThreadFlags, 他會導致線程創(chuàng)建完成后一直掛起無法恢復進程運行,導致注入失敗。而想要注冊成功,把該參數(shù)的值改為0即可。
實現(xiàn)過程
在win10系統(tǒng)下如果我們要注入系統(tǒng)權限的exe,就需要使用到debug調(diào)試權限,所以先寫一個提權函數(shù)。

在進程注入dll的過程中,是不能夠使用MessageBox的,系統(tǒng)程序不能夠顯示程序的窗體,所以這里編寫一個ShowError函數(shù)來獲取錯誤碼

首先打開進程獲取句柄,使用到OpenProcess

然后是在注入的進程申請內(nèi)存地址,使用到VirtualAllocEx

再使用WriteProcessMemory寫入內(nèi)存
? ??

加載ntdll,獲取LoadLibraryA函數(shù)地址

獲取ZwCreateThreadEx函數(shù)地址

使用?ZwCreateThreadEx創(chuàng)建遠線程, 實現(xiàn) DLL 注入

這里還有一點需要注意的是ZwCreateThreadEx在?ntdll.dll?中并沒有聲明,所以我們需要使用?GetProcAddress從?ntdll.dll中獲取該函數(shù)的導出地址
這里加上ZwCreateThreadEx的定義,因為64位、32位結構不同,所以都需要進行定義

完整代碼如下




因為在dll注入的過程中是看不到messagebox的,所以這里我選擇cs注入進行測試,若注入成功即可上線
首先生成一個32位的dll文件,這里跟位數(shù)有關,我選擇注入的是32位的進程,所以這里我選擇生成32位的dll

得到路徑

這里我選擇的是有道云筆記進行注入,查看一下pid

然后把我們函數(shù)的pid改為有道云的pid

實現(xiàn)效果如下所示

APC注入
APC,全稱為Asynchronous Procedure Call,即異步過程調(diào)用,是指函數(shù)在特定線程中被異步執(zhí)行,在操作系統(tǒng)中,APC是一種并發(fā)機制。
這里去看一下msdn中異步過程調(diào)用的解釋如下

首先第一個函數(shù)
QueueUserApc: 函數(shù)作用,添加制定的異步函數(shù)調(diào)用(回調(diào)函數(shù))到執(zhí)行的線程的APC隊列中
APCproc: 函數(shù)作用: 回調(diào)函數(shù)的寫法.
往線程APC隊列添加APC,系統(tǒng)會產(chǎn)生一個軟中斷。在線程下一次被調(diào)度的時候,就會執(zhí)行APC函數(shù),APC有兩種形式,由系統(tǒng)產(chǎn)生的APC稱為內(nèi)核模式APC,由應用程序產(chǎn)生的APC被稱為用戶模式APC。這里介紹一下應用程序的APC,APC是往線程中插入一個回調(diào)函數(shù),但是用的APC調(diào)用這個回調(diào)函數(shù)是有條件的,如msdn所示

核心函數(shù)
QueueUserAPC

QueueUserAPC 函數(shù)的第一個參數(shù)表示執(zhí)行函數(shù)的地址,當開始執(zhí)行該APC的時候,程序會跳轉(zhuǎn)到該函數(shù)地址處來執(zhí)行。第二個參數(shù)表示插入APC的線程句柄,要求線程句柄必須包含THREAD_SET_CONTEXT 訪問權限。第三個參數(shù)表示傳遞給執(zhí)行函數(shù)的參數(shù),與遠線程注入類似,如果QueueUserAPC 的第一個參數(shù)為LoadLibraryA,第三個參數(shù)設置的是dll路徑即可完成dll注入。
實現(xiàn)原理
在 Windows系統(tǒng)中,每個線程都會維護一個線程 APC隊列,通過QucueUserAPC把一個APC 函數(shù)添加到指定線程的APC隊列中。每個線程都有自己的APC隊列,這個 APC隊列記錄了要求線程執(zhí)行的一些APC函數(shù)。Windows系統(tǒng)會發(fā)出一個軟中斷去執(zhí)行這些APC 函數(shù),對于用戶模式下的APC 隊列,當線程處在可警告狀態(tài)時才會執(zhí)行這些APC 函數(shù)。一個線程在內(nèi)部使用SignalObjectAndWait 、 SleepEx、WaitForSingleObjectEx、WaitForMultipleObjectsEx等函數(shù)把自己掛起時就是進入可警告狀態(tài),此時便會執(zhí)行APC隊列函數(shù)。
通俗點來概括過程可分為以下幾步:
1)當EXE里某個線程執(zhí)行到SleepEx()或者WaitForSingleObjectEx()時,系統(tǒng)就會產(chǎn)生一個軟中斷(或者是Messagebox彈窗的時候不點OK的時候也能注入)。
2)當線程再次被喚醒時,此線程會首先執(zhí)行APC隊列中的被注冊的函數(shù)。
3)利用QueueUserAPC()這個API可以在軟中斷時向線程的APC隊列插入一個函數(shù)指針,如果我們插入的是Loadlibrary()執(zhí)行函數(shù)的話,就能達到注入DLL的目的。
但是想要使用apc注入也有以下兩點條件:
1.必須是多線程環(huán)境下
2.注入的程序必須會調(diào)用那些同步對象
每一個進程的每一個線程都有自己的APC隊列,我們可以使用QueueUserAPC函數(shù)把一個APC函數(shù)壓入APC隊列中。當處于用戶模式的APC被壓入到線程APC隊列后,線程并不會立刻執(zhí)行壓入的APC函數(shù),而是要等到線程處于可通知狀態(tài)(alertable)才會執(zhí)行,即只有當一個線程內(nèi)部調(diào)用SleepEx等上面說到的幾個特定函數(shù)將自己處于掛起狀態(tài)時,才會執(zhí)行APC隊列函數(shù),執(zhí)行順序與普通隊列相同,先進先出(FIFO),在整個執(zhí)行過程中,線程并無任何異常舉動,不容易被察覺,但缺點是對于單線程程序一般不存在掛起狀態(tài),所以APC注入對于這類程序沒有明顯效果。
實現(xiàn)過程
這里的常規(guī)思路是編寫一個根據(jù)進程名獲取pid的函數(shù),然后根據(jù)PID獲取所有的線程ID,這里我就將兩個函數(shù)集合在一起,通過自己輸入PID來獲取指定進程的線程并寫入數(shù)組


然后是apc注入的主函數(shù),首先使用VirtualAllocEx遠程申請內(nèi)存

然后使用WriteProcessMemory把dll路徑寫入內(nèi)存

再獲取LoadLibraryA的地址

便利線程并插入APC,這里定義一個fail并進行判斷,如果QueueUserAPC返回的值為NULL則線程遍歷失敗,fail的值就+1

然后在到主函數(shù),定義dll地址

使用OpenProcess打開句柄

調(diào)用之前寫好的APCInject函數(shù)實現(xiàn)APC注入

完整代碼如下





之前說過我沒有使用進程名 -> pid的方式,而是直接采用手動輸入的方式,通過cin >> ulProcessID將接收到的參數(shù)賦給ulProcessID

這里可以選擇寫一個MessageBox的dll,這里我直接用的是cs的dll,演示效果如下所示

最后
關注私我獲取【網(wǎng)絡安全學習資料·攻略】
