Post

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 :

TechniqueSignificationRĂ©sumĂ©Quand l’utiliser ?
SROPSigreturn-Oriented ProgrammingContourner l’appel systĂšme sigreturn afin de contrĂŽler l’exĂ©cution du programmePrĂ©sence d’instructions syscall/int 0x80, libc inaccessible, etc.
JOPJump-Oriented ProgrammingUtiliser des instructions de sauts pour contrĂŽler l’exĂ©cution du programmeQuand le ROP n’est pas possible 🙄
BROPBlind ROPTechnique permettant de faire du ROP “à l’aveugle” sur un programme distant dont on ne dispose pas du binaireProgramme 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 N premiers octets de la pile en ayant la possibilitĂ© d’insĂ©rer des octets nuls ;
  • contrĂŽler eax / rax afin de pouvoir y mettre le numĂ©ro de l’appel systĂšme sigreturn ;
  • trouver l’adresse d’un gadget syscall/int 0x80
  • contrĂŽler rip/eip.

N est la taille de la structure rt_sigframe en octets. S’il n’est pas possible d’avoir exactement N octets 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/rax pour l’appel systĂšme final ;
  • mettre l’adresse de l’instruction int 0x80/syscall dans 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 :

source

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) :

  1. 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_sigframe est en đŸ””, en dessous de l’adresse de l’instruction syscall ;
  2. tout d’abord, il va falloir trouver et exĂ©cuter un gadget qui chargera la valeur 0xf dans rax, Ă©tant donnĂ© qu’il s’agit du numĂ©ro de l’appel systĂšme sigreturn. Ce gadget 🔮 peut ĂȘtre de diffĂ©rent type, par exemple, un pop rax ; ret fera l’affaire ;
  3. l’appel systĂšme sigreturn est exĂ©cutĂ© avec la structure forgĂ©e rt_sigframe dans la pile. Les valeurs les plus intĂ©ressantes Ă  contrĂŽler sont en gras, notamment rdi, rsi et rdx pour les arguments du prochain appel systĂšme Ă  exĂ©cuter. rax pour y mettre le numĂ©ro du prochain appel systĂšme, ici execve. Enfin, rip afin d’exĂ©cuter immĂ©diatement Ă  la sortie de sigreturn le prochain appel systĂšme ;
  4. Ă©tant donnĂ© que sigreturn s’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’instruction syscall implique l’appel de execve("/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Ăč sigreturn va ĂȘ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, ecx et edx au lieu de rdi, rsi et rdx ;
  • l’agencement de rt_sigreturn est 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 😅.

This post is licensed under CC BY-NC 4.0 by the author.