Análisis de los desafíos de reversing del DASCTF 2024

![CDATA[

tryme

El primer desafío emplea una variante de codificación Base64. El binario utiliza un alfabeto propio de 64 símbolos y, tras la codificación, aplica XOR con el valor 0x02 a cada byte. Para rceuperar la bandera se invierte el proceso: aplicar XOR 2 al buffer y decodificar usando la tabla modificada.

import base64

ALFABETO_BINARIO = "..."  # 64 símbolos extraídos del ejecutable
ALFABETO_STD = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"

def decodificar_tryme(datos):
    tr = str.maketrans(ALFABETO_BINARIO, ALFABETO_STD)
    temp = datos.translate(tr)
    dec = base64.b64decode(temp)
    return bytes(b ^ 2 for b in dec)

Los hackers no lloran

Este reto se apoya en CUDA. El programa configura tensores para vectores de cuatro y ocho elementos. La comprobación convierte cada carácter de la entrada con la expresión (carácter × 1.020123456789) + 1, multiplica el resultado por un arreglo double y lo compara con un segundo arreglo.

La comparación presenta un desfase constante cercano a 100, por lo que se compensa restando dicho valor antes de invertir la operación.

muestras = [
    (4358.58716,   60.51846366284686),
    (6122.2983,    89.4737043286176),
    (2158.74574,   24.03104711352393),
    (5973.017537,  84.68873702464015),
    (9173.840881,  104.6695364464632),
    (6164.67827,   83.75627693648984),
    (12293.528276, 96.41044018110416),
    (4091.327439,  75.27071882034213),
    (3360.696562,  60.33140727998576),
    (2403.667017,  46.10475987767577),
    (3199.455077,  56.28563000222285),
    (4962.117508,  86.68936481373537),
    (8266.407604,  80.87786332435297),
    (2863.062918,  55.29894355978243),
    (1044.626306,  9.261748448423328),
    (1067.530873,  20.6272127322797),
    (3217.476319,  31.1897419717479),
    (6260.942959,  116.1865600512257),
    (3278.952568,  30.85991826286804),
    (160.724197,   1.063344600421732),
    (596.797742,   10.59144776777723),
    (3277.973032,  55.64965261721374),
    (6368.757598,  122.950447694522),
    (842.858109,   7.140637105592679),
    (5925.142209,  55.44977106531295),
    (3046.937162,  62.82703886751251),
    (12752.384458, 125.3057489450499),
    (2442.54747,   45.94487116254584),
    (1827.164764,  32.57185367060958),
    (4903.961921,  92.37291765689986),
    (5619.869598,  117.6805078353046),
    (3851.247916,  63.42241478603398),
    (4472.987644,  84.08593452538155),
    (13135.636855, 125.3035418960081),
    (1640.630636,  26.50460072585211),
    (975.429551,   15.6085145259943),
    (2174.379531,  35.68707511621358),
    (2289.845471,  37.67352051379848),
    (2605.707441,  24.32434117146088),
    (1488.586824,  25.69248490815507),
    (12216.019619, 116.4638282572803),
    (4588.270425,  86.30264794289376),
    (4803.36317,   79.51984419851664),
    (13035.30263,  100.6517460100543),
]

DESPLAZAMIENTO = 100.0
FACTOR = 1.020123456789

bandera = []
for esperado, divisor in muestras:
    normalizado = (esperado - DESPLAZAMIENTO) / divisor
    recuperado = int(normalizado / FACTOR - 0.5)
    bandera.append(chr(recuperado))

print("".join(bandera))

La ejecución del script produce la bandera: DASCTF{34056b0c-a3d7-71ef-b132-92e8688d4e29}.

Estereotipo RE

El análisis estático muestra la función de cifrado en sub_401740, pero en 0x401795 hay una instrucción que manipula el retorno de pila y hace que IDA descarte el código posterior. Sustituyendo esos bytes por NOP se revela el flujo completo.

La primera capa es XTEA combinada con XOR, pero genera una bandera falsa: fakeflag_plz_Try_more_hard_to_find_the_true_flag. Al inspeccionar el ejecutable se detecta un TLS callback con lógica adicional y código auto-modificante (SMC) en sub_41F000.

El algoritmo real es XXTEA con clave {What_is_this_?} y delta 0x11451419. Tras cifrar se aplica un XOR con una constante de 64 bytes y se compara con la cadena objetivo. La solución descifra primero con XXTEA, luego con XTEA usando otra clave, intercalando las operaciones XOR.

import struct

DELTA_XTEA = 0x9E3779B9
DELTA_XXTEA = 0x11451419

def bloque_xtea(par, clave, rondas=32):
    v0, v1 = par
    suma = (DELTA_XTEA * rondas) & 0xFFFFFFFF
    for _ in range(rondas):
        v1 = (v1 - ((((v0 << 4) ^ (v0 >> 5)) + v0) ^ (suma + clave[(suma >> 11) & 3]))) & 0xFFFFFFFF
        suma = (suma - DELTA_XTEA) & 0xFFFFFFFF
        v0 = (v0 - ((((v1 << 4) ^ (v1 >> 5)) + v1) ^ (suma + clave[suma & 3]))) & 0xFFFFFFFF
    return [v0, v1]

def mezcla_xxtea(z, y, suma, clave, pos, e):
    return ((((z >> 5) ^ (y << 2)) + ((y >> 3) ^ (z << 4))) ^ ((suma ^ y) + (clave[(pos & 3) ^ e] ^ z)))

def descifrar_xxtea(bloques, clave):
    n = len(bloques)
    rondas = 6 + 52 // n
    suma = (rondas * DELTA_XXTEA) & 0xFFFFFFFF
    y = bloques[0]
    for _ in range(rondas):
        e = (suma >> 2) & 3
        for p in range(n - 1, 0, -1):
            z = bloques[p - 1]
            bloques[p] = (bloques[p] - mezcla_xxtea(z, y, suma, clave, p, e)) & 0xFFFFFFFF
            y = bloques[p]
        z = bloques[-1]
        bloques[0] = (bloques[0] - mezcla_xxtea(z, y, suma, clave, -1, e)) & 0xFFFFFFFF
        y = bloques[0]
        suma = (suma - DELTA_XXTEA) & 0xFFFFFFFF
    return bloques

def xor_bloques(a, b):
    return bytes(x ^ y for x, y in zip(a, b))

if __name__ == '__main__':
    clave1 = [struct.unpack('<i b="" bloques="[struct.unpack('<I'," bytes.fromhex="" clave1="" clave2="[0x756F797B," datos="bytes.fromhex('18091C14371D162D3C05163E0203102C0E313915043A39030D132B3E06083700170B001D1C0016060717300330060A71')" for="" i="" in="" intermedio="b''.join(struct.pack('<I'," len="" print="" range="" resultado="b''" struct.pack="" v="" v0="" v1="bloque_xtea(bloques[i:i+2],"></i>

La salida final es: DASCTF{You_come_to_me_better_than_all_the_good.}

secret_of_inkey

Partiendo de la cadena del cuadro de diálogo se localiza la función sub_417240. El verdadero procesamiento ocurre en sub_416D70: para cada bloque de 32 bytes aplica XOR byte a byte con el índice y la clave introducida, y después descifra con AES en modo ECB usando esa misma clave.

La estructura forma una cadena de claves: descifrar un bloque revela la siguiente clave, que a su vez permite descifrar más bloques. El siguiente script itera hasta obtener todas y extrae el texto legible.

from Crypto.Cipher import AES
import re

datos = bytes.fromhex('C9EFAB9D79AEA0435699970D9CBDF02989D26F51BCC425EEFCFFD46B7E86C2FCA1D7544BA64FBD757CE9206... datos omitidos ...EACE0E6AFDCA5FA7B327BE42CEA6A6F9905AF097C2DC4C4A6DAFE94955B4368DF1E8AE87A5A5E47C6E9ACF6E092F9179719422')

conocidas = {'565': '9fc82e15d9de6ef2'}
bloques = [datos[i:i+32] for i in range(0, len(datos), 32)]

activo = 1
while activo:
    activo = 0
    for etiqueta in list(conocidas.keys()):
        texto = conocidas[etiqueta]
        cifrado = AES.new(texto.encode(), AES.MODE_ECB)
        for bloque in list(bloques):
            temp = bytearray(bloque)
            for i in range(32):
                temp[i] ^= i ^ ord(texto[i % 16])
            plano = cifrado.decrypt(bytes(temp))
            if plano[:6] == b'key_of':
                coincidencia = re.findall(r'key_of_(\d+)_is_"([0-9a-f]+)"', plano.decode())
                if coincidencia:
                    idx, nueva = coincidencia[0]
                    if idx not in conocidas:
                        conocidas[idx] = nueva
                bloques.remove(bloque)
                activo = 1
            elif b'nothing_here' in plano:
                bloques.remove(bloque)
                activo = 1

for etiqueta in list(conocidas.keys()):
    texto = conocidas[etiqueta]
    cifrado = AES.new(texto.encode(), AES.MODE_ECB)
    for bloque in bloques:
        temp = bytearray(bloque)
        for i in range(32):
            temp[i] ^= i ^ ord(texto[i % 16])
        plano = cifrado.decrypt(bytes(temp))
        if all(0x20 <= b <= 0x7e for b in plano):
            print(etiqueta, plano.decode())

Entre los bloques legibles se obtiene la bandera: DASCTF{Do_y0u_l1ke_wh4t_y0u_s3e}.

]]>

Etiquetas: DASCTF XTEA XXTEA AES-ECB CUDA

Publicado el 7-1 19:49