详解银狐远控源码中那些设计缺陷
在前一篇文章《详解银狐远控源码中那些C++编码问题》中,我们介绍了银狐远控源码中细粒度的一些C++编码问题,本文继续介绍银狐框架上的一些设计缺陷,相比较细粒度的编码问题,这些设计上的缺陷牵扯整个框架,修复起来就不太容易,且工作量较大。

特别申明:
本文内容仅限于用作技术交流,请勿使用本文介绍的技术做任何其他用途,否则后果自负,与本号无关。
先来两个开胃菜,比较简单:
问题1:服务列表和驱动服务列表无法展示
默认的版本,如果以非管理员权限运行被控,打开系统管理的界面时Win32服务和驱动服务列表是无法显示的,如下图所示:


原因是获取服务列表时请求的的权限过大,非管理员权限的程序被拒绝,代码在如下两个位置皆会因为权限不走,而调用失败。


调用Windows API OpenSCManager接下来是为了调用QueryServiceConfig2查询服务的状态,因为只需要查询权限即可,不需要特别大的权限。
修改方法:
只需要请求查询权限即可,将这两行代码修改如下:
sc = OpenSCManager(NULL, NULL, SC_MANAGER_CONNECT | SC_MANAGER_ENUMERATE_SERVICE);...sh = OpenService(sc, lpServices[i].lpServiceName, SERVICE_QUERY_CONFIG);
这样,服务列表就能在非管理员权限下显示了:


操作服务的几个右键菜单修复方法与此类似,这里不再一一列举。
问题2:视频查看功能,在被控不存在摄像头时,release版本崩溃
当被控没有摄像头时,如果是debug版本,主控绘制摄像头画面的逻辑如下:

m_lpbmi是指向BITMAPINFO对象的指针,表示当前摄像头一帧画面数据,当电脑不存在摄像头设备时,debug版本做了对m_lpbmi判空操作,因此没啥问题。
release版本走的是WM_PAINT消息处理,落入下:

修复方法,在OnPaint函数之前和debug版本一样判空即可,或者干脆提示无摄像头不要走这里的绘制逻辑。
接下来的问题就比较复杂了:
问题3:使用主控插件功能期间,如果插件对话框已经被关闭,可能会引起崩溃
这个问题的是整个框架上的设计缺陷。以刷新文件管理列表为例,看一下更新文件列表的调用堆栈:
> Quick.exe!CFileManagerDlg::FixedRemoteFileList(unsigned char * pbBuffer=0x15830000, unsigned long dwBufferLen=1351) Line 969 C++ Symbols loaded. Quick.exe!CFileManagerDlg::OnReceiveComplete() Line 648 C++ Symbols loaded. Quick.exe!CMainFrame::ProcessReceiveComplete(ClientContext * pContext=0x10aeefb8) Line 1785 C++ Symbols loaded. Quick.exe!CMainFrame::NotifyProc(ClientContext * pContext=0x10aeefb8, unsigned int nCode=5) Line 1727 C++ Symbols loaded. Quick.exe!CHpTcpServer::OnReceive(ITcpServer * pSender=0x10c7269c, unsigned long dwConnID=3, int iLength=1366) Line 200 C++ Symbols loaded. Quick.exe!CTcpServer::DoFireReceive(TSocketObj * pSocketObj=0x1cfc3d50, int iLength=1366) Line 153 C++ Symbols loaded. Quick.exe!CTcpPullServerT<CTcpServer>::DoFireReceive(TSocketObj * pSocketObj=0x1cfc3d50, const unsigned char * pData=0x20973680, int iLength=1366) Line 77 C++ Symbols loaded. Quick.exe!CTcpServer::FireReceive(TSocketObj * pSocketObj=0x1cfc3d50, const unsigned char * pData=0x20973680, int iLength=1366) Line 134 C++ Symbols loaded. Quick.exe!CTcpServer::TriggerFireReceive(TSocketObj * pSocketObj=0x1cfc3d50, TBufferObj * pBufferObj=0x20973648) Line 51 C++ Symbols loaded. Quick.exe!CTcpServer::HandleReceive(unsigned long dwConnID=3, TSocketObj * pSocketObj=0x1cfc3d50, TBufferObj * pBufferObj=0x20973648) Line 1133 C++ Symbols loaded. Quick.exe!CTcpServer::HandleIo(unsigned long dwConnID=3, TSocketObj * pSocketObj=0x1cfc3d50, TBufferObj * pBufferObj=0x20973648, unsigned long dwBytes=1366, unsigned long dwErrorCode=0) Line 1015 C++ Symbols loaded. Quick.exe!CTcpServer::WorkerThreadProc(void * pv=0x10c7269c) Line 949 C++ Symbols loaded. Quick.exe!thread_start<unsigned int (__stdcall*)(void *),1>(void * const parameter=0x10ac2d18) Line 97 C++ Symbols loaded.
可以看到网络线程直接穿透到界面中,调用界面元素来更新,如果网络有波动或者延迟,此时界面被用户关掉,由于引用的界面对象已经无效,程序就会崩溃。
这个问题在系统管理界面功能上更为突出,因为系统管理的列表比较大,收发需要一定时间,读者可以使用原版本的主控做测试,反复在系统管理各个子界面反复切换和开启与关闭,一定会出现崩溃。
在网络线程中操作UI元素,Windows虽然允许,但是不是一种好的做法,银狐主控大量使用这种方式,但却没做好保护措施,这是各个插件共同的问题,你看:
void CMainFrame::ProcessReceiveComplete(ClientContext* pContext){ //...省略部分代码... // 交给窗口处理if (pContext->m_Dialog[0] > 0) { switch (pContext->m_Dialog[0]) {case SCREENSPY_DIF_DLG: //差异屏幕 ((CDifScreenSpyDlg*)dlg)->OnReceiveComplete();break;case SCREENSPY_QUICK_DLG: //高速屏幕 ((CQuickScreenSpyDlg*)dlg)->OnReceiveComplete();break;case SCREENSPY_PLAY_DLG: //娱乐屏幕 ((H264CScreenSpyDlg*)dlg)->OnReceiveComplete();break;;case SCREENSPY_HIDE_DLG: //后台屏幕 ((CHideScreenSpyDlg*)dlg)->OnReceiveComplete();break;case WEBCAM_DLG: //视频查看 ((CWebCamDlg*)dlg)->OnReceiveComplete();break;case FILEMANAGER_DLG: //文件管理 ((CFileManagerDlg*)dlg)->OnReceiveComplete();break;case KEYBOARD_DLG: //键盘记录 ((CKeyBoardDlg*)dlg)->OnReceiveComplete();break;case AUDIO_DLG: //GSM麦克风监听 ((CAudioDlg*)dlg)->OnReceiveComplete();break;case SPEAKER_DLG: //扬声器 ((CSpeakerDlg*)dlg)->OnReceiveComplete();break;case SHELL_DLG: //远程终端 ((CShellDlg*)dlg)->OnReceiveComplete();break;case MACHINE_DLG: //主机管理 ((CMachineDlg*)dlg)->OnReceiveComplete();break;case REGEDIT_DLG: //查注册表 ((CRegeditDlg*)dlg)->OnReceiveComplete();break;case CHAT_DLG: //远程交谈 ((CChat*)dlg)->OnReceiveComplete();break;case STARTUPMGR_DLG: //启动管理 ((CStartupMgrDlg*)dlg)->OnReceiveComplete();break;case PROXY_DLG: //代理 ((CProxyMapDlg*)dlg)->OnReceiveComplete();break;case EXPAND_DLG: //注入管理 ((CExpandDlg*)dlg)->OnReceiveComplete();break;case KERNEL_DLG: //注入管理 ((CKernelDlg*)dlg)->OnReceiveComplete();break; default: TRACE(" if (pContext->m_Dialog[0] > 0) 非法数据 %s");break; }return; } //...省略部分代码...case TOKEN_DRIVE_LIST: // 驱动器列表 g_pFrame->PostMessage(WM_OPENMANAGERDIALOG, 0, (LPARAM)pContext);break;case TOKEN_BITMAPINFO_DIF: //差异屏幕 g_pFrame->PostMessage(WM_OPENSCREENSPYDIALOG_DIF, 0, (LPARAM)pContext);break;case TOKEN_BITMAPINFO_QUICK: //高速屏幕 g_pFrame->PostMessage(WM_OPENSCREENSPYDIALOG_QUICK, 0, (LPARAM)pContext);break;case TOKEN_BITMAPINFO_PLAY: //娱乐屏幕 g_pFrame->PostMessage(WM_OPENSCREENSPYDIALOG_PLAY, 0, (LPARAM)pContext);break;case TOKEN_BITMAPINFO_HIDE: //后台屏幕 g_pFrame->PostMessage(WM_OPENSCREENSPYDIALOG_HIDE, 0, (LPARAM)pContext);break;case TOKEN_WEBCAM_BITMAPINFO: // 摄像头 g_pFrame->PostMessage(WM_OPENWEBCAMDIALOG, 0, (LPARAM)pContext);break;case TOKEN_AUDIO_START: //麦克风 g_pFrame->PostMessage(WM_OPENAUDIODIALOG, 0, (LPARAM)pContext);break;case TOKEN_SPEAK_START: // 扬声器 g_pFrame->PostMessage(WM_OPENSPEAKERDIALOG, 0, (LPARAM)pContext);break;case TOKEN_KEYBOARD_START://键盘 g_pFrame->PostMessage(WM_OPENKEYBOARDDIALOG, 0, (LPARAM)pContext);break;case TOKEN_SHELL_START://远程终端 g_pFrame->PostMessage(WM_OPENSHELLDIALOG, 0, (LPARAM)pContext);break;case TOKEN_SYSINFOLIST://系统管理 g_pFrame->PostMessage(WM_OPENSYSINFODIALOG, 0, (LPARAM)pContext);break;case TOKEN_CHAT_START://远程交谈 g_pFrame->PostMessage(WM_OPENCHATDIALOG, 0, (LPARAM)pContext);break;case TOKEN_STARTUP_STATUS_START: g_pFrame->PostMessage(WM_OPENSTARTUPMGRDIALOG, 0, (LPARAM)pContext);break;case TOKEN_REGEDIT: //注册表管理 g_pFrame->PostMessage(WM_OPENREGEDITDIALOG, 0, (LPARAM)pContext);break;case TOKEN_PROXY_START: //代理 g_pFrame->PostMessage(WM_OPENPROXYDIALOG, 0, (LPARAM)pContext);break;case TOKEN_KERNEL: //驱动插件 g_pFrame->PostMessage(WM_OPENPKERNELDIALOG, 0, (LPARAM)pContext);break;case TOKEN_EXPAND: //互动插件 g_pFrame->PostMessage(WM_OPENPEXPANDDIALOG, 0, (LPARAM)pContext);break;case TOKEN_DDOS: //压力测试 p_CDDOSAttackDlg->PostMessage(WM_DDOS_CLIENT, 0, (LPARAM)pContext);break;case TOKEN_MONITOR: //屏幕监控 g_pScreenMonitorDlg->PostMessage(WM_MONITOR_CLIENT, 0, (LPARAM)pContext);break; default: TRACE("switch (pContext->m_DeCompressionBuffer.GetBuffer(0)[0]) 非法数据");break; }return;}
基本上每个每个界面元素都在网络线程中操作了一遍。
修复方法:
梳理所有逻辑,调整框架。让网络线程操作UI线程通过线程间同步技术,工作量大。
上述代码还存在另外一个严重的问题。
问题4:连接对象生命周期和使用范围错误
我们以这一行为例:
case TOKEN_REGEDIT: //注册表管理 g_pFrame->PostMessage(WM_OPENREGEDITDIALOG, 0, (LPARAM)pContext);break;
不知道读者都否能看出问题?
这里调用PostMessage向UI线程的消息队列投递WM_OPENREGEDITD消息,并带上连接对象pContext。UI线程处理是异步的,这里以对这个消息处理为例:

问题是pContext对象的产生和回收由网络线程控制,假设UI线程在使用这个对象期间,网络断开了,这个对象被回收了,那么UI线程就会出现问题,银狐网络线程回收这个对象时并没有销毁它,而是放回空闲对象池中,虽然不一定引起崩溃,但也会造成数据显示不正常。
struct ClientContext{ ULONG_PTR m_Socket; // Store buffers CBuffer m_WriteBuffer; CBuffer m_CompressionBuffer; // 接收到的压缩的数据 CBuffer m_DeCompressionBuffer; // ...其他结构省略...};EnHandleResult CHpTcpServer::OnAccept(ITcpServer* pSender, CONNID dwConnID, UINT_PTR soClient){ // ...其他代码省略... ClientContext* pContext = NULL; m_clcs.lock();if (!m_listFreePool.IsEmpty()) { pContext = m_listFreePool.RemoveHead(); }else { pContext = new(std::nothrow) ClientContext; } m_clcs.unlock(); ZeroMemory(pContext, sizeof(ClientContext)); pContext->m_bIsMainSocket = FALSE; memset(pContext->m_Dialog, 0, sizeof(pContext->m_Dialog)); pContext->pView = NULL; pContext->m_Socket = dwConnID; pContext->switchsocket = tcp; // ...其他代码省略...}EnHandleResult CHpTcpServer::OnClose(ITcpServer* pSender, CONNID dwConnID, EnSocketOperation enOperation, int iErrorCode){ // ...其他代码省略... ClientContext* pContext = NULL;if (m_TcpServer->GetConnectionExtra(dwConnID, (PVOID*)&pContext) && pContext != nullptr) m_TcpServer->SetConnectionExtra(dwConnID, NULL);if (!pContext) return HR_OK; pContext->IsConnect = 888; m_pNotifyProc(pContext, NC_CLIENT_DISCONNECT); MovetoFreePool(pContext);return HR_OK;}
要解决这个问题,可以把数据对象复制一份,传递给UI线程,但是对于少量数据的应用可以这么做;对于像代理映射和远程桌面等应用复制的方法太笨重,会占用大量内存,且带来内存碎片问题,可以使用内存池和双缓冲区方式去解决。
由于到处是这样的逻辑,因此这项修复工作量也非常大。
问题5:网络收包与解包逻辑
很多同学想调整银狐的协议格式,但最终放弃。放弃的原因是因为,银狐的网络装包和解包分散在各个地方,一共有五百多处。
最佳实践:
将收包和解包收拢在同一个入口和出口处,在入口和出口处做解包和装包处理。
例如,对于被控端,我们可以定义如下接口:
class INetworkClient{public: /** * 建立连接 * @param host * @param port * @param m_pluginNetworkService 插件网络服务 * @param autoReconnect 自动重连 * @param pReceiver 数据接受者 * @return */ virtual BOOL Start(const wchar_t* host, USHORT port, bool autoReconnect) = 0; /** * 停止连接 */ virtual void Stop() = 0; /** * 设置协议加密类型 */ virtual void SetProtocolEncryptType(int encryptType) = 0; /** * 使用AES加密时的秘钥,从身份验证信息中获取 */ virtual void SetAESKey(const wchar_t* pszAESKey) = 0; /** * 是否启用心跳保活 * @param: heartbeatIntervalMs 心跳保活的间隔,单位为秒 */ virtual void EnableHeartbeat(int32_t heartbeatIntervalSec) = 0; /** * 是否禁用心跳日志 */ virtual void EnableHeartbeatLogs(bool enable) = 0; virtual bool Send(std::string& contentBuffer, int8_t flags) = 0; virtual bool Send(const char* contentBuffer, size_t contentSize, int8_t flags) = 0; /** * 发送插件消息,对端匹配GUID的插件消息收消息 */ virtual bool SendPluginPacket(const GUID& guid, std::string& contentBuffer, int8_t flags) = 0; virtual bool SendPluginPacket(const GUID& guid, const char* contentBuffer, size_t contentSize, int8_t flags) = 0; /** * 获取状态 */ virtual bool IsConnected() = 0; /** * 获取最后连接时间 * @return */ virtual int64_t GetLastConnectedTime() = 0;protected: virtual ~INetworkClient() = default;};
所有的网络装包操作都封装在Send系列接口。
这样无论是调整协议格式还是对数据包做加密和压缩操作,在这里统一处理就可以了,且可以很小成本的替换。
问题6:银狐真的支持UDP协议吗
看这个标题,有同学可能会很纳闷,实测下来银狐可以支持UDP连接的呀。确实如此,银狐使用的hpsocket中的UDP实现,总结起来就是使用Windows完成端口统一处理UDP socket的连接。
贴一下hpsocket部分代码:
BOOL CUdpServer::Start(LPCTSTR lpszBindAddress, USHORT usPort){ //...省略无关代码if(CreateListenSocket(lpszBindAddress, usPort))if(CreateCompletePort())if(CreateWorkerThreads())if(StartAccept()) { m_enState = SS_STARTED; m_evWait.Reset();return TRUE; } //...省略无关代码}
可以看到基本上和TCP一模一样,只不过socket换成了UDP。
然后框架收到UDP数据包后处理逻辑和TCP一模一样:
EnHandleResult CHpUdpServer::OnReceive(IUdpServer* pSender, CONNID dwConnID, const BYTE* pData, int iLength){ // TRACE("%s --%d\r\n", __FUNCTION__, dwConnID); ClientContext* pContext = NULL;if ((!m_UdpServer->GetConnectionExtra(dwConnID, (PVOID*)&pContext)) && (pContext != nullptr) && (iLength <= 0))return HR_ERROR;if (pContext->IsConnect != 666) return HR_ERROR; pContext->m_CompressionBuffer.Write((PBYTE)pData, iLength); m_pNotifyProc(pContext, NC_RECEIVE); // 检测数据大小while ((int)pContext->m_CompressionBuffer.GetBufferLen() > m_headerlength) {if (pContext->m_password[8] != TOKEN_ACTIVED) {for (size_t i = 0; i < 9; i++) {if (pContext->m_password[i] != 0)return HR_ERROR; } memcpy(pContext->m_password, pContext->m_CompressionBuffer.GetBuffer(4), 10); (pContext->m_password[9] == 1) ? (pContext->bisx86 = FALSE) : (pContext->bisx86 = TRUE); } int nSize = 0; CopyMemory(&nSize, pContext->m_CompressionBuffer.GetBuffer(0), sizeof(int));if ((nSize > 0) && (((int)pContext->m_CompressionBuffer.GetBufferLen()) >= nSize)) {if ((int)pContext->m_CompressionBuffer.GetBufferLen() < nSize) { TRACE("%s %d memcmp baddata \r\n", __FUNCTION__, dwConnID);return HR_ERROR; } try { pContext->m_allpack_rev++; pContext->m_alldata_rev += nSize; pContext->m_DeCompressionBuffer.ClearBuffer(); //清理旧数据if (pContext->m_password[8] != TOKEN_ACTIVED)return HR_ERROR; pContext->m_DeCompressionBuffer.Write(pContext->m_CompressionBuffer.GetBuffer(m_headerlength), nSize - m_headerlength, 1, pContext->m_password); //写入数据 m_pNotifyProc(pContext, NC_RECEIVE_COMPLETE); pContext->m_CompressionBuffer.Delete(nSize); //清理剩下数据 } catch (...) {return HR_ERROR; } }elsebreak; }return HR_OK;}
这里这样写,虽然没错,但却没必要,首先,UDP是数据包,不存在TCP半包和粘包问题,收到的数据包操作系统协议栈交付给应用程序时就是一个完整的报文,所以不需要有半包和粘包处理逻辑,因此这里的逻辑多余。当然,这里这样写也不算错。
最大的问题是,UDP协议缺少了TCP协议的可靠传输保证,即包缺少重传和乱序保障,因此业务必须自己处理这些问题,但是银狐却没有处理这些问题。因此,这套UDP实现在本机简单测试没啥问题,拿到实际场景去使用,业务一定是不稳定的。
关于银狐远控的bug远不止于此,维护这一年多以来,我总共修复了100+问题,github提交记录就有568次之多。

当然,虽然这套源码bug挺多的,但瑕不掩瑜,它仍然是学习C/C++开发、多线程编程、网络编程、安全工程、综合项目实践、红蓝攻防非常好的材料。
为了更方便排查和优化代码,我除了修复以上bug以外,还将这套代码从原来的Visual Studio 2010工程全部升级成Visual Studio 2022,并补全和重编译了所有依赖库代码,并去掉所有后门,现在它是一款可以放心使用的远控软件。
由于篇幅有限,本次分享就到这里了,后续我将会更新更多的C/C++开发知识,欢迎关注公众号CppGuide。
源码获取
如果对银狐(winos)有兴趣,可以通过下面的方式获取全套源码:
关注后回复【winos】即可获取源码
夜雨聆风
