hod24-ctf/remote_shell/writeup_ita.md
2024-12-03 14:22:56 +01:00

5.4 KiB

Descrizione della Sfida

RemoteShell - WIP

I've this WIP remote shell project. I want it to be super secure! (flag is in
./flag.txt)

Viene fornito solo il binario (rsh) ma la difficoltà della sfida sarebbe stata simile con il codice sorgente (main.c).

Analisi del Binario

Il binario fornito (rsh) legge il file banner.txt in un buffer globale chiamato banner. Successivamente, richiede un nome utente e una password e verifica se la password fornita inizia con "super_secret_password". Se questa verifica fallisce, il programma si interrompe. Se la verifica ha successo, la funzione stampa due stringhe e termina. L'input dell'utente viene letto dallo standard input utilizzando la funzione read. Possiamo notare che la seconda lettura (quella della password) può causare un overflow del buffer, poiché legge 128 byte in un buffer di 32 byte. Nel codice sorgente:

void remote_shell(void) {
    char username[32];
    char password[32];
    int n;

    read_string("login: ", username, sizeof(username));
    read_string("password: ", password, 128);
    if (strncmp(password, SECRET_PASSWORD, SECRET_PASSWORD_LEN) != 0)
        exit(0xc35f);
    printf("Welcome %s!\n", username);
    puts("TO BE IMPLEMENTED\n");

    return;
}

Soluzione ed exploit

Il binario non è protetto con stack canaries:

$checksec rsh
[*] '/home/garulf/ctf_deploy/rsh/rsh'
    Arch:       amd64-64-little
    RELRO:      Partial RELRO
    Stack:      No canary found
    NX:         NX enabled
    PIE:        No PIE (0x400000)
    Stripped:   No
$

Questo significa che possiamo sovrascrivere l'indirizzo di ritorno della funzione. Inoltre il binario non è PIE, quindi il testo e i dati del programma sono sempre caricati agli stessi indirizzi. Possiamo quindi fare un return-to-plt e riutilizzare le funzioni importate dal programma. Le funzioni importate:

$rz-bin -zi rsh
[Imports]
nth vaddr      bind   type   lib name
---------------------------------------------------
1   ---------- GLOBAL FUNC       __libc_start_main
2   0x00401030 GLOBAL FUNC       strncmp
3   0x00401040 GLOBAL FUNC       puts
4   0x00401050 GLOBAL FUNC       setbuf
5   0x00401060 GLOBAL FUNC       printf
6   0x00401070 GLOBAL FUNC       close
7   0x00401080 GLOBAL FUNC       read
8   ---------- WEAK   NOTYPE     __gmon_start__
9   0x00401090 GLOBAL FUNC       open
10  0x004010a0 GLOBAL FUNC       exit
...

Abbiamo quindi tutto ciò che serve per leggere la flag: open per aprire ./flag.txt, read per leggere la flag dal file in memoria e puts per scrivere la flag nello standard output. Ovviamente dobbiamo chiamare le funzioni con gli argomenti corretti. Per poter controllare gli argomenti delle funzioni, usiamo alcuni gadget ROP. Dato che si tratta di un binario a 64 bit, gli argomenti vengono passati alle funzioni utilizzando i registri rdi, rsi e rdx per il primo, secondo e terzo argomento, rispettivamente. Vediamo quali gadget possiamo utilizzare:


$ ipython -c 'from pwn import *; ROP(ELF("./rsh")).gadgets'
[*] Loaded 8 cached gadgets for './rsh'
Out[1]:
{4198419: Gadget(0x401013, ['add esp, 8', 'ret'], [8], 0xc),
 4198418: Gadget(0x401012, ['add rsp, 8', 'ret'], [8], 0xc),
 4198901: Gadget(0x4011f5, ['leave', 'ret'], ['ebp', 'esp'], 0x2540be403),
 4198781: Gadget(0x40117d, ['pop rbp', 'ret'], ['rbp'], 0x8),
 4198982: Gadget(0x401246, ['pop rdi', 'ret'], ['rdi'], 0x8),
 4198870: Gadget(0x4011d6, ['pop rdx', 'ret'], ['rdx'], 0x8),
 4199123: Gadget(0x4012d3, ['pop rsi', 'ret'], ['rsi'], 0x8),
 4198422: Gadget(0x401016, ['ret'], [], 0x4)}

Abbiamo tutti i gadget necessari, poiché possiamo controllare i registri rdi, rsi e rdx utilizzando i gadget pop rdi, pop rsi e pop rdx.

Ci serve anche un'area di memoria in cui scrivere la stringa "flag.txt" da usare come argomento per open e anche per memorizzare la flag letta dal file. Possiamo usare il buffer banner per questo.

In sintesi, il piano è:

  • leggere da standard input con destinazione il buffer banner e scrivere la stringa "flag.txt"
  • invocare la funzione open, utilizzando l'indirizzo di banner come argomento. Il numero di file descriptor restituito è prevedibile, sarà 3.
  • usare read per leggere dal file descriptor 3 (flag.txt) nel buffer banner
  • usare puts per stampare la flag

Ma c'è un problema: la quantità di stack che possiamo sovrascrivere è limitata, quindi la nostra chain non può essere più lunga di 128 bytes - len(super_secret_password).

Possiamo in parte superare questo problema terminando ogni chain ROP con l'indirizzo della funzione remote_shell. In questo modo ci verrà nuovamente chiesta la password e potremo risovrascrivere lo stack e eseguire un altro "passo".

Ad esempio, una possibile chain potrebbe essere:

rop1.raw([pop_rsi, exe.sym['banner'], pop_rdx, 0x1000, exe.sym['read_string'], exe.sym['remote_shell']])

Ma abbiamo pur sempre un limite di lunghezza per ogni "passo". In particolare, non si possono settare 3 registri, chiamare una funzione a aggiungere l'indirizzo di remote_shell in modo da re-iniziare il ciclo. Non sono riuscito a trovare un modo per leggere dal file usando meno di tre argomenti, ho quindi deciso di usare un altro approccio. Prima ho scritto una chain senza le restrizioni di lunghezza dentro il buffer di banner, poi ho usato dei gadget rop per far si che il registro rsp punti al buffer banner. In questo modo ho creato uno stack fasullo con dentro una chain di lunghezza sufficente per contenere gli steps necessari.

Puoi trovare l'exploit completo in expl.py