Cet article a pour but d’introduire le lecteur au développement de plugins pour radare2 afin d’automatiser certaines tâches de rétro-ingénierie ou de recherche de vulnérabilités.
1. Une introduction au scripting de radare2
1.1 Installation
Étant donné que radare2 est en perpétuelle évolution, il est recommandé de l’installer directement depuis le dépôt git :
$ git clone https://github.com/radare/radare2.git
La compilation et l’installation se font ensuite via le script sys/install.sh.
Afin de simplifier le processus de mise à jour, ce script n’installera que des liens symboliques vers les binaires générés sur le système de fichier cible. Ainsi, il suffira d’exécuter les commandes suivantes afin de mettre a jour une installation :
$ git pull origin master:master
$ make
1.2 L'aide de radare2
Comme tout outil de reverse engineering digne de ce nom, radare2 (parfois nommé simplement r2) propose un nombre incalculable de commandes (désassemblage, analyse, débogage, etc.). Toutes sont documentées et accessibles depuis la ligne de commandes. La commande ? nous permet d’y accéder :
[0x00000000]> ?
Usage: [.][times][cmd][~grep][@[@iter]addr!size][|>pipe] ; ...
Append '?' to any char command to get detailed help
Prefix with number to repeat command N times (f.ex: 3x)
|%var =valueAlias for 'env' command
| *[?] off[=[0x]value] Pointer read/write data/values (see ?v, wx, wv)
| (macro arg0 arg1) Manage scripting macros
| .[?] [-|(m)|f|!sh|cmd] Define macro or load r2, cparse or rlang file
| =[?] [cmd] Send/Listen for Remote Commands (rap://, http://, <fd>)
| /[?] Search for bytes, regexps, patterns, ..
| ![?] [cmd] Run given command as in system(3)
| #[?] !lang [..] Hashbang to run an rlang script
| a[?] Analysis commands
| b[?] Display or change the block size
| c[?] [arg] Compare block with given data
| C[?] Code metadata (comments, format, hints, ..)
| d[?] Debugger commands
| e[?] [a[=b]] List/get/set config evaluable vars
| f[?] [name][sz][at] Add flag at current address
| g[?] [arg] Generate shellcodes with r_egg
| i[?] [file] Get info about opened file from r_bin
| k[?] [sdb-query] Run sdb-query. see k? for help, 'k *', 'k **' ...
| L[?] [-] [plugin] list, unload load r2 plugins
| m[?] Mountpoints commands
| o[?] [file] ([offset]) Open file at optional address
| p[?] [len] Print current block with format and length
| P[?] Project management utilities
| q[?] [ret] Quit program with a return value
| r[?] [len] Resize file
| s[?] [addr] Seek to address (also for '0x', '0x1' == 's 0x1')
| S[?] Io section manipulation information
| t[?] Types, noreturn, signatures, C parser and more
| T[?] [-] [num|msg] Text log utility
| u[?] uname/undo seek/write
| V Visual mode (V! = panels, VV = fcngraph, VVV = callgraph)
| w[?] [str] Multiple write operations
| x[?] [len] Alias for 'px' (print hexadecimal)
| y[?] [len] [[[@]addr Yank/paste bytes from/to memory
| z[?] Zignatures management
| ?[??][expr] Help or evaluate math expression
| ?$? Show available '$' variables and aliases
| ?@? Misc help for '@' (seek), '~' (grep) (see ~??)
Les commandes de r2 sont classifiées en plusieurs sous-types, la première lettre de la commande permet de définir sa catégorie : d comme debug, p comme print, w comme write, et ainsi de suite.
Il est possible d'obtenir plus de détails au sujet d'une commande en la suffixant d'un ?.
Ainsi la commande d? nous permettra d'obtenir plus d‘informations sur les commandes de type debug.
[0x00000000]> d?
|Usage: d # Debug commands
| db[?] Breakpoints commands
| dbt[?] Display backtrace based on dbg.btdepth and dbg.btalgo
| dc[?] Continue execution
| dd[?] File descriptors (!fd in r1)
| de[-sc] [rwx] [rm] [e] Debug with ESIL (see de?)
| dg <file> Generate a core-file (WIP)
| dH [handler] Transplant process to a new handler
| di[?] Show debugger backend information (See dh)
| dk[?] List, send, get, set, signal handlers of child
| dL [handler] List or set debugger handler
| dm[?] Show memory maps
| do[?] Open process (reload, alias for 'oo')
| doo[args] Reopen in debugger mode with args (alias for 'ood')
| dp[?] List, attach to process or thread id
| dr[?] Cpu registers
| ds[?] Step, over, source line
| dt[?] Display instruction traces (dtr=reset)
| dw <pid> Block prompt until pid dies
| dx[?] Inject and run code on target process (See gs)
De la même manière, on peut ainsi récupérer encore plus de détails au sujet des sous-commandes associées.
Il est aussi possible de demander à radare2 de fournir récursivement toute l'aide ou uniquement celle correspondant à une sous-commande donnée, via l’opérateur ?* :
- ?* permettra de récupérer l’intégralité de l'aide ;
- d?* permettra de récupérer toute l'aide des sous-commandes de d.
Radare2 fournit aussi un grep interne (~) afin de filtrer la sortie d'une commande. On pourra par exemple l'utiliser entre autres pour faire de la recherche dans l'aide de radare2.
Ainsi si on souhaite chercher les commandes liées aux gadgets ROP, par exemple :
[0x7feb2f4827d1]> ?*~+gadgets
| /R [grepopcode] search for matching ROP gadgets, semicolon-separated
|Usage: /R Search for ROP gadgets
| /R [filter-by-string] Show gadgets
| /R/ [filter-by-regexp] Show gadgets [regular expression]
| /Rl [filter-by-string] Show gadgets in a linear manner
| /R/l [filter-by-regexp] Show gadgets in a linear manner [regular expression]
| /Rk [select-by-class] Query stored ROP gadgets
|Usage: /Rk Query stored ROP gadgets
| /Rk [nop|mov|const|arithm|arithm_ct] Show gadgets
| /Rkq List Gadgets offsets
Plus d'informations au sujet du grep interne de radare2 sont disponibles via la commande ~?.
Il est également possible d'utiliser le caractère pipe (|) afin de rediriger la sortie standard d'une commande vers n’importe quelle commande shell. Avec les chevrons (> ou >>), il est possible de rediriger vers un fichier.
1.3 Commandes de base
Dans cette partie, nous allons aborder les commandes de base de r2. Ces commandes pourront aussi bien être utiles pour une analyse statique et manuelle que dans le cadre de développement d'outils.
1.3.1 Information sur le binaire
Lors de l'analyse d'un binaire, nous serons amenés à récupérer un certain nombre de métadonnées telles que l'architecture utilisée, le nombre de bits, la liste des imports, la liste des relocations, etc. Toutes ces informations sont disponibles via la commande i.
On pourra par exemple récupérer l'adresse du point d’entrée du binaire grâce à la commande ie :
[0x00402c59]> ie
[Entrypoints]
vaddr=0x004049a0 paddr=0x000049a0 baddr=0x00400000 laddr=0x00000000 haddr=0x00000018 type=program
1 entrypoints
Ou encore afficher ses informations générales avec la commande iI :
[0x00402c59]> iI
arch x86
binsz 124726
bintype elf
bits 64
canary true
class ELF64
crypto false
endian little
havecode true
intrp /lib64/ld-linux-x86-64.so.2
lang c
linenum false
lsyms false
machine AMD x86-64 architecture
maxopsz 16
minopsz 1
nx true
os linux
pcalign 0
pic false
relocs false
relro partial
rpath NONE
static false
stripped true
subsys linux
va true
La commande i permet également d’obtenir des informations liées aux symboles utilisés par le binaire, en effet, les commandes iE, ii, is, iS et ir permettront respectivement d'afficher la liste des exports, imports, symboles, sections et relocations du binaire en cours d'analyse.
1.3.2 La commande seek
Une des premières commandes à connaître de radare2 est la commande seek (s), cette commande va nous permettre de nous déplacer dans le fichier aux adresses/symboles de notre choix.
[0x00400430]> s main
[0x00400526]>
En plus des paramètres de base (registres, flag, valeur), s accepte aussi bien des opérations mathématiques que des accès mémoire :
$ r2 -d /bin/ls .bashrc
[0x7f4f9ecc1ed0]> dcu main
[0x564b8fec2c10]> dr rsi
0x7fff3c570948
[0x564b8fec2c10]> s [rsi + 8]
[0x7fff3c572331]>
Il est intéressant de savoir que radare2 propose aussi un moyen de se déplacer de manière temporaire avant d’exécuter une commande. Il suffit de préfixer les commandes d'un @ suivi d'une adresse, d’un flag, d’une expression mathématique ou encore un d’accès mémoire.
1.3.3 Les flags
Les flags de radare2 peuvent être perçus comme un système de marque-page, ils permettent d'associer une chaîne de caractères (un flag) à un offset.
Il est possible de regrouper des ensembles de flags dans des groupes de flags appelés « flagspace ».
La commande f va nous permettre de les manipuler.
Sans argument, f permet de lister l’ensemble des flags ou des flagspaces sélectionnés.
Il est possible de créer un flag à l'adresse de son choix via la commande suivante :
[0x00000000]> f mon_flag @ 0x1000
[0x00000000]> f~mon_flag
0x00001000 1 mon_flag
Les flags générés par r2 sont automatiquement rangés dans différents flagspaces. Il est possible de récupérer la liste de tous les flagspaces via fs :
[0x00400526]> fs
0 2 * strings
1 33 * symbols
2 82 * sections
3 3 * relocs
4 3 * imports
Par défaut, tout les flagspaces sont sélectionnés. Afin de sélectionner un flagspace spécifique, il suffit de le donner en argument à fs.
fs permet aussi de créer ses propres flagspaces, il suffit de sélectionner un flagspace inexistant pour qu'il soit automatiquement créé et sélectionné.
[0x00400526]> fs test
[0x00400526]> fs
0 2 . strings
1 33 . symbols
2 82 . sections
3 3 . relocs
4 3 . imports
5 0 * test
1.3.4 Interprétation des données en mémoire
Les sous-commandes de p vont nous permettre d'afficher et d’interpréter certaines informations.
On peut par exemple utiliser la commande pd (print disassembly) pour désassembler un certain nombre d'instructions :
[0x00400527]> pd 4
0x00400527 4889e5 mov rbp, rsp
0x0040052a 4883ec10 sub rsp, 0x10
0x0040052e 897dfc mov dword [rbp - 4], edi
0x00400531 488975f0 mov qword [rbp - 0x10], rsi
Il est également possible d’afficher le contenu d'une adresse sous différents formats.
On peut par exemple utiliser les sous-commandes de px (print hex) afin de présenter l'affichage dans un format hexadécimal.
0x00402c59]> px
- offset - 0 1 2 3 4 5 6 7 8 9 A B C D E F 0123456789ABCDEF
0x00402c59 4531 ed45 31e4 904c 8d44 2430 b9a0 4d41 E1.E1..L.D$0..MA
0x00402c69 00ba 3889 4100 4889 ee89 dfc7 4424 30ff ..8.A.H.....D$0.
0x00402c79 ffff ffe8 9ff8 ffff 83f8 ff0f 84eb 0600 ................
0x00402c89 0005 8300 0000 3d12 0100 000f 8784 0600 ......=.........
Certaines commandes (comme ici px) acceptent une taille en paramètre, si on ne leur en fournit pas, radare va afficher/interpréter tout le contenu d'un bloc (256 octets par défaut). Il est possible de modifier la taille d'un bloc à l'aide de la commande b :
[0x00402c59]> b 32
[0x00402c59]> px
- offset - 0 1 2 3 4 5 6 7 8 9 A B C D E F 0123456789ABCDEF
0x00402c59 4531 ed45 31e4 904c 8d44 2430 b9a0 4d41 E1.E1..L.D$0..MA
0x00402c69 00ba 3889 4100 4889 ee89 dfc7 4424 30ff ..8.A.H.....D$0.
[0x00402c59]> b 16
[0x00402c59]> px
- offset - 0 1 2 3 4 5 6 7 8 9 A B C D 0123456789ABCD
0x00402c59 4531 ed45 31e4 904c 8d44 2430 b9a0 4d41 E1.E1..L.D$0..MA
Les commandes px sont pratiques pour une analyse visuelle, mais dans le cadre du développement de plugins pour r2, nous serons plus amenés à récupérer des informations brutes (pr).
Il existe un nombre important de variantes de la commande p, je vous invite donc à en voir la liste complète via p?.
1.3.5 Les opérations d’écriture
La commande w va nous permettre aussi bien d’écrire en mémoire lorsque nous utiliserons le mode debug, que de modifier, de manière temporaire ou permanente, le fichier en cours d'analyse.
Par défaut, le binaire à analyser est ouvert en lecture seule, pour pouvoir le modifier, il faudra l’ouvrir à nouveau, en lecture/écriture, à l'aide de la commande oo :
[0x00400430]> oo?
|Usage: oo[-] [arg] # map opened files
| oo reopen current file
| oo+ reopen in read-write
| oob reopen loading rbin info
| ood reopen in debug mode
| oom reopen in malloc://
| oon reopen without loading rbin info
| oon+ reopen in read-write mode without loading rbin info
| oonn reopen without loading rbin info, but with header flags
| oonn+ reopen in read-write mode without loading rbin info, but with
[0x00400430]> oo+
[0x00400430]> px 1
- offset - 0 1 2 3 4 5 6 7 8 9 A B C D E F 0123456789ABCDEF
0x00400430 31 1
[0x00400430]> wx 90
[0x00400430]> px 1
- offset - 0 1 2 3 4 5 6 7 8 9 A B C D E F 0123456789ABCDEF
0x00400430 90 .
[0x00400430]>
En lecture seule, il sera également possible d'activer un cache d’entrée/sortie qui permettra à la session en cours de procéder à des modifications sans altérer le fichier grâce l’option io.cache:
[0x00400430]> e io.cache=true
La liste de toutes les options disponibles et de leurs descriptions est disponible via la commande e??.
En activant le cache, il est également possible d'obtenir la liste des modifications apportées grâce à la commande wc :
[0x00400430]> wc
idx=0 addr=0x00400430 size=1 31 -> 90 (not written)
Ces modifications ne seront ensuite effectivement appliquées sur le fichier ou la mémoire du process débogué uniquement sur demande de l'utilisateur. La commande wci permet d'appliquer toutes les modifications apportées au domaine d’entrée-sortie utilisé (l'espace mémoire du process débogué en mode debug, le fichier en mode écriture, autres plugins d’io).
Il est également possible d'appliquer partiellement des modifications, ou encore d’annuler une modification locale à l'aide des commandes wc+ et wc-.
De la même façon que les sous-commandes p, les sous-commandes w permettent d’écrire différents types de données en mémoire. On pourra par exemple écrire directement une valeur hexadécimale, ou du code assembleur, que r2 assemblera lui-même, avant écriture :
[0x00400526]> wx 90
[0x00400526]> pd 1
;-- main:
;-- main:
0x00400526 90 nop
[0x00400526]> wa ret
Written 1 byte(s) (ret) = wx c3
[0x00400526]> pd 1
;-- main:
;-- main:
0x00400526 c3 ret
La commande w est tout aussi complète que la commande p, je vous invite à plonger dans sa documentation via w?.
1.3.6 Le mode debug
Nous avons rapidement vu le mode debug dans la section précédente, c'est donc l'occasion d'aborder plus en détail ses fonctionnalités.
Il est possible de lancer r2 directement en mode debug grâce au flag -d :
$ r2 -d /bin/ls
Ou alors de demander à r2 de rouvrir le fichier courant en mode debug grâce à la commande ood.
Par défaut, radare2 nous donne la main au point d’entrée du binaire, juste après qu’il soit chargé en mémoire. Il est possible de changer ce comportement avec l'option dbg.bep. Les valeurs possibles sont loader, main, entry ou n'importe quelle adresse/symbole.
Il est aussi possible de récupérer la liste des constructeurs/destructeurs grâce à la commande iee.
1.3.6.1 Reprise d’exécution
Le mode debug de radare2 propose un certain nombre de commandes intéressantes. Il va par exemple nous permettre non seulement de continuer l’exécution avec dc, mais on peut également lui demander de continuer jusqu'à une adresse particulière avec dcu :
[0x7f31e6f55c30]> dcu main
Continue until 0x00400566 using 1 bpsize
hit breakpoint at: 400566
[0x00400566]>
ou encore continuer jusqu'au prochain appel système via dcs :
[0x00400566]> dcs write
Running child until syscalls:1
child stopped with signal 133
--> SN 0x7f755e78bc34 syscall 5 fstat (0x1 0x7ffde3c2ded0)
child stopped with signal 133
--> SN 0x7f755e791e19 syscall 12 brk (0x0)
child stopped with signal 133
--> SN 0x7f755e791e19 syscall 12 brk (0x193f000)
child stopped with signal 133
--> SN 0x7f755e78c2c0 syscall 1 write (0x1 0x191e010 0x5)
[0x7f755e78c2c0]>
De la même façon, dcf, dcc, dccu, dcp permettent respectivement de continuer l’exécution jusqu’à un fork, un appel de fonction, un appel indirect de fonction, l’entrée de la section du code du binaire. La sous-commande dc comporte un grand nombre de sous-commandes, il est vivement recommandé de consulter sa documentation via dc?.
1.3.6.2 Les points d’arrêt
Il est possible de poser des points d’arrêt logiciel avec la commande db :
[0x7f757c25bc30]> db main
[0x7f757c25bc30]> dc
hit breakpoint at: 400566
[0x00400566]>
Ou encore des points d’arrêt matériels/watchpoints via la commande drx :
[0x7f319ffa1c30]> drx 0 main 1 x
[0x7f319ffa1c30]> dc
[0x00400566]>
1.3.6.3 Exécution pas à pas
Radare2 nous fournit aussi un ensemble de commandes permettant de faire du débogage pas à pas et de tracer l’exécution du code.
Les commandes ds et dso vont respectivement permettre de step-in et step-over. D'autres commandes ds sont disponibles, en voici un bref aperçu :
- dsu : permet de tracer le code jusqu’à un symbole ou une adresse ;
- dsui : permet de tracer le code jusqu’à une instruction ;
- dsf : permet de tracer le code jusqu’à une détection de fin de stack frame.
1.3.6.4 Le système de trace
Par défaut, r2 n'enregistre pas les traces d’exécution (stepping). Afin de permettre à r2 d'enregistrer une trace, il suffit d'activer l'option dbg.trace :
[0x00400566]> e dbg.trace = true
[0x00400566]> dsui jmp rax
0x00400567 3 mov rbp, rsp
0x0040056a 4 sub rsp, 0x10
0x0040056e 3 mov dword [rbp - 4], edi
0x00400571 4 mov qword [rbp - 0x10], rsi
0x00400575 4 cmp dword [rbp - 4], 0x3b
0x00400579 2 ja 0x4005c2
0x0040057b 3 mov eax, dword [rbp - 4]
0x0040057e 8 mov rax, qword [rax*8 + 0x400698]
0x00400586 2 jmp rax
Stop.
Il est possible ensuite de récupérer la trace via la commande dt :
[0x00400566]> dt
0x00400567 size=3 count=1 times=1 tag=0
0x0040056a size=3 count=2 times=1 tag=0
0x0040056e size=4 count=3 times=1 tag=0
0x00400571 size=4 count=4 times=1 tag=0
0x00400575 size=2 count=5 times=1 tag=0
0x00400579 size=3 count=6 times=1 tag=0
0x0040057b size=8 count=7 times=1 tag=0
0x0040057e size=2 count=8 times=1 tag=0
La valeur count donne le numéro de l'instruction, la valeur times nous donne le nombre de fois où une instruction a été atteinte.
1.3.7 Analyse de code
Tout comme IDA, r2 est capable de procéder à l'analyse automatique d'un binaire. Cela étant dit, le workflow r2 est différent de celui proposé par IDA, en effet les développeurs de radare2 déconseillent de procéder à l'analyse complète de l’ensemble d’un binaire à son ouverture.
À la place, r2 propose un éventail de commandes permettant, au fur et à mesure, de récupérer de nombreuses informations concernant les instructions, basic blocks, fonctions, graphe de flot de contrôle (CFG), etc.
La commande af par exemple, permettra d'analyser les basic blocks d’une fonction, d'en extraire son CFG, et de renommer automatiquement les variables locales.
[0x004004d6]> af
[0x004004d6]> pdf
;-- main:
/ (fcn) main 31
| main (int arg_1h);
| ; var int local_10h @ rbp-0x10
| ; var int local_4h @ rbp-0x4
| ; arg int arg_1h @ rbp+0x1
| 0x004004d6 55 push rbp
| 0x004004d7 4889e5 mov rbp, rsp
| 0x004004da 897dfc mov dword [local_4h], edi
| 0x004004dd 488975f0 mov qword [local_10h], rsi
| 0x004004e1 837dfc01 cmp dword [local_4h], 1 ; [0x1:4]=-1 ; 1
| ,=< 0x004004e5 7f07 jg 0x4004ee
| | 0x004004e7 b8ffffffff mov eax, 0xffffffff ; -1
| ,==< 0x004004ec eb05 jmp 0x4004f3
| |`-> 0x004004ee b800000000 mov eax, 0
| | ; JMP XREF from 0x004004ec (main)
| `--> 0x004004f3 5d pop rbp
\ 0x004004f4 c3 ret
Vous remarquerez qu'afin d'afficher le code de notre fonction, nous avons utilisé la commande pdf, cette commande utilise les informations de l'analyse faite par r2 pour n'afficher que le code de la fonction analysée.
La commande afb va nous permettre à partir de cette analyse de récupérer la liste des basic blocks de cette fonction :
[0x004004d6]> afb
0x004004d6 0x004004e7 00:0000 17 j 0x004004ee f 0x004004e7
0x004004e7 0x004004ee 00:0000 7 j 0x004004f3
0x004004ee 0x004004f3 00:0000 5 j 0x004004f3
0x004004f3 0x004004f5 00:0000 2
La première colonne correspond à l'adresse du basic block, la seconde à sa fin, la troisième aux informations de trace (count, times), la quatrième au nombre d’instructions, la cinquième à l'adresse du saut en cas de validation de condition, et enfin la dernière à l’adresse du saut si la condition n’est pas validée.
Il est possible de modifier ces informations de basic block via les commandes afb+ et afbe.
Cela peut être utile lorsque l’analyse automatique de r2 ne suffit pas, on pourra ainsi reconstruire par exemple les différentes branches d’une switch table.
Radare2 nous permet aussi de récupérer de précieuses informations concernant les instructions, en effet, les commandes ao vont nous permettre aussi bien d’obtenir des informations basiques (ao), telles que le type d'une instruction, son mnemonique, les différents registres utilisés, etc.
Mais aussi la traduction de l’instruction en un langage intermédiaire (REIL), facilement analysable (aor).
[0x004004d6]> pd 1
0x004004d6 55 push rbp
[0x004004d6]> ao
address: 0x4004d6
opcode: push rbp
mnemonic: push
prefix: 0
id: 588
bytes: 55
refptr: 0
size: 1
type: upush
esil: rbp,8,rsp,-=,rsp,=[8]
stack: inc
stackop: -320062632
stackptr: 8
[0x004004d6]> aor
0x4004d6
0000.00: SUB R_rsp:64 , 8:64 , V_00:64
0000.01: STR R_rsp:64 , , V_01:64
0000.02: STR V_00:64 , , R_rsp:64
0001.00: LDM R_rsp:64 , , V_02:64
0001.01: STM R_rbp:64 , , R_rsp:64
[0x004004d6]>
En interne, radare2 utilise un langage intermédiaire appelé ESIL, ce langage ayant été choisi historiquement pour de l’émulation (sujet que nous ne couvrirons pas dans cet article), il trouve rapidement ses limites en termes d’analyse.
Le langage REIL permet de découper chaque instruction analysée en groupe de micro-instructions facilement analysables. Chacune de ces instructions est au format mnemonique src1, src2, dest. src1 et src2 étant soit une référence, soit un registre soit une valeur immédiate, dest sera soit une référence, soit un registre.
REIL ne propose que deux instructions pour l’accès mémoire : STM et LDM. Ces instructions ne prennent que deux opérandes, une source et une destination.
Les branchements (conditionnels ou non) sont tous gérés par l’instruction JCC qui est au format mnemonique condition, target, où condition et target peuvent être une valeur immédiate, un registre ou une référence.
1.4 Le développement de plugins/scripts sous r2
Le développement de plugins pour r2 peut se faire de différentes façons en utilisant différents langages.
En effet, r2 propose plusieurs manières de développer des scripts et plugins :
- r2pipe pour le simple scripting ;
- les bindings lang-{python, rust, js, etc.} couplés avec r2pipe pour le développement de plugins ;
- C en liant avec les bibliothèques r2.
Dans cette section, nous allons nous intéresser à la deuxième solution : lang-python et r2pipe.
r2pipe est un module minimaliste (ici en Python, mais il est disponible dans un grand nombre de langages) : il permet de contrôler une session r2 en offrant à l'utilisateur la possibilité d'appeler des commandes r2 via la méthode cmd.
>>> import r2pipe
>>> r2 = r2pipe.open("/bin/ls")
>>> r2.cmd("p8 10")
'31ed4989d15e4889e248'
Il est possible d'installer r2pipe depuis pip, mais étant donné que radare2 est un projet en constante évolution, il est recommandé, comme évoqué dans l’introduction, de toujours récupérer la dernière version depuis GitHub.
On peut également appeler un script r2pipe directement depuis r2 :
[0x004049a0]> #!pipe python mon_script.py
Lorsque r2pipe est invoqué depuis r2, il est permis d'omettre le nom du fichier :
[0x004049a0]> #!pipe python
>>> import r2pipe
>>> r2 = r2pipe.open()
>>> r2.cmd("p8 10")
'31ed4989d15e4889e248'
r2 va créer un canal de communication bidirectionnel entre le script et r2 à l’aide de deux pipes.
En plus de la méthode cmd, r2pipe propose une autre façon très pratique de récupérer des informations. En effet, un grand nombre de commandes r2 accepte le suffixe j. Ces commandes vont alors générer des données au format json.
[0x004049a0]> iej~{}
[
{
"vaddr": 4213152,
"paddr": 18848,
"baddr": 4194304,
"laddr": 0,
"haddr": 24,
"type": "program"
}
]
~{} est la fonction pretty print du grep interne de r2.
r2pipe est ensuite capable de les convertir en objets python grâce à cmdj :
>>> import r2pipe
>>> r2 = r2pipe.open("/bin/ls")
>>> entrypoint_data = r2.cmdj("iej")
>>> print entrypoint_data[0]["type"]
program
Cette méthode trouve rapidement ses limites lorsqu'on veut développer un plugin relativement complexe nécessitant la présence d'un contexte.
En effet, la commande #!pipe python va lancer un nouveau shell python à chaque appel, tandis que lang-python va embarquer directement le moteur python dans radare2, annulant ainsi la limitation de r2pipe.
Afin d'installer lang-python, il faut d'abord installer et configurer r2pm (rien de bien méchant, il suffit juste d’exporter une variable d’environnement) :
$ git clone https://github.com/radare2/radare2-pm
$ cd radare2-pm
$ export R2PM_DBDIR="$PWD/db"
$ r2pm -i lang-python2
r2pm est un gestionnaire de paquets permettant de lister et installer tous les plugins connus de radare2.
Et voilà une interface offrant de plus grandes perspectives :
[0x00005520]> #!python
In [1]: a = 10
In [2]:
Do you really want to exit ([y]/n)? y
[0x00005520]> #!python -e print a
10
1.4.1 L’interface de plugin
Une fois lang-python2 installé, r2 met à notre disposition le module r2lang, permettant l’enregistrement de plugins auprès de radare2. Nous allons pouvoir enregistrer 5 types de plugins :
- asm : ce type de plugin va nous permettre d’assembler/désassembler du code pour de nouvelles architectures.
- io : les plugins d’io ont pour but de fournir les données à analyser à radare2. Un fichier binaire est géré par un type d’io, le contenu de la mémoire d’un process par un autre.
- anal : ce type de plugin permet de fournir à r2 des informations détaillées au sujet d’instructions d’une nouvelle architecture. Radare2 utilisera ces informations lors de l’analyse du code, permettant ainsi entre autres la détection de basic blocks et de fonctions.
- bin : les plugins bin sont utilisés afin d’ajouter le support de nouveaux formats de fichier, ils permettent de définir un nombre important d’informations, à savoir la liste des segments, des sections, des imports, etc.
- core : permet simplement d’enregistrer des commandes auprès de radare2.
Afin de s’enregistrer au système de plugins, il suffit d’appeler la méthode r2lang.plugin :
>>> r2lang.plugin("core", mon_plugin.plugin)
Le premier paramètre de cette méthode permet de sélectionner le type de plugin que nous voulons implémenter. Le second paramètre est une fonction retournant un dictionnaire de description et de callbacks.
import r2lang
def plugin(a):
def _call(s):
if s.startswith("coucou"):
print("coucou depuis python !")
return 1
return 0
return {
"name": "mon_plugin",
"licence": "GPLv3",
"desc": "mon premier plugin",
"call": _call,
}
r2lang.plugin("core", plugin)
Ici comme nous enregistrons un plugin core, seule la callback call est nécessaire. C’est elle qui va permettre d’intercepter les commandes utilisateur et de les gérer ou non. Lorsqu’une commande est gérée par le plugin, il doit retourner 1. Autrement, il devra retourner 0 afin de laisser r2 le faire à sa place.
Il suffit ensuite de lancer r2 de la manière suivante pour enregistrer le plugin :
$ r2 /bin/ls -i plugin.py
[0x00005520]> coucou
coucou depuis python !
1.5 Demo time
Afin d'illustrer ce que nous avons vu dans les précédentes sections, nous allons écrire ensemble un moteur d’analyse par teinte basé sur r2 et REIL. Il s’agit ici d’une approche naïve avec un certain nombre de limitations afin de nous introduire aux bases de l’analyse de code.
Nous allons donc ajouter trois commandes à r2 :
- t.tr : applique une teinte à un registre de notre choix ;
- t.tm : applique une teinte à une adresse de notre choix ;
- t.ds : en mode debug, cette commande permettra d'analyser l'instruction courante, appliquera des commentaires, puis procèdera à un step.
Pour ce faire, nous allons d’abord mettre en place une interface avec r2. Pour cet exemple, nous avons, a priori, besoin de 7 commandes pour arriver à nos fins :
- CC qui nous permettra de commenter le code automatiquement ;
- s afin de nous déplacer dans le fichier ;
- ds pour procéder à l’exécution pas à pas du code ;
- dmj pour la détection du mode debug ;
- ? qui nous permettra d’évaluer des expressions mathématiques simples ;
- aoj afin de récupérer certaines informations concernant l’instruction ;
- aor pour les informations sur la sémantique des instructions.
class R2(object):
CMD_HANDLED = 1
CMD_NOT_HANDLED = 0
def __init__(self):
self.r2 = r2pipe.open()
self.commands = {}
def isdebugging(self):
if self.get_maps():
return True
return False
def get_maps(self):
return self.r2.cmdj("dmj")
def seek(self, addr=None):
if addr:
self.r2.cmd("s {}".format(addr))
return int(self.r2.cmd("s"), 16)
def set_comment(self, comment, address=None):
if address:
address = str(address)
self.r2.cmd("CC- @ {}".format(address))
self.r2.cmd("CC {} @ {}".format(comment, address))
else:
self.r2.cmd("CC-".format(comment))
self.r2.cmd("CC {}".format(comment))
def analyse_inst(self):
return self.r2.cmd("aor")
def step(self):
self.r2.cmd("ds")
Ensuite, nous aurons besoin d’un embryon de classe qui représentera notre plugin, que nous appellerons R2TE :
class R2TE(object):
def __init__(self):
self.r2 = R2()
self.handlers = dict()
self.register("ds", self.cmd_step)
self.register("tr", self.cmd_taint_register)
self.register("tm", self.cmd_taint_memory)
def register(self, name, func):
self.handlers[name] = func
def handle(self, command, args):
self.handlers[command](args)
def cmd_analyse(self, a):
pass
def cmd_step(self, a):
pass
def cmd_taint_register(self, a):
pass
def cmd_taint_memory(self, a):
pass
def plugin(self, a):
def _call(s):
args = s.split()
if not args[0].startswith("t."):
return R2.CMD_NOT_HANDLED
try:
module, command = args[0].split(".")
self.handle(command, args[1:])
except Exception as e:
print e
return R2.CMD_NOT_HANDLED
return {
"name": "R2TE",
"licence": "GPLv3",
"desc": "Radare2 taint engine",
"call": _call,
}
r2te = R2TE()
r2lang.plugin("core", r2te.plugin)
Afin de pouvoir procéder à l’analyse par teinte, il nous faudra d’abord parser les informations de la commande aor (il n’existe pas de commande aorj). Nous créons donc une classe operand et nous mettons à jour la méthode analyse_inst de la classe R2 afin qu’elle retourne une liste d’instructions REIL (une instruction étant implémentée comme un tuple contenant le mnémonique et une liste d’opérandes).
class operand(object):
def __init__(self, optype, opval, opsize):
self.optype = optype
self.opval = opval
if self.optype == "reg":
self.opval = opval
if opval in unalias:
self.opval = unalias[opval]
self.opsize = opsize
Le dictionnaire unalias est utilisé ici uniquement dans le but de ne pas avoir à différencier les variantes de taille d’un registre donné et de toujours retourner la version 64 bits du registre.
Cela dit, nous gardons l’information de taille opsize fournie par REIL, ce qui nous permettra de déterminer la taille de la mémoire teintée lorsque la teinte passera d’un registre à la mémoire.
def analyse_inst(self):
def _ssplit(s, d):
return [ e.strip() for e in s.split(d, 1) ]
def _parse_op(op):
if ":" not in op:
return operand("none", None, None)
val, size = _ssplit(op, ":")
if val.startswith("R_"):
return operand("reg", val[2:], int(size))
if val.startswith("V_"):
return operand("ref", val[2:], int(size))
if val.startswith("0x"):
return operand("imm", int(val, 16), int(size))
return operand("imm", int(val, 10), int(size))
def _parse_ops(ops):
operands = list()
for op in ops:
op = _parse_op(op)
if op.optype != "none":
operands.append(op)
return operands
def _parse_ril(ril):
ril = ril.strip()
try: cmd, ops = _ssplit(ril, " ")
except: return None
ops = ops.strip().split(",")
return cmd, _parse_ops(ops)
instructions = list()
reil_code = self.r2.cmd("aor").split("\n")[1:]
for line in reil_code:
if "," not in line:
continue
_, inst = _ssplit(line, ":")
instructions.append(_parse_ril(inst))
return instructions
Ensuite, il nous faudra trouver un moyen de suivre la teinte qui transite entre les registres.
Les instructions REIL étant au format MNEMONIQUE OPERAND1, OPERAND2, DESTINATION, nous allons partir de l’hypothèse que pour tous les mnémoniques autres que STM, LDM et JCC, si OPERAND1 et/ou OPERAND2 sont teintés, alors destination le sera aussi. Dans le cas où la destination se trouve être teintée, nous la placerons dans un pool des références/registres teintés.
Nous nous intéresserons plus tard à la teinte de la mémoire (STM, LDM) et de RIP en cas de branchement conditionnel (JCC).
Nous aurons donc besoin d’ajouter un pool des références teintées à la classe R2TE :
class R2TE(object):
def __init__(self):
self.r2 = R2()
self.handlers = dict()
self.register("ai", self.cmd_analyse)
self.register("ds", self.cmd_step)
self.register("tr", self.cmd_taint)
self.regs = dict()
Nous avons besoin de deux méthodes : une pour mettre à jour le pool de références, une pour vérifier si un registre ou un référence est teinté.
def update_pool(self, op, taint):
self.refs[op.opval] = taint
def is_tainted(self, op):
if op.opval in self.refs:
return self.refs[op.opval]
return False
Maintenant que nous avons toutes les billes en main pour détecter le passage de teinte d’un registre à un autre, nous pouvons ajouter les méthodes process cmd_taint_register et cmd_step à R2T2 :
def _process_minst_v1(self, mn, ops):
ops, dst = ops[:-1], ops[-1]
taint = any([self.is_tainted(op) for op in ops])
self.update_pool(dst, taint)
def process(self, code):
for mn, ops in code:
self._process_minst(mn, ops)
def cmd_taint_register(self, a):
self.refs[a[0]] = True
def cmd_step(self, a):
if not self.r2.isdebugging():
print "You need to switch to debug mode, see `ood?`"
self.r2.seek("rip")
reil_code = self.r2.analyse_inst()
self.process(reil_code)
tainted_regs = [ reg for reg in self.get_tainted_regs() ]
if len(tainted_regs):
self.r2.set_comment("tainted: {}".format(", ".join(tainted_regs)))
self.r2.step()
Enfin, nous pouvons enregistrer le plugin auprès de radare2 :
r2te = R2TE()
success = r2lang.plugin("core", r2te.plugin)
Observons le résultat depuis radare2 :
[0x7f36f4a04ed0]> #!python misc-simple.py
[0x7f36f4a04ed0]> "wa mov rax, rsp; add rbx, rax; mov rax, rsi"
Written 9 byte(s) (mov rax, rsp; add rbx, rax; mov rax, rsi) = wx 4889e04801c34889f0
[0x7f36f4a04ed0]> t.tr rsp
[0x7f36f4a04ed0]> t.ds
[0x7f36f4a04ed0]> t.ds
[0x7f36f4a04ed3]> t.ds
[0x7f36f4a04ed6]> pd 3 @ 0x7f36f4a04ed0
0x7f36f4a04ed0 4889e0 mov rax, rsp ; tainted: rsp, rax
0x7f36f4a04ed3 4801c3 add rbx, rax ; '#' ; tainted: zf, cf, rax, pf, rsp, rbx, of, sf
0x7f36f4a04ed6 4889f0 mov rax, rsi ; tainted: zf, cf, pf, rsp, rbx, of, sf
[0x7f36f4a04ed6]>
Nous voyons clairement que rax et rsp sont teintés après l’exécution de l’instruction mov rax, rsp. Ensuite, la teinte est bien propagée dans les eflags et rbx après l’exécution de l’instruction mov rbx, rax. Enfin, nous observons qu’à la suite de l’exécution de l’instruction mov rax, rsi, le registre rax n’est plus teinté.
Maintenant que nous sommes en mesure de propager la teinte de registre à registre, tâchons de faire de même pour la propagation registre-mémoire et inversement.
La première problématique à résoudre afin de procéder à la propagation de teinte d’un registre vers une adresse mémoire, est d’être capable de retrouver l’adresse mémoire en question. En effet, pour les architectures x86, un offset peut avoir plusieurs formes (registre * constante + disp, registre * constante + registre, registre, etc.).
Étant donné que radare2 est capable d’évaluer ce genre d’opération via la commande ?, nous allons extraire l’expression entre crochets et utiliser r2 pour l’évaluer. Évidemment, cela ne fonctionnera qu’en mode debug.
De plus, certaines instructions telles que push, pop, call et retvont modifier la stack.
Nous allons donc ajouter à la classe R2 la méthode resolve_access qui nous permettra de résoudre l’accès mémoire de l’instruction courante :
def resolve_access(self):
self.seek("rip")
inst = self.r2.cmdj("aoj")[0]
if inst["mnemonic"] == "push":
address = self.r2.cmd("? rsp - 8").split(" ")[0]
elif inst["mnemonic"] == "call":
address = self.r2.cmd("? rsp - 8").split(" ")[0]
elif inst["mnemonic"] == "pop":
address = self.r2.cmd("? rsp").split(" ")[0]
elif inst["mnemonic"] == "ret":
address = self.r2.cmd("? rsp").split(" ")[0]
else:
access = inst["opcode"].split("[")[1].split("]")[0]
address = self.r2.cmd("? {}".format(access)).split(" ")[0]
return int(address, 10)
STM et LDM étant respectivement de la forme : MNEMONIQUE OPERAND, , MEMOIRE et MNEMONIQUE MEMOIRE, , OPERAND. On pourra facilement propager la teinte d’un registre vers une adresse mémoire et inversement.
De plus, JCC étant de la forme : JCC OPERAND, , MEMOIRE, il sera possible de déterminer la teinte de RIP en fonction de la teinte de OPERAND.
Nous mettons donc à jour la méthode _process_minst de la classe R2TE :
def _process_minst(self, mn, ops):
dst = ops[-1]
ops = ops[:-1]
if mn == "STM":
access = self.r2.resolve_access()
for offset in xrange(ops[0].opsize / 8):
self.mems[access + offset] = self.is_tainted(ops[0])
elif mn == "LDM":
access = self.r2.resolve_access()
if access in self.mems:
self.update_pool(dst , self.mems[access])
else:
self.update_pool(dst , False)
elif mn == "JCC":
dst = operand("reg", "rip", 64)
taint = self.is_tainted(ops[0])
self.update_pool(dst , taint)
else:
taint = any([self.is_tainted(op) for op in ops])
self.update_pool(dst, taint)
Observons le résultat depuis radare2 :
[0x7fca73c14ed0]> dcu main
Continue until 0x5635b6904c10 using 1 bpsize
hit breakpoint at: 5635b6904c10
[0x5635b6904c10]> #!python misc-simple.py
[0x5635b6904c10]> ? rsi
140725972353176 0x7ffd5197c098 03777652145740230 131061.3G d5197000:0098 140725972353176 "\x98\xc0\x97Q\xfd\x7f" 0b011111111111110101010001100101111100000010011000 140725972353176.0 140725970796544.000000f 140725972353176.000000 0t200110021021022222022011221122
[0x5635b6904c10]> t.tm 140725972353176
[0x5635b6904c10]> t.tm 140725972353177
[0x5635b6904c10]> t.tm 140725972353178
[0x5635b6904c10]> t.tm 140725972353179
[0x5635b6904c10]> t.tm 140725972353180
[0x5635b6904c10]> t.tm 140725972353181
[0x5635b6904c10]> t.tm 140725972353182
[0x5635b6904c10]> t.tm 140725972353183
[0x5635b6904c10]> t.ds
[0x5635b6904c10]> t.ds
[0x5635b6904c12]> t.ds
[0x5635b6904c16]> t.ds
[0x5635b6904c18]> t.ds
[0x5635b6904c19]> t.ds
[0x5635b6904c1a]> t.ds
[0x5635b6904c1c]> t.ds
[0x5635b6904c1f]> t.ds
[0x5635b6904c23]> t.ds
[0x5635b6904c26]> t.ds
[0x5635b6904c2f]> t.ds
[0x5635b6904c34]> t.ds
[0x5635b6904c36]> pd 13 @ main
;-- main:
;-- section_end..plt.got:
;-- section..text:
0x5654e7c6dc10 4157 push r15 ; [14] --r-x section size 72792 named .text
0x5654e7c6dc12 4156 push r14
0x5654e7c6dc14 4155 push r13
0x5654e7c6dc16 4154 push r12
0x5654e7c6dc18 55 push rbp
0x5654e7c6dc19 53 push rbx
0x5654e7c6dc1a 89fd mov ebp, edi
0x5654e7c6dc1c 4889f3 mov rbx, rsi
0x5654e7c6dc1f 4883ec58 sub rsp, 0x58 ; 'X'
0x5654e7c6dc23 488b3e mov rdi, qword [rsi] ; tainted: rdi
0x5654e7c6dc26 64488b042528. mov rax, qword fs:[0x28] ; [0x28:8]=-1 ; '(' ; 40 ; tainted: rdi
0x5654e7c6dc2f 4889442448 mov qword [rsp + 0x48], rax ; tainted: rdi
0x5654e7c6dc34 31c0 xor eax, eax ; tainted: rdi
Nous voilà en mesure de récupérer la propagation de teinte depuis la mémoire vers un registre. Cette implémentation n’est évidemment pas complète, et pleine de limitations (si rax est teinté, un xor rax, rax marquera rax comme étant teinté, ou encore si al est tenté, un shr rax, 8 marquera rax comme étant teinté).
Conclusion
Nous avons pu voir dans cet article qu’il était possible de rapidement étendre les possibilités d’analyse de code avec r2pipe et ce, en utilisant uniquement quelques commandes de base de r2.
On pourra par la suite coupler r2 avec d’autres outils/frameworks tels que Triton ou Angr afin de fournir des informations plus détaillées sur le comportement d’un binaire.