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
:
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.