-
-
[原创]Windows远端线程执行任意API的设计与实现
-
发表于: 2022-7-7 23:08 9024
-
在本进程空间内我们可以做很多事,毕竟是自己的地儿。比如调用SetProcessDPIAware设置一下自己进程的DPI模式,调用GetWindowLongPtr(hWnd, GWLP_WNDPROC)获取本进程所创建窗口的窗口过程等等,但如果我们想操作其它进程就难了。虽然可以注入DLL到目标进程空间进行操作,然后再把结果反馈回来,但这样的方式比较复杂:一是涉及DLL和注入,二是涉及进程间通信。本文给出基于远端线程注入(CreateRemoteThread)的方式,实现在目标进程中执行任意API,并附源码及成品lib可供调用。
我开发AlleyWind(一个窗口管理工具)时想实现一个功能,可以使目标(任意)顶层窗口防捕获(截屏)。Win7开始为DRM(Digital Rights Management,数字版权管理)提供了SetWindowDisplayAffinity函数。通过调用该函数,可使指定的窗口无法被截屏,以保护窗口显示内容不被随意外泄(当然也可以拿来Anti一些监控软件的截屏,再也不用担心划水摸鱼被截屏监控了?)。
SetWindowDisplayAffinity也很简单,一参窗口句柄,二参显示掩码,指定为WDA_MONITOR或WDA_EXCLUDEFROMCAPTURE它就再也不会被截屏了:
看上去水到渠成,可是正如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)如下图所示:
【在目标进程中调用ShellCode】
ShellCode有了,我们将ShellCode写入目标进程,执行完毕后我们也能读取目标进程,将ShellCode返回的内容读取回来,善始善终。
实现方案NTAssassin!Hijack_ExecShellcode()函数原型如下:
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、GetProcAddress,再将获取到的目标函数地址传回来即可。
ShellCode的C语言实现可参考NTAssassin!Hijack_LoadProcAddr_InjectThread()。可以看到Hijack_LoadProcAddr_InjectThread函数是一个远端线程函数,进行了以下操作:接收入参(DLL名称、函数名称)->遍历进程DLL链表(按加载顺序)->找到第一个加载的DLL(必定是ntdll.dll)->寻址LdrLoadDll和LdrGetProcedureAddress->调用这俩获得目标函数地址->反馈。当然用kernel32的LoadLibrary和GetProcAddress也一样,看心情就好了。
这套流程封装成函数,原型如下:
ProcessHandle:目标进程句柄
LibName:DLL名称或路径
ProcName:函数名,如果为NULL则只加载DLL,不寻址函数
ProcAddr:指向一个指针变量,接收要寻址函数的地址
Timeout:等待远端线程的超时,计以毫秒。可选,若为0则不等待,INFINITE无限等待
实现源码在NTAssassin!Hijack_LoadProcAddr()中,基于上述实现。
简简单单一行,即可获取目标进程(hProc)空间中SetWindowDisplayAffinity函数地址,返回于pfnSetWindowDisplayAffinity中。
函数在目标进程中的地址已经得到,那么我们如何调用它呢?
如果我们创建一个CREATE_SUSPENDED的远端线程,然后修改线程上下文(SetThreadContext),PC(EIP/RIP)指向目标函数,根据函数调用约定构造入参,然后执行……似乎可行,但“善始”容易“善后”难。我们只能获取到函数的返回值(也就是远端线程的退出码),获取不到LastError。
赞赏
- [原创]浅探内联挂钩的水有多深 7197
- [原创]为未公开API生成Lib并与MSBuild集成(一) 17801
- [原创]Windows远端线程执行任意API的设计与实现 9025
- [讨论]如今大家都用啥PE查看器/编辑器? 7822
- [原创]Win32应用程序DPI适配的设计与实现 14597