Avec l’équipe Tilde (ou ‘~‘), j’ai participé à l’édition 2018 du BreizhCTF. Voici les write-ups de quelques uns des challenges sur lesquels j’ai travaillé. Sont concernés Trace Me, BabyAPK, Cryptonik et Desprecito. Vous pouvez retrouver les write-ups d’autres membres de tilde sur :
Trace Me
Ce challenge consiste en un fichier traceme.py qui lit une ligne sur l’entrée standard, l’encode et vérifie qu’il s’agit du flag. Nous disposons aussi de la fonction d’encodage, ce qui permet de renverser le processus et de retrouver le flag.
Le reste s’est fait avec un papier et un crayon pour comprendre l’encodage et écrire un programme qui décode le flag.
def _encoder(flag):
corresp = ['A', 'B', 'C', 'D','E','F','G','H','I','J','K','L','M','N','O','P','Q','R','S','T','U','V','W','X','Y','Z','a','b','c','d','e','f','g','h','i','j','k','l','m','n','o','p','q','r','s','t','u','v','w','x','y','z','0','1','2','3','4','5','6','7','8','9','+','/']
out = ""
for i in [i for i in xrange(len(flag)) if not(i%3)]:
print i
if i + 3 > len(flag):
out = None
break
out += corresp[(ord(flag[i]) >> 2) & 0x3F]
out += corresp[(((ord(flag[i]) << 6 ) >> 2) & 0x30) | ((ord(flag[i + 1]) >> 4) & 0x0F)]
out += corresp[(((ord(flag[i + 1]) << 4) >> 2) & 0x3C) | ((ord(flag[i + 2]) >> 6) & 0x03)]
out += corresp[ord(flag[i + 2]) & 0x3F]
print out
return out
if __name__ == '__main__':
while True:
flag = raw_input()
if _encoder(flag) == 'aXRJc1NvbWV0aW1lc1ZlcnlFYXN5VG9EZWZlYXRUaGlzS2luZE9mT2JmdXNjYXRpb24h':
print 'BZHCTF{' + flag + '}'
else:
print "Try again!"
Globalement, en faisant les opérations à la main, on s’aperçoit que la fonction d’encodage marche par blocs : Elle découpe la chaîne de caractère initiale en blocs de trois octets et les réarrange pour en sortir des blocs de quatre octets. Elle passe enfin chaque octet résultant dans un tableau de correspondance, ce qui donne le résultat.
Pour chaque bloc de 3 octets, voici l’opération qui est effectuée au niveau binaire :
Bloc initial :
1er octet 2e octet 3e octet
b07|b06|b05|b04|b03|b02|b01|b00 b17|b16|b15|b14|b13|b12|b11|b10 b27|b26|b25|b24|b23|b22|b21|b20
Nouveau bloc :
1er octet 2e octet 3e octet 4e octet
0|0|b07|b06|b05|b04|b03|b02 0|0|b01|b00|b17|b16|b15|b14 0|0|b13|b12|b11|b10|b27|b26 0|0|b25|b24|b23|b22|b21|b20
Chaque octet obtenu est finalement passé par le tableau de correspondance
Inverser ce processus est facile, il suffit de construire un tableau de correspondance inverse et de réarranger les bits pour retrouver le flag. Le code Go qui résout ce challenge est disponible ici.
package main
import (
"fmt"
)
func main() {
corresp := []byte{'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z', 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '+', '/'}
encoded := []byte("aXRJc1NvbWV0aW1lc1ZlcnlFYXN5VG9EZWZlYXRUaGlzS2luZE9mT2JmdXNjYXRpb24h")
// Build the reverse table
reverseTable := make([]byte, 256)
for i, val := range corresp {
reverseTable[val] = byte(i)
}
// Reverse the effects of the correspondance table
reversedCheck := make([]byte, len(encoded))
for i, val := range encoded {
reversedCheck[i] = reverseTable[val]
}
// Iterate over 4 bytes
out := make([]byte, 0)
for i := 0; i < len(reversedCheck); i = i + 4 {
byte0 := ((reversedCheck[i] << 2) & 0xFC) | ((reversedCheck[i+1] >> 4) & 0x03)
byte1 := ((reversedCheck[i+1] << 4) & 0xF0) | ((reversedCheck[i+2] >> 2) & 0x0F)
byte2 := ((reversedCheck[i+2] << 6) & 0xC0) | ((reversedCheck[i+3]) & 0x3F)
out = append(out, byte0, byte1, byte2)
}
fmt.Printf("Le flag est : BZHCTF{%s}\n", out)
}
La méthode papier crayon est celle qui a été utilisée lors de l’événement. Cependant, nous avons essentiellement passé du temps à comprendre ce qui n’était qu’un encodage simplifié en base64 (sans le padding). Il était possible de résoudre ce challenge avec le one-liner suivant :
echo "BZHCTF{$(echo 'aXRJc1NvbWV0aW1lc1ZlcnlFYXN5VG9EZWZlYXRUaGlzS2luZE9mT2JmdXNjYXRpb24h' | base64 -d)}"
Le flag est : BZHCTF{itIsSometimesVeryEasyToDefeatThisKindOfObfuscation!}
.
BabyAPK
Dans ce challenge, nous avons le droit à un fichier babyapk.apk avec un flag caché dedans. Pour le trouver, les programmes dex2jar et jd-gui ont été utilisé :
- dex2jar prend en entrée un APK pour en sortir un .jar;
- jd-gui prend en entrée un .jar et décompile le bytecode Java pour l’afficher.
Dans cette application, un package fr.breizhctf.saxx.babyapk semble nous indiquer où il faut chercher. La classe LoginActivity contient quelques identifiants de test (foo@exemple.com:hello et bar@example.com:world), mais surtout une méthode isPasswordValid qui semble contenir le flag recherché. La sortie de jd-gui est un peu plus verbeuse, mais voici globalement le code de cette fonction :
private boolean isPasswordValid(String paramString) {
boolean check = true;
if (paramString.length() == 45) {
for (int j = 0; j < paramString.length(); j++) {
if (")79$#!&#^l\\t<v\\x00Q\\x17\\x11HOXyD2k:!\\x18\\x040@xy\\x089g0\\x01_\\t\\x1c#oGF^".charAt(j) != ("kmqgwg]Tm3=NE_#$%$#!&#^_^~/4ouKJW@WE^(:p@_*##".charAt(j) ^ paramString.charAt(j))) {
check = false;
break;
}
}
} else {
check = false;
}
if (check) {
Toast.makeText(this, "Seems I don't recognize you! go out :(", 0).show();
} else {
Toast.makeText(this, "Hey buddy! It's you, Welcome :)", 0).show();
}
return check;
}
Au final, la vérification du mot de passe effectue juste un XOR entre le mot de passe et une chaîne de caractères, et vérifie que le résultat est correct en le comparant à une autre chaîne de caractères. Il suffit de XORer les deux chaînes entre elles pour faire sortir le mot de passe attendu (BabyApkSolver.go):
package main
import "fmt"
func main() {
str1 := []byte(")79$#!&#^l\t<v\x00Q\x17\x11HOXyD2k:!\x18\x040@xy\x089g0\x01_\t\x1c#oGF^")
str2 := []byte("kmqgwg]Tm3=NE_#$%$#!&#^_^~/4ouKJW@WE^(:p@_*##")
pass := make([]byte, len(str1))
for i := 0; i < len(str1); i++ {
pass[i] = str1[i] ^ str2[i]
}
fmt.Printf("Le mot de passe est : %s\n", pass)
}
On récupère le flag : BZHCTF{w3_4r3_r34lly_gl4d_70_533_y0u_w3lc0me}
.
Cryptonik
Cette fois-ci, on récupère l’application cryptonik.apk. Une fois transformée en jar et décompilé, on retrouve un package fr.breizhctf.saxx.jetepromets. Là on y trouve la classe Cryptonik, qui est décompilée de la manière suivante :
package fr.breizhctf.saxx.jetepromets;
import java.io.FileOutputStream;
import java.io.ObjectOutputStream;
import java.io.PrintStream;
import java.io.Serializable;
import java.util.Random;
public class Cryptonik {
public static byte[] cryptonik_gen() {
Random localRandom = new Random();
byte[] arrayOfByte = new byte[30];
localRandom.nextBytes(arrayOfByte);
return arrayOfByte;
}
public static byte[] encrypt(byte[] paramArrayOfByte1, byte[] paramArrayOfByte2) {
byte[] arrayOfByte = new byte[paramArrayOfByte1.length];
for (int i = 0; i < paramArrayOfByte1.length; i++)
arrayOfByte[i] = ((byte)(paramArrayOfByte1[i] ^ paramArrayOfByte2[(i % paramArrayOfByte2.length)]));
return arrayOfByte;
}
public static void main(String[] paramArrayOfString) throws Exception {
paramArrayOfString = cryptonik_gen();
int i = "Je te promets mes bras pour porter tes angoisses".length();
if ("BZHCTF{TEUTEUTEU_TEUTEUTEU_TEUTEUTEU_TEUTEUTEU_}".length() != i)
System.out.println("len(text) must be " + i + "!");
while (true) {
return;
paramArrayOfString = new Data(encrypt("Je te promets mes bras pour porter tes angoisses".getBytes(), paramArrayOfString), encrypt("BZHCTF{TEUTEUTEU_TEUTEUTEU_TEUTEUTEU_TEUTEUTEU_}".getBytes(), paramArrayOfString));
ObjectOutputStream localObjectOutputStream = new ObjectOutputStream(new FileOutputStream("enc.bin"));
localObjectOutputStream.writeObject(paramArrayOfString);
localObjectOutputStream.close();
}
}
static class Data implements Serializable {
private final byte[] data1;
private final byte[] data2;
public Data(byte[] paramArrayOfByte1, byte[] paramArrayOfByte2) {
this.data1 = paramArrayOfByte1;
this.data2 = paramArrayOfByte2;
}
public byte[] getData1() { return this.data1; }
public byte[] getData2() { return this.data2; }
}
}
On retrouve un chiffrement utilisant le XOR avec une clé générée aléatoirement. Cette application génère une clé, chiffre deux chaînes de caractères et les utilise dans un objet Data. Cet objet est ensuite sérialisé en un fichier appelé enc.bin. Si on décompresse l’apk à la main (avec unzip), on trouve ce fichier dans assets/enc.bin. (Note, ce challenge a été fix plusieurs fois, le fichier enc.bin initialement dans l’apk n’est pas le bon. Le fichier corrigé est celui-ci.
Il faut d’abord le désérialiser, puis on pourra récupérer le flag en jouant avec les informations à dispositions. Pour déserialiser ce fichier, on peut simplement réimplémenter la classe Data et lui demander de lire le fichier enc.bin. Java requiert que chaque classe sérialisable possède un champ serialVersionUID qu’il utilise pour reconnaître le type d’objet. En son absence, il en choisit un au hasard selon des règles à lui. En l’occurence, la première fois que l’on essaye de lire enc.bin, il va raler parce que l’UID généré pour la classe Data locale sera différent de celui présent dans le fichier. Il est aussi suffisemment gentil pour indiquer l’UID qui est attendu, ce qui permet de le remplacer par la bonne valeur. Au vu des Strings présentes, on se doute que la chaîne BZHCTF{.....}
a été modifiée afin de ne pas donner le flag tout de suite. L’hypothèse est que le flag a été XORé avec la clé, et mis dans Data.data2. On essaye donc de récupérer cette valeur.
On connait les paramètres :
str1 = "Je te promets mes bras pour porter tes angoisses"
Data.data1 = key ^ str1
Data.data2 = key ^ flag
On peut faire :
key = Data.data1 ^ str1
flag = Data.data2 ^ key
Le code Java associé, qui désérialise ce fichier et retrouve le flag est (Cryptonik.java) :
import java.io.*;
public class Cryptonik {
public static void main(String[] args) throws Exception {
FileInputStream streamIn = new FileInputStream("cryptonik_enc.bin");
ObjectInputStream objectinputstream = new ObjectInputStream(streamIn);
Data data = null;
data = (Data) objectinputstream.readObject();
byte[] cipher1 = new byte[30];
for (int i = 0; i < 30; i++) {
cipher1[i] = data.data1[i];
}
byte[] key = encrypt(cipher1, "Je te promets mes bras pour porter tes angoisses".getBytes());
byte[] flag = encrypt(data.data2, key);
System.out.println(new String(flag));
}
public static byte[] encrypt(byte[] paramArrayOfByte1, byte[] paramArrayOfByte2) {
byte[] arrayOfByte = new byte[paramArrayOfByte1.length];
for (int i = 0; i < paramArrayOfByte1.length; i++)
arrayOfByte[i] = ((byte)(paramArrayOfByte1[i] ^ paramArrayOfByte2[(i % paramArrayOfByte2.length)]));
return arrayOfByte;
}
class Data
implements Serializable {
public static final long serialVersionUID = 8528270223441554948L;
private final byte[] data1;
private final byte[] data2;
public Data(byte[] paramArrayOfByte1, byte[] paramArrayOfByte2) {
this.data1 = paramArrayOfByte1;
this.data2 = paramArrayOfByte2;
}
public byte[] getData1() { return this.data1; }
public byte[] getData2() { return this.data2; }
}
}
On retrouve le flag BZHCTF{Wat_do_u_dont_understand_in_0n3_71m3_p4d}
.
Desprecitor
Le dernier challenge mobile nous donne le fichier Desprecito.apk. Encore une fois, on le transforme puis on le décompile. On retrouve le package fr.breizhctf.saxx.desprecito qui contient la classe Desprecitor :
package fr.breizhctf.saxx.desprecito;
import java.io.FileOutputStream;
import java.io.ObjectOutputStream;
import java.io.PrintStream;
import java.security.Key;
import javax.crypto.Cipher;
import javax.crypto.KeyGenerator;
import javax.crypto.SealedObject;
import javax.crypto.SecretKey;
public class Desprecitor
{
public static void main(String[] paramArrayOfString)
throws Exception
{
if (paramArrayOfString.length != 4)
System.out.println("Desprecitor <<algorithm>> <<cipher mode>> <<flag>> <<output_file>>");
while (true)
{
return;
Object localObject1 = KeyGenerator.getInstance(paramArrayOfString[0]).generateKey();
Object localObject2 = Cipher.getInstance(paramArrayOfString[1]);
((Cipher)localObject2).init(1, (Key)localObject1);
localObject1 = new Data(new SealedObject(new Flag(paramArrayOfString[2]), (Cipher)localObject2), (SecretKey)localObject1);
localObject2 = new ObjectOutputStream(new FileOutputStream(paramArrayOfString[3]));
((ObjectOutputStream)localObject2).writeObject(localObject1);
((ObjectOutputStream)localObject2).close();
System.out.println("File created: " + paramArrayOfString[3]);
}
}
}
La classe Flag ne contient qu’un paramètre de type String, tandis que la classe Data contient un SealedObject et une Key. Le flag est stocké chiffré à l’intérieur du SealedObject, et le tout est sérialisé en un fichier enc.bin. Comme pour le challenge précédent, on peut écrire une classe Data et une classe Flag et désérialiser les données. Enfin, comme on a la clé secrète qui a servi à chiffrer, on peut aisément déchiffrer le flag. Le code qui effectue ces opérations est le suivant (DesprecitoSolver.java, nécessite les classes Data.java et Flag.java à côté) :
import java.io.*;
import javax.crypto.*;
public class DesprecitoSolver {
public static void main(String[] args) throws Exception {
FileInputStream streamIn = new FileInputStream("desprecito_enc.bin");
ObjectInputStream objectinputstream = new ObjectInputStream(streamIn);
Data data = null;
data = (Data) objectinputstream.readObject();
Flag flag = (Flag) data.getSealed().getObject(data.getKey());
System.out.println(flag.getFlag());
}
}
On trouve le flag : BZHCTF{#Desprecito_Quiero_respirar_tu_DES_CIPHER_despacito#}
.
Conclusion
Je me suis beaucoup amusé au BreizhCTF au final et j’y retrournerai avec grand plaisir. Ou alors, nous ferons un autre CTF avec l’équipe tilde avant cela, ce n’est pas exclu.