Introduction au développement de plugins pour Radare2

MISC HS n° 017 | avril 2018 | Ayman Khamouma
Creative Commons
  • Actuellement 0 sur 5 étoiles
0
Merci d'avoir participé !
Vous avez déjà noté cette page, vous ne pouvez la noter qu'une fois !
Votre note a été changée, merci de votre participation !
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]>

Note

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"

  }                  

]

Note

~{} 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

Note

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.