소스 파일 다운로드
이러한 제약 사항들은 운영체제나 사용자의 보안성에 좋은 영향을 주지만, 시스템 유틸리티나 보안 소프트웨어를 개발하는 사람들에게 있어서는 매우 치명적인 일이 될 수 밖에 없다.
하지만, 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 의 권한을 조절하는 것이다.
위의 코드는 Pipe 를 생성할 때, Security Attribute 속성을 지정하여, 낮은 권한의 프로그램에서도 해당 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 서버는 특정한 값을 받아서 처리하게 된다. 이 소스에서 필자는 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 를 기반으로 작성되어 있다.