THC 2021 - Write-Up : Mission Impossible
Ce post fait parti d’une série de write-ups faisant suite au CTF de la Toulouse Hacking Convention auquel j’ai eu la chance d’avoir participé en équipe avec \@0x_Seb.
Je souhaite remercier également le créateur du challenge, cryptax. Nous avons pris beaucoup de plaisir sur ce challenge qui nous a motivé à écrire notre premier writup. Si vous souhaitez réaliser le challenge avant ou pendant la lecture de l’article, vous pouvez télécharger l’APK ici: mission-impossible.apk

Le 4ème challenge de la catégorie reverse est un challenge Android. L’énoncé ne donne pas beaucoup d’indices sur l’emplacement du flag, nous allons donc tout simplement commencer par exécuter l’application après l’avoir téléchargée.
> curl -O https://challenges.thcon.party/reverse-axelleapvrille-mission-impossible/mission-impossible.apk
% Total % Received % Xferd Average Speed Time Time Time Current
Dload Upload Total Spent Left Speed
100 5696k 100 5696k 0 0 7336k 0 --:--:-- --:--:-- --:--:-- 7331k
Nous avons un smartphone Android sous la main et le plus simple lorsque l’on est
sous Linux est d’utiliser la commande ADB (Android Debug Bridge) fournie par la majorité des
gestionnaires de packets. (L’option -t est nécessaire car le package est en
testOnly):
> adb -t install mission-impossible.apk
Performing Streamed Install
Success
L’application est maintenant installée sur le téléphone. Elle affiche simplement l’image d’une cassette audio et trois boutons qui nous permettent de contrôler la lecture d’une piste audio: le thème de mission impossible.

Nous savons maintenant que l’APK embarque très probablement une piste audio stockée localement, mais aucune autre information ne semble intéressante pour le moment. Le travail de rétro-conception va pouvoir commencer.
Le format APK n’est qu’une archive qui contient du bytecode compatible avec la
machine virtuelle d’Android. C’est une sorte de langage intermédiaire qui va être
interprété dynamiquement pour générer du véritable code machine. Une fois extrait, ce
bytecode à la particularité d’être très facilement décompilable en un ensemble
de fichiers source très proche de ceux écrits par les développeurs. Nous
pourrions utiliser la commande unzip puis un décompileur sur chaque fichier
et chercher les bons arguments pour obtenir le code java d’origine. Heureusement
pour nous, le projet open source jadx
automatise tout ce processus en analysant le fichier AndroidManifest.xml
contenu dans l’APK.
> jadx mission-impossible.apk
INFO - loading ...
INFO - processing ...
INFO - done
Le résultat est un dossier mission-impossible contenant la structure d’un
projet Android entièrement recompilable.
> tree -L 2 mission-impossible
mission-impossible
├── resources
│ ├── AndroidManifest.xml
│ ├── assets
│ ├── classes2.dex
│ ├── classes3.dex
│ ├── classes.dex
│ ├── META-INF
│ └── res
└── sources
├── android
├── androidx
├── com
└── thcon21
9 directories, 4 files
Nous savons que les flags du CTF auront le format THCon21{...}. Le premier réflexe est
alors de chercher le format du flag dans l’arborescence de fichiers :
> grep -r THCon21 mission-impossible/
grep: mission-impossible/resources/assets/MissionImpossibleTheme.mp3: binary file matches
Un seul match dans la totalité du code correspond au format du flag et il se
trouve dans le fichier mp3. Hourra ? La commande strings nous permettra
d’extraire ce qui semble être le flag :
> strings MissionImpossibleTheme.mp3 | grep THCon21
THCon21{DUMMY-SEARCH-MORE}
Malheureusement, la célébration était un peu prématurée. Cependant, le fichier ne semble pas contenir qu’une piste audio. Listons un peu le texte qui se trouve autour de notre pseudo-flag.
> strings MissionImpossibleTheme.mp3 | grep -A 10 -B 10 THCon21
(Ljavax/crypto/IllegalBlockSizeException;
%Ljavax/crypto/NoSuchPaddingException;
$Ljavax/crypto/spec/GCMParameterSpec;
!Ljavax/crypto/spec/SecretKeySpec;
Lthcon21/ctf/payload/MIRead;
Lthcon21/ctf/payload/smalldex;
MIRead.java
MMcjCaXX2AAY20H
MissionImpossible
R3JlZXR6RnJvbUNyeXB0YXgK
THCon21{DUMMY-SEARCH-MORE}
UTF-8
VEhDb24yMQo=
VILL
[Ljava/lang/String;
append
args
cipher
ciphertext
d0_you_acc3pt_it
decode
Les chaînes parlent de Java, de cryptographie et de ciphertext. Il semble donc
que l’on ait du code compilé dans le fichier mp3. Malheureusement, l’outil
binwalk ne détecte aucune signature spécifique sur le fichier :
> binwalk MissionImpossibleTheme.mp3
DECIMAL HEXADECIMAL DESCRIPTION
--------------------------------------------------------------------------------
Il va donc falloir y aller à la main pour extraire ce code. On ouvre le fichier mp3 avec Vim et on entre la
commande :%!xxd pour l’éditer au format hexadécimal. On se rend rapidement
compte que la piste contient bien du bytecode avec une signature qui commence
par .dex, le tout encadré par des nullbytes :

On note donc les octets de début et de fin de la séquence :
0x0032d770 => 3331952
0x0032e580 => 3335552
La taille de la zone qui nous concerne est de 3335552 - 3331952 = 3600 octets. La commande
dd va nous permettre d’extraire cette partie du binaire :
> dd bs=1 skip=3331952 count=3600 if=MissionImpossibleTheme.mp3 of=out.bin
3600+0 records in
3600+0 records out
3600 bytes (3.6 kB, 3.5 KiB) copied, 0.0297562 s, 121 kB/s
La commande file va nous permettre de savoir à quel type de fichier nous avons à faire :
> file out.bin
out.bin: Dalvik dex file version 035
Nous voilà donc face à un fichier “Dalvik”. C’est une forme de bytecode Java que
l’on retrouve au sein des APK. Après un peu de recherches, nous avons découvert
l’outil dexdump qui permet d’extraire des informations sur la structure du
fichier dex.
> dexdump out.bin
Processing 'out.bin'...
dexdump E 06-19 13:36:32 1634 1634 dexdump.cc:1884] Failure to verify dex file 'out.bin': Bad file size (3600, expected 3616)
Le format Dalvik supporte une forme de contrôle d’intégrité qui permet à dexdump
de nous indiquer que 16 octets sont manquants au fichier. Il nous suffit
simplement de réutiliser dd en mettant à jour nos options pour extraire le code avec la
partie manquante cette fois-ci.
> dd bs=1 skip=3331952 count=3616 if=MissionImpossibleTheme.mp3 of=out.dex
3616+0 records in
3616+0 records out
3616 bytes (3.6 kB, 3.5 KiB) copied, 0.0103355 s, 350 kB/s
Maintenant, dexdump est en mesure de lire le fichier en entier et nous donne
les informations suivantes :
> dexdump out.dex
Processing 'out.dex'...
Opened 'out.dex', DEX version '035'
Class #0 -
Class descriptor : 'Lthcon21/ctf/payload/MIRead;'
Access flags : 0x0001 (PUBLIC)
Superclass : 'Ljava/lang/Object;'
Interfaces -
Static fields -
#0 : (in Lthcon21/ctf/payload/MIRead;)
name : 'CIPHER_ALGO'
type : 'Ljava/lang/String;'
access : 0x001a (PRIVATE STATIC FINAL)
value : "AES/GCM/NoPadding" # (1)
#1 : (in Lthcon21/ctf/payload/MIRead;)
name : 'IV'
type : 'Ljava/lang/String;'
access : 0x001a (PRIVATE STATIC FINAL)
value : "your_m1ssi0n" # (2)
#2 : (in Lthcon21/ctf/payload/MIRead;)
name : 'KEY'
type : 'Ljava/lang/String;'
access : 0x001a (PRIVATE STATIC FINAL)
value : "d0_you_acc3pt_it" # (3)
Instance fields -
#0 : (in Lthcon21/ctf/payload/MIRead;)
name : 'cipher' # (4)
type : 'Ljavax/crypto/Cipher;'
access : 0x0002 (PRIVATE)
#1 : (in Lthcon21/ctf/payload/MIRead;)
name : 'parameterSpec'
type : 'Ljavax/crypto/spec/GCMParameterSpec;'
access : 0x0002 (PRIVATE)
#2 : (in Lthcon21/ctf/payload/MIRead;)
name : 'secretKeySpec'
type : 'Ljavax/crypto/spec/SecretKeySpec;'
access : 0x0002 (PRIVATE)
Direct methods -
#0 : (in Lthcon21/ctf/payload/MIRead;)
name : '<init>'
type : '()V'
access : 0x10001 (PUBLIC CONSTRUCTOR)
code -
registers : 5
ins : 1
outs : 3
insns size : 44 16-bit code units
catches : (none)
positions :
0x0000 line=26
0x0003 line=27
0x0014 line=28
0x001c line=29
0x002b line=30
locals :
0x0000 - 0x002c reg=4 this Lthcon21/ctf/payload/MIRead;
Virtual methods -
#0 : (in Lthcon21/ctf/payload/MIRead;)
name : 'decrypt' # (5)
type : '(Ljava/lang/String;)Ljava/lang/String;'
access : 0x0001 (PUBLIC)
code -
registers : 7
ins : 2
outs : 4
insns size : 33 16-bit code units
catches : (none)
positions :
0x0000 line=39
0x000b line=40
0x0015 line=41
0x001b line=42
locals :
0x000b - 0x0021 reg=0 valueDecoded [B
0x001b - 0x0021 reg=1 plaintext [B
0x0000 - 0x0021 reg=5 this Lthcon21/ctf/payload/MIRead;
0x0000 - 0x0021 reg=6 ciphertext Ljava/lang/String;
#1 : (in Lthcon21/ctf/payload/MIRead;)
name : 'encrypt' # (6)
type : '(Ljava/lang/String;)Ljava/lang/String;'
access : 0x0001 (PUBLIC)
code -
registers : 6
ins : 2
outs : 4
insns size : 33 16-bit code units
catches : (none)
positions :
0x0000 line=32
0x000a line=33
0x0016 line=34
locals :
0x0016 - 0x0021 reg=0 encryptedBytes [B
0x0000 - 0x0021 reg=4 this Lthcon21/ctf/payload/MIRead;
0x0000 - 0x0021 reg=5 plaintext Ljava/lang/String;
source_file_idx : 36 (MIRead.java)
Class #1 -
Class descriptor : 'Lthcon21/ctf/payload/smalldex;'
Access flags : 0x0001 (PUBLIC)
Superclass : 'Ljava/lang/Object;'
Interfaces -
Static fields -
Instance fields -
Direct methods -
#0 : (in Lthcon21/ctf/payload/smalldex;)
name : 'main'
type : '([Ljava/lang/String;)V'
access : 0x0009 (PUBLIC STATIC)
code -
registers : 8
ins : 1
outs : 2
insns size : 58 16-bit code units
catches : (none)
positions :
locals :
0x0000 - 0x003a reg=7 args [Ljava/lang/String;
#1 : (in Lthcon21/ctf/payload/smalldex;)
name : 'testFlag'
type : '()V'
access : 0x0009 (PUBLIC STATIC)
code -
registers : 7
ins : 0
outs : 2
insns size : 81 16-bit code units
catches : 1
0x0005 - 0x0038
Ljavax/crypto/NoSuchPaddingException; -> 0x004b
Ljava/security/NoSuchAlgorithmException; -> 0x0046
Ljava/security/InvalidAlgorithmParameterException; -> 0x0044
Ljava/security/InvalidKeyException; -> 0x0042
Ljava/io/UnsupportedEncodingException; -> 0x003d
Ljavax/crypto/BadPaddingException; -> 0x003b
Ljavax/crypto/IllegalBlockSizeException; -> 0x0039
positions :
locals :
0x000f - 0x0039 reg=3 encrypted Ljava/lang/String;
0x0013 - 0x0039 reg=4 decrypted Ljava/lang/String;
0x003e - 0x0042 reg=0 e Ljava/lang/Exception;
0x0047 - 0x004a reg=0 e Ljava/security/GeneralSecurityException;
0x004c - 0x004f reg=0 e Ljavax/crypto/NoSuchPaddingException;
0x0004 - 0x0051 reg=1 dummyFlag Ljava/lang/String;
0x0005 - 0x0051 reg=2 mission Lthcon21/ctf/payload/MIRead;
Virtual methods -
source_file_idx : -1 (unknown)
La sortie de dexdump est très riche en informations sur les classes Java
d’origine du binaire. Nous sommes bien en présence d’un code manipulant de la
cryptographie et beaucoup d’informations utiles sont alors accessibles :
- CIPHER_ALGO: AES/GCM/NoPadding (
1) - IV: your_m1ssi0n (
2) - KEY: d0_you_acc3pt_it (
3)
Nous sommes en possession de la clef et du vecteur d’initialisation d’AES. Nous
sommes donc en mesure de déchiffrer toutes données que cet algorithme aurait
traité. Malheureusement, nous n’avons pour l’instant pas de donnée à déchiffrer
et le champ cipher (4) de la classe n’est pas accessible par dexdump.
Pour aller plus loin nous allons devoir utiliser à nouveau l’outil jadx pour
retrouver le code Java à partir de notre fichier Dalvik.
> jadx out.dex
INFO - loading ...
INFO - processing ...
INFO - done
Jadx génère une arborescence et fait apparaître les deux classes que dexdump avait déjà détecté.
> tree .
.
├── out
│ └── sources
│ └── thcon21
│ └── ctf
│ └── payload
│ ├── MIRead.java
│ └── smalldex.java
└── out.dex
La fonction main de smalldex.java est très intéressante. Elle utilise des
techniques d’obfuscation pour construire une chaîne de caractères qui semble
être encodée en base64 sans laisser fuiter de façon évidente la donnée dans le
binaire.
> cat out/sources/thcon21/ctf/payload/smalldex.java | grep -A 18 main
public static void main(String[] args) {
testFlag();
String str = args[0];
do {
} while (0 != 0);
StringBuilder sb = new StringBuilder();
sb.append("IkUegPuai+gfBce7nTf");
if ("IkUegPuai+gfBce7nTf" != "VEhDb24yMQo=") {
sb.append("CkMZzZSwne3X3mnyrc5oBcD2yGHUXy");
sb.append("MMcjCaXX2AAY20H");
String sb2 = sb.toString();
if (str.equals("MissionImpossible")) {
System.out.println(sb2);
return;
}
return;
}
sb.append("MissionImpossible");
}
Sans prendre beaucoup de risque, nous pouvons partir du principe que le ciphertext est notre chaîne résultante :
IkUegPuai+gfBce7nTfCkMZzZSwne3X3mnyrc5oBcD2yGHUXyMMcjCaXX2AAY20H
Il ne reste plus qu’à déchiffrer ce ciphertext avec les informations que l’on a récupéré sur le reste du code. Pour cela, il y a deux méthodes.
Fin alternative 1
On utilise les informations récupérées jusqu’ici pour déchiffrer le ciphertext à l’aide de python :
from Crypto.Cipher import AES
from base64 import b64decode
key = b'd0_you_acc3pt_it'
iv = b'your_m1ssi0n'
ciphertext = b64decode('IkUegPuai+gfBce7nTfCkMZzZSwne3X3mnyrc5oBcD2yGHUXyMMcjCaXX2AAY20H')
print(AES.new(key, AES.MODE_GCM, iv).decrypt(ciphertext))
b'THCon21{Th1s-Was-Poss1ble-For-U}\x8c\x0c\xdab\xbc\x92\x13V\xee5m\xa0\xfeE}c'
Fin alternative 2
On s’inspire du code java extrait du fichier Dalvik pour créer une classe qui permet de déchiffrer le ciphertext :
import javax.crypto.Cipher;
import javax.crypto.spec.GCMParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import java.util.Base64;
public class Main
{
private static final String CIPHER_ALGO = "AES/GCM/NoPadding";
private static final String IV = "your_m1ssi0n";
private static final String KEY = "d0_you_acc3pt_it";
public static void main(String[] args) {
try {
Main programm = new Main();
System.out.println(programm.decrypt("IkUegPuai+gfBce7nTfCkMZzZSwne3X3mnyrc5oBcD2yGHUXyMMcjCaXX2AAY20H"));
} catch (Exception e) {}
}
public String decrypt(String str) throws Exception{
Base64.Decoder dec = Base64.getDecoder();
byte[] decode = dec.decode(str.getBytes("UTF-8"));
GCMParameterSpec parameterSpec = new GCMParameterSpec(128, IV.getBytes("utf-8"));
SecretKeySpec secretKeySpec = new SecretKeySpec(KEY.getBytes("utf-8"), "AES");
Cipher cipher = Cipher.getInstance(CIPHER_ALGO);
cipher.init(2, secretKeySpec, parameterSpec);
return new String(cipher.doFinal(decode));
}
}