| ESTUDIO COLECTIVO DE DESPROTECCIONES Última Actualizacion: 25/10/2001 |
Programa | Access 97. Fase 2 | W95 / W98 / NT
Descripción | Motor de bases de datos
Tipo | Aplicación comercial
Tipo de Tutorial | [X]Original, []Adaptación, []Aplicación, []Traducción
Url | http://www.microsoft.com
Protección | Contraseña de acceso a bases de datos
Dificultad | 1) Principiante, 2) Amateur, 3) Aficionado, 4) Profesional, 5) Especialista
Herramientas | SoftIce v3.25, W32dasm v8.9, UltraEdit v6.10a ...etc
Objetivo | Simular que las bases de datos no tienen contraseña
Cracker | Mr.Blue
Grupo | Whiskey Kon Tekila
Fecha | 16 de Octubre de 1999
INTRODUCCION
Vamos a demostrar la "seguridad" de los datos en Access 97. Todo lo que se va explicar ha sido probado sobre Access 97, aunque sospecho que es válido para cualquier base de datos en formato Access (al menos desde el Access 95). He dividido nuestro proyecto en varias fases, cada una más difícil que la anterior, pero todas muy fáciles. Ya veréis. Parchear una base de datos para eliminar la contraseña de acceso. Parchear el Access 97 para poder abrir bases de datos protegidas con contraseña de acceso (sin saberla, claro). Diseño de un gestor de contraseñas para bases de datos de Access que permita descubrir la contraseña de una base de datos, modificarla o borrarla.
RESUMEN DE LO ANTERIOR
En la primera parte comenzamos por identificar el mecanismo utilizado por Acess97 para almacenar las contraseñas de acceso a bases de datos. Descubrimos en que lugar exacto de los ficheros .mdb se almacenan dichas contraseñas, así como una forma de eliminar dichas contraseñas. Ya disponemos de una herramienta para poder acceder a cualquier base de datos protegida con contraseña de acceso, eliminándola. A partir de aquí, vamos a llegar más lejos. Con ayuda del SoftIce vamos a entrar de lleno en el código del Access 97, y vamos a intentar descifrar el algoritmo de encriptación y desencriptación de la contraseña de acceso para construir una aplicación capaz de leer y modificar dicha contraseña. En esta fase nos vamos a limitar a descubrir en dónde se realiza la comprobación de la contraseña introducida por el usuario y la almacenada en la base de datos.
LOS PREPARATIVOS
Pues manos a la obra. Para localizar dónde se encuentran las rutinas de desencriptación que buscamos, se nos pueden ocurrir muchas formas. Voy a utilizar la que considero más lógica y directa. El proceso es bien sencillo. En primer lugar, el Access debe cargar la base de datos para decidir si debe pedir contraseña de acceso o no. Para llevar a cabo esta decisión, puede que tenga que realizar la desencriptación que buscamos. Aquí es donde vamos a atacar. Si pide contraseña, debe comparar la introducida con la almacenada en el fichero, por lo que esta vez no tendrá más remedio que desencriptar la parte que contiene la contraseña, si no lo ha hecho antes. Si no hemos tenido éxito atacando a la rutina que dice si la base de datos tiene contraseña o no, atacaremos a la rutina que realiza la comparación. La rutina de desencriptación, lógicamente accederá a una zona de memoria donde previamente el programa ha cargado el fichero de la base de datos. En el SoftIce, crearemos un punto de ruptura que salte cuando la aplicación acceda a las posiciones de memoria donde el Access ha cargado el fichero. Un poco de "API-culturilla" Antes de comenzar poniendo puntos de ruptura a diestro y siniestro, vamos a consultar la ayuda de la API de Windows. Vamos a conocer las funciones CreateFile y ReadFile. Los que ya estén familiarizados con ellas, pueden saltarse esta parte. En Windows, para leer un fichero se utiliza la función ReadFile, con los siguientes parámetros:
`BOOL ReadFile(` |
`HANDLE hFile,` | `// handle del fichero a leer`
`LPVOID lpBuffer,` | `// puntero buffer que recibe los datos`
`DWORD nNumberOfBytesToRead,` | `// número de bytes a leer`
`LPDWORD lpNumberOfBytesRead,` | `// puntero al número de bytes leídos`
`LPOVERLAPPED lpOverlapped );` | `// puntero a estructura OVERLAPPED`
El parámetro 'hFile' es el handle (que no es más que un número utilizado por Windows para referenciar a un determinado objeto) devuelto por la función CreateFile cuando se abrió el fichero. Para no confundirnos con otra posible lectura a un fichero distinto, tenemos que saber cuál es el handle que referencia a la base de datos que estamos abriendo para poder colocar un punto de ruptura en ReadFile que salte solo cuando sea invocado por Access para leer nuestra base de datos. Por ello, tendremos que colocar otro punto de ruptura en la llamada función CreateFile que abre nuestra base de datos, para ver qué handle devuelve. Otro parámetro de interés para nosotros es en qué posición de memoria se van a guardar los bytes que se lean de dicho fichero. Este parámetro es 'lpBuffer', que contiene la dirección de memoria a partir de la cual se van a cargar los bytes que se lean del fichero apuntado por 'hFile'. El número de bytes que se quieren leer es 'nNumberOfBytesToRead' y el que en realidad ha leído ReadFile después de ejecutarse lo devuelve en la dirección apuntada por 'lpNumberOfBytesRead'. Para abrir un fichero se utiliza CreateFile:
`HANDLE CreateFile(` |
`LPCTSTR lpFileName,` | `// dirección del nombre del fichero`
`DWORD dwDesiredAccess,` | `// modo acceso (lectura/escritura)`
`DWORD dwShareMode,` | `// modo compartido`
`LPSECURITY_ATTRIBUTES lpSecurityAttributes,` | `//descriptor seguridad`
`DWORD dwCreationDistribution,` | `// acción`
`DWORD dwFlagsAndAttributes,` | `// attributos`
`HANDLE hTemplateFile` `);` | `// handle modelo para atributos`
Para nuestros "nobles" propósitos, tan sólo nos interesa el primer parámetro, 'lpFileName', que es un puntero a una cadena terminada en carácter cero que indica el nombre completo del fichero que se desea abrir. Y otro poco de "ASM-culturilla" Todo esto está muy bien pero ¿cómo se come esto en ensamblador? Y más concretamente ¿cómo se pasan parámetros en llamadas a funciones en ensamblador? El paso de parámetros se realiza mediante la pila. Antes de la llamada, los parámetros se introducen en la pila mediante instrucciones 'PUSH' en orden inverso. Así, por ejemplo, una llamada a una función declarada así: Funcion (par1,par2,par3) se realiza así:
`.......`
`Push parámetro3`
`Push parámetro2`
`Push parámetro1`
`Call Funcion`
`Cmp Eax, 0`
`.........`
En la primera instrucción de Funcion, la pila estará así:
`(Esp)` | `Dirección de retorno. (Cmp Eax, 0)`
`(Esp+4)` | `parámetro1`
`(Esp+8`) | `parámetro2`
`(Esp+c`) | `parámetro3`
Así, por ejemplo, si ponemos un punto de ruptura en la función CreateFile, cuando salte, el contenido de la posición 'Esp+4' será el parámetro 'lpFileName'. Por otra parte, las funciones que devuelvan un valor, lo hacen a través del registro 'Eax'.
LA CAZA
Pues empezamos. Tal y como se dijo al principio, colocaremos un punto de ruptura en las posiciones donde se almacene el fichero abierto por el Access, para cazar las rutinas de desencriptación cuando comiencen a funcionar. Para saber en qué parte de la memoria se ha cargado la base de datos, tendremos que colocar otro punto de ruptura en la función ReadFile y ver el contenido del parámetro 'lpBuffer'. El problema es que el único parámetro que nos indica qué fichero es el que se está leyendo es 'hFile', que a priori, no sabemos cual será el que se asignará a la base de datos que estamos intentando abrir. Tendremos que poner otro punto de ruptura en CreateFile y apuntar el puntero al fichero abierto que devuelve. Manos a la obra. Abrimos el Access, elegimos una de nuestras bases de datos con contraseña, por ejemplo 'bd2.mdb' con contraseña "WKT!". Antes de pulsar el botón de 'Abrir', pulsamos Ctrl+D y colocamos un punto de ruptura en CreateFile: `bpx CreateFileA` Volvemos al Access con Ctrl+D y pulsamos el botón de 'Abrir'. Salta el SoftIce y estamos en la primera instrucción de CreateFile. Nos aseguramos que se está abriendo nuestra base de datos examinando la cadena a la que debe apuntar del primer parámetro, 'lpFileName', para ello escribimos: `d*(esp+4)` Si no tenéis ninguna otra aplicación, funcionando en paralelo con el Access, esta llamada será la que buscamos. En caso contrario, puede que esta otra aplicación ser la que está intentando abrir otro fichero distinto, por lo que tendremos que ir pulsando Ctrl+D hasta encontrar la llamada a CreateFile que abre nuestra 'bd2.mdb'. Una vez localizada, examinamos el puntero al fichero que devuelve en el registro 'Eax'. Para esto, pulsaremos una vez F11. Tras el F11, salimos a: `4035A0A mov [esi],eax` pertenenciente a 'MsJet35'. El Access está utilizando la librería 'MsJet35.dll', que se encuentra en 'Windows\System'. Anotamos el valor del registro 'eax', que en mi caso es 29 (en hexadecimal). Ahora colocamos un punto de ruptura en ReadFile, para descubrir en dónde se está cargando el fichero: `bpx ReadFile if *(esp+4)==29` El punto de ruptura es condicional, para que salte solo cuando el parámetro 'hFile' (primer parámetro de ReadFile) sea el asociado con nuestra base de datos. Pulsamos Ctrl+D y aparecemos en la primera instrucción de ReadFile. Mostramos la zona de memoria donde se va a almacenar el fichero, que viene marcada por el segundo parámetro, 'lpBuffer', de la función. Escribimos: `d*(esp+8)` Imaginemos que esto nos muestra el contenido de la memoria a partir de la dirección 1864000 (por ejemplo). Pulsamos F11, y en la zona de memoria apuntada debe aparecer el inicio del fichero. Este contiene, a partir del quinto byte, la cadena 'Standard Jet DB'. Colocamos un punto de ruptura en los accesos a la dirección donde sabemos que comienza la contraseña encriptada, que en mi caso será en: `1864000 + 42 = 1864042` El punto de ruptura quedará así: `bpm 1864042 rw` Pulsamos Ctrl+D, y volvemos a aparecer al inicio de ReadFile. Se va a volver a intentar la lectura del fichero. Deshabilitamos el 'bpm' anterior y miramos donde se va a almacenar esta nueva lectura. En mi caso, sale que se va a escribir en el mismo sitio de antes. Pulsamos F11 y como no ha cambiado la zona donde se carga el fichero, volvemos a habilitar el 'bpm' en el mismo sitio. Si la carga se hace en un sitio distinto, tendremos que colocar el 'bpm' en el sitio correspondiente. Pulsamos Ctrl+D y aparecemos en la dirección 4035793:
`:0403578F` | `02D1` | `add dl, cl`
`:04035791` | `8A2F` | `mov ch, byte ptr [edi]`
`:04035793` | `47` | `inc edi`
`:04035794` | `8A0416` | `mov al, byte ptr [esi+edx]`
`:04035797` | `880C16` | `mov byte ptr [esi+edx], cl`
`:0403579A` | `88041E` | `mov byte ptr [esi+ebx], al`
`:0403579D` | `02C1` | `add al, cl`
`:0403579F` | `FEC3` | `inc bl`
`:040357A1` | `322C06` | `xor ch, byte ptr [esi+eax]`
`:040357A4` | `4D` | `dec ebp`
`:040357A5` | `886FFF` | `mov byte ptr [edi-01], ch`
`:040357A8` | `8A0C1E` | `mov cl, byte ptr [esi+ebx]`
`:040357AB` | `75E2` | `jne 0403578F`
`:040357AD` | `5D` | `pop ebp`
En la instrucción anterior a ésta, se ha cargado el primer carácter de la contraseña encriptada en el registro CH. A partir de aquí vamos pulsando F8 y vemos como se va desencriptando la contraseña, apareciendo finalmente la nuestra "WKT!". Lo que tenemos aquí, es parte de la rutina de desencriptación, que ya analizaremos en la siguiente fase del proyecto. Con esto, ya somos capaces de conocer la contraseña de cualquier base de datos, aunque eso sí, con la ayuda del SoftIce. Borramos el 'bpm' anterior y creamos un punto de ruptura en los accesos a las posiciones ocupadas por la contraseña desencriptada, en mi caso: `bpr 1864042 1864045 rw` Pulsamos Ctrl+D y aparecemos en 4036028:
`:04036020` | `8D7542` | `lea esi, dword ptr [ebp+42]`
`:04036023` | `E840F7FFFF` | `call 04035768`
`:04036028` | `803E00` | `cmp byte ptr [esi], 00`
`:0403602B` | `0F85E4680500` | `jne 0408C915`
`:04036031` | `837D3E00` | `cmp dword ptr [ebp+3E], 00000000`
`:04036035` | `752A` | `jne 04036061`
En esa intrucción, estamos comparando el primer carácter de la contraseña con el carácter cero. Es el momento de parar un momento y recordar dónde estamos y a dónde vamos ;-) Tal y como dijimos anteriormente, Access debe comprobar inicialmente al abrir una base de datos si ésta tiene contraseña de acceso o no. Si la tiene, debe interrumpir la apertura de la base de datos y pedírsela al usuario. Puede que una base de datos sin contraseña, tenga almacenada como contraseña una cadena de caracteres de ASCII 0. Para comprobar esto, podemos borrar todos los puntos de rupturas, colocar uno de ejecución en 403628 ('bpx 403628'), volver al Access e intentar abrir una base de datos sin contraseña de acceso. Efectivamente, al saltar el punto de ruptura, si vemos el contenido de la zona apuntada por el registro ESI, donde antes aparecía "WKT!", ahora aparecen un montón de ceros. Hemos descubierto el significado de la cadena que utilizábamos en la fase anterior para parchear las bases de datos. No es más que una cadena de 14 caracteres de ASCII 0. El parche consistirá en hacer creer al Access, que la contraseña de la base de datos que está abriendo es una cadena de ASCII 0, o lo que es lo mismo, que la base de datos que se está abriendo no tiene contraseña. El objetivo final del parche sobre 'Msjet35.dll' será que la ejecución, al llegar a 4036028 continue por 4036031, sin saltar a 408C915. Comprobamos si tenemos razón, con el SoftIce. Usando el mismo punto de ruptura de antes, e intentamos abrir una base de datos con contraseña. Cuando volvamos al SoftIce, escribimos el registro EIP con la dirección 4036031: `reip 4036031` Se abre la base de datos sin más problemas. Ya hemos cazado a la "liebre".
REMATANDO A LA PRESA
Ahora bien, ¿qué posibilidades se nos ocurren para hacer el parche?. La primera que se nos ocurre es "nopear" los 6 bytes del salto en 403602B. Es lo más sencillo, escribir 6 NOP's, pero poco elegante. Además, esta técnica debe evitarse en todo lo posible ya que, puede que más adelante, la aplicación repita la comprobación (aunque esto no ocurre en el caso que nos ocupa, y "nopear" sería una solución válida), por lo que tendríamos que "currarnos" el crack un poco más, y parchear en más sitios. La solución ideal sería que la aplicación, si más adelante vuelve a realizar la comprobación que hemos "petado", el resultado sea el mismo que el logrado al "petar" la primera comprobación. En nuestro caso, lo que nos interesa es que la aplicación encuentre una cadena de caracteres de ASCII 0 donde debería encontrarse la contraseña desencriptada. Formas de hacerlo: Modificar la rutina que realiza la desencriptación para que escriba solo ceros. Justo despues de ejecutar la rutina de desencriptación, sobreescribir con ceros los valores escritos por dicha rutina, mediante código que nosostros añadimos. Veamos la primera opción. Si la llevamos a cabo, la rutina de desencriptación no funcionará adecuadamente y ¿qué ocurre si esa rutina es utilizada para más cosas, aparte de desencriptar la contraseña? Como hemos visto al tracear la rutina, ésta desencripta algo más aparte de las catorce posiciones de la contraseña. Además, si usamos el W32Dasm para desensamblar 'MsJet35.dll', la rutina de desencriptación que comienza en 4035768, es llamada desde muchos otros sitios, aparte de la llamada en 4036023. Desechada la primera opción por "peligrosa", nos centramos en la segunda. Tenemos que añadir código, o bien sobreescribiendo parte del existente si tenemos sitio, buscando una zona de código "sobrante" a donde poder saltar y meter nuestra rutina, o bien haciéndonos sitio modificando la cabecera PE. ¿ Cuántos bytes vamos a necesitar? Dependerá de lo que queramos hacer y de nuestra calidad como "coders". Vamos a hacer una solución intermedia. No escribiremos los 14 bytes de la contraseña a cero, solo los cuatro primeros y después dejaremos la misma comparación:
`:04036020` | `8D7542` | `lea esi, dword ptr [ebp+42]`
`:04036023` | `E840F7FFFF` | `call 04035768`
`:04036028` | `C70600000000` | `mov dword ptr [esi], 00000000`
`:0403602E` | `803E00` | `cmp byte ptr [esi], 00`
`:04036031` | `837D3E00` | `cmp dword ptr [ebp+3E], 00000000`
`:04036035` | `752A` | `jne 04036061`
La probamos con el SoftIce y ... perfecto. Abrimos el editor hexadecimal y buscamos la cadena "80 3E 00 0F 85 E4 68 05 00" en el fichero 'MsJet35.dll' del directorio 'Windows\System'. Esta cadena aparece a partir del offset 35628h. Sustituímos por "C7 06 00 00 00 00 80 3E 00" y probamos. El parcheador se realiza exactamente igual que en la fase anterior aunque en este caso es hasta más sencillo porque ya sabemos qué fichero es nuestro objetivo, 'MsJet35.dll'. Para saber cuál es el directorio del sistema, donde estará 'MsJet35.dll' podemos usar la función de la API GetSystemDirectory.
.... era una raza muy atrasada ....
.... al borde del siglo XXI aún utilizaban aplicaciones Micro$oft ...
[ Entrada | Documentos Genéricos | WkT! Web Site ]
[ Todo el ECD | x Tipo de Protección | x Fecha de Publicación | x Orden Alfabético ]
(c) Whiskey Kon Tekila [WkT!] - The Original Spanish Reversers. Si necesitas contactar con nosotros, leeestoantes e infórmate de cómo puedes ayudarnos