Breaking Micosoft Lunix à HackQuest 2019


Bonjour, Habr!

À HackQuest, avant la conférence ZeroNight 2019, il y avait une mission divertissante. Je n'ai pas rendu la décision à temps, mais j'ai reçu ma part de sensations fortes. Je pense que vous serez intéressé de savoir ce que les organisateurs et l'équipe de r0.Crew ont préparé pour les participants.

Tâche: obtenir un code d'activation pour le système d'exploitation secret Micosoft 1998 .

Dans cet article, je vais vous expliquer comment procéder.

Table des matières


0. Tâche
1. Outils
2. Inspectez l'image
3. Périphériques de caractères et noyau
4. Recherchez register_chrdev
4.1. Préparation d'une nouvelle image Linux minimale
4.2. Quelques préparations supplémentaires
4.3. Désactiver KASLR sur lunix
4.4. Nous recherchons et trouvons une signature
5. Recherchez les fops depuis / dev / activate et la fonction d'écriture
6. Nous étudions écrire
6.1. Fonction de hachage
6.2. Algorithme de génération de clés
6.3. Keygen

Défi


Une image lancée dans QEMU nécessite un mail et une clé d'activation. Nous connaissons déjà le courrier, cherchons le reste!

1. Outils


  • Gdb
  • QEMU
  • binwalk
  • IDA

Dans ~/.gdbinit vous devez écrire une fonction utile:

 define xxd dump binary memory dump.bin $arg0 $arg0+$arg1 shell xxd dump.bin end 

2. Inspectez l'image


Renommez d'abord jD74nd8_task2.iso en lunix.iso.

En utilisant binwalk, nous voyons qu'il existe un script à l'offset 0x413000 . Ce script vérifie le courrier et la clé:


Nous cassons le contrôle avec l'éditeur hexadécimal directement dans l'image et faisons exécuter par le script nos commandes. À quoi il ressemble maintenant:


Notez que vous avez dû couper la ligne activated pour l' activated afin que la taille de l'image reste la même. Heureusement, il n'y a pas de contrôle de hachage. L'image s'appelle lunix_broken_activation.iso.

Exécutez-le via QEMU:

 sudo qemu-system-x86_64 lunix_broken_activation.iso -enable-kvm 

Creusons à l'intérieur:


Nous avons donc:

  1. Distribution - Minimal Linux 5.0.11.
  2. Le caractère device /dev/activate est engagé dans la vérification du courrier, la clé, ce qui signifie que la logique de vérification doit être recherchée quelque part dans les entrailles du noyau.
  3. Le courrier, la clé sont transmis au format email|key .

L'image target_broken_activation.iso ne sera plus requise.

3. Périphériques de caractères et noyau


Des périphériques comme /dev/mem , /dev/vcs , /dev/activate , etc. inscrivez-vous en utilisant la fonction register_chrdev :

 int register_chrdev (unsigned int major, const char * name, const struct fops); 

name est le nom, et la structure fops contient des pointeurs vers les fonctions du pilote:

 struct file_operations { struct module *owner; loff_t (*llseek) (struct file *, loff_t, int); ssize_t (*read) (struct file *, char *, size_t, loff_t *); ssize_t (*write) (struct file *, const char *, size_t, loff_t *); int (*readdir) (struct file *, void *, filldir_t); unsigned int (*poll) (struct file *, struct poll_table_struct *); int (*ioctl) (struct inode *, struct file *, unsigned int, unsigned long); int (*mmap) (struct file *, struct vm_area_struct *); int (*open) (struct inode *, struct file *); int (*flush) (struct file *); int (*release) (struct inode *, struct file *); int (*fsync) (struct file *, struct dentry *, int datasync); int (*fasync) (int, struct file *, int); int (*lock) (struct file *, int, struct file_lock *); ssize_t (*readv) (struct file *, const struct iovec *, unsigned long, loff_t *); ssize_t (*writev) (struct file *, const struct iovec *, unsigned long, loff_t *); }; 

Nous ne sommes intéressés que par cette fonction:

 ssize_t (*write) (struct file *, const char *, size_t, loff_t *); 

Ici, le deuxième argument est le tampon avec les données transférées, le suivant est la taille du tampon.

4. Recherchez register_chrdev


Par défaut, Minimal Linux compile avec des informations de débogage désactivées pour réduire la taille de l'image, mais minime. Par conséquent, vous ne pouvez pas simplement démarrer le débogueur et rechercher la fonction par son nom. Mais c'est possible par signature.

Et la signature est dans l'image Linux minimale avec des informations de débogage incluses. En général, vous devez construire votre Minimal.

Autrement dit, le schéma est le suivant:

  Minimal Linux ->   register_chrdev ->  ->   register_chrdev  Lunix 

4.1. Préparation d'une nouvelle image Linux minimale


  1. Installez les outils nécessaires:
     sudo apt install wget make gawk gcc bc bison flex xorriso libelf-dev libssl-dev 
  2. Téléchargement de scripts:

     git clone https://github.com/ivandavidov/minimal cd minimal/src 
  3. Corrigez 02_build_kernel.sh :
    le supprimer

     # Disable debug symbols in kernel => smaller kernel binary. sed -i "s/^CONFIG_DEBUG_KERNEL.*/\\# CONFIG_DEBUG_KERNEL is not set/" .config 

    ajoutez-le

     echo "CONFIG_GDB_SCRIPTS=y" >> .config 

  4. Compilation

     ./build_minimal_linux_live.sh 

L'image est minimal / src / minimal_linux_live.iso.

4.2. Quelques préparations supplémentaires


Décompressez minimal_linux_live.iso dans le dossier minimal / src / iso.

Le fichier minimal / src / iso / boot rootfs.xz kernel.xz noyau rootfs.xz et l' rootfs.xz FS. Renommez-les en kernel.minimal.xz , rootfs.minimal.xz .

De plus, vous devez retirer le noyau de l'image. Le script extract-vmlinux vous y aidera:

 extract-vmlinux kernel.minimal.xz > vmlinux.minimal 

Maintenant, dans le dossier minimal / src / iso / boot, nous avons cet ensemble: kernel.minimal.xz , rootfs.minimal.xz , vmlinux.minimal .

Mais à partir de lunix.iso, nous n'avons besoin que du noyau. Par conséquent, nous vmlinux.lunix toutes les mêmes opérations, nous appelons le vmlinux.lunix kernel.xz , oublions kernel.xz , rootfs.xz , maintenant je vais vous dire pourquoi.

4.3. Désactiver KASLR sur lunix


J'ai réussi à désactiver KASLR dans le cas de Linux minimal fraîchement assemblé dans QEMU.
Mais cela n'a pas fonctionné avec Lunix. Par conséquent, vous devez modifier l'image elle-même.

Pour ce faire, ouvrez-le dans un éditeur hexadécimal, recherchez la ligne "APPEND vga=normal" et remplacez-la par "APPEND nokaslr\x20\x20\x20" .

Et l'image s'appelle lunix_nokaslr.iso.

4.4. Nous recherchons et trouvons une signature


Nous lançons un nouveau Linux minimal dans un seul terminal:

 sudo qemu-system-x86_64 -kernel kernel.minimal.xz -initrd rootfs.minimal.xz -append nokaslr -s 

Dans un autre débogueur:

 sudo gdb vmlinux.minimal (gdb) target remote localhost:1234 

Recherchez maintenant register_chrdev dans la liste des fonctions:


De toute évidence, notre option est __register_chrdev .
Nous ne sommes pas confus d'avoir recherché register_chrdev, mais trouvé __register_chrdev

Démonter:


Quelle signature prendre? J'ai essayé plusieurs options et j'ai opté pour la pièce suivante:

  0xffffffff811c9785 <+101>: shl $0x14,%esi 0xffffffff811c9788 <+104>: or %r12d,%esi 


Le fait est que dans lunix il n'y a qu'une seule fonction qui contient 0xc1, 0xe6, 0x14, 0x44, 0x09, 0xe6 .

Maintenant, je vais montrer, mais nous découvrons d'abord dans quel segment le rechercher.


La fonction __register_chrdev adresse 0xffffffff811c9720 , il s'agit du segment .text . Là, nous allons regarder.

Déconnectez-vous de la référence Minimal Linux. Connectez-vous à lunix maintenant.

Dans un terminal:

 sudo qemu-system-x86_64 lunix_nokaslr.iso -s -enable-kvm 

Dans un autre:

 sudo gdb vmlinux.lunix (gdb) target remote localhost:1234 

Nous regardons les limites du segment .text :


Bordures 0xffffffff81000000 - 0xffffffff81600b91 , recherchez 0xc1, 0xe6, 0x14, 0x44, 0x09, 0xe6 :


On retrouve la pièce à l'adresse 0xffffffff810dc643 . Mais ce n'est qu'une partie de la fonction, voyons ce qui est ci-dessus:


Et voici le début de la fonction 0xffffffff810dc5d0 (car retq est la sortie de la fonction voisine).

5. Recherche fops depuis / dev / activate


Le prototype de la fonction register_chrdev est le suivant:

 int register_chrdev (unsigned int major, const char * name, const struct fops); 

Nous avons besoin d'une structure fops .

Redémarrage du débogueur et de QEMU. Nous avons 0xffffffff810dc5d0 pause sur 0xffffffff810dc5d0 . Cela fonctionnera plusieurs fois. Ce sont les périphériques mem, vcs, cpu/msr, cpu/cpuid et activate immédiatement après eux.


Le pointeur vers le nom est stocké dans le rcx . Et le pointeur sur fops est en r8 :


Je rappelle la structure fops
 struct file_operations { struct module *owner; loff_t (*llseek) (struct file *, loff_t, int); ssize_t (*read) (struct file *, char *, size_t, loff_t *); ssize_t (*write) (struct file *, const char *, size_t, loff_t *); int (*readdir) (struct file *, void *, filldir_t); unsigned int (*poll) (struct file *, struct poll_table_struct *); int (*ioctl) (struct inode *, struct file *, unsigned int, unsigned long); int (*mmap) (struct file *, struct vm_area_struct *); int (*open) (struct inode *, struct file *); int (*flush) (struct file *); int (*release) (struct inode *, struct file *); int (*fsync) (struct file *, struct dentry *, int datasync); int (*fasync) (int, struct file *, int); int (*lock) (struct file *, int, struct file_lock *); ssize_t (*readv) (struct file *, const struct iovec *, unsigned long, loff_t *); ssize_t (*writev) (struct file *, const struct iovec *, unsigned long, loff_t *); }; 


Ainsi, l'adresse de la fonction d' write est 0xffffffff811f068f .

6. Nous étudions écrire


La fonction comprend plusieurs blocs intéressants. Cela ne vaut pas la peine de décrire tous les points d'arrêt ici, c'est une routine habituelle. De plus, les blocs de calculs sont visibles à l'œil nu.

6.1. Fonction de hachage


vmlinux.lunix l'IDA, chargeons le noyau vmlinux.lunix et voyons ce que la fonction d'écriture a à l'intérieur.

La première chose à remarquer est ce cycle:


Une fonction sub_FFFFFFFF811F0413 est sub_FFFFFFFF811F0413 , qui commence comme ceci:


Et à l'adresse 0xffffffff81829ce0 , une table pour sha256 est détectée:


Autrement dit, sub_FFFFFFFF811F0413 = sha256. Les octets dont le hachage doit être obtenu sont transmis via $sp+0x50+var49 , et le résultat est stocké à $sp+0x50+var48 . Soit dit en passant, var49=-0x49 , var48=-0x48 , donc $sp+0x50+var49 = $sp+0x7 , $sp+0x50+var48 = $sp+0x8 .

Vérifiez-le.

Nous commençons qemu, gdb, définissons une pause sur l' 0xffffffff811f0748 call sub_FFFFFFFF811F0413 et sur l'instruction 0xffffffff811f074d xor ecx, ecx , qui est immédiatement derrière la fonction. test@mail.ru mail test@mail.ru , mot de passe 1234-5678-0912-3456 .

L'octet de courrier est transmis à la fonction, et le résultat est le suivant:


 >>> import hashlib >>> hashlib.sha256(b"t").digest().hex() 'e3b98a4da31a127d4bde6e43033f66ba274cab0eb7eb1c70ec41402bf6273dd8' >>> 

C'est-à-dire, oui, c'est vraiment sha256, seulement il calcule les hachages pour tous les octets de courrier, et pas un seul hachage à partir du courrier.

Ensuite, les hachages sont additionnés par octet. Mais si la somme est supérieure à 0xEC , le reste de la division par 0xEC :

 import hashlib def get_email_hash(email): h = [0]*32 for sym in email: sha256 = hashlib.sha256(sym.encode()).digest() for i in range(32): s = h[i] + sha256[i] if s <= 0xEC: h[i] = s else: h[i] = s % 0xEC return h 

Le montant est enregistré à 0xffffffff81c82f80 . Voyons ce que sera le hachage de test@mail.ru .

Nous avons ffffffff811f0786 dec r13d pause sur ffffffff811f0786 dec r13d (c'est la sortie de la boucle):


Et comparer avec:

 >>> get_email_hash('test@mail.ru') 2b902daf5cc483159b0a2f7ed6b593d1d56216a61eab53c8e4b9b9341fb14880 

Mais le hachage lui-même est clairement un peu long pour la clé.

6.2. Algorithme de génération de clés


La clé est responsable de ce code:


Voici le calcul final de chaque octet:

 0xFFFFFFFF811F0943 imul eax, r12d 0xFFFFFFFF811F0947 cdq 0xFFFFFFFF811F0948 idiv r10d 

Dans les octets de hachage eax et r12d , ils sont multipliés, puis le reste de la division par 9 est pris.

Parce que


Et les octets sont pris dans un ordre inattendu. Je l'indiquerai dans keygen.

6.3. Keygen


 def keygen(email): email_hash = get_email_hash(email) pairs = [(0x00, 0x1c), (0x1f, 0x03), (0x01, 0x1d), (0x1e, 0x02), (0x04, 0x18), (0x1b, 0x07), (0x05, 0x19), (0x1a, 0x06), (0x08, 0x14), (0x17, 0x0b), (0x09, 0x15), (0x16, 0x0a), (0x0c, 0x10), (0x13, 0x0f), (0x0d, 0x11), (0x12, 0x0e)] key = [] for pair in pairs: i = pair[0] j = pair[1] key.append((email_hash[i] * email_hash[j])%9) return [''.join(map(str, key[i:i+4])) for i in range(0, 16, 4)] 

Générons donc une clé:

 >>> import lunix >>> lunix.keygen("m.gayanov@gmail.com") ['0456', '3530', '0401', '2703'] 


Et maintenant, vous pouvez vous détendre et jouer au jeu 2048 :) Merci de votre attention! Code ici

Source: https://habr.com/ru/post/fr471778/


All Articles