Session 0 Isolation 적용 전과 후

Windows XP, 2003 이하의 운영체제의 경우 처음으로 로그인하는 사용자의 user-mode 애플리케이션과 함께 서비스들이 모두 세션 0에서 실행되었다. 그 중에는 상승된 권한으로 실행되는 서비스들도 존재하였고 이 서비스들은 권한 상승을 노리는 공격자들의 타겟이 되었다.

 

이러한 문제를 해결하고자,그리고 세션 0에서 실행되는 서비스들은 상호작용 대상에서 제외시켰다. 이로 인해 더이상 서비스들은 다른 세션에서 실행되는 사용자 애플리케이션과 상호작용 할 수 없게 되었으며, 동시에 사용자 애플리케이션에 포함된 악성 코드로부터 보호되었다. 모든 서비스들과 user-mode 드라이버들은 세션 0에서 실행되도록 분리시키고, 사용자 세션은 세션 1 이후의 세션에서 실행되도록 하였다.

 

 

적용 대상

  • Windows Kernel 6 이상이 탑재되는 Windows 7 이후 운영체제부터 적용

 

영향

  • Session 0에서 실행되는 서비스가 UI와 같은 상호작용 기능을 수행할 경우, 서비스는 사용자의 Windows 세션과 다른 세션에서 독립적으로 실행되고 있기 때문에 정상적으로 서비스의 동작을 확인할 수 없다.
  • Window 메세지 기능을 이용하여 서비스와 애플리케이션 간의 통신을 수행하는 경우, 서비스와 애플리케이션이 서로 다른 메세지 큐를 갖게 되어 정상적으로 메세지 전달이 안된다.

 

Mitigation

  • Windows Vista에 내장된 mitigation을 사용할 경우,
    특수 desktop 내에서 세션 0과 사용자 간의 상호작용 가능
  • Windows XP compatibility mode를 사용할 경우,
    전역으로 생성된 오브젝트를 이용해 세션 0에서 실행되는 서비스와 사용자 애플리케이션 간의 작업 수행

 

해결 방안

  • Client - Server 모델을 적용하여 RPC나 명명된 파이프를 사용해서 서비스와 애플리케이션 간 통신하기
  • CreateProcessAsUser API를 사용하여 사용자의 세션에서 프로세스를 생성하고 상호작용 하기
  • 간단한 메세지 박스의 경우 WTSSendMessage API를 사용하여 사용자 세션에 알림 전달하기
  • 오브젝트에 명시적으로 Local\이나 Global\ 네임스페이스를 적용하여 서비스에서 사용할 수 있도록 하기

 


참고자료

  • 리버싱 핵심원리 42장
  • microsoft.com 웹페이지 본문
 

Application Compatibility - Session 0 Isolation

First published on TECHNET on Apr 27, 2007 In Windows XP, Windows Server 2003, and earlier versions of the Windows operating system, all services run in the..

techcommunity.microsoft.com

 

Interacting with Services

Table of contents Interacting with Services Article 09/14/2005 In this article --> In the comments for my first services post, someone asked about the SERVICE_INTERACTIVE_PROCESS flag that can be specified for the CreateService API. This flag allows the us

learn.microsoft.com

 

'OS' 카테고리의 다른 글

[Windows] Process 생성 및 종료 과정  (0) 2023.12.28
[Windows] Kernel Object  (0) 2023.12.27

 

Process

MSDN에서는 프로세스를 간단히 말해 “실행 중인 프로그램”으로 정의하고 있다. 하나의 프로세스는 하나 이상의 스레드(thread)를 갖는데, 이 중 첫 번째로 실행된 스레드를 primary 스레드라고 부른다. 프로세스의 주소 공간에 저장된 코드가 실제로 실행되기 위해서는 이를 실행하기 위한 스레드가 필요하다. 따라서 프로세스가 실행되면 주 스레드가 생성되고, 프로세스 내 모든 스레드가 종료될 경우 프로세스도 종료된다.

 

Process 구성 요소

프로세스는 크게 두 개의 컴포넌트로 구성된다.

  • 프로세스 커널 오브젝트
  • 사용할 코드와 데이터를 수용하기 위한 주소 공간

프로세스 커널 오브젝트는 프로세스 자체가 아니다. 프로세스를 관리하기 위한 목적으로 운영체제에서 사용되는 커널 오브젝트이며, 프로세스에 대한 통계 정보를 가지는 메모리 블록이다. 따라서 프로세스 생성과 종료 과정 시 가장 처음으로 생성되고, 가장 마지막으로 삭제된다.

각 프로세스는 프로세스 별 주소 공간을 가진다. 이 주소 공간에는 프로세스의 실행에 필요한 코드와 데이터가 저장될 공간이 있으며, 실행에 사용되는 Thread Stack이나 Heap 할당을 위한 메모리 공간도 포함된다.

 


Process 생성 과정

프로세스 생성 시 CreateProcess API를 이용하여 새로운 프로세스를 생성할 수 있다.

BOOL CreateProcess(
  [in, optional]      LPCTSTR               lpApplicationName,
  [in, out, optional] LPTSTR                lpCommandLine,
  [in, optional]      LPSECURITY_ATTRIBUTES lpProcessAttributes,
  [in, optional]      LPSECURITY_ATTRIBUTES lpThreadAttributes,
  [in]                BOOL                  bInheritHandles,
  [in]                DWORD                 dwCreationFlags,
  [in, optional]      LPVOID                lpEnvironment,
  [in, optional]      LPCTSTR               lpCurrentDirectory,
  [in]                LPSTARTUPINFOT        lpStartupInfo,
  [out]               LPPROCESS_INFORMATION lpProcessInformation
);
// T는 사용되는 문자 타입에 따라 None(ANSI)나 W(Unicode)가 될 수 있다.

 

이 API는 다음의 과정으로 Process를 생성한다.

  1. 프로세스 커널 오브젝트 생성
  2. 새 프로세스를 위한 가상 주소 공간 생성
  3. 생성된 가상 주소 공간에 실행 파일 코드와 데이터, 사용되는 DLL 파일 로드
  4. 새 프로세스의 primary 스레드에 대한 스레드 커널 오브젝트 생성
  5. primary 스레드에 의해 C/C++ 런타임 시작 함수 실행

여기서 5번 단계를 거쳐 실행된 C/C++ 런타임 시작 함수는 다음의 작업을 수행한다.

  • C/C++ 런타임 라이브러리 초기화: 전역변수와 Heap 초기화
  • 코드 상에서 선언된 전역 오브젝트, static 클래스 오브젝트 생성자 호출

 

이렇게 초기화 과정을 모두 완료하고 난 후 애플리케이션의 진입점 함수를 호출해 본 프로그램을 시작한다.

 

참고. Linker와 Loader

Linker와 Loader는 운영체제의 구성 요소로 프로그램을 개발하고 실행할 때 사용되는 요소이다. Linker는 프로그램 개발 시 컴파일 단계에서 생성된 여러 오브젝트 파일과 라이브러리 파일들을 하나의 실행 파일로 결합하기 위해 사용된다. Loader는 실행 파일의 코드와 데이터 등을 프로세스의 가상 주소 공간에 로드하기 위해 사용된다.

앞서 언급된 프로세스 생성 과정 중 Linker와 Loader가 기여하는 파트는 다음과 같다.

Linker는 프로세스 생성 과정 중 5번 단계에 사용되는 C/C++ 런타임 시작 함수를 결정한다. 이 때 결정의 기준이 되는 정보가 개발된 프로그램에 대한 Subsystem 링커 스위치이다. 이 링커 스위치는 실행 파일의 형태에 따른 필요 서브시스템 정보를 가지는데, CUI 프로그램일 경우 /SUBSYSTEM:CONSOLE 링커 스위치를 가지고 GUI 프로그램일 경우 /SUBSYSTEM:WINDOWS 링커 스위치를 가진다.

프로그램 빌드 단계에서 Linker는 이 서브시스템 정보를 확인한 후 CONSOLE 링커 스위치가 설정된 경우 프로그램 내 구현된 main이나 wmain 함수를 찾고, 이 함수가 존재하는 경우 mainCRTStartup이나 wmainCRTStartup 함수를 런타임 시작 함수로 설정한다. 만일 서브시스템 정보가 WINDOWS 링커 스위치로 설정된 경우 프로그램 내 WinMain이나 wWinMain 함수가 구현되었는지 확인하고, WinMainCRTStartup이나 wWinMainCRTStartup 함수를 런타임 시작 함수로 설정한다. 이렇게 설정된 런타임 시작 함수가 프로세스 생성 단계 5번에서 실행된다.

 

Loader는 3번 단계를 수행하는데 핵심적인 역할을 하며, 5번 단계에서 C/C++ 런타임 시작 함수가 실행되기 전 프로그램 실행에 필요한 환경을 구성하는 역할을 한다.

이 때에도 마찬가지로 프로그램의 서브시스템 정보를 확인한다. Windows의 PE 파일 헤더의 필드 중 subsystem 정보를 얻을 수 있는데, 이를 이용하여 실행에 필요한 서브시스템 정보를 얻을 수 있다. 그리하여 서브시스템 정보가 CONSOLE일 경우 프로그램 실행 전에 콘솔 윈도우를 생성하거나 기존에 열려있던 콘솔 윈도우를 사용하여 실행에 필요한 환경을 구성한다. 서브시스템 정보가 WINDOWS라면 곧바로 애플리케이션을 로드한다.

 


Process 종료 과정

프로세스는 여러 요인에 의해 종료될 수 있다. 정상적으로 프로그램이 시작하고 끝나면서 종료되는 경우도 있겠지만, 프로그램 내에서 호출된 ExitProcess 함수에 의해 종료될 수도 있고 다른 프로세스에서 호출된 TerminateProcess 함수에 의해서도 종료될 수 있다. 이 중 프로그램의 진입점 함수가 반환되면서 프로그램이 graceful 종료되는 상황에 대해 설명한다.

프로세스의 실행 과정을 보면 프로세스가 생성된 후 런타임 시작 함수가 호출되고, 런타임 시작 함수의 실행 과정에서 프로그램의 진입점 함수가 호출되며 프로그램이 실행되는 것을 알 수 있다. 그렇기에 함수의 실행 과정을 고려했을 때 진입점 함수가 반환 된 뒤 프로그램의 제어가 다시 런타임 시작 함수로 돌아올 것임을 알 수 있다. 그렇기에 진입점 함수가 반환 된 후 런타임 시작 함수로 제어가 돌아간 이후 단계부터 정리하였다.

진입점 함수가 반환되면 진입점 함수의 반환값을 인자로 갖는 C/C++ 런타임 라이브러리의 exit 함수를 호출한다. exit 함수는 다음의 작업을 수행한다.

  • _onexit 함수를 통해 프로그램 종료 시 호출되도록 등록해둔 루틴 수행
  • 모든 전역 클래스 오브젝트와 static C++ 클래스 오브젝트의 파괴자 호출
  • 진입점 함수의 반환값(종료 코드)을 인자로 갖는 ExitProcess 함수 호출

 

마지막 과정인 ExitProcess 함수를 호출하고 나면 운영체제에 의해 프로세스가 종료된다.

프로세스가 종료된 뒤에는 다음의 작업이 수행된다.

  1. 프로세스 내에 남아있는 스레드 종료
  2. 프로세스에 의해 할당되었던 모든 사용자 오브젝트와 GDI 오브젝트 삭제
  3. 프로세스에서 사용된 모든 커널 오브젝트에 대한 사용 카운트 1 감소
  4. 프로세스의 종료 코드를 STILL_ALIVE에서 반환된 종료 코드로 변경
  5. 프로세스 커널 오브젝트의 상태를 시그널 상태로 변경
  6. 프로세스 커널 오브젝트의 사용 카운트 1 감소

여기서 6번 단계 이후 프로세스 커널 오브젝트의 사용 카운트가 0이 되어야 프로세스의 커널 오브젝트까지 삭제되면서 프로세스의 모든 정보가 온전히 파괴된다. 만약 여전히 다른 프로세스에서 해당 프로세스의 정보를 이용하고자 프로세스 커널 오브젝트에 대한 핸들을 사용 중이라면 사용 카운트가 0이 되지 않아 곧바로 파괴되지 않을 것이다.

 


- 참고: 제프리 리처의 Windows via C/C++

'OS' 카테고리의 다른 글

[Windows] Session 0 isolation  (0) 2024.01.23
[Windows] Kernel Object  (0) 2023.12.27

 

Kernel Object

커널 오브젝트는 커널에 의해 할당된 메모리 블록으로, 커널에 의해서만 접근 가능한 객체이다. 이를 이용하여 시스템에서 사용되는 여러 자원들을 효과적으로 관리할 수 있다.

커널 오브젝트는 파일 오브젝트, Mutex 오브젝트, Process 오브젝트 등 여러 종류의 형태로 존재한다. 각 오브젝트는 보안과 안정성을 위해 커널에 의해서만 접근 가능하며, 이를 생성 및 조작하기 위해서는 Windows에서 제공하는 API를 이용해야 한다.

 

각 커널 오브젝트는 형태에 따라 서로 다른 정보를 가진다. 그럼에도 불구하고 다음의 두 정보는 커널 오브젝트 마다 공통적으로 사용된다.

1. 사용 카운트

: 해당 커널 오브젝트를 사용 중인 프로세스의 수. 0 이상의 정수값을 갖는다.

  • 커널 오브젝트를 최초 생성할 경우 사용 카운트값으로 1을 갖는다.
  • 커널 오브젝트를 사용하는 프로세스의 수가 증가할 때마다 사용 카운트값이 1씩 증가한다.
  • 커널 오브젝트의 사용을 종료(프로세스의 종료 또는 CloseHandle(hObj) 호출)할 때마다 사용 카운트 값이 1씩 감소한다.
  • 커널 오브젝트의 사용 카운트값이 0이 되면 해당 오브젝트가 삭제된다. (해당 커널 오브젝트에 대한 사용이 모두 종료되어야 삭제된다.)

 

2. 보안 디스크립터(SECURITY_ATTRIBUTES 구조체)

: 커널 오브젝트에 대한 소유자, 접근 권한에 대한 정보

커널 오브젝트와 유저 오브젝트/GDI 오브젝트를 구분하는 팁으로, 오브젝트 생성 API에 보안 특성을 지정하는 매개변수가 존재하는지를 확인하는 방법이 있다고 한다.

 


Handle & Process Handle Table

Windows API를 이용해 커널 오브젝트를 생성하는 데 성공하면 커널 오브젝트를 조작하는 데 사용 가능한 핸들(handle) 값을 얻을 수 있다. 이 핸들값을 다른 Windows API 매개변수로 전달하여 커널 오브젝트를 사용할 수 있다.

생성된 커널 오브젝트에 대한 핸들값은 각 프로세스마다 고유하다. 각 프로세스는 자신만의 독립된 프로세스 핸들 테이블을 갖기 때문에, 프로세스 내에서 생성된 핸들값은 해당 프로세스 내에서 생성된 모든 스레드에서 사용 가능하지만 다른 프로세스에서는 사용할 수 없다. 다음은 프로세스 핸들 테이블의 구조이다.

Process Handle Table

커널 오브젝트에 대한 핸들값은 프로세스 핸들 테이블에 매핑된 인덱스값과도 관련이 있는데, 가령 프로세스 A에서 프로세스 핸들 테이블 내 인덱스 2번을 가리키는 핸들값을 프로세스 B에서 동일하게 사용할 경우 프로세스 B의 프로세스 핸들 테이블 내 인덱스 2번에 다른 타입의 커널 오브젝트가 참조되어 있으면 잘못된 오브젝트를 참조하게 된다.

 


How to share Handles

앞서 설명한 바와 같이 두 프로세스 간 커널 오브젝트를 공유해야 할 경우 단순히 핸들값을 공유하는 방법은 옳지 않다. 그렇기에 이후 내용에서 서로 다른 두 프로세스 간에 커널 오브젝트를 공유하기 위한 방안을 정리하였다.

 

1. Kernel Object Handle 상속

이 방법은 커널 오브젝트를 공유하고자 하는 두 프로세스가 Parent - Child 관계일 경우 사용 가능하다. 이 방법은 크게 다음과 같은 단계로 진행된다.

  1. Parent process에서 상속하고자 하는 핸들 설정: 핸들 생성 또는 핸들 속성 변경 시
  2. Child process 생성 시 child process에게 핸들을 상속할 것인지 유무 설정

위 두 단계를 거치고 나면 Parent process의 process handle table 내 상속된 핸들에 대한 레코드가 새로 생성된 Child process의 process handle table 내 동일한 인덱스로 복사된다.

이러한 메커니즘으로 인해 핸들 상속 후 Parent process에서 대상 커널 오브젝트에 대해 사용 중이던 핸들의 사용을 종료하더라도, Child process의 process handle table 내에 커널 오브젝트에 대한 정보가 남아있으므로 Child process에서 상속된 핸들을 통해 여전히 커널 오브젝트에 접근할 수 있다. 또한 상속을 통해 대상 커널 오브젝트를 사용하는 프로세스의 수가 1 증가하였으므로 대상 커널 오브젝트의 사용 카운트값도 1 증가하게 된다.

 

2. Kernel Object의 이름 이용

이 방법은 커널 오브젝트 생성 시 고유한 이름을 부여하여, 다른 프로세스에서 커널 오브젝트의 이름을 이용해 대상 오브젝트에 대한 핸들을 얻는 방식이다. 이 방식이 가능한 이유는 커널에 의해 할당된 커널 오브젝트들은 타입에 상관없이 동일한 네임스페이스를 공유하기 때문에 서로 다른 프로세스라 하더라도 공유 중인 네임스페이스 내 정의된 커널 오브젝트의 이름을 이용하여 공유할 수 있다. 따라서 하나의 네임스페이스 내에서 명명되는 커널 오브젝트들은 고유한 이름을 가져야 하며, 다른 타입의 커널 오브젝트라 하더라도 서로 다른 이름을 가져야 한다.

만일 명명하고자 하는 커널 오브젝트의 이름이 이미 네임스페이스 내에서 사용 중인 경우 커널 오브젝트의 생성은 실패할 것이다. 이러한 메커니즘을 악용하면, 공유 중인 네임스페이스를 통해 공격 대상 프로세스에서 사용할 커널 오브젝트의 이름을 수집한 후 미리 해당 이름을 갖는 커널 오브젝트를 생성하여 공격 대상 프로세스의 정상 구동을 방해하는 공격(DoS 공격의 일종)에 취약해진다.

따라서 이를 막고자 Private 네임스페이스를 사용하여, 다른 프로세스에서 사용 중인 이름과 충돌되거나 사용되는 이름이 탈취되는 것으로부터 안전해질 수 있다. 이 때 Boundary descriptor를 함께 사용하여 private 네임스페이스에 대한 접근 권한을 설정하는 것으로 private 네임스페이스 자체에 대한 보안을 강화할 수 있다.

추가로, 터미널 서비스를 사용하는 경우에는 모든 클라이언트 세션으로부터 접근 가능한 Global 네임스페이스가 있고, 각 클라이언트 세션별 고유 네임스페이스(Local)가 존재한다. 기본적으로 터미널 서비스에서 사용되는 커널 오브젝트는 Global 네임스페이스 내 명명되고, 각 터미널 서비스 내 애플리케이션에서 사용되는 커널 오브젝트는 Local 네임스페이스에 명명된다. 그렇기 때문에 서로 다른 클라이언트 세션에서 동일한 애플리케이션을 실행하여도 충돌이 발생하지 않을 수 있다.

 

3. Kernel Object Handle 복사

이 방법은 source process의 process handle table 내에 기록된 커널 오브젝트에 대한 참조를 target process의 process handle table로 복사하는 것이다. 이를 위해 DuplicateHandle API를 사용하면 간편하게 복사할 수 있다.

BOOL DuplicateHandle(
  [in]  HANDLE   hSourceProcessHandle,
  [in]  HANDLE   hSourceHandle,
  [in]  HANDLE   hTargetProcessHandle,
  [out] LPHANDLE lpTargetHandle,
  [in]  DWORD    dwDesiredAccess,
  [in]  BOOL     bInheritHandle,
  [in]  DWORD    dwOptions
);

이 함수를 호출하면 공유하고자 하는 target process에서 사용 가능한 커널 오브젝트의 핸들값을 얻을 수 있다. 만일 source process에서 위 함수를 사용하여 target process를 위한 핸들값을 구했다면 IPC와 같은 프로세스 간 통신 방법으로 target process에 핸들값을 전달하여 커널 오브젝트를 사용하도록 할 수 있다.

 


- 참고: 제프리 리처의 Windows via C/C++

 

'OS' 카테고리의 다른 글

[Windows] Session 0 isolation  (0) 2024.01.23
[Windows] Process 생성 및 종료 과정  (0) 2023.12.28

+ Recent posts