Hace unos años cuando empecé a trastear con Android y animado por mi afición a la Ingeniería Inversa, decidí realizar una serie de crackmes. Los dos primeros pasaron algo desapercibidos, pero el Crackme nº3 tuvo una gran repercusión en el mundillo y, aunque no fue el primer crackme para Android ni mucho menos, si que fue uno de los más estudiados. Todos ellos fueron publicados a través de crackmes.de y el nº3 en cuestión el 6 de Noviembre de 2010. Os dejo una lista de unas cuantas webs donde aparece analizado para que veáis la repercusión que a mi parecer tuvo.

Soluciones al crackme

Referencias al crackme

Links


st2 arcade
He de iniciar esta entrada diciendo que la segunda temporada de Stranger Things es sencillamente genial. Son 9 horas intensas
Introducción Esta vez se trata de un crackme realizado en VC++ 5.0/6.0 y en sus entrañas utiliza RSA-24. En este
Hace unos años cuando empecé a trastear con Android y animado por mi afición a la Ingeniería Inversa, decidí realizar
Si te interesa el mundo del hacking, ya sea como aficionado o como profesional, seguramente querrás estar al día de

st2 arcade

He de iniciar esta entrada diciendo que la segunda temporada de Stranger Things es sencillamente genial. Son 9 horas intensas que no dejan indiferente a nadie y además en el capítulo 8 nos han dejado una de esas perlas informáticas que tanto nos gustan.

La escena la protagoniza Bob Newby, un buen hombre amante de la electrónica de aquella época que trabaja en RadioShack y transcurre en el laboratorio secreto de Hawkins. En un momento dado, Bob propone «saltarse» la seguridad del laboratorio y para ello se traslada al sótano donde se encuentran los «servidores».

El PC de la época

Para comprender esta escena hay que situarse temporalmente. Estamos hablando de los años 80, en concreto la escena transcurre en 1984 y los equipos de los que dispone el laboratorio son unos maravillosos IBM. No se llega a apreciar bien el modelo de IBM utilizado pero teniendo en cuenta que el monitor que aparece es un terminal IBM 3180, la búsqueda se reduce a los sistemas compatibles S/36, S/38, AS/400, 5294 ó 5394.

IBM 3180

IBM 3180 (https://www.argecy.com/3180)

Cracking BASIC or BASIC Cracking?

La escena plantea un ataque de fuerza bruta a un código de 4 dígitos como se puede observar en la imagen a continuación. Esto puede parecer una chorrada hoy día pero podía suponer un pequeño reto para un micro de 8 bits.

Cracking Basic or Basic Cracking?

Cracking Basic or Basic Cracking?

A simple vista se aprecian una serie de bucles recursivos, una llamada a una función y una sentencia condicional. Desconozco si la sintaxis del lenguaje es la correcta pero mucho me temo que es más bien una mezcla de BASIC y pseudocódigo. Pero lo que más me ha llamado la atención sin duda es que la palabra THEN parece que se sale del monitor como si estuviera realizado en post-producción. Os invito a que ampliéis la imagen y comentéis lo que os parece a vosotr@s.

Os dejo aquí el código para los más curiosos.

10 DIM FourDigitPassword INTEGER
20 FOR i = 0 TO 9
30 		FOR j = 0 TO 9
40			FOR k = 0 TO 9
50				FOR l = 0 TO 9
60					FourDigitPassword = getFourDigits (i,j,k,l)
70					IF checkPasswordMatch(FourDigitPassword) = TRUE THEN
80						GOTO 140
90					END
100				NEXT l
110			NEXT k
120		NEXT j
130 NEXT i
140 PRINT FourDigitPassword

Aunque  la entrada está dentro del contexto de los Blooper Tech Movies, digamos que en esta ocasión no voy a ir más allá. La escena es creíble y queda bien integrada en la época en la que se desarrolla el capítulo. Por esto mismo, solamente espero que las temporadas venideras sean tan buenas y cuiden tanto los detalles como sus predecesoras.

Referencias

[1] Ficha IMDB – Stranger Thing

[2] Wikia de Stranger Things

[3] IBM 3180

[4] BASIC

Introducción

Esta vez se trata de un crackme realizado en VC++ 5.0/6.0 y en sus entrañas utiliza RSA-24. En este caso la peculiaridad es que el nombre no interviene en la generación del serial siendo un resultado único.

Resumen RSA

Parámetros

p = Primer número primo
q = Segundo número primo
e = Exponente público que cumpla MCD(e,(p-1)*(q-1))==1
n = Módulo público siendo n=p*q
d = Exponente privado que cumpla d=e^(-1) mod ((p-1)*(q-1))

De este modo e y n son la parte pública de la clave y d y n la parte privada. Los número primos p y q se utilizan solo para generar los parámetros y de ahí en adelante se pueden desechar.

Funciones de Cifrado/Descifrado

cifrado = descifrado ^ e mod n
descifrado = cifrado ^ d mod n

OllyDbg

Nuestro primer vistazo con OllyDbg nos muestra cuatro números de los que podemos hacernos una idea de que 9901 es un buen candidato a ser el exponente público (e) y 12790891 el módulo n ya que casualmente es un número de 24 bits. Los otros dos números de momento no nos dicen nada.

Referencias de texto

A continuación de los números tenemos la rutina de comprobación en la que comprueba que nuestro serial tenga 14 dígitos y lo divide en dos partes de 7 dígitos. Interesante ya que los otros dos números que aparecían en las referencias de texto tienen 7 dígitos cada uno.

004029CD  |.  68 DC004200   PUSH    RSA24.004200DC                         ;  ASCII "9901"
004029D2  |.  8D8C24 E40000>LEA     ECX,[DWORD SS:ESP+E4]
004029D9  |.  E8 52E7FFFF   CALL    RSA24.00401130
004029DE  |.  68 D0004200   PUSH    RSA24.004200D0                         ;  ASCII "12790891"
004029E3  |.  8D4C24 1C     LEA     ECX,[DWORD SS:ESP+1C]
004029E7  |.  C78424 640600>MOV     [DWORD SS:ESP+664],0
004029F2  |.  E8 39E7FFFF   CALL    RSA24.00401130
004029F7  |.  68 C8004200   PUSH    RSA24.004200C8                         ;  ASCII "8483678"
004029FC  |.  8D8C24 740200>LEA     ECX,[DWORD SS:ESP+274]
00402A03  |.  C68424 640600>MOV     [BYTE SS:ESP+664],1
00402A0B  |.  E8 20E7FFFF   CALL    RSA24.00401130
00402A10  |.  68 C0004200   PUSH    RSA24.004200C0                         ;  ASCII "5666933"
00402A15  |.  8D8C24 AC0100>LEA     ECX,[DWORD SS:ESP+1AC]
00402A1C  |.  C68424 640600>MOV     [BYTE SS:ESP+664],2
00402A24  |.  E8 07E7FFFF   CALL    RSA24.00401130
00402A29  |.  8B9424 680600>MOV     EDX,[DWORD SS:ESP+668]
00402A30  |.  83CE FF       OR      ESI,FFFFFFFF
00402A33  |.  8BFA          MOV     EDI,EDX
00402A35  |.  8BCE          MOV     ECX,ESI
00402A37  |.  33C0          XOR     EAX,EAX
00402A39  |.  C68424 600600>MOV     [BYTE SS:ESP+660],3
00402A41  |.  F2:AE         REPNE   SCAS [BYTE ES:EDI]
00402A43  |.  F7D1          NOT     ECX
00402A45  |.  49            DEC     ECX
00402A46  |.  83F9 0E       CMP     ECX,0E                                 ;  serial 0xE chars -> 14 digitos
00402A49  |.  0F85 63010000 JNZ     RSA24.00402BB2
00402A4F  |.  33C9          XOR     ECX,ECX
00402A51  |>  8A0411        /MOV     AL,[BYTE DS:ECX+EDX]                  ;  {
00402A54  |.  3C 30         |CMP     AL,30
00402A56  |.  0F8C 56010000 |JL      RSA24.00402BB2
00402A5C  |.  3C 39         |CMP     AL,39                                 ;  comprueba que el serial sea numerico
00402A5E  |.  0F8F 4E010000 |JG      RSA24.00402BB2
00402A64  |.  41            |INC     ECX
00402A65  |.  83F9 0E       |CMP     ECX,0E
00402A68  |.^ 7C E7         \JL      SHORT RSA24.00402A51                  ;  }
00402A6A  |.  8BC2          MOV     EAX,EDX
00402A6C  |.  C64424 17 00  MOV     [BYTE SS:ESP+17],0                     ;  {
00402A71  |.  C64424 0F 00  MOV     [BYTE SS:ESP+F],0
00402A76  |.  8B08          MOV     ECX,[DWORD DS:EAX]
00402A78  |.  894C24 10     MOV     [DWORD SS:ESP+10],ECX
00402A7C  |.  66:8B48 04    MOV     CX,[WORD DS:EAX+4]
00402A80  |.  66:894C24 14  MOV     [WORD SS:ESP+14],CX
00402A85  |.  8B4A 07       MOV     ECX,[DWORD DS:EDX+7]
00402A88  |.  8A40 06       MOV     AL,[BYTE DS:EAX+6]                     ;  divide el serial en dos partes de 7 digitos
00402A8B  |.  894C24 08     MOV     [DWORD SS:ESP+8],ECX
00402A8F  |.  884424 16     MOV     [BYTE SS:ESP+16],AL
00402A93  |.  8D42 07       LEA     EAX,[DWORD DS:EDX+7]
00402A96  |.  8D4C24 10     LEA     ECX,[DWORD SS:ESP+10]
00402A9A  |.  66:8B50 04    MOV     DX,[WORD DS:EAX+4]
00402A9E  |.  8A40 06       MOV     AL,[BYTE DS:EAX+6]                     ;  }

A continuación hace una serie de operaciones matemáticas para finalmente comparar el resultado con 8483678 y con 5666933. Lo que está haciendo es cifrar con nuestro serial en dos partes para comprobar que tenemos el número descifrado. Veamos un ejemplo con el serial 12345678901234.

descifrado ^ e mod n = cifrado
x1 = 1234567 y x2 = 8901234
1º parte del serial
x1 ^ 9901 mod 12790891 != 8483678
2º parte del serial
x2 ^ 9901 mod 12790891 != 5666933

Obviamente el resultado de las operaciones anteriores no da ese resultado y el Crackme nos tira fuera de modo que no nos queda más que atacar a RSA para obtener los primos p y q y el módulo privado d. De este modo podremos obtener los números buenos.

Los primos p y q se obtienen factorizando (botón Factor N) y una vez que tenemos p y q hallamos d (botón Calc. D). Todo esto es coser y cantar con la ayuda de la herramienta RSA-Tool 2. El exponente público e se introduce en hexadecimal.

Obteniendo p, q y d

Una vez que tenemos d hallamos el serial de forma sencilla con la herramienta Big Integer Calculator.

cifrado ^ d mod n = descifrado
1º parte del serial
8483678 ^ 10961333 mod 12790891 = 7167622
2º parte del serial
5666933 ^ 10961333 mod 12790891 = 3196885

SERIAL = 71676223196885
1º parte del serial
2º parte del serial

Enlaces

Hace unos años cuando empecé a trastear con Android y animado por mi afición a la Ingeniería Inversa, decidí realizar una serie de crackmes. Los dos primeros pasaron algo desapercibidos, pero el Crackme nº3 tuvo una gran repercusión en el mundillo y, aunque no fue el primer crackme para Android ni mucho menos, si que fue uno de los más estudiados. Todos ellos fueron publicados a través de crackmes.de y el nº3 en cuestión el 6 de Noviembre de 2010. Os dejo una lista de unas cuantas webs donde aparece analizado para que veáis la repercusión que a mi parecer tuvo.

Soluciones al crackme

Referencias al crackme

Links


Warning: This challenge is still active and therefore should not be resolved using this information. Aviso: Este reto sigue en
Rebuscando entre todo el caos que puede llegar a ser mi disco duro, he encontrado una serie de programas que
Introducción Aquí tenemos un CrackMe diferente a lo que estamos acostumbrados, ya que en vez del típico número de serie
El reto consiste en dos imágenes (v1.png y v2.png) que, a simple vista, parecen contener ruido aleatorio. Sin embargo, ambas

Warning: This challenge is still active and therefore should not be resolved using this information.
Aviso: Este reto sigue en activo y por lo tanto no se debería resolver utilizando esta información.

Introducción

En los retos de esteganografía ya uno se espera de todo, y cuantos más haces más enrevesados encuentras. Hoy no, hoy vamos a tratar un clásico dentro de este tipo de retos, ocultar un archivo dentro de otro.

Buscando la solución

Prácticamente lo primero que hago cuando me descargo una imágen en éste tipo de retos es abrirla con un editor hexadecimal, y en este caso hemos dado en el clavo. La abrimos con un editor cualquiera y al final del archivo encontramos que estamos tratando con un archivo ZIP (cabecera PK).

29-08-2014 03-00-03

La abrimos con 7zip y vemos el prometido archivo txt, dentro ¿qué abrá?

29-08-2014-03-02-19

Links

Rebuscando entre todo el caos que puede llegar a ser mi disco duro, he encontrado una serie de programas que utilizaba antiguamente cuando empezó a interesarme el Cracking. Ya sabéis que no soy partidario de crackear programas comerciales pero hoy voy a hacer una excepción ya que la versión del programa es muy vieja (1997) e incluso podría considerarse abandonware.

Este ejercicio es ideal para los que están empezando ya que es fácil localizar donde está el algoritmo y éste es sumamente sencillo.

Table of Contents

Algoritmo

Address   Hex dump          Command                                      Comments
00402213    E8 78170000     CALL HEdit.00403990
........
004039C0    8BC1            MOV EAX,ECX
004039C2    99              CDQ
004039C3    33C2            XOR EAX,EDX
004039C5    2BC2            SUB EAX,EDX
004039C7    83E0 03         AND EAX,00000003
004039CA    33C2            XOR EAX,EDX
004039CC    2BC2            SUB EAX,EDX
004039CE    8A540C 04       MOV DL,BYTE PTR SS:[ECX+ESP+4]  ;Coge el dígito i*3
004039D2    8A5C04 04       MOV BL,BYTE PTR SS:[EAX+ESP+4]  ;Coge el dígito i
004039D6    8D4404 04       LEA EAX,[EAX+ESP+4]             ;Guarda en memoria 12EE90
004039DA    32DA            XOR BL,DL
004039DC    41              INC ECX                         ; i +=1
004039DD    81F9 00010000   CMP ECX,100                     ;El bucle se repite 256 veces (0x100)
004039E3    8818            MOV BYTE PTR DS:[EAX],BL
004039E5  ^ 7C D9           JL SHORT HEdit.004039C0
004039E7    8B4424 04       MOV EAX,DWORD PTR SS:[ESP+4]
004039EB    85C0            TEST EAX,EAX                    ;Comprueba que el serial no sea 0
004039ED    7D 02           JGE SHORT HEdit.004039F1        ;Si es 0 se acabó
004039EF    F7D8            NEG EAX
004039F1    3B8424 0C010000 CMP EAX,DWORD PTR SS:[ESP+10C]  ;Comprobación de serial válido
004039F8    75 13           JNE SHORT HEdit.00403A0D        ;Si no es igual bad boy
004039FA    85C0            TEST EAX,EAX                    ;Comprueba que el serial no sea 0
004039FC    74 0F           JE SHORT HEdit.00403A0D         ;Si es 0 se acabó
004039FE    B8 01000000     MOV EAX,1
00403A03    5B              POP EBX
00403A04    81C4 00010000   ADD ESP,100
00403A0A    C2 0800         RETN 8

En resumen hace esto:

- Nombre introducido: deurus
- Convierte el nombre a mayúsculas

D  E  U  R  U  S
44 45 55 52 55 53 (En Hexadecimal)

1) 55 xor 44 = 11
2) 53 xor 45 = 16
3) 00 xor 55 = 55
4) 00 xor 52 = 52
   --------------- solo vale hasta aquí EAX(32 bits)
5) 00 xor 55 = 55
6) 00 xor 53 = 53
7) 00 xor 00 = 00
8) ...
            HEX         DEC
Serial = 52551611 = 1381307921

Como veis, realiza un bucle 256 veces pero como al final utiliza el registro EAX para hacer la comparación, solamente nos sirven las cuatro primeras operaciones. De hecho, no comprueba ni la longitud del nombre por lo que si introducimos un solo dígito como nombre, el serial será el valor ascii de ese dígito en decimal. La única comprobación que realiza es que el serial no sea 0.

Keygen

Os dejo una prueba de concepto en Javascript.

var nombre = "deurus";
nombre = nombre.toUpperCase();
var serial = "";

serial = serial + nombre.charCodeAt(3).toString(16) + nombre.charCodeAt(2).toString(16);
serial = serial + (nombre.charCodeAt(5) ^ nombre.charCodeAt(1)).toString(16);
serial = serial + (nombre.charCodeAt(2) ^ nombre.charCodeAt(0)).toString(16);
serial = "Nº Serie: " + parseInt(serial,16);

document.write(serial);

Enlaces

Yuri Software

Introducción

Aquí tenemos un CrackMe diferente a lo que estamos acostumbrados, ya que en vez del típico número de serie asociado a un nombre la comprobación se realiza mediante checkboxes con una matriz de 7×3. El CrackMe está realizado en Visual C++ lo que facilita en parte encontrar rápidamente la rutina de comprobación.

Comprobación

004013C5   > /8B7424 10     MOV     ESI,[DWORD SS:ESP+10]						;
004013C9   . |33FF          XOR     EDI,EDI
004013CB   > |8B86 74304000 MOV     EAX,[DWORD DS:ESI+403074]                   ;
004013D1   . |8BCB          MOV     ECX,EBX
004013D3   . |50            PUSH    EAX
004013D4   . |E8 6F020000   CALL    <JMP.&MFC42.#3092_CWnd::GetDlgItem>			; Lee el estado del checkbox
004013D9   . |8B48 20       MOV     ECX,[DWORD DS:EAX+20]
004013DC   . |6A 00         PUSH    0
004013DE   . |6A 00         PUSH    0
004013E0   . |68 F0000000   PUSH    0F0
004013E5   . |51            PUSH    ECX                                         ; 
004013E6   . |FFD5          CALL    NEAR EBP
004013E8   . |3B86 20304000 CMP     EAX,[DWORD DS:ESI+403020]					; Comprueba el estado del checkbox (1 activado 0 desactivado)
004013EE   . |75 20         JNZ     SHORT Matrix_C.00401410						; Salto a chico malo
004013F0   . |47            INC     EDI											; Incrementa contador
004013F1   . |83C6 04       ADD     ESI,4
004013F4   . |83FF 07       CMP     EDI,7										; ¿Hemos terminado de leer las columnas? ¿contador = 7?
004013F7   .^|7C D2         JL      SHORT Matrix_C.004013CB                     ; si terminan las columnas deja pasar
004013F9   . |8B4424 10     MOV     EAX,[DWORD SS:ESP+10]
004013FD   . |83C0 1C       ADD     EAX,1C										; contador de filas
00401400   . |83F8 54       CMP     EAX,54										; 3 filas = 1C+1C+1C=54
00401403   . |894424 10     MOV     [DWORD SS:ESP+10],EAX
00401407   .^\7C BC         JL      SHORT Matrix_C.004013C5						; ¿Hemos terminado de leer la fila? ¿contador = 54?
00401409   .  68 D8304000   PUSH    Matrix_C.004030D8                           ;  ASCII "Registration successful!"
0040140E   .  EB 05         JMP     SHORT Matrix_C.00401415
00401410   >  68 C8304000   PUSH    Matrix_C.004030C8                           ;  ASCII "Not registered!"

En la rutina de comprobación se ve fácil un CMP EDI,7 por lo que podemos deducir que si el creador no se ha molestado mucho la comprobación se realiza de izquierda a derecha y de arriba hacia abajo.

Orden de comprobación

Tal es así que si ponemos un breakpoint en 4013E8, podemos ir sacando el estado correcto de los checkboxes sin mucha molestia.

Resultado final

Enlaces

El reto consiste en dos imágenes (v1.png y v2.png) que, a simple vista, parecen contener ruido aleatorio. Sin embargo, ambas forman parte de un sistema de criptografía visual en la que cada imagen contiene información parcial que no es interpretable por separado, pero que al combinarse correctamente revelan información oculta.

La trampa está en que la combinación no se hace con operaciones normales como suma, resta o multiplicación. El autor del reto espera que el jugador use una herramienta como StegSolve y pruebe distintas operaciones tipo XOR, AND o MUL hasta encontrar una transformación en la que uno de los métodos muestre algo significativo. El truco está en llegar a la conclusión de que una de las imágenes hay que invertirla antes de combinar ambas imágenes. Todo esto se puede hacer con StegSolve sin necesidad de utilizar ninguna herramienta adicional, pero voy a aprovechar para hacerlo con python y así de paso entendemos como realiza las operaciones StegSolve. En resumen, para resolver el reto basta con:

  1. Invertir (Colour Inversion XOR) una de las imágenes.
  2. Combinar ambas imágenes mediante Analyse > Combine images.
  3. Operación MUL del combinador.

La operación MUL no es una multiplicación normalizada, sino una multiplicación de enteros de 24 bits (0xRRGGBB) con overflow, algo que la mayoría de herramientas no replican correctamente.

¿Por qué aparece la solución con esa combinación

Las imágenes están preparadas para que ciertos bits de color en una imagen sean el complemento de los de la otra. Por tanto:

  • Si se muestran tal cual → parecen ruido
  • Si se combinan mediante XOR → parte de la estructura aparece, pero no se ve el resultado correcto
  • Si se combinan mediante MUL «normal» → tampoco aparece
  • Si se aplica la multiplicación bitwise exacta usada por StegSolve → se alinean las partes ocultas

La operación MUL de StegSolve no es una multiplicación de píxeles, es decir, no hace:

R = (R1 * R2) / 255

sino:

c1 = 0xRRGGBB  (pixel 1)
c2 = 0xRRGGBB  (pixel 2)
resultado = (c1 * c2) & 0xFFFFFF

Con todo esto claro, he preparado un script para combinar las imágenes de forma automática.

import os
import numpy as np
from PIL import Image

# =========================================================
# UTILIDADES
# =========================================================

def ensure_output():
    if not os.path.exists("output"):
        os.makedirs("output")

def load_rgb(path):
    img = Image.open(path).convert("RGB")
    return np.array(img, dtype=np.uint32)

def save_rgb(arr, name):
    Image.fromarray(arr.astype(np.uint8), "RGB").save(os.path.join("output", name))

def invert_xor(arr):
    """Colour Inversion (Xor) de StegSolve."""
    out = arr.copy()
    out[..., :3] = 255 - out[..., :3]
    return out

# =========================================================
# FUNCIONES DE COMBINER EXACTAS DE STEGSOLVE
# =========================================================

def to24(arr):
    """Convierte RGB → entero 0xRRGGBB."""
    return ((arr[..., 0] << 16) |
            (arr[..., 1] << 8)  |
             arr[..., 2])

def from24(c):
    """Convierte entero 0xRRGGBB → RGB."""
    R = (c >> 16) & 0xFF
    G = (c >> 8)  & 0xFF
    B = c & 0xFF
    return np.stack([R, G, B], axis=-1).astype(np.uint8)

# ------------------------------
# Funciones auxiliares
# ------------------------------

def comb_xor(c1, c2):
    return from24((c1 ^ c2) & 0xFFFFFF)

def comb_or(c1, c2):
    return from24((c1 | c2) & 0xFFFFFF)

def comb_and(c1, c2):
    return from24((c1 & c2) & 0xFFFFFF)

def comb_add(c1, c2):
    return from24((c1 + c2) & 0xFFFFFF)

def comb_add_sep(c1, c2):
    R = (((c1 >> 16) & 0xFF) + ((c2 >> 16) & 0xFF)) & 0xFF
    G = (((c1 >> 8)  & 0xFF) + ((c2 >> 8)  & 0xFF)) & 0xFF
    B = ((c1 & 0xFF) + (c2 & 0xFF)) & 0xFF
    return from24((R << 16) | (G << 8) | B)

def comb_sub(c1, c2):
    return from24((c1 - c2) & 0xFFFFFF)

def comb_sub_sep(c1, c2):
    R = (((c1 >> 16) & 0xFF) - ((c2 >> 16) & 0xFF)) & 0xFF
    G = (((c1 >> 8)  & 0xFF) - ((c2 >> 8)  & 0xFF)) & 0xFF
    B = ((c1 & 0xFF) - (c2 & 0xFF)) & 0xFF
    return from24((R << 16) | (G << 8) | B)

def comb_mul(c1, c2):
    """MUL EXACTO StegSolve"""
    return from24((c1 * c2) & 0xFFFFFF)

def comb_mul_sep(c1, c2):
    R = (((c1 >> 16) & 0xFF) * ((c2 >> 16) & 0xFF)) & 0xFF
    G = (((c1 >> 8)  & 0xFF) * ((c2 >> 8)  & 0xFF)) & 0xFF
    B = ((c1 & 0xFF) * (c2 & 0xFF)) & 0xFF
    return from24((R << 16) | (G << 8) | B)

def comb_lightest(c1, c2):
    """Máximo por canal"""
    R = np.maximum((c1 >> 16) & 0xFF, (c2 >> 16) & 0xFF)
    G = np.maximum((c1 >> 8)  & 0xFF, (c2 >> 8)  & 0xFF)
    B = np.maximum(c1 & 0xFF, c2 & 0xFF)
    return from24((R << 16) | (G << 8) | B)

def comb_darkest(c1, c2):
    """Mínimo por canal"""
    R = np.minimum((c1 >> 16) & 0xFF, (c2 >> 16) & 0xFF)
    G = np.minimum((c1 >> 8)  & 0xFF, (c2 >> 8)  & 0xFF)
    B = np.minimum(c1 & 0xFF, c2 & 0xFF)
    return from24((R << 16) | (G << 8) | B)

# Lista de transformaciones
TRANSFORMS = {
    "xor": comb_xor,
    "or": comb_or,
    "and": comb_and,
    "add": comb_add,
    "add_sep": comb_add_sep,
    "sub": comb_sub,
    "sub_sep": comb_sub_sep,
    "mul": comb_mul,
    "mul_sep": comb_mul_sep,
    "lightest": comb_lightest,
    "darkest": comb_darkest,
}

# =========================================================
# GENERACIÓN DE TODAS LAS COMBINACIONES
# =========================================================

def generate_all(imA, imB, labelA, labelB):
    print(f"Generando combinaciones: {labelA} vs {labelB}")

    c1 = to24(imA)
    c2 = to24(imB)

    for name, fun in TRANSFORMS.items():
        out = fun(c1, c2)
        save_rgb(out, f"{labelA}__{labelB}__{name}.png")

    print(f"{labelA}-{labelB} completado.")

# =========================================================
# MAIN
# =========================================================

ensure_output()

print("Cargando imágenes v1.png y v2.png...")
im1 = load_rgb("v1.png")
im2 = load_rgb("v2.png")

print("Generando invertidas estilo StegSolve...")
im1_x = invert_xor(im1)
im2_x = invert_xor(im2)

save_rgb(im1_x, "v1_xored.png")
save_rgb(im2_x, "v2_xored.png")

# Generar las 52 combinaciones:
generate_all(im1,   im2,   "v1",   "v2")
generate_all(im1_x, im2,   "v1x",  "v2")
generate_all(im1,   im2_x, "v1",   "v2x")
generate_all(im1_x, im2_x, "v1x",  "v2x")

print("\nResultados en carpeta ./output/")

A continuación os muestro parte de las imágenes generadas por el script. El secreto oculto era un código QR que nos da la solución al reto.