En termes simples, je m'ennuyais un jour et j'ai demandé à mon ami, Maximus Hackerman, de me donner quelque chose à faire. Il a rapidement envoyé un exécutable simple appelé "reverseme.exe" (lien Virustotal) avec un simple commentaire de "Imprimez le message caché dans votre propre application". Naturellement, les fichiers d'en-tête CPP n'étaient pas fournis. En exécutant l'exécutable, il s'ouvre simplement en tant qu'application basée sur la console, imprime "Envoi tous les paquets magiques, sortant bientôt, BEEP BOOP!" puis sort presque immédiatement; Je suppose que «bientôt» est subjectif.
Heureusement, il ne semble pas y avoir de débugage, donc je suppose que Hackerman se sentait bien le jour.
Après avoir attaché WINDBG et un démontbler, nous pouvons voir que l'application lance, ce qui semble initialement être une divide by zero .
Cependant, en inspection de plus près, nous constatons que cette exception est lancée juste avant l'impression de l'application son seul message visible. Il est important de noter que deux véhicules (gestionnaires d'exceptions vectores) sont légèrement enregistrés avant ce point, il est donc probable que cette exception soit intentionnelle et utilisée pour masquer le flux de contrôle. Astuce bon marché vraiment pour essayer de nous jeter vraiment!
Oubliant les véhicules pour le moment, il est sûr de supposer que si l'application «envoie» des paquets, il utilise la fonction WinSock Envoyez-vous (bien que l'application dise clairement des paquets «magiques», donc nous verrons).
Comme prévu, il s'avère que vous ne pouvez pas envoyer de paquets par magie, de sorte que la fonction send Winsock est en effet importée.
En plaçant un point d'arrêt sur la fonction Send, nous pouvons jeter un œil et voir s'il existe des données pertinentes que nous pouvons résumer au moment de l'envoi. Malheureusement, au moment où le tampon est chargé dans la fonction, les données sont déjà cryptées. Cependant, nous pouvons déterminer que le tampon a toujours une longueur de 3 octets, la prise est toujours liée à une valeur codée en dur de 0x69 ( NICE ), et que le point de rupture de la fonction d'envoi est atteint 14 fois au total.
Il existe plusieurs façons de contourner ledit chiffrement. L'un est de l'inverser entièrement, ce qui pourrait s'avérer être beaucoup d'efforts, un autre consiste à localiser les données souhaitées avant le cryptage et l'abstraire avant qu'elle ne soit cryptée. Ce dernier est nettement plus facile que le premier, donc nous allons avec cela; N'hésitez pas à inverser le chiffrement, j'ai eu un bref aperçu et ce n'est pas horrible. Alternativement, si vous vous sentez fantaisiste, vous pouvez toujours écraser la valeur de la poignée de socket et avoir un accord de demande de réception avec.
Malheureusement, il n'y a pas de chaînes utiles dans le segment de données en lecture seule, en dehors du message initial, donc pas de pointeurs utiles de cet aspect!
Pour en revenir aux véhicules, nous pouvons voir que les deux gestionnaires enregistrés sont utilisés pour copier quelques objets inconnus dans un tampon de mémoire alloué. Cela semble être fait en utilisant MEMCPY, en utilisant une fonction comme deuxième paramètre, qui à son tour utilise un entier codé en dur comme deuxième paramètre. Il convient de noter que ces valeurs codées en dur ne dépassent 14 à aucun moment. Je n'ai inclus qu'un des véhicules dans la capture d'écran, car ils sont essentiellement identiques en termes de code à l'exception des valeurs codées en dur.
En interrompant l'une des fonctions MEMCPY et en inspectant la sous-fonction dans le deuxième paramètre, nous pouvons voir que l'entier codé en dur (13 / 0dh dans l'exemple ci-dessous) est défini sur le premier octet du paramètre A1, et A1 + 1 contient un caractère, juste après que le pointeur est déféré.
Si nous vérifions les 14 autres appels à cette fonction, nous pouvons trouver le même comportement répétant; En organisant les caractères de 1 à 14, en utilisant le numéro correspondant comme indicateur d'ordre, nous pouvons voir qu'ils commencent à épeler des mots lisibles. Maintenant, nous pourrions être super paresseux et créer une application de console pour imprimer le message, une fois que nous savons ce que c'est, mais cela ressemble vraiment à la tricherie. De plus, le message peut changer à un moment donné. Alors, écrivons une grotte de code pour intercepter les valeurs avant d'être cryptées.
Désolé, mais nous utilisons C ++ pour cela! Si vous préférez utiliser C #, n'hésitez pas à passer par l'ensemble du processus Invoke Invoke pour 9,7 millions de fonctions et revenez ici lorsque vous avez terminé. Quoi qu'il en soit, nous devons d'abord décider où coder la grotte. Heureusement, nous savons déjà! En analysant la fonction ci-dessus, mais brièvement, nous savons que par le point mis en évidence, RDX contient l'index, et RDX+1 contient le caractère correspondant. Vous trouverez ci-dessous le code d'assemblage de la fonction discutée.
Maintenant, logiquement parlant, le meilleur endroit pour faire le saut serait de mov [rsp+arg_8], rdx , car nous ne nous soucions pas vraiment du troisième paramètre, mais je veux intercepter le registre RDX et RDX+1 . Pour faire ces enfants, nous allons avoir besoin de quelques octets: 10 octets pour l'instruction MOV , pour déplacer l'adresse de notre grotte de code vers un registre (nous utiliserons RAX , plus à ce sujet plus tard au cours des nouvelles de 10 heures), et 2 octets pour l'instruction JMP , pour passer au registre. Pour ceux d'entre vous qui ont le SSPT de MATHS supplémentaires, qui totalise 12 octets. Maintenant, avant d'aller à toute tante Bessie et Spaghettifitify ce code avec le système de création écrit, nous devons considérer qu'il n'y a pas de lieu idéal pour remplacer 12 octets dans l'assemblage ci-dessus. Si nous voulons passer du décalage 0x2905 ( mov [rsp+arg_8], rdx ), et nous avons besoin de 12 octets pour le faire, cela nous amène à compenser 0x2917 , ce qui est un coup entre deux instructions MOV . Malheureusement, si nous devions simplement écrire nos octets là-bas, il mâchait complètement l'assemblage, et provoquerait probablement certains, euh, «intéressants». En conséquence, ça va être plus facile (peut-être un peu plus hacky, désolé pas désolé) d'ajouter des instructions à un octet pour le rembourrage et d'arrondir à la fin d'une instruction. Bienvenue à bord, 0x90.
Quoi qu'il en soit, alors maintenant que nous savons quel est notre plan, alors écrivons du code: Cue Intense Hacker Man Music !
Vous trouverez ci-dessous ce à quoi ressemblera le saut initial de notre cave de code une fois que les octets seront écrits dans l'assemblage de l'application, avec sa propre diapositive NOP.
Cependant, avant d'écrire et de remplacer n'importe quel assemblage, nous devons démarrer l'application dans un état suspendu; Cela arrêtera l'exécution de l'application à un stade précoce, afin que les modifications de la mémoire puissent être apportées avant que l'application n'atteigne l'instruction qui nous intéresse. L'extrait de code ci-dessous montre ce processus, et je ne le reviendrai pas trop car vous pouvez le visualiser dans les fichiers source de ce référentiel, et il parle principalement de lui-même.
Maintenant que le processus est engendré, nous devrons acquérir l'adresse de base du processus. Normalement, vous pouvez utiliser l'EumprocessModules pour cela, mais comme nous avons immédiatement suspendu le thread de processus principal, le PEB ne contient pas de structure PEB_LDR_DATA entièrement peuplée, en particulier le InMemoryOrderModuleList , nous ne pouvons donc pas obtenir l'adresse de base. Soit dit en passant, cela ne semble pas être documenté n'importe où sur MSDN. Heureusement, c'est relativement facile à contourner; En reprenant très rapidement le processus, en interrogeant les modules, puis en repensant le processus, nous pouvons obtenir les informations dont nous avons besoin sans que le processus ne progresse trop. Comme pour la plupart des choses dans Windows, Microsoft aime réitérer que son système d'exploitation est supérieur en ne documentant à nouveau pas les fonctions dont nous avons besoin: NtSuspendProcess et NtResumeProcess . Idéalement, mon père est ami avec Bill Gates, et il me dit que ces fonctions en direct sont couplées dans ntdll.dll , afin que nous puissions les récupérer en utilisant la classe ci-dessous que j'ai faite plus tôt:
Maintenant que nous avons les deux fonctions dont nous avons besoin, nous pouvons reprendre le processus, interroger les modules et remettre en suspension le processus:
Vous vous demandez peut-être, pourquoi la boucle While attend-elle deux découvertes de modules par opposition à un? Eh bien, Microsoft cherche à marquer un Hattrick avec une boulette de viande à l'arrière du filet de spaghetti sans papiers en ne mentionnant pas non plus que le premier module trouvé par EnumProcessModules sera ntdll.dll, et le second sera l'exécutable. Bien que cela semble raisonnable, une fois l'exécutable trouvé, il échangera des index avec ntdll.dll. Voici un exemple:
Le résultat après interroger uniquement le premier module:
Le résultat après interroger deux modules:
Avant d'aller plus loin, nous devons écrire le code d'assemblage qui fait le saut initial à la grotte de code et la grotte de code elle-même. Essentiellement, nous écraserons une mémoire à partir du décalage 0x2905 , faisons un saut, faisons notre code d'espionnage de code, puis remonte à 0x2911 pour continuer le flux normal du programme. Initialement, nous déclarons le déménagement de l'adresse au registre RAX en tant que MOV RAX, 0x0 , car l'adresse que nous sauterons est dynamique et nous ne savons pas encore ce qu'elle est. RAX est un registre sûr à utiliser, car son registre volatile et est remplacé peu de temps après de toute façon. Fait amusant, c'est ainsi que Nintendo a programmé le jeu de plateforme Mario Bros original, avec beaucoup de sauts (dites-moi que je suis drôle)! Vous trouverez ci-dessous à quoi ressemble le code dans le compilateur; Il peut être créé d'une autre manière, mais j'ai choisi de dissimuler les instructions d'assemblage requises dans ByteCode. Si vous cherchez à le faire à la maison, utilisez ce site Web.
Le code de la grotte de code réel est un peu plus complexe, et la logique pour elle est également commentée dans ce fichier, mais voici le processus approximatif:
R10RDX , qui contient l'indice de paquets, dans R11BRDX , qui contient le caractère, dans R11B+12 ) à R10 (qui est 0x2911 )R10 nous devons également réécrire le code d'assemblage que nous avons écrasé (avec la diapositive NOP) dans notre grotte de code pour préserver la pile, etc. Ce code est référencé dans la région de « Predetermined Assembly », à l'exclusion des NOP. Vous trouverez ci-dessous la grotte de code dans sa laide gloire:Partie difficile, surtout.
À ce stade, nous avons essentiellement tout ce dont nous avons besoin pour siphonner le message caché hors mémoire, nous avons juste besoin de l'implémenter. Pour récapituler, nous avons une poignée pour un processus en suspension que nous avons engendré, des tableaux de 2 octets qui représentent la logique d'assemblage, l'adresse de base du processus suspendu, stocké dans modules[0] , et le décalage de l'endroit où nous devons écrire la logique de saut. L'extrait de code ci-dessous crée l'adresse pour sauter, l'adresse de la grotte de code (pour sauter), l'adresse de notre stockage de mémoire de 3 octets, écrit les adresses dans le code d'assemblage, puis écrit le code d'assemblage du processus en suspension, avant de le reprendre:
Le coulage magique de style Harry Potter pour le codeCaveStorageAddr est de convertir l'adresse en octets, et les valeurs codées en dur dans les boucles sont destinées aux placements d'adresse dynamique dans les tableaux d'assemblage. Bien sûr, cela peut être fait de manière beaucoup plus propre (n'écrivez pas les enfants magiques), je suis juste paresseux après avoir dû écrire manuellement tous ces octets. Les derniers bits à écrire sont les boucles à lire, en utilisant ReadProcessMemory, la mémoire, stocker les caractères et les index dans un tableau d'octets ordonné, signalez l'octet, en utilisant WriteProcessMemory , lorsque la lecture de la mémoire actuelle est terminée et imprime le message caché. Nous savons que la fonction d'envoi ne se produit que 14 fois, nous sortons donc de la boucle while une fois le tableau d'octets rempli; Cela pourrait être modifié avec plus de modifications de mémoire pour signaler à notre application que le processus «sort» et que notre boucle peut être arrêtée, plutôt que d'utiliser une valeur codée en dur de 14, mais cela fonctionne pour cet exemple.
Le résultat? Notre message caché est imprimé dans notre propre application de console! Si quelqu'un est curieux, le NPT est une référence à un autre élément de pirate de logiciel, pirater, quoi que ce soit, écrit.