
如何快速實現REST API集成以優化業務流程
[in] HWND hWnd,
[in] DWORD dwAffinity
);
看上去水到渠成,可是正如MSDN文檔里說的,hWnd指定的窗口必須屬于本進程。如果窗口是人家的,咱們調用這個函數就不頂用了。
如果我們能讓目標進程調用它,問題就能迎刃而解。前面說了,如果注入DLL到目標進程中來執行它,會涉及DLL注入和進程間通信。如果我們使用CreateRemoteThread,能否直接讓遠端線程執行這個函數呢?理論上,行得通,接下來看看該如何設計方案。
我們拿SetWindowDisplayAffinity為例,當然最終的方案要可以推廣到任意API上。
這里先簡單介紹本方案用到的一些其它技術能力,尤其是ShellCode相關。不具體展開描述,后面直接利用。
【C語言編寫ShellCode】
比起使用匯編編寫ShellCode,用C編寫有不少優勢:
>編寫復雜的ShellCode更加容易。
>一份C源碼,即可同時編譯出運行于x86、x64,甚至ARM等不同目標平臺的ShellCode。
>配合Precomp4C將不同平臺的ShellCode放到源文件里,x64進程向x86進程注入x86 ShellCode也非常方便。
實現方案(Precomp4C,https://github.com/KNSoft/Precomp4C)如下圖所示:
【在目標進程中調用ShellCode】
ShellCode有了,我們將ShellCode寫入目標進程,執行完畢后我們也能讀取目標進程,將ShellCode返回的內容讀取回來,善始善終。
實現方案NTAssassin!Hijack_ExecShellcode()函數(https://github.com/KNSoft/NTAssassin/blob/master/Source/NTAssassin/NTAHijack.c)原型如下:
/// <summary>
/// Injects shellcode and starts new thread to execute in remote process
/// </summary>
/// <param name="ProcessHandle">Handle to the process</param>
/// <param name="ShellCode">Pointer to the shellcode</param>
/// <param name="ShellCodeSize">Size of shellcode in bytes</param>
/// <param name="Param">User defined parameter passed to the remote thread</param>
/// <param name="ParamSize">Size of Param in bytes</param>
/// <param name="ExitCode">Pointer to variable to receive remote thread exit code</param>
/// <param name="Timeout">Timeout in milliseconds</param>
/// <returns>TRUE if succeeded, or FALSE if failed, error code storaged in last STATUS</returns>
/// <remarks>
/// HIJACK_PROCESS_ACCESS access is required
/// if Timeout is 0, ExitCode always returns STILL_ACTIVE
/// </remarks>
_Success_(return != FALSE) NTA_API BOOL NTAPI Hijack_ExecShellcode(_In_ HANDLE ProcessHandle, _In_reads_bytes_(ShellCodeSize) PVOID ShellCode, SIZE_T ShellCodeSize, _In_reads_bytes_opt_(ParamSize) PVOID Param, SIZE_T ParamSize, _Out_opt_ PDWORD ExitCode, DWORD Timeout);
ProcessHandle:目標進程句柄。
ShellCode:指向要注入并執行的ShellCode二進制字節碼。ShellCodeSize:ShellCode大小,計以字節。Param:傳遞給ShellCode的參數,指向本進程的一片內存區域,內存區域會映射到目標進程,并作為遠端線程(LPTHREAD_START_ROUTINE)的入參。遠端線程執行完ShellCode后還會將這片內存區域寫回本進程。既是入參也是出參,實現和ShellCode交互,這里的SAL批注“_In_reads_bytes_opt_(ParamSize)”有誤,后續會修正。ParamSize:Param大小,計以字節。ExitCode:可選,接收遠端線程的退出碼。
Timeout:等待遠端線程的超時,計以毫秒。可選,若為0則不等待,INFINITE無限等待。如果為0的同時傳入了ExitCode,則固定返回退出碼為STILL_ACTIVE。
一些內存操作、線程函數即可實現,并不復雜。重點是實現與ShellCode交互,通過內存讀寫和線程等待,實現既能給ShellCode提供花式入參,也能接收ShellCode的花式出參,也就是Param參數指向的那片內存區域寫過去等ShellCode執行完再讀回來。
實際應用中還得考慮一下特殊情況:
>如果ShellCode執行很慢直到超時,此時不能釋放目標進程的內存,否則會導致崩潰。
>如果目標為被系統掛起的UWP,那么創建的遠端線程也會被掛起,并且無法喚醒。
好了,有以上兩個技術儲備,我們接下來著手實現目標。
首先我們得知道SetWindowDisplayAffinity在目標進程的地址,才能讓遠端線程調用。如果目標函數所在的DLL未加載,則我們加載該DLL再尋址(為了推廣到支持任意API)。有了前文提到的【C語言編寫ShellCode】和【在目標進程中執行ShellCode】技術儲備,這個實現很容易。
目標函數所在的DLL名與目標函數名作為入參傳給我們的ShellCode,ShellCode負責尋址并調用LoadLibrary(https://docs.microsoft.com/en-us/windows/win32/api/libloaderapi/nf-libloaderapi-loadlibraryw)、GetProcAddress(https://docs.microsoft.com/en-us/windows/win32/api/libloaderapi/nf-libloaderapi-getprocaddress),再將獲取到的目標函數地址傳回來即可。
ShellCode的C語言實現可參考NTAssassin!Hijack_LoadProcAddr_InjectThread()(https://github.com/KNSoft/NTAssassin/blob/master/Source/NativeASM/HijackC.c)。可以看到Hijack_LoadProcAddr_InjectThread函數是一個遠端線程函數,進行了以下操作:接收入參(DLL名稱、函數名稱)->遍歷進程DLL鏈表(按加載順序)->找到第一個加載的DLL(必定是ntdll.dll)->尋址LdrLoadDll和LdrGetProcedureAddress->調用這倆獲得目標函數地址->反饋。當然用kernel32的LoadLibrary和GetProcAddress也一樣,看心情就好了。
這套流程封裝成函數,原型如下:
/// <summary>
/// Gets procedure address in remote process space, if specified library not loaded in remote process, will be load
/// </summary>
/// <param name="ProcessHandle">Handle to the process</param>
/// <param name="LibName">Library name</param>
/// <param name="ProcName">Procedure name, can be NULL if loads library only</param>
/// <param name="ProcAddr">Pointer to a pointer variable to receive remote procedure address, can be NULL only if ProcName also is NULL</param>
/// <param name="Timeout">Timeout in milliseconds</param>
/// <returns>TRUE if succeeded, or FALSE if failed, error code storaged in last STATUS</returns>
/// <remarks>HIJACK_PROCESS_ACCESS access is required</remarks>
NTA_API BOOL NTAPI Hijack_LoadProcAddr(_In_ HANDLE ProcessHandle, _In_z_ PCWSTR LibName, _In_opt_z_ PCSTR ProcName, _When_(ProcName != NULL, _Notnull_) PVOID64* ProcAddr, DWORD Timeout);
ProcessHandle:目標進程句柄。LibName:DLL名稱或路徑。ProcName:函數名,如果為NULL則只加載DLL,不尋址函數。ProcAddr:指向一個指針變量,接收要尋址函數的地址。
Timeout:等待遠端線程的超時,計以毫秒。可選,若為0則不等待,INFINITE無限等待。
實現源碼在NTAssassin!Hijack_LoadProcAddr()(https://github.com/KNSoft/NTAssassin/blob/master/Source/NTAssassin/NTAHijack.c)中,基于上述實現。
Hijack_LoadProcAddr(hProc, L"user32.dll", "SetWindowDisplayAffinity", &pfnSetWindowDisplayAffinity, 5000);
簡簡單單一行,即可獲取目標進程(hProc)空間中SetWindowDisplayAffinity函數地址,返回于pfnSetWindowDisplayAffinity中。
函數在目標進程中的地址已經得到,那么我們如何調用它呢?
如果我們創建一個CREATE_SUSPENDED的遠端線程,然后修改線程上下文(SetThreadContext,https://docs.microsoft.com/en-us/windows/win32/api/processthreadsapi/nf-processthreadsapi-setthreadcontext),PC(EIP/RIP)指向目標函數,根據函數調用約定構造入參,然后執行……似乎可行,但“善始”容易“善后”難。我們只能獲取到函數的返回值(也就是遠端線程的退出碼),獲取不到LastError。
但我們可以故技重施,再度利用ShellCode。我們將目標函數的入參構造好,傳遞給ShellCode,ShellCode來調用目標函數,目標函數返回后,ShellCode獲取LastError再回傳給我們。這樣,即使在目標進程中調用函數失敗了,也能失敗得明明白白。
剩下的就是如何傳參的問題。
首先看調用約定。Windows在x64中調用約定一致,都是微軟Style的FASTCALL,前四個參數用寄存器傳遞,后面的壓棧。在x86下Windows API基本都是STDCALL,我們暫時只支持它,參數從右往左依次入棧即可。要支持其它調用約定也很容易,根據約定把參數按順序傳到該傳的地方就行了。
再看參數類型。如果參數為立即數,那最簡單了,直接賦值給寄存器/壓棧即可。注意如果是浮點數,x64的FASTCALL是用xmm寄存器傳遞的。如果為指針,那么通過內存操作,在目標進程里開辟內存空間,將指針指向的內容寫入,再傳入這個內存地址即可。
傳遞給ShellCode的參數如何構造,來描述要調用的函數地址、調用約定、各個入參,這個仁者見仁智者見智了,兩端對齊即可。注意C的默認結構體字節對齊和匯編是不同的,可以用“#pragma pack”指令進行操作。
這里的ShellCode需要直接操作寄存器,咱們還是得用匯編來寫,也是方案核心實現的一部分。這里貼x86版本的匯編代碼作示例:
頭文件(NTAssassin – HijackASM.inc,https://github.com/KNSoft/NTAssassin/blob/master/Source/NativeASM/HijackASM.inc)
INCLUDELIB OLDNAMES
IFDEF _DEBUG
IFDEF _DLL
INCLUDELIB msvcrtd.lib
INCLUDELIB vcruntimed.lib
INCLUDELIB ucrtd.lib
ELSE
INCLUDELIB libcmtd.lib
INCLUDELIB libvcruntimed.lib
INCLUDELIB libucrtd.lib
ENDIF
ELSE
IFDEF _DLL
INCLUDELIB msvcrt.lib
INCLUDELIB vcruntime.lib
INCLUDELIB ucrt.lib
ELSE
INCLUDELIB libcmt.lib
INCLUDELIB libvcruntime.lib
INCLUDELIB libucrt.lib
ENDIF
ENDIF
STATUS_NOT_IMPLEMENTED equ 0C0000002h
CC_FASTCALL equ 0
CC_CDECL equ 1
CC_MSCPASCAL equ 2
CC_PASCAL equ 2
CC_MACPASCAL equ 3
CC_STDCALL equ 4
CC_FPFASTCALL equ 5
CC_SYSCALL equ 6
CC_MPWCDECL equ 7
CC_MPWPASCAL equ 8
CC_MAX equ 9
HIJACK_CALLPROCHEADER STRUCT
Procedure DWORD ?
Padding0 DWORD ?
CallConvention DWORD ?
RetValue DWORD ?
Padding1 DWORD ?
LastError DWORD ?
LastStatus DWORD ?
ExceptionCode DWORD ?
ParamCount DWORD ?
HIJACK_CALLPROCHEADER ENDS
HIJACK_CALLPROCPARAM STRUCT
_Address DWORD ?
Padding0 DWORD ?
_Size DWORD ?
Padding1 DWORD ?
_Out DWORD ?
HIJACK_CALLPROCPARAM ENDS
HIJACK_CALLPROCHEADER是ShellCode接收的輸入結構體,里面Procedure描述要調用的目標函數地址,CallConvention描述目標函數調用約定,直接照搬Windows SDK里的CC_*定義即可。RetValue接收函數返回值,LastError、LastStatus、ExceptionCode接收函數執行完后的信息,這幾個字段都在TEB(線程環境塊)里。ParamCount是參數的數量,也就是跟在HIJACK_CALLPROCHEADER后面HIJACK_CALLPROCPARAM結構體的數量。
HIJACK_CALLPROCPARAM結構體描述目標函數的各個參數,若_Size為0,則_Address為立即數;若_Size為-1,則_Address為浮點數;否則_Address為指針,_Size指定其大小。ShellCode無需關注參數類型,只管把_Address傳入即可。調用者據此構造參數(為指針的時候要在目標進程里開辟內存并寫值)。
里面的Padding是為了兼顧x64,可惜ml64似乎還不支持結構體,只能硬編碼偏移量。
源文件(NTAssassin!Hijack_CallProc_InjectThread_x86,https://github.com/KNSoft/NTAssassin/blob/master/Source/NativeASM/HijackASM_x86.asm):
.686P
.XMM
.model flat, stdcall
include HijackASM.inc
.code
assume fs:nothing
; DWORD WINAPI Hijack_CallProc_InjectThread_x86(LPVOID lParam)
Hijack_CallProc_InjectThread_x86 PROC USES ebx edi esi lParam
; edi point to HIJACK_CALLPROCHEADER
xor eax, eax
mov edi, lParam
assume edi:ptr HIJACK_CALLPROCHEADER
; Support stdcall(CC_STDCALL) only
.if [edi].CallConvention != CC_STDCALL
mov eax, STATUS_NOT_IMPLEMENTED
ret
.endif
; esi point to HIJACK_CALLPROCPARAM array, ebx point to random parameters
lea esi, [edi + sizeof HIJACK_CALLPROCHEADER]
assume esi:ptr HIJACK_CALLPROCPARAM
mov ecx, [edi].ParamCount
mov eax, sizeof HIJACK_CALLPROCPARAM
mul ecx
lea ebx, [esi + eax]
; Enum HIJACK_CALLPROCPARAM
@@:
mov eax, [esi]._Size
.if eax && eax != -1
; edx = address to random parameter
mov edx, ebx
; Align size of random parameter to 4
add eax, 3
and eax, -4
; ebx point to the next random parameter
add ebx, eax
.else
mov edx, [esi]._Address
.endif
; Push parameter
push edx
add esi, sizeof HIJACK_CALLPROCPARAM
loop @b
; Clear LastError, LastStatus and ExceptionCode
xor eax, eax
mov fs:[34h], eax
mov fs:[0BF4h], eax
mov fs:[1A4h], eax
; Call procedure
call [edi].Procedure
; Write RetValue, LastError, LastStatus and ExceptionCode
mov [edi].RetValue, eax
mov eax, fs:[34h]
mov [edi].LastError, eax
mov eax, fs:[0BF4h]
mov [edi].LastStatus, eax
mov eax, fs:[1A4h]
mov [edi].ExceptionCode, eax
assume edi:nothing, esi:nothing
; Return
xor eax, eax
ret
Hijack_CallProc_InjectThread_x86 ENDP
END
ShellCode遠端線程入口,edi指向調用者傳來的內存區域,以HIJACK_CALLPROCHEADER結構體開頭。esi指向其后緊隨的HIJACK_CALLPROCPARAM結構體數組,是函數的各個參數。進行以下操作:>檢查調用約定為支持的STDCALL。>loop指令遍歷HIJACK_CALLPROCPARAM數組并將參數壓棧。>清空TEB的LastError、LastStatus、ExceptionCode。>調用目標函數。>反饋返回值(eax)、LastError、LastStatus、ExceptionCode。
x64版本的匯編ShellCode復雜一點,因為FASTCALL前四個參數要放在不同寄存器里,還要考慮是不是浮點數,不像STDCALL一個loop指令一股腦壓棧就完事了。還有,ml64寫得也硌手。總體思路是一致的,就不貼上來辣眼睛了,源文件NTAssassin!Hijack_CallProc_InjectThread_x64(https://github.com/KNSoft/NTAssassin/blob/master/Source/NativeASM/HijackASM_x64.asm)。
這段ShellCode的調用者通過內存操作,構造好ShellCode入參和指針類型的參數,寫入ShellCode并執行再接收其返回內容即可。我們封裝為Hijack_CallProc,如下文所示。
最終封裝簡單很多了:
/// <summary>
/// Starts a thread and calls a procedure in remote process
/// </summary>
/// <param name="ProcessHandle">Handle to the process</param>
/// <param name="CallProcHeader">Pointer to a HIJACK_CALLPROCHEADER structure contains procedure information and receives return values</param>
/// <param name="Params">Pointer to a HIJACK_CALLPROCPARAM array, corresponding to each parameters of procedure to call</param>
/// <param name="Timeout">Timeout in milliseconds</param>
/// <returns>TRUE if succeeded, or FALSE if failed, error code storaged in last STATUS</returns>
/// <remarks>HIJACK_PROCESS_ACCESS access is required</remarks>
NTA_API BOOL NTAPI Hijack_CallProc(_In_ HANDLE ProcessHandle, _Inout_ PHIJACK_CALLPROCHEADER CallProcHeader, _In_opt_ PHIJACK_CALLPROCPARAM Params, DWORD Timeout);
ProcessHandle:目標進程句柄。CallProcHeader:指向HIJACK_CALLPROCHEADER結構體。成員含義上文已說明,Procedure(目標函數地址)、CallConvention(調用約定)、ParamCount(參數數量)為入參,RetValue(返回值)、LastError、LastStatus、ExceptionCode為出參,共用一個結構體。Params:指向HIJACK_CALLPROCPARAM結構體數組,為目標函數的各個入參(按順序)。Timeout:等待遠端線程的超時,計以毫秒,INFINITE表示無限等待。
調用方式參考AlleyWind在其它進程空間中調用SetWindowDisplayAffinity給其它進程的窗口設置顯示掩碼實現防捕獲(AlleyWind – Operation.c,https://github.com/KNSoft/AlleyWind/blob/master/Source/AlleyWind/Operation.c):
可以看到,經過封裝,讓目標進程調用任意API已經變得很容易了,用AlleyWind讓記事本反截屏:
可以看到,AlleyWind中勾選防捕獲(Anti Capture)后,我們的遠端線程注入到記事本里,讓記事本調用SetWindowDisplayAffinity,為自己的窗口開啟了防捕獲功能。于是截圖工具一開始截圖,記事本窗口就立即消失了。
經過封裝,像上面的代碼僅需幾行便能實現。C語言編寫ShellCode ->?在目標進程中調用ShellCode并支持花式入參出參?->?遠端線程執行任意API,一路走來不容易。這樣的技術可以“借花獻佛”,也可以“借刀殺人”——讓其它進程調用API執行進行惡意操作,一切審計結果都會算到其它進程頭上,畢竟我們連DLL也沒注入。唯一能讓遠端線程留痕的,只有安裝的第三方安防軟件了,如SysMon的審計。
本文章轉載微信公眾號@看雪學苑