Partie 3 - Fonctionnalités de base (bis)
Utiliser les hooks comme un pro đȘ
Si vous nâavez pas la mĂ©moire trop courte, vous devriez vous rappeler de la maniĂšre dont on avait utilisĂ© un hook. Pour rappel on avait fait un truc du genre :
1
p.hook(0x40113f, hook_atoi,5)
Cela permettait de hooker lâinstruction call atoi
(de taille 5 octets) afin que lâon puisse en modifier le comportement via Python. Cependant il y a dâautres cas dâusage dans lesquels on peut utiliser les hooks.
Par exemple, vous avez dĂ» remarquer que lorsquâun programme utilise puts
ou printf
, on ne voit jamais la chaßne de caractÚre affichée directement. Essayons de modifier ce comportement avec un hook grùce aux SimProcedures
afin de toujours afficher le contenu de puts
.
DĂ©finir son propre hook via SimProcedure
Les SimProcedures
permettent de faire du hooking avancé en ayant, par exemple, facilement accÚs aux arguments de la fonction hookée.
Faisons un simple programme C qui réalise des appels successifs à puts
:
1
2
3
4
5
6
7
8
9
10
#include <stdio.h>
int main()
{
puts("Salut");
puts("tout");
puts("le");
puts("monde !");
return 0;
}
Si on exĂ©cute le programme avec angr jusquâau return
, on ne verra pas les chaĂźnes de caractĂšres dans notre terminal ( sauf si on trifouille dans state.posix.dumps(sys.stdout.fileno())
pour avoir accĂšs Ă lâoutput).
Avant de balancer tout le code en question, analysons ensemble le bout de code de hook afin de bien le comprendre :
1
2
3
4
5
class MyPuts(angr.SimProcedure):
def run(self, addr_str):
#(...)
p.hook_symbol('puts', MyPuts())
Dans les arguments de
hook
ethook_symbol
, lorsquâune classe dĂ©rivĂ©e deSimProcedure
est utilisĂ©e, il faut absolument mettre les parenthĂšses sinon angr risque dâĂȘtre ⊠angry đ
Pour pouvoir utiliser les SimProcedures
, il faut toujours déclarer sa classe dérivée de la sorte : MaClasse(angr.SimProcedure)
. Cela permet par la suite dâavoir accĂšs Ă certaines fonctions prĂ©-Ă©tablies telle que run
qui est la fonction exécutée lorsque notre hook sera déclenché.
Maintenant, il va bien falloir remplir cette fonction run
, quâallons-nous mettre ?
Bah câest simple on a quâĂ faire
print(addr_str)
đ
Bien tentĂ© mais cela ne fonctionnera pas ! En fait il faut voir lâargument addr_str
comme lâargument de puts
en C. Or lâargument de puts
est une chaßne de caractÚre, plus précisément, un pointeur vers une zone mémoire contenant des caractÚres dont la fin est signalée par un octet nul.
Il va donc falloir bricoler un peu pour rĂ©cupĂ©rer la chaĂźne de caractĂšres Ă lâadresse addr_str
. Rien de bien méchant, une boucle for
et le tour est joué :
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class MyPuts(angr.SimProcedure):
def run(self, addr_str):
string = ""
# Récupération de la chaßne de caractÚre
# en lisant octet par octet
for i in range(1000) :
val = self.state.memory.load(addr_str+i,1).concrete_value
# Fin de la chaĂźne
if val == 0 :
break
string += chr(val)
# Affichage de la chaĂźne
print(string)
return 0
Quelques remarques :
- On a accĂšs Ă lâĂ©tat courant via
self.state
- On utilise le fameux
state.memory.load
pour lire en mĂ©moire et rĂ©cupĂ©rer les bytes de donnĂ©es Ă lâadresseaddr_str[i]
dans la boucle - Lorsque lâon arrive Ă lâoctet nul, câest la fin de la chaĂźne de caractĂšres
range(1000)
est utilisé comme garde-fou pour ne pas tourner en rond indéfiniment
Le script final est celui-ci (attention Ă modifier lâadresse du return
par celle de votre programme) :
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
import angr
# Initialisation du projet, Ă©tat initial ...
p = angr.Project("./exe")
main = p.loader.find_symbol("main")
state_0 = p.factory.blank_state(addr= main.rebased_addr)
sm = p.factory.simulation_manager(state_0)
class MyPuts(angr.SimProcedure):
def run(self, addr_str):
string = ""
# Récupération de la chaßne de caractÚre
for i in range(1000) :
val = self.state.memory.load(addr_str+i,1).concrete_value
# Fin de la chaĂźne
if val == 0 :
break
string += chr(val)
# Affichage de la chaĂźne
print(string)
return 0
p.hook_symbol('puts', MyPuts())
# Adresse du 'return'
sm.explore( find = 0x401193)
En exécutant ce script on voit bien dans le terminal lex chaßnes de caractÚres attendues :
1
2
3
4
Salut
tout
le
monde !
Définir un hook avec un décorateur
Il est possible dâutiliser un dĂ©corateur Python pour dĂ©finir un hook. Par exemple, pour le hook que lâon a dĂ©jĂ vu :
1
p.hook(0x40113f, hook_atoi,5)
Il est possible de faire :
1
2
3
@project.hook(0x40113f, length=5)
def hook_atoi(state):
# (...)
Cela permet de dĂ©finir le hook en mĂȘme temps que la fonction associĂ©e. Câest un peu plus joli et câest plus lisible quand on lit le script.
Il est possible de dĂ©finir plusieurs hooks en utilisant plusieurs dĂ©corateurs autour du hook associĂ©. Cela est utile lorsquâune fonction hookĂ©e est appelĂ©e Ă de maintes reprises dans le programme :
1
2
3
4
@project.hook(0x40113f, length=5)
@project.hook(0x409795, length=5)
def hook_atoi(state):
# (...)
Une histoire de symboles
Hooker des fonctions de la libc est chose aisée car :
- soit angr le fait dĂ©jĂ
- soit on a accĂšs au symbole (et donc on peut rĂ©cupĂ©rer lâadresse de la fonction via son nom) que le programme soit strippĂ© ou non
Toutefois, lorsque le programme est strippĂ© (les symboles des fonctions internes sont supprimĂ©s), on a plus accĂšs au nom des fonctions internes. MĂȘme le main
nâest plus accessible directement via son symbole avec main = p.loader.find_symbol("main")
đą.
Dans une telle situation, lorsque lâon veut hooker une fonction fun_prgrm
du programme, on a deux maniĂšres de faire :
- Soit on sait exactement oĂč est appelĂ©e cette fonction et il suffit de hooker toutes les instructions du type :
call fun_prgrm
- Soit on ne sait pas oĂč cela est fait et il va falloir hooker toute la fonction
On a déjà été confronté au premier cas, et on sait gérer. Mais comment faire alors si on se retrouve dans le second cas ?
Dans le second cas, il y a deux maniĂšres de faire :
- Utiliser un hook classique : câest laborieux car il faut calculer la taille de la fonction, sortir de la fonction nous mĂȘme en modifiant
rip
avec la valeur idoine đ„± âŠ - Utiliser une classe dĂ©rivĂ©e de
SimProcedure
: il sâagit de la mĂ©thode la plus simple car on nâaura pas besoin de calculer la taille de la fonction ni mĂȘme besoin de retourner nous-mĂȘme ; angr le fait dĂ©jĂ pour nous
Utiliser un hook classique
La premiĂšre mĂ©thode peut ĂȘtre intĂ©ressante dans le cas oĂč on veut modifier le comportement dâun gros bloc de code qui nâest pas une fonction appelĂ©e. Par exemple, si vous arrivez Ă identifier un bout de code qui fait de la dĂ©tection anti-debug, sleep ou qui nâest pas trĂšs intĂ©ressant, vous pouvez simplement le hooker avec une fonction qui ne fait rien ( cela revient Ă âNOPerâ tout le bout de code).
Exemple :
1
2
3
4
# NOPer plusieurs instructions
@p.hook(adresse_de_depart, length=taille_totale_des_instructions)
def nop(state):
print("NOP")
Utiliser une classe dérivée de SimProcedure
La condition pour utiliser cette mĂ©thode est seulement de savoir oĂč se situe la fonction que lâon souhaite hooker (appelons-la fun_prgrm
). Ensuite on utilise une classe dérivée de SimProcedure
et cette derniĂšre se chargera toute seule de retourner comme il faut.
Par exemple, si fun_prgrm
est située à 0x401149
, on peut faire :
1
2
3
4
5
6
7
class MyFunc(angr.SimProcedure):
def run(self):
print("'fun_prgrm' hookée")
# (...)
return
p.hook(0x401149, MyFunc())
Les limites dâangr
AprĂšs avoir vu les principales fonctionnalitĂ©s quâoffre angr, vous vous dites sĂ»rement que vous allez pouvoir enfin dĂ©molir tous les crackmes et reverse bien plus aisĂ©ment nâimporte quel programme. Eh bien malheureusement ce nâest pas aussi simple que cela car angr a tout de mĂȘme pas mal de limitation đ«Ł âŠ
Moteur dâexĂ©cution codĂ© en Python đ
Lâune des faiblesse majeure dâangr face Ă dâautres outils dâexĂ©cution symbolique tels que Triton ou Binsec est quâil est codĂ© totalement en Python.
Ainsi, mĂȘme le moteur dâexĂ©cution est codĂ© en Python contrairement Ă dâautres outils dont le Python est simplement un wrapper pour en faciliter lâutilisation.
Python câest chouette, câest simple mais quâest-ce que câest lent ^^â !
Lâexplosion de chemin đ„
On en a briĂšvement parlĂ© mais il sâagit dâun des plus gros problĂšmes de lâexĂ©cution symbolique. Cela ne concerne pas seulement angr mais nâimporte quel moteur dâexĂ©cution symbolique.
Prenons un exemple concret pour voir comment va rĂ©agir angr lors dâune explosion de chemin.
Voici le code C que nous allons utiliser :
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
#include <stdio.h>
int un()
{
return 1;
}
int zero()
{
return 0;
}
int main()
{
unsigned char data[16];
printf("Enter 16 bytes of data: ");
fread(data, sizeof(unsigned char), 16, stdin);
for (int i = 0; i < 16; i++)
{
for (int j = 7; j >= 0; j--)
{
if ((data[i] >> j) & 1)
{
un();
}
else
{
zero();
}
}
}
return 0;
}
Il sâagit dâun code assez simple, son fonctionnement devrait vous ĂȘtre facile Ă comprendre.
Compilons-le avec gcc main.c -o exe
. Maintenant, lançons angr sur le programme en lui donnant une adresse inatteignable lors de lâexploration :
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
import angr
import IPython
import claripy
import os
import signal
# Code pour pouvoir ouvrir IPython
# avec Ctrl+C
def kill():
current_pid = os.getpid()
os.kill(current_pid, signal.SIGTERM)
def sigint_handler(signum, frame):
print('Kill with : kill()')
IPython.embed()
signal.signal(signal.SIGINT, sigint_handler)
p = angr.Project("./exe")
flag = claripy.BVS('flag', 16*8)
# On utilise 'rebased_addr' car le programme est compilé
# avec la protection PIE
main = p.loader.find_symbol("main")
state_0 = p.factory.blank_state(addr= main.rebased_addr,stdin=flag)
sm = p.factory.simulation_manager(state_0)
# Adresse inatteignable
print("Exploration en cours depuis le main")
sm.explore( find = 0xdeadbeef)
Plusieurs remarques :
- Le programme que lâon vient de compiler nâest pas strippĂ© donc on a accĂšs Ă tous les symboles, dont le symbole
main
viap.loader.find_symbol("main")
- Comme on a compilĂ© le programme sans lâoption
-no-pie
, lemain
est Ă lâoffset0x11a7
. Mais lors de lâexĂ©cution, il sera exĂ©cutĂ© Ă une adresse alĂ©atoire du type :adresse_de_base_aleatoire + 0x11a7
, par exemple :0x00005555555551af
. Ainsi, on utilisemain.rebased_addr
pour ne pas avoir Ă se prĂ©occuper du PIE - On insĂšre le bout de code permettant dâouvrir IPython avec
Ctrl+C
, cela nous sera utile !
En lançant le script Python, on constate quâil consomme de plus en plus de mĂ©moire. Initialement on a : Puis aprĂšs quelques secondes / minutes dâexĂ©cution :
On constate de que le script consomme Ă©normĂ©ment de mĂ©moire et comme on a pas envie que le PC finisse par freeze đ„¶, on utilise lâarme fatale du Ctrl+C đ«.
Un terminal IPython sâouvre alors et on peut analyser ce quâil se passe. Essayons de voir ce que contient le simulation manager qui, pour rappel, gĂšre tous les Ă©tats.
On voit que 1410 Ă©tats sont actifs, ce qui est Ă©norme ! DĂ©jĂ quand on en a plus dâune centaine faut commencer Ă se poser des questions, mais lĂ câest beaucoup trop !
Je vous conseille dâen finir avec le script en saisissant kill()
dans le terminal IPython pour libérer les gigas de RAM occupées par le script.
Cet exemple vous permet de comprendre la principale limite de lâexĂ©cution symbolique Ă travers lâexplosion de chemins.
Les bibliothĂšques externes
Une autre faiblesse dâangr est quâil gĂšre mal les bibliothĂšques un peu complexes. Autant pour la libc certaines fonctions comme printf
, read
etc., ça, il sait faire. Autant des fonctions comme celles de lâAPI Windows, il galĂšre davantage đ€.
De ce fait, lorsque lâon analyse un programme Windows avec angr (par exemple, un malware), il va falloir hooker pas mal de fonction pour que le script nâaille pas dans les choux.
Cela ne veut pas dire quâangr ne peut pas sâexĂ©cuter sur un programme Windows, câest juste quâil va falloir faire plus attention et faire plus dâanalyse sur le code en amont avant dâentamer un script avec angr.
Je vous rassure, angr ne sert pas QUE pour la rĂ©solution de crackme. Il peut ĂȘtre utilisĂ© pour dĂ©sobfusquer certains programmes. On peut mentionner Ă ce titre la dĂ©sobfuscation des switch tables pour VM Protect.
Quand utiliser angr ?
Pour conclure, je vous propose de lister les cas dans lesquels il peut ĂȘtre intĂ©ressant/facile dâutiliser angr et, a contrario, les cas dans lesquels ce nâest pas forcĂ©ment la meilleure idĂ©e.
Evidemment, câest une liste assez subjective et ce nâest pas parce que lâon a classĂ© un cas dans ceux oĂč il faut Ă©viter dâutiliser angr que câest une vĂ©ritĂ© absolu.
Dans lâidĂ©al il sâagit de regarder au cas par cas lâobjectif attendu et la maniĂšre dont est conçu le binaire ( programme, firmware âŠ) Ă analyser.
Les cas favorables â
- Un crackme qui utilise un algo assez linĂ©aire avec des opĂ©rations simples (xor, add,sub âŠ)
- Un programme Linux : oui angr a un peu plus de mal avec les programmes Windows ( notamment les bibliothÚques utilisées)
- Un bout dâassembleur : cela peut ĂȘtre une fonction ou simplement un bout de code dĂ©sassemblĂ© dont vous souhaitez comprendre le fonctionnment. angr permet en effet de charger directement de lâassembleur et de lâexĂ©cuter.
- DĂ©sobfuscation classique : sachez quâil est possible de dĂ©sobfusquer de maniĂšre efficace un programme avec angr. Cela demandera peut-ĂȘtre des notions avancĂ©es mais angr dispose dâun panel dâoutils qui, utilisĂ©s ensemble, peuvent permettre de dĂ©sobfusquer un programme. Cela Ă©tant, on parle ici dâobfuscation classique (switch table linĂ©aire, prĂ©dicats opaques, MBA âŠ) et pas dâobfuscation poussĂ©e (switch tables non linĂ©aires, nĂ©cessitĂ© dâexĂ©cuter en dynamique âŠ)
Les cas dĂ©favorables â ïž
- Les programmes Windows : cf la raison plus haut. Evidemment cela ne veut pas dire quâil nâest pas possible dâutiliser angr sur un malware (et câest parfois utile dâailleurs), mais câest juste quâil va falloir faire attention Ă la maniĂšre dont vous configurez angr.
- Un programme qui fait trop souvent appel Ă des fonctions externes : typiquement les programmes Windows qui font 1000 appels aux fonctions de lâAPI Windows
- Programmes fortement obfusquĂ©s avec de lâobfuscation trĂšs poussĂ©e
- Programmes qui utilisent de la crypto state of the art ( câest pas demain quâangr va casser AES :) )