AISLARME DE LA ZONA DE PRUEBAS: EXPLORE LA ELEVACIÓN DEL PRIVILEGIO DEL AISLAMIENTO DE CLAVES DE CNG

Autor: k0shl de Cyber ​​Kunlun

Resumen

En los últimos meses, Microsoft parchó las vulnerabilidades que informé en el servicio CNG Key Isolation, a las que asigné CVE-2023-28229 y CVE-2023-36906, la CVE-2023-28229 incluyó 6 vulnerabilidades de uso después de la liberación con causa raíz similar y la CVE-2023- 36906 es una divulgación de información de lectura fuera de límites. Microsoft las marcó como "Explotación menos probable" en el estado de evaluación, pero en realidad completé la explotación con estas dos vulnerabilidades.

Como blogger de actualización anual (lo siento: P), comparto esta publicación de blog para presentar mi explotación del servicio CNG Key Isolation, ¡así que comencemos nuestro viaje!

Descripción general sencilla

CNG Key Isolation es un servicio bajo el proceso lsass que proporciona aislamiento de procesos clave a claves privadas, CNG Key Isolation funciona como un servidor RPC al que se puede acceder con el proceso Appcontainer Integrity, como el proceso de renderizado en Adobe o Firefox. Hay algunos objetos importantes en el servicio keyiso, repasémoslos simplemente de la siguiente manera:

  1. Objeto de contexto. El objeto de contexto es como el objeto de administración del servidor RPC keyiso, contendrá el objeto de proveedor cuando el cliente invoque al proveedor de almacenamiento abierto para crear un nuevo objeto de proveedor y es administrado por una lista global llamada SrvCryptContextList. Este objeto debe inicializarse primero.
  2. Objeto proveedor. El cliente debe abrir un proveedor existente en una colección de todos los proveedores; si el proveedor se abre correctamente, asignará el objeto del proveedor y almacenará el puntero en el objeto de contexto.
  3. Objeto clave. El objeto clave es administrado por el objeto de contexto, se asignará e insertará en el objeto de contexto.
  4. Objeto de búfer de memoria. El objeto Memory Buffer es administrado por el objeto de contexto, se asignará e insertará en el objeto de contexto.
  5. Objeto secreto. El objeto secreto es administrado por el objeto de contexto, se asignará e insertará en el objeto de contexto.

En estos cuatro objetos, el objeto proveedor/objeto clave/objeto secreto tiene una estructura de objeto similar, el desplazamiento 0x0 del objeto almacena el valor mágico, 0x44444446 significa objeto proveedor, 0x44444447 significa objeto clave, 0x44444449 significa objeto secreto, cuando estos objetos se liberan, la magia El valor se establecerá en otro valor, el desplazamiento 0x8 del objeto almacena el recuento de referencias y el desplazamiento 0x30 del objeto almacena el índice del objeto. Este índice es como el identificador del objeto, será una bandera cuando el cliente lo use. busca el objeto especificado, lo que significa que el objeto es predecible, comienza en 0 y cuando se asigna un nuevo objeto, agregará 1.

Hay información adicional para hablar de cómo gano la carrera con el handle de object , cuando revisé el código, noté que el handle podía ser predecible, revisemos la función SrvAddKeyToList:

SrvAddKeyToList : 
  handlevalue  =  ++* ( _QWORD  * )( context_object  +  0xA0 );  // =====> [a] 
  * ( _QWORD  * )( objeto_clave  +  0x30 )  =  valor de manejo ;  // =====> [b]

SrvFreeKey : 
  if  (  * (( _QWORD  * ) key_object  +  6 )  ==  handlevalue  )  // ====> [c] 
      break ;

El valor del identificador se almacena en el desplazamiento 0xA0 del objeto de contexto y, de hecho, el valor del identificador es como un valor de índice, el valor inicializado es 0 y cuando se asigna un nuevo objeto clave, el índice agregará 1 [a] y establecerse en el desplazamiento 0x30 del nuevo objeto clave [b]. Cuando se libera el objeto clave, comparará el valor del identificador; si coincide con [c], continuará encontrando código vulnerable. Por lo tanto, el valor del identificador podría ser predecible, por ejemplo, podría llamar a SrvFreeKey con el valor del identificador es 1 cuando crea la primera clave, o podría llamar a SrvFreeKey con el valor del identificador es 10 cuando crea el objeto clave número 10. para que el objeto clave pueda recuperarse en la función FreeKey al agregar la clave al objeto de contexto con el nuevo valor de identificador.

Hago el siguiente cuadro simple para mostrarle la relación entre estos objetos.

1.PNG

Causa raíz de CVE-2023-28229

En esta sección, presentaré la causa raíz de CVE-2023-28299, usaré el objeto clave como ejemplo; en realidad, el resto de los objetos tienen un problema similar.

Cuando investigo sobre el servicio keyiso, descubro que cada objeto tiene su propia interfaz de asignación y libre, como el objeto clave, existe la interfaz RPC de asignación llamada s_SrvRpcCryptCreatePersistedKey y la interfaz RPC gratuita llamada s_SrvRpcCryptFreeKey. Y rápidamente me doy cuenta de que hay un problema entre la asignación de objetos y la liberación.

__int64  __fastcall  SrvCryptCreatePersistedKey ( 
        estructura  _RTL_CRITICAL_SECTION  * a1 , 
        __int64  a2 , 
        _QWORD  * a3 , 
        __int64  a4 , 
        __int64  a5 , 
        int  a6 , 
        int  a7 ) 
{ 
[...] 
    keyobject =  RtlAllocateHeap  ( NtCurrentPeb ( ) -> ProcessHeap ,  0 ,  0x38ui64 ) ; 
[...] 
    * (( _DWORD  * ) objeto clave  +  1 )  =  0 ; 
    * ( _DWORD  * ) objeto clave  =  0x44444447 ; 
    * (( _DWORD  * ) objeto clave  +  2 )  =  1 ;  // ==========> [a] 
    * (( _QWORD  * ) objeto clave  +  4 )  =  v12 ; 
    SrvAddKeyToList (( __int64 ) a1 ,  ( __int64 ) objeto clave );  // =============> [b] 
    v11  =  0 ; 
    * a3  =  * (( _QWORD  * ) objeto clave  +  6 ); 
    devolver  v11 ; 
[...] 
}

__int64  __fastcall  SrvCryptFreeKey ( __int64  a1 ,  __int64  a2 ,  __int64  a3 ) 
{ 
[...] 
  if  (  _InterlockedExchangeAdd ( freebuffer  +  2 ,  0xFFFFFFFF )  ==  1  )  // ============> [c ] 
  { 
    v17  =  SrvFreeKey (( PVOID ) búfer libre );  // ===============> [d] 
    if  (  v17  <  0  ) 
      DebugTraceError ( 
        ( unsigned  int ) v17 , 
        "Estado" , 
        "onecore \\ ds \\ seguridad \\ cryptoapi \\ ncrypt \\ iso \\ service \\ srvutils.c" , 
        700 i64 ); 
  } 
  if  (  _InterlockedExchangeAdd ( freebuffer  +  2 ,  0xFFFFFFFF )  ==  1  )  // ================> [e] 
  { 
    v12  =  ( * ( __int64  ( __fastcall  ** )( _QWORD ,  _QWORD ))( * (( _QWORD  * ) freebuffer  +  4 )  +  0x80 i64 ))(  // ===============> [f] 
            * ( _QWORD  * )( * (( _QWORD  * ) búfer libre  +  4 )  +  0x118 i64 ), 
            * (( _QWORD  * ) búfer libre  +  5 )); 
    v13  =  v12 ; 
[...] 
}

Cuando el cliente invoca la asignación de interfaz RPC, keyiso asignará un montón del montón de proceso e inicializará la estructura, establecerá el recuento de referencia del objeto clave en 1 primero [a], luego agregará el objeto clave al objeto de contexto y agregará el recuento de referencias [b], y cuando el cliente libere el objeto clave, keyiso verificará si la referencia es 1 [c], si lo es, keyiso liberará el objeto clave [d], pero aún usará el objeto clave después de liberarlo [e], luego llamará a la función en vftable.

No hay función de bloqueo cuando el recuento de referencia del objeto clave se inicializa en 1 y se agrega, lo que significa que hay una ventana de tiempo entre la inicialización y la adición, el objeto clave se liberará [c] [d] después de que se complete el recuento de referencia. establecido en 1 [a], y podría pasar la siguiente verificación [e] cuando el recuento de referencias agregue 1 [b], finalmente, provocará el uso después de la liberación cuando la función de vftable llame a [f].

Escribí el PoC y descubrí que puede ser explotable, pero como se muestra en el código siguiente, la función de vftable se selecciona del puntero almacenado en el desplazamiento 0x20 del objeto clave, lo que significa que incluso yo podría controlar el búfer libre, todavía necesito un validar la dirección en el desplazamiento 0x20 del objeto clave. Necesito una divulgación de información.

Causa raíz de CVE-2023-36906

Luego trato de encontrar una divulgación de información, reviso la interfaz RPC y descubro que hay una estructura de propiedad que está almacenada en el objeto del proveedor, y la propiedad se puede consultar y configurar con la interfaz RPC SPCryptSetProviderProperty y SPCryptGetProviderProperty.

__int64  __fastcall  SPCryptSetProviderProperty ( __int64  a1 ,  const  wchar_t  * a2 ,  _DWORD  * a3 ,  unsigned  int  a4 ,  int  a5 ) 
{ 
[...] 
    if  (  ! wcscmp_0 ( a2 ,  L"Usar contexto" )  ) 
    { 
      v15  =  * ( void  * * )( v8  +  32 ); 
      si  (  v15  ) 
        RtlFreeHeap ( NtCurrentPeb () -> ProcessHeap ,  0 ,  v15 ); 
      Montón  =  RtlAllocateHeap ( NtCurrentPeb () -> ProcessHeap ,  0 ,  v6 ); 
      * ( _QWORD  * )( v8  +  32 )  =  Montón ; 
      if  (  ! Montón  ) 
      { 
        v10  =  1450 i64 ; 
ETIQUETA_21 : 
        v9  =  - 2146893810 ; 
        v11  =  2148073486i64 ; _ ir a LABEL_42 ; } v17 = Montón ; ir a LABEL_40 ; } memcpy_0 ( v17 , a3 , v6 ); // ============> [b] } [...] }
         
      
        
       
      
         
     



__int64  __fastcall  SPCryptGetProviderProperty ( 
        __int64  a1 , 
        const  wchar_t  * a2 , 
        _DWORD  * a3 , 
        unsigned  int  a4 , 
        unsigned  int  * a5 , 
        int  a6 ) 
{ 
[...] 
    if  (  ! wcscmp_0 ( a2 ,  L"Usar contexto" )  ) 
    { 
      v17  =  * ( _QWORD  * )( v10  +  32 ); 
      v15  =  21 ; 
      si  (  ! v17  ) 
        pasa  a LABEL_31 ; 
      hacer 
        ++ v13 ; 
      mientras  (  * ( _WORD  * ) ( v17  +  2  *  v13 )  );  // =============> [c] 
      v16  =  2  *  v13  +  2 ; 
      if  (  2  *  ( _DWORD ) v13  ==  - 2  ) 
      { 
ETIQUETA_31 : 
        v11  =  517 i64 ; 
ETIQUETA_32 : 
        v9  =  - 2146893807 ; 
        v12  =  2148073489i64 ; _ ir a LABEL_57 ; } v25 = * ( const void ** )( v10 + 32 ); memcpy_0 ( a3 , v25 , v16 ); // ============> [d] } [...] }
         
      
            
         
     


El cliente podría especificar qué propiedad establecer, si la propiedad se denomina "Usar contexto", asignará un nuevo búfer con el tamaño que podría controlar el cliente y almacenará el búfer "Usar contexto" en el objeto del proveedor, pero cuando Revise el código de consulta, noto que "Usar contexto" debe ser un tipo de cadena, pasará por el búfer en un bucle while y se interrumpirá cuando encuentre el carácter nulo [c], luego devolverá todo el búfer al cliente.

Habrá una lectura fuera de límites cuando configuro la propiedad "Usar contexto" con un contenido distinto de cero en el búfer y, de hecho, esta propiedad es un buen objeto para explotar porque el cliente puede controlar el tamaño y el contenido del búfer. .

Etapa de explotación

Ahora, tengo una lectura fuera de límites que podría filtrar el contenido del objeto adyacente y un uso después del privilegio de elevación libre podría llamar a una dirección arbitraria si pudiera controlar el búfer libre. Creo que es hora de encadenar la vulnerabilidad.

Vuelvo a mirar el búfer libre para descubrir qué necesito primero:

v12  =  ( * ( __int64  ( __fastcall  ** )( _QWORD ,  _QWORD ))( * (( _QWORD  * ) freebuffer  +  4 )  +  0x80 i64 ))(  
            * ( _QWORD  * )( * (( _QWORD  * ) freebuffer  +  4 )  +  0x118 i64 ), 
            * (( _QWORD  * ) búfer libre  +  5 ));

Si pudiera controlar el freebuffer y tuviera una dirección útil, podría configurar esta dirección en el desplazamiento 0x20 del freebuffer, y hay dos direcciones importantes en la dirección de validación, el desplazamiento 0x80 de la dirección debería ser una dirección de función de validación. y el desplazamiento 0x118 debería ser otro búfer.

El proceso lsass habilita la mitigación XFG, por lo que no podría usar ROP en esta explotación, pero si pudiera controlar el primer parámetro de la función, podría usar LoadLibraryW para cargar una ruta dll controlada, de modo que el objetivo se establezca en un desplazamiento de 0x80 de valide la dirección en la dirección LoadlibraryW y configure la carga útil dll en la dirección que se almacenó en el desplazamiento 0x118 de la dirección.

Como presenté en la sección anterior, la propiedad "Usar contexto" es un buen objeto primitivo porque puedo controlar el tamaño y el contenido completo de esta propiedad, y tengo un problema de lectura fuera de límites, por lo que la pregunta es qué objeto debería ser adyacente a mi objeto de propiedad?

Reviso todos los objetos de keyiso y descubro que el búfer de memoria puede ser un objetivo útil.

    v7  =  SrvLookupAndReferenceProvider ( hContext ,  hProvider ,  0 ); 
    [...] 
    _InterlockedIncrement (( volátil  con signo  __int32  * )( v7  +  8 )); 
    * ( _QWORD  * ) Montón  =  v7 ;  // ===========> [a] 
    * (( _QWORD  * ) Montón  +  1 )  =  v32 ; 
    SrvAddMemoryBufferToList (( __int64 ) hContext ,  ( __int64 ) Montón ); 
    v26  =  * (( _QWORD  * ) Montón  +  4 ); 
    Montón  =  0 i64 ; 
    * v15  =  v26 ;

Cuando se crea el búfer de memoria, keyiso buscará el objeto proveedor y almacenará el objeto proveedor en el desplazamiento 0x0 del búfer de memoria [a], por lo que si lleno el objeto de propiedad con un valor distinto de cero y cuando consulto el objeto de propiedad, filtrará la dirección del objeto del proveedor.

Y, por supuesto, diferentes objetos tienen diferentes tamaños, no necesito preocuparme de que los diferentes objetos influyan en el diseño cuando hago un montón de fengshui.

Finalmente, descubro el escenario de explotación de la siguiente manera:

  1. Rocíe el objeto proveedor y el objeto del búfer de memoria. El objeto proveedor es para la etapa final de explotación y el búfer de memoria es para filtrar el objeto proveedor.

2.PNG

  1. Libere algunos objetos del búfer de memoria para crear un agujero en el montón, luego asigne una propiedad con el mismo tamaño de objeto del búfer de memoria, ocupará uno de los agujeros liberados y luego consultará la propiedad para obtener la dirección del objeto del proveedor.

3.PNG

  1. Libere suficientes objetos de proveedor para asegurarse de que el objeto de proveedor filtrado se libere y rocíe las propiedades con el mismo tamaño de objeto de proveedor para ocupar la dirección del objeto de proveedor filtrado. La dirección LoadlibraryW y la carga útil dll deben almacenarse en el desplazamiento 0x80 y el desplazamiento 0x118 en el objeto de proveedor falso. Pero solo tengo una dirección filtrada, podría configurar la ruta de la dll de carga útil en otro desplazamiento en el búfer de propiedades y establecer la dirección en el desplazamiento 0x118 del búfer de propiedades.

4.PNG

  1. Finalmente, podría activar el uso después de la liberación con varios tres subprocesos diferentes, el subproceso A es para asignar el objeto clave, el subproceso B es para liberar el objeto clave, el subproceso C es para asignar el objeto de propiedad con el mismo tamaño del objeto clave y establecer el recuento de referencias falsas y la dirección de propiedad filtrada en el desplazamiento 0x20 del búfer de propiedades.

5.PNG

Cuando el cliente gana la carrera, lo que significa que el objeto de propiedad ocupa el hueco del objeto clave después de que el objeto clave se libera en la función SrvFreeKey, finalmente cargará un dll arbitrario en el proceso lsass, lo que finalmente provocará el escape del entorno limitado de appcontainer.

Parche

Parche de Microsoft para agregar funciones de bloqueo entre el objeto clave inicializado y liberado.

Antes:

[...] 
  RtlLeaveCriticalSection ( v5 ); 
  if  (  _InterlockedExchangeAdd ( buffer libre  +  2 ,  0xFFFFFFFF )  ==  1  ) 
  { 
    v17  =  SrvFreeKey (( PVOID ) buffer libre ); 
    if  (  v17  <  0  ) 
      DebugTraceError ( 
        ( unsigned  int ) v17 , 
        "Estado" , 
        "onecore \\ ds \\ security \\ cryptoapi \\ ncrypt \\ iso \\ service \\ srvutils.c" , 
        700 i64 ); 
  } 
  if  (  _InterlockedExchangeAdd ( freebuffer  +  2 ,  0xFFFFFFFF )  ==  1  ) 
  { 
    v12  =  ( * ( __int64  ( __fastcall  ** )( _QWORD ,  _QWORD ))( * (( _QWORD  * ) freebuffer  +  4 )  +  0x80 i64 ))( 
            * ( _QWORD  * )( * (( _QWORD  * ) freebuffer  +  4 )  +  0x118 i64 ), 
            * (( _QWORD  * ) freebuffer  +  5 )); 
[...]

Después:

[...] 
    RtlEnterCriticalSection ( v8 ); 
    v12  =  * (( _QWORD  * ) v9  +  2 ); 
    if  (  * ( volátil  con signo  __int64  ** )( v12  +  8 )  !=  v9  +  2 
      ||  ( v13  =  ( volátil con  signo  __int64  ** ) * (( _QWORD  * ) v9  +  3 ),  * v13  !=  v9  +  2 )  ) 
    { 
      __fastfail ( 3u ); 
    } 
    * v13  =  ( volátil  con signo  __int64  * ) v12 ; 
    * ( _QWORD  * )( v12  +  8 )  =  v13 ; 
    if  (  _InterlockedExchangeAdd64 ( v9  +  1 ,  0xFFFFFFFFFFFFFFFFu i64 )  ==  1  ) 
    { 
      v14  =  SrvFreeKey ( v9 ); 
      if  (  v14  <  0  ) 
        DebugTraceError ( 
          ( unsigned  int ) v14 , 
          "Estado" , 
          "onecore \\ ds \\ seguridad \\ cryptoapi \\ ncrypt \\ iso \\ service \\ srvutils.c" , 
          705 i64 ); 
    } 
    RtlLeaveCriticalSection ( v8 ); 
    if  (  _InterlockedExchangeAdd64 ( v9  +  1 ,  0xFFFFFFFFFFFFFFFFu i64 )  ==  1  ) 
    { 
      v15  =  ( * ( __int64  ( __fastcall  ** )( _QWORD ,  _QWORD ))( * (( _QWORD  * ) v9 +  4 )  +  128 i64 ))( 
              * ( _QWORD  * )( * (( _QWORD  * ) v9  +  4 )  +  280 i64 ), 
              * (( _QWORD  * ) v9  +  5 )); 
[...]

Gracias por hablar con @chompie1337, @DannyOdler y @cplearns2h4ck. En realidad, incluso después del parche, debería haber UAF después de que se llame a SrvFreeKey, porque la función SrvFreeKey debe liberar el objeto clave pero todavía hay una referencia después de que la función regresa, pero parece que la función nunca se pudo llamar, este es un código extraño que no No sé por qué Microsoft lo diseñó así, pero después de agregar la función de bloqueo entre el objeto clave, se inicializa y libera, la condición de carrera UAF se soluci
Link: https://whereisk0shl.top/post/isolate-me-from-sandbox-explore-elevation-of-privilege-of-cng-key-isolationonó.

Comentarios

Entradas populares