FCSC 2022 - Forensic Android

C-3PO

Le challenge consistait en un pcap contenant des trames provenant d’un téléphone android.

D’après l’énoncé on cherche une image qui a été exfiltrée.

La méthodologie appliquée pour retrouver lestrames contenant l’image :

Ce qui nous donne le filtre suivant : tcp.dstport != 443 && tcp.srcport != 443 && ip.src==10.0.2.16 && ip.dst==172.18.0.1 && tcp.dstport!=1337

On voit assez vite apparaitre de la base64 dans les trames, pour l’extraire : Click droit -> Suivre -> Flux TCP.

Une fois décodée, nous récupérons un fichier PNG contenant le flag.

R2-D2

D’après l’énoncé de ce challenge, il va falloir analyser un téléphone pour trouver une backdoor.

Aprés avoir lancé l’AVD (Android Virtual Device) fourni par le challenge, on se rend compte qu’il est basé sur un émulateur Android classique : Android 11 x86_64 (Google APIs). J’ai donc setup un AVD officiel à coté afin d’avoir une base de comparaison.

La première idée qui me vient à l’esprit lorsqu’on me parle d’une backdoor sous Android est le fait qu’une application malveillante ai pu être installée.

Un diff des applications installées (adb shell pm list packages) entre les deux émulateurs met en évidence deux applications :

com.google.android.apps.authenticator2
com.google.android.inputmethod.greek

La première est annoncée comme ne faisant pas partie du challenge. On récupère la seconde pour l’analyser :

$ adb shell pm path com.google.android.inputmethod.greek
package:/data/app/~~GPM5LYhZShicXImrjgL3ow==/com.google.android.inputmethod.greek-imAWCH1SzBvEhmcDPmZzKQ==/base.apk

$ adb pull /data/app/~~GPM5LYhZShicXImrjgL3ow==/com.google.android.inputmethod.greek-imAWCH1SzBvEhmcDPmZzKQ==/base.apk

On l’ouvre dans un décompilateur, ici JADX ( https://github.com/skylot/jadx/releases ).

L’entrypoint de l’application est indiquée dans Manifest.xml :

manifest.xml

En allant voir le code de la classe, plusieurs base64 sont présentes dont une qui a l’air d’être déchiffrée :

flag chiffré

code de déchiffrement

Un copier-coller dans un IDE et le flag apparaît.

R5-D4

D’après l’énoncé du challenge, on cherche ce coup-ci une backdoor plus avancée. La phrase il est convaincu que son téléphone est compromis jusqu'à l'os nous indique par un jeu de mots qu’il va falloir chercher côté OS du téléphone.

En éxécutant uname -a sur le téléphone on récupère le résultat suivant :

generic_x86_64_arm64:/ $ uname -a
Linux localhost 5.4.191-android11-2-g84c84ac7a3af-dirty #1 SMP PREEMPT Wed Apr 27 13:56:55 UTC 2022 x86_64

Un autre indice est présent, la version du kernel est dirty. Cela signifie que lorsque le kernel a été compilé, du code n’ayant pas été commité était présent. On en déduit que du code a été ajouté coté kernel (https://stackoverflow.com/questions/25090803/linux-kernel-kernel-version-string-appended-with-either-or-dirty).

Analyse du kernel

Le kernel du téléphone ayant été compromis, il nous faut pouvoir l’ouvrir dans un désassembleur pour l’analyser.

En temps normal, il est assez simple de transformer une image kernel en ELF grâce à vmlinux-to-elf mais dans ce cas l’outil ne fonctionne pas. J’ai également testé d’utiliser extract-vmlinux qui ne m’a rien donné ( même si j’ai appris plus tard qu’il fonctionnait dans ce cas).

Ce n’est pas grave, on va faire de l’artisanat. Bien qu’un émulateur Android soit lancé avec ./emulator, ce binaire se base sur qemu et il est possible d’ajouter des arguments à la ligne de commande qemu. En ajoutant -qemu-args -s -S, on est capable de s’attacher au kernel du téléphone avec GDB :

set architecture i386:x86-64
target remote :1234 

L’idée à partir de là est de dumper la section code du kernel. L’adresse de base de sa section de code est présente dans /proc/kallsyms, mais il faut désactiver kptr_restrict pour y avoir accès :

generic_x86_64_arm64:/ # echo 0 > /proc/sys/kernel/kptr_restrict                                        
generic_x86_64_arm64:/ # cat /proc/kallsyms | grep _text
ffffffffa3200000 T _text
...

Pour connaitre la longueur de la section :

generic_x86_64_arm64:/ # cat /proc/iomem  | grep "Kernel code"                                        
  05a00000-06c5c7d3 : Kernel code

Avec toutes ces informations, on extrait la section depuis GDB:

dump memory binary code.bin <adresse_base>-<adresse_base + longueur>

On ouvre code.bin en prenant soin de changer l’adresse de chargement du code par l’adresse base du kernel.

Pour faciliter l’analyse, on créé un plugiciel Ghidra qui va ajouter des symboles au code à partir de /proc/kallsyms (légèrement modifié de load_kallsyms.py )

USER_DEFINED = ghidra.program.model.symbol.SourceType.USER_DEFINED
baseAddress = currentProgram.getImageBase()

print("base address", baseAddress)

def set_name(addr, name):
    name = name.replace(' ', '-')
    createLabel(addr, name, True, USER_DEFINED)

f = askFile("kallsyms", "Open")
for line in open(f.absolutePath, 'rb').readlines():
    addr_str, type, name = line.strip().split(" ")
    addr_long = long(addr_str, 16)
    print(addr_long,type, name)
    try:
     addr = toAddr(addr_str)
     set_name(addr, name)
    except:
 print("oh no !")

On à une base propre on peut commencer l’analyse.

Recherche du code ajouté

Pour chercher le code ajouté, j’ai fait un script permet de diff deux fichiers /proc/kallsyms, en retirant les symboles liés à cfi et en retirant les hashs des fonctions :


sanitized_lines = []
with open("clean_kernel_kallsyms.txt") as fd:
    lines  = fd.readlines()
    for line in lines :
        func_name = line.split()[2].split("$")[0]
        sanitized_lines.append(func_name)
with open("infected_kernel_kallsyms.txt") as fd:
    lines = fd.readlines()
    for line in lines :
        func_name = line.split()[2].split("$")[0]
        if  not "cfi" in func_name and func_name not in sanitized_lines:
            print(func_name)

On trouve assez vite la fonction RC4 qui n’est pas présente dans le kernel linux.

En analysant ses cross-references on trouve la fonction de keylogging mod_events:

mod_event

Le code suivant permet de déchiffrer les données exfiltrées par le keylogger :

import base64
from Crypto.Cipher import ARC4

partial_key = "mdnqiOezDoB1AoX8vS87sJ8qTlySjzUKsf0F6SSlr6NtX5Bj1OEUzAF68jhJmbFEeHC33XI1WH9dDaZp973VM3DEG4knV9RLQIjmX55XBsVUle6vhO6tfCoNurGsnIkT6dHaSvoSfRsLvTWadDWZhTO2nojCZlOXoqtLF48KeOrkFopiUBoX6VwjKQQPKkeVSUzdi6PDF8tlWkhuQPZTXQQky7BgorgtmQ9o0lxlFSQfika1C5kmaUuiw4".encode()

if name == '__main__':
    b64_data = open("cipher.txt").read()
    b64_fragment = base64.b64decode(b64_data).split()
    for frag in b64_fragment:
        key_data = base64.b64decode(frag)
        key = key_data[:0xe]
        data = key_data[0xe:]
        plain = ARC4.new((key + partial_key)[:256] ).encrypt(data)
        print(plain.decode().replace('KEY_',''),end="")

En reprenant le filtre de la solution du challenge C-3PO, on trouve une deuxième base64 dans le pcap fourni. Cette base64 déchiffrée contient le flag.