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