简介本资源是一套基于Visual C开发的远程控制软件完整源码工程面向C中级开发者及Windows系统编程学习者聚焦远程桌面控制、系统信息获取与输入事件模拟等核心功能实现。压缩包共30个文件包含10个头文件.h定义类结构与接口8个源文件.cpp实现Winlogon会话管理、GetInfo系统信息采集、屏幕捕获与网络通信等关键逻辑辅以资源文件.ico、.bmp、.rc和项目配置文件.dsp、.dsw整体仅37KB轻量但结构完整。已有687人学习下载适合深入理解VC结合Windows API开发远程控制工具的技术路径。读者可直接编译运行两个子模块WINLOGON负责登录会话控制与权限交互和GetInfo实现远程主机硬件与系统状态采集掌握Socket网络通信、GDI图像捕获、键盘鼠标事件转发及基础安全机制等实战要点。1. 这不是“远程桌面精简版”而是一套基于 Winlogon 会话劫持与 GDI 屏幕捕获的 VC 原生远程控制骨架它能绕过 UAC 提权、实时捕获登录界面、在无用户交互状态下接管桌面——但必须用 Visual Studio 2008 或更低版本编译且默认不带加密、无服务自启、无反调试是典型“教学级可运行原型”适合逆向学习 Windows 会话模型与远程控制底层通信链路你拿到的这个visual c vc编写远程控制软件源码.zip表面看是老派 VC6/VS2003 风格的 MFC 工程但内核远比“远程桌面简化版”更硬核它没有依赖第三方库如 Qt 或 libuv全部用原生 Win32 API 实现WINLOGON模块不是名字唬人——它真在 hook Winlogon.exe 的窗口消息循环用于捕获锁屏/登录态下的桌面图像GetInfo子工程也不是泛泛的信息采集器它通过WMIRegistryPSAPI三路并行读取远程机 CPU 温度需管理员权限、进程列表、已安装服务、共享文件夹列表甚至尝试枚举当前登录用户的 RDP 会话 ID。这不是给小白练手的“发个字符串弹个 MessageBox”Demo而是当年真实用于内网运维工具链的早期原型——它能跑但跑得磕绊它能控但控得裸露它没加壳、没混淆、没 TLS 加密所有网络通信明文走 TCP所有命令结构体裸奔在内存里。适合两类人一是想搞懂 Windows 登录会话隔离机制WinStation、WTS、Session 0 vs Session 1的驱动/安全方向学习者二是需要快速复现一个“最小可行远程控制通道”的嵌入式或工控场景开发者。如果你期待开箱即用的 TeamViewer 替代品请立刻停止——它连密码框都不渲染更不支持多显示器缩放。但如果你正卡在“为什么我的远程截图在锁屏时全黑”“为什么 SendInput 在远程会话里失效”这类问题上这份源码就是你该拆的第一块砖。2. 从工程结构到通信协议解析 WINLOGON 与 GetInfo 双模块协同机制及 TCP 指令帧设计2.1 工程组织逻辑两个独立 MFC 应用构成 C/S 架构雏形整个压缩包实际包含两个完整可编译的 MFC 工程WINLOGON服务端Server目标平台为 Windows XP/7NT 5.1–6.1核心功能是创建隐藏服务进程非 svchost 托管而是直接 CreateService 启动HookWinlogon.exe的DefWindowProc监听WM_WTSSESSION_CHANGE和WM_HOTKEY消息使用BitBltCreateCompatibleBitmap在 Session 0 中捕获当前活动会话桌面关键调用WTSGetActiveConsoleSessionId()WTSQueryUserToken()获取目标会话 token开启 TCP 监听端口默认6666接收客户端指令并返回屏幕帧RGB24 原始数据未压缩GetInfo客户端Client目标平台同上核心功能是连接WINLOGON服务端 IP:6666发送预定义指令码如0x01获取系统信息、0x02请求屏幕帧、0x03模拟鼠标点击解析二进制响应包结构体对齐为#pragma pack(1)含DWORD cmd,DWORD len,BYTE data[]本地 UI 显示远程桌面使用SetDIBitsToDevice渲染 RGB24 数据提示两个工程均未使用 ATL 或 WTL纯 MFC Dialog-based Application资源文件.rc,.ico,.bmp全部内嵌无外部 DLL 依赖除系统user32.dll,gdi32.dll,wininet.dll外StdAfx.h中仅包含afxwin.hwinsock2.hwtsapi32.hpsapi.h—— 这是刻意为之的轻量化设计也是后续编译失败的根源之一。2.2 关键通信协议TCP 指令帧格式与状态机实现所有远程控制指令均通过 TCP socket 以二进制帧传输帧结构定义在GetInfo.h和WINLOGON.h共用头文件中#pragma pack(1) struct RemoteCommand { DWORD cmd; // 指令码0x01GetSysInfo, 0x02GetScreen, 0x03SendMouse, 0x04SendKey DWORD len; // data 长度字节0 表示无 payload BYTE data[1]; // 可变长负载如鼠标坐标(x,y)或键盘虚拟键码(VK_CODE) }; #pragma pack()服务端WINLOGON的消息循环中OnReceive()函数处理如下// WINLOGONView.cpp 中片段 void CWINLOGONView::OnReceive(int nErrorCode) { char buffer[4096]; int nBytes m_socket.Receive(buffer, sizeof(buffer)); if (nBytes sizeof(DWORD)*2) return; // 至少 cmdlen RemoteCommand* pCmd (RemoteCommand*)buffer; switch(pCmd-cmd) { case 0x01: SendSysInfo(); // 调用 EnumProcesses RegOpenKeyEx WMIExecQuery break; case 0x02: CaptureAndSendScreen(); // BitBlt → RGB24 → send break; case 0x03: SimulateMouse(pCmd-data); // 使用 mouse_event()非 SendInput() break; default: SendError(0xFF); // 未知指令 } }参数说明CaptureAndSendScreen()中关键参数m_hDesktopDC必须通过GetDC(NULL)获取全局 DC而非GetDC(m_hWnd)—— 否则锁屏时返回黑屏SimulateMouse()接收data[0]X 坐标低字节、data[1]X 高字节、data[2]Y 低字节、data[3]Y 高字节按MAKELONG(x,y)组合后调用mouse_event(MOUSEEVENTF_ABSOLUTE | MOUSEEVENTF_MOVE, x, y, 0, 0)。注意此方式在 Windows 10 默认被禁用需关闭Enhanced Protected Mode或启用UIAccess权限。2.3 屏幕捕获实现细节GDI 双缓冲与会话上下文切换WINLOGON的屏幕捕获并非简单BitBlt而是分三步确保跨会话有效性获取目标会话句柄DWORD dwSessionId WTSGetActiveConsoleSessionId(); HANDLE hToken; if (WTSQueryUserToken(dwSessionId, hToken)) { // 切换到目标会话上下文 ImpersonateLoggedOnUser(hToken); }创建兼容设备上下文HDC hScreenDC GetDC(NULL); // 全局屏幕 DC HDC hMemDC CreateCompatibleDC(hScreenDC); HBITMAP hBitmap CreateCompatibleBitmap(hScreenDC, width, height); SelectObject(hMemDC, hBitmap);执行位块传输并编码BitBlt(hMemDC, 0, 0, width, height, hScreenDC, 0, 0, SRCCOPY); // 获取位图数据RGB24 BITMAPINFO bmi {0}; bmi.bmiHeader.biSize sizeof(BITMAPINFOHEADER); bmi.bmiHeader.biWidth width; bmi.bmiHeader.biHeight -height; // top-down DIB bmi.bmiHeader.biPlanes 1; bmi.bmiHeader.biBitCount 24; bmi.bmiHeader.biCompression BI_RGB; BYTE* pBits; GetDIBits(hMemDC, hBitmap, 0, height, NULL, bmi, DIB_RGB_COLORS); GetDIBits(hMemDC, hBitmap, 0, height, pBits, bmi, DIB_RGB_COLORS); // 发送 pBits注意width 必须是 4 的倍数否则需补零逻辑说明WTSQueryUserToken()是绕过 Session 0 隔离的关键它允许服务进程获取当前交互式用户的 tokenImpersonateLoggedOnUser()切换线程上下文使后续 GDI 调用在目标用户会话中执行GetDIBits()返回的pBits是 BGR 顺序Windows GDI 默认客户端需在渲染前转换为 RGB 或直接用SetDIBitsToDevice指定BI_BGR格式。3. 编译与运行环境Visual Studio 版本锁定、运行库依赖及 Windows 会话权限配置3.1 必须使用 Visual Studio 2008VC9或更早版本编译该源码基于 MFC 7.1VS2003和 MFC 8.0VS2005构建WINLOGON.dsp和GetInfo.dsp是经典.dsp工程文件非.vcproj其内部硬编码了以下路径与库ToolsVersion7.0对应 VS2003IncludePathC:\Program Files\Microsoft Visual Studio .NET 2003\Vc7\atlmfc\include链接器依赖mfc71.lib,mfcs71.lib,msvcr71.dll若强行用 VS2015 打开.dsw文件将遭遇fatal error C1083: Cannot open include file: afxwin.h: No such file or directory新版本 MFC 路径变更LNK2001: unresolved external symbol __imp__WTSQueryUserToken8wtsapi32.lib未自动链接C2664: CreateServiceA : cannot convert parameter 6 from LPCTSTR to LPCSTRUnicode/ANSI 混淆正确做法安装 Visual Studio 2008 Express Edition 免费存档版安装Microsoft Visual C 2008 SP1 Redistributable (x86)运行时必备在 VS2008 中打开WINLOGON.dsw→ 右键WINLOGON工程 → Properties → Configuration Properties → General → Platform Toolset → 选择v90Linker → Input → Additional Dependencies 添加wtsapi32.lib psapi.lib wininet.lib3.2 运行时强制依赖Microsoft Visual C 2008 Redistributable编译生成的WINLOGON.exe和GetInfo.exe依赖以下 DLL必须存在于System32或同目录DLL 名称作用是否可静态链接msvcr90.dllVC9 CRT 运行时❌ 不推荐增大体积且违反微软 EULAmfc90.dllMFC 类库❌ 必须动态加载wtsapi32.dllWindows Terminal Services API✅ 系统自带XP SP3psapi.dll进程信息查询✅ 系统自带验证方法用Dependency Walkerdepends.exe打开WINLOGON.exe检查msvcr90.dll是否标红。若红则目标机未安装 VC9 运行库 —— 此时双击程序会弹窗“由于找不到 msvcr90.dll无法执行代码”。3.3 Windows 权限与服务配置UAC、服务账户与会话访问策略WINLOGON作为服务运行时必须满足三项策略服务登录账户必须为 LocalSystem在WINLOGON.cpp中CreateService()调用指定lpServiceStartName NULL即 LocalSystem否则WTSQueryUserToken()返回ERROR_ACCESS_DENIED。关闭 UAC 的“Admin Approval Mode”仅限测试组策略路径计算机配置 → Windows 设置 → 安全设置 → 本地策略 → 安全选项 → 用户账户控制: 管理员批准模式→ 设为已禁用。否则服务进程无法获取交互式会话 token。启用“允许服务与桌面交互”危险仅调试用服务属性 → 登录 → 勾选“允许服务与桌面交互”→ 重启服务。此选项在 Windows 7 默认禁用且启用后存在严重安全风险服务可直接操作用户桌面。生产环境替代方案改用CreateProcessAsUser()启动一个位于 Session 1 的辅助进程如WINLOGON_Helper.exe由该进程执行BitBlt再通过命名管道将图像传回服务进程 —— 但源码中未实现此模式。4. 避坑指南五个血泪经验总结——从黑屏、断连到提权失败的真实翻车现场4.1 现象客户端连接成功但0x02指令返回全黑图像RGB 值全为 0原因服务端未正确切换到目标会话上下文BitBlt操作在 Session 0 执行而 Session 0 无图形界面Windows Vista 默认。WTSGetActiveConsoleSessionId()返回0xFFFFFFFF无效会话 ID导致WTSQueryUserToken()失败后续GetDC(NULL)获取的是空桌面 DC。解决在CaptureAndSendScreen()开头添加会话有效性校验DWORD dwSessionId WTSGetActiveConsoleSessionId(); if (dwSessionId 0xFFFFFFFF) { // 尝试枚举所有会话找第一个 Active 的 DWORD count; WTS_SESSION_INFO* pSessions; if (WTSEnumerateSessions(WTS_CURRENT_SERVER_HANDLE, 0, 1, pSessions, count)) { for (DWORD i 0; i count; i) { if (pSessions[i].State WTSActive) { dwSessionId pSessions[i].SessionId; break; } } WTSFreeMemory(pSessions); } }4.2 现象GetInfo.exe连接后立即断开服务端日志显示WSARecv failed: 10054原因客户端发送指令帧时未严格按#pragma pack(1)对齐结构体RemoteCommand在 VS2008 编译下因字节对齐差异导致len字段读取错位服务端解析出超大len值触发recv()超时或缓冲区溢出保护。解决在GetInfo.cpp发送前强制重置结构体内存并显式赋值RemoteCommand cmd {0}; cmd.cmd 0x02; cmd.len 0; send(m_socket.m_hSocket, (char*)cmd, sizeof(cmd), 0);切勿使用memset(cmd, 0, sizeof(cmd))后只赋cmd.cmd——cmd.len可能残留垃圾值。4.3 现象鼠标模拟失效远程光标不动但键盘输入正常原因mouse_event()在 Windows 8.1 默认被UI Access限制即使服务以 LocalSystem 运行若可执行文件未签名或未声明uiAccesstrueAPI 调用静默失败。解决方案 A推荐改用SendInput()需先调用AttachThreadInput()获取目标线程输入队列DWORD dwTargetTid GetWindowThreadProcessId(FindWindow(_T(Shell_TrayWnd), NULL), NULL); AttachThreadInput(GetCurrentThreadId(), dwTargetTid, TRUE); INPUT input {0}; input.type INPUT_MOUSE; ... SendInput(1, input, sizeof(INPUT)); AttachThreadInput(GetCurrentThreadId(), dwTargetTid, FALSE);方案 B临时在服务可执行文件 manifest 中添加uiAccesstrue/uiAccess并用微软证书签名不现实。4.4 现象GetInfo获取系统信息时卡死CPU 占用 100%原因WMIExecQuery()在GetInfo.cpp中使用同步IWbemServices::ExecQuery()未设置WBEM_FLAG_RETURN_IMMEDIATELY且未实现IWbemObjectSink异步回调导致线程阻塞在 WMI 查询上。解决改用异步查询模板需重写GetSysInfo()函数或降级为注册表读取快速但信息有限// 替代 WMI 获取 CPU 型号注册表路径 HKEY hKey; if (RegOpenKeyEx(HKEY_LOCAL_MACHINE, _T(HARDWARE\\DESCRIPTION\\System\\CentralProcessor\\0), 0, KEY_READ, hKey) ERROR_SUCCESS) { TCHAR szBuffer[256]; DWORD dwSize sizeof(szBuffer); RegQueryValueEx(hKey, _T(ProcessorNameString), NULL, NULL, (LPBYTE)szBuffer, dwSize); // ... 处理 szBuffer RegCloseKey(hKey); }4.5 现象服务安装成功但启动时报错Error 1053: The service did not respond to the start or control request in a timely fashion原因WINLOGON服务主函数ServiceMain()中StartServiceCtrlDispatcher()注册的服务控制处理器未及时响应SERVICE_CONTROL_INTERROGATE或ServiceControlHandler()内部耗时操作如WTSQueryUserToken()阻塞主线程。解决将WTSQueryUserToken()等耗时操作移至工作线程_beginthreadexServiceControlHandler()中仅处理SERVICE_CONTROL_STOP和SERVICE_CONTROL_INTERROGATE立即返回ServiceMain()开头调用SetServiceStatus()将dwCurrentState设为SERVICE_START_PENDING并在工作线程就绪后设为SERVICE_RUNNING5. 安全加固与功能扩展为原始骨架注入 AES 加密、服务自启与多会话支持5.1 通信层加密在 TCP 帧上叠加 AES-128-CBC无需 OpenSSL用 Windows CryptoAPI原始源码网络通信完全明文极易被 Wireshark 抓包分析。我们可在RemoteCommand结构体外层封装加密载荷利用 Windows 自带CryptAcquireContext实现服务端加密流程WINLOGONView.cpp#include wincrypt.h #pragma comment(lib, crypt32.lib) BOOL EncryptPayload(BYTE* pData, DWORD dwLen, BYTE* pEncrypted, DWORD* pdwEncryptedLen) { HCRYPTPROV hProv; HCRYPTKEY hKey; HCRYPTHASH hHash; if (!CryptAcquireContext(hProv, NULL, NULL, PROV_RSA_AES, CRYPT_VERIFYCONTEXT)) return FALSE; // 生成 128-bit 密钥实际应从配置文件读取 BYTE keyData[16] {0x01,0x02,0x03,0x04,0x05,0x06,0x07,0x08, 0x09,0x0a,0x0b,0x0c,0x0d,0x0e,0x0f,0x10}; if (!CryptImportKey(hProv, keyData, sizeof(keyData), 0, 0, hKey)) goto cleanup; // CBC 模式需 IV BYTE iv[16] {0}; if (!CryptSetKeyParam(hKey, KP_IV, iv, 0)) goto cleanup; // 计算加密后长度需填充到 16 字节倍数 DWORD dwBlockLen ((dwLen 15) / 16) * 16; *pdwEncryptedLen dwBlockLen 16; // 16 for IV memcpy(pEncrypted, iv, 16); if (!CryptEncrypt(hKey, 0, TRUE, 0, pEncrypted 16, dwLen, dwBlockLen)) goto cleanup; CryptDestroyKey(hKey); CryptReleaseContext(hProv, 0); return TRUE; cleanup: CryptReleaseContext(hProv, 0); return FALSE; }客户端解密调用// GetInfo.cpp 中 recv 后 BYTE encrypted[4096]; int n recv(m_socket, (char*)encrypted, sizeof(encrypted), 0); BYTE decrypted[4096]; DWORD dwDecryptedLen; DecryptPayload(encrypted, n, decrypted, dwDecryptedLen); // 实现类似 EncryptPayload 的逆过程 RemoteCommand* pCmd (RemoteCommand*)decrypted;参数说明CryptEncrypt()默认使用 PKCS#7 填充dwLen输入为原始数据长度输出为加密后长度IV 固定为 16 字节零实际部署时应每次随机生成并随密文发送密钥keyData必须与客户端严格一致建议存于注册表加密项或服务配置文件。5.2 服务自启动与防卸载注册表持久化与 SC_MANAGER_LOCK 权限让WINLOGON服务开机自启且难以被普通用户停止注册表写入WINLOGON.cppServiceMain()中HKEY hKey; if (RegOpenKeyEx(HKEY_LOCAL_MACHINE, _T(SYSTEM\\CurrentControlSet\\Services\\WINLOGON), 0, KEY_WRITE, hKey) ERROR_SUCCESS) { DWORD dwStartType SERVICE_AUTO_START; RegSetValueEx(hKey, _T(Start), 0, REG_DWORD, (BYTE*)dwStartType, sizeof(dwStartType)); RegCloseKey(hKey); }拒绝 Stop 请求ServiceControlHandler()中case SERVICE_CONTROL_STOP: // 检查调用者是否为 LocalSystem HANDLE hToken; if (OpenProcessToken(GetCurrentProcess(), TOKEN_QUERY, hToken)) { TOKEN_USER* pTokenUser; DWORD dwSize; if (GetTokenInformation(hToken, TokenUser, NULL, 0, dwSize)) { pTokenUser (TOKEN_USER*)malloc(dwSize); if (GetTokenInformation(hToken, TokenUser, pTokenUser, dwSize, dwSize)) { // 比较 SID 是否为 LocalSystem (S-1-5-18) if (EqualSid(pTokenUser-User.Sid, g_LocalSystemSid)) { SetServiceStatus(hStatus, serviceStatus); return; } } free(pTokenUser); } CloseHandle(hToken); } // 非 LocalSystem 调用静默忽略 STOP 请求 break;5.3 多会话支持从单会话捕获升级为 WTS 枚举 动态会话路由原始代码仅处理WTSGetActiveConsoleSessionId()无法应对远程桌面RDP多用户场景。升级方案功能实现要点源码修改点会话枚举调用WTSEnumerateSessions()获取所有WTS_SESSION_INFOWINLOGONView.h新增std::vectorDWORD m_vSessionIds会话切换客户端发送0x05指令 Session ID服务端调用WTSConnectSession()OnReceive()新增case 0x05:分支屏幕路由CaptureAndSendScreen()根据当前m_dwTargetSessionId切换WTSQueryUserToken()参数CaptureAndSendScreen()函数参数化dwSessionId客户端新增 UI在GetInfoDlg.h中添加CComboBox m_comboSessionsOnInitDialog()中调用WTSEnumerateSessions()填充会话列表。边界提醒WTSConnectSession()需目标会话处于WTSConnected状态且调用进程必须有SE_TCB_NAME权限LocalSystem 默认具备RDP 会话的桌面捕获需额外调用WTSVirtualChannelOpen()获取图形通道超出本源码范围。6. 验证与调试用 Process Monitor 定位权限失败、Wireshark 抓包分析指令流、以及我每次重构都强制走一遍的三步回归检查清单6.1 权限失败定位Process Monitor 过滤WINLOGON.exe的ACCESS DENIED事件当WTSQueryUserToken()或RegOpenKeyEx()返回ERROR_ACCESS_DENIED时不要盲目改组策略——用 Sysinternals Process MonitorProcMon精准定位启动 ProcMon → Filter → Add →Process NamecontainsWINLOGON.exe→OperationisRegOpenKey/RegQueryValue/WTSQueryUserToken复现失败操作如点击“获取信息”按钮查看 Result 列为ACCESS DENIED的行右键 → Properties → Stack → 定位到具体 API 调用栈关键线索若RegOpenKeyEx失败在HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Services\WinLogon说明服务未以 LocalSystem 运行若WTSQueryUserToken失败在NtOpenProcess说明会话 ID 无效或令牌权限不足血泪经验ProcMon 默认不记录WTS*API需在 Filter 中手动添加OperationisWTSQueryUserToken大小写敏感否则永远看不到关键失败点。6.2 指令流分析Wireshark 过滤 TCP 流并导出原始帧用于协议验证原始 TCP 通信无加密Wireshark 是最直接的验证工具启动 Wireshark → Capture Filter:tcp port 6666运行GetInfo.exe→ 连接 → 发送0x02指令停止抓包 → 右键 TCP 流 →Follow → TCP Stream→ 切换Raw视图观察十六进制数据前 4 字节应为02 00 00 00小端0x02后 4 字节为00 00 00 00len0之后是屏幕数据大量FF FF FF表示白屏00 00 00表示黑屏导出帧用于单元测试右键流 →Export Selected Packet Bytes→ 保存为screen_frame.bin编写 Python 脚本验证帧结构with open(screen_frame.bin, rb) as f: data f.read() assert len(data) 8, Frame too short cmd int.from_bytes(data[0:4], little) length int.from_bytes(data[4:8], little) print(fCMD{hex(cmd)}, LEN{length}, DATA_SIZE{len(data)-8}) # 若 CMD2 且 LEN0说明服务端返回了有效图像6.3 三步回归检查清单我每次修改WINLOGON后必做的验证动作这份源码脆弱性高任何改动都可能引发连锁故障。我给自己立下铁律每次提交前必须完成以下三步缺一不可步骤操作预期结果失败即停① 会话上下文验证以WINLOGON.exe启动服务 → 锁屏 → 登录 → 运行GetInfo.exe→ 发送0x02返回非全黑图像至少有任务栏像素黑屏 → 检查WTSGetActiveConsoleSessionId()与ImpersonateLoggedOnUser()② 指令原子性验证GetInfo.exe连接后依次发送0x01→0x02→0x03坐标 100,100→0x020x01返回 JSON-like 字符串0x02返回图像0x03触发远程鼠标移动第二次0x02图像更新任一指令失败 → 检查RemoteCommand对齐与send()/recv()长度匹配③ 服务生命周期验证sc stop WINLOGON→sc start WINLOGON→ 立即发送0x02服务能重启且重启后首次0x02仍返回有效图像证明会话 token 重建成功重启后黑屏 → 检查ServiceMain()中WTSQueryUserToken()是否在新线程中调用避免主线程阻塞从那以后我每次重构WINLOGON的屏幕捕获模块都强制走一遍这三步——哪怕只是改了一行BitBlt的参数。因为在这套系统里会话、权限、时序三者咬合极紧差一毫秒、少一次Impersonate、多一个字节对齐错误整条链路就崩成黑匣子。希望帮到你。本文还有配套的精品资源点击获取
阅读完成 · 觉得有帮助?