martes, 23 de julio de 2019

Criptografía (CLV): ataque a cifrados por bloques en modo CBC mediante "Padding Oracle" (I)

En este y siguientes posts comparto lo que voy aprendiendo sobre los ataques a los algoritmos de cifrado simétrico por bloques. En este caso me referiré al ataque 'Padding Oracle' a cifradores de este tipo en modo de operación CBC ('Cipher Block Chaining').

Para ello, explico muy brevemente lo que he entendido sobre este ataque (por favor, como siempre, agradecería que si no lo he comprendido bien y/o no lo explico correctamente, se realicen las correcciones y/o ampliaciones de información oportunas en forma de comentarios a esta entrada).

Ante de empezar, comentar que este tipo de ataque sirve tanto para descifrar criptogramas como para "cifrar" mensajes en claro (lo he entrecomillado porque creo que es más correcto decir: obtener un texto cifrado para forzar que éste se descifre como el texto plano deseado, ya que hay que tener en cuenta que no se conoce la clave de cifrado/descifrado y mediante este ataque no se consigue obtenerla). No obstante, en este post pondré el foco única y exclusivamente en el primero de los aspectos comentados, el descifrado, mientras que el segundo, el cifrado de mensajes, queda para ser tratado en una entrada posterior. 

Este tipo de ataque es posible cuando un servidor, ante una petición de descifrado, informa sobre si se produce un error o no conforme a si el relleno ('padding') del criptograma que se le envía es incorrecto o válido, respectivamente. Como más adelante diré, en estos casos, el atacante adquiere una ventaja fundamental para descifrar posibles criptogramas interceptados e, incluso, para modificarlos con objeto de que sean descifrados conforme a su conveniencia.

Pero, como en todo en esta vida, las cosas se comprenden mejor si se pone algún ejemplo y, para ello, utilizo un reto tipo CTF sobre criptografía de la plataforma picoCTF 2018. En dicho desafío se plantea lo siguiente: '
Can you help us retreive the flag from this crypto service? Connect with nc 2018shell.picoctf. com 27533. We were able to recover some Source Code'.

Me conecto al servicio, se me muestra un ejemplo de 'cookie' y se me pide que introduzca la mía:
Después de realizar sucesivas pruebas, introduzco la siguiente cadena (64 caracteres "a"):

 "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"

 y se me informa de que el relleno es incorrecto:
Lo que me indica, tal y como he dicho anteriormente, que este servicio es vulnerable al ataque 'Padding Oracle'.

A la vista del archivo fuente que también se proporciona en este reto (pkcs7.py), veo que el servicio descifra la 'cookie' que se introduce utilizando el criptosistema AES en modo CBC y sólo se proporcionará la flag (se logrará el acceso deseado y, por ende, se conseguirá la información secreta) si el relleno PKCS#7 es válido, el JSON obtenido tras el descifrado también es válido, y en él el valor de "is_admin" es "true" y la fecha de caducidad de la 'cookie', valor de "expires", es posterior a la del sistema, es decir, no ha caducado. Bonito reto y, aunque se puede resolver sin descifrar la 'cookie' que se muestra, voy a utilizar esta última como ejemplo en este post para ver cómo es posible su descifrado sin conocer la clave mediante el ataque 'Padding Oracle'.

Antes de proceder con el ataque, y aunque en este post, tal y como he comentado, únicamente me voy a centrar en el descifrado de la 'cookie' que se proporciona como ejemplo, me conecto de nuevo al servicio y copio e introduzco la misma 'cookie' que se muestra:
Y veo que, efectivamente, no se me muestra la flag porque la 'cookie' introducida ha caducado y, además y aunque estuviera todavía vigente, no soy administrador.

Adicionalmente, comentar muy brevemente que el relleno PKCS#7 consiste en agregar al último bloque de texto plano tantos Bytes como falten para completar su longitud, caso de que éste tenga un tamaño inferior, con la que le correspondería conforme al tamaño de la clave empleada (por ejemplo: en AES-128 serían 16 Bytes).

Cada uno de los Bytes agregados contiene precisamente como valor el número de Bytes de relleno. Por ejemplo: supongamos que el último bloque de texto plano tiene 3 Bytes, es decir, faltan 13 Bytes para llegar a completar el tamaño de 16 Bytes. Pues bien, en este caso se añadirían 13 Bytes con valor 13 ('0d' en hexadecimal). Es importante hacer notar que en caso de que el último bloque tenga una longitud de 16 Bytes se añadiría también un último bloque de texto plano con valor 16 ('10' en hexadecimal) en todos sus Bytes.

El relleno se elimina después del descifrado, por lo que no afecta al contenido del texto plano original.

Y dicho esto vuelvo al ataque 'Padding Oracle', pero antes también hay que recordar el funcionamiento del descifrado en modo de operación CBC:
Es decir:
Lo primero que hace el servidor es descifrar todos los bloques de texto cifrado de la manera que se muestra en la figura anterior, después valida el relleno PKCS#7, elimina éste y obtiene el texto plano del mensaje.

Considerando como ejemplo la 'cookie' que se nos proporciona al conectarnos al servicio, el ataque se inicia utilizando la fuerza bruta para obtener el último Byte del texto plano (P6[16]) correspondiente al último Byte (C6[16]) del último bloque de texto cifrado (C6).

Para ello, genero un nuevo bloque cifrado n-1 con todos sus Bytes con valor \x00 y concateno este nuevo bloque (C'5) con el último bloque de texto cifrado (C6):

C’5 || C6 = 00000000000000000000000000000000fd8703dce82b480d21dfb56717e3a1ec

envío al servidor esa cadena cifrada de dos bloques. Tal y como he dicho, el servicio:

a) Descifra el segundo bloque (C6).

b) Realiza XOR entre Dk(C6y C’5.

c) Verifica el relleno del bloque resultante y si no es correcto informa de que éste no es válido, lo que es más que probable, puesto que hay 1 caso favorable entre 256 casos posibles (en hexadecimal de \x00 a \xff, o en decimal de 0 a 255).

d) Si el relleno no es válido aplico fuerza bruta en el Byte 16 del bloque cifrado n-1 (C’5), es decir, voy probando todos los posibles valores en C’5[16] (de \x00 a \xff), hasta obtener en el Byte 16 del texto plano en claro (P’6[16]) un relleno válido (\x01).
Posteriormente a que el proceso de fuerza bruta en “XXproduzca un relleno válido (P’6[16] = \x01) puedo averiguar el valor del último Byte ("??") de Dk(C6) de la siguiente manera: Dk(C6)[16] = C'5[16] \x01.

Y, por consiguiente, puedo obtener el valor del último Byte del texto plano sin más que realizar la siguiente operaciónP6[16] = Dk(C6)[16]  C5[16].

Para verlo más claro, creo un pequeño script en python, envío al servidor la cadena indicada anteriormente (C’5 || C6) y el servicio me informa de que el valor de C'5[16] que produce un relleno válido es \x1d. Por tanto:

Dk(C6)[16] = \x1d  \x01 = \x1c
P6[16] = \x1c  \x11 = \x0d

Con lo que el valor del último Byte del texto plano del último bloque es \x0d, de momento sólo un valor de relleno.

El valor del Byte anterior del texto plano del último bloque se puede obtener de la siguiente manera:

1.- Pongo el valor de C’5[16] = Dk(C6)[16]  \x02 \x1c  \x02 = \x1e, con objeto de forzar a que, tras aplicar el proceso de fuerza bruta en el Byte 15 del bloque cifrado n-1 (C’5), el relleno válido que se produzca sea P’6[15] || P’6[16] = \x02 || \x02.
2.- Es decir, envío la siguiente cadena al servidor:

C’5 || C6 = 0000000000000000000000000000001efd8703dce82b480d21dfb56717e3a1ec

3.- Posteriormente a que el proceso de fuerza bruta en en “XX” (C'5[15]) produzca un relleno válido (P’6[15] || P’6[16] = \x02 || \x02) el servicio me informa de que el valor de C'5[15] que lo produce es \x82). Entonces:

Dk(C6)[15] = \x82  \x02 = \x80
P6[15] = \x80  \x8d = \x0d

Con lo que, como no podría ser de otra manera, el valor del anteúltimo Byte del texto plano del último bloque es también \x0d, ya que, como anteriormente he dicho, de momento se trata sólo de un valor de relleno. Es decir, el texto plano tiene 13 Bytes de relleno con el valor \x0d (13 en decimal).

Y repitiendo este mismo proceso podemos obtener el resto de Bytes del texto plano del último bloque. Pongo el resultado obtenido con el script de python que he creado para ello:

P6 (hex.) = 65227d0d0d0d0d0d0d0d0d0d0d0d0d0d

Quitando el relleno:
P6 (ASCII) = e"}

Siguiendo este mismo proceso, excepto para el primer bloque del criptograma (que se corresponde con el vector de inicialización, IV, y que no se descifra), se pueden obtener el resto de bloque de texto plano. Pongo el resultado de descifrar el resto de los bloques de 16 Bytes de la 'cookie' que se nos muestra como ejemplo en el reto y el texto plano completo que se obtiene al final del proceso:

P5 (hex.) = 69735f61646d696e223a202266616c73
P5 (ASCII) = is_admin": "fals

P4 (hex.) = 2022323030302d30312d3037222c2022
P4 (ASCII) =  "2000-01-07", "

P3 (hex.) = 657374222c202265787069726573223a
P3 (ASCII) =  est", "expires":

P2 (hex.) = 7b22757365726e616d65223a20226775
P3 (ASCII) = {"username": "gu

Es decir, el texto plano que se obtiene como resultado es:

P (ASCII) = {"username": "guest", "expires": "2000-01-07", "is_admin": "false"}

Si no me equivoco, con este ataque y suponiendo que el cifrado se ha realiza utilizando AES-128, el atacante conseguirá obtener el texto plano (P) en un número de intentos no mayor que el número de bloques del criptograma - 1 (es decir, excluido el IV), multiplicado por el número de Bytes de cada bloque (16) y multiplicado por 256 (número máximo de intentos por cada Byte). En el caso de la 'cookie' que ha servido de ejemplo en este post en no más de 20.480 intentos: (6 - 1) bloques x 16 Bytes/bloque x 256 intentos como máximo/Byte; lo que mejora sustancialmente el número de intentos máximos que serían necesarios para romper mediante fuerza bruta una clave de 128 bits.

Ahora sólo queda saber cómo podemos generar una 'cookie' para engañar al servicio (fecha de caducidad mayor que la del sistema y valor "true" en "is_admin") y que éste tras descifrarla nos muestre la solución al desafío, pero, como ya he dicho, el "cifrado" utilizando el ataque 'Padding Oracle' será objeto de un post posterior.

No hay comentarios:

Publicar un comentario