Partie 23 - Exploiter un binaire par ROP - SROP et JOP (4/6)
Exploiter un binaire par ROP : SROP et JOP (4/6)
Dans ce chapitre, nous allons nous intĂ©resser Ă quelques techniques dâexploitation qui sont proches du ROP et qui peuvent ĂȘtre utilisĂ©es dans certains contextes :
| Technique | Signification | RĂ©sumĂ© | Quand lâutiliser ? |
|---|---|---|---|
| SROP | Sigreturn-Oriented Programming | Contourner lâappel systĂšme sigreturn afin de contrĂŽler lâexĂ©cution du programme | PrĂ©sence dâinstructions syscall/int 0x80, libc inaccessible, etc. |
| JOP | Jump-Oriented Programming | Utiliser des instructions de sauts pour contrĂŽler lâexĂ©cution du programme | Quand le ROP nâest pas possible đ |
| BROP | Blind ROP | Technique permettant de faire du ROP âĂ lâaveugleâ sur un programme distant dont on ne dispose pas du binaire | Programme distant dont on ne dispose pas du binaire |
Ces techniques sont gĂ©nĂ©ralement trĂšs dĂ©pendantes du contexte et, honnĂȘtement, il nâest pas indispensable de toutes les maĂźtriser. Retenez simplement que ce chapitre vous fournit les informations nĂ©cessaires Ă leur mise en Ćuvre.
Ătant donnĂ© leur caractĂšre assez niche, nous nous concentrerons ici sur la partie thĂ©orique.
SROP (ou Sigreturn-Oriented Programming)
Le SROP est une technique dâexploitation basĂ©e sur la rĂ©utilisation de code, mais dans une moindre mesure que le ROP. Elle consiste Ă forger une structure rt_sigframe Ă donner en paramĂštre Ă lâappel systĂšme sigreturn, dâoĂč le nom de cette technique đ€.
Cette technique peut ĂȘtre trĂšs intĂ©ressante dans le cas oĂč il nâest pas possible dâaccĂ©der Ă la libc et que lâon arrive Ă trouver lâadresse dâune instruction syscall ou int 0x80.
Conditions préalables
Bien que trÚs puissant, quelques conditions sont à satisfaire avant de réaliser du SROP :
- contrĂŽler les
Npremiers octets de la pile en ayant la possibilitĂ© dâinsĂ©rer des octets nuls ; - contrĂŽler
eax/raxafin de pouvoir y mettre le numĂ©ro de lâappel systĂšmesigreturn; - trouver lâadresse dâun gadget
syscall/int 0x80 - contrĂŽler
rip/eip.
Nest la taille de la structurert_sigframeen octets. Sâil nâest pas possible dâavoir exactementNoctets lors du dĂ©passement de mĂ©moire, il faudra au moins pouvoir contrĂŽler les principaux registres dont nous aurons besoin.
Fonctionnement global
LâingĂ©niositĂ© de cette technique rĂ©side dans le fait de contourner lâutilisation de lâappel systĂšme sigreturn afin dâexĂ©cuter un autre appel systĂšme, par exemple execve.
Lâobjectif de sigreturn est de permettre Ă un gestionnaire de signal de ne pas se prĂ©occuper de rĂ©tablir le contexte dâexĂ©cution tel quâil lâĂ©tait avant de traiter le signal. De ce fait, en utilisant sigreturn, câest le noyau qui sâoccupe de restaurer le contexte dâexĂ©cution Ă partir de la sauvegarde quâil a faite ⊠sur la pile !
Et vous savez quoi ? Peu importe lâarchitecture, 32 bits ou 64 bits, la structure rt_sigframe, qui contient la sauvegarde du contexte, est toujours sauvegardĂ©e dans la pile đ. Ce qui signifie quâavec un dĂ©passement de mĂ©moire sur la pile, nous pourrons contrĂŽler le contenu de cette structure.
Si vous souhaitez approfondir le concept de signal en programmation C, vous pouvez jeter un Ćil Ă ce cours.
LâidĂ©e va donc ĂȘtre de forger la structure rt_sigframe de maniĂšre Ă :
- charger les registres utilisés comme arguments des appels systÚme avec les valeurs adéquates ;
- mettre la valeur adéquate dans
eax/raxpour lâappel systĂšme final ; - mettre lâadresse de lâinstruction
int 0x80/syscalldans lâendroit oĂč est censĂ© ĂȘtre sauvegardĂ©eip/rip.
De la sorte, sigreturn va se charger, pour nous, de rĂ©aliser nâimporte quel appel systĂšme, merci pour les travaux đ !
Cette technique ressemble sur certains points Ă lâutilisation de la fonction setcontext de la libc. Nous en reparlerons dans un chapitre dĂ©diĂ© Ă lâexploitation dans le tas.
La structure rt_sigframe
Tout dâabord il faut savoir que le contenu et la taille de cette structure dĂ©pend de lâarchitecture utilisĂ©e. Vous imaginez bien que sauvegarder des registres de 32 bits ne prend pas la mĂȘme place que sauvegarder des registres de 64 bits.
IntĂ©ressons-nous Ă la version 64 bits. De toute maniĂšre, la version 32 bits suit le mĂȘme principe et les deux sont dĂ©finies dans ce fichier.
Voici sa définition :
1
2
3
4
5
struct rt_sigframe {
char __user *pretcode;
struct ucontext uc;
struct siginfo info;
};
En descendant dâun cran :
1
2
3
4
5
6
7
8
9
10
11
12
struct rt_sigframe {
char __user *pretcode;
struct ucontext uc
{
unsigned long uc_flags;
struct ucontext *uc_link;
stack_t uc_stack;
struct sigcontext uc_mcontext;
sigset_t uc_sigmask;
};
struct siginfo info;
};
Et en descendant encore dâun cran :
Câest bon jâai compris jâarrĂȘte đ„Č. Et puis, ce qui nous intĂ©resse, câest ce que ça donne en mĂ©moire, câest-Ă -dire ceci :
Imaginez prendre le contrĂŽle de cette structure et avoir la possibilitĂ© de modifier arbitrairement tous ces registres dont rip đ€€!
Exploiter cette structure
En fait, il va falloir utiliser le buffer overflow pour forger toutes ces valeurs sur la pile et faire croire au programme, en exĂ©cutant lâappel systĂšme
sigreturn, quâil doit rĂ©tablir le contexte ?
Câest exactement ça ! La seule difficultĂ© consiste Ă se rappeler la position de chaque registre dans la structure. Enfin, âdifficultĂ©â on se comprend, car pwntools permet de forger cette structure, nous verrons comment plus tard.
Voici comment exploiter un dépassement mémoire avec du SROP (ici en 64 bits) :
- nous nous plaçons au moment oĂč le dĂ©passement de mĂ©moire est terminĂ©. A partir de maintenant la chaĂźne SROP dĂ©bute. La structure
rt_sigframeest en đ”, en dessous de lâadresse de lâinstructionsyscall; - tout dâabord, il va falloir trouver et exĂ©cuter un gadget qui chargera la valeur
0xfdansrax, Ă©tant donnĂ© quâil sâagit du numĂ©ro de lâappel systĂšmesigreturn. Ce gadget đŽ peut ĂȘtre de diffĂ©rent type, par exemple, unpop rax ; retfera lâaffaire ; - lâappel systĂšme
sigreturnest exĂ©cutĂ© avec la structure forgĂ©ert_sigframedans la pile. Les valeurs les plus intĂ©ressantes Ă contrĂŽler sont en gras, notammentrdi,rsietrdxpour les arguments du prochain appel systĂšme Ă exĂ©cuter.raxpour y mettre le numĂ©ro du prochain appel systĂšme, iciexecve. Enfin,ripafin dâexĂ©cuter immĂ©diatement Ă la sortie desigreturnle prochain appel systĂšme ; - Ă©tant donnĂ© que
sigreturnsâest gentiment chargĂ© de restaurer le contexte dâexĂ©cution avec les valeurs que nous avons prĂ©alablement choisies, la prochaine exĂ©cution de lâinstructionsyscallimplique lâappel deexecve("/bin/sh", NULL, NULL).
Ăvidemment, si lâon veut invoquer execve, il faut en outre pouvoir Ă©crire la chaĂźne "/bin/sh" (ou la trouver en mĂ©moire) Ă une adresse connue Ă lâavance.
Pour ce qui est de la valeur de eflags et cs/gs/fs dans rt_sigframe il y a deux possibilités pour avoir des valeurs cohérentes :
- soit mettre les valeurs trouvĂ©es via gdb au moment oĂč
sigreturnva ĂȘtre exĂ©cutĂ© ; - ou bien laisser pwntools remplir ces valeurs pour nous đ.
Construire une chaĂźne de SROP avec pwntools
64 bits
Construire des chaĂźnes de ROP comme des pros, vous savez tous le faire Ă prĂ©sent đ. Ainsi, je ne doute pas de votre capacitĂ© Ă vous dĂ©brouiller pour trouver un gadget ou une astuce pour mettre la valeur 0xf (sigreturn) dans rax.
Câest pourquoi nous allons seulement nous intĂ©resser Ă la gĂ©nĂ©ration dâune structure rt_sigframe grĂące Ă pwntools et voir comment lâajouter en tant quâoctets dans notre payload final. Ainsi, nous pourrons nous en servir comme modĂšle en cas de besoin. La voici :
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
from pwn import *
context.arch = 'amd64'
io = process("./exe")
# Ces valeurs sont a adapter
syscall = 0x400123 # Adresse de l'instruction `syscall`
addr_bin_sh = 0x400213
bourrage = 40
# Creation de la structure `rt_sigreturn`
frame = SigreturnFrame()
frame.rax = int(constants.SYS_execve) # 0x3b
frame.rdi = addr_bin_sh # parametre n°1
frame.rsi = 0 # parametre n°2
frame.rdx = 0 # parametre n°3
frame.rip = syscall
payload = b"A" * bourrage
#payload += ... # Mettre `constants.SYS_rt_sigreturn` dans `rax`
payload += p64(syscall) # Appel systeme `sigreturn`
payload += bytes(frame) # Structure `rt_sigframe`
"""
io.send(payload)
(...)
"""
On peut mĂȘme afficher le contenu de frame en utilisant IPython :
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
In [5]: frame
Out[5]:
{'uc_flags': 0,
'&uc': 0,
'uc_stack.ss_sp': 0,
'uc_stack.ss_flags': 0,
'uc_stack.ss_size': 0,
'r8': 0,
'r9': 0,
'r10': 0,
'r11': 0,
'r12': 0,
'r13': 0,
'r14': 0,
'r15': 0,
'rdi': 287454020,
'rsi': 0,
'rbp': 0,
'rbx': 0,
'rdx': 0,
'rax': 59,
'rcx': 0,
'rsp': 0,
'rip': 2864434397,
'eflags': 0,
'csgsfs': 51,
'err': 0,
'trapno': 0,
'oldmask': 0,
'cr2': 0,
'&fpstate': 0,
'__reserved': 0,
'sigmask': 0}
Nous remarquons que pwntools sâest chargĂ© de mettre une valeur dans csgsfs afin que la restauration du contexte dâexĂ©cution ne fasse pas planter le programme.
32 bits
Comme cela a Ă©tĂ© prĂ©cĂ©demment mentionnĂ©, le SROP en 32 bits suit le mĂȘme principe quâen 64 bits, les seules diffĂ©rences sont :
- les registres ont une taille de 32 bits (merci Sherlock đ”ïžââïž) ;
- lâinstruction dâappel systĂšme est
int 0x80; - la convention dâappel est
ebx,ecxetedxau lieu derdi,rsietrdx; - lâagencement de
rt_sigreturnest un peu différent.
Ci-dessous, le script pour la version 32 bits :
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
from pwn import *
context.arch = 'i386'
io = process("./exe")
# Ces valeurs sont a adapter
int_0x80 = 0x400123 # Adresse de l'instruction `int 0x80`
addr_bin_sh = 0x400213
bourrage = 40
# Programme 32 bits mais kernel 64 bits
frame = SigreturnFrame(kernel="amd64")
frame.eax = int(constants.SYS_execve) # 0xb
frame.ebx = addr_bin_sh # parametre n°1
frame.ecx = 0 # parametre n°2
frame.edx = 0 # parametre n°3
frame.eip = int_0x80
payload = b'A' * bourrage
#payload += ... # Mettre `constants.SYS_rt_sigreturn` dans `eax`
payload += p32(int_0x80) # Appel systeme `sigreturn`
payload += bytes(frame) # Structure `rt_sigframe`
"""
io.send(payload)
(...)
"""
Finalement, une fois le principe assimilé, passer du 32 bits au 64 bits ne présente pas de difficulté majeure.
JOP (ou Jump-Oriented Programming)
Le JOP est une technique dâexploitation qui a Ă©tĂ© proposĂ©e afin dâĂ©viter dâavoir Ă dĂ©pendre de la pile et contourner dâĂ©ventuelles protections ciblant le ROP qui pourraient ĂȘtre introduites ultĂ©rieurement.
Cette technique est toujours basĂ©e sur de la rĂ©utilisation de bouts de code prĂ©sent en mĂ©moire, protection NX oblige. Toutefois, cette rĂ©utilisation de code ne va pas se faire de la mĂȘme maniĂšre.
Par exemple, les maillons de la chaĂźne de JOP nâont pas nĂ©cessairement besoin dâĂȘtre prĂ©sents sur la pile. Ils peuvent ĂȘtre placĂ©s autre part en mĂ©moire comme les sections .bss/.data ou mĂȘme le tas.
Fonctionnement global
Le processus global du JOP peut ĂȘtre rĂ©sumĂ© ainsi :
Ce schéma tiré du papier qui décrit le fonctionnement du JOP synthétise les différentes parties dont on a besoin pour faire du JOP.
Le gadget dispatcher
Tout dâabord, il y a ce que lâon appelle le gadget dispatcher. Il sâagit dâun gadget, une succession dâinstructions donc, qui permet de simuler le fonctionnement dâun pointeur dâinstruction mais de maniĂšre globale. Comme son nom lâindique, son rĂŽle est de dispatcher le flux dâexĂ©cution vers les diffĂ©rents gadgets placĂ©s dans la dispatch table.
Il peut avoir diffĂ©rentes formes en fonction du registre ou de la zone mĂ©moire qui jouera le rĂŽle de pointeur dâinstruction. Par exemple, le gadget suivant accomplit cette tĂąche via le registre rbx :
1
2
add rbx, 8
jmp [rbx]
OĂč rbx pointe Ă chaque instant vers lâune des entrĂ©es de la dispatch table.
Lâoutil xgadet possĂšde lâoption -d/--dispatcher permettant de trouver de potentiels gadget dispatcher.
La dispatch table
Câest la zone mĂ©moire qui va contenir les adresses des diffĂ©rents gadgets Ă exĂ©cuter en vue dâatteindre un certain objectif (appeler une fonction de la libc, un appel systĂšme âŠ). Câest ici que seront placĂ©s les diffĂ©rents Ă©lĂ©ments de la chaĂźne de JOP.
En fonction de ce que fait tel ou tel gadget, il sera peut-ĂȘtre nĂ©cessaire dâintercaler des donnĂ©es entre les adresses de gadgets. Auquel cas, il faudra aussi faire en sorte que le gadget dispatcher ne tente pas dâexĂ©cuter une zone mĂ©moire qui contient des donnĂ©es au lieu dâune adresse de gadget.
Mais va falloir plusieurs gadget dispatcher si on insÚre aussi des données dans la chaßne de JOP ?
Oui, sâil y a des donnĂ©es, il faudra peut-ĂȘtre trouver des instructions qui feront incrĂ©menter de 16, 24 ou 32 octets le registre pointant vers la dispatch table.
Sinon, il est possible de faire en sorte que :
- les données soient stockées à un endroit différent de la dispatch table ;
- les gadgets qui nĂ©cessitent des donnĂ©es fassent avancer eux-mĂȘmes le pointeur de dispatch table.
Les gadgets
Les gadgets propres au JOP se terminent la plupart du temps par un saut indirect tel que jmp rax, ` jmp [rdx] ... Pour autant, nous pouvons également envisager d'utiliser des instructions qui se terminent par un **appel indirect** tel que call rax ou call qword ptr [r13 + 0x10]`.
Comme le JOP nâest, normalement, pas destinĂ© Ă dĂ©pendre de la pile, nous ne pourrons pas utiliser de gadget du type pop rxx. Sauf si lâon rĂ©alise un pivot de pile auquel cas autant finir avec du bon vieux ROP đ.
Ainsi, il va falloir se dĂ©brouiller pour trouver des gadgets de JOP permettant de lire des donnĂ©es, en Ă©crire, charger des registres etc. Câest sans doute la partie la plus pĂ©nible oĂč il va falloir prendre le temps de trouver de tels gadgets.
Par ailleurs, il ne faut pas oublier un point important â ïž : tous les gadgets de la chaĂźne de JOP doivent retourner, une fois leur exĂ©cution terminĂ©e, dans le gadget dispatcher afin que ce dernier exĂ©cute le prochain gadget.
Si un gadget ne remplit pas cette condition :
- soit on le met de cÎté ;
- soit il doit faire en sorte dâexĂ©cuter lui-mĂȘme le prochain gadget de la chaĂźne.
Je vous lâaccorde, sur le papier ça a lâair dâĂȘtre une technique dâexploitation rĂ©volutionnaire. Mais en pratique, câest trĂšs compliquĂ© Ă mettre en place et, comme vous pouvez le voir, elle nĂ©cessite pas mal de bidouillage et de nombreuses contraintes Ă respecter đźâđš.
Exercice
Si vous souhaitez vous faire mal Ă la tĂȘte entraĂźner Ă faire du JOP, le challenge suivant devrait ĂȘtre ce quâil vous faut : juujuu. Il possĂšde mĂȘme une solution de rĂ©solution (en anglais).
Je nâai pas testĂ© ce challenge donc je nâai malheureusement pas dâindice Ă vous donner đ .



