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:
$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
movabs 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
's/.*#//p' $< > $@ sed -n
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 :
$<
qui désigne la première dépendance de la règle (ici le fichier.s
),$@
qui désigne la cible (ici le fichier.hex
).
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 :
- Elles ne produiront pas de fichier, nous devrions donc en faire
dépendre
.PHONY
, - 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
$< -al -o /dev/null
as
%-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).