DGSE CTF - Mission 5 (Mobile)
đ Reconnaissance
Lâapplication se prĂ©sente de la maniĂšre suivante :
- Une section
Messages on previous devices
contenant des messages chiffrĂ©s, probablement avec les identifiants dâun ancien appareil. - Une section
Messages on this device
affichant les messages en clair destinĂ©s Ă lâappareil actuel.
đ Interception du traffic HTTP
Avant de passer Ă la dĂ©compilation de lâAPK, jâai commencĂ© par intercepter le trafic HTTP via un proxy avec Burp Suite. Voici le guide proposĂ© par PortSwigger pour configurer un appareil Android avec Burp : Configuring an Android device to work with Burp Suite
Voici la requĂȘte intĂ©ressante que jâai interceptĂ© :
Interception du traffic HTTP avec Burpsuite
On peut voir que lâapplication envoie une requĂȘte GET Ă /messages
avec un paramĂštre id=5vmObbpVKWesTEbiSxlCzXSEXFUwues1nVIGiMYOED8=
. En réponse, on obtient une liste de messages, chacun contenant :
content
: le contenu du messageisEncrypted
: un booléen indiquant si le message est chiffrésender
: lâĂ©xpĂ©diteurtimestamp
: la date dâenvoi
Ce quâon observe ici, câest que tous les messages renvoyĂ©s par lâAPI sont chiffrĂ©s, alors que certains apparaissent en clair dans lâapplication. Cela suggĂšre que le dĂ©chiffrement est effectuĂ© localement, dans lâapplication elle-mĂȘme, avant lâaffichage.
đ§© Reverse de lâAPK
On peut dĂ©compiler lâAPK grĂące Ă un outil comme jadx ou directement avec androidstudio. En fouillant un peu, on tombe sur mĂ©thode decryptMessage
trÚs intéréssante dans smali\out\com\nullvastation\cryssage\ui\home\HomeViewModel.smali
:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
.method private final decryptMessage(Ljava/lang/String;)Ljava/lang/String;
.registers 8
.line 73
:try_start_0
sget-object v0, Landroid/os/Build;->MODEL:Ljava/lang/String;
const-string v1, "MODEL"
invoke-static {v0, v1}, Lkotlin/jvm/internal/Intrinsics;->checkNotNullExpressionValue(Ljava/lang/Object;Ljava/lang/String;)V
sget-object v1, Landroid/os/Build;->BRAND:Ljava/lang/String;
const-string v2, "BRAND"
invoke-static {v1, v2}, Lkotlin/jvm/internal/Intrinsics;->checkNotNullExpressionValue(Ljava/lang/Object;Ljava/lang/String;)V
invoke-direct {p0, v0, v1}, Lcom/nullvastation/cryssage/ui/home/HomeViewModel;->hashDeviceId(Ljava/lang/String;Ljava/lang/String;)Ljava/lang/String;
move-result-object v0
.line 74
const-string v1, "s3cr3t_s@lt"
invoke-direct {p0, v0, v1}, Lcom/nullvastation/cryssage/ui/home/HomeViewModel;->deriveKey(Ljava/lang/String;Ljava/lang/String;)[B
move-result-object v0
.line 75
const-string v1, "LJo+0sanl6E3cvCHCRwyIg=="
const/4 v2, 0x0
invoke-static {v1, v2}, Landroid/util/Base64;->decode(Ljava/lang/String;I)[B
move-result-object v1
.line 77
const-string v3, "AES/CBC/PKCS5Padding"
invoke-static {v3}, Ljavax/crypto/Cipher;->getInstance(Ljava/lang/String;)Ljavax/crypto/Cipher;
move-result-object v3
.line 78
new-instance v4, Ljavax/crypto/spec/SecretKeySpec;
const-string v5, "AES"
invoke-direct {v4, v0, v5}, Ljavax/crypto/spec/SecretKeySpec;-><init>([BLjava/lang/String;)V
.line 79
new-instance v0, Ljavax/crypto/spec/IvParameterSpec;
invoke-direct {v0, v1}, Ljavax/crypto/spec/IvParameterSpec;-><init>([B)V
.line 80
check-cast v4, Ljava/security/Key;
check-cast v0, Ljava/security/spec/AlgorithmParameterSpec;
const/4 v1, 0x2
invoke-virtual {v3, v1, v4, v0}, Ljavax/crypto/Cipher;->init(ILjava/security/Key;Ljava/security/spec/AlgorithmParameterSpec;)V
.line 82
invoke-static {p1, v2}, Landroid/util/Base64;->decode(Ljava/lang/String;I)[B
move-result-object p1
.line 83
invoke-virtual {v3, p1}, Ljavax/crypto/Cipher;->doFinal([B)[B
move-result-object p1
.line 84
invoke-static {p1}, Lkotlin/jvm/internal/Intrinsics;->checkNotNull(Ljava/lang/Object;)V
sget-object v0, Ljava/nio/charset/StandardCharsets;->UTF_8:Ljava/nio/charset/Charset;
const-string v1, "UTF_8"
invoke-static {v0, v1}, Lkotlin/jvm/internal/Intrinsics;->checkNotNullExpressionValue(Ljava/lang/Object;Ljava/lang/String;)V
new-instance v1, Ljava/lang/String;
invoke-direct {v1, p1, v0}, Ljava/lang/String;-><init>([BLjava/nio/charset/Charset;)V
:try_end_50
.catch Ljava/lang/Exception; {:try_start_0 .. :try_end_50} :catch_51
return-object v1
:catch_51
move-exception p1
.line 86
const-string v0, "Error decrypting message"
check-cast p1, Ljava/lang/Throwable;
const-string v1, "DECRYPT_ERROR"
invoke-static {v1, v0, p1}, Landroid/util/Log;->e(Ljava/lang/String;Ljava/lang/String;Ljava/lang/Throwable;)I
.line 87
const-string p1, "[Encrypted] This message was encrypted with old device credentials"
return-object p1
.end method
Cette mĂ©thode implĂ©mente un dĂ©chiffrement AES en mode CBC avec un padding PKCS5. Voici ce quâelle fait :
- RécupÚre les valeurs MODEL et BRAND du téléphone.
- ConcatĂšne ces deux valeurs pour former un identifiant unique dâappareil.
- DĂ©rive une clĂ© AES Ă partir de cet identifiant et dâun sel fixe (
s3cr3t_s@lt
). - Utilise un IV fixe (
LJo+0sanl6E3cvCHCRwyIg==
, encodé en Base64). - Déchiffre le message passé en argument (lui aussi encodé en Base64).
Si le dĂ©chiffrement Ă©choue (par exemple si le message a Ă©tĂ© chiffrĂ© avec un autre appareil), lâapplication retourne ce message par dĂ©faut : [Encrypted] This message was encrypted with old device credentials
Voici également la méthode deriveKey
utilisée pour générer la clé AES :
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
.method private final deriveKey(Ljava/lang/String;Ljava/lang/String;)[B
.registers 5
.line 66
new-instance v0, Ljava/lang/StringBuilder;
invoke-direct {v0}, Ljava/lang/StringBuilder;-><init>()V
invoke-virtual {v0, p1}, Ljava/lang/StringBuilder;->append(Ljava/lang/String;)Ljava/lang/StringBuilder;
move-result-object p1
const/16 v0, 0x3a
invoke-virtual {p1, v0}, Ljava/lang/StringBuilder;->append(C)Ljava/lang/StringBuilder;
move-result-object p1
invoke-virtual {p1, p2}, Ljava/lang/StringBuilder;->append(Ljava/lang/String;)Ljava/lang/StringBuilder;
move-result-object p1
invoke-virtual {p1}, Ljava/lang/StringBuilder;->toString()Ljava/lang/String;
move-result-object p1
.line 67
const-string p2, "SHA-256"
invoke-static {p2}, Ljava/security/MessageDigest;->getInstance(Ljava/lang/String;)Ljava/security/MessageDigest;
move-result-object p2
.line 68
sget-object v0, Ljava/nio/charset/StandardCharsets;->UTF_8:Ljava/nio/charset/Charset;
const-string v1, "UTF_8"
invoke-static {v0, v1}, Lkotlin/jvm/internal/Intrinsics;->checkNotNullExpressionValue(Ljava/lang/Object;Ljava/lang/String;)V
invoke-virtual {p1, v0}, Ljava/lang/String;->getBytes(Ljava/nio/charset/Charset;)[B
move-result-object p1
const-string v0, "this as java.lang.String).getBytes(charset)"
invoke-static {p1, v0}, Lkotlin/jvm/internal/Intrinsics;->checkNotNullExpressionValue(Ljava/lang/Object;Ljava/lang/String;)V
invoke-virtual {p2, p1}, Ljava/security/MessageDigest;->digest([B)[B
move-result-object p1
const-string p2, "digest(...)"
invoke-static {p1, p2}, Lkotlin/jvm/internal/Intrinsics;->checkNotNullExpressionValue(Ljava/lang/Object;Ljava/lang/String;)V
return-object p1
.end method
Cette fonction concatĂšne lâidentifiant dâappareil avec le sel, puis calcule le SHA-256 de la chaĂźne rĂ©sultante pour produire la clĂ© de 256 bits.
Jâai réécrit les fonctions DecryptMessage
et DeriveKey
en Python, puis jâai tentĂ© de dĂ©chiffrer le dernier message â dont nous connaissions le contenu dĂ©chiffrĂ© â en utilisant lâidentifiant interceptĂ© via BurpSuite : :
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
import hashlib
import base64
from Crypto.Cipher import AES
from Crypto.Util.Padding import unpad
# Fonction pour déchiffrer le message
def decrypt_message(encrypted_message, device_id):
try:
key = derive_key(device_id, "s3cr3t_s@lt")
iv = base64.b64decode("LJo+0sanl6E3cvCHCRwyIg==")
encrypted_data = base64.b64decode(encrypted_message)
cipher = AES.new(key, AES.MODE_CBC, iv)
decrypted_data = unpad(cipher.decrypt(encrypted_data), AES.block_size)
return decrypted_data.decode('utf-8')
except Exception:
return None
# Fonction pour dériver la clé
def derive_key(device_id, salt):
device_id_with_salt = f"{device_id}:{salt}"
derived_key = hashlib.sha256(device_id_with_salt.encode('utf-8')).digest()
return derived_key
encrypted_message = "XeRtXUezp7oP9fqmL1mCho48gIf1weTI6CK09Nl7ZtmonWC9yOFTqbqlSeTgFRXVrYPTPYLtynBIieLafbViGo2JXQ6/E2rPPpS+7afHr/S28VtsIncUNx66q6qPvFT9"
device_id = "5vmObbpVKWesTEbiSxlCzXSEXFUwues1nVIGiMYOED8="
print(decrypt_message(encrypted_message, device_id))
Le résultat est concluant :
1
2
$ python3 decrypt.py
URGENT: DGSE is on our trail. They got one of us. Destroy all traces and go dark
đ DĂ©chiffrement des autres messages
Maintenant que nous maĂźtrisons la logique de dĂ©chiffrement, il nous faut retrouver lâidentifiant (device_id) utilisĂ© sur lâancien appareil. DâaprĂšs le brief de mission, celui-ci est gĂ©nĂ©rĂ© Ă partir du MODEL et du BRAND de lâappareil.
Voici la fonction Python qui permet de reproduire cette génération :
1
2
3
4
def hash_device_id(model, brand):
device_id = f"{model}:{brand}"
hashed_device_id = hashlib.sha256(device_id.encode('utf-8')).digest()
return base64.encodebytes(hashed_device_id).decode('utf-8').strip()
DOn sait Ă©galement que lâappareil saisi appartient Ă la marque Google. On peut donc itĂ©rer sur tous les modĂšles dâappareils Google connus, gĂ©nĂ©rer leur device_id, et tenter de dĂ©chiffrer les messages un par un. La liste officielle des appareils compatibles avec Google Play nous servira de base.
Voici le script final :
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
import hashlib
import base64
from Crypto.Cipher import AES
from Crypto.Util.Padding import unpad
import csv
# Fonction pour hasher l'identifiant du device
def hash_device_id(model, brand):
device_id = f"{model}:{brand}"
hashed_device_id = hashlib.sha256(device_id.encode('utf-8')).digest()
return base64.encodebytes(hashed_device_id).decode('utf-8').strip()
# Fonction pour dériver la clé
def derive_key(device_id, salt):
device_id_with_salt = f"{device_id}:{salt}"
derived_key = hashlib.sha256(device_id_with_salt.encode('utf-8')).digest()
return derived_key
# Fonction pour déchiffrer le message
def decrypt_message(encrypted_message, model, brand):
try:
device_id = hash_device_id(model, brand)
key = derive_key(device_id, "s3cr3t_s@lt")
iv = base64.b64decode("LJo+0sanl6E3cvCHCRwyIg==")
encrypted_data = base64.b64decode(encrypted_message)
cipher = AES.new(key, AES.MODE_CBC, iv)
decrypted_data = unpad(cipher.decrypt(encrypted_data), AES.block_size)
return decrypted_data.decode('utf-8')
except Exception:
return None
# Chargement des configs depuis un fichier CSV
device_configs = []
with open("devices.csv", newline='', encoding='utf-8') as csvfile:
reader = csv.reader(csvfile)
next(reader) # ignorer l'en-tĂȘte
for brand, model in reader:
if brand == "Google":
device_configs.append((model, brand))
# Message chiffré
encrypted_message = "Swz/ycaTlv3JM9iKJHaY+f1SRyKvfQ5miG6I0/tUb8bvbOO+wyU5hi+bGsmcJD3141FrmrDcBQhtWpYimospymABi3bzvPPi01rPI8pNBq8="
# Bruteforce
for model, brand in device_configs:
print(f"Testing with model: {model}, brand: {brand}")
decrypted_message = decrypt_message(encrypted_message, model, brand)
if decrypted_message:
print(f"â
Decrypted message: {decrypted_message}")
break
else:
print("â Failed to decrypt\n")
1
2
3
4
5
6
7
8
9
10
11
12
13
14
$ python3 decrypt.py
Testing with model: guybrush, brand: Google
â Failed to decrypt
Testing with model: skyrim, brand: Google
â Failed to decrypt
Testing with model: zork, brand: Google
â Failed to decrypt
...
Testing with model: Yellowstone, brand: Google
â
Decrypted message: Keep this safe. RM{788e6f3e63e945c2a0f506da448e0244ac94f7c4}
Nous apprenons ainsi que lâappareil utilisĂ© Ă©tait un Google Yellowstone. Et nous rĂ©cupĂ©rons le flag : RM{788e6f3e63e945c2a0f506da448e0244ac94f7c4}
Voici tous les autres messages dĂ©chiffrĂ©s avec ce mĂȘme identifiant :
1
2
3
4
5
6
7
8
â
Decrypted message: Target acquired. Hospital network vulnerable. Initiating ransomware deployment.
â
Decrypted message: New target identified. School district network. Estimated payout: 500k in crypto.
â
Decrypted message: New ransomware strain ready for deployment. Testing phase complete.
â
Decrypted message: Security patch released. Need to modify attack vector. Meeting at usual place.
â
Decrypted message: New zero-day exploit in a linux binary discovered. Perfect for next operation. Details incoming.
â
Decrypted message: Payment received. Sending decryption keys now. Next target: City infrastructure.
â
Decrypted message: Encryption complete. Ransom note deployed. 48h countdown started.
â
Decrypted message: URGENT: DGSE is on our trail. They got one of us. Destroy all traces and go dark.