Descripción general de MS-RPC y sus mecanismos de seguridad
¿Qué es RPC?
Una llamada a procedimiento remotoRPC) es una forma de comunicación entre procesos (IPC). Permite a un cliente invocar un procedimiento expuesto por un servidor RPC. El cliente llama a la función como si fuera una llamada a procedimiento normal, sin (casi) necesidad de codificar los detalles para la interacción remota. El servidor se puede alojar en un proceso diferente en la misma máquina o en una máquina remota.
En este artículo, analizaremos la implementación de RPC de Microsoft (MS-RPC). MS-RPC se deriva de la implementación de referencia (V1.1) del protocolo RPC en el núcleo del entorno informático distribuido.
Windows utiliza mucho RPC para muchos servicios diferentes, como la programación de tareas, la creación de servicios, la configuración de impresoras y recursos compartidos y la gestión de datos cifrados almacenados de forma remota. Debido a la naturaleza de RPC como vector remoto, recibe mucha atención desde el punto de vista de la seguridad. Esta publicación del blog tratará de cubrir los conceptos básicos sobre el funcionamiento de MS-RPC, centrándose en los mecanismos de seguridad incorporados.
¿Cómo funciona MS-RPC?
Los procedimientos y sus parámetros se definen en un lenguaje descriptivo denominado lenguaje de definición de interfaz (IDL). A cada interfaz se le asigna un identificador único, denominado UUID, que utiliza tanto el servidor como el cliente para abordar la interfaz exacta que expone el servidor.
A continuación, el archivo IDL se compila mediante el compilador IDL de Microsoft en archivos de código fuente y encabezado que contienen funcionalidad de tiempo de ejecución. Técnicamente hablando, estos son stubs utilizados por el servidor y el cliente, y pasan el control al tiempo de ejecución de RPC, que se implementa en rpcrt4.dll. El tiempo de ejecución de RPC en el lado del cliente calcula los datos y los pasa al tiempo de ejecución de RPC al otro lado (Figura 1).
Fig. 1: Tiempo de ejecución de RPC
Puede preguntarse cómo se comunican el servidor y el cliente desde un punto de vista de red (o local). El servidor escucha las conexiones RPC entrantes registrando una combinación de una secuencia de protocolo y un terminal. Una secuencia de protocolo puede ser, por ejemplo, ncacn_ip_tcp (TCP), ncacn_np (canal con nombre), o ncalrpc (LPC). El terminal puede ser un puerto como TCP 5555 o \\pipe\\example, si se utiliza un canal con nombre. Los canales con nombre se transportan a través del transporte SMB a través del puerto TCP 445 utilizando el uso compartido de IPC$ oculto. La lista completa de secuencias de protocolos está disponible en el sitio web de Microsoft.
Bien, el servidor escucha las conexiones en algún terminal. ¿Cómo sabe el cliente dónde conectarse? La respuesta depende del tipo de punto terminal que sea: dinámico o conocido (también conocido como estático).
Un terminal dinámico es un terminal registrado a través del asignador de terminales en el lado del servidor. El asignador de terminales (también conocido como epmapper) es un servicio RPC que asigna un servicio al terminal real. El epmapper utiliza los puertos TCP 135 y 593 para RPC mediante HTTP. Por lo tanto, un cliente puede enumerar (mediante API designadas) todos los servidores RPC registrados dinámicamente en un equipo remoto mediante el epmapper.
Un terminal conocido es aquel que no se registró a través del epmapper. El cliente debe conocer por adelantado el terminal que ha registrado el servidor. Esto se puede hacer si el terminal está codificado con el código del cliente y del servidor, o si el terminal está presente en el archivo IDL. Este es un ejemplo del IDL del servicio Print Spooler.
[
uuid(12345678-1234-ABCD-EF00-0123456789AB),
version(1.0),
ms_union,
endpoint("ncacn_np:[\\pipe\\spoolss]"),
pointer_default(unique)
]
RPC representa una conexión lógica entre un cliente y un servidor utilizando un identificador de enlace. Tanto el servidor como el cliente utilizan funciones para manipular los datos de enlace, por ejemplo, al configurar la información de autenticación. Cuando el cliente establece la autenticación en el enlace, el enlace se considera autenticado. Cuando el programa cliente llama a la función RPC, se produce el enlace de red. Desde una perspectiva de tráfico de red, el cliente RPC inicia la interacción RPC enviando una solicitud de enlace con información de autenticación en el paquete. El servidor puede responder con bind_ack (confirmado) o bind_nak (se ha producido un error).
Fig. 2: Fragmento de Wireshark que muestra una resolución dinámica de terminales
En el fragmento de Wireshark de la figura 2, podemos ver la resolución dinámica de terminales para la interfaz del programador de tareas. Una vez que el cliente tiene la información de terminal del programador de tareas, crea una nueva conexión. Entonces podemos ver una nueva conexión con otro proceso de enlace, incluyendo un paquete AUTH3 como parte de la autenticación de enlace.
Al igual que los terminales, los enlaces tienen tipos: automático, implícito y explícito (Figura 3). Difieren en la cantidad de control que tiene la aplicación sobre el proceso de enlace.
- Enlace automático (obsoleto): las aplicaciones de cliente y servidor no gestionan el proceso de enlace y, en su lugar, permiten que el tiempo de ejecución de RPC controle completamente este proceso.
- Enlace implícito: el cliente tiene la opción de configurar el identificador de enlace antes de que tenga lugar el enlace. Una vez que el cliente establece un enlace, la biblioteca de tiempo de ejecución RPC controla el resto.
- Enlace explícito: el cliente debe configurar el identificador de enlace. A continuación, el tiempo de ejecución de RPC solo lo pasa al servidor.
Seguridad RPC para solicitudes remotas
Ahora que conocemos los aspectos básicos del funcionamiento de RPC, queremos comprender qué acciones, políticas y mecanismos pueden bloquear a un cliente para que no acceda a una función. Esto es interesante tanto desde una perspectiva ofensiva como defensiva.
Autenticación de transporte
Algunos transportes, como los canales con nombre o HTTP, tienen autenticación como parte de su protocolo. Por ejemplo, los canales con nombre se transfieren a través de SMB, que tiene autenticación. Esto significa básicamente que cuando un servidor registra un terminal de canal con nombre, solo los clientes con las credenciales de un usuario válido pueden conectarse a ese terminal. En un entorno de dominio, tener un usuario de dominio en el mismo dominio es suficiente para pasar la comprobación de autenticación. Si los equipos no forman parte de un dominio, el cliente deberá tener las credenciales de un usuario local en el servidor remoto (Figura 4). En esta publicación trataremos las exclusiones más adelante.
Fig. 4 : ejemplo de un error del sistema que deniega el acceso
Autenticación de enlace
El servidor puede tener un mecanismo de autenticación mediante la autenticación de enlace. Para que esto ocurra, el servidor debe registrar la información de autenticación llamando a RpcServerRegisterAuthInfo. El cliente debe utilizar el nombre principal de servicio correcto y el método de proveedor de asistencia de seguridad que utiliza el servidor; de lo contrario, recibirá “RPC_S_UNKNOWN_AUTHN_SERVICE” del servidor.
Un cliente puede autenticarse en un servidor estableciendo datos de autenticación y autorización en el enlace mediante API como RpcBindingSetAuthInfo y RpcBindingSetAuthInfoEx. El cliente especifica el método de autenticación (por ejemplo, NTLM, Kerberos, Negotiate, SCHANNEL, etc.) y el nombre principal del servicio.
Dos cosas importantes que debe saber:
Si un cliente establece la autenticación en el enlace y el servidor no ha registrado ninguna información de autenticación, el servidor devolverá “RPC_S_UNKNOWN_AUTHN_SERVICE”.
El hecho de que el servidor haya registrado un enlace de autenticación no significa que el cliente deba utilizar un enlace autenticado. Además, el tiempo de ejecución de RPC no despachará el enlace de autenticación de cliente con credenciales no válidas. Más adelante en esta publicación, trataremos cómo puede impedir el servidor el acceso a enlaces no autenticados.
Cabe destacar que el tiempo de ejecución de RPC guarda la información de autenticación globalmente. Esto significa que si dos servidores RPC comparten el mismo proceso y uno de ellos registra información de autenticación, el otro servidor también tendrá información de autenticación. Ahora un cliente puede autenticar el enlace al acceder a cada uno de los servidores. A este comportamiento lo llamamos multiplexación de SSPI.
Seguridad de extremos
Un servidor puede establecer un descriptor de seguridad en el terminal. El descriptor de seguridad es un mecanismo de seguridad de comprobación de acceso general en Windows. Permite la creación de “reglas” para decidir quién tiene permiso para acceder a un objeto y a quién se le deniega el acceso. Cuando se intenta acceder al objeto, el sistema operativo compara el token de acceso de quien llama con el descriptor de seguridad para ver si se permite el acceso. En este caso, el objeto es el terminal y el token de acceso es el token de acceso del cliente derivado de la autenticación del protocolo de transporte. Esta comprobación solo se aplica a transportes autenticados como canales con nombre, ALPC y HTTP (cuando se utiliza la autenticación). TCP es un protocolo de transporte no autenticado y, por lo tanto, no tendría esta comprobación de acceso.
De forma similar a la multiplexación de SSPI, los terminales también se multiplexan: la interfaz y el terminal no están enlazados. Un servidor registra las interfaces y los terminales por separado. Si un proceso tiene varios terminales registrados, se puede acceder a cada interfaz registrada en este proceso mediante de cada uno de estos terminales.
Imagine, por ejemplo, que registra su interfaz y crea un terminal que escucha en \\pipe\mypipe. Otro servidor RPC alojado en el mismo proceso registró su propio terminal en TCP 7777. Su interfaz también será accesible a través de TCP. Esto omitirá las restricciones de seguridad impuestas en el terminal, como un descriptor de seguridad (Figura 5). Por lo tanto, es recomendable no confiar en la seguridad de los terminales ni verificar que el cliente ha pasado por el protocolo de transporte esperado.
Fig. 5: ejemplo de omisión del descriptor de seguridad de terminal debido a la multiplexación de terminal
Seguridad de interfaz
Con respecto a la seguridad de la interfaz, hay tres maneras de proteger una interfaz: establecer una devolución de llamada de seguridad, establecer un descriptor de seguridad en la interfaz y utilizar indicadores de interfaz.
Establecimiento de una devolución de llamada de seguridad
El primer mecanismo de seguridad de interfaz es una devolución de llamada de seguridad. Una devolución de llamada de seguridad es una devolución de llamada personalizada implementada por el desarrollador del servidor. La lógica dentro de la devolución de llamada de seguridad depende del desarrollador y permite al servidor restringir el acceso a la interfaz. Si la devolución de llamada devuelve RPC_S_OK, entonces se permite el cliente.
Si se registra una devolución de llamada de seguridad, el tiempo de ejecución de RPC denegará automáticamente a los clientes no autenticados a menos que el indicador RPC_IF_ALLOW_CALLBACKS_WITH_NO_AUTH esté definido.
La definición de una devolución de llamada de seguridad y del indicador RPC_IF_ALLOW_SECURE_ONLY no significa que el cliente tenga privilegios elevados. Por lo tanto, la devolución de llamada de seguridad es el lugar en el que el servidor debe consultar el nivel de privilegios del cliente. Esto se hace llamando a RpcBindingInqAuthClient. Otra comprobación común es la del método de transporte utilizado por el cliente para aplicar la conexión a través de transporte autenticado seguro, como canales con nombre. Esto se hace llamando a RpcBindingServerFromClient → RpcBindingToStringBinding → RpcStringBindingParse y comparando el parámetro Protseq (secuencia de protocolo). Esto también evita el abuso de multiplexación de terminal.
Almacenamiento en caché de devolución de llamada de seguridad
Si se registra una devolución de llamada de seguridad, se llamará durante las comprobaciones de seguridad. Si la devolución de llamada de seguridad se aprueba (es decir, devuelve RPC_S_OK), el resultado se almacenará en caché, si se utiliza el almacenamiento en caché. La próxima vez que el mismo cliente llame a una función en la interfaz, y como el resultado de la devolución de llamada de seguridad se almacena en caché, la devolución de llamada de seguridad no se volverá a invocar y se utilizará la entrada almacenada en caché en su lugar.
La implementación del almacenamiento en caché es sencilla, pero depende de algunos factores.
- El almacenamiento en caché está enlazado al contexto de seguridad del cliente, que se toma del enlace. Por lo tanto, si el servidor (o cualquier otro servidor en el proceso) no registró ningún enlace de autenticación, o si el cliente no estableció el enlace de autenticación, el almacenamiento en caché se deshabilitaría para esta llamada.
- Si el servidor registra la interfaz con el indicador RPC_IF_SEC_NO_CACHE, el tiempo de ejecución de RPC fuerza la llamada de la devolución de llamada de seguridad para cada llamada, deshabilitando así el mecanismo de almacenamiento en caché.
- El indicador de interfaz no documentado RPC_IF_SEC_CACHE_PER_PROC también afecta al mecanismo de almacenamiento en caché. Si el servidor especificó este indicador, el almacenamiento en caché se realizará por llamada y no por interfaz . Esto significa que si la caché contiene un valor correcto para la función X en la PRUEBA de interfaz, una llamada a la función Y en la PRUEBA de interfaz activará de nuevo la devolución de llamada de seguridad.
El mecanismo de almacenamiento en caché puede provocar vulnerabilidades lógicas para cualquier devolución de llamada de seguridad que dependa de la función a la que llama el cliente. Como desarrollador de RPC o auditor, debe conocer el mecanismo de almacenamiento en caché. Hemos publicado una investigación exhaustiva sobre la implementación del almacenamiento en caché, incluidas las vulnerabilidades que encontramos debido a este mecanismo.
Establecimiento de un descriptor de seguridad
El segundo mecanismo para proteger una interfaz es establecer un descriptor de seguridad en la interfaz. Solo la función RpcServerRegisterIf3 tiene la opción de registrar un descriptor de seguridad en la interfaz. Si no se ha definido un descriptor de seguridad, el descriptor de seguridad predeterminado es:
- NT AUTHORITY\ANONYMOUS LOGON
- Everyone
- NT AUTHORITY\RESTRICTED
- BUILTIN\Administrators
- SELF
Uso de los indicadores de interfaz
La tercera forma de asegurar una interfaz es utilizar los indicadores de interfaz, que se establecen al crear la interfaz mediante las funciones RpcServerRegisterIf*. Desde el punto de vista de la seguridad, los indicadores interesantes son:
RPC_IF_ALLOW_CALLBACKS_WITH_NO_AUTH : se llamará a la devolución de llamada de seguridad para cada invocación, independientemente del método de transporte utilizado o del nivel de autenticación.
RPC_IF_ALLOW_LOCAL_ONLY : solo se permitirán las solicitudes locales.
RPC_IF_ALLOW_SECURE_ONLY : Las conexiones se limitan a clientes con un nivel de autorización superior a RPC_C_AUTHN_LEVEL_NONE. Al especificar este indicador se deniega a los clientes que llegan a través de la sesión NULL. Este indicador no garantiza el nivel de privilegio de un usuario que llama, solo garantiza que el cliente tenga credenciales válidas.
RPC_IF_SEC_NO_CACHE : este indicador deshabilitará completamente el almacenamiento en caché para la devolución de llamada de seguridad de la interfaz.
RPC_IF_SEC_CACHE_PER_PROC : en lugar de deshabilitar todo el mecanismo de almacenamiento en caché, este indicador cambiará el comportamiento predeterminado para que sea por llamada en lugar de por interfaz.
Políticas de todo el sistema
Existen varias políticas para todo el sistema que se establecen en función del tipo de equipo: cliente, servidor o controlador de dominio (DC).
Una interesante política del sistema relacionada con la seguridad de los terminales es la “política Restringir clientes RPC no autenticados”. Se pueden establecer tres valores.
- “Autenticado”: el tiempo de ejecución de RPC bloqueará el acceso a los clientes TCP que no se hayan autenticado por adelantado si el indicador RPC_IF_ALLOW_CALLBACKS_WITH_NO_AUTH no está establecido en la interfaz.
- “Autenticado sin excepciones”: se bloquean todas las conexiones no autenticadas.
- "Ninguno": todos los clientes RPC pueden conectarse a servidores RPC que se ejecuten en el equipo.
Por lo tanto, una superficie de ataque interesante para los clientes no autenticados serían las interfaces a las que se puede acceder a través de TCP y que registran el indicador RPC_IF_ALLOW_CALLBACKS_WITH_NO_AUTH. Obviamente, el cliente tendrá que pasar la comprobación de devolución de llamada de seguridad para invocar las funciones de las interfaces.
Como se mencionó anteriormente, las conexiones a través de canales con nombre, que se transmiten a través de SMB, tienen su propia autenticación como parte del protocolo SMB. Un cliente puede utilizar un “inicio de sesión anónimo” (también conocido como sesión NULL) al conectarse mediante SMB. Dos políticas del sistema relacionadas son ““Restringir acceso anónimo a canales con nombre y elementos de uso compartido” y “Acceso de red: canales con nombre a los que se puede acceder de forma anónima”. Si la primera está activada, solo se podrán conectar anónimamente los canales con nombre definidos en la segunda. Para las estaciones de trabajo, la segunda política está vacía, lo que significa básicamente que no puede utilizar una sesión NULL en las estaciones de trabajo del dominio. Para un equipo DC, la lista de canales con nombre de la política incluye “\pipe\netlogon”, “\pipe\samr” y “\pipe\lsarpc”. Esto es interesante desde el punto de vista de un atacante, ya que son terminales a los que se puede conectar una máquina desde fuera del dominio.
Por último, con respecto a la comprobación de descriptores de seguridad de terminales, existe una política para todo el sistema que está deshabilitada de forma predeterminada: “Acceso de red: permitir que los permisos Everyone (Todos) se apliquen a usuarios anónimos”. Cuando está habilitada, el identificador de seguridad Everyone (Todos) se agrega al token creado para las conexiones anónimas.
Comprobaciones de seguridad del procedimiento
El programador del servidor puede implementar comprobaciones de seguridad como parte de las funciones que se exponen en la interfaz y, por lo tanto, tiene la opción de cambiar o personalizar la lógica de la comprobación según la función a la que se llama. Una comprobación de seguridad común es una comprobación de acceso, como la que se muestra en la Figura 6 (tomada de srvsvc):
Fig. 6: Un ejemplo de comprobación de seguridad común, la comprobación de acceso.
En este ejemplo, LocalrSessionGetInfo llama a SsAccessCheck. SsAccessCheck básicamente suplanta al cliente mediante una llamada a RpcImpersonateClient, seguida de una llamada a la función NtAccessCheckAndAuditAlarm, que realiza la comprobación de acceso. Esto va seguido de una llamada RpcRevertToSelf para revertir al token del servidor. Es importante recordar comprobar el valor devuelto de RpcImpersonateClient, ya que si falla, el servidor continúa ejecutándose en el token de seguridad del proceso del servidor y no en el proceso del cliente.
Resumen
Esta publicación del blog ha presentado mucha información sobre MS-RPC y sus mecanismos de seguridad. Animamos a otros investigadores a que busquen diferentes servidores MS-RPC, ya que presentan una gran superficie de ataque con el potencial de nuevas vulnerabilidades. Esperamos que nuestra publicación ayude a otros investigadores a dar sus primeros pasos en la investigación de MS-RPC.
Seguimos creando y recopilando más recursos sobre RPC para nuestro repositorio de Github. Gracias a todos los que han contribuido con sus conocimientos y experiencia a este tema, especialmente a James Forshaw y Carsten Sandker.