Ecrivez votre premier shellcode en asm x86 !

Une application pratique de la programmation en binaire.

Cet article se veut la suite logique de l’écriture de votre premier programme en langage d’assemblage intel x86 sous Linux. Au travers de ces écrits, nous allons étudier, parmi la multitude de cas pratiques d’utilisation du langage d’assemblage (ou "binaire"), l’écriture de shellcodes.

Il convient de définir ce terme anglais et technique. Nous pouvons voir que le terme shellcode est constitué de deux mots : shell et code. Avis aux amateurs et utilisateurs de systèmes d’exploitation type UNIX : nous parlons bien du même "shell", de l’interface système de votre distribution Linux favorite, de "l’invite de commande". Et puis nous avons le terme code. Ici, il désigne du code machine, du code exécutable.

Mais alors, à quoi correspond exactement le terme "shellcode" ?

Dans le domaine des sciences de l’information et des systèmes, s’il y a bien une ressource qui se veut fiable, c’est bien la célèbre encyclopédie Wikipédia. Citons une partie de sa page sur les shellcodes :

Un shellcode est une chaîne de caractères qui représente un code binaire exécutable. À l’origine destiné à lancer un shell (’/bin/sh’ sous Unix ou command.com sous DOS et Microsoft Windows par exemple), le mot a évolué pour désigner tout code malicieux (et souvent malveillant) qui détourne un programme de son exécution normale. Un shellcode peut être utilisé par un hacker voulant avoir accès à la ligne de commande.

http://fr.wikipedia.org/wiki/Shellcode

En réalité, le terme "malicieux" n’a pas sa place ici et il s’agit d’un abus de langage causé par le faux-ami malicious qui lui veut bien dire malveillant.

Mais, stop ! Un shellcode est un code malveillant ? Je ne mange pas de ce pain-là ! Zeste de Savoir ne devrait recenser aucune ressource utile à la conception de codes malveillants !

Tout comme il est possible de faire des choses désastreuses avec des shellcodes, il est tout autant possible de faire des choses désastreuses avec un langage de programmation. Ce n’est pas l’outil qui est mauvais en soi, mais bien l’utilisation qui en est faite.

Ainsi, bien que la technique d’écriture du shellcode ait été conçue afin d’exploiter le flux d’exécution d’un binaire que nous avons préalablement exploité, il est tout à fait possible de faire un shellcode qui, par exemple, ne se contente que d’afficher un "Hello world" à l’écran. Auquel cas ça n’aurait plus vraiment la définition de shellcode, mais nous garderons ce terme pour des raisons de simplicité.

Ainsi, nous définirons par shellcode une séquence binaire autonome destinée à exécuter du code dans un environnement inconnu. Le binaire qu’un hacker exploite est l’environnement inconnu, destiné à exécuter le shellcode une fois la vulnérabilité effectuée.

Si nous faisons l’analogie avec le monde du vivant, un shellcode est ni plus ni moins qu’un acaryote (une cellule sans noyau) qui a besoin d’une cellule hôte (avec noyau) pour se développer. Je n’ai pas fait le rapprochement avec un virus (qui lui aussi a besoin d’une cellule hôte pour se développer), car un virus se reproduit, se propage. Ce qui n’est pas le cas d’un shellcode. :)

Des exemples valent mieux qu’un long discours. C’est pourquoi nous commencerons en douceur par le traditionnel "Hello world". Et enfin, nous verrons le cas d’un shellcode réaliste qui exécute une invite de commande et est apprécié dans son utilisation concrète..

Exemple 1 : "Hello world!"

Pour ceux qui se souviennent de mon premier article, nous avons écrit un "Hello World" en binaire pour plateforme intel x86. La version qui nous intéresse plus particulièrement est celle avec les appels systèmes.

En effet, un code binaire autonome aura plus de facilité à "appeler le service 4 de l’interruption 0x80" (4 = sys_write, ne l’oubliez pas) qu’à "récupérer l’adresse de printf". Je m’explique.

Dans le format ELF, il existe d’autres sections que la section .text (qui contient par définition notre code exécutable) ou la section .data (qui contient par définition les données statiques de notre programme). L’une d’elles se nomme par définition .plt, pour Procedure Linkage Table. Cette section regroupe l’ensemble des fonctions que nous voulons "importer" de bibliothèques partagées. Par exemple, un programme compilé avec les options par défaut sous gcc et utilisant l’appel printf va stocker dans la section .plt l’adresse de la fonction printf, elle-même véritablement localisée dans la bibliothèque partagée libc.so.6.

Il serait fastidieux (mais pas impossible) de récupérer l’adresse de cette fonction dans notre shellcode. Il faudrait aller chercher des données à certaines adresses, elles-mêmes correspondantes à des adresses mémoires… Cela représente du temps de programmation et du temps de test. C’est beaucoup ! Surtout si nous voulons simplement afficher une phrase a l’écran. C’est pour ça que l’utilisation directe d’un appel système est toute indiquée ! :D

Vous l’aurez compris : la première contrainte d’un shellcode est d’être simple. Ce n’est pas un programme à part entière, simplement une série d’instructions dites "arbitraires" puisqu’elles sont exécutées généralement à l’insu du programme dans lequel elles ont été injectées.

Un shellcode a d’autres contraintes. Si vous vous souvenez de notre exemple avec les appels systèmes, la chaîne de caractères que nous voulions afficher était à une adresse fixe, dans une autre section. Ici, notre shellcode aura pour contrainte d’embarquer notre chaîne et de récupérer son adresse. Sachant qu’un véritable shellcode ne sait pas à quelle adresse il est exécuté. Nous voilà bien embêtés ! :D

Il existe cependant une technique géniale pour aboutir à ce genre de chose : jmp-call-pop. Je l’expliquerai en même temps que je vous fournirai l’exemple ; sachez juste qu’elle permet grossièrement de récupérer l’adresse d’une donnée intéressante.

Une troisième contrainte non universelle cependant : certains shellcodes sont injectés au travers d’une fonction qui, par exemple, manipule un buffer qui ne doit pas contenir d’octets nuls (de "null-bytes", ou les fameux \0 en C).

Et si je devais ajouter une quatrième contrainte : faites en sorte que votre shellcode soit le plus petit possible. Dans la majorité des cas vous l’injecterez dans un buffer potentiellement limité, ou à travers une trame réseau où il faut tabler sur un petit espace mémoire. Parfois ces contraintes sont absentes, mais il reste cependant intéressant de ne pas faire trop gros non plus. ;)

En résumé…

Un shellcode "de base" doit :

  • être simple : ne pas faire des pirouettes inutiles, aller droit au but. Ce n’est pas un programme, mais une série d’instructions arbitraires ;
  • ne pas dépendre de l’environnement courant : il doit embarquer ses propres données et savoir les manipuler ;
  • répondre à certaines contraintes de caractères interdits : on se retrouve parfois dans l’impossibilité d’injecter des null-bytes dans un buffer, par exemple ;
  • être le plus compact possible : il se peut que tout notre shellcode ne soit pas copié en mémoire, et là c’est le drame !

J’attire une dernière fois votre attention sur le deuxième : certains shellcodes peuvent dépendre de l’environnement dans le cadre d’une attaque ciblée sur un binaire particulier, où l’environnement est déjà connu. Sachez avant tout qu’il s’agit d’un cas spécifique où, généralement, les attaquants ont besoin de faire des manipulations spéciales qui n’auraient pas lieu d’être dans d’autres cas (réutiliser des variables, manipuler la disposition de la pile d’exécution, etc.)

Les explications sont passées. On va enfin pouvoir présenter notre premier shellcode qui se contente de faire un Hello World en asm x86 !

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
 % cat helloworld/helloworld.asm 
bits 32

helloworld_shellcode:
    ; On réinitialise les registres pour éviter les soucis
    xor eax, eax
    xor ebx, ebx
    xor ecx, ecx
    xor edx, edx

    ; L'appel système sera le numéro 4
    mov al, 4 ; utiliser al revient à ne pas laisser de null-bytes dans le shellcode
    ; Si on avait utilisé eax, qui est un registre 32 bits, alors la valeur 4
    ; aurait été considérée comme une valeur sur 32 bits, à savoir 0x00000004...
    ; Or on ne veut aucun null-byte !

    ; On écrit sur la sortie standard
    mov bl, 1 ; bl et non ebx, pour les mêmes raisons que ci-dessus

    jmp helloworld_string ; ici intervient le jmp-call-pop.
    ; On jump à l'étiquette "helloworld_string" ... (1)

helloworld_shellcode_next:
    ; (2) Au sommet de la pile, on a l'adresse de la chaîne
    ; "Hello World\n". On peut la dépiler et la mettre dans
    ; ecx, qui contient le buffer à afficher !
    pop ecx ; cette instruction n'utilise pas de null-byte non plus ! :)

    ; On met la taille des données à afficher dans dl, soit 13
    mov dl, 13

    ; Nos registres sont initialisés !
    ; On peut appeler l'interruption 0x80
    int 0x80

    ; On va maintenant terminer le programme.
    ; l'appel système sera le numéro 1.
    xor eax, eax ; eax = 0
    inc eax ; incrémente eax, donc eax = 1 

    xor ebx, ebx ; le code de retour sera zéro

    ; On peut appeler une seconde fois l'interruption 0x80
    int 0x80 

helloworld_string:
    ; (1) ... Nous nous retrouvons ici. Nous faisons ensuite
    ; un Call sur l'étiquette "helloworld_string_next". Le
    ; call va empiler l'adresse mémoire qui pointe sur nos données
    ; "Hello World\n", à savoir l'adresse de retour. Et ce même si ce
    ; n'est pas du code exécutable que nous avons ici... (2)
    call helloworld_shellcode_next
    db `Hello world!\n`

Les explications sont données dans les commentaires. Néanmoins pour ceux qui auraient du mal à visualiser la technique du jmp-call-pop, des schémas seront les bienvenus.

Nous sommes à l’instruction :

1
jmp helloworld_string

Et la pile ressemble à…

1
2
3
4
5
+---------------------------------+ <-- esp
|     ... On ne sait pas          |
|     vraiment ce qu'il y a       |
|    en fait, et on s'en fout !   |
+---------------------------------+ <-- ebp

En vérité, on peut le savoir si on débogue notre programme, mais cela ne nous intéresse absolument pas.

En revanche, ce qui nous intéresse, c’est le flux d’exécution après avoir exécuté le jmp sur l’étiquette indiquée. Nous allons en effet exécuter ce code :

1
2
3
helloworld_string:
    call helloworld_shellcode_next
    db `Hello world!\n`

L’instruction call helloworld_shellcode_next va rediriger le flux d’exécution sur l’étiquette indiquée, mais en plus d’un simple jmp, va déposer sur la pile l’adresse de l’instruction suivante.

Ici, nous n’avons pas vraiment d’instruction suivante, puisque nous avons des données. Mais notre shellcode "croit" qu’il s’agit en fait d’instructions à exécuter, puisque nous avons embarqué nos données au sein de notre shellcode. Après l’exécution de ladite instruction, la pile ressemble à ça :

1
2
3
4
5
6
7
8
+---------------------------------+ <-- esp
| Adresse de retour (qui pointe   |
|      sur "Hello World\n")       |
+---------------------------------+ 
|     ... On ne sait pas          |
|     vraiment ce qu'il y a       |
|    en fait, et on s'en fout !   |
+---------------------------------+ <-- ebp

On a réussi à récupérer l’adresse mémoire qui pointe sur notre chaîne, et ce indépendamment du contexte d’exécution. Rappelez-vous, un shellcode ne sait pas à quelle adresse de base il s’exécute. C’est pour ça qu’il doit ruser. Le fait de faire un "call" permet de savoir à quelle adresse nous nous trouvons, puisqu’il suffit de lire l’adresse déposée sur le sommet de la pile d’exécution pour le savoir ! :)

Il ne nous reste plus qu’à récupérer cette fameuse adresse "calculée" à partir de la technique "jmp-call-pop". Ainsi, l’instruction :

1
pop ecx

Va récupérer la donnée au sommet de la pile (à savoir notre adresse mémoire qui pointe sur notre "Hello world") et incrémenter le pointeur esp en conséquence, ainsi, la pile ressemblera à ça au final :

1
2
3
4
5
6
7
8
+---------------------------------+ 
| Adresse de retour (qui pointe   |
|      sur "Hello World\n")       |
+---------------------------------+ <-- esp
|     ... On ne sait pas          |
|     vraiment ce qu'il y a       |
|    en fait, et on s'en fout !   |
+---------------------------------+ <-- ebp

Mais cela n’a plus d’importance pour nous. Ce qui importe, c’est d’initialiser nos registres comme il faut, souvenez-vous ! :)

Passé cette explication, nous allons assembler notre shellcode et le tester :

1
% nasm -f bin helloworld/helloworld.asm -o helloworld/helloworld.bin

On spécifie bien le format bin car nous voulons du binaire pur, et non un ELF.

Si nous affichons le contenu de notre shellcode avec l’outil hexdump, voici ce que nous obtenons :

1
2
3
4
5
 % hexdump -C helloworld/helloworld.bin
00000000  31 c0 31 db 31 c9 31 d2  b0 04 b3 01 eb 0c 59 b2  |1.1.1.1.......Y.|
00000010  0d cd 80 31 c0 40 31 db  cd 80 e8 ef ff ff ff 48  |...1.@1........H|
00000020  65 6c 6c 6f 20 77 6f 72  6c 64 21 0a              |ello world!.|
0000002c

Aucun null-byte ? (00) Alors on est bon ! Il ne manque plus qu’à le tester. Pour cela, nous allons nous servir d’un programme simple, écrit en C, qui va recevoir notre shellcode sur la ligne de commande :

Contenu du fichier testshellcode.c :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

int main(int argc, char** argv) {
    if(argc < 2) {
        printf("Usage: %s <shellcode>\n", argv[0]);
        exit(EXIT_FAILURE);
    }

    printf("Size: %d bytes.\n", strlen(argv[1]));
    void (*shellcode)() = (void((*)())) (argv[1]);

    shellcode();

    return EXIT_SUCCESS;
}

Puisqu’il ne contient pas de null-bytes, il est possible de calculer sa taille avec la fonction standard strlen. Nous le faisons à des fins de statistiques.

Ensuite, nous déclarons un pointeur de fonction qui va pointer sur notre shellcode stocké dans argv[1] et l’exécuter ! :)

Compilez le programme avec l’option -m32 (32 bits oblige) et -z execstack afin que la pile d’exécution puisse contenir des données exécutables. En effet, les arguments de la ligne de commande se situent dans la pile d’exécution, et notre shellcode se situe dans la ligne de commande… En résumé, notre shellcode se situe dans la pile d’exécution, qui est une zone non exécutable de base.

1
 % gcc -o testshellcode testshellcode.c -m32 -z execstack

Et vient ensuite le moment décisif…

1
2
3
 % ./testshellcode `cat helloworld/helloworld.bin`        
Size: 36 bytes.
Hello world!

HOURRA ! :D Notre shellcode fonctionne du feu de dieu !

Et si on attaquait un exemple concret, maintenant ? :pirate:

Exemple 2 : execve(/bin/sh)

Dans ce shellcode, l’objectif sera d’appeler l’appel système sys_execve avec les paramètres suivants : /bin/sh, NULL (pas d’argument nécessaire) et NULL (pas d’environnement nécessaire non plus).

La seule difficulté réside à référencer la fameuse donnée /bin/sh au sein de notre shellcode. Je vous ai évidemment montré la technique jmp-call-pop, mais ça n’est pas la seule qui existe.

En effet, les shellcodes que vous trouverez sur le net auront tendance à déposer sur la pile des octets de sorte que //bin/sh se retrouve sur la pile, suivi d’un null-byte puisque l’appel système sys_execve attend une chaîne à la C comme précisé dans le manuel (man execve) :

1
2
3
4
       #include <unistd.h>

       int execve(const char *filename, char *const argv[],
                  char *const envp[]);

Le shellcode que nous ferons va donc d’abord empiler n/sh puis //bi, de manière à avoir sur la pile :

1
2
//bi
n/sh

Le fait d’empiler les valeurs quatre octets par quatre octets est que nous sommes dans un fonctionnement d’une architecture x86, à savoir 32-bits. La pile est alignée sur quatre octets de fait. Ainsi, le fait d’utiliser les micro-instructions push et pop nous fera manipuler les octets quatre par quatre.

Si nous empilons un, deux ou trois octets dans nos instructions, des zéros de bourrage seront rajoutés ; ce qui aura pour effet d’insérer des null-bytes dans notre shellcode. Chose que nous ne souhaitons pas.

Ainsi, //bin/sh est une chaîne dont la taille (8) est un multiple de 4. On est bon !

Et donc une chaîne de caractère cohérente et contiguë en mémoire. Et puisqu’elle sera au-dessus de la pile, il sera naturellement facile de récupérer son adresse. :)

Il ne faudra pas oublier d’empiler avant cela un null-byte afin que notre chaîne construite soit correctement terminée.

Récupérons le numéro d’appel système de sys_execve :

1
2
 % cat /usr/include/asm/unistd_32.h | grep execve
#define __NR_execve              11

Il s’agit de l’appel système numéro 11. On construit notre shellcode ? :)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
bits 32

shellcode:
    ; On réinitialise les registres
    xor eax, eax
    xor ebx, ebx
    xor ecx, ecx
    xor edx, edx

    ; Appel système 11
    mov al, 11

    ; ebx doit contenir un pointeur vers //bin/sh
    ; donc on construit la chaîne sur la pile

    push ebx ; ebx = 0, donc on a notre null-byte
    push `n/sh`
    push `//bi`

    ; A ce moment là, esp pointe sur `//bin/sh\0`
    mov ebx, esp ; on met dans ebx l'adresse de notre chaîne.

    ; ecx et edx valent déjà zéro (NULL)
    ; donc nous sommes tranquilles :)

    ; On exécute l'appel système
    int 0x80

    ; Et on quitte proprement
    ; (cf. shellcode Hello World)
    mov al, 1
    xor ebx, ebx
    int 0x80

On assemble notre shellcode et on le teste grâce au programme C fourni lors de l’illustration de l’exemple "Hello world" :

1
2
3
4
5
6
7
8
 % nasm -f bin execve_binsh/execve_binsh.asm -o execve_binsh/execve_binsh.bin
 % ./testshellcode `cat execve_binsh/execve_binsh.bin`
Size: 31 bytes.
$ echo "Coucou, je suis /bin/sh"   
Coucou, je suis /bin/sh 
$ ls
execve_binsh  helloworld  testshellcode  testshellcode.c
$ exit

Enfin ! :ninja: Nous avons créé un shellcode qui a un véritable intérêt. L’article touche désormais à sa fin !

Et pour la suite ?

Nous avons vu le b.a.-ba de l’écriture des shellcodes. Sachez qu’il existe des shellcodes plus robustes, parmi lesquels :

  • les shellcodes à code auto-modifiant : ceux-ci possèdent un "stub", ou un petit morceau de code destiné à déchiffrer le shellcode à la volée pour ensuite l’exécuter. L’intérêt d’avoir un shellcode chiffré est de contourner certains mécanismes de protection (caractères interdits, présence de /bin/sh dans le flux, …) ;
  • les shellcodes alphanumériques : des shellcodes uniquement composés des lettres de l’alphabet et des chiffres de 0 à 9. Cette contrainte fait qu’ils sont plus longs, mais sont plus difficiles à détecter. :)

La route est longue, mais la voie est libre. La conception de shellcodes n’a de limite que votre imagination.


34 commentaires

Merci pour cet excellent article. :)
Il me semble juste qu'il y a une petite coquille dans cette phrase :

Et enfin, nous verrons le cas d'un shellcode réaliste qui exécute une invite de commande et apprécié dans son utilisation concrète.

+0 -0

@Taurre : c'est le shellcode qui est apprécié dans son utilisation concrète, quoi. La petite coquille, c'est le shellcode dont tu parles. ( :D )

Ge0

Le pire c'est que la boutade n'était pas voulue. :p

Ce n'est pas Français, ce que j'ai dit ? :(

Ge0

Pour moi, non. :-°
Quand je lis la fin de la phrase, je ne comprend pas à quoi elle se rapporte. Plus précisémment, j'ai bien l'intuition que tu fais allusion au shellcode, mais la construction ne me semble pas correcte (je ne sais pas dire exactement ce qui fait défaut, mais cela ne va pas).

Sinon, maintenant que je le vois, ce n'est pas un invite de commande ?

+0 -0

Je formulerais ainsi :

Et enfin, nous verrons le cas d'un shellcode réaliste qui exécute une invite de commande et est apprécié dans son utilisation concrète.

+0 -0

J'ai quelques questions !

  • Dans quelles circonstances doit-on rendre la pile d'exécution exécutable dans la vraie vie ? C'est une belle source de vulnérabilité si on en croit l'article !
  • Pourquoi un programme "normal" (hors compilateur, interpréteur …) accepterait-il du code en entrée ?
  • A quoi peut-bien servir cette technique autrement que pour se protéger du piratage (et pour se divertir) ?

J'ai une question sur le jump-call-pop, n'est-ce pas plus simple de recalculer l'adresse à partir du compteur programme et d'un offset ? A moins qu'on n'ait pas d'accès direct au pc sur x86 ?

Natalya

Ce n'est pas possible avec le jeu d'instruction x86-32 bits, mais ça l'est avec le 64 bits.

J'ai une question sur le jump-call-pop, n'est-ce pas plus simple de recalculer l'adresse à partir du compteur programme et d'un offset ? A moins qu'on n'ait pas d'accès direct au pc sur x86 ?

Natalya

On n'a pas directement accès au pc sur x86. Tu ne peux pas faire :

1
mov eip, address ; FAUX

En revanche, tu peux faire :

1
2
push address
ret

Puisque l'instruction ret va dépiler la valeur au sommet de la pile et la placer dans eip. ^^

De manière plus générale… Le shellcode ne sait pas à partir d'où il s'exécute, pour faire simple. Et l'instruction call, une fois assemblée, produira la série d'opcodes suivant : e8 xx xx xx xx où les xx ne représentent non pas une adresse, mais un offset.

Cette instruction va déposer la valeur de retour sur la pile d'exécution ; cette valeur de retour peut correspondre au pointeur de programme qui a été calculé par ton processeur à partir de "la prochaine instruction à exécuter". Je ne sais pas si j'ai été clair ! :)

J'ai quelques questions !

  • Dans quelles circonstances doit-on rendre la pile d'exécution exécutable dans la vraie vie ? C'est une belle source de vulnérabilité si on en croit l'article !

Pour tout un tas de raisons. Si tu veux faire un programme qui a une bonne raison d'accueillir du code dans la pile d'exécution pour l'exécuter, c'est déjà une raison suffisante. :)

En fait, les préoccupations en ce qui concerne la sécurité des OS sont bien plus fortes qu'il y a 20 ans. Prends l'exemple de Windows XP SP1 : tout était exécutable en mémoire, que ce soit la pile, les données, … Et puis il se sont améliorés avec XP SP2 en introduisant le paramètre "DEP" pour "Data Execution Prevention". Plus d'infos ici : http://fr.wikipedia.org/wiki/Data_Execution_Prevention

  • Pourquoi un programme "normal" (hors compilateur, interpréteur …) accepterait-il du code en entrée ?

Imagine une porte dérobée, un rootkit, un cheval de troie qui attend des instructions de l'extérieur… Certes il ne s'agit pas d'un exemple "trivial" mais si la question est de ne pas exécuter de code dans la pile, le programme peut avoir recours à d'autres techniques (trouver une zone en mémoire à la fois inscriptible (w) et exécutable (e)).

  • A quoi peut-bien servir cette technique autrement que pour se protéger du piratage (et pour se divertir) ?

Aabu

De quelle technique parles-tu ? Les shellcodes ne permettent pas de se protéger du piratage (pas à mon sens tout du moins). Ils permettent d'exécuter du code au sein d'un processus "hôte".

Edit :

Ce dont mewtwo parle, c'est l'adressage relatif possible sur architectures x64 : https://www.tortall.net/projects/yasm/manual/html/nasm-effaddr.html

@Coyote : le sous-titre est trop vague pour ce que vaut l'article. :)

+1 -0

Ouais donc en gros un shellcode n'est ni plus ni moins qu'une simple fonction injectée dans un code host si je comprend bien.

+0 -1

C'est envisageable d'intervertir le titre et le sous-titre ?
Si je me mets dans la peau d'un débutant ou d'un nouveau, sur la home le titre actuel fait un peu bourrin. :euh:

Coyote

Je pense également que ça augmenterait la visibilité de l'article. Les trois mots-clés "asm", "shellcode" et "x86" risquent de faire peur aux gens qui pourraient pourtant tout à fait comprendre ce qui se passe dans cet article et les précédents.

+1 -0

Super article ! Merci beaucoup.

Cependant, je me retrouvais avec des null-bytes dans mon shellcode. Après quelques recherches il s'avère que l'utilisation du jmp se fait par défaut sur 16 bits. jmp short règle le problème vu que notre code est bien court, peut-être pourrais-tu ajouter une petite note à ce sujet ?

(Et corriger le code si ce n'était pas volontaire)

Ouais donc en gros un shellcode n'est ni plus ni moins qu'une simple fonction injectée dans un code host si je comprend bien.

shaynox

Si tu définis par fonction un morceau de code qui prend des paramètres en entrée et qui retourne une valeur en sortie, non, ce n'est pas tout à fait ça.

Pense plutôt à une série d'instructions que le programme exécute à son insu.

Super article ! Merci beaucoup.

Cependant, je me retrouvais avec des null-bytes dans mon shellcode. Après quelques recherches il s'avère que l'utilisation du jmp se fait par défaut sur 16 bits. jmp short règle le problème vu que notre code est bien court, peut-être pourrais-tu ajouter une petite note à ce sujet ?

(Et corriger le code si ce n'était pas volontaire)

PainKetchup

Cela signifie que l'espace entre ton instruction jmp et ton label est supérieur à 127 (0x7f) ou inférieur à -128 (0x80). C'est pour cela que l'offset sera converti en valeur longue contenant des null-bytes.

nasm fait implicitement la différence.

Une fonction void func(void); donc ^^

+1 -1

Enfaite je suis un peu ronchon car je n'aime pas vraiment les terminologies from Linux :p

+0 -2

Très intéressant les shellcodes !

Merci pour cette suite d'article, mais ça commence à faire beaucoup d'introductions. Pourquoi ne pas en profiter pour entrer un peu plus en profondeur ? Je pense par exemple à placer un shellcode via un buffer overflow, puis passer en root. Montrer les dangers que ça implique et comment s'en immuniser.

+0 -0

Le problème avec ce genre d'initiative, il n'y a jamais d'explication de comment s'en protéger (comme ici) que de comment exploiter des fails, alors le prétexte de s'en servir pour s'en protéger hum hum (troll off) :p

+0 -3
Connectez-vous pour pouvoir poster un message.
Connexion

Pas encore membre ?

Créez un compte en une minute pour profiter pleinement de toutes les fonctionnalités de Zeste de Savoir. Ici, tout est gratuit et sans publicité.
Créer un compte