MS-RPC와 그 보안 메커니즘에 관한 개요
RPC란 무엇인가요?
RPC(Remote Procedure Call)는 IPC(Interprocess Communication)의 한 형태입니다. RPC를 통해 클라이언트는 RPC 서버에서 노출하는 프로시저를 호출할 수 있습니다. 클라이언트는 원격 상호 작용에 대한 세부 정보를 코딩할 필요 없이 일반적인 프로시저 호출인 것처럼 함수를 호출합니다. 서버는 동일한 시스템 또는 원격 시스템에서 다른 프로세스로 호스팅할 수 있습니다.
이 문서에서는 Microsoft의 RPC(MS-RPC) 구현에 대해 살펴봅니다. MS-RPC는 분산 컴퓨팅 환경(DCE)의 핵심에 있는 RPC 프로토콜의 참조 구현(V1.1)에서 도출됩니다.
RPC는 Windows에서 작업 예약, 서비스 생성, 프린터 및 공유 설정, 원격에 저장된 암호화된 데이터의 관리 등 다양한 서비스에 많이 사용됩니다. RPC는 원격 기법이라는 특성 때문에 보안 관점에서 많은 관심을 받고 있습니다. 이 블로그 게시물에서는 MS-RPC의 작동 방식에 관한 기본 사항을 다루며 통합된 보안 메커니즘에 중점을 둡니다.
MS-RPC는 어떻게 작동하나요?
프로시저와 그 매개변수는 IDL(인터페이스 정의 언어)이라는 설명 언어로 정의됩니다. 각 인터페이스에는 UUID라고 하는 고유 식별자가 할당되며, 이 식별자는 서버와 클라이언트 모두에서 서버가 노출하는 정확한 인터페이스를 처리하는 데 사용됩니다.
그런 다음, Microsoft IDL 컴파일러를 사용해 IDL 파일을 런타임 기능이 포함된 헤더와 소스 코드 파일로 컴파일합니다. 기술적으로 말하자면 이 파일들은 서버와 클라이언트에서 모두 사용하는 스텁이며 rpcrt4.dll에 구현된 RPC 런타임에 제어를 전달합니다. 클라이언트 측의 RPC 런타임은 데이터를 정돈해 반대쪽의 RPC 런타임에 전달합니다(그림 1).
그림 1: RPC 런타임
서버와 클라이언트가 네트워크(또는 로컬) 관점에서 어떻게 통신하는지 궁금할 수 있습니다. 서버는 프로토콜 시퀀스와 엔드포인트의 조합을 등록해 들어오는 RPC 연결을 수신 대기합니다. 프로토콜 시퀀스의 예를 들면 다음과 같습니다. ncacn_ip_tcp (TCP), ncacn_np (명명된 파이프) 또는 ncalrpc (LPC). 명명된 파이프를 사용하는 경우, 엔드포인트는 TCP 5555 또는 \\pipe\\example과 같은 포트가 될 수 있습니다. 명명된 파이프는 숨겨진 IPC$ share를 사용해 TCP 포트 445를 통한 SMB 전송으로 전달됩니다. 프로토콜 시퀀스의 전체 목록은 Microsoft 웹사이트에 있습니다.
따라서 서버는 일부 엔드포인트에서 연결을 수신 대기합니다. 클라이언트는 어디에 연결해야 하는지 어떻게 알 수 있습니까? 이 질문에 대한 응답은 엔드포인트의 종류(동적 엔드포인트 또는 잘 알려진(정적) 엔드포인트)에 따라 다릅니다.
동적 엔드포인트는 서버 측의 엔드포인트 매퍼를 통해 등록된 엔드포인트입니다. 엔드포인트 매퍼(일명 'epmapper')는 서비스를 실제 엔드포인트에 매핑하는 RPC 서비스입니다. epmapper는 HTTP를 통한 RPC에 TCP 포트 135와 593을 사용합니다. 따라서 클라이언트는 epmapper를 사용해 원격 시스템에서 동적으로 등록된 모든 RPC 서버를 ( 지정된 API를 사용해) 열거할 수 있습니다.
잘 알려진(정적) 엔드포인트는 epmapper를 통해 등록되지 않은 엔드포인트입니다. 클라이언트는 서버가 미리 등록한 엔드포인트를 알고 있어야 합니다. 이 작업은 엔드포인트가 클라이언트와 서버의 코드에서 하드코딩되어 있거나 엔드포인트가 IDL 파일에 있는 경우에 수행할 수 있습니다. 인쇄 스풀러 서비스 IDL의 예를 들면 다음과 같습니다.
[
uuid(12345678-1234-ABCD-EF00-0123456789AB),
version(1.0),
ms_union,
endpoint("ncacn_np:[\\pipe\\spoolss]"),
pointer_default(unique)
]
RPC는 바인딩 핸들을 통해 클라이언트와 서버 간의 논리적 연결을 나타냅니다. 서버와 클라이언트 모두 인증 정보를 설정할 때와 같이 바인딩 데이터를 조작하는 함수를 사용합니다. 클라이언트가 바인딩에 인증을 설정하면 바인딩이 인증된 것으로 간주됩니다. 클라이언트 프로그램에서 RPC 함수를 호출하면 네트워크 바인딩이 진행됩니다. 네트워크 트래픽 측면에서 RPC 클라이언트는 패킷에 인증 정보가 포함된 바인드 요청을 전송해 RPC 상호 작용을 시작합니다. 서버는 bind_ack(확인됨) 또는 bind_nak(오류 발생)로 응답할 수 있습니다.
그림 2: 동적 엔드포인트 분해능을 보여주는 와이어샤크 스니펫
그림 2의 Wireshark 스니펫에서는 작업 스케줄러 인터페이스의 동적 엔드포인트 분해능을 볼 수 있습니다. 클라이언트가 작업 스케줄러에 대한 엔드포인트 정보를 갖고 있으면 새 연결이 생성됩니다. 그런 다음, 바인딩 인증의 일부로 AUTH3 패킷을 포함해 다른 바인딩 프로세스와의 새 연결을 볼 수 있습니다.
엔드포인트와 마찬가지로 바인딩은 세 가지 종류 즉, 자동 바인딩, 암시적 바인딩, 명시적 바인딩으로 구분됩니다(그림 3). 이 세 가지 바인딩은 바인딩 프로세스에 대한 애플리케이션의 제어량에서 차이가 있습니다.
- 자동 바인딩(폐기) – 클라이언트와 서버 애플리케이션에서 바인딩 프로세스를 처리하지 않고 그 대신 RPC 런타임에서 이 프로세스를 완전히 제어하도록 합니다.
- 암시적 바인딩 – 바인딩이 발생하기 전에 클라이언트에서 바인딩 핸들을 설정할 수 있습니다. 클라이언트에서 바인딩을 설정하면 RPC 런타임 라이브러리가 나머지 부분을 처리합니다.
- 명시적 바인딩 – 클라이언트가 바인딩 핸들을 설정해야 합니다. 그런 다음 RPC 런타임은 이 값을 서버로 전달합니다.
원격 요청에 대한 RPC 보안
이제 RPC의 작동 방식에 대한 기본 사항을 알았으므로 클라이언트가 하나의 함수에 접근하는 것을 차단할 수 있는 동작, 정책, 메커니즘에 대해 알아보고자 합니다. 이는 공격적인 관점과 방어적인 관점에서 모두 흥미롭습니다.
전송 인증
명명된 파이프 또는 HTTP와 같은 일부 전송에는 프로토콜의 일부에 속하는 인증이 있습니다. 예를 들어, 명명된 파이프는 인증이 있는 SMB를 통해 전달됩니다. 이는 기본적으로 서버가 명명된 파이프 엔드포인트를 등록할 때 유효한 사용자의 인증정보를 가진 클라이언트만 해당 엔드포인트에 연결할 수 있음을 의미합니다. 도메인 환경에서는 동일한 도메인에 도메인 사용자가 있으면 인증 검사를 충분히 통과할 수 있습니다. 시스템이 도메인에 속하지 않은 경우, 클라이언트는 원격 서버에 있는 로컬 사용자의 인증정보를 가지고 있어야 합니다(그림 4). 예외 사항은 이 게시물의 뒷부분에서 다루기로 하겠습니다.
그림 4: 접속을 거부하는 시스템 오류의 예
바인딩 인증
서버는 바인딩 인증을 사용해 인증 메커니즘을 가질 수 있습니다. 이렇게 하려면 서버에서 RpcServerRegisterAuthInfo를 호출해 인증 정보를 등록해야 합니다. 클라이언트는 서버에서 사용하는 올바른 서비스 사용자 이름과 보안 지원 공급자 메서드를 사용해야 합니다. 그렇지 않으면 서버에서 'RPC_S_UNKNOWN_AUTHN_SERVICE'를 수신합니다.
클라이언트는 RpcBindingSetAuthInfo, RpcBindingSetAuthInfoEx 등의 API를 사용해 바인딩에 인증 및 권한 부여 데이터를 설정함으로써 서버에 인증할 수 있습니다. 클라이언트는 인증 방법(예: NTLM, Kerberos, Negotiate, SCHANNEL 등)과 서비스 사용자 이름을 지정합니다.
알아두어야 할 두 가지 중요 사항:
클라이언트가 바인딩에 인증을 설정하고 서버에서 인증 정보를 등록하지 않은 경우, 서버는 'RPC_S_UNKNOWN_AUTHN_SERVICE'를 반환합니다.
서버가 인증 바인딩을 등록했다고 해서 클라이언트가 인증된 바인딩을 사용해야 하는 것은 아닙니다. 또한 RPC 런타임은 잘못된 인증정보가 있는 클라이언트 인증 바인딩을 보내지 않습니다. 이 게시물의 뒷부분에서는 서버가 인증되지 않은 바인딩에 대한 접속을 방지하는 방법에 대해 설명합니다.
여기서 주목해야 할 것은 RPC 런타임이 인증 정보를 전역적으로 저장한다는 점입니다. 즉, 두 개의 RPC 서버가 동일한 프로세스를 공유하고 이 중 하나가 인증 정보를 등록하면 다른 서버도 인증 정보가 있습니다. 이제 클라이언트는 각 서버에 접속할 때 바인딩을 인증할 수 있습니다. 이러한 동작을 일컬어 SSPI 멀티플렉싱이라고 합니다.
엔드포인트 보안
서버는 엔드포인트에 보안 설명자를 설정할 수 있습니다. 보안 설명자는 Windows의 일반적인 접속 검사 보안 메커니즘에 속합니다. 이 설명자는 하나의 개체에 접근할 수 있는 사람과 접근이 거부된 사람에 대한 '룰' 생성을 허용합니다. 해당 개체에 대한 접근이 시도되면 운영 체제는 호출자의 접속 토큰을 보안 설명자와 비교해 접근이 허용되는지 확인합니다. 이 경우, 그 개체는 엔드포인트에 해당되며 접속 토큰은 전송 프로토콜 인증에서 파생된 클라이언트의 접속 토큰입니다. 이 검사는 명명된 파이프, ALPC, HTTP(인증이 사용되는 경우) 등과 같은 인증된 전송에만 적용됩니다. TCP는 인증되지 않은 전송 프로토콜이므로 이 접속 검사를 받지 않습니다.
SSPI 멀티플렉싱과 마찬가지로 엔드포인트도 멀티플렉싱됩니다. 즉, 인터페이스와 엔드포인트는 바인딩되지 않습니다. 서버는 인터페이스와 엔드포인트를 별도로 등록합니다. 프로세스에 등록된 엔드포인트가 여러 개인 경우, 이 프로세스에 등록된 각 인터페이스는 이들 엔드포인트 중 하나를 통해 접속할 수 있습니다.
예를 들어, 인터페이스를 등록하고 \\pipe\mypipe를 수신하는 엔드포인트를 생성한다고 가정합니다. 동일한 프로세스에서 호스팅되는 다른 RPC 서버가 자체 엔드포인트를 TCP 7777에 등록했습니다. TCP를 통해서도 인터페이스에 접속할 수 있습니다. 이렇게 하면 보안 설명자와 같이 엔드포인트에 적용되는 보안 제한을 우회합니다(그림 5). 따라서 권장사항은 보안에 의존하지 말고 클라이언트가 예상 전송 프로토콜을 통과했는지 확인하는 것입니다.
그림 5: 엔드포인트 멀티플렉싱으로 인한 엔드포인트 보안 설명자 우회의 예
인터페이스 보안
인터페이스 보안을 살펴볼 때 인터페이스를 보호하는 세 가지 방법으로는 보안 콜백 설정, 인터페이스에 보안 설명자 설정, 인터페이스 플래그 사용이 있습니다.
보안 콜백 설정
첫 번째 인터페이스 보안 메커니즘은 보안 콜백입니다. 보안 콜백은 서버 개발자가 구현한 사용자 지정 콜백입니다. 보안 콜백 내부의 논리는 개발자가 선택할 수 있으며, 이를 통해 서버에서 인터페이스에 대한 접속을 제한할 수 있습니다. 콜백이 RPC_S_OK를 반환하는 경우, 클라이언트는 허용됩니다.
보안 콜백이 등록된 경우, 인증되지 않은 클라이언트는 RPC 런타임에 의해 자동으로 거부됩니다. 단, 플래그RPC_IF_ALLOW_CALLBACKS_WITH_NO_AUTH 가 설정되지 않아야 합니다.
보안 콜백을 설정하고 RPC_IF_ALLOW_SECURE_ONLY 플래그를 설정한다고 해서 클라이언트에 높은 권한이 있는 것은 아닙니다. 따라서 보안 콜백은 서버가 클라이언트 권한 수준에 대해 문의할 수 있는 곳입니다. 이 작업은 RpcBindingInqAuthClient를 호출해 수행됩니다. 또 다른 일반적인 검사는 명명된 파이프와 같이 보안 인증된 전송 방식을 통해 연결을 적용하도록 클라이언트에서 사용하는 전송 방법입니다. 이렇게 하려면 RpcBindingServerFromClient → RpcBindingToStringBinding → RpcStringBindingParse를 호출하고 Protseq(프로토콜 시퀀스) 매개변수를 비교합니다. 이는 또한 엔드포인트 멀티플렉싱 남용을 방지합니다.
보안 콜백 캐싱
보안 콜백이 등록된 경우 이는 보안 검사 중에 호출됩니다. 보안 콜백이 통과하면(즉, RPC_S_OK 반환) 캐싱이 사용되는 경우 결과가 캐싱됩니다. 다음 번에 동일한 클라이언트가 인터페이스의 함수를 호출할 때 보안 콜백 결과가 캐싱되므로 보안 콜백이 다시 호출되지 않고 캐싱된 항목이 대신 사용됩니다.
캐싱의 구현은 간단하지만 몇 가지 요인에 따라 달라집니다.
- 캐싱은 바인딩에서 가져온 클라이언트의 보안 컨텍스트에 바인딩됩니다. 따라서 서버(또는 처리 중인 다른 서버)가 인증 바인딩을 등록하지 않았거나 클라이언트가 인증 바인딩을 설정하지 않은 경우, 이 호출에 대해 캐싱이 비활성화됩니다.
- 서버가 RPC_IF_SEC_NO_CACHE 플래그를 사용해 인터페이스를 등록하면 RPC 런타임은 각 호출에 대해 보안 콜백을 강제로 호출하므로 캐싱 메커니즘이 비활성화됩니다.
- 문서화되지 않은 인터페이스 플래그 RPC_IF_SEC_CACHE_PER_PROC도 캐싱 메커니즘에 영향을 미칩니다. 서버에서 이 플래그를 지정한 경우, 캐싱은 호출 기반으로 실행되며 인터페이스 기반으로 실행되지는 않습니다. 즉, 캐시가 TEST 인터페이스의 함수 X에 대한 성공 값을 보유하고 있는 경우 인터페이스 TEST에서 함수 Y를 호출하면 보안 콜백이 다시 트리거됩니다.
클라이언트가 호출하는 함수에 종속된 모든 보안 콜백에서는 캐싱 메커니즘으로 인해 논리 취약점이 발생할 수 있습니다. RPC 개발자 또는 감사자는 캐싱 메커니즘을 알고 있어야 합니다. 이 메커니즘으로 인해 발견된 취약점을 비롯해 캐싱 구현에 관한 철저한 연구를 발표했습니다.
보안 설명자 설정
인터페이스 보안을 위한 두 번째 메커니즘은 인터페이스에 보안 설명자를 설정하는 것입니다. 인터페이스에 보안 설명자를 등록하는 옵션은 RpcServerRegisterIf3 함수에만 있습니다. 보안 설명자가 설정되지 않은 경우 기본 보안 설명자는 다음과 같습니다.
- NT AUTHORITY\ANONYMOUS LOGON
- Everyone
- NT AUTHORITY\RESTRICTED
- BUILTIN\Administrators
- SELF
인터페이스 플래그 사용
인터페이스를 보호하는 세 번째 방법은 RpcServerRegisterIf* 함수를 통해 인터페이스를 만들 때 설정되는 인터페이스 플래그를 사용하는 것입니다. 보안 측면에서 흥미로운 플래그를 열거하면 다음과 같습니다.
RPC_IF_ALLOW_CALLBACKS_WITH_NO_AUTH – 사용된 전송 방법이나 인증 수준에 관계없이 각 호출에 대해 보안 콜백이 호출됩니다.
RPC_IF_ALLOW_LOCAL_ONLY – 로컬 요청만 허용됩니다.
RPC_IF_ALLOW_SECURE_ONLY – 연결은 권한 수준이 RPC_C_AUTHN_LEVEL_NONE보다 높은 클라이언트에만 허용됩니다. 이 플래그를 지정하면 NULL 세션을 통과하는 클라이언트가 거부됩니다. 이 플래그는 호출하는 사용자의 권한 수준을 보장하지 않으며 클라이언트에 유효한 인증정보가 있다는 것만 보장합니다.
RPC_IF_SEC_NO_CACHE – 이 플래그는 인터페이스의 보안 콜백에 대한 캐싱을 완전히 비활성화합니다.
RPC_IF_SEC_CACHE_PER_PROC – 이 플래그는 전체 캐싱 메커니즘을 비활성화하는 대신, 기본 동작을 인터페이스 기반이 아닌 호출 기반으로 변경합니다.
시스템 전체 정책
시스템 종류에 따라 클라이언트, 서버 또는 도메인 컨트롤러(DC) 등 다양한 시스템 전체 정책이 설정됩니다.
엔드포인트 보안과 관련된 흥미로운 시스템 정책은 '인증되지 않은 RPC 클라이언트 정책 제한'입니다. 세 가지 값을 설정할 수 있습니다.
- '인증됨' – RPC_IF_ALLOW_CALBACKS_WITH_NO_AUTH 플래그가 인터페이스에 설정되어 있지 않은 경우, RPC 런타임은 미리 인증되지 않은 TCP 클라이언트에 대한 접속을 차단합니다.
- '예외 없이 인증됨' – 인증되지 않은 모든 연결이 차단됩니다.
- "없음" – 모든 RPC 클라이언트가 시스템에서 실행 중인 RPC 서버에 연결할 수 있습니다.
따라서 인증되지 않은 클라이언트에 대한 흥미로운 공격표면은 TCP를 통해 접속할 수 있으며 RPC_IF_ALLOW_CALBACKS_WITH_NO_AUTH 플래그를 등록하는 인터페이스입니다. 물론 클라이언트는 인터페이스의 함수를 호출하기 위해 보안 콜백 검사를 통과해야 합니다.
앞서 언급한 것처럼 SMB를 통해 전달되는 명명된 파이프를 통한 연결은 SMB 프로토콜의 일부로서 자체 인증을 갖습니다. 클라이언트는 SMB를 통해 연결할 때 '익명 로그인'(NULL 세션이라고도 함)을 사용할 수 있습니다. 이와 관련된 두 가지 시스템 정책으로는 '명명된 파이프와 공유에 대한 익명 접근 제한'과 '네트워크 접속: 익명으로 접근할 수 있는 명명된 파이프'가 있습니다. 첫 번째 정책이 활성화된 경우, 두 번째 정책에 정의된 명명된 파이프만 익명으로 연결할 수 있습니다. 워크스테이션의 경우 두 번째 정책은 비어 있습니다. 즉, 도메인의 워크스테이션에 대해 NULL 세션을 사용할 수 없습니다. DC 시스템의 경우, 정책의 명명된 파이프 목록에는 '\pipe\netlogon', '\pipe\samr', '\pipe\lsarpc'가 포함됩니다. 이는 도메인 외부의 시스템이 연결할 수 있는 엔드포인트에 해당되기 때문에 공격자의 관점에서 볼 때 흥미롭습니다.
마지막으로, 엔드포인트 보안 설명자 검사와 관련해 기본적으로 비활성화된 시스템 전체 정책이 하나 있는데 그것은 바로 '네트워크 접속: Everyone 사용 권한을 익명 사용자에게 적용하도록 허용합니다'입니다. 이 정책을 활성화하면 Everyone 보안 식별자가 익명 연결을 위해 생성된 토큰에 추가됩니다.
절차 보안 검사
서버 프로그래머는 인터페이스에 노출되는 함수의 일부로 보안 검사를 구현할 수 있으므로 호출되는 함수에 따라 해당 검사의 논리를 변경하거나 사용자 지정할 수 있습니다. 일반적인 보안 검사는 그림 6에 표시된 것과 같은 접속 검사입니다(srvsvc에서 가져옴).
그림 6: 일반적인 보안 검사인 접속 검사의 예
이 예제에서 LocalrSessionGetInfo는 SsAccessCheck를 호출합니다. SsAccessCheck 는 기본적으로 RpcImpersonateClient를 호출한 다음 접속 검사를 수행하는 NtAccessCheckAndAuditAlarm함수를 호출해 클라이언트를 가장합니다. 그런 다음, 서버의 토큰으로 되돌리기 위한 RpcRevertToSelf 호출이 이어집니다. RpcImpersonateClient의 반환 값을 꼭 확인하는 것이 중요합니다. 이 반환 값이 실패하면 서버는 클라이언트의 프로세스가 아닌 서버 측 프로세스의 보안 토큰에서 계속 실행되기 때문입니다.
요약
이 블로그 게시물에서는 MS-RPC와 그 보안 메커니즘에 관한 많은 정보를 제공합니다. MS-RPC 서버는 새로운 취약점이 발생할 가능성이 있는 대규모 공격표면을 제공하므로 그 밖의 연구원들이 여러 MS-RPC 서버를 살펴보는 것이 좋습니다. Akamai의 게시물이 MS-RPC 연구에 첫발들 내딛는 다른 연구자들에게 도움이 되기를 바랍니다.
Akamai는 RPC에서 GitHub 리포지토리를 위한 더 많은 리소스를 지속적으로 만들고 수집하고 있습니다. 이 주제에 대한 지식과 경험을 제공한 모든 분들과 특히 James Forshaw 및 Carsten Sandker에게 감사의 말을 전합니다.