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.
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:
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:
String content = MainActivity.this.txtSecretPassword.getText().toString();
...
byte[] key = content.getBytes(StandardCharsets.UTF_8);
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();
Serializer.serializeObject(cipherManagement, "key.ser");
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:
CipherManagement cipherManagement = (CipherManagement) Serializer.deserializeObject("key.ser");
if (cipherManagement == null) {
Toast.makeText(MainActivity.this.getApplicationContext(), "Nothing encrypted yet", 1).show();
return;
}
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));
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:
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”.