ØxOPOSɆC | Underground Leaks (Crypto)

Write-up for retired Oposec's October challenge

ØxOPOSɆC | Underground Leaks (Crypto)
Rui Tinto admitted having some information about the snitch from the 0XOPOLEAKS. However, he's unwilling to give the passwords unless his demands of having Kripthor on 0xOPOSEC get fulfilled. 
Meanwhile, government forces tried to crack the passwords with no success. 
We know from underground sources that Rui Tinto does cipher each file individually for higher security and privacy. 
We manage to put our hands on his top-notch closed source software used for encrypting the information.
Leak: https://sefod.eu/oposec/underground_leaks.tar.gz

The tar contains a java executable and 3 files that look like ciphertexts:

~ ls -la ./
...
-rw-r--r--   1 inesmartins  staff    32 Oct 16 11:09 1
-rw-r--r--   1 inesmartins  staff    32 Oct 16 11:09 2
-rw-r--r--   1 inesmartins  staff    32 Oct 16 11:09 3
-rw-r--r--   1 inesmartins  staff  3992 Oct 16 10:38 HelloWorldApp.class
~ xxd 1
00000000: ad42 207a 20db eb81 b536 25a0 865e 6778  .B z ....6%..^gx
00000010: f23d 2a26 8e76 9970 6cfd 0673 2051 002b  .=*&.v.pl..s Q.+

~ xxd 2
00000000: 2dcd cdcd a751 4e47 fd46 e8e0 8334 eade  -....QNG.F...4..
00000010: 1796 e6e4 b25f 6f8d 5d47 f2b6 f688 38f7  ....._o.]G....8.

~ xxd 3
00000000: dada da4d 43e1 9595 3914 ca23 d84a df52  ...MC...9..#.J.R
00000010: d897 0b06 89e7 773e 7f31 7dca 4a52 e2b7  ......w>.1}.JR..

It seems like this application encrypts a secret in a file that we provide:

~ touch plaintext.txt
~ java HelloWorldApp encode plaintext.txt
Hey kid, do you have any leaks?
tell me:
this is my very secret secret
Keep this key: E5F1F22FE01B9E10D17E3825ACE23799

Once we have they key, we're then able to get the secret back using the same application:

~ java HelloWorldApp decode plaintext.txt
Please provide a key:
E5F1F22FE01B9E10D17E3825ACE23799
this is my very secret secret

Analysing the application

I started by decompiling the file using tintinweb:

import java.io.BufferedReader;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.InputStreamReader;
import java.util.Random;
import javax.crypto.Cipher;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;

class HelloWorldApp {
  static String encryption_mode = "AES/CBC/PKCS5Padding";
  
  public static void main(String[] paramArrayOfString) {
    if (paramArrayOfString.length == 2) {
      if (paramArrayOfString[0].equals("decode")) {
        decode(paramArrayOfString[1]);
      } else if (paramArrayOfString[0].equals("encode")) {
        encode(paramArrayOfString[1]);
      } else {
        System.out.print("encode|decode file_input|file_output");
        System.exit(0);
      } 
    } else {
      System.out.print("encode|decode file_input|file_output");
      System.exit(0);
    } 
  }
  
  public static void encode(String paramString) {
    Random random = new Random(5040508L);
    int i = (new Random()).nextInt(1000000);
    for (b = 0; b < i; b++)
      random.nextInt(256); 
    try {
      String str1 = "read text from input";
      BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(System.in));
      System.out.print("Hey kid, do you have any leaks?\ntell me:\n");
      String str2 = bufferedReader.readLine();
      str1 = "creating iv key";
      byte[] arrayOfByte1 = new byte[16];
      for (byte b1 = 0; b1 < 16; b1++) {
        int j = random.nextInt(256);
        arrayOfByte1[b1] = (byte)j;
      } 
      str1 = "creating key";
      byte[] arrayOfByte2 = new byte[16];
      for (byte b2 = 0; b2 < 16; b2++) {
        int j = random.nextInt(256);
        arrayOfByte2[b2] = (byte)j;
      } 
      str1 = "encrypting content";
      byte[] arrayOfByte3 = encrypt(str2, arrayOfByte2, arrayOfByte1);
      str1 = "write content to file";
      FileOutputStream fileOutputStream = new FileOutputStream(paramString);
      fileOutputStream.write(arrayOfByte1);
      fileOutputStream.write(arrayOfByte3);
      fileOutputStream.close();
      System.out.print("Keep this key: ");
      System.out.print(bytesToHex(arrayOfByte2));
    } catch (Exception b) {
      Exception exception;
      exception.printStackTrace();
    } 
  }
  
  public static void decode(String paramString) {
    String str = "read file, split iv and encrypted content and decode with the input key";
    try {
      File file = new File(paramString);
      FileInputStream fileInputStream = new FileInputStream(file);
      byte[] arrayOfByte1 = fileInputStream.readAllBytes();
      byte[] arrayOfByte2 = new byte[16];
      for (byte b = 0; b < arrayOfByte2.length; b++)
        arrayOfByte2[b] = arrayOfByte1[b]; 
      System.out.print("Please provide a key:\n");
      BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(System.in));
      String str1 = bufferedReader.readLine();
      byte[] arrayOfByte3 = hexStringToByteArray(str1);
      byte[] arrayOfByte4 = new byte[arrayOfByte1.length - arrayOfByte2.length];
      for (int i = arrayOfByte2.length; i < arrayOfByte1.length; i++)
        arrayOfByte4[i - arrayOfByte2.length] = arrayOfByte1[i]; 
      String str2 = decrypt(arrayOfByte4, arrayOfByte3, arrayOfByte2);
      System.out.print(str2);
      System.out.print("\n");
    } catch (Exception exception) {
      exception.printStackTrace();
    } 
  }
  
  public static void noNeedToLookBelowThis() {}
  
  private static final char[] HEX_ARRAY = "0123456789ABCDEF".toCharArray();
  
  public static String bytesToHex(byte[] paramArrayOfByte) {
    char[] arrayOfChar = new char[paramArrayOfByte.length * 2];
    for (byte b = 0; b < paramArrayOfByte.length; b++) {
      byte b1 = paramArrayOfByte[b] & 0xFF;
      arrayOfChar[b * 2] = HEX_ARRAY[b1 >>> 4];
      arrayOfChar[b * 2 + 1] = HEX_ARRAY[b1 & 0xF];
    } 
    return new String(arrayOfChar);
  }
  
  public static byte[] hexStringToByteArray(String paramString) {
    int i = paramString.length();
    byte[] arrayOfByte = new byte[i / 2];
    for (byte b = 0; b < i; b += 2)
      arrayOfByte[b / 2] = 
        (byte)((Character.digit(paramString.charAt(b), 16) << 4) + Character.digit(paramString.charAt(b + 1), 16)); 
    return arrayOfByte;
  }
  
  public static byte[] encrypt(String paramString, byte[] paramArrayOfByte1, byte[] paramArrayOfByte2) throws Exception {
    Cipher cipher = Cipher.getInstance(encryption_mode);
    SecretKeySpec secretKeySpec = new SecretKeySpec(paramArrayOfByte1, "AES");
    cipher.init(1, secretKeySpec, new IvParameterSpec(paramArrayOfByte2));
    return cipher.doFinal(paramString.getBytes());
  }
  
  public static String decrypt(byte[] paramArrayOfByte1, byte[] paramArrayOfByte2, byte[] paramArrayOfByte3) throws Exception {
    Cipher cipher = Cipher.getInstance(encryption_mode);
    SecretKeySpec secretKeySpec = new SecretKeySpec(paramArrayOfByte2, "AES");
    cipher.init(2, secretKeySpec, new IvParameterSpec(paramArrayOfByte3));
    return new String(cipher.doFinal(paramArrayOfByte1));
  }
}

The first thing that stood out was the AES/CBC/PKCS5Padding encryption mode, which is vulnerable to  Padding oracle attacks. However, after trying and failing to follow this route, I took a closer look at to the logic behind the random number generation:

public static void encode(String paramString) {
    ...
    Random random = new Random(5040508L);
    int i = (new Random()).nextInt(1000000);
    for (int b = 0; b < i; b++) {
    	random.nextInt(256);
    }
    ...
    try {
    
    	byte[] iv = new byte[16];
    	for (byte b1 = 0; b1 < 16; b1++) {
            int j = random.nextInt(256);
            iv[b1] = (byte)j;
    	}
        
        byte[] aeskey = new byte[16];
    	for (byte b2 = 0; b2 < 16; b2++) {
            int j = random.nextInt(256);
            aeskey[b2] = (byte)j;
    	}
        ...
    }
}

It seems like:

  • the random numbers are being generated using a Random object (a Java pseudo-number generator), which is being initialised with a fixed seed: 5040508L;
  • the AES key is being generated right after the IV.

So, it's just a matter of brute-forcing the solution:

import java.util.Random;

public class RandomBreaker {
    
    public static String iv = "bbbbbbded5a15b60b0d7fdfe922e53a5";
    
    public static void main(String[] args) {
        byte[] ivData = hexStringToByteArray(iv);
        Random random = new Random(5040508L);
        for (int i = 0; i < 1000000; i++) {
            if (matchesNext16(random, ivData)) {
                String keyHex = byteArrayToHexString(getNext16Bytes(random));
                System.out.println(keyHex);
                return;
            }
        }
    }
    
    private static boolean matchesNext16(Random random, byte[] bytes) {
        for (int i = 0; i < bytes.length; i++) {
            byte nextByte = (byte)random.nextInt(256);
            if (nextByte != bytes[i]) {
                return false;
            }
        }
        return true;
    }
    
    private static byte[] getNext16Bytes(Random random) {
        byte[] bytes = new byte[16];
        for (int i = 0; i < 16; i++) {
            bytes[i] = (byte)random.nextInt(256);
        }
        return bytes;
    }
    
    private static String byteArrayToHexString(byte[] a) {
        StringBuilder sb = new StringBuilder(a.length * 2);
        for(byte b: a)
            sb.append(String.format("%02x", b));
        return sb.toString();
    }
    
    private static byte[] hexStringToByteArray(String s) {
        int len = s.length();
        byte[] data = new byte[len / 2];
        for (int i = 0; i < len; i += 2) {
            data[i / 2] = (byte) ((Character.digit(s.charAt(i), 16) << 4) + Character.digit(s.charAt(i+1), 16));
        }
        return data;
    }
    
}

After repeating this process for all the 3 files, we get the flag: flag{The_truth_is_out_there}.

You can find the repository with the original challenge and the other solutions here.