找回密码
立即注册
搜索
热搜: Java Python Linux Go
发回帖 发新帖

2147

积分

0

好友

301

主题
发表于 前天 23:11 | 查看: 2| 回复: 0

银狐远控软件调试界面与代码结构

原始的银狐远程控制软件代码中,存在着大量的C++编码问题,其中绝大多数都属于容易忽视的低级错误。正是这些错误导致了软件稳定性差,在运行时极易崩溃。

本次分析的代码基于4.0版本,是一个Visual Studio 2010工程,希望这些问题的剖析与修复方案,能对大家提高实际的C/C++开发水平有所帮助。

问题1:GetModuleFileName参数误用

Visual Studio代码截图展示GetModuleFileName错误调用

如图所示,银狐代码中大量调用了Windows API GetModuleFileName 来获取当前程序所在路径。该函数的最后一个参数本应传递缓冲区能容纳的字符数量,而非字节数目。由于工程设置使用了Unicode字符集(一个字符占两个字节),图中传递字节数目的做法,在存放的路径较长时,极易造成内存越界,引发程序崩溃。

DWORD GetModuleFileNameW(
  [in, optional] HMODULE hModule,
  [out]          LPWSTR  lpFilename,
  [in]           DWORD   nSize // 这个参数是字符数量,不是字节数目
);

修改方法:
将第三个参数改为字符数量。Windows提供了一个宏 ARRAYSIZE,可以在编译期间自动计算数组的字符长度(其实现原理是数组总字节数除以第一个元素的字节数)。因此,应将上述代码修改为:

TCHAR ExePath[MAX_PATH] = { 0 };
GetModuleFileName(NULL, ExePath, ARRAYSIZE(ExePath));

此类错误在银狐代码中至少存在十几处,尤其是在主控端代码中。

问题2:memcpy拷贝空字符串导致越界

Visual Studio代码截图展示memcpy错误用法

这段代码节选自“系统管理”插件模块,你能看出标红处的问题吗?

这里使用 memcpy 将一个空字符串(宽字符 L".")拷贝到 lpBuffer + dwOffset 指向的内存位置,但拷贝长度却是 MAX_PATH * sizeof(TCHAR)。由于宽字符空串 _T("") 所指向的内存值就是 L'\0',这种“拷贝过多”的行为实际上会读取源地址之后未定义的内存区域,造成内存越界,其行为是未知的。

修改方法:
作者的原意可能是将 MAX_PATH * sizeof(TCHAR) 长度的内存区域置零。因此,这里应该直接使用 memset 函数进行清零操作。

memset(lpBuffer + dwOffset, 0, MAX_PATH * sizeof(TCHAR));

类似这样的代码在“系统管理”插件模块中随处可见。

问题3:剪贴板数据处理中的连环崩溃

Visual Studio代码截图展示剪贴板处理逻辑错误

这段问题代码位于“登录模块”的离线键盘记录功能中。短短一段逻辑,竟然隐藏着三个潜在的崩溃点:

  1. 错误计算数据长度GlobalSize 这个API返回的是当前剪贴板数据的字节数,即使剪贴板内容是宽字符。原代码将其乘以2,可能导致后续 wcscmp(lpstr, Clipboard_old) != 0 比较时,读取 lpstr 越界。
  2. memcpy可能越界:同理,memcpy(Clipboard_old, lpstr, nPacketLen); 中的 nPacketLen 因乘以2而过大,读取 lpstr 时也可能越界。
  3. 格式化字符串未限制长度:使用 wsprintf(temp, _T("\r\n[剪切板:]%s\r\n"), lpstr); 格式化时,没有指定 temp 缓冲区的最大长度。如果 lpstr 不是以 \0 结尾的有效字符串(由于越界读取,这很可能发生),格式化函数会一直向后读取内存,导致严重越界。

真是十行代码,三个崩溃隐患。

完整修复后的代码如下:

if (GetTickCount() - m_dwLastCapture > 1500)
{
    InterlockedExchange((LPLONG)&m_dwLastCapture, GetTickCount());
    OpenClipboard(NULL);
    HGLOBAL hglb = GetClipboardData(CF_UNICODETEXT);
    if (hglb != NULL)
    {
        //int nPacketLen = int(GlobalSize(hglb)) * 2 + 2; // 错误写法
        int nPacketLen = int(GlobalSize(hglb)); // 正确写法
        LPCTSTR  lpstr = (LPCTSTR)GlobalLock(hglb);
        if (lpstr != NULL)
        {
            if (nPacketLen < sizeof(szClipboard_old))  // 判断长度
            {
                //if (wcscmp(lpstr, szClipboard_old) != 0)  // 不安全的内容比较
                if (memcmp((char*)lpstr, (char*)szClipboard_old, nPacketLen) != 0) // 安全的二进制比较
                {
                    memcpy(szClipboard_old, lpstr, nPacketLen);
                    //wsprintf(szTemp, _T("\r\n[剪切板:]%s\r\n"), lpstr); // 不安全格式化
                    swprintf_s(szTemp, ARRAYSIZE(szTemp), _T("\r\n[剪切板:]%s\r\n"), lpstr); // 安全格式化
                    Input::SaveToFile(szTemp);
                    memset(szTemp, 0, sizeof(szTemp));
                }
            }
        }
        ::GlobalUnlock(hglb);
    }
    ::CloseClipboard();
}

问题4:GetWindowText参数错误与格式化风险

Visual Studio代码截图展示GetWindowText错误调用

这段代码同样位于“登录模块”的离线键盘记录功能,负责获取被控机器上当前活动窗口的标题。

与问题一类似,GetWindowText 的最后一个参数应该传入缓冲区能容纳的字符数量(包括结尾的 L'\0'),但代码中传入了缓冲区的字节数目,这相当于允许的长度翻了一倍。如果用户电脑上的活动窗口标题较长,就会造成内存溢出,导致被控端崩溃。

修改方法:

BOOL Input::IsWindowsFocusChange()
{
    memset(WindowCaption, 0, sizeof(WindowCaption));
    hFocus = GetForegroundWindow();
    GetWindowText(hFocus, WindowCaption, ARRAYSIZE(WindowCaption)); // 使用ARRAYSIZE
    WindowCaption[ARRAYSIZE(WindowCaption)-1] = 0; // 确保终止符
    //... 省略无关代码
}

下面的代码段:

if (lstrlen(WindowCaption) > 0)
{
   SYSTEMTIME   s;
   GetLocalTime(&s);
   wsprintf(temp, _T("\r\n[标题:]%s\r\n[时间:]%d-%d-%d  %d:%d:%d\r\n"), WindowCaption, s.wYear, s.wMonth, s.wDay, s.wHour, s.wMinute, s.wSecond);
   SaveToFile(temp);
   memset(temp, 0, sizeof(temp));
   memset(WindowCaption, 0, sizeof(WindowCaption));
   ReturnFlag = TRUE;
}

其中调用 wsprintf 进行格式化存在与问题三相同的内存越界风险,当窗口标题较长时,temp 缓冲区可能被写爆。这也解释了为什么被控端在切换不同窗口时,有时会意外掉线(实际上是程序崩溃退出了)。

问题5:CreateStreamOnHGlobal内存管理不当

Visual Studio代码截图展示CreateStreamOnHGlobal错误用法

这段代码位于主控端的远程屏幕相关模块,存在一个较为隐蔽的内存管理问题,如果不熟悉相关Windows API,很难发现。其现象是启动远程屏幕时,主控端偶现崩溃。

这个问题是使用Visual Studio 2022集成的Google Address Sanitizer工具排查出来的,该工具能高效定位C/C++内存问题。

解决方法:
查阅MSDN可知,CreateStreamOnHGlobal 函数的第二个参数 fDeleteOnRelease 如果指定为 TRUE,则在调用返回的 IStream 接口的 Release 方法时,会自动释放第一个参数 hGlobal 所指向的内存。在本例中,hGlobal 是由 GlobalAlloc(GMEM_MOVEABLE, ...) 分配的可移动内存。

问题在于,代码中创建的OLE流对象(pStreampOutStream)可能只占用了 GlobalAlloc 分配的部分内存或对其进行封装。当 fDeleteOnReleaseTRUE 时,流对象释放时会尝试释放整个 hGlobal 内存块,这可能与后续显式调用 GlobalFree(hGlobal) 产生冲突或访问违规,尤其是在多线程或复杂调用路径下。

因此,解决方案是将两处调用 CreateStreamOnHGlobal 函数时的第二个参数改为 FALSE,即禁止流对象自动释放全局内存,由我们手动管理 GlobalFree

修改后代码如下:

//显示截图窗口
void CMainFrame::OnOpenDesktop(ClientContext* pContext)
{
    //...省略无关代码...

    HGLOBAL hGlobal = GlobalAlloc(GMEM_MOVEABLE, pContext->m_DeCompressionBuffer.GetBufferLen() - 1);
    void* pData = GlobalLock(hGlobal);
    memcpy(pData, pContext->m_DeCompressionBuffer.GetBuffer(1), pContext->m_DeCompressionBuffer.GetBufferLen() - 1);
    GlobalUnlock(hGlobal);
    IStream* pStream = NULL;
    if (CreateStreamOnHGlobal(hGlobal, FALSE, &pStream) == S_OK) // 改为FALSE
    {
        CImage image;
        if (SUCCEEDED(image.Load(pStream)))
        {
            IStream* pOutStream = NULL;
            if (CreateStreamOnHGlobal(NULL, FALSE, &pOutStream) == S_OK) // 改为FALSE
            {
                image.Save(Ttime);
            }
        }
        pStream->Release();
    }
    GlobalFree(hGlobal); // 手动释放

    //...省略无关代码...
}

问题6:UDP断线重连失效的线程同步Bug

Visual Studio代码对比截图展示UDP连接逻辑差异

如图所示,当被控端使用UDP模式连接主控时,一旦发生断线,被控端将永远无法重新连接成功。这意味着所有依赖网络连接的模块(如上线、登录、键盘记录、远程屏幕等)都会因此失效。

这段逻辑的原理是:发起UDP连接后,会调用 WaitForSingleObject 等待一个内核事件对象 m_hEvent_run 受信(以此标志连接成功)。连接成功时,会调用 SetEvent 设置 m_hEvent_run 为受信状态。如下图所示:

Visual Studio代码截图展示OnHandShake中设置事件

随后,代码会调用 CUdpSocket::run_event_loop() 进入事件循环等待。

问题根源:
关键就在 CUdpSocket::run_event_loop() 函数内部,它关闭了 m_hEvent_run 这个事件对象句柄,同时被关闭的还有 m_hEvent

Visual Studio代码截图展示run_event_loop关闭事件句柄

这样一来,当发生断线需要重连时,再次调用 CUdpSocket::Connect 函数。由于 m_hEvent_run 的句柄已被关闭,WaitForSingleObject 会立即返回 WAIT_FAILED 状态,导致连接函数永远返回失败。

Visual Studio代码截图展示Connect函数中WAIT_FAILED分支

解决方法:
有两种思路可以解决此问题:

  1. 方法一:连接成功后,在 CUdpSocket::run_event_loop() 函数中不要调用 CloseHandle(m_hEvent_run); 来关闭这个事件对象,使其在程序生命周期内持续有效。
  2. 方法二:在每次尝试重连时,先判断事件句柄是否有效,若无效则重新调用 CreateEvent 函数创建新的 m_hEvent_runm_hEvent 对象。

修复这个Bug需要对Windows多线程同步和Socket编程有深入的理解。这类成体系的知识往往需要在实际工作中长期积累和总结。

为了更彻底地排查和优化这套代码,除了修复上述问题,我还将工程从Visual Studio 2010升级到了Visual Studio 2022,补全并重新编译了所有依赖库,并移除了所有潜在的后门代码,使其成为一款更稳定、可用于学习研究的远程控制软件示例。

尽管这套初始源码存在不少缺陷,但它仍然是深入学习C/C++、Windows系统编程、网络通信、安全工程以及大型项目实战的宝贵材料。在像云栈社区这样的技术论坛中,与同行交流此类复杂项目的调试和优化经验,对开发者的成长大有裨益。




上一篇:老板裁掉芯片硬件团队后,面对超高层PCB与AI服务器订单陷入困境
下一篇:专家-路由器耦合损失(ERC)优化MoE模型训练,字节团队提出控制专家专业化新方法
您需要登录后才可以回帖 登录 | 立即注册

手机版|小黑屋|网站地图|云栈社区 ( 苏ICP备2022046150号-2 )

GMT+8, 2026-1-12 01:28 , Processed in 0.203864 second(s), 40 queries , Gzip On.

Powered by Discuz! X3.5

© 2025-2025 云栈社区.

快速回复 返回顶部 返回列表