Un makefile pour produire des shellcodes

Divulgâchage : Plutôt que de se souvenir (et réécrire) les commandes nécessaires pour assembler et injecter les shellcode, pourquoi pas déléguer tout ça à un makefile ?

Pendant l’écriture du livre sur les shellcodes, nous en avons écrit beaucoup, dont plein de variantes différentes pour voir lesquelles seraient les plus pédagogiques et, bien sûr, nous avons du tester tout ça très régulièrement pour vérifier qu’ils fonctionnent effectivement comme prévu.

Le livre contient la démarche à suivre pour passer du code source des shellcodes à leur version injectable avec quelques commandes. Elles sont relativement simples mais c’est toujours plus agréable lorsque les tâches répétitives sont automatisées.

Et puisque le livre utilisait les outils de compilation classiques, nous disposions de make et sa capacité à produire des fichiers pourvu qu’on lui fournisse les recettes à appliquer. Et comme on a trouvé le résultat bien pratique bien pratique, on va voulu le partager avec vous.

Pour commencer, on vous conseil de créer une première recette dans votre fichier makefile pour la cible all comme suit :

all:

Comme ça, vous savez qu’en lançant make sans argument, c’est cette cible qui sera construite. Si vous voulez qu’elle fasse quelque chose, vous pouvez lui ajouter une recette ou des dépendances ici ou plus loin dans le fichier.

Exemple de shellcode

Il y a plein de façons d’écrire des shellcodes. Certains auteurs vous les fournissent sous formes de chaînes de caractères (où ils sont exprimés en hexadécimal) et d’autres le font en assembleur (syntaxe Intel ou AT&T).

Dans notre livre, nous avons choisi de les écrire en assembleur (syntaxe AT&T) et d’afficher leur traduction hexadécimale en vis à vis (en commentaire). C’est plus pratique pour en parler et comprendre leur traduction en binaire.

Ce n’est sûrement pas le plus impressionnant de tous, mais voici ce que ça donne avec arthurdent.s, un shellcode qui se termine avec un code d’erreur 42 (cf. page 44).

ArthurDent:
    movabs $0x3c, %rax   # 48 B8 3C 00 00 00 00 00 00 00
    movabs $0x2a, %rdi   # 48 BF 2a 00 00 00 00 00 00 00
    syscall              # 0F 05

Produire le fichier injectable

Via l’hexadécimal

L’avantage de mettre la traduction hexadécimal en commentaire, et d’utiliser le caractère dièse pour ça, c’est qu’on peut extraire l’hexadécimal automatiquement avec une règle sed (cf. page 21) qu’on peut inscrire dans une recette générique comme suit :

%.hex: %.s
    sed -n 's/.*#//p' $< > $@

Cette règle dit qu’à partir de n’importe quel fichier se terminant par .s (nos codes assembleurs), on peut produire un même fichier mais d’extension .hex en utilisant la commande Bash sur la ligne suivante.

Cette règle utilise deux variables automatiques de make :

Une fois le code hexadécimal extrait du fichier, on peut le convertir en binaire avec xxd et, encore une fois, la recette s’écrit simplement.

%.bin: %.hex
    xxd -r -p $< > $@

Avec ces deux règles, nous pouvons convertir simplement n’importe quel fichier source de nos shellcodes en fichier binaire injectable. Voici un exemple pour le shellcode d’exemple :

$ make arthurdent.bin
sed -n 's/.*#//p' arthurdent.s > arthurdent.hex
xxd -r -p arthurdent.hex > arthurdent.bin
rm arthurdent.hex

Lorsque nous avons demandé à make de produire le fichier, il s’est débrouillé tout seul pour trouver et exécuter les recettes nécessaires (sed puis xxd). Et comme il s’agit de règles génériques, il supprime même les fichiers temporaires.

Via l’assembleur

Les deux recettes précédentes nécessitent qu’on ait fourni la traduction en hexadécimal mais comme on n’a pas toujours envie de faire ce travail, voici les deux recettes suivantes pour produire le shellcode directement à partir du code assembleur (cf. page 24).

%.o: %.s
    as -o $@ $^
    
%.raw: %.o
    objdump -j .text -O binary $< $@

La première recette produit un fichier objet contenant le code assemblé par gas et la seconde extrait le code des instructions pour produire un fichier injectable.

Nettoyer

Pour éviter de polluer le répertoire de travail avec tous ces fichiers produits par nos recettes, nous ajoutons une recette de nettoyage, traditionnellement appelée clean comme suit :

clean:
    rm -f *.o *.raw *.hex *.bin
    
.PHONY: clean

Les deux premières lignes définissent la recette (supprimer les fichiers d’après leurs extensions). La dernière ligne est un peu spéciale, elle dit à make de considérer la cible comme n’étant pas un fichier mais un simple mot clé (sans cette ligne, si vous créez un fichier clean plus récent que vos shellcodes, make n’exécutera jamais le nettoyage).

Variante pour Windows

Pour rester au plus simple, le livre proposes d’utiliser WinLibs pour compiler les chargeurs et les shellcodes en gardant les mêmes outils (gcc, gas, objcopy, …). Les recette pour produire les shellcodes via l’assembleur restent compatibles mais celles via l’hexadécimal doivent être remplacées.

La commande PowerShell pour extraire l’hexadécimal (cf. page 196) ne peut pas être écrite directement dans le makefile car make n’aime pas PowerShell (qui le lui rend bien).

Nous l’avons donc écrite dans un fichier de script powershell grepsed.ps1 en l’adaptant pour qu’il puisse prendre des paramètre en ligne de commande.

Select-String -Path $args[0] -Pattern "#"         `
| foreach-object {([string]$_).split("#")[1] }    `
> $args[1]

De cette façon, la recette pour produire le fichier est exprimable avec make : elle lui dit de lancer powershell avec le script précédent.

%.hex: %.s
    powershell grepsed.ps1 $< $@

La recette pour produire le fichier injectable à partir de l’hexadécimal n’est pas compliquée, on remplace xxd par certutil.exe en changeant quelques paramètres.

%.bin: %.hex
    certutil.exe -decodehex $< $@

Injecter automatiquement

Les règles précédentes sont largement suffisantes pour produire les fichiers injectables à partir des codes sources du livre. Mais quitte à en être ici, pourquoi ne pas automatiser aussi l’injection du shellcode ?

Compiler le chargeur

Pour pouvoir injecter le shellcode, il faut disposer d’un chargeur opérationnel. Les règles sont on ne peut plus classiques :

loader: loader.c
    gcc -o loader loader.c
    
cleanall: clean
    rm -f loader
    
.PHONY: cleanall

Exécuter le shellcode

Il nous reste à écrire des règles pour exécuter nos shellcodes en les injectant dans notre chargeur et ces recettes sont un peu particulières :

  1. Elles ne produiront pas de fichier, nous devrions donc en faire dépendre .PHONY,
  2. Les règles dont dépend .PHONY ne peuvent pas être génériques et cette solution ne marche donc pas…

Nous utilisons donc un truc pour forcer make à exécuter ces recettes, qu’importe si un fichier correspondant à la cible existe ou pas. Ce truc consiste à créer une cible qui ne dépend de personne et n’a pas de recette (ici .FORCE) et d’en faire dépendre les recettes que nous voulons forcer.

.FORCE:

%-bin: %.bin loader .FORCE
    ./loader $<

%-raw: %.raw loader .FORCE
    ./loader $<

Avec ces règles, on peut alors facilement relancer un shellcode lorsqu’on l’a mis à jour (ou lorsqu’on a mis à jours le chargeur) comme suit :

$ make arthurdent-bin
gcc -o loader loader.c
sed -n 's/.*#//p' arthurdent.s > arthurdent.hex
xxd -r -p arthurdent.hex > arthurdent.bin
./loader arthurdent.bin
make: *** [makefile:33 : arthurdent] Erreur 42
rm arthurdent.bin arthurdent.hex

L’erreur 42 en avant dernière règle est le comportement que nous attendions ici. Le shellcode a été exécuté par le chargeur et s’est terminé avec le statu 42. Cette commande ayant été lancée par make, c’est lui qui récupère le code de retour. Comme il est sensé le faire, il l’interprète comme une erreur et nous en informe (et c’est ce qu’on voulait).

Et après ?

Rien ne vous empêche d’adapter et d’étendre ce makefile. Personnellement, nous lui avons ajouté ces recettes.

%-gas: %.s .FORCE
    as $< -al -o /dev/null
    
%-objd: %.o .FORCE
    objdump -d $<
    
%-strace: % loader .FORCE
    strace ./loader $<

Les deux premières affichent l‘assemblage fait par gas (cf. page 22) et le désassemblage fait par objdump (cf. page 23). La troisième n’est pas dans le livre, elle utilise strace pour afficher la liste des appels systèmes faits par le chargeur puis le shellcode (pratique pour les déboguer).