Solution de areizen pour R5-D4

forensics disque réseau android

6 novembre 2023

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_events

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.