Comme tous les programmeurs, vous adorez le code. Vous et lui êtes les meilleurs amis. Mais tôt ou tard dans la vie, il y aura un tel moment où il n'y aura pas de code avec vous. Oui, c'est difficile à croire, mais il y aura un énorme fossé entre vous: vous êtes à l'extérieur, et il est profondément à l'intérieur. Du désespoir, vous, comme tout le monde, devrez aller de l'autre côté. Du côté de la rétro-ingénierie.
En utilisant l'exemple de la tâche n ° 2 de la phase en ligne de
NeoQUEST-2019, nous analyserons le principe général du pilote inversé Windows. Bien sûr, l'exemple est assez simplifié, mais l'essence du processus ne change pas de cela - la seule question est la quantité de code qui doit être affichée. Armés d'expérience et de chance, commençons!
Étant donné
Selon la légende, nous avons reçu deux fichiers: un vidage du trafic et un fichier binaire qui a généré le même trafic. Tout d'abord, jetez un œil au vidage à l'aide de Wireshark:
Le vidage contient un flux de paquets UDP, chacun contenant 6 octets de données. Ces données, à première vue, sont un ensemble aléatoire d'octets - il n'est pas possible d'extraire quoi que ce soit du trafic. Par conséquent, nous tournons notre attention vers le binaire, qui devrait vous dire comment tout décrypter.
Ouvrez-le dans l'IDA:
Il semble que nous soyons confrontés à une sorte de conducteur. Les fonctions avec le préfixe WSK font référence à Winsock Kernel, l'interface de programmation réseau en mode noyau de Windows. Sur MSDN, vous pouvez
voir une description des structures et des fonctions utilisées dans WSK.
Pour plus de commodité, vous pouvez charger la bibliothèque Windows Driver Kit 8 (mode noyau) - wdk8_km (ou toute autre version plus récente) dans l'IDA pour utiliser les types définis ici:
Attention, inversez!
Comme toujours, commencez par le point d'entrée:
Allons dans l'ordre. Tout d'abord, Wsk est initialisé, un socket est créé et se lie - nous ne décrirons pas ces fonctions en détail, elles ne contiennent aucune information qui nous soit utile.
La fonction sub_140001608 définit 4 variables globales. Appelons-le InitVars. Dans l'un d'eux, une valeur est écrite à l'adresse 0xFFFFF78000000320. En recherchant un peu cette adresse, nous pouvons faire l'hypothèse qu'elle enregistre le nombre de ticks du minuteur du système à partir du moment où le système démarre. Pour l'instant, nommons la variable TickCount.
EntryPoint configure ensuite les fonctions de traitement des paquets IRP (I / O Request Packet). Vous pouvez en
savoir plus à leur sujet sur MSDN. Pour tous les types de requêtes, une fonction est définie qui passe simplement le paquet au pilote suivant dans la pile.
Mais pour le type IRP_MJ_READ (3) une fonction distincte est définie; appelons-le IrpRead.
Dans celui-ci, à son tour, CompletionRoutine est installé.
CompletionRoutine remplit la structure inconnue avec les données reçues de l'IRP et la place sur la liste. Jusqu'à présent, nous ne savons pas ce qu'il y a à l'intérieur du paquet - nous reviendrons sur cette fonction plus tard.
Nous regardons plus loin dans EntryPoint. Après avoir défini les gestionnaires IRP, la fonction sub_1400012F8 est appelée. Regardons à l'intérieur et remarquons immédiatement qu'un périphérique (IoCreateDevice) y est créé.
Appelez la fonction AddDevice. Si les types sont corrects, nous verrons que le nom du périphérique est "\\ Device \\ KeyboardClass0". Notre pilote interagit donc avec le clavier. En recherchant sur IRP_MJ_READ dans le contexte du clavier, vous pouvez
constater que la structure KEYBOARD_INPUT_DATA est transmise en paquets. Revenons à CompletionRoutine et voyons quel type de données il transmet.
L'IDA ici n'analyse pas bien la structure, mais par des décalages et d'autres appels, vous pouvez comprendre qu'elle se compose de ListEntry, KeyData (le code de balayage de la clé est stocké ici) et KeyFlags.
Après AddDevice, la fonction sub_140001274 est appelée dans EntryPoint. Elle crée un nouveau flux.
Voyons ce qui se passe dans ThreadFunc.
Elle tire la valeur de la liste et les traite. Faites immédiatement attention à la fonction sub_140001A18.
Il transmet les données traitées à l'entrée de la fonction sub_140001A68, avec un pointeur sur WskSocket et le numéro 0x89E0FEA928230002. Après avoir analysé le numéro de paramètre en octets (0x89 = 137, 0xE0 = 224, 0xFE = 243, 0xA9 = 169, 0x2328 = 9000), nous obtenons exactement la même adresse et le même port du vidage du trafic: 169.243.224.137:9000. Il est logique de supposer que cette fonction envoie un paquet réseau à l'adresse et au port spécifiés - nous ne l'examinerons pas en détail.
Voyons comment les données sont traitées avant l'envoi.
Pour les deux premiers éléments, un équivalent est effectué avec la valeur générée. Puisque le nombre de ticks est utilisé pour calculer, on peut supposer que nous sommes confrontés à la génération d'un nombre pseudo-aléatoire.
Après avoir généré le nombre, il écrase la valeur de la variable que nous appelions précédemment TickCount. Les variables de la formule sont définies dans InitVars. Si nous revenons à l'appel à cette fonction, nous découvrirons les valeurs de ces variables et, par conséquent, nous obtiendrons la formule suivante:
(54773 + 7141 * valeur_précédente)% 259200Il s'agit d'un
générateur de nombres pseudo aléatoires congruents linéaires. Il est initialisé dans InitVars à l'aide de TickCount. Pour chaque numéro suivant, le précédent sert de valeur initiale (le générateur renvoie une valeur à deux octets, et la même est utilisée pour la génération suivante).
Après équivalent avec un nombre aléatoire de deux valeurs transmises par le clavier, une fonction est appelée qui forme les deux octets restants du message. Il produit simplement
xor de deux paramètres déjà chiffrés et une valeur constante. Il est peu probable que cela décrypte les données, de sorte que les deux derniers octets du message ne contiennent pas d'informations utiles et ne peuvent pas être pris en compte. Mais que faire des données chiffrées?
Examinons de plus près ce qui est exactement chiffré. KeyData est un code d'analyse qui peut prendre une gamme de valeurs assez large; deviner que ce n'est pas facile. Mais
KeyFlags est un champ de bits:
Si vous regardez le
tableau des codes
de balayage, vous remarquerez que le plus souvent le drapeau sera soit 0 (la clé est enfoncée) ou 1 (la clé est levée). KEY_E0 sera exposé assez rarement, mais il peut arriver, mais pour rencontrer KEY_E1, les chances sont très faibles. Par conséquent, vous pouvez essayer de faire ce qui suit: nous parcourons les données du vidage, sélectionnons une valeur chiffrée KeyFlags, faisons un équivalent avec 0, générons deux PSC successifs. Premièrement, KeyData est un octet unique, et nous pouvons vérifier l'exactitude du MSS généré par octet élevé. Et deuxièmement, les prochains KeyFlags chiffrés, lors de l'exécution d'un équivalent avec le PSC correct, prendront les mêmes valeurs de bits. Si cela s'avérait incorrect, nous supposons que les KeyFlags que nous avons initialement examinés étaient 1, etc.
Essayons d'implémenter notre algorithme. Nous utiliserons python pour cela:
Implémentation d'algorithme Exécutez notre script sur les données reçues du vidage:
Et dans le trafic décrypté, nous trouvons notre ligne la plus désirable!
NQ2019DABE17518674F97DBA393415E9727982FC52C202549E6C1740BC0933C694B3DEBientôt, il y aura des articles avec une analyse des tâches restantes, ne manquez pas!
PS Et nous vous rappelons que toute personne ayant accompli au moins une tâche à NeoQUEST-2019, a droit à un prix! Vérifiez votre courrier pour une lettre, et si elle ne vous est pas parvenue, écrivez à
support@neoquest.ru !