Misc Ctf

CTF - Solving Serialized

CTF - Solving Serialized

In this post I will solve the challenge called Serializado, created for the Ekoparty 2020 - Mobile Hacking Space CTF. In this challenge we have the following file.

serializable.ab

The serializable.ab seems to be a back-up created through an Android OS. In order to get the content of the file we can execute the following command:

dd if=serializable.ab bs=1 skip=24 | python -c "import zlib,sys;sys.stdout.write(zlib.decompress(sys.stdin.read()))" | tar -xvf -

This will generate the following result:

  • apps/com.mhs.serializado/manifest
  • apps/com.mhs.serializado/a/base.apk
  • apps/com.mhs.serializado/r/key.ser
  • apps/com.mhs.serializado/r/content.txt

In order to check what is each file, we have to open the base.apk file, which is the application backed-up in this file:

jadx-gui apps/com.mhs.serializado/a/base.apk

In this application we have two activitied, SplashActivity.java (which is a splash screen page with no logic), and MainActivity.java which has two buttons btnStore and btnLoad.

We check what the btnStore does by going through the decompiled source code:

  1. get bytes from txtSecretPassword value:
String content = MainActivity.this.txtSecretPassword.getText().toString();
...
byte[] key = content.getBytes(StandardCharsets.UTF_8);
  1. Generate a CipherManagement class:
CipherManagement cipherManagement = new CipherManagement();
int i = 0;
cipherManagement.x1 = key[0];
cipherManagement.x2 = key[1];
cipherManagement.x3 = key[2];
cipherManagement.x4 = key[3];
cipherManagement.x5 = key[4];
cipherManagement.x6 = key[5];
cipherManagement.x7 = key[6];
cipherManagement.x8 = key[7];
cipherManagement.x9 = key[8];
cipherManagement.x10 = key[9];
cipherManagement.x11 = key[10];
cipherManagement.x12 = key[11];
cipherManagement.x13 = key[12];
cipherManagement.x14 = key[13];
cipherManagement.x15 = key[14];
cipherManagement.x16 = key[15];
cipherManagement.generateSecret();
  1. Call serializeObject from class Serializer. This class does, but here we can see that there is a key.ser String, which is the name of a file in the backup found.
Serializer.serializeObject(cipherManagement, "key.ser");
  1. Generates a file called ontent.txt with the value taken from txtContent, encrypted through the CipherManagement class, and then encoded in Base64. That is the second file we have in the backup.
String multiLines = MainActivity.this.txtContent.getText().toString();
try {
    FileOutputStream fOut = new FileOutputStream(new File("/data/data/com.mhs.serializado/content.txt"));
    OutputStreamWriter myOutWriter = new OutputStreamWriter(fOut);
    String[] lines = multiLines.split("\n");
    try {
        int length = lines.length;
        int i2 = 0;
        while (i2 < length) {
            myOutWriter.append(Base64.encodeToString(cipherManagement.encrypt(lines[i2]), i));
            i2++;
            i = 0;
        }
    } catch (Exception e) {
        e.printStackTrace();
    }
    myOutWriter.close();
    fOut.flush();
    fOut.close();
} catch (IOException e2) {
    Log.e("Exception", "File write failed: " + e2.toString());
}

In the case of the btnLoad, the application does the following:

  1. Recover the key from the key.ser file:
CipherManagement cipherManagement = (CipherManagement) Serializer.deserializeObject("key.ser");
if (cipherManagement == null) {
    Toast.makeText(MainActivity.this.getApplicationContext(), "Nothing encrypted yet", 1).show();
    return;
}
  1. Sets the keyin the txtSecretPassword field:
byte[] key = {cipherManagement.x1, cipherManagement.x2, cipherManagement.x3, cipherManagement.x4, cipherManagement.x5, cipherManagement.x6, cipherManagement.x7, cipherManagement.x8, cipherManagement.x9, cipherManagement.x10, cipherManagement.x11, cipherManagement.x12, cipherManagement.x13, cipherManagement.x14, cipherManagement.x15, cipherManagement.x16};
cipherManagement.generateSecret();
MainActivity.this.txtSecretPassword.setText(new String(key, StandardCharsets.UTF_8));
  1. Gets the content of the content.txt file, and tries to decode it. First it decodes the content of the fiñe with a Base64 decoder, then the application tries to decrypt the decoded text with the CipherManagement class, and the result is being set in the txtContext field.
new InputStreamReader(new FileInputStream(new File("/data/data/com.mhs.serializado/content.txt")));
StringBuilder resultingContent = new StringBuilder();
if (Build.VERSION.SDK_INT >= 26) {
    for (String line : Files.readAllLines(Paths.get("/data/data/com.mhs.serializado/content.txt", new String[0]), Charset.defaultCharset())) {
        String decodedLine = cipherManagement.decrypt(Base64.decode(line, 0));
        resultingContent.append(decodedLine + "\n");
    }
}
MainActivity.this.txtContent.setText(resultingContent.toString());

The Serializer stores and loads a Java serialized object in the root package of the application. In this case the object being stored and loaded is a ChiperManagement instance, which holds the key.

After the analysis of the source code, we need to install the application and upload the backup.

So we first try to install the apk:

adb install apps/com.mhs.serializado/a/base.apk 

which returns the following output:

Performing Streamed Install
adb: failed to install apps/com.mhs.serializado/a/base.apk: Failure [INSTALL_FAILED_TEST_ONLY: installPackageLI]

this error is due to a flag of test in the manifest. We need to add a -t option in the adb install command to avoid this error:

adb install -t apps/com.mhs.serializado/a/base.apk

Now we need to restore the backup. As this is trivial in this case I will do it manually:

adb shell rm /data/data/com.mhs.serializado/content.txt
adb shell rm /data/data/com.mhs.serializado/key.ser

adb push content.txt /data/data/com.mhs.serializado
adb push key.ser /data/data/com.mhs.serializado

Now we need to change the user rights on the files. So first you need to know the name of the user assigned by Android to the application, which will depend on the cellphone, so execute:

adb shell ls -al /data/data/com.mhs.serializado 

where you will get the a content similar to the following one:

drwx------   4 u0_a118 u0_a118        4096 2020-11-28 17:00 .
drwxrwx--x 169 system  system        12288 2020-11-09 12:08 ..
drwxrws--x   2 u0_a118 u0_a118_cache  4096 2020-08-18 03:10 cache
drwxrws--x   2 u0_a118 u0_a118_cache  4096 2020-08-18 03:10 code_cache

In this case the user is u0_118, so we execute:

adb shell chown u0_a118:u0_a118 /data/data/com.mhs.serializado/content.txt
adb shell chown u0_a118:u0_a118 /data/data/com.mhs.serializado/key.ser

When we try to restore the content we get an error in the log:

System.out  I  IOException is caught

We have no information about what is going on, so let’s check the content of the key file. As it is a Java-serialized object, we can use a tool called SerializationDumper (add link here).

java -jar SerializationDumper-v1.13.jar -f key.ser 

STREAM_MAGIC - 0xac ed
STREAM_VERSION - 0x00 05
Contents
  TC_OBJECT - 0x73
    TC_CLASSDESC - 0x72
      className
        Length - 36 - 0x00 24
        Value - com.mhs.serializado.CipherManagement - 0x636f6d2e6d68732e73657269616c697a61646f2e4369706865724d616e6167656d656e74
      serialVersionUID - 0xb2 8a 53 35 20 34 41 18
      newHandle 0x00 7e 00 00
      classDescFlags - 0x02 - SC_SERIALIZABLE
      fieldCount - 16 - 0x00 10
      Fields
        0:
          Byte - B - 0x42
          fieldName
            Length - 2 - 0x00 02
            Value - x1 - 0x7831
        1:
          Byte - B - 0x42
          ...
        15:
          Byte - B - 0x42
          fieldName
            Length - 2 - 0x00 02
            Value - x9 - 0x7839
      classAnnotations
        TC_ENDBLOCKDATA - 0x78
      superClassDesc
        TC_NULL - 0x70
    newHandle 0x00 7e 00 01
    classdata
      com.mhs.serializado.CipherManagement
        values
          x1
            (byte)97 (ASCII: a) - 0x61
          x10
            ...
          x6
            (byte)57 (ASCII: 9) - 0x39
          x7
Exception in thread "main" java.util.NoSuchElementException
	at java.util.LinkedList.removeFirst(LinkedList.java:270)
	at java.util.LinkedList.pop(LinkedList.java:801)
	at nb.deser.SerializationDumper.readByteField(SerializationDumper.java:1369)
	at nb.deser.SerializationDumper.readFieldValue(SerializationDumper.java:953)
	at nb.deser.SerializationDumper.readClassDataField(SerializationDumper.java:939)
	at nb.deser.SerializationDumper.readClassData(SerializationDumper.java:886)
	at nb.deser.SerializationDumper.readNewObject(SerializationDumper.java:467)
	at nb.deser.SerializationDumper.readContentElement(SerializationDumper.java:359)
	at nb.deser.SerializationDumper.parseStream(SerializationDumper.java:331)
	at nb.deser.SerializationDumper.main(SerializationDumper.java:113)

Through the error we get that the serialized file seems to be corrupted. There seems to be three values missing. As we can see in the definition of the class:

public class CipherManagement implements Serializable {
    private transient byte[] inferedKey = new byte[16];
    public byte x1;
    public byte x10;
    public byte x11;
    public byte x12;
    public byte x13;
    public byte x14;
    public byte x15;
    public byte x16;
    public byte x2;
    public byte x3;
    public byte x4;
    public byte x5;
    public byte x6;
    public byte x7;
    public byte x8;
    public byte x9; 

Now we can assume the following:

  • The known values are ordered.
  • We do not know which values are missing.
  • We’ll try to test this with alphanumeric (a-zA-Z0-9) and if that does not work we’ll test it with printable characters.

I built the following java application in order to test this, and make the process faster than using Frida:

public static void main(String[] args) {
	// TODO Auto-generated method stub
	String content = "jBUwXJdjVaHvZuYfaw/cew==";
	byte array[] = Base64.getDecoder().decode(content);

	CipherManagement cipher = new CipherManagement();

	cipher.generateSecret();
	CharsetEncoder encoder = Charset.forName("US-ASCII").newEncoder();

	//starting time 
	printTime();
	// rotating positions
	for (int pos1 = 0; pos1 < 14; pos1++) {
		for (int pos2 = pos1 + 1; pos2 < 15; pos2++) {
			for (int pos3 = pos2 + 1; pos3 < 16; pos3++) {
				
				setStaticValues(cipher, pos1, pos2, pos3);
				//bruteforcing just alphanumeric values to check if it returns results fast.
				for (byte x = 49; x <= 122; x++) {
					setValue(cipher, pos1, x);
					for (byte y = 49; y <= 122; y++) {
						setValue(cipher, pos2, y);
						for (byte z = 49; z <= 122; z++) {
							setValue(cipher, pos3, z);
							cipher.generateSecret();
							String decrypted;
							try {
								decrypted = cipher.decrypt(array);
								//this filters the solutions sent to output, but still there are false positives.
								if (encoder.canEncode(decrypted)) {
									System.out.println(decrypted);
									//this loop is just to measure what time it takes to get the key
									if ("banana_loca".equals(decrypted)) {
										printTime();
										return;
									}
								}
								
							} catch (Exception e) {
								// TODO Auto-generated catch block
							}
						}
					}
				}
			}
		}
	}
}

the full source code is in the following repository: solving-serializado

With this algorithm I found the key in 13,5 minutes, and decrypted the content of the text that is “banana_loca”.