windows进程间通信系列 第三篇 匿名管道与命名管道

剪贴板的话只能实现本机上进程之间的通信,而邮槽的话虽然是可以实现跨网络之间的进程的通信,但麻烦的是邮槽的服务端只能接收数据,邮槽的客户端只能发送数据,太悲剧了,而对于匿名管道的话,其也只能实现本机上进程之间的通信,你要是能够实现本机进程间的通信也就算了,关键是它还只用来实现本地的父子进程之间的通信,也太局限了吧?而这里介绍的这个命名管道的话,就和他们有些不同了,在功能上也就显得强大很多了,至少其可以实现跨网络之间的进程的通信,同时其客户端既可以接收数据也可以发送数据,服务端也是既可以接收数据,又可以发送数据的。

管道(pipe)是用于进程间通信的共享内存区域。创建管道的进程称为管道服务器,而连接到这个管道的进程称为管道客户端。一个进程向管道写入信息,而另外一个进程从管道读取信息。

管道是一种进程间,确切的说应该是线程间的通信方法。顾名思义。管道是一个有两端的对象。进程可以从这个对象的一个端口写数据,从另一个端口读数据。管道其实也是一种共享内存,只是更加规范。

管道有两种类型:匿名管道和命名管道。匿名管道是单向的,也就是说数据只能从写句柄写入,从读句柄读出。

异步管道是基于字符和半双工的(即单向),一般用于程序输入输出的重定向;命名管道则强大地多,它们是面向消息和全双工的,同时还允许网络通信,用于创建客户端/服务器系统。

(1)异步管道

前面已经说到过,异步管道的缺陷:只能够在父子进程之间通信,而且是半双工的,server与client不能同时发送数据。

创建匿名管道的函数式CreatePipe,创建一个匿名管道,并从中得到读写管道的句柄。

函数的原型为:

BOOL WINAPI CreatePipe( _Out_PHANDLE hReadPipe, _Out_PHANDLE hWritePipe, _In_opt_LPSECURITY_ATTRIBUTES lpPipeAttributes, _In_DWORD nSize);
参数说明:

  • hReadPipe[out]
    返回一个可用于读管道数据的文件句柄
  • hWritePipe[out]
    返回一个可用于写管道数据的文件句柄
  • lpPipeAttributes[in, optional]
    传入一个 SECURITY_ATTRIBUTES结构的指针,该结构用于决定该函数返回的句柄是否可被子进程继承。如果传NULL,则返回的句柄是不可继承的。
    该结构的 lpSecurityDescriptor成员用于设定管道的安全属性,如果传NULL,那么该管道将获得一个默认的安全属性,该属性与创建该管道的用户账户权限ACLs的安全令牌(token)相同。
  • nSize[in]
    管道的缓冲区大小。但是这仅仅只是一个理想值,系统根据这个值创建大小相近的缓冲区。如果传入0 ,那么系统将使用一个默认的缓冲区大小。

说明:匿名管道不允许异步操作,所以如在一个管道中写入数据,且缓冲区已满,那么除非另一个进程从管道中读出数据,从而腾出了缓冲区的空间,否则写入函数不会返回


关于匿名管道的一个应用:重定向。

这个例子是这样的:后门程序需要调用目标机器上的CDM,后门程序发送命令给cmd.exe程序,由cmd.exe程序把结果返回给我们的后门程序。这里就有两个问题了:

(1)cmd程序的默认输出在cmd窗口,那么如何把输出传给后门程序呢?

(2)后门程序的输入如何传给cmd程序呢?

解决方法:后门程序创建cmd进程,即cmd程序是后门程序的子进程,这样的话我们可以方便把需要的命令直接传给子进程,让其执行即可,这样第二个问题就解决了。cmd执行后的结果如何传给其父进程(后门程序)?这里我们需要使用一个重定向来处理,把cmd程序的输出重定位到后门程序。

windows进程间通信系列 第三篇 匿名管道与命名管道_第1张图片

又是子进程与父进程的关系,又是重定向,那么我们顺其自然使用管道了,这里使用匿名管道。

从图中可以看到,创建了两个管道,因为匿名管道是单向的,所以必须使用两个管道,要不然没法通信。

cmd程序把执行结果写入到管道1中,并从管道2中读出后门发过来的命令;后门程序从管道1中读出cmd程序的执行结果,把命令写入到管道2中。

每一个管道都有一个读句柄和一个写句柄,就是通过这两个句柄来进行数据读写从而来实现通信的。


创建子进程我们知道,可以使用CreateProcess函数,重定位我们需要使用进程创建过程中的一个参数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;
STARTUPINFO结构 该结构用于指定新进程的主窗口特性。

这个结构中只有三个参数跟我们的重定向有关,就是:hStdOutup,hStdInput,hStdError。通过设置这个结构中的参数为管道的输入输出句柄的值,可以设置创建的进程的输入输出重定向到管道。最终创建的进程的输入输出会与管道的句柄联系,达到了重定向的效果。

设置如下:

STARTUPINFO s ;

s.hStdError = hWritePipe ;  // 错误输出到管道1写句柄

s.hStdOutput = hWritePipe ; // 执行结构从管道写句柄输入

s.hStdInput = hReadPipe ;  // 从管道2读句柄输入,读取命令

当然了我们要在目标主机上创建一个cmd.exe,要具有一定的隐藏性至少不能出现窗口,所以创建的进程窗口的属性也是必须要设置的,因为默认情况下是可见的,所以需要把进程窗口特性wShowWindow改变为SW_HIDE属性。



实际上在不使用管道的情况下也可以实现cmd与后门的直接通信。那么我们就需要抛弃在unix,linux以及windows下都可以使用的socket,而选择MS为我们提供的WSASocket了,它与Socket最大的不同就是WSASocket支持异步I/O操作,而Socket编写的通信进程都会发生阻塞,除非使用多线程来处理。在上面使用管道的方法中我们就使用了两个管道来处理Socket的阻塞情况。

在我们编程过程中,为了方面我将使用WSASocket来处理cmd与后门之间的通信。那么我们就需要直接把cmd的输入输出与后门WASSocket关联起来,具体关联的方法就是:

s.hStdInput = s.hStdOutput = s.StdError = (void*)sock ;

也就是说,cmd处理命令后的结果直接发送给后门套接字,而后门套接字的命令也直接发送给cmd进行处理。这样就只需创建好后门套接字进行等待就行。


WriteFile函数

将数据写入一个文件。该函数比fwrite函数要灵活的多。也可将这个函数应用于对通信设备、管道、套接字以及邮槽的处理


BOOLWriteFile(
HANDLEhFile, //文件句柄
LPCVOIDlpBuffer, //数据缓存区指针
DWORDnNumberOfBytesToWrite, //你要写的字节数
LPDWORDlpNumberOfBytesWritten, //用于保存实际写入字节数的存储区域的指针
LPOVERLAPPEDlpOverlapped //OVERLAPPED结构体指针
);

从 文件指针指向的位置开始将数据写入到一个文件中, 且支持同步和异步操作,
如果文件打开方式没有指明FILE_FLAG_OVERLAPPED的话,当程序调用成功时,它将实际写入文件的字节数保存到lpNumberOfBytesWriten指明的 地址空间中
如果文件要交互使用的话,当函数调用完毕时要记得调整文件指针
参数说明
HANDLE hFile, 需要写入数据的 文件指针,这个指针指向的文件必须是GENERIC_WRITE access 访问属性的文件
LPOVERLAPPED lpOverlapped OVERLAPPED 结构体 指针,如果文件是以FILE_FLAG_OVERLAPPED方式打开的话,那么这个指针就不能为NULL
vc返回值
调用成功,返回非0
调用不成功,返回为0

ReadFile 函数

从 文件指针指向的位置开始将数据读出到一个文件中, 且支持同步和异步操作,
如果文件打开方式没有指明FILE_FLAG_OVERLAPPED的话,当程序调用成功时,它将实际读出文件的字节数保存到lpNumberOfBytesRead指明的 地址空间中。
FILE_FLAG_OVERLAPPED 允许对文件进行重叠操作
如果文件要交互使用的话,当 函数调用完毕时要记得调整 文件指针。
从文件中读出数据。与fread函数相比,这个函数要明显灵活的多。该函数能够操作通信设备、管道、 套接字以及邮槽。

BOOLReadFile(
HANDLEhFile, //文件的句柄
LPVOIDlpBuffer, //用于保存读入数据的一个缓冲区
DWORDnNumberOfBytesToRead, //要读入的字节数
LPDWORDlpNumberOfBytesRead, //指向实际读取字节数的指针
LPOVERLAPPEDlpOverlapped
//如文件打开时指定了FILE_FLAG_OVERLAPPED,那么必须,用这个参数引用一个特殊的结构。
//该结构定义了一次异步读取操作。否则,应将这个参数设为NULL
);

(2)命名管道

命名管道具有以下几个特征:

(1)命名管道是双向的,所以两个进程可以通过同一管道进行交互。

(2)命名管道不但可以面向字节流,还可以面向消息,所以读取进程可以读取写进程发送的不同长度的消息。

(3)多个独立的管道实例可以用一个名称来命名。例如几个客户端可以使用名称相同的管道与同一个服务器进行并发通信。

(4)命名管道可以用于网络间两个进程的通信,而其实现的过程与本地进程通信完全一致。




服务端:

服务端进程调用 CreateNamedPipe 函数来创建一个有名称的命名管道,

在创建命名管道的时候必须指定一个本地的命名管道名称(不然就不叫命名管道了),

Windows 允许同一个本地的命名管道名称有多个命名管道实例,

所以,服务器进程在调用 CreateNamedPipe 函数时必须指定最大允许的实例数(0 -255),

如果 CreateNamedPipe 函数成功返回后,服务器进程得到一个指向一个命名管道实例的句柄,

然后,服务器进程就可以调用 ConnectNamedPipe 来等待客户的连接请求,

这个 ConnectNamedPipe 既支持同步形式,又支持异步形式,

若服务器进程以同步形式调用 ConnectNamedPipe 函数,

(同步方式也就是如果没有得到客户端的连接请求,则会一直等到)

那么,当该函数返回时,客户端与服务器之间的命名管道连接也就已经建立起来了。

在已经建立了连接的命名管道实例中,

服务端进程就会得到一个指向该管道实例的句柄,这个句柄称之为服务端句柄。

同时,服务端进程可以调用 DisconnectNamedPipe 函数,

将一个管道实例与当前建立连接的客户端进程断开,从而可以重新连接到新的客户进程。

当然在服务端也是可以调用 CloseHandle 来关闭一个已经建立连接的命名管道实例。


客户端

客户端进程调用 CreateFile 函数连接到一个正在等待连接的命名管道上,

在这里客户端需要指定将要连接的命名管道的名称,

当 CreateFile 成功返回后,客户进程就得到了一个指向已经建立连接的命名管道实例的句柄,

到这里,服务器进程的 ConnectNamedPipe 也就完成了其建立连接的任务。

客户端进程除了调用 CreateFile 函数来建立管道连接以外,

还可以调用 WaitNamedPipe 函数来测试指定名称的管道实例是否可用。

在已经建立了连接的命名管道实例中,客户端进程就会得到一个指向该管道实例的句柄,

这个句柄称之为客户端句柄。

在客户端可以调用 CloseHandle 来关闭一个已经建立连接的命名管道实例。














你可能感兴趣的:(windows系统编程)