'비스타'에 해당되는 글 1

  1. 2008.06.02 Pipe 를 사용한 Inter Process Communication. (1)

Pipe 를 사용한 Inter Process Communication.

PipeVista.rar

소스 파일 다운로드

 Windows Vista 에 들어오면서 많은 것들이 바뀌었다. 그 중 가장 큰 변화중 하나가 바로 UAC 이다. UAC 의 새로운 등장으로 인하여, 많은 프로그래머들이 아무생각 없이 사용해왔던, 관리자 권한 Software 들이, 더 이상 아무런 사용자의 동의 없이 실행될 수 없게 되었다.
 이러한 제약 사항들은 운영체제나 사용자의 보안성에 좋은 영향을 주지만, 시스템 유틸리티나 보안 소프트웨어를 개발하는 사람들에게 있어서는 매우 치명적인 일이 될 수 밖에 없다.

 하지만, Windows Vista 에서도, 시스템 유틸리티나 보안 소프트웨어를 개발하는 프로그래머들을 위한 길을 아예 없애버린것은 아니다. 단지, 좀 더 명확한 정책을 따르도록 변경된 것일 뿐이다. 일반적으로 Windows XP 혹은 그 이전에는 '서비스 프로그램' 이 그다지 많은 호응을 받지 못했다. 왜냐하면, 서비스 프로그램은 만들기도 힘들고, 관리도 귀찮았기 때문이다. 하지만, Windows Vista 와 UAC 가 등장하면서 부터, 서비스 프로그램은, 관리자 권한 프로그램을 실행하고자 할 때 가장 안전하고 간편한 방법 중 하나이다. 이번에는 이러한 서비스 프로그램을 사용할 때 가장 문제가 되는 Inter Process Communcation 에 대하여 알아볼 것이다.

 먼저, IPC 를 하는 방법은 여러가지가 있다. WM_COPYDATA 를 사용하는 방법, Shared Memory 를 사용하는 방법, Pipe 를 사용하는 방법, Socket 을 사용하는 방법...등 매우 다양한 IPC 방법들이 존재한다. 하지만, 여기서 다루고자 하는 것은 Pipe 을 사용하는 방식이다. 왜 이 방법을 택해야 하는 걸까? 그에 대한 답변은 매우 간단하다. 먼저, 메시지를 주고 받는 방법은 서비스 프로그램과 IPC 를 할 대상 프로그램과의 Desktop 이 다른 경우 사용할 수 없다. Shared Memory 의 경우도 앞의 경우와 같은 문제를 가지고 있는데, Session 이 다른 경우, 다른 Session 의 Shared Memory에 접근할 수 없다. Socket의 경우에는 가능은 하지만, 단순한 IPC 를 위해서 Socket 을 사용하는 것은, 구덩이를 파기 위해 포크레인을 동원하는 것과 같다.

사용자 삽입 이미지

[그림1] Shared Memory (Section) 는 Session 마다 개별적으로 이름을 가지게 되며(e.g. \Session\1\이름) 각 Session 은 다른 Session 의 Section 을 침범할 수 없다.



그래서 결국 선택된 것이 Pipe 이다. Pipe 는 간단한 IPC 를 구현하는데 유효하며, 프로그램이 실행되는 Desktop 이나 계정에 상관없이, 공유가 가능하다.

 Pipe 는 CreateNamedPipe 함수와 ConnectNamedPipe 함수를 통하여 초기화 작업을 진행한다. 그 이후로는 ReadFile 과 WriteFile 을 사용하여 데이터에 접근하게 된다.

CPipeServer
CPipeServer 는 필자가 비스타에서 서비스 프로그램과 IPC 를 하기 위하여 만든 Pipe Server 클래스 이다. 이 클래스는 StartPipeServer 함수를 가지고 있으며, 이 함수를 호출하면, 서버 파이프를 초기화 하고, 클라이언트의 접속을 대기하는 쓰레드를 생성한다.

앞에서 설명한 대로라면, Pipe 는 Vista 에서 쉽게 동작하는 것 처럼 보였지만, 실제로는 한가지 더 신경써줘야 할 것이 있다. 바로 Pipe 의 권한이다. Vista 의 UAC 는 프로그램마다 권한을 가지도록 하였으며, 낮은 권한의 프로그램이 높은 권한의 프로그램이 가지고 있는 리소스에 접근할 수 없도록 제한하고 있다. 그래서 필요한 것이, Pipe 의 권한을 조절하는 것이다.

  BYTE sd[SECURITY_DESCRIPTOR_MIN_LENGTH];
  SECURITY_ATTRIBUTES sa;
 
  sa.nLength = sizeof(sa);
  sa.bInheritHandle = TRUE;
  sa.lpSecurityDescriptor = &sd;
 
  InitializeSecurityDescriptor(&sd, SECURITY_DESCRIPTOR_REVISION);
  SetSecurityDescriptorDacl(&sd, TRUE, (PACL) 0, FALSE);

     
  hPipe = CreateNamedPipe(
   pInfo->szPipeName,             // pipe name
   PIPE_ACCESS_DUPLEX,       // read/write access
   PIPE_TYPE_MESSAGE |       // message type pipe
   PIPE_READMODE_MESSAGE |   // message-read mode
   PIPE_WAIT,                // blocking mode
   PIPE_UNLIMITED_INSTANCES, // max. instances 
   BUFSIZE,                  // output buffer size
   BUFSIZE,                  // input buffer size
   0,                        // client time-out
   &sa);                    // default security attribute
 
위의 코드는 Pipe 를 생성할 때, Security Attribute 속성을 지정하여, 낮은 권한의 프로그램에서도 해당 Pipe 를 열 수 있도록 허용하는 코드이다.

이렇게 만들어진 Pipe 서버는 특정한 값을 받아서 처리하게 된다. 이 소스에서 필자는 Pipe 의 간단한 프로토콜을 정의하였는데, 그것은 다음과 같다.

프로토콜 정의

[CMDS (정상적인 PIPE 패킷이라는 서명(sign)), 4바이트]
[명령, 4바이트]
[명령 번호, 4바이트]
[추가 정보 길이, 4바이트]
[추가 정보, 추가 정보 길이에 명시된 길이 ~ 버퍼의 최대 길이]

이 프로토콜에 맞는 형식을 갖춘 데이터들은 내부적으로 사용자가 지정한 함수 포인터를 호출한다. 그리고 마지막으로, 그렇게 호출된 함수의 반환값을 다시 클라이언트에 보내주는 것으로 서버는 하나의 패킷에 대한 처리를 마무리 하게 된다.

이 클래스를 사용하는 방법은 매우 간단하다.
먼저, 클래스를 선언한 후, StartPipeServer 함수를 호출해 주면 된다.

다음은 간단한 예제이다.

#include "PipeServer.h"
CPipeServer g_pipeServer;

int WINAPI ProcessIO(DWORD dwCmd, DWORD dwCmdNumber, DWORD dwLen, char* lpszData, DWORD* pdwRetLen, char* lpszReturn)
{
 char szMsg[1024];
 sprintf(szMsg, "Cmd : %d, CmdNumber : %d, dwLen : %d, lpszData : %s",
  dwCmd, dwCmdNumber, dwLen, lpszData );

 strcpy( lpszReturn, "ProcessIO 에서 처리한 내용! : " );
 strcat( lpszReturn, szMsg );
 *pdwRetLen = strlen(lpszReturn);

 return 0;
}


DWORD WINAPI ServiceExecutionThread(LPDWORD param)
{
 g_pipeServer.StartPipeServer( "\\\\.\\pipe\\VistaSvcIPC", ProcessIO );
 return 0;
}

이렇게 구성된 프로그램을 서비스로 만들고, 일반 권한 프로그램에서 이 서비스와 통신하게 할 수 있다. 클라이언트에 대한 코드는 아래와 같다.

void CNormalPrgDlg::OnButton1()
{
 static BOOL bConnected = FALSE;

 if(bConnected) {
  CString szText;
  m_edt.GetWindowText( szText );

  char szBuffer[BUFSIZE] = { 0, };
  char szRetData[BUFSIZE] = { 0, };
  DWORD dwRetLen = 0;
  strcpy(szBuffer, szText);
  g_pipeClient.Send(1024, GetTickCount(), strlen(szBuffer)+1, szBuffer, &dwRetLen, szRetData);
 
  m_lst.InsertString(0, szRetData);
 } else {
  bConnected = g_pipeClient.ConnectPipe("\\\\.\\pipe\\VistaSvcIPC");
 }
}

 이 예제가 동작하는 모습은 아래와 같다.

사용자 삽입 이미지

[그림2] 실제 IPC 예제 프로그램이 동작하는 화면



첨부된 파일의 소스는 서버와 클라이언트이며, MFC 를 기반으로 작성되어 있다.


 

Trackback 1 Comment 1