Vulnerando RSA para obtener la llave de Fernet

By Alejandro Jacobo & Dylan Ordaz
 

Contexto

Este reto fue solucionado durante el CSAW FINALS 2020 en el cual Mayas estuvo presente. Dicho reto deja un gran aprendizaje debido a que la manera de solucionarlo fue muy colaborativa por parte del equipo.

Al iniciar el reto nos dan como descripcion la siguiente:

Here are some funky numbers I got from sniffing around where I'm not suppose to. 
Could you get the flag for me? (this replaces the previous hybrid challenge)

y tambien nos dan los siguientes archivos

Archivos:

  • encrypted_flag.txt
  • RSA.py
  • values.txt

encrypted_flag.txt

gAAAAABfprGds2-Sl4iF5BMjjotnTDKFPsfL8AtJOOeeruqB4w8RGk5gNUt0JM0q2xDve9x9PNHkNkk7f9rf1LekcIBjT1MHIIrvIlnhGqunRRwX59Eo42M=

Analizando el archivo encrypted_flag.txt a primera vista parece ser un fernet symmetric encryption, debido a su formato similar a un base64.

Considerando el formato, un fernet token debe contener lo siguiente:

Overview

Fernet es un sistema de cifrado y descifrado simétrico, autentica el mensaje, lo que significa que el destinatario puede saber si el mensaje se ha alterado de alguna manera con respecto a lo que se envió originalmente.

Para cifrar y descifrar datos, necesitaremos una llave secreta que debe compartir cualquier persona que necesite cifrar o descifrar datos.
Una llave fernet devuelta por un generador de llaves en realidad contiene dos llaves de 16 bytes:

  • Una llave de firma utilizada para firmar el HMAC.
  • Una llave privada utilizada por el cifrado.

Esta llave de 32 bytes luego se codifica usando la codificación Base64. Esto codifica la cantidad binaria como una cadena de caracteres ASCII. La variante de Base64 utilizada es segura para URL y nombre de archivo, lo que significa que no contiene ningún carácter que no esté permitido en una URL o un nombre de archivo válido. Si tenemos la llave, el resultado es una cadena de 44 bytes que representa el valor general de 32 bytes.


Analizando RSA

Considerando que el challenge trata de un fernet encryption. Solo quedaria obtener un llave que pueda decifrar el token. para ello analizamos el archivo RSA.py

import random
import string
from rsa_values import checkKeys, n1, n2, n3, n4, e

def get_random_string(length):
    characters = string.ascii_letters+string.digits
    result = ''.join(random.choice(characters) for i in range(length))
    return result

def RSAEncrypt(password, n, e):
	c = (int(password.encode('utf-8').hex(),16) ** e) % n
	return c

def main():
	password = get_random_string(32)
	print(password)
	checkKeys()
	c1 = RSAEncrypt(password,n1,e)
	c2 = RSAEncrypt(password,n2,e)
	c3 = RSAEncrypt(password,n3,e)
	c4 = RSAEncrypt(password,n4,e)

	file = open("values.txt",'w')
	file.write("n1:" + str(n1) + '\n')
	file.write("c1:" + str(c1) + '\n')
	file.write("n2:" + str(n2) + '\n')
	file.write("c2:" + str(c2) + '\n')
	file.write("n3:" + str(n3) + '\n')
	file.write("c3:" + str(c3) + '\n')
	file.write("n4:" + str(n4) + '\n')
	file.write("c4:" + str(c4) + '\n')
	file.write("e:" + str(e) + '\n')
	file.close()

if __name__ == '__main__':
	main()

Como se puede observar dentro del codigo, sabemos que genera una contraseña de 32 bytes la cual se utiliza cuatro veces con diferentes variables de n.

password = get_random_string(32)
print(password)
checkKeys()
c1 = RSAEncrypt(password,n1,e)
c2 = RSAEncrypt(password,n2,e)
c3 = RSAEncrypt(password,n3,e)
c4 = RSAEncrypt(password,n4,e)

Como salida del programa se almacenan las variables en un archivo llamado values.txt.

n1:993026244695684152720385884540934236152899333556368140632626642931977663455763577814539451675010742634734414120506873127681575400889367126382788249627522167388706763687223391964637583980012499335053836288149762800461352926871
c1:919185245450085070842500396016408106190564102841807386352380063509870500097738484099609889796995083614948316196284397915697587992595215560226954302540303441147142319086774144200044451484633098049523092465251856761343186171446
n2:2120858645090903183026514121355650736640788936981118406136042282902569410681811232597743281933258598295558440757608733371867831987066752871107340815085437033645770613051826725100320202337307710202802730187794048230226233246437
c2:1208266765754514111395360277918056208640323550343906922007564328002144299927657437792873335826000580646064707967588174785153292261822967987055788013175865915201771920259922766547552097804855479381196953971070003030552476914575
n3:13566626315514098994196793247987944584439249998535190838667639010645726083604266690794903208593054256985816076154703189151830750410096794348919817516657177422145305767806102534164484511642213686511016911921215486685198372816147
c3:1217497400118662279329845790782375666818255286641902450369699752528387025736733412718188595857511268363598010406858933873651883505914392791968214369018429930629428806698086713411413268400019005784163187283297818419415844058298
n4:3781687268076859825619936261231343132436633759923146857815563164944282031661985906371461417791140109723961921392569564055561036370381503090194581545155223783851590130524287100727964018153092190082596699871644182610730089104887
c4:1581630010861681991426638552365806430756733284791722127829411178122452158350095552531779719660231210643815340517737141369431301977856820846393801475741850207897534313201631075802421935603144591231461900365190172816004331334424
e:5

Teniendo en cuenta lo anterior podemos aplicar un Reminder Chinese Theorem para obtener la llave.

Un ejemplo muy bueno en el que se entiende la teoría con números más pequeños lo pueden ver aquí CRT

# Definicion de funciones para CRT

def chinese_remainder(n, a):
    sum = 0
    prod = reduce(lambda a, b: a*b, n)
 
    for n_i, a_i in zip(n, a):
        p = prod / n_i
        sum += a_i * mul_inv(p, n_i) * p
    return sum % prod
 
 
def mul_inv(a, b):
    b0 = b
    x0, x1 = 0, 1
    if b == 1: return 1
    while a > 1:
        q = a / b
        a, b = b, a%b
        x0, x1 = x1 - q * x0, x0
    if x1 < 0: x1 += b0
    return x1

def find_invpow(x,n):
    high = 1
    while high ** n < x:
        high *= 2
    low = high/2
    while low < high:
        mid = (low + high) // 2
        if low < mid and mid**n < x:
            low = mid
        elif high > mid and mid**n > x:
            high = mid
        else:
            return mid
    return mid + 1

# Variables 

n1=993026244695684152720385884540934236152899333556368140632626642931977663455763577814539451675010742634734414120506873127681575400889367126382788249627522167388706763687223391964637583980012499335053836288149762800461352926871
c1=919185245450085070842500396016408106190564102841807386352380063509870500097738484099609889796995083614948316196284397915697587992595215560226954302540303441147142319086774144200044451484633098049523092465251856761343186171446
n2=2120858645090903183026514121355650736640788936981118406136042282902569410681811232597743281933258598295558440757608733371867831987066752871107340815085437033645770613051826725100320202337307710202802730187794048230226233246437
c2=1208266765754514111395360277918056208640323550343906922007564328002144299927657437792873335826000580646064707967588174785153292261822967987055788013175865915201771920259922766547552097804855479381196953971070003030552476914575
n3=13566626315514098994196793247987944584439249998535190838667639010645726083604266690794903208593054256985816076154703189151830750410096794348919817516657177422145305767806102534164484511642213686511016911921215486685198372816147
c3=1217497400118662279329845790782375666818255286641902450369699752528387025736733412718188595857511268363598010406858933873651883505914392791968214369018429930629428806698086713411413268400019005784163187283297818419415844058298
n4=3781687268076859825619936261231343132436633759923146857815563164944282031661985906371461417791140109723961921392569564055561036370381503090194581545155223783851590130524287100727964018153092190082596699871644182610730089104887
c4=1581630010861681991426638552365806430756733284791722127829411178122452158350095552531779719660231210643815340517737141369431301977856820846393801475741850207897534313201631075802421935603144591231461900365190172816004331334424
e=5

# Aplicacion de CRT

format=chinese_remainder([n1,n2,n3,n4],[c1,c2,c3,c4]) 
key = str(hex(find_invpow(format,e)))[2:-1].decode("hex")
print("Key: " + key)

Teniendo como llave xYDFDoqcOACPKeT5gT0wBzAfBSoGieVc podemos confirmar que es de 32 bytes, por lo que solo quedaria aplicar el decrypt con fernet.

# Formato de llave 
key_fernet = b64encode(key)
f = Fernet(key_fernet)

# Flag decryption

flag = f.decrypt('gAAAAABfprGds2-Sl4iF5BMjjotnTDKFPsfL8AtJOOeeruqB4w8RGk5gNUt0JM0q2xDve9x9PNHkNkk7f9rf1LekcIBjT1MHIIrvIlnhGqunRRwX59Eo42M=')
print("Flag: " + flag)

Flag: flag{u5e_b1gg3r_e_pl34s3}

GO MAYAS!!

Comentarios