책정리/Windows VIA C,C++

3장 커널 오브젝트

GONII 2021. 7. 1. 21:40

이번 장은 커널 오브젝트(kernel object)와 그 핸들(handle)을 다루는 마이크로소프트 윈도우 애플리케이션 프로그래밍 인터페이스(API)에 대한 설명부터 시작한다.

1. 커널 오브젝트란 무엇인가?

윈도우 소프트웨어 개발자는 항시 커널 오브젝트를 생성하고, 열고, 조작하는 등의 작업을 수행한다. 
운영체제는 액세스 토큰 오브젝트(access token object), 이벤트 오브젝트(event object), 파일 오브젝트(file object), 파일-매핑 오브젝트(file-mapping object), I/O컴플리션 포트 오브젝트(I/O completion port object), 잡 오브젝트(job object), 메일슬롯 오브젝트(mailslot object), 뮤텍스 오브젝트(mutex object), 파이프 오브젝트(pipe object), 프로세스 오브젝트(process object), 세마포어 오브젝트(semaphore object), 스레드 오브젝트(thread object) , 대기 타이머 오브젝트(waitable timer object), 스레드 풀 워커 팩토리 오브젝트(thread pool worker factory object) 등 다양한 형태의 커널 오브젝트를 생성하고 조작한다.
이러한 오브젝트들은 다양한 종류의 함수들을 통해 만들어지는데, 함수의 이름에 포함된 오브젝트의 명칭이 반드시 커널 레벨의 오브젝트 이름과 일치하는 것은 아니다. 예를 들어 CreateFileMapping함수는 파일 매핑과 관련된 Section오브젝트를 생성하도록 한다.
각 커널 오브젝트는 커널에 의해 할당된 간단한 메모리 블록이다. 이 메모리 블록은 커널에 의해서만 접근이 가능 구조체로 구성되어 있으며, 커널 오브젝트에 대한 세부 정보들을 저장하고 있다. 몇몇 값들(보안 디스크립터, 사용 카운트)은 모든 오브젝트 타입에 공통적으로 존재한다. 하지만 대부분의 값들은 각 오브젝트별로 독특하다. 예를 들어 프로세스 오브젝트는 프로세스 ID, 기본 우선순위와 같은 정보를 가지고 있는 반면 파일 오브젝트의 경우 바이트 오프셋, 공유 모드, 오픈 모드 와 같은 정보를 가지고 있다.
커널 오브젝트의 데이터 구조체는 커널에 의해서만 접근이 가능하기 때문에 애플리케이션에서 데이터 구조체가 저장되어 있는 메모리 위치를 직접 접근하여 그 내용을 변경하는 것은 불가능하다. 마이크로소프트는 커널 오브젝트의 구조체가 가능한 한 일관되게 유지될 수 있도록 하기 위해 이러한 제약사항을 의도적으로 만들어 두었다. 이렇게 구조체에 대한 직접적인 접근을 제한함으로써 마이크로소프트는 이미 개발되어 있는 애플리케이션에 영향을 미치지 않고도 구조체에 내용을 임의로 추가, 삭제, 변경할 수 있다.
마이크로소프트는 정제된 방법을 통해 구조체의 내용에 접근할 수 있도록 일련의 함수 집합을 제공하고 있어서 이를 통해 커널 오브젝트의 내부적인 값에 접근할 수 있다. 커널 오브젝트를 생성하는 함수를 호출하면 함수는 각 커널 오브젝트를 구분하기 위한 핸들 값을 반환해 준다. 핸들 값은 프로세스 내의 모든 스레드에 의해 사용 가능한 값이지만 특별한 의미를 가지고 있지는 않다. 핸들은 32비트 윈도우 프로세스에서는 32비트 값이고, 64비트 윈도우 프로세스에서는 64비트 값이다. 이렇나 핸들은 다양한 윈도우 함수들의 매개변수로 전달될 수 있는데, 운영체제는 매개변수로 전달된 핸들 값을 통해 어떤 커널 오브젝트를 조작하고자 하는지 구분할 수 있다.
운영체제를 견고하게 하기 위해 이러한 핸들 값들은 프로세스별로 독립적으로 유지된다. 만일 어떤 스레드가 다른 프로세스의 스레드에게 자신의 핸들 값을 전달했을 경우 이 핸들 값을 이용하여 수행하는 동작은 실패할 수도 있고, 혹은 더 좋지 않은 결과를 초래할 수도 있다. 이는 각 프로세스별로 독립된 프로세스 핸들 테이블이 존재하고 동일한 핸들 값이라도 전혀 다른 커널 오브젝트를 참조할 수 있기 때문이다.

2. 사용 카운트

커널 오브젝트는 프로세스가 아니라 커널에 의해 소유된다. 다시 말해 만일 프로세스가 특정 함수를 통해 커널 오브젝트를 생성한 후 종료된다 하더라도 반드시 생성된 커널 오브젝트가 프로세스와 함께 삭제되는 것은 아니라는 의미이다. 대부분의 경우 커널 오브젝트는 프로세스와 함께 삭제되겠지만 다른 프로세스가 동일 커널 오브젝트를 사용하고 있다면 커널 오브젝트를 사용하는 모든 프로세스가 종료될 때까지 삭제되지 않고 남아있게 된다. 반듯이 기억해야 할 점은 커널 오브젝트는 자신을 생성한 프로세스보다 더 오랫동안 삭제되지 않고 남아 있을 수 있다는 것이다. 각 커널 오브젝트는 내부적으로 사용 카운트 값을 유지하고 있기 때문에 커널은 이 값을 통해 얼마나 많은 프로세스들이 커널 오브젝트를 사용하고 있는지 알 수 있다. 사용 카운트는 모든 커널 오브젝트 타입이 가지고 있는 공통적인 값이다. 커널 오브젝트가 최초로 생성되면 이 값은 1로 설정된다. 다른 프로세스가 이미 생성된 커널 오브젝트에 접근 권한을 획득하면 사용 카운트가 증가된다. 프로세스가 종료되면 커널은 이 프로세스가 사용하고 있던 모든 커널 오브젝트의 사용 카운트를 감소시키고, 만일 이 값이 0이 되면 커널 오브젝트는 삭제된다.

3. 보안

커널 오브젝트는 보안 디스크립터를 통해 보호될 수 있다. 보안 디스크립터는 누가 커널 오브젝트를 소유하고 있으며, 어떤 그룹과 사용자들에 의해 접근되거나 사용될 수 있는지, 혹은 어떤 그룹과 사용자들에 대해 접근이 제한되어 있는지에 대한 정보를 가지고 있다. 보안 디스크립터는 서버 애플리케이션을 개발할 때 주로 많이 사용된다. 그렇지만 윈도우 비스타에서는 이러한 기능이 프라이비트 네임스페이스와 함께 클라이언트측 애플리케이션에서도 더욱 가시적이 되었며, 이러한 내용은 "4.5 관리자가 표준 사용자로 수행되는 경우" 절에서 다시 보게 된다.
커널 오브젝트를 생성하는 거의 대부분의 함수들은 SECURITY_ATTRIBUTES 구조체에 대한 포인터를 인자로 받아들인다.
대부분의 애플리케이션에서는 현재 프로세스의 보안 토큰을 근간으로 하는 기본 보안 디스크립터를 사용하기 때문에 커널 오브젝트 생성 시 단순히 NULL값을 전달하면 된다. 하지만 SECURITY_ATTRIBUTES구조체를 할당하고 초기화한 후 구조체의 주소를 념겨줄 수도 있다. SECURITY_ATTRIBUTES 구조체는 아래와 같다.

typedef struct _SECURITY_ATTRIBUTES {
    DWORD nLength;
    LPVOID lpSecurityDescriptor;
    BOOL bInheriHandle;
} SECURITY_ATTRIBUTES;

비록 이 구조체가 SECURITY_ATTRIBUTES로 불리긴 하지만 구조체 내의 lpSecurityDescriptor멤버만이 보안과 관련되어 있다. 만일 오브젝트 생성 시 커널 오브젝트의 접근 권한을 제한하고자 한다면 보안 디스크립터를 생성하고 다음과 같이 초기화를 수행해야 한다.

SECURITY_ATTRIBUTES sa;
sa.nLength = sizeof(sa);        // 버전 확인을 위한 정보
sa.lpSecurityDescriptor = pSD;    // 초기화된 SD 주소
sa.bInheritHandle = FALSE;    // 추후에 논의함
HANDLE hFileMapping = CreateFileMapping(INVALID_HANDLE_VALUE, &sa, PAGE_READWRITE, 
			0, 1024, TEXT("MyFileMapping"));

이미 존재하는 파일-매핑 커널 오브젝트를 이용하여 데이터를 읽으려 한다면 다음과 같이 OpenFileMapping을 호출하면 된다.
HANDLE hFileMapping = OpenFileMapping(FILE_MAP_READ, FALSE, TEXT("MyFileMapping"));
OpenFileMapping 함수는 유효한 핸들 값을 반환하기에 앞서 보안 권한을 먼저 확인한다. 로그인한 사용자가 이 파일-매핑 커널 오브젝트에 접근할 수 있는 권한이 있다면 OpenFileMapping 함수는 유효한 핸들 값을 반환한다. 하지만 접근이 거부될 경우 OpenFileMapping은 NULL 값을 반환하게 되고 GetLastError를 호출해보면 5(ERROR_ACCESS_DENIED)가 반환될 것이다. 만일 이렇게 획득된 핸들을 이용하여 FILE_MAP_READ 외의 다른 권한이 필요한 API를 호출하게 되면 "접근 거부" 에러가 발생하게 된다.
애플리케이션들은 커널 오브젝트 외에도 메뉴, 윈도우, 마우스 커서, 브러시, 폰트와 같은 또 다른 형태의 오브젝트를 다루기도 한다. 이러한 오브젝트들은 유버 오브젝트나 그래픽 디바이스 인터페이스(GDI) 오브젝트이며, 커널 오브젝트와는 서로 구분이 된다.
어떤 오브젝트가 커널 오브젝트인지 여부를 결정하는 가장 간단한 방법은 오브젝트를 생성하기 위함 함수가 무엇인지 찾아보고, 앞서 CreateFileMapping 함수에서 보여준 바와 같이 보안 특성을 지정하는 매개변수가 있는지를 확인하는 것이다.
유저 오브젝트나 GDI 오브젝트를 생성하는 함수 중 PSECURITY_ATTRIBUTES형의 매개변수를 취하는 함수는 없다.

4. 프로세스의 커널 오브젝트 핸들 테이블

프로세스가 초기화되면 운영체제는 프로세스를 위해 커널 오브젝트 핸들 테이블을 할당한다. 이러한 핸들 테이블은 사용자 오브젝트나 GDI 오브젝트에 의해서는 사용되지 않고 유일하게 커널 오브젝트에 의해서만 사용된다.
[표 3-1]은 프로세스의 오브젝트 핸들 테이블의 모습을 보여주고 있다. 보는 바와 같이 이는 단순한 데이터 구조체의 배열로 이루어져 있으며, 각 데이터 구조체는 커널 오브젝트에 대한 포인터, 액세스 마스크, 플래그로 구성된다.

[표 3-1] 프로세스 핸들 테이블의 구조

인덱스 커널 오브젝트의 메모리 블록을 가리키는 포인터 액세스 마스크(각 비트별 플래그 값을 가지는 DWORD) 플래그
0x????????  0x????????  0x???????? 
0x????????  0x????????  0x???????? 

4.1 커널 오브젝트 생성하기

프로세스가 최초로 초기화되면 프로세스의 핸들 테이블은 비어 있다. 프로세스 내의 스레드가 CreateFileMapping과 같은 함수를 호출하면 커널은 커널 오브젝트를 위한 메모리 블록을 할당하고 초기화한다. 이후 커널은 프로세스의 핸들 테이블을 조사하여 비어있는 공간을 찾아낸다. [표 3-1]에 나타난 핸들 테이블은 완전히 비어 있기 때문에 커널은 인덱스가 1인 위치를 찾아내고 초기화를 수행한다. 포인터 멤버를 커널 오브젝트의 자료 구조를 가리키는 내부적인 메모리 주소를 할당하며, 액세스 마스크는 "풀 액세스"로, 플래그는 "설정" 상태로 초기화된다.
커널 오브젝트를 생성하는 모든 함수는 프로세스별로 고유한 핸들 값을 반환하며, 이 값은 프로세스 내의 모든 스레드들에 의해 사용될 수 있다. 애플리케이션을 디버깅하거나 커널 오브젝트 핸들의 실제 값을 조사해보면 4, 8등과 같이 작은 값을 가지고 있음을 볼 수 있다. 만일 유효하지 않은 핸들 값을 전달하게 되면 이러한 함수들은 실패하고 GetLastError 호출 결과로 6(ERROR_INVALID_HANDLE)을 반환한다. 핸들 값은 실제로 프로세스 핸들 테이블의 인덱스 값으로 활용될 수 있기 때문에 프로세스별로 고유한 값이며, 다른 프로세스에 의해 사용될 수 없는 값이다.
커널 오브젝트를 생성하는 함수가 실패하면 반환되는 핸들 값은 보통 0(NULL)이 된다. 이러한 이유로 유효한 커널 오브젝트 핸들 값은 4부터 시작된다. 시스템의 가용 메모리가 매우 작거나 보안 문제로 인해 함수가 실패하는 경우 몇몇 함수들은 -1(INVALID_HANDLE_VALUE, WinBase.h에 정의된)을 반환하는 경우가 있다. 따라서 커널 오브젝트를 생성하는 함수의 반환 값을 확인할 때에는 상당한 주의가 필요하다.
커널 오브젝트 핸들을 인자로 취하는 함수를 호출할 때에는 항상 Create*류의 함수 중 하나를 호출하여 반환된 핸들 값을 전달해야 한다. 내부적으로 이러한 함수들은 프로세스 핸들 테이블로부터 사용하고자 하는 커널 오브젝트의 실제 주소를 얻어낸 후 잘 정의된 방식으로 커널 오브젝트의 자료 구조를 변경한다.

4.2 커널 오브젝트 삭제하기

커널 오브젝트를 어떻게 생성했는지와 상관없이 CloseHandle 함수를 호출하여 더 이상 커널 오브젝트를 사용하지 않을 것임을 시스템에게 알려줄 수 있다.

BOOL CloseHandle(HANDLE hObject);

내부적으로 이 함수는 프로세스의 핸들 테이블을 검사하여 전달받은 핸들 값을 통해 실제 커널 오브젝트에 접근 가능한지를 확인한다. 핸들이 유효한 값이고 시스템이 커널 오브젝트의 자료 구조를 획득하게 되면, 구조체 내의 사용 카운트 멤버를 감소시킨다. 만일 이 값이 0이 되면 커널 오브젝트를 파괴하고 메모리로부터 제거한다.
유효하지 않은 핸들(invalid handle)을 CloseHandle함수에 전달하면 두 가지 경우의 수가 생긴다. 첫째로, 프로세스는 정상적으로 수행되고 CloseHandle 함수는 FALSE를 반환한다. GetLastError를 호출하면 ERROR_INVALID_HANDLE 값을 반환한다. 또 다른 경우로는 프로세스가 디버깅 중인 경우로, 에러를 디버깅할 수 있도록 0xC0000008("유효하지 않은 핸들이 지정되었습니다")예외가 발생한다.
CloseHandle 함수는 반환되기 직전에 프로세스의 핸들 테이블에서 해당 항목을 삭제한다. 이렇게 되면 핸들은 더 이상 유효하지 않은 값이 되고 이 핸들로는 어떠한 작업도 수행할 수 없다. CloseHandle을 호출 하면 더 이상 해당 커널 오브젝트에 접근하는 것이 불가능해지지만, 커널 오브젝트 자체는 삭제되었을 수도 있고 그렇지 않을 수도 있다. 오브젝트의 사용 카운트가 0이 되지 않는 이상 커널 오브젝트는 파괴되지 않는다. 하나 혹은 다수의 다른 프로세스가 해당 커널 오브젝트를 여전히 사용하고 있는 경우라면 커널 오브젝트는 삭제되지 않는다. 다른 프로세스가 이 오브젝트를 더 이상 사용하지 않으면 오브젝트는 그때 비로소 파괴될 것이다.
CloseHandle을 호출하는 것을 잊어버리면 오브젝트 누수가 발생하게 될까? 그럴 수도 있고 아닐 수도 있다. 프로세스가 계속해서 수행 중이라면 오브젝트 누수 상황이 될 수 있다. 하지만 프로세스가 종료되면 운영체제는 프로세스가 사용하던 모든 리소스들을 반환한다. 커널 오브젝트의 경우 시스템은 다음과 같은 절차를 수행한다: 프로세스가 종료되면 운영체제는 프로세스의 핸들 테이블을 검사하여 테이블 상에 유효한 항목이 있는 경우 이러한 오브젝트 핸들을 삭제한다. 이 과정에서 커널 오브젝트의 사용 카운트가 0이 되면 커널 오브젝트도 파괴될 것이다.
따라서 애플리케이션 수행 중에는 커널 오브젝트에 대한 누수가 발생할 수 있지만, 프로세스가 종료될 때에는 시스템이 적절하게 모든 오브젝트 핸들을 정리해 주는 것을 보장하기 때문에 커널 오브젝트 누수 문제는 발생하지 않는다. 이러한 메커니즘은 GDI 오브젝트나 메모리 블록들에 대해서도 동일하게 적용된다. 프로세스가 종료되면 시스템은 프로세스와 관련된 어떠한 것도 남김없이 삭제한다.

5. 프로세스간 커널 오브젝트의 공유

서로 다른 프로세스에서 각기 수행되는 스레드들 간에 동일 커널 오브젝트를 공유해야 하는 경우는 빈번하게 발생할 수 있다. 여기에 몇 가지 예를 들어 보았다.
파일-매핑 오브젝트는 단일 머신에서 수행되는 두 프로세스 사이에서 데이터의 블록을 공유할 수 있도록 해준다.
메일슬롯과 명명 파이프를 이용하면 네트워크로 연결된 서로 다른 머신 사이에서 데이터를 주고받을 수 있다.
뮤텍스, 세마포어, 이벤트는 서로 다른 프로세스에서 수행되는 스레드 간에 동기화를 수행할 수 있게 해준다. 이를 이용하면 애플리케이션이 특정 작업을 완료했을 때 다른 애플리케이션에게 완료 사실을 통보해 줄 수 있다.
커널 오브젝트의 핸들은 프로세스별로 고유한 값이기 때문에 이러한 핸들 값을 공유하는 것은 간단하지 않다. 그럼에도 마이크로소프트가 핸들을 프로세스별로 고유한 값으로 설계한 이유가 있다. 가장 중요한 이유는 안정성이다. 만일 커널 오브젝트의 핸들 값이 시스템 전역적인 값이라면 어떤 프로세스라도 다른 프로세스가 사용하고 있는 오브젝트의 핸들 값을 획득할 수 있을 것이고, 이를 통해 간단히 다른 프로세스를 오작동하게 만들 수 있다. 또 다른 이유는 프로세스별로 고유한 핸들이 좀 더 보안에 강하기 때문이다. 커널 오브젝트는 보안 요소에 의해 보호되고 있으며, 커널 오브젝트를 사용하기 위해서는 먼저 적절한 권한을 획득해야만 한다. 커널 오브젝트를 생성하는 자는 접근 권한을 제한함으로써 허가되지 않은 사용자의 접근을 방지할 수 있다.

5.1 오브젝트 핸들의 상속을 이용하는 방법

오브젝트 핸들의 상속은 오브젝트를 공유하고자 하는 프로세스들이 페어런트-차일드 관계를 가질 때에만 사용될 수 있다. 즉, 하나 혹은 다수의 커널 오브젝트 핸들이 페어런트 프로세스에 의해 사용되고 있고, 페어런트 프로세스가 새로운 차일드 프로세스를 생성하기로 결정하였을 때 차일드 프로세스가 페어런트 프로세스가 사용하고 있는 커널 오브젝트에 접근할 수 있도록 해주는 방법이다.
먼저, 페어런트 프로세스는 커널 오브젝트를 생성할 때 이를 가리키는 핸들이 상속될 수 있음을 시스템에게 알려주어야 한다.
상속 가능한 핸들을 만들기 위해서는 페어런트 프로세스가 SECURITY_ATTRIBUTES 구조체를 초기화하고 이렇게 초기화된 값을 Create함수에 전달해야 한다. 다음은 뮤텍스 오브젝트를 생성하고 상속 가능한 핸들을 얻어내는 코드이다.

SECURITY_ATTRIBUTES sa;
sa.nLength = sizeof(sa);
sa.lpSecurityDescriptor = NULL;
sa.bInheritHandle = TRUE;
HANDLE hMutext = CreateMutext(&sa, FALSE, NULL);

위 코드는 기본 보안 디스크립터를 사용하고 상속 가능한 핸들을 반환하도록 SECURITY_ATTRIBUTES를 초기화한다.
이제 프로세스 핸들 테이블에 저장된 플래그 정보에 대해 알아볼 차례다. 각 핸들 테이블 요소는 핸들이 상속 가능한지 여부를 가리키는 플래그 비트를 가지고 있다. 만일 커널 오브젝트를 생성할 때 PSECURITY_ATTRIBUTES 매개변수로 NULL을 전달하면 반환되는 핸들은 상속 불가능하며, 상속 가능 여부를 나타내는 비트는 0이 된다. bInheritHandle 멤버를 TRUE로 지정하면 이 플래그 비트는 1로 설정된다. 이때 프로세스 핸들 테이블의 모습은 [표 3-2]와 유사할 것이다.

[표 3-2] 두 개의 유효한 요소를 가진 프로세스 핸들 테이블

인덱스 커널 오브젝트의 메모리 블록을 가리키는 포인터 액세스 마스크(각 비트별 플래그 값을 가지는 DWORD) 플래그
0xF0000000  0x????????  0x00000000 
0x00000000  (N/A)  (N/A) 
0xF0000010  0x????????  0x00000001 

[표 3-2]는 이 프로세스가 두 개의 커널 오브젝트(인덱스가 1, 3)에 접근할 수 있고, 인덱스가 1인 핸들은 상속 불가능하며, 인덱스가 3인 핸들은 상속 가능함을 보여주고 있다.
상속 가능한 오브젝트 핸들을 사용하기 위한 다음 단계는 페어런트 프로세스가 차일드 프로세스를 생성하는 것이다. 이러한 작업은 CreateProcess 함수를 이용하면 된다.

BOOL CreateProcess(
    PCTSTR pszApplicationName,
    PTSTR pszCommandLine,
    PSECURITY_ATTRIBUTES psaProcess,
    PSECURITY_ATTRIBUTES psaThread,
    BOOL bInheritHandles,
    DWORD dwCreationFlags,
    PVOID pvEnvironment,
    PCTSTR pszCurrentDirectory,
    LPSTARTUPINFO pStartupInfo,
    PROCESS_INFORMATION pProcessInformation);

bInheritHandles 매개변수는 시스템에게 차일드 프로세스가 페어런트 프로세스 핸들 테이블에 있는 상속 가능한 핸들을 상속하기를 원하지 않는다는 것을 시스템에게 알려주는 역할을 한다.
bInheritHandles에 TRUE를 전달하면 차일드 프로세스는 페어런트 프로세스의 상속 가능한 핸들 값들을 상속하게 된다. bInheritHandles에 TRUE를 전달하면 운영체제는 한 가지 추가적인 작업을 수행한다. 그것은 페어런트 프로세스의 핸들 테이블을 조사하여 상속 가능한 핸들을 찾아내는 일이다. 시스템은 찾아낸 항목들을 차일드 프로세스의 핸들 테이블에 복사한다. 이때 차일드 프로세스 핸들 테이블 내의 복사 위치는 페어런트 프로세스 핸들 테이블에서의 위치와 정확히 일치한다. 이것은 매우 중요한데, 이렇게 함으로써 특정 커널 오브젝트를 구분하는 핸들 값이 페어런트 프로세스와 차일드 프로세스에 걸쳐 동일한 값을 이용할 수 있게 되기 때문이다.
이제 두 개의 프로세스가 동일한 커널 오브젝트를 사용하게 되므로, 운영체제는 핸들 테이블의 항목을 복사하는 작업과 병행하여 커널 오브젝트 내의 사용 카운트를 증가시킨다. 커널 오브젝트를 파괴하려면 페어런트 프로세스와 차일드 프로세스 양쪽에서 모두 CloseHandle함수를 호출하거나 프로세스를 종료하면 된다.
오브젝트 핸들 상속은 차일드 프로세스를 새로 생성할 때에만 적용이 가능함을 알아야 한다. 만약 부모 프로세스가 상속이 가능하도록 새로운 커널 오브젝트를 생성한다 하더라도, 이미 수행되고 있었던 차일드 프로세스는 이 새로운 핸들을 상속받지 못한다.
오브젝트 핸들 상속은 매우 이상한 특징을 하나 가지고 있다. 오브젝트 핸들 상속을 사용하면 차일드 프로세스는 어떤 핸들이 상속된 것인지 알 수 없다. 커널 오브젝트 핸들 상속은 차일드 프로세스가 다른 프로세스에 의해 생성될 때 어떤 커널 오브젝트에 접근해야 할지 알고 있을 때에 한해서 유용하다.
차일드 프로세스가 사용할 커널 오브젝트의 핸들을 전달하는 가장 일반적인 방법은 차일드 프로세스 수행 시 명령행 인자를 이용하여 커널 오브젝트의 핸들 값을 전달하는 것이다. 차일드 프로세스의 초기화 코드는 명령행 인자를 분석하여(보통 _stscanf_s를 사용하여) 핸들 값을 얻어낼 수 있다. 이렇게 획득된 핸들 값은 페어런트 프로세스에서와 동일한 접근 권한을 가지게 된다. 핸들 상속은 공유 커널 오브젝트에 대한 핸들 값이 페어런트 프로세스와 차일드 프로세스 사이에서 동일하게 유지되는 유일한 공유 방법이기도 하다. 이러한 이유로 페어런트 프로세스는 명령행 인자를 핸들 값으로 전달할 수 있다.
물론 프로세스간 통신 방법을 이용하여 페어런트 프로세스가 차일드 프로세스에게 상속한 커널 오브젝트의 핸들을 전달할 수도 있다. 첫 번째 방법은 차일드 프로세스의 스레드가 생성한 윈도우로 메시지를 센드(send)하거나 포스트(post)하는 것이다.
또 다른 방법으로는 페어런트 프로세스가 환경변수 블록에 상속할 커널 오브젝트에 대한 핸들 값을 가지고 있는 새로운 환경변수를 추가하는 것이다. 변수의 이름은 차일드 프로세스와 약속된 이름이라면 무엇이든 사용될 수 있다.

5.2 명명된 오브젝트를 사용하는 방법

프로세스 간에 커널 오브젝트를 공유하는 두 번째 방법은 명명된 오브젝트를 사용하는 방법이다. 모두는 아니지만 대부분의 커널 오브젝트는 이름을 가질 수 있다.
명명된 오브젝트를 생성할 수 있는 커널 오브젝트 생성 함수들은 공통적으로 마지막 매개변수로 pszName을 가진다. 이 매개변수로 NULL을 전다랗명 명명되지 않은(익명의) 커널 오브젝트를 생성하게 된다. 명명되지 않은 오브젝트를 생성할 경우라도 핸들 상속이나 DuplicateHandle을 이용하여 프로세스 간에 커널 오브젝트를 공유할 수 있다.
pszName에 NULL대신 '\0'으로 끝나는 문자열을 가리키는 주소를 전달하여 오브젝트의 이름을 지정할 수 있다.
타입은 다르지만 동일한 이름의 다른 커널 오브젝트가 이미 존재할 경우 NULL을 리턴하고 생성에 실패한다.
이제 어떻게 오브젝트를 공유하는지 알아본다. A프로세스가 수행되어 다음과 같이 함수를 호출했다고 하자.
HANDLE hMutextProcessA = CreateMutex(NULL, FALSE, TEXT("Mutext1");
B프로세스가 수행되면 다음과 같은 코드를 수행한다.
HANDLE hMutextProcessB = CreateMutex(NULL, FALSE, TEXT("Mutext1");

B프로세스가 CreateMutex를 호출하게 되면 운영체제는 먼저 Mutext1이라는 이름의 커널 오브젝트가 존재하는지 확인한다. 만일 동일 이름의 오브젝트가 존재한다면, 다음으로 오브젝트의 타입을 확인한다. 왜냐하면 "Mutext1"이라는 이름의 뮤텍스를 생성하려는 것이기 때문에 이미 생성된 오브젝트의 타입도 뮤텍스이어야 할 것이다. 이후 운영체제는 B프로세스가 오브젝트에 대한 최대 접근 권한을 가지고 있는지 확인한다. 만일 그렇다면 운영체제는 B프로세스의 핸들 테이블 상에 비어 있는 항목을 추가하고 이미 존재하고 있던 커널 오브젝트를 가리키도록 설정한다. 만일 오브젝트의 타입이 일치하지 않거나 접근권한이 없을 경우 CreateMutex는 실패하고 NULL을 반환한다.
B프로세스가 CreateMutex 호출에 성공한다 하더라도 실제로는 새로운 뮤텍스가 생성되는 것이 아니라 기존의 뮤텍스 오브젝트에 접근할 수 있는 B프로세스 고유의 핸들 값이 생성될 뿐이다. 물론 B프로세스의 핸들 테이블은 이러한 오브젝트를 참조하는 항목을 가지고 있으며, 뮤텍스 오브젝트의 사용 카운트 값은 증가될 것이다. 이제 이 뮤텍스 오브젝트는 A프로세스와 B프로세스 양쪽 모두에서 관련 핸들을 삭제할 때까지 파괴되지 않는다.
명명된 오브젝트가 이미 생성되어 있는 경우에 한해서만 활용할 수 있는 Open*류의 함수들도 있다.
Create*류의 함수와 Open*류의 함수 사이의 주요 차이점은 커널 오브젝트가 존재하지 않는 경우에 Create*류의 함수는 새로운 오브젝트를 생성하지만 Open*류의 함수는 실패한다는 것이다

  • 터미널 서비스 네임스페이스
    터미널 서비스를 수행하는 머신은 커널 오브젝트에 대해 다수의 네임스페이스를 가진다. 모든 터미널 서비스 클라이언트 세션에서 접근 가능한 커널 오브젝트를 위한 전역 네임스페이스가 있는데, 이는 주로 서비스 타입의 애플리케이션에 의해 사용된다. 이와는 별도로 각 클라이언트 세션은 자신만의 고유 네임스페이스를 가지게 된다. 이러한 구성으로 인해 두 개 혹은 다수의 세션에서 동일한 애플리케이션이 각기 수행될지라도 서로간에 영향을 미치지 않게 된다. 하나의 세션은 설사 오브젝트의 이름이 같은 경우라도 다른 세션의 오브젝트에 접근할 수 없다. 이러한 시나리오는 서버 머신에서만 적용되는 내용이 아니라 리모트 데스크톱이나 빠른 사용자 전환에서도 동일하게 적용된다.
    만일 어떤 터미널 세션에서 특정 프로세스가 수행되고 있는지를 알아내고 싶다면 ProcessIdToSessionId함수를 이용하면 된다.
    서비스에서 사용되는 명명된 커널 오브젝트는 항상 전역 네임스페이스에 생성된다. 기본적으로, 터미널 서비스에서 기동되는 애플리케이션은 각 세션별 네임스페이스 내에 명명된 커널 오브젝트를 생성한다. 하지만 아래 예제와 같이 오브젝트의 이름 앞에 "Global\\"를 붙여주어 전역 네임스페이스 내에 커널 오브젝트를 생성하도록 명시할 수도 있다.
    HANDLE h = CreateEvent(NULL, FALSE, FALSE, TEXT("Global\\MyName");
    오브젝트 이름 앞에 "Local\\"를 붙여주어 현재 세션의 네임스페이스 내에 커널 오브젝트를 생성하도록 명시할 수도 있다.
  • 프라이비트 네임스페이스
    커널 오브젝트를 생성할 때 SECURITY_ATTRIBUTES 구조체의 포인터를 전달함으로써 오브젝트에 대한 접근을 보호할 수 있다. 하지만 윈도우 비스타 출시 이전에는 다른 프로세스가 공유 오브젝트의 이름을 훔치는 것으로부터 보호할 수 없었다. 설령 가장 낮은 권한에서 수행되는 프로세스라 하더라도 동일 이름으로 오브젝트를 생성하는 것이 가능했다. 이렇게 만들어진 애플리케이션이 사용자의 애플리케이션보다 먼저 수행되고 있다면 사용자 애플리케이션은 동일한 애플리케이션이 이미 수행되고 있다고 판단하고 바로 종료되어버릴 것이다. 이러한 방식은 서비스 거부 공격의 기본 메커니즘이 된다. 명명되지 않은 오브젝트는 서비스 거부 공격의 대상이 되지 않는다. 따라서 프로세스 간에 공유할 필요가 없는 오브젝트의 경우 명명되지 않은 커널 오브젝트를 사용하는 것이 좀 더 일반적이다.
    애플리케이션에서 생성한 명명된 커널 오브젝트가 다른 애플리케이션에서 사용하는 오브젝트의 이름과 절대로 출돌하지 않으며, 이름을 훔치려는 시도로부터 안전하기를 원한다면 Global이나 Local을 사용하는 것과 같이 사용자 고유의 프라이비트 네임스페이스를 만들어서 사용하면 된다.

5.3 오브젝트 핸들의 복사를 이용하는 방법

프로세스 간에 커널 오브젝트를 공유하는 마지막 방법은 DuplicateHandle 함수를 사용하는 것이다.

BOOL DuplicateHandle(
    HANDLE hSourceProcessHandle,
    HANDLE hSourceHandle,
    HANDLE hTargetProcessHandle,
    PHANDLE phTargetHandle,
    DWORD dwDesiredAccess,
    BOOL bInheritHandle,
    DWORD dwOptions);

DuplicateHandle 함수는 특정 프로세스 핸들 테이블 내의 항목을 다른 프로세스 핸들 테이블로 복사하는 함수다.

커널 오브젝트의 상속 방법과 마찬가지로 DuplicateHandle 함수의 단점 중의 하나는 T프로세스가 새로운 커널 오브젝트에 접근 가능하게 되었다는 사실을 전혀 통보받지 못한다는 것이다. 따라서 C프로세스는 T프로세스에게 어떤 방법으로든 새로운 커널 오브젝트에 접근이 가능해졌다는 사실을 알려주어야 한다. 이때 프로세스간 통신 방법을 이용하여 T프로세스에게 hObj 변수가 가지고 있는 핸들 값을 전달해야 할 것이다. T프로세스는 이미 수행 중인 프로세스이므로 명령행 인자를 사용하거나 T프로세스의 환경변수 값을 바꾸는 것과 같은 방법은 사용될 수 없다. 대신 윈도우 메시지나 프로세스간 통신 메커니즘(IPC)을 사용하면 된다.

반응형

'책정리 > Windows VIA C,C++' 카테고리의 다른 글

1장 에러 핸들링  (0) 2019.02.23
목차  (0) 2019.02.23