Blog de Cymo - un poquito abandonado

martes, 13 de agosto de 2013

Hackit 2013 - Nivel 2 (parte I)

Hola de nuevo, gente. Para darle gusto a marcan así como a los inexistentes lectores de este no-blog, vamos a publicar una posible solución al nivel 2 del hackit, la que se daba por hecho que cualquiera iba a intentar (aunque gracias a los chicos de DiarioLinux conocemos una forma más rápida) ;-)

Recordemos la prueba consistía en un archivo RAR cifrado, del que nos daban una pista sobre la contraeña:


 echo sha1(md5($password)); ?>
77134aa1b02b61cd841d2a81bf64796a31234e28


Tanto SHA1 como MD5 son funciones de resumen criptográfico (reducciones criptográficas, digests), esto es: cualquiera de las dos toma como entrada un flujo de octetos de tamaño arbitrario y producen una salida de tamaño fijo: 20 octetos (160bits) para SHA-1 y 16 octetos (128 bits) para MD5.

Existen muchas funciones que convierten unos datos arbitrarios en uno de tamaño fijo: por ejemplo la función CERO que, para cualquier dato de entrada siempre produce el resultado (constante) 0.

Las funciones de resumen criptográfico tienen interés porque su resultado no es trivial y cumple una serie de  propiedades, de las que cito las relevantes para entender como resolver el nivel, a continuación:

  • Calcular la salida para unos datos de entrada no debe ser muy costoso.
  • Para una misma entrada, la salida siempre es la misma.
  • Es muy difícil (costoso) averiguar el mensaje original a partir del valor resumen.

Así pues, salvo que "hagamos trampa" y usemos tablas arcoíris propios o ajenos, como proponen en DiarioLinux, tendremos que usar un enfoque más... programático.

Implementaremos lo que se llama un "ataque basado en diccionario", esto es, partiremos de un diccionario de palabras e iremos computando los hashes o resúmenes de cada una de las palabras, a ver si encontramos una coincidencia con lo que nos proponen en el reto. Como en el enunciado se nos avisa que el "orgo" correspondiente nació en Whiston, supondremos que fala Inglés, y tiraremos del diccionario correspondiente:

#  apt-get install wbritish-insane
#  wc -l /usr/share/dict/british-english-insane
650656 /usr/share/dict/british-english-insane

Tenemos 650656 palabras para probar. Ahí es nada.

A continuación nos tenemos que currar algún programa que implemente el algoritmo del marinero:

Tome una palabra del fichero
Calcule el MD5 de dicha palabra
    Exprese el MD5 en hexadecimal, minúsculas (¡el MD5 son 16 octetos!)
Calcule el SHA1 del valor hexadecimal del paso anterior
¿Coincide con el valor con lo buscado? Pues si es que no, siguiente palabra. Y si es que sí, tenemos la palabra buscada.


En mi caso, el diccionario para la máquina Java estaba guardado en ~/temp. Ahí van la soluciones propuestas en Python, PHP (ya que así se planteaba en el reto) y JAVA.

El tiempo de ejecución en mi pc fue de 785 milisegundos para la versión Java, 1.1 segundos para PHP y de 2.12 segundos en Python; para obtener la clave 'hackster'. En qué andaría pensando el orgo para escoger esa contraseña ...

Python

#!/usr/bin/python
# Python 2.7.3

from hashlib import md5, sha1

with open("/usr/share/dict/british-english-insane", "rt") as f:
    for word in f.readlines():
        if sha1(md5(word.rstrip()).hexdigest()).hexdigest() == '77134aa1b02b61cd841d2a81bf64796a31234e28':
            print 'Your word is %s' % word
            exit

PHP


foreach (file("/usr/share/dict/british-english-insane", FILE_IGNORE_NEW_LINES) as $i => $palabra)
        if (sha1(md5($palabra)) == '77134aa1b02b61cd841d2a81bf64796a31234e28') {
                echo 'El password es '. $palabra ;
                break;
        }
?>


Java (hazte un café para mientras te lo lees y eso)

package org.euskal.hackit.passCrack;

import java.io.BufferedReader;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.UnsupportedEncodingException;
import java.nio.charset.Charset;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.Arrays;
import java.util.LinkedList;
import java.util.List;

/**
 * Hackit 2013 - Level 2 Cracker
 * 
 * @author cymo
 * 
 */

public class Crackit implements Runnable {

 /**
  * ; ?> 77134aa1b02b61cd841d2a81bf64796a31234e28
  * 
  * @param args
  */

 private static final int ERROR_LOADING_WORDLIST = 100;

 /**
  * Name of the dictionary to load & use
  */
 String dictName = "american-english-insane";

 /**
  * Words we are loading in memory as byte-arrays (faster to compare)
  */
 List<byte[]> wordList = new LinkedList<>();

 /**
  * The md5
  */
 MessageDigest md5;

 /**
  * The sha1
  */
 MessageDigest sha1;

 /**
  * Expected hash, as byte-array
  */
 byte[] expectedHash;

 /**
  * Constructor
  */
 Crackit() {
  String hash = "77134aa1b02b61cd841d2a81bf64796a31234e28"; // sha1(md5($password)

  expectedHash = new byte[hash.length() / 2];
  // Convert the hexstring-value to a bytearraybacked-value
  for (int i = 0; i < hash.length(); i += 2) {
   String strByte = "" + hash.charAt(i) + hash.charAt(i + 1);
   expectedHash[i / 2] = Integer.valueOf(strByte, 16).byteValue();
  }

 }

 public static void main(String[] args) {
  Crackit program = new Crackit();
  long initMillis = System.currentTimeMillis();
  program.run();
  System.out.println(String.format("Excecution took: %d milliseconds", System.currentTimeMillis() - initMillis));
 }

 @Override
 public void run() {
  try {
   // Load wordList
   loadWordList();
   // Create MessageDigests
   initDigests();
   // Iterate cracking
   iterateCracking();
  } catch (UnsupportedEncodingException e) {
   e.printStackTrace();
   System.exit(102);
  }

 }

 /**
  * Iterate over the dictionary, computing sha1(md5(word)) and comparing it
  * to the desired result
  * 
  * @throws UnsupportedEncodingException
  *             If your JVM is broken enough not to support ASCII encoding
  */
 private void iterateCracking() throws UnsupportedEncodingException {

  int i = 0;
  for (byte[] word : wordList) {
   if (i > 0 && i % 100 == 0)
    System.out.printf("Progress: %d of %d (%s)\n", i,
      wordList.size(), new String(word, "ASCII"));
   ++i;
   byte[] md5Bytes = md5.digest(word); // this is binary!
   byte[] md5AsHex = Conversion.byteArray2HexAsciiByteArray(md5Bytes);
   byte[] result = sha1.digest(md5AsHex); // get the sha1

   if (Arrays.equals(result, expectedHash)) {
    System.out.printf("The missing word is: %s\n", new String(word,
      "ASCII"));
    return;
   }
  }
  System.out.println("Password not found in dictionary :S");

 }

 /**
  * Create the message digests
  */
 protected void initDigests() {
  try {
   md5 = MessageDigest.getInstance("MD5");
   sha1 = MessageDigest.getInstance("SHA1");
  } catch (NoSuchAlgorithmException e) {
   e.printStackTrace();
   System.exit(101);
  }
  System.out.println("Message Digests Succesfully Created!");
 }

 /**
  * Loads a list of words from a file located at ~/temp
  */
 private void loadWordList() {
  String home = System.getProperty("user.home");
  String fileSeparator = System.getProperty("file.separator");
  String fileName = home + fileSeparator + "temp" + fileSeparator
    + dictName;
  try {
   BufferedReader br = new BufferedReader(new InputStreamReader(
     new FileInputStream(fileName), Charset.forName("ASCII")));
   String line = br.readLine();

   while (line != null) {
    // do not store the words as strings but as byte[]
    wordList.add(line.getBytes("ASCII")); 
    line = br.readLine();
   }
   br.close();
   System.out.printf("Wordlist of %d words, loaded\n", wordList.size());
  } catch (IOException e) {
   e.printStackTrace();
   System.exit(ERROR_LOADING_WORDLIST);
  }

 }

}

/**
 * Helper class to convert a binary-value backed by a byte[], to a hex-value
 * backed by another byte[]
 * 
 * @author cymo
 * 
 */
class Conversion {
 private static final int a_OFFSET = 'a' - 0x0a;
 /**
  * Helper variable for MD5s only!
  */
 private static byte[] resultHelper = new byte[32];
 
 /**
  * Thread UNSAFE method to convert a bin-value (byte[]) to a byte[] backed
  * asciihex value
/>
  * 
  * @param binaryArray
  *            The binary value to convert
  * @return The asciihex-value backed by a byte[]
  */
 static byte[] byteArray2HexAsciiByteArray(byte[] binaryArray) {

  int i = 0;
  for (byte b : binaryArray) {
   resultHelper[i] = (byte) ((b & 0xF0) >>> 4);
   resultHelper[i] += ((resultHelper[i++] <= 9) ? '0'
     : a_OFFSET);
   resultHelper[i] = (byte) (b & 0x0F);
   resultHelper[i] += ((resultHelper[i++] <= 9) ? '0'
     : a_OFFSET);
  }

  return resultHelper;
 }
}


Nos vemos (bueno, es un decir, lector inexistente) en la parte II donde explicaremos una vía alternativa a la DL para obtener las contraseñas de Maven.


viernes, 9 de agosto de 2013

HackIt! 2013. Level 5. Cifrado casero

Continuando (y agradeciendo) el trabajo publicado por Diario Linux, y pasando de puntillas por el fail del algoritmo (conocido, pero ignorado), quiero pasar a comentar cuál era el planteamiento de resolución que esperaba que se aplicara (si bien ellos lo resolvieron de una manera más sencilla).

Si no hiciste el hackit, o no recuerdas de qué iba el nivel, te sugiero que te leas previamente los dos artículos publicados por DL: primero y segundo.

Uno de los ficheros que se entregaban era el correspondiente a la clase PrepareHackit2013, que como su nombre sugiere, era el programa que preparaba los ficheros "clazz" for the great craic:


public static void main(final String[] args) {
final CryptUtil crypter = new CryptUtil(PasswordReveal.daKey);
final String clearTextClassNames[] = { "TestClass1", "TestClass2" };
final String cryptedClassNames[] = { "PasswordReveal" };

encryptClass(crypter, pkgName, true, cryptedClassNames);
encryptClass(crypter, pkgName, false, clearTextClassNames);
}

El false de la segunda llamada a encriptación es lo que nos abre las puertas a un ataque criptográfico de libro: las clases TestClass1 y TestClass2, se cifran pero no serán borradas. Unido a lo patatero, no ya del algoritmo, sino del mecanismo de cifrado que usa un keystream generado con la misma clave para todos los ficheros sin ningún tipo de IV/SV, implica un big fail de seguridad.

Al turrón: 

La operación XOR (⊕) es reversible: si A ⊕ B = C => A ⊕ C = B

Resulta que disponemos de al menos un texto claro (TestClass1.class) y su correspondiente versión cifrada (TestClass1.clazz) lo cual nos permite obtener un keystream de 1.35kB de longitud 
Basta con aplicar el keystream a cualquier texto cifrado de longitud inferior o igual, y obtendremos el texto en claro. Y viceversa.


Osea: TestClass1.class ⊕ keystream = TestClass1.clazz
=>
TestClass1.clazz ⊕ TestClass1.class = keystream

Conociendo el keystream, resulta que el fichero de la clase Java que nos interesa es PasswordReveal.clazz, que ocupa 928 bytes... y que por tanto, podemos descifrar sin conocer la clave aplicando únicamente el keystream porque:

PasswordReveal.clazz ⊕ keystream = PasswordReveal.class

Y eso era todo: sólo faltaba ejecutar dicha clase y obtener la clave del nivel que era... bueno, descúbrela por ti mismo. Sólo diré que encima de la CPU tengo una caja de caramelos de café (de ahí lo apropiado del nivel Java).

Una posible solución Java7:



package org.euskal.hackit.solution._2013;

import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;
import java.nio.file.FileSystems;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.StandardOpenOption;

/**
 * Example solution for 2013 Java-crypt-hackit-cymolevel
 * @author Cymo
 *
 */
public class Solve2013 implements Runnable {

/**
*  Eclipse project, bytecode located @bin folder
*/
private static final String BYTECODE_SUBFOLDER = "bin\\";

/**
* Package with prepared files
*/
public static final String thePackage = "org.euskal.hackit._2013";

/**
* Entry point
* @param args Will be unmercifully ignored
*/
public static void main(String[] args) {
new Solve2013().run();
}

@Override
public void run() {
// Load TestClass1.class
// Load TestClass1.clazz
// Load PasswordReveal.clazz
// Get the keystream
// Apply keystram to PasswordReveal
// Write back PasswordReveal.class
// Load an excecute PasswordReveal.class

try {
String fileSeparator = FileSystems.getDefault().getSeparator();
String rootDir = BYTECODE_SUBFOLDER + thePackage.replace(".", fileSeparator)
+ fileSeparator;

Path[] paths = {
Paths.get(rootDir + "TestClass1.clazz"),
Paths.get(rootDir + "TestClass1.class"),
Paths.get(rootDir + "PasswordReveal.clazz"),
Paths.get(rootDir + "PasswordReveal.class") };

ByteBuffer[] buffers = new ByteBuffer[paths.length];

// Load the pair (plain text + cypertext) and PasswordReveal cyphertext
for (int i = 0; i < 3; i++) {
System.out.println("[LOAD] " + paths[0]);
// Not a good general solution, but close enough due to small filesize
buffers[i] = ByteBuffer.allocateDirect((int) Files.size(paths[i]));
FileChannel fc = FileChannel.open(paths[i],StandardOpenOption.READ);
fc.read(buffers[i]); fc.close();
// Buffer is loaded, we will want to read from it afterwards
buffers[i].flip();
}

// Check cyphertext.length == cleartext.length and there is enough data to generate the keystream to decypher PasswordReveal 
if (!(buffers[0].limit() == buffers[1].limit() && buffers[1]
.limit() >= buffers[2].limit())) {
System.err.println("Something went wrong :S");
System.exit(100);
}

// Allocate space for decrypted class
int cryptedBufferSize = buffers[paths.length - 2].limit();
buffers[3] = ByteBuffer.allocateDirect(cryptedBufferSize);

// Decrypt data into ram
for (int i = 0; i < cryptedBufferSize; i++)
buffers[3].put((byte) (buffers[0].get() ^ buffers[1].get() ^ buffers[2].get()));

buffers[3].flip();
FileChannel dest = FileChannel.open(paths[3],
StandardOpenOption.CREATE,
StandardOpenOption.TRUNCATE_EXISTING,
StandardOpenOption.WRITE);
System.out.println("[WRITE] Decrypted " + paths[3]);
dest.write(buffers[3]);
dest.close();

System.out.println("\n\n\n");
// Behold!
((Runnable) Class.forName(thePackage + ".PasswordReveal")
.newInstance()).run();

} catch (Exception e) {
e.printStackTrace();
}

}
}


Otra pysibilidad:


#!/usr/bin/python
# python 2.7

import os
from subprocess import call

destPath='org/euskal/hackit/_2013'
try:
        os.makedirs( destPath )
except:
        pass

fs = ( open("TestClass1.class", "rb"),  open("TestClass1.clazz", "rb"), open("PasswordReveal.clazz", "rb"),  open("%s/PasswordReveal.class" % destPath, "wb+") )

crb, i= fs[2].read(1), 0
while crb:
        crb, i = ord(crb), i+1
        ctb = ord(fs[0].read(1)) ^ ord(fs[1].read(1)) ^ crb
        fs[3].write('%c' % ctb)
        crb = fs[2].read(1)

print 'Written %d octets' % i

for f in fs:
        f.close()
call(['java', '%s.PasswordReveal' % destPath.replace('/', '.')])