一
摘要
在本进程空间内我们可以做很多事,毕竟是自己的地儿。比如调用SetProcessDPIAware设置一下自己进程的DPI模式,调用GetWindowLongPtr(hWnd, GWLP_WNDPROC)获取本进程所创建窗口的窗口过程等等,但如果我们想操作其它进程就难了。虽然可以注入DLL到目标进程空间进行操作,然后再把结果反馈回来,但这样的方式比较复杂:一是涉及DLL和注入,二是涉及进程间通信。本文给出基于远端线程注入(CreateRemoteThread)的方式,实现在目标进程中执行任意API,并附源码及成品lib可供调用。
二
遇到问题的场景
我开发AlleyWind(一个窗口管理工具https://github.com/KNSoft/AlleyWind)时想实现一个功能,可以使目标(任意)顶层窗口防捕获(截屏)。Win7开始为DRM(Digital Rights Management,数字版权管理)提供了SetWindowDisplayAffinity(https://docs.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-setwindowdisplayaffinity)函数。通过调用该函数,可使指定的窗口无法被截屏,以保护窗口显示内容不被随意外泄(当然也可以拿来Anti一些监控软件的截屏,再也不用担心划水摸鱼被截屏监控了?)。
SetWindowDisplayAffinity也很简单,一参窗口句柄,二参显示掩码,指定为WDA_MONITOR或WDA_EXCLUDEFROMCAPTURE它就再也不会被截屏了:
BOOL SetWindowDisplayAffinity(
[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提供花式入参,也能接收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:目标进程句柄。
实现源码在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结构体数组,是函数的各个参数。进行以下操作:
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);
调用方式参考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的审计。
本文实现的Hijack_LoadProcAddr(获取目标进程函数地址)与Hijack_CallProc(调用目标进程函数)都是NTAssassin(https://github.com/KNSoft/NTAssassin)导出的函数,GitHub源码和lib都有,欢迎一键三连,更欢迎指正与交流。
Ratin/[email protected]
国家认证 系统架构设计师
看雪ID:Ratin
https://bbs.kanxue.com/user-home-853701.htm
# 往期推荐
3、安卓加固脱壳分享
球分享
球点赞
球在看
文章引用微信公众号"看雪学苑",如有侵权,请联系管理员删除!