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.
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é) | Alimente | Devient |
|---|---|---|
r15 | mov rdx, r15 | rdx (3e arg) |
r14 | mov rsi, r14 | rsi (2e arg) |
r13 | mov edi, r13d | 32 bits de poids faible de rdi (1er arg) |
r12, rbx | call [r12 + rbx*8] | le pointeur de fonction à appeler |
rbp | cmp rbp, rbx | contrô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_mainou d'autres objets CRT qui déplacent par hasard des valeurs dansrdx. - ret2dlresolve — abuser des structures de résolution paresseuse de l'éditeur de liens dynamique pour résoudre et appeler
systempar 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 :
| Mitigation | Effet |
|---|---|
| Canari de pile | Arrête l'écrasement qui amorce la chaîne |
| PIE + ASLR | Randomise les adresses du gadget csu et de la GOT ; nécessite une fuite |
| Full RELRO | L'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 / CET | Avorte 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_initpour chargerrdx/rsi/rdi(poids faible) et faire un appel contrôlé — le correctif standard pour unpop rdxmanquant. - Mettez
rbx=0,rbp=1pour sortir de la boucle, et pointezr12vers une entrée GOT contenant la fonction à appeler. mov edi, r13dest sur 32 bits, doncrdiest 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.