05 - Audi-K - Nouvelles aventures en Kernel Land 2/2
Blackclowns magazine Issue 1 Article 5
Tolwin <tolwin100@hotmail.com>
- Introduction à Audi-K
- 1.1 - Réseau dans le noyau
- 1.2 - Audi-K - TDI Kernel Socket API
- 1.3 - Audi-K - Ce qui sera dit et ce qui ne le sera pas. Ce qui est fait et ce qui n'est pas fait
- Concepts de base pour TDI
- 2.1 - création des objets
- 2.2 - Référencement du driver tcpip.sys : méthode simple
- 2.3 - Référencement du driver tcpip.sys : méthode ciblée
- 2.4 - Macros TDI de gestion des IRP
- 2.5 - Appel au driver Tcpip
- 2.6 - Structures de données
- 2.7 - Direct I/O, bienvenue dans le monde des Memory Descriptor List (MDL)
- Fonctions niveau TDI
- 3.1 - Petits elastics là pour éviter les fuites mémoire avec des
- 3.2 - Première connection, premier chat
- 3.3 - Et pour le reste ?
- Parlons Winsock
- 4.1 - Kernel Sockets c'est bien, en abuser ça craint !
- 4.2 - Structures de données, encore
- 4.3 - Fonctions Audi-K
- La marque du capitaine crochet
- Conclusion
- Références
3 - Fonctions niveau TDI
Pour commencer simplement et bien se concentrer sur les mécanismes TDI, on va réaliser une implémentation peu efficace. Toutes les opérations seront réalisées pour le moment de manière bloquante. Ce système va permettre d'utiliser tranquillement les macros fournies par TDI, de penser son code de manière linéaire sans trop se prendre le chou avec des completion routines, et avoir un fonctionnement ressemblant à du WinSock lorsqu'on ne débloque pas les sockets. Bon début.
3.1 - Petits elastics là pour éviter les fuites mémoire avec des
Pour la gestion de la mémoire, je réutilise les fonctions memtrack. Celles-ci, développées pour ndis_fw, se retrouvent dans plusieurs projets, dont le honeypot Sebek où je les ai découvertes et où j'en suis tombé fou amoureux. Ces fonctions assurent un contrôle des allocations mémoire, contre les débordements et contre les oublis de libération de mémoire allouée. Pour cela, memtrack maintient une liste de tous les blocs alloués, et ajoute un "canari" avant et après chaque bloc. Lors du nettoyage final, memtrack s'assure que chaque mémoire demandée a bien été restituée, et indique le problème en cas d'erreur. Ca n'a l'air de rien mais c'est tout simplement excellent !
void memtrack_init(void);
void memtrack_free(void);
void *mt_malloc( ULONG size,
const char *file,
ULONG line);
#define malloc_np(size) mt_malloc((size), __FILE__, __LINE__)
void free(void *ptr);
3.2 - Première connection, premier chat
On parle, on parle, mais quand est-ce qu'on code ? Et bien c'est le moment ! Dans ce chapitre, on va mettre en pratique tout le blabla qui précède pour enfin établir une connexion ! On sera en mesure de connecter un serveur distant, à partir de son adresse IP, et donc par exemple d'aller demander la page d'accueil de Google.
Prenons le cas de TCP. Comme on l'a vu plus haut, pour créer un socket TCP, il faut un transport object, pour les ressources du local endpoint, et un connection object pour les ressources propres à la gestion de la session TCP.
Transport Object
Pour la création du transport object, il faut préparer une structure extended attributes qui contiendra les informations requises. Il s'agit ici du couple local interface : port. Comme en WinSock lors d'un bind(), ces valeurs peuvent être mises à zéro si on ne veut pas spécifier d'interface particulière et si on souhaite une allocation dynamique du port. Les extended attributes doivent spécifier ce qu'ils encapsulent. Ceci est fait par une chaëne de caractères.
Les extended attributes sont un gros buffer. Assez gros pour contenir à la suite
- une structure FILE_FULL_EA_INFORMATION
- la chaëne de caractères spécifiant le type d'infos passées dans le extended attributes
- une structure TA_IP_ADDRESS pour donner les infos sur interface : port
Pour le premier et le dernier élément, pas de problème, un sizeof et le tour est réglé. Pour la chaëne ce n'est pas plus dur. D'une part, cette chaëne est définie dans tdi.h sous le nom TdiTransportAddress. D'autre part, sa taille est également définie sous le nom TDI_TRANSPORT_ADDRESS_LENGTH. Par contre, cette taille mesure la chaine ASCII du nom, et pas la chaëne ASCIIZ : elle ne prend pas en compte le caractère \0 indicateur de fin de chaëne. Dans la gestion de la mémoire de notre buffer, il faudra en tenir compte.
La pile est une ressource rare et chère dans le noyau. Des structures volumineuses ne devraient pas y être utilisées dans les fonctions, mais il vaut mieux les allouer dynamiquement. Même si dans ce code SIZE_EA_TRANSPORT ne vaut que 0x33, c'est tout de même une bonne habitude à prendre. D'un autre côté, on va fragmenter la mémoire. On n'a rien sans rien. Dans ce projet, je n'ai pas eu l'impression d'utiliser très fréquemment des buffers de taille fixe, alors je n'ai pas mis en place les fonctions basées sur les lookasides. Mais ça peut être une idée d'optimisation à apporter.
Un transport object peut être créé avec le device attributes de \Device\Tcp ou \Device\Udp. Par contre, seuls les transports objects relevant de Tcp pourront être plus tard associés avec un connection object. Du Udp connecté, non mais vraiment, vous n'y pensez pas !
Code à voir : Niveau_TDI : MyTdiCreateTransport
Connection Object
Pour la création du connection object, il faut également une structure extended attributes. Elle sera bien plus simple car elle n'a qu'à encapsuler un seul élément : un contexte. Pour le moment sans utilité, on considèrera qu'il est mis à NULL par l'appelant. L'extended attribute contiendra donc :
- une structure FILE_FULL_EA_INFORMATION
- la chaëne de caractères spécifiant le type d'infos passées dans l'extended attributes
- une structure CONNECTION_CONTEXT qui est un PVOID
La chaëne de caractères s'appellera cette fois TdiConnectionContextet sa taille, toujours en ASCII et pas ASCIIZ, sera TDI_CONNECTION_CONTEXT_LENGTH.
Un transport object ne peut se demander que sur l'object attributes initialisé pour \Device\Tcp.
Code à voir : Niveau_TDI : MyTdiCreateConnection
Association
Maintenant, la première fonction TDI utilisant IoCallDriver va entrer en jeu. Il s'agit en effet de procéder à l'association de ces deux handles. Pas exactement les deux handles : il nous faut le file object du connection object et pas juste son handle retourné par MyTdiCreateConnection. Il faudra donc utiliser ObReferenceObjectByHandle, et garder dans un coin de sa tête qu'au moment de libérer les ressources, un déréférencement s'imposera.
Ce traitement d'IRP n'a pas de completion routine. On n'a pas à se soucier de l'utilisation de IoFreeIrp ou de IoCompleteRequest.
Code à voir : Niveau_TDI : MyTdiAssociateHandles
La fonction inverse, MyTdiDisassociateHandles, devra être appelée avant de pouvoir libérer les objets en toute tranquillité. Son fonctionnement est exactement le même que MyTdiAssociateHandles donc je ne m'étends pas dessus, je vous invite à vérifier vos intuitions dans le code source.
Connexion sortante
Pour notre première connexion, qui sera de type sortante, on prend les mêmes et on recommence. La connexion suit la même architecture que l'association, en mode synchrone, à quelques détails près.
- Il est possible de spécifier un timeout. Lors du traitement de l'IRP par les couches inférieures de tcpip, si il y a dépassement du timeout alors l'IRP sera immédiatement complétée avec comme status 0xc00000b5.
- La destination est identifiée par une structure TDI_CONNECTION_INFORMATION, qui contiendra le TA_IP_ADDRESS donné en paramètres.
En cas d'erreur, je réserve un traitement spécial à quelques cas : le timeout et la connexion refusée dans le cas où le serveur filtre les ip :port autorisés. En cas de succès, un petit test permet de confirmer qu'on est bien connecté sur la destination voulue.
Code à voir : Niveau_TDI : MyTdiConnect
Comme pour la désassociation, la déconnection est quasiment identique. Goto code source.
Emettre et recevoir
Il nous reste à émettre et à recevoir. Comme d'habitude depuis le début de ce chapitre, ces opérations se dérouleront de manière synchrone. Le driver Tcpip fonctionne en mode Direct IO, il y aura donc des MDL au programme. Pour ces fonctions, je partirai du principe que le MDL est alloué par l'appelant. Code à voir : Niveau_TDI : MyTdiSend_s
Pour la réception, un compteur de timeout a été ajouté. C'est bien beau d'écouter, mais si rien n'arrive on peut rester longtemps, très longtemps comme ça. Alors mieux vaut prévoir le timeout ! Celui-ci entre en action lorsque la fonction attend la fin de traitement de l'IRP. Si l'attente se termine pour cause de timeout, on procède alors à une demande d'annulation de l'IRP.
- Enregistrer une fonction d'annulation d'Irp (IoSetCancelRoutine)
- Lancer l'IRP (IoCallDriver)
- Attendre la completion ou le timeout (KeWaitForSingleObject)
- Si l'IRP timeout, demander l'annulation (IoCancelIrp)
- Dans la fonction d'annulation, libérer le spinlock propre aux annulations (IoReleaseCancelSpinLock), puis compléter l'IRP (IoCompleteRequest)
PDRIVER_CANCEL
IoSetCancelRoutine(
IN PIRP Irp,
IN PDRIVER_CANCEL CancelRoutine
);
BOOLEAN
IoCancelIrp(
IN PIRP Irp
);
VOID
IoReleaseCancelSpinLock(
IN KIRQL Irql
);
VOID
IoCompleteRequest(
IN PIRP Irp,
IN CCHAR PriorityBoost
);
Code à voir : Niveau_TDI : MyTdiRecv_s, Niveau_TDI : CancelIrp
3.3 - Et pour le reste ?
Après cette petite présentation, vous devriez être bien à l'aise avec le fonctionnement de base de TDI. MDLs, création d'objets, appel au driver, attente sur event. Je vais juste attirer votre attention sur quelques fonctions qui exploitent des mécanismes particuliers :
Serveur TCP
Placer un port TCP en écoute pour faire office de serveur se fait en plusieurs temps.
- Il faut donc associer une fonction sur l' event TDI_EVENT_CONNECT par un appel à TDI_SET_EVENT_HANDLER.
- Puis placer le connection object en écoute avec un appel à TDI_LISTEN. La pile réseau est désormais avertie que le couple interface : port du transport object est ouvert et accepte les connections de clients.
- l'IRP complète immédiatement.
- Lorsqu'un client pointe le bout de son nez, l'événement TDI_EVENT_CONNECT est activé.
- La fonction de l'event handler pourra alors accepter le client par un appel à TDI_ACCEPT.
Mais il est possible de simplifier le traitement et de se passer de la gestion de l'event. On perd un peu de souplesse car tous les traitements auxquels pouvait procéder l'event handler ne sont plus faisable : test de diverses conditions avant d'accepter le candidat à la connexion par exemple. Cette méthode simplifiée est celle que j'ai retenue. Le port est toujours ouvert en écoute par un appel à TDI_LISTEN. Mais l'IRP ne complète que lorsqu'un client se connecte.
Code à voir : Niveau TDI : MyTdiListenAutoAccept, MyTdiSetEventHandler
UDP
L'utilisation de l'UDP est archi simple. Le tout est de bien garder en tête que les opérations se feront toutes sur le transport object et pas sur le connection object. Normal, il n'y en a pas !
Code à voir : Niveau TDI : MyTdiSendto_s, MyTdiRecvfrom_s
Filtrage
TDI permet de mettre en place ce à quoi beaucoup de princesses ont du rêver dans leur château : un filtre automatique des prétendants. Les fonctions TDI "d'écoute" permettent de définir des critères de sélection permettant un rejet automatique des remote endpoints ne correspondant pas à ces critères, typiquement adresse IP et / ou port. On peut filtrer les candidats à une connection TCP lors d'un TDI_LISTEN, ou bien l'origine des datagrammes que l'on écoute avec TDI_RECEIVE_DATAGRAM. Ces filtres sont archi simples à mettre en place.
Code à voir : Niveau TDI : MyTdiListenAutoacceptFiltre, MyTdiRecvfromFiltre_s
4 - Parlons Winsock
C'est sur ce socle de fonctions TDI que vient se greffer Audi-K, le module de compatibilité WinSock. Si vous n'avez pas encore eu l'occasion de bricoler avec winsock, je vous renvoie à la lecture de différents docs sur le sujet, notamment le classique Beej's Guide.
Audi-K tente de recréer une grande partie des fonctions winsock. Certaines seront impitoyablement laissées de côté : les fonctions WSAGetOption / WSASetOption et les fonctions relatives à la gestion des sockets non bloquants, FD_SET et compagnie. Donc pas de sockets débloqués, pas de mode promiscuous.
Les fonctions de support classiques sont faites : ntoh et hton, itoa et atoi.
Les fonctions de gestion des noms sont implémentées : nom de l'ordinateur local, récupération de ses adresses Ips, résonution DNS d'un nom de domaine et résolution RDNS d'une adresse IP.
4.1 - Kernel Sockets c'est bien, en abuser ça craint !
*DONG DONG DONG* Tiens, c'est l'heure pour un peu d'autodénigrement ? Bah, à *petite dose ca ne peut pas faire de mal ! Qu'est-ce qu'il y a à dénigrer ici ? *Le concept même de Kernel Socket.
Avec des kernel sockets bloquants, on va disposer d'une interface facile à manipuler, mais qui va forcer le driver à travailler non pas comme un driver devrait le faire, mais comme un programme userland banal. Tout simplement parce qu'on va diluer massivement l'efficacité dans de l'attente en bloquant jusqu'à completion des traitements. Pire que de la térébenthine. Or dans le noyau, s'il ne devait y avoir qu'une seule consigne, ça serait celle-ci : NE PAS BLOQUER BORDEL DE MERDE !!! C'est notamment pour cette raison que beaucoup d'articles et de code sur TDI ne regardent pas la création de kernel sockets : ca écarterait l'utilisateur des bonnes pratiques TDI.
Pour un projet utilisant le réseau de manière légère, comme pour établir un canal de communication et de contrôle, alors pas de problème. Mais la méthode bloquante sera atrocement pénalisante pour un projet utilisant le réseau de manière intensive et principale, comme un serveur FTP. Tout ce temps passé à bloquer est du temps perdu !
Avant d'aller plus loin, car on va les faire quand même, ces kernel sockets, quelques mots sur ce que devrait faire un bon driver utilisant TDI.
Premièrement, se passer du mode synchrone et fonctionner de manière asynchrone. Le code source devra proposer les fonctions send et recv TCP implémentées en asynchrone : on ne bloque pas après le IoCallDriver. Pour mémoire, les IRP traitées en asynchrone doivent être non threadées. On ne sait pas quel sera le contexte courant au moment de leur completion, on ne peut pas les laisser se terminer sous le contrôle du IO Manager, et elles doivent donc se nettoyer elles-mêmes dans leur completion routine.
Code à voir : Niveau_TDI : MyTdiSend_a, MyTdiRecv_a, MyTdiCompletion_routine_a
Deuxièmement donc, il faut utiliser les TDI Events. Si on ne bloque pas lors de la fonction, mais que juste après on rentre dans un WaitForXxxxx afin d'être certain que le message est arrivé à destination ou que notre buffer contient la réponse, on ne peut pas vraiment parler d'asynchrone !
Il est possible de définir des fonctions qui seront appelées automatiquement dans certaines situations, typiquement en réponse à une action du remote peer ou en cas d'erreur. On a un peu soulevé ce point en parlant de TDI_LISTEN. Voici la liste des événements utilisables :
Connection et déconnection d'un remote peer
TDI_EVENT_CONNECT
TDI_EVENT_DISCONNECT
Réception TCP
TDI_EVENT_RECEIVE
TDI_EVENT_CHAINED_RECEIVE
TDI_EVENT_RECEIVE_EXPEDITED
TDI_EVENT_CHAINED_RECEIVE_EXPEDITED
Réception UDP
TDI_EVENT_RECEIVE_DATAGRAM
TDI_EVENT_CHAINED_RECEIVE_DATAGRAM
Prêt à envoyer
TDI_EVENT_SEND_POSSIBLE
Remontée d'erreur
TDI_EVENT_ERROR
TDI_EVENT_ERROR_EX
La bonne architecture pour un driver utilisant TDI n'est donc pas de mimer WinSock mais par exemple pour un échange de message :
- Enregistrer une fonction sur l'event TDI_EVENT_RECEIVE
- Envoyer un message avec TDI_SEND. On ne bloque pas, on balance juste la sauce et on laisse TDI se débrouiller. On peut donc passer à autre chose mais SURTOUT on ne touche pas à ce qui a été alloué pour cet envoi : buffer, MDLs On ne libère rien, on ne réutilise rien, on ne touche à rien.
- Lorsqu'un message est envoyé par le remote peer, l'event est déclenché et la fonction appelée
- Cette fonction peut alors faire un appel receive pour récupérer l'intégralité du message. C'est également le gestionnaire d'event qui se charge de la libération des ressources. Le gestionnaire d'event peut poursuivre tout seul la conversation, relançant des TDI_SEND.
Pour une utilisation plus concrète des événements de réception, vous pouvez aller regarder le fonctionnement de query_dns. Bien que la requête DNS soit envoyée de manière synchrone, la réponse n'est pas écoutée via un recvfrom mais via un événement.
Code à voir : Niveau socket : query_dns, ReceiveDnsAnswer
Si on fonctionne en mode asynchrone, l'IRP et les MDL ne seront plus libérées automatiquement par le IO Manager. On ne peut plus la créer en utilisant les macros TDI prévues à cet effet. Voici donc quelques APIs supplémentaires qui peuvent servir :
PIRP
IoAllocateIrp(
IN CCHAR StackSize,
IN BOOLEAN ChargeQuota
);
LONG
KeSetEvent(
IN PRKEVENT Event,
IN KPRIORITY Increment,
IN BOOLEAN Wait
);
VOID
IoFreeIrp(
IN PIRP Irp
);
Un autre avantage à utiliser des IRP asynchrones : la fonction IoAllocateIRP peut tourner jusqu'au niveau IRQL DISPATCH_LEVEL alors que les macros TDI de création d'IRP threadées ne tournent qu'à PASSIVE_LEVEL. Pour pleinement exploiter cet avantage, il faut que tout le reste du code autour de la création de l'IRP puisse également tourner au dessus de Passive Level. Mais avec un code soigneusement prévu dans cette intention, il est possible de se faire des fonctions tournant à un haut IRQL et donc ne pas avoir à utiliser de thread système pour faire baisser l'IRQL.
4.2 - Structures de données, encore
Les fonctions Audi-K reposent sur l'utilisation d'un tableau structure dont l'int manipulé sous le nom sock_fd est un index. Ca se voit bien dans l'utilisation des fonctions WinSock, où le numéro de socket n'a pas du tout la tête d'une adresse comme pour bien des handles, mais commence à zéro ou un, et est incrémenté lorsqu'on demande de nouveaux sockets.
typedef struct _SOCKET_OBJECT
{
//Pour la gestion du socket
int current_status;
//Infos récupérées à la création du socket
int domain;
int type;
//Elements TDI
HANDLE TransportHandle;
PFILE_OBJECT pTransportObject;
HANDLE ConnectionHandle;
PFILE_OBJECT pConnectionObject;
//Gestion du backlog, pour listen(x) avec x > 1
int backlog_is_head;
int backlog_is_element;
int backlog_who_is_my_head;
int backlog_size;
int *backlog_list;
//Utilisé pour shutdown()
int send_forbiden;
int recv_forbiden;
//Pour les connections TCP
int remote_host;
short remote_port;
//Pour fonctionner en asynchrone
async_context contexte;
} SOCKET_OBJECT, *PSOCKET_OBJECT;
La structure globale WSADATA est allouée lors de l'appel à WSAStartup. Diverses informations sont contenues dedans : pointeurs vers les devices objects, pointeur vers le nom de l'ordinateur local, adresse d'un serveur DNS :
typedef struct _WSADATA
{
//Infos pour chacun des protocoles : TCP, UDP et Raw
struct protocol_info
{
UNICODE_STRING usDeviceName;
OBJECT_ATTRIBUTES oaDeviceAttributes;
PDEVICE_OBJECT pDeviceObject;
} Protocol[3];
int in_use;
PSOCKET_OBJECT pskSocketArray;
PDRIVER_OBJECT pDriverObject;
//Adresse des event handlers
PVOID EventDisconnected;
PVOID EventPeerAckDisconnect;
PVOID EventDnsAnswer;
//Valeur du serveur DNS
int serveur_dns;
char* hostname;
} WSADATA, *PWSADATA;
4.3 - Fonctions Audi-K
Pour le reste, je ne vais pas décrire les fonctions Audi-K dans le menu détail. Je vais présenter les grandes lignes des fonctions et la manière dont elles utilisent l'interface TDI. Pour le reste, je vous renvoie au code source pour de plus amples informations.
Je vous donne également quelques pistes d'amélioration qui me trottent dans la tête. Mais je n'ai pas plus de temps à consacrer à Audi-K pour le moment, alors ces belles idées resteront des doux rêves jusqu'à ce que vous les implémentiez si elles vous plaisent.
- L'emplacement dans le tableau des sockets est alloué lors de l'appel à socket().
- Le transport object est alloué lors du bind(), qui peut être implicite lors d'un connect() ou d'un sendto().
- Le connection object est créé lors des appels à connect() ou listen(). Les associations se font dans la foulée.
- La désassociation et fermeture des objets se fait lors d'un close().
- Les fonctions listen() et accept() sont un peu complexes car elles permettent de gérer un backlog de plusieurs connections de clients sur un seul port serveur. Listen() créé l'ensemble des objets pour tout le backlog, et c'est en fait accept() qui fait un appel à TDI_LISTEN.
- L'astuce pour accepter plusieurs clients sur un seul port serveur est d'utiliser un seul objet transport et l'associer à plusieurs objets transport.
- Les fonctions établissant une connection, listen() et connect(), placent un évent de détection de déconnexion par le remote peer.
- Lors de la déconnexion, close() utilise un event pour détecter la confirmation de déconnexion par le remote peer.
- La fonction close() doit se dépatouiller avec les status des sockets au moment de la fermeture, ainsi que l'appartenance d'un socket à un backlog actif ou ayant existé. Pour l'analyse de l'état du backlog, je procède par un parcours de tout le tableau de backlog. Une méthode utilisant des compteurs de référence et des suppressions automatique lorsque les références tombent à zéro pourraient être plus efficaces, notamment pour la gestion du transport object utilisé par un backlog.
- La gestion des sockets UDP "connectés" est supportée. Un socket UDP peut être "connecté" sur un ordinateur distant au moyen de connect(). Il peut alors être utilisé avec les fonctions send() et recv(). Recv() renvoie alors vers recvfrom_filtre afin d'exploiter la possibilité de filtrer les interlocuteurs afin de ne recevoir des datagrammes que de l'ordinateur sur lequel on a connecté.
- Les fonctions de gestion de noms utilisent beaucoup la base de registre. Celle-ci contient les adresses des serveurs DNS, les adresses du localhost sur ses diverses interfaces, et le nom des interfaces.
- Lors de l'appel à gethostbyname() sur le nom du localhost obtenu par gethostname(), un hostent est généré contenant les adresses IP des différentes interfaces. Je n'ai pas extrait le nom des interfaces de la base de registre, mais il pourrait être pratique de les recopier dans le hostent.
- Les fonctions gethostname() et getpeername() reposent principalement sur la fonction de support query_dns(). Celle-ci utilise les fonctions Audi-K UDP afin de procéder aux résolutions DNS et aux reverse DNS. Le code de query_dns utilise un évent pour détecter les réponses du serveur DNS. Celles-ci peuvent être très rapides, plus rapides que compléter la demande et se mettre en écoute ! Placer un event avant d'envoyer la requête permet d'être certain de ne pas louper la réponse. C'est notamment le cas lors de l'utilisation de VmWare qui induit un petit lag : testée sur le système hôte, la méthode sans event fonctionne, mais testée sur le système virtuel, elle ne fonctionne plus !
- Quelques fonctions n'ont pas leur équivalent winsock :
- accept_filter() fonctionne comme accept() mais bénéficie des capacités de filtrage offertes par TDI.
- recvfrom_filter() pour les mêmes raisons.
- Getlocalname() s'utilise comme getpeername() sur un socket TCP connecté. Le sockaddr en retour indique l'IP de l'interface utilisée et le port local qui sert à cette connexion.
- Les fonctions de support comme itoa, atoi, ntohs etc ... ne sont pas dans les fichiers source du niveau_socket mais dans les netutil.
5 - La marque du capitaine crochet
Fun with razors... and TDI
Oh god !
Il est maintenant temps, sur un fond de Velvet Acid Christ, de retourner les tripes de TDI afin de le faire gémir ! Je parle bien d'attaquer directement TDI. Il ne s'agit pas d'une mise en situation d'Audi-K.
Je prévoyais de mettre en place quelques hooks pour monter un sniffer TDI. Mais finalement cette idée ne me plait pas trop. Le mécanisme des events, le fait qu'un event ne reçoit pas forcément le message entier mais doit parfois procéder à des recv pour avoir la suite, tout ceci rend la réalisation d'un sniffer TDI très lourdingue. Ce projet devrait migrer vers NDIS pour être plus simple à réaliser. J'ai par contre trouvé une utilisation divertissante du hook.
Que serait le monde sans Google ? Voilà ce que je vous propose de faire ! Plonger un ordinateur dans un monde de ténêbres et d'obscurantisme médiéval ! Plus de gmail, plus de google news, plus rien que le silence. Pis encore que le silence : une réponse inattendue et méprisante. Et pourquoi pas remplacer Google par Yahoo ? (Mes excuses aux utilisateurs de Yahoo, il me faut une tête de turc répondant à certains critères)
Comme vous vous en doutez, tout ça va se baser sur des manipulations de DNS. Internet Explorer, Firefox, tout et n'importe quoi en Userland se repose sur les fonctions de résolution DNS fournies par Windows, typiquement gethostbyname en WinSock. Cette application peut être plus discrète avec les fonctions réseau évoluées où il suffit d'indiquer une URI à la fonction pour rapatrier le résultat. J'ai un blanc, mais vous voyez ce que je veux dire.
Les programmes développés par des petits malins peuvent, comme Audi-K, recomposer les requêtes DNS à la main en UDP.
Mais dans le noyau et vu par TDI, tout ça revient (presque) au même : demander à un transport object d'envoyer un buffer mappé en MDL à destination d'un TA_IP_ADDRESS dont le port est 53. Voilà un point d'interception unique qui semble intéressant !
J'explique le (presque) de tout à l'heure, ou plutôt je vais décrire des symptômes observés : cette méthode marche dans mon XP virtuel, sur un autre poste XP doté de McAfee, mais pas sur un 3eme XP doté d'Avast! Allez un peu de franchise: je n'ai pas cherché plus loin. Mon code actuel change les adresses des fonctions majeures du driver object en Top de Stack et pas directement de TcpIp, et je n'ai même pas essayé de voir si ça changeait quelque chose. C'est donc un "article dons vous êtes le héro", et si vous voulez vous attaquer à ce point, allez en <76>.
Ensuite, si un programme passe par un proxy, typiquement le navigateur Web, on ne verra qu'une seule résolution DNS : le nom du proxy. Et encore, si ca n'est pas directement l'IP qui est configurée dans le browser. Ensuite, le navigateur ne fait que parler au proxy. On ne verra pas l'historique des navigations. Par contre s'il n'y a pas de proxy, c'est tout bon.
Le moteur de hook est simple, basé sur le changement d'adresses des fonctions dans le tableau IRP_MJ du driver object. Les fonctions do_the_hook_stuff() et do_the_unhook_stuff() font l'affaire. Cette méthode est plus que connue, je ne vais pas entrer dans les détails.
- Allocation d'un driver object en variable globale, et renseignement de son tableau IRP_MJ en recopiant les adresses contenues dans le tableau IRP_MJ du driver object de TcpIp.
- Renseignement du tableau IRP_MJ du driver object de TcpIp avec l'adresse d'un handler générique
- Traitement spécial réservé au handler de l'IRP IRP_MJ_INTERNAL_DEVICE_CONTROL vu que c'est elle qui fait tout le travail. J'y place l'adresse de ma fonction dispatch_device_io_control()
C'est justement dispatch_device_io_control qui va faire le petit tour de magie, rendant google invisible:
- Accéder au stack location courant
- vérifier que la minor function est 09 (send datagram)
- maper les paramètres de l'irp dans une structure pTdiRequest
- celle-ci contient une structure TA_IP_ADDRESS décrivant la destination
- je veux parler à un port distant 53 ?
Hahaaaa on a intercepté ce qui semble très fortement être une demande de résolution DNS / RDNS ! Accéder au buffer est facile : il suffit de calculer l'adresse en mémoire à partir de l'adresse de page et de l'offset dans la page depuis le champ MDL de l'IRP.
Que peut-on faire avec ces requêtes ?
- Les logger, pour surveiller l'activité DNS de l'ordinateur. Ca peut mettre en évidence le fait par exemple que tel peogramme, tel jeu, l'air de rien, tente de connecter le serveur de l'éditeur pour faire je ne sais quoi. Vérifier si il y a une nouvelle version peut-être ?
- Si je souhaite être moins permissif, un test sur le nom que l'ordinateur veut résoudre permet de droper des requêtes. Au lieu de faire un : return OriginalTdiDriverObject.MajorFunction[pIoStackLocation->MajorFunction] (pDeviceObject, pIrp); un return STATUS_UNSUCCESSFUL; bloquera l'émission de la requête DNS. Vous vouliez rendre tout google inaccessible ? Si la chaine du nom à résoudre contient la sous-chaine "google", alors drop ! (toute référence à un geste sportif d'une quelconque coupe de monde est purement fortuite. Ce genre d'événements a déjà trop tendance à envahir les médias pour que je m'y mette ! Non aux OPA lancées sur nos esprits !)
- Le remplacement à la volée de la requête me pose encore des problèmes. J'ai bien tenté la chose suivante : si demande de www.google.com, faire en fait résoudre www.yahoo.fr. C'est très con et ca ne sert à rien. Mais en plus ça ne marche pas très bien. Il reste une solution plus brutale et moins polivalente : écraser le nom dans la requête d'origine... si et seulement si le nouveau nom a la même taille que l'ancien !
L'écrasement du buffer à la volée est le plus fun. Mais le moins flexible. Pourquoi se limiter à avoir le même nom ? J'ai cherché à contourner la difficulté mais je suis arrivé à ces conlusions :
- je ne suis pas maëtre de la chaëne MDL
- je ne suis pas maëtre du (des) buffer(s) décrits dans cette chaëne
- si je remplace le MDL de l'IRP par un MDL custom, décrivant un buffer custom contenant une requête custom, je ne le maëtrise plus
- c'est l'aveuglette totale
- les IRP provenant de résolutions DNS "classiques" venant de gethostbyname par exemple marhceront toujours pareil. Je pourrais reverser ce qui se passe avant l'appel à send datagram, regarder la completion routine, le callback, et me faire une idée de commenr ça marche. Je serais alors en mesure de nettoyer proprement la MDL, et vraisemblablement le buffer. Je m'arrangerais pour que ma propre MDL et mon propre buffer soient bien nettoyées par le mécanisme que je parasite. Ca peut marcher nickel...
- Jusqu'au jour où je tombe sur un send datagram procédant à une résolution DNS codée à la main par un utilisateur. Le buffer ne se désalloue plus, il est en mémoire globale dans son projet par exemple. Alors mon buffer n'est plus libété automatiquement et ma mémoire fuit. En plus je tente de libérer le buffer d'origine alors qu'il n'a pas été alloué dynamiquement. Catastrophe !
- Moralité : pas bon !
- Autre solution : droper la requête d'origine et envoyer une nouvelle. Hmmmmmmm alors il faut que je colle sur MA requête la même completion routine, et le même TDI event. Donc il faut que j'espionne les créations d'event pour tous les transports objects sur le device UDP du système afin de l'avoir sous le coude. Ben oui, comme on l'a vu pour les fonctions DNS d'Audi-K, la réception de la réponse DNS doit passer par un event sinon on va la louper si le poste rame, typiquement un VMWARE. Les résolutions DNS de Windows ne plantent pas sous VMWARE, c'est donc qu'elles passent par un event de réception de datagramme. Solution trop lourdingue, ca ne me plait pas.
- Encore une approche qui semble sympa : droper la demande initiale mais envoyer un nouvel appel à send datagram avec un socket custom dans une nouvelle IRP mais sur le même transport object. Du coup on récupère l'event sans même s'en soucier. Reste à gérer la completion routine. Mais là encore, je ne suis pas maëtre de trop de choses : la completion routine nettoie-t-elle des buffers que je souhaite voir rester en mémoire ? Catastrophe !
Même en tentant des approches différentes (changer la MDL dans l'IRP, envoyer carrément tout une nouvelle requête DNS à partir de rien, renvoyer une requête DNS sur le même transport object), on se retrouve dans la même situation : on ne maëtrise PAS des facteurs systèmes de la requête d'origine, et on ne maëtrisera PLUS nos propres facteurs systèmes. Les facteurs systèmes, c'est le buffer, son allocation, sa libération, le MDL seul ou en chaëne...
Voilà pourquoi il faut que la chaëne résolue de remplacement et la chaëne résolue parasite aient la même taille. Et dans ces conditions ça fonctionne parfaitement et de manière très stable.
6 - Conclusion
Et voilà c'est terminé. Je regrette de vous refiler Audi-K comme ça, un peu à poil. Je prévoyais de faire quelques trucs fun avec histoire d'ajouter du piment. Malheureusement, je manque un peu de temps en ce moment, alors ça sera pour une prochaine fois. Mais les idées ne manquent pas : comme l'envoi à distance de clefs de registre (HKLM\SAM\SAM\Domains\Account\Users ?), de pages mémoire du noyau ou de process utilisateur, la construction d'un mini-kernel shell...
J'ai laissé la grosse fonction faisant tourner les jeux de test dans le source new_tdi. A défaut de donner des exemples concrets d'utilisation, cette fonction permet de montrer les diverses utilisations de l'ensemble des fonctions d'Audi-K
7 - Références
Articles :
- Trip in da TCPIP http://ivanlef0u.free.fr/?p=60
- Driver Development Part 5: Introduction to the Transport Device Interface http://www.codeproject.com/system/driverdev5asp.asp
- Kernel mode sockets library for the masses http://www.rootkit.com/newsread.php?newsid=416
- High performance kernel mode web server for Windows http://www.acc.umu.se/~bosse/High%20performance%20kernel%20mode%20web%20server%20for%20Windows.pdf
- NDIS and TDI Hooking, Part II http://www.rootkit.com/newsread.php?newsid=253
- Answers to TDI Frequently Asked Questions http://www.pcausa.com/resources/tdifaq.htm
- Windows Driver Kit: Network Devices and Protocols : TDI Drivers http://msdn2.microsoft.com/en-us/library/aa505007.aspx
- DNS Query - Retrieve IPv4 address of hostnames http://www.codeproject.com/internet/dns_query.asp
Livres :
Chacun de ces livres sur la programmation système et noyau sur Windows comporte une partie, plus ou moins bien détaillée, de la programmation TDI.
- Rootkits: Subverting the Windows Kernel
- Professional Rootkits
A bientôt pour de nouvelles aventures !
Code:
begin 644 DNSpy.tar.gz
M'XL("./\$$<``RYF<BXQ-C@U-"XP+D1.4W!Y+G1A<@#L/&MSTTBV\]54\1^:
M;`$VXR2V\R`DP)1C*XD7/X0?<+E`J12I[6B0):%'2&:6_[K?[F1_Q3VGNR6U
...
M@V<9%>P@\;M&,5^@!;>-/$<8E<F<(18HOR+!]+_6G_5G_5E_UI_U9_U9?]:?
M]6?]67_6G_5G_5E_UI_U9_U9?]:?]6?]67_6G_5G_5E_UI\_X.?_#V;&\[D`
#V`0`
`
end