0x1 前言
本文主要通过逆向分析的方法分析PsExec的技术实现和有关PsExec和类PsExec工具的检测方法,当然其中也会掺杂一些乱七八糟的点,都是学习PsExec的一些总结。本文行文仓促,如有错误,请各位积极指正。
0x2 什么是PsExec
在微软文档中,PsExec 是一种轻量级的 telnet 替代品,可让您在其他系统上执行进程,并与控制台应用程序完全交互,而无需手动安装客户端软件。国内也有很多大佬写过PsExec原理分析的文章,RcoIl通过分析数据包的方式为我们介绍了PsExec实现的原理。具体可以参考http://rcoil.me/2019/08/【知识回顾】深入了解%20PsExec/
RcoIl通过分析流量,大致将PsExec的执行过程分为3个部分,而PSEXESVC 服务充当一个重定向器(包装器)。它在远程系统上运行指定的可执行文件
- 将 PSEXESVC.exe 上传到 ADMIN$ (指向 /admin$/system32/PSEXESVC.EXE)共享文件夹内
- 远程创建用于运行 PSEXESVC.exe 的服务
- 远程启动服务
总结一下,就是PsExec通过ADMIN$将一个PSEXESVC文件上传到目标机器,然后通过命名管道的机制,将攻击者输入的命令重定向执行被攻击者机器的文件。但是实际上我还是不知道PsExec是怎么通过ADMIN$进行上传文件的,也不知道怎么通过管道机制使被攻击者机器执行命令的。
0x3 如何传递PsExecSvr
PsExec是Mark Russinovich编写的 Sysinternals Suite中的工具,可以在https://docs.microsoft.com/en-us/sysinternals/downloads/psexec处下载。
除去对参数,和运行环境做一些校验外,直接定位到PsExec最为关键的代码sub_7040。
在sub_7040,首先会构造远程文件的地址,然后将服务名,服务文件路径,用户名,密码等相关信息作为参数传入sub_41D0。而sub_41D0这个函数正是投送PSEXESVC.exe的原因所在。
首先,sub_41D0会将参数重新赋值到局部变量,这是很常见的操作,这些变量分别是
- filepath_svcpe:即PSEXESVC.exe文件的路径
- lpDisplayName:即服务的DisplayName,友好名称
- psz_PSEXESVC:就是那串字符串,表示文件名(一般为PSEXESVC.exe)
- RemoteComputerName:远程主机名
- lpUserName:用户名
- lpPassWord:密码
- lpServiceName:服务名
接着判断远程主机名是否就是本地主机名,如果是本地主机的话,就会通过gethostname,gethostbyname,inet_ntoa等函数转化为点分十进制地址,然后进行下一步的链接。当然,如果远程主机就是本地主机的话是不需要将PSEXESVC.exe直接上传到远程主机的,所以直接创建进程
如果待定的远程主机名不是本地主机名的话,执行开线程链接。
在Thread_ConnectRemoteHostBySMB2(相对地址为0x4A50)中,首先建立IPC$连接,并从资源中释放PSEXESVC.exe
当PSEXESVC.exe释放之后,创建DisplayName = “PSEXESVC”的服务
然后向远程主机创建authentication key
接着通过CopyFileW直接复制到远程主机路径,亦可通过WriteFile写入远程主机
接着创建三个命名管道用于和PSEXESVC进行通讯,分别是stdin,stdout,stderr
随后分别拉起三个线程,用于三个命名管道的数据传输(并不是这三个管道之间进行传输),第一个参数为phKey,如果有phkey则需要将数据解密在进行传输,否则的话直接进行传输即可,第二个参数管道句柄。
剩下的就是一些退出操作,比如停止及删除服务,删除文件等操作
值得注意的是,PsExec使用了ADMIN$传输PSEXESVC.exe,但是仅仅使用WNetAddConnection2W添加了一个关于IPC$的链接,并在退出之时关闭了IPC$的链接。
总结一下,其实PsExec就是通过Admin$共享将文件传输到远程主机,Admin$相当于虚拟了一个文件夹,也可以理解成Admin$是C:\Windows的符号链接。如果将Admin$理解成一个符号链接,并指向的是C:\Windows,那么我们可以在上面执行任何关于文件的操作,包括创建,写入,删除文件,这些都是可以通过Windows提供的API实现。Admin$本质其实是利用SMB协议实现的,当我去链接windowx xp的机器的时候,可以看到使用的是SMB1的协议,而当我链接windows7的时候使用的却是SMB2的协议。
有师傅也注意到了,为什么使用Admin$上传文件,但是为什么不需要使用WNetAddConnection2W添加一个Admin$链接。这也是我在分析时候的一个困惑,于是,我做了以下实验。
首先,使用net use \192.168.80.128\ipc$ “password” /user:”domain/username”建立一个远程连接。
然后上传一份文件上去,可以看见文件以及上传成功。
|
|
接着,我删除上述链接,并创建一个Admin$的远程链接,然后在上传一份bbb.png的文件,可以发现无论ipc$还是admin$都是可以上传成功的。我觉得ipc$亦或是admin$在此处的作用都是为了验证用户名和密码是否有效,验证完成之后,可以直接通过Admin$上传文件。这也是有的文章上面说可以先建立一个ipc$链接,然后在执行PsExec的原因所在了。
除此以外,为什么一定是要利用创建服务的方式去实现呢,实现驻留的方法也有很多,但为什么一定是服务呢,我在这篇文章中找到了原因,当使用Admin$共享的时候,如果要执行复制到远程主机的文件的时候,这些文件之一必须是服务。
0x4 如何执行命令
传递和返回结果的原理就很简单了,本质就如很多文章所说的,通过命名管道实现从本地主机到远程主机发送和接收数据。具体是这样的。
首先分别创建stdin,stdout,stderr三个管道,并连接这三个命名管道用于接收和传递数据!
mark
然后传递一些参数进sub_67B0中,这些参数分别是:
- phKey,
- &Msg,
- hPipe_Service,
- hNamedPipe_stdin,
- hNamedPipe_stdout,
- hNamedPipe_stderr,
- hEvent,
- hHandle
首先phKey是表示秘钥句柄,其可能是通过sub_404420中的CryptDeriveKey,或者是sub_4059C0中的CryptImportKey函数产生。
了解&Msg参数,其实是需要了解第三个参数hPipe_Service。hPipe_Service其实是服务于服务本身的一个管道,PsExec一共会创建四个管道,其中一个用于服务自身,另外三个管道用于重定向。因为这个管道是以模块名为管道名称的,所以这个管道是服务于自身的一个管道。
接着看Msg就很好理解了,在sub_404720中,第二个参数是服务本身的管道,显然,这是从管道中读取相关信息。
接下来就是三个用于转发的管道,和事件句柄和线程句柄,和本文并没有多少关系。
在sub_405AF0函数中,创建三个普通的管道,这三个管道并不是命名管道,作用是用于向进程发送数据,以及接受返回结果的作用。可以看到StartupInfo的dwFlags是USESHOWINDOW & USESTDHANDLES。USESTDHANDLES表示着需要使用进程的三个管道。
接着拉起三个线程,用于从普通管道读取数据,然后将数据写回到命名管道中,完成数据的传递。
最后创建进程,后续三个线程监听管道的数据即可。
0x5 如何检测PsExec
在前面,我们了解到PsExec通过SMB协议将PSEXESVC.exe传递到远程主机,并将其创建为一个服务,然后通过创建四个命名管道进行数据通信,然后通过CreateProcessAsUser创建带有管道的进程来监听或者发送命令。所以综上,我们可以通过三个维度来检测是否是PsExec程序。
- 创建的子进程是否存在管道
- 父进程也就是PSEXESVC.exe,是否是服务
- 父进程是否存在命名管道
经过测试,对于类似于PsExec一样是可以检测的。
根据之前的分析,PsExec最终会调用CreateProcessAsUserW拉起进程,并在StartupInfo设置重定向管道。所以,我们可以在通过Hook CreateProcessAsUserW函数或者类似函数CreateProcess,判断StartUpinfo是否存在重定向管道。
通过分析CreateProcessAsUserW函数和CreateProcess函数发现,其最终会调用CreateProcessInternalW函数。并发现,其最终会调用Nt层的函数NtCreateUserProcess实现进程创建。
因为不仅要考虑PsExec,还要考虑其他类似于PsExec的工具,我打算在Nt层进行Hook。所以,我决定将NtCreateUserProcess作为我的目标
接下来就是确定参数StartUpInfo这个参数是如何传递给NtCreateUserProcess的,通过分析lpStartupInfo会最终传递到BasepCreateProcessParameters函数处理,然后返回。然后再将返回的ProcessParameters作为参数传递给NtCreateUserProcess。
在BasepCreateProcessParameters中,发现其就是在StartupInfo做了校验和复制。
对于了解windows内核的师傅,肯定了解其中的运行机制,也了解ProcessParameters结构的内容
因为,我需要对系统全局的进程创建进行Hook,所以我决定采用SSDT Hook来达成这一目的,最重要的原因是我之前写过类似的Demo,可以直接使用。当然,使用回调一样是可以解决问题的,关于SSDTHook原理和编程,网络上也有很多讲解。当Hook NtCreateUserProcess之后,继而检查ProcessParameters->StandardInput,ProcessParameters->StandardOutput,ProcessParameters->StandardError是否为空即可。
相对于PsExecSvc.exe,NtCreateUserProcess创建的进程是其子进程,所以PsExecSvc.exe算是NtCreateUserProcess创建的进程的父进程。NtCreateUserProcess函数原型如下,很显然,第一个参数是创建进程的ProcessHandle。所以,我们需要函数返回的ProcessHandle获取其父进程的进程数据(Pid或者Handle)
可以通过 ZwQueryInformationProcess 获取父进程Pid,具体可以这样做。
接下来,就可以判断返回的Pid的进程是否是服务,这一步,网上也有现成的代码。具体来说,就是通过EnumServicesStatusEx函数获取整个服务列表的LPENUM_SERVICE_STATUS_PROCESS,而LPENUM_SERVICE_STATUS_PROCESS保存着对于进程的Pid。
接下来,就是判断PsExecSvc.exe是否具有命名管道,命名管道实际上就是Type为”File”的句柄,所以,只需要遍历进程的句柄表,然后检查句柄类型为File,且句柄名称带有NamedPipe的,就是存在命名管道。可以使用Process Explorer来搜索命名管道。
|
|
只需要将获取的Pid传到R3,然后检查服务和命名管道符合这三个维度就可以基本判断其可能是PsExec类似软件。