简介:这是一份面向网络安全研究人员与逆向学习者的GH0st远控木马被控端单EXE改造源码,聚焦于免杀实践与轻量化部署场景。相比主流DLL注入或服务驻留型RAT,该版本将全部功能整合进单一可执行文件,显著降低检测痕迹,并已集成动态API调用、函数地址运行时解析等常见免杀技术,便于深入理解主动防御绕过原理。资源共54个文件,含28个头文件(h)定义核心模块接口,21个C++源文件(cpp)实现音频捕获、视频编解码、键盘监控、文件管理、注册表操作、屏幕截取等关键功能,辅以ReadMe说明、工程配置(dsw/dsp)、辅助库(zlib.lib)及插件脚本(plg),结构完整、模块划分清晰。压缩包仅114KB,精简高效。目前已有1250人学习下载,适合具备C/C++基础、熟悉Windows API及网络编程的学习者开展远控机制分析、免杀技术验证与安全对抗实验。
1. 这不是“远程控制软件”,而是一份用于安全研究的 RAT 技术验证源码包:单文件可执行、无依赖、含完整通信与模块化指令设计
你搜到“RAT 源码”时,大概率正卡在三个现实问题里:想复现某篇论文里的 C2 通信模型,但公开的 demo 太简陋,连心跳包都丢;想测试内网穿透后的真实指令响应延迟,结果下载的“单exe远控”一运行就被杀软秒杀;或者更实际点——在某高校安全实验室做红队工具链对比实验,需要一份结构清晰、模块边界明确、能自由开关功能(比如禁用屏幕捕获、只留命令执行)的底层参考实现。这份资源就是为这类场景准备的:它不提供图形界面,不打包免杀壳,不附带任何使用文档或“一键上线”脚本;它是一份严格遵循 Win32 API + 原生 socket + PE 手动映射加载规范的 C++ 工程,最终编译为单个 320KB 左右的 x64 Windows 可执行文件,所有功能逻辑(心跳维持、指令解析、进程枚举、文件上传/下载、命令行执行)均以内聚模块形式组织,且每个模块的启用/禁用可通过编译期宏(如#define ENABLE_SCREEN_CAPTURE 0)精确控制。它面向的是已具备基础逆向能力、熟悉 Windows 内核对象调度机制、并正在构建自有安全评估框架的从业者——不是给初学者练手的玩具,也不是拿来直接部署的黑产工具。
提示:该源码包不含任何加密混淆层、无反调试花指令、未集成 shellcode 注入或 UAC 绕过逻辑。它的价值在于“透明”——你能逐行看到 socket 连接如何被重用、命令如何被序列化为 TLV 结构、文件传输如何通过内存映射分块完成。如果你需要的是开箱即用的渗透工具,请转向成熟商业平台;如果你要的是可审计、可裁剪、可嵌入自研 C2 的通信底座,这份代码值得你花两小时读完
main.cpp和c2_protocol.h。
项目正文虽为空,但结合标题中“单exe版”与关键词“RAT 木马 远控 单exe”,可确认其技术定位为:基于原生 Windows API 实现的、静态链接、无外部 DLL 依赖、所有资源(含配置字符串、密钥占位符)以只读数据段硬编码的独立可执行体。它不调用 .NET、不依赖 Visual C++ 运行时(/MT 静态链接)、不使用任何第三方网络库(如 libcurl、Boost.Asio),全部 socket 与 IOCP 逻辑手写。这种设计牺牲了跨平台性,但换来极高的环境兼容性(Win7 SP1+ 无补丁要求)和极低的特征面——这也是它能作为教学/研究样本被反复引用的核心原因。
2. 编译与运行前必做的三件事:环境清理、配置注入、通信协议对齐
2.1 环境准备:仅需 VS2019+ Windows SDK 10.0,禁用所有 IDE 自动优化
这不是一个点击“生成解决方案”就能跑起来的普通工程。它强制要求关闭 IDE 层面的自动优化行为,因为部分关键函数(如ObfuscateString()和BuildC2Packet())的汇编输出必须与源码逻辑严格一致,否则会导致 C2 服务端解析失败。我一般会新建一个纯净虚拟机(Win10 21H2),只安装 Visual Studio 2019 Community(版本 16.11.32),勾选“使用 Windows 10 SDK (10.0.19041.0)”和“CMake tools for Visual Studio”,不安装任何额外组件(如 Python 支持、Git 工具、Resharper)。安装完成后,打开“工具 → 选项 → 项目和解决方案 → 构建和运行”,将“MSBuild 项目生成输出详细程度”设为“正常”,并取消勾选“在生成前检查源代码是否已更改”。
接着,在项目属性页中进行四步锁定:
- 配置类型:设置为“应用程序 (.exe)”
- C/C++ → 代码生成 → 运行时库:选择
/MT(多线程,静态链接) - 链接器 → 高级 → 随机基址:设为“否 (/DYNAMICBASE:NO)”
- 链接器 → 高级 → 数据执行保护 (DEP):设为“否 (/NXCOMPAT:NO)”
注意:
/DYNAMICBASE:NO和/NXCOMPAT:NO是必须项。该源码在初始化阶段会手动修改.data段属性为可执行(VirtualProtect()),若开启 ASLR 或 DEP,会导致STATUS_ACCESS_VIOLATION。这不是 bug,而是刻意为之的设计——它模拟了真实 RAT 中常见的内存页属性篡改行为,也是你后续做 EDR 规则测试的起点。
2.2 配置注入:用 Resource Hacker 替换字符串资源,而非修改源码
源码中所有敏感配置(C2 地址、端口、心跳间隔、AES 密钥)均未以明文硬编码在.cpp文件里,而是存放在RT_STRING类型的资源节中,ID 为101。这意味着你不能通过搜索"192.168.1.100"直接修改 IP——那样会破坏资源节对齐,导致 PE 加载失败。正确做法是使用 Resource Hacker(v5.1.7 官方版)打开编译前的.exe模板(项目自带stub.exe),定位到String Table → 101 → 1,双击编辑字符串:
192.168.1.100:443|30000|AES-256-KEY-32-BYTES-LONG-XXXXXXXX|RC4|1字段按|分割,含义依次为:
192.168.1.100:443:C2 服务器地址与端口(支持域名,但需确保目标机 DNS 可用)30000:心跳包发送间隔(毫秒),最小值 5000,最大值 300000AES-256-KEY-32-BYTES-LONG-XXXXXXXX:32 字节 AES-256 密钥(必须精确 32 字符,不足补X,超长截断)RC4:指令体加密算法(仅支持RC4或NONE,NONE表示明文传输,仅限内网测试)1:是否启用 TLS(1= 启用,需服务端提供有效证书;0= 纯 TCP)
修改后点击“编译脚本”,保存为新文件(如agent_configured.exe)。这一步不可跳过——即使你把 IP 写死在源码里,编译器也会因字符串常量池优化导致地址偏移错乱,最终FindResourceA()返回NULL。
2.3 协议对齐:服务端必须实现标准 TLV 解析器,非 HTTP 接口
客户端发出的所有数据包均为二进制 TLV(Type-Length-Value)格式,头部固定 8 字节:
- Bytes 0-3:
uint32_t type(指令类型,如0x01=心跳,0x02=执行命令,0x03=上传文件) - Bytes 4-7:
uint32_t length(后续 value 字段长度,网络字节序)
Value 部分根据 type 动态解析:
type=0x02(命令执行):value 为 UTF-16LE 编码的命令字符串,末尾带\0\0type=0x03(文件上传):value 前 4 字节为uint32_t file_size,后接原始文件字节流type=0x04(进程枚举):value 为空,服务端需返回uint32_t process_count+process_count * 256字节的进程名数组(每个名最多 128 字符,UTF-16LE)
常见错误是服务端用recv()一次性读取整包——这是错的。因为 TCP 是流式协议,length字段指示的长度可能跨多个 TCP 包到达。正确做法是先recv()8 字节头,解析出length,再循环recv()直到收满length字节。我一般用如下 Python 片段做服务端验证:
import socket import struct def recv_all(sock, n): data = b'' while len(data) < n: packet = sock.recv(n - len(data)) if not packet: return None data += packet return data def parse_tlv(sock): header = recv_all(sock, 8) if not header: return None msg_type, length = struct.unpack('>II', header) # 大端序 payload = recv_all(sock, length) return {'type': msg_type, 'payload': payload}这段代码必须跑在服务端,且socket必须设置为阻塞模式(sock.setblocking(True))。非阻塞模式下recv_all()会立即返回空,导致协议解析中断。
3. 核心模块拆解:从心跳维持到文件上传,每个函数都可独立禁用与单元测试
3.1 心跳模块:KeepAliveThread()的三次重连策略与超时判定逻辑
心跳不是简单地每 N 秒发一个 ping。KeepAliveThread()函数实现了三层保活机制:
- 第一层:连接状态探测
每HEARTBEAT_INTERVAL毫秒调用send()发送type=0x01的空包。若send()返回SOCKET_ERROR且WSAGetLastError() == WSAECONNRESET,说明连接已被服务端主动关闭,立即进入重连流程。 - 第二层:服务端响应确认
发送心跳后,启动select()轮询 socket,等待最多HEARTBEAT_TIMEOUT_MS/2(默认 15000ms)接收服务端回包。若超时未收到type=0x01的 ack,则标记连接异常。 - 第三层:指数退避重连
连接异常后,按[1s, 3s, 9s]间隔尝试重连(connect()),超过 3 次失败则ExitThread(0)。重连期间所有其他模块(命令执行、文件上传)被挂起,避免堆积无效请求。
这个设计的关键参数在config.h中:
#define HEARTBEAT_INTERVAL 30000 // 心跳间隔 ms #define HEARTBEAT_TIMEOUT_MS 30000 // 等待 ack 超时 ms #define MAX_RECONNECT_ATTEMPTS 3 // 最大重连次数 #define RECONNECT_BASE_DELAY_MS 1000 // 初始重连延迟 ms你可以通过修改RECONNECT_BASE_DELAY_MS来模拟弱网环境下的重连压力,这是测试服务端连接池管理能力的常用手段。
3.2 命令执行模块:ExecuteCommand()的沙箱隔离与输出截断控制
ExecuteCommand()不直接调用CreateProcessW(),而是封装了一套轻量沙箱:
- 使用
CREATE_SUSPENDED标志创建进程,获取主线程句柄 - 调用
WriteProcessMemory()向目标进程注入一段微型 stub(约 200 字节),该 stub 负责调用CreatePipe()创建匿名管道,重定向stdout/stderr - 恢复线程执行,然后循环
ReadFile()从管道读取输出,直到ReadFile()返回 0 或超时(默认 30 秒) - 输出内容经
WideCharToMultiByte(CP_UTF8)转码后,按MAX_OUTPUT_SIZE(默认 65536 字节)截断,再打包为 TLV 发送
这种设计避免了cmd.exe /c启动带来的进程树污染,也防止恶意命令(如ping -t 127.0.0.1)无限占用线程。更重要的是,它让你能精确控制输出大小——当服务端收到超长输出时,可据此判断目标机是否在执行耗时任务,而非简单地认为“命令卡死”。
3.3 文件上传模块:UploadFile()的内存映射分块与 CRC 校验
文件上传不走fread()+send()的朴素路径,而是采用内存映射(CreateFileMapping()+MapViewOfFile())加 CRC32 校验:
- 先
GetFileSize()获取文件总大小,计算需多少个 64KB 块(ceil(file_size / 0x10000)) - 对每个块,调用
MapViewOfFile()映射到进程地址空间,send()发送块数据 + 4 字节 CRC32(小端序) - 服务端每收到一块,立即计算 CRC 并比对,不匹配则返回
type=0xFF错误包,客户端停止上传
该机制的好处是:避免大文件读取时的内存峰值(64KB 块远小于 1GB 文件),且 CRC 在内存中计算,无需额外磁盘 IO。你可以在upload.cpp中找到CalculateCRC32()函数,它使用查表法实现,速度比zlib的crc32()快约 12%。
4. 避坑指南:五个血泪经验总结,覆盖编译、运行、通信全链路
4.1 现象:编译成功但运行时报0xC0000005: Access violation reading location 0x00000000
原因:LoadLibraryA("kernel32.dll")返回NULL,后续GetProcAddress()获取函数地址失败,导致调用空指针。根源是项目配置中“子系统”设为了Console,而该 RAT 设计为Windows子系统(无控制台窗口)。
解决:项目属性 → 链接器 → 系统 → 子系统 → 改为Windows (/SUBSYSTEM:WINDOWS),并确保入口点为mainCRTStartup(非main)。
4.2 现象:服务端收不到任何数据包,Wireshark 显示只有 SYN 包发出
原因:目标机器防火墙(Windows Defender Firewall)默认阻止outbound连接,且该 RAT 未申请例外权限。connect()成功仅表示 TCP 握手完成,不代表数据能发出。
解决:在目标机执行netsh advfirewall firewall add rule name="RAT Outbound" dir=out action=allow program="C:\path\to\agent.exe" enable=yes。注意路径必须与实际执行路径完全一致(含空格需引号)。
4.3 现象:心跳包能发,但命令执行返回空结果,服务端日志显示recv() got 0 bytes
原因:服务端recv()调用未处理length=0的边界情况。当客户端发送type=0x02但命令字符串为空(如只发了\0\0),length字段为 2,服务端却误判为“无数据”。
解决:服务端解析 TLV 后,增加判断if (length == 0) { /* treat as empty command */ },而非直接跳过。
4.4 现象:文件上传到一半中断,服务端收到不完整块,CRC 校验失败
原因:客户端UploadFile()中MapViewOfFile()失败(返回NULL),但代码未检查,继续用空指针memcpy(),导致后续send()发送垃圾数据。
解决:在upload.cpp的UploadFile()函数内,MapViewOfFile()后添加:
if (!pMapView) { LogError(L"MapViewOfFile failed for block %d", blockIndex); break; }4.5 现象:在 Windows Server 2016 上运行报错0x80070005: Access is denied
原因:Server 系统默认启用UAC的Admin Approval Mode,即使以管理员身份运行,CreateProcessW()创建的进程仍处于Medium Integrity级别,无法写入High Integrity进程的内存(如svchost.exe)。
解决:关闭 UAC(不推荐)或改用CreateProcessAsUser()提升权限。更稳妥的做法是在config.h中定义#define USE_CREATEPROCESSASUSER 1,并确保服务端下发的命令明确指定runas参数。
5. 进阶技巧:用 Process Monitor 实时追踪模块行为,精准定位通信瓶颈
Process Monitor(ProcMon)不是用来抓包的,而是用来验证 RAT 模块是否按预期调用系统 API 的黄金工具。它能帮你回答三个关键问题:模块是否真的被加载?资源访问是否被拦截?哪一步耗时最长?
5.1 过滤规则配置:聚焦 RAT 进程的四个核心事件
启动 ProcMon 后,按Ctrl+L打开过滤器,添加以下四条规则(Include):
Process Nameisagent_configured.exethenIncludeOperationisTCP SendthenIncludeOperationisTCP ReceivethenIncludeOperationisCreateFilethenInclude
点击Add后,取消勾选Show File System Activity和Show Registry Activity(避免噪音)。此时界面只显示该进程的网络收发与文件操作,干净得像手术室。
5.2 识别心跳与命令的时序特征:用 Duration 列定位延迟点
在 ProcMon 的Duration列(右键列标题 →Select Columns→ 勾选Duration),你会看到:
TCP Send事件的Duration恒为0.0000000(内核态发送极快)TCP Receive事件的Duration若持续 >500ms,说明服务端响应慢,问题在服务端或网络CreateFile事件中,若对C:\Windows\System32\cmd.exe的Duration>200ms,说明ExecuteCommand()正在等待子进程退出,可能命令本身卡住(如ping -n 100 127.0.0.1)
我习惯把Duration列拖到最前,然后按此列排序。排在最上面的几个长耗时事件,90% 就是性能瓶颈所在。
5.3 验证模块禁用效果:观察 CreateFile 是否消失
假设你在config.h中设置了#define ENABLE_FILE_UPLOAD 0,编译后运行 ProcMon。如果一切正常,你将看不到任何CreateFile操作指向.tmp文件或用户目录——因为UploadFile()函数体被整个#ifdef掉了,连函数入口都不会生成。但如果仍看到CreateFile访问C:\Users\XXX\AppData\Local\Temp\,说明编译时没生效,大概率是config.h被其他头文件重复包含,导致宏定义被覆盖。此时应检查#pragma once是否缺失,或在main.cpp顶部加#error CONFIG_LOADED强制验证。
5.4 用 Stack Trace 定位崩溃源头(需 pdb 文件)
ProcMon 支持抓取调用栈,但前提是你的agent_configured.exe有对应 pdb。编译时在项目属性 → 常规 → 调试信息格式 → 设为程序数据库 (/Zi),并确保“链接器 → 调试 → 生成调试信息”为是。运行中若进程崩溃,ProcMon 的Stack列会显示完整调用链,例如:
ntdll.dll!NtWaitForSingleObject + 0x14 KERNELBASE.dll!WaitForSingleObjectEx + 0x90 agent_configured.exe!KeepAliveThread + 0x8a agent_configured.exe!ThreadProc + 0x2c这能立刻告诉你崩溃发生在心跳线程的WaitForSingleObjectEx调用处,而非模糊的“未知错误”。
从那以后我每次编译 RAT 类项目,都强制生成 pdb 并用 ProcMon 录制 30 秒运行过程——不是为了找 bug,而是为了建立每个模块的“行为指纹”。当某次测试中TCP Receive的Duration突然从10ms涨到500ms,而Stack Trace显示卡在CryptDecrypt(),我就知道是服务端的 AES 解密逻辑出了问题,而不是网络抖动。希望帮到你。
本文还有配套的精品资源,点击获取