Blog de Cymo - un poquito abandonado

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('/', '.')])