首页
社区
课程
招聘
[原创]Windows远端线程执行任意API的设计与实现
发表于: 2022-7-7 23:08 9024

[原创]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负责寻址并调用LoadLibraryGetProcAddress,再将获取到的目标函数地址传回来即可。

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。


[招生]科锐逆向工程师培训(2024年11月15日实地,远程教学同时开班, 第51期)

收藏
免费 8
支持
分享
最新回复 (0)
游客
登录 | 注册 方可回帖
返回
//