Aller au contenu principal

📱 Mobile

Premier challenge de reverse d'application mobile pour ma part...

Description du challenge

  • Nom du CTF : Root-Me Pro et DGSE
  • Catégorie : Mobile, Crypto, Reverse
  • Difficulté : Facile/Intermédiaire
  • Date : Avril-Mai 2025

On nous donne le challenge suivant:

Mission 5

Exploration de l'APK

Je télécharge alors le fichier APK de la messagerie chiffrée. N'ayant pas de téléphone Android, ni d'émulateur sous la main, je décide immédiatement d'essayer de reverse l'application et lire son code.

Je lance alors Kali Linux et Jadx-GUI.

alt text

Première chose qui saute à l'oeil dans le dossier du code source com.nullvastation.cryssage, on voit un dossier api avec deux fichiers intéressants :

  1. Une interface ApiService
  2. Une classe RetrofitClient

Dans l'interface, on peut lire le code suivant :

alt text

On peut voir que l'application appelle une API au niveau d'une endpoint /messages, avec un paramètre id et que l'API renvoie un fichier JSON. L'objectif est maintenant de trouver l'adresse de l'API, ce que l'on peut voir facilement dans la classe RetrofitClient.

alt text

Bingo ! L'adresse de l'API est là, ligne 15: http://163.172.67.201:8000

Je décide alors de lancer Insomnia pour appeler l'API, en donnant aucune valeur au paramètre id:

alt text

Raté ! Il faut envoyer un ID d'appareil. J'essaie alors de donner une valeur complètement absurde à id, soit a. Sur un malentendu, ça peut marcher...

alt text

On donne la liste de tous les échanges de la messagerie, qu'il faut maintenant déchiffrer. Pour cela, explorons plus en détail le code source de l'application. On arrive facilement à comprendre comment déchiffrer les messages.

Dans la classe HomeViewModel de com.nullvastation.cryssage.ui.home, on tombe sur plusieurs choses intéressantes.

Premièrement, le vecteur d'initialisation IV et le sel sont hardcodés dans le code:

alt text

  • IV: LJo+0sanl6E3cvCHCRwyIg==
  • SALT: s3cr3t_s@lt

Un peu plus bas dans le fichier, on a la fonction de déchiffrage.

alt text

L'algorithme de déchiffrage est alors sous nos yeux:

  1. On hashe en SHA-256 la marque et le modèle de l'appareil où l'application est installée (fonction hashDeviceId)
  2. On met en place le déchiffrement AES en mode CBC et remplissage PKCS5. Pour cela, on appelle deriveKey avec l'identifiant précédemment hashé et le sel. On crée aussi le vecteur d'initialisation qui est le IV décodé en Base64
  3. On décode le message (encryptedMessage) depuis Base64
  4. On déchiffre le message avec AES

Problème ? Il faut déterminer la marque et le modèle corrects pour déchiffrer les messages. La seule indication de l'énoncé pouvant nous aider est :

l'équipe a saisi une vielle tablette Google utilisée pour leurs communications.

Recherche de l'appareil de chiffrement

Il faut alors trouver une liste de toutes les marques et modèles d'appareils Google pour déchiffrer les messages.

Après une recherche Google rapide, on se rend compte que Google fournit un CSV des appareils pouvant avoir le Play Store.

Plus qu'à mettre un place un script Python pour tester toute cette liste :

import base64
import hashlib
import json
import pandas as pd
from Crypto.Cipher import AES
from Crypto.Util.Padding import unpad

def hash_device_id(model, brand):
"""Hash the device ID using SHA-256"""
device_id = f"{model}:{brand}"
digest = hashlib.sha256(device_id.encode('utf-8')).digest()
return base64.b64encode(digest).decode('utf-8')

def derive_key(device_id, salt):
"""Derive the encryption key from the device ID and salt"""
key_material = f"{device_id}:{salt}"
key = hashlib.sha256(key_material.encode('utf-8')).digest()
return key

def decrypt_message(encrypted_message, key, iv):
"""Decrypt a message using AES/CBC/PKCS5Padding"""
try:
encrypted_bytes = base64.b64decode(encrypted_message)
cipher = AES.new(key, AES.MODE_CBC, iv)
decrypted_bytes = unpad(cipher.decrypt(encrypted_bytes), AES.block_size)
return decrypted_bytes.decode('utf-8')
except Exception as e:
return f"[Decryption error: {str(e)}]"

def main():
# Static values from the code
STATIC_IV = "LJo+0sanl6E3cvCHCRwyIg=="
STATIC_SALT = "s3cr3t_s@lt"

try:
df = pd.read_csv('supported_devices.csv', encoding='utf-16')
# Convert to list of dictionaries with only the brand and model columns
devices = df[['Retail Branding', 'Model']].rename(
columns={'Retail Branding': 'brand', 'Model': 'model'}
).dropna().to_dict('records')
print(f"Loaded {len(devices)} devices from CSV file")
except Exception as e:
print(f"Error loading devices from CSV: {e}")
return

# Load messages
with open('messages.json', 'r') as f:
data = json.load(f)

iv_bytes = base64.b64decode(STATIC_IV)

# Track successful decryptions for each message
successful_decryptions = {}

# Try each device on each message
print("Testing each device on each message:\n")

for msg_index, msg in enumerate(data["messages"]):
print(f"Message {msg_index+1} from {msg['sender']} at {msg['timestamp']}:")
print(len(devices))
for device in devices:
device_name = f"{device['brand']} {device['model']}"
hashed_device_id = hash_device_id(device["model"], device["brand"])
key = derive_key(hashed_device_id, STATIC_SALT)

# Try to decrypt this message with this device
decrypted = decrypt_message(msg["content"], key, iv_bytes)

# Check if decryption was successful (no error message)
if not decrypted.startswith("[Decryption error"):
print(f" ✓ Successfully decrypted with {device_name}")
print(f" Content: {decrypted[:50]}..." if len(decrypted) > 50 else f" Content: {decrypted}")

# Track this successful combination
if msg_index not in successful_decryptions:
successful_decryptions[msg_index] = []
successful_decryptions[msg_index].append({
"device": device_name,
"decrypted": decrypted
})

if msg_index not in successful_decryptions:
print("No working device found for this message")
print()

# Summary of results
print("\n=== DECRYPTION SUMMARY ===")

if successful_decryptions:
# Check if one device works for all messages
all_devices = set()
for msg_index, successes in successful_decryptions.items():
devices_for_msg = {s["device"] for s in successes}
all_devices.update(devices_for_msg)

common_devices = None
for msg_index, successes in successful_decryptions.items():
devices_for_msg = {s["device"] for s in successes}
if common_devices is None:
common_devices = devices_for_msg
else:
common_devices &= devices_for_msg

if common_devices:
print(f"\nDevices that work for ALL messages: {', '.join(common_devices)}")

# Use the first common device to save all decrypted messages
working_device = list(common_devices)[0]
print(f"\nSaving all decrypted messages using {working_device}...")

# Find the corresponding device dictionary
for device in devices:
device_name = f"{device['brand']} {device['model']}"
if device_name == working_device:
hashed_device_id = hash_device_id(device["model"], device["brand"])
key = derive_key(hashed_device_id, STATIC_SALT)

# Save decrypted messages to file
with open('decrypted_messages.json', 'w') as f:
decrypted_data = {
"messages": [
{
**msg,
"decrypted_content": decrypt_message(msg["content"], key, iv_bytes),
"isEncrypted": False
}
for msg in data["messages"]
]
}
json.dump(decrypted_data, f, indent=2)
print("Decrypted messages saved to 'decrypted_messages.json'")
break
else:
print("\nNo single device works for all messages!")
print("\nDecryption results by message:")

for msg_index, successes in sorted(successful_decryptions.items()):
msg = data["messages"][msg_index]
print(f"\nMessage {msg_index+1} from {msg['sender']} at {msg['timestamp']}:")
for success in successes:
print(f" - Decrypted with {success['device']}")
else:
print("No messages could be decrypted with any of the provided devices.")

Le script indique que les criminels ont utilisé un appareil de marque Google et de modèle Yellowstone.

Résultat :

{
"messages": [
{
"content": "M2geCVKOzPlyug9p9DvthxPip0oe9BPiT2sDfFhWy7iC3+JQI4SfO7+SLAlFSUmu8LoGj1hrUWil/uNXvc+5mKBMrRNFQT8ijBK14P0Z8qA=",
"isEncrypted": false,
"sender": "Agent-02",
"timestamp": "2025-04-01 08:00:00",
"decrypted_content": "Target acquired. Hospital network vulnerable. Initiating ransomware deployment."
},
{
"content": "//5PBsYWhHlgqhVgG1omUyevzmlErLZVsTCLO78Rbb9qBMPnsKCS5/RZ4GEdWRBPiZ4BtO5h7j2PuIutfqf7ag==",
"isEncrypted": false,
"sender": "Agent-1337",
"timestamp": "2025-04-01 10:00:00",
"decrypted_content": "Keep this safe. RM{788e6f3e63e945c2a0f506da448e0244ac94f7c4}"
},
{
"content": "2uNMSnJZa5JExhYgNA+V3RAiafhuLkj8Jnr4U+lSZOrrpMWjyA13w0Do3IIPcVBgK070rmweRKX/GkCAxat4i3JfWk1UvWNSmEZbHQlFznR7VFW6FKK84iJKhiDOp8Tk",
"isEncrypted": false,
"sender": "Agent-01",
"timestamp": "2025-04-02 15:30:00",
"decrypted_content": "New target identified. School district network. Estimated payout: 500k in crypto."
},
{
"content": "Swz/ycaTlv3JM9iKJHaY+f1SRyKvfQ5miG6I0/tUb8bvbOO+wyU5hi+bGsmcJD3141FrmrDcBQhtWpYimospymABi3bzvPPi01rPI8pNBq8=",
"isEncrypted": false,
"sender": "Agent-02",
"timestamp": "2025-04-03 13:20:00",
"decrypted_content": "New ransomware strain ready for deployment. Testing phase complete."
},
{
"content": "NAe44oieygG7xzLQT3j0vN+0NoPNUu0TAaid9Az3IlpcKwR0lSKaPT8F4y1zpbArWFIGpgzsPZtPAwL50qocTRMG/g5u+/wcc1nxmhBjCbg=",
"isEncrypted": false,
"sender": "Agent-04",
"timestamp": "2025-04-04 08:30:00",
"decrypted_content": "Security patch released. Need to modify attack vector. Meeting at usual place."
},
{
"content": "dfeKlZP/gIntHySBYine2YUlNiX3LjlMOLu7y9tgprFyJIIcQpfghlQXut6cJUG2wtzGBVQUm7ITdpLNeVaZjamQHhPWEtNIJE/xtFg66Klui1qCKYKSrmZ4wm1CG/ZPy4csqbM28Ur8dts7XoV5FA==",
"isEncrypted": false,
"sender": "Agent-04",
"timestamp": "2025-04-05 16:45:00",
"decrypted_content": "New zero-day exploit in a linux binary discovered. Perfect for next operation. Details incoming."
},
{
"content": "PUk3sId1fEI817QsaFeFIv2UZ5w7xS12e84rH66UBAj6u4PmrotwtHHxUf2gGbvZEGSYlp32AZblP159t55mW2hK9Qg/0Rh9i/TglPwrwFXGNwRC1wJUPo4EvSidaHnN",
"isEncrypted": false,
"sender": "Agent-03",
"timestamp": "2025-04-06 11:15:00",
"decrypted_content": "[Decryption error: Padding is incorrect.]"
},
{
"content": "Cw+C1qOFWb0dmB92JeDOCKFJO2eNJ2Wz22ncWNwtQHReWeRfnrb2guekWddgerRxk8knGAsP66X96/rkeIxKE6GSiPzXBWjU7diNOJKZyJA=",
"isEncrypted": false,
"sender": "Agent-01",
"timestamp": "2025-04-06 14:20:00",
"decrypted_content": "[Decryption error: Padding is incorrect.]"
},
{
"content": "tk2MS/rgL2DQaNPIYa77Epg5yr+oV3LGyOmctJCX9p67L2LNRhsiaMZSkinMKI7lfrAumbNtHOfByPhUTJjCkt31wiCiOFc3l2w0+UyxsKLt4rYvNlP/y7mVEn8D+qaX",
"isEncrypted": false,
"sender": "Agent-00",
"timestamp": "2025-04-07 09:00:00",
"decrypted_content": "[Decryption error: PKCS#7 padding is incorrect.]"
}
]
}
FLAG

RM{788e6f3e63e945c2a0f506da448e0244ac94f7c4}