È possibile scrivere shellcode in C ++?

21

Di solito scrivo programmi di assemblaggio e poi scarico i loro contenuti per ottenere lo shellcode. Quello che mi chiedevo è se potessi scrivere un programma C ++ invece di assemblare, quindi scaricare e usare quello invece dell'assembly?

    
posta John Doe 15.05.2013 - 12:01
fonte

2 risposte

32

Il kernel Linux può essere visto come una sorta di codice di shell definitivo, poiché è "iniettato" su un raw macchina (che ha solo il codice BIOS a quel punto) e quindi fornisce molte funzionalità. Quel kernel è scritto in C.

Se scrivi codice shell in C o C ++, ti imbatterai in problemi con le chiamate di biblioteca e linking , che sono due aspetti dello stesso problema.

Il codice prodotto da un compilatore C può contenere riferimenti ad altre parti del codice o funzioni o variabili esterne. Ad esempio, se si accede a una variabile static , anche se definita nel proprio codice, allora gli opcode degli assembly conterranno un "buco": a livello di assieme (quello che si ottiene con gcc -S , se si utilizza gcc come Compilatore C), vedresti opcode base "mov" e il nome della variabile. Quando è tradotto in binario, nel file oggetto (il .o o .obj , a seconda della piattaforma), il buco non è ancora stato riempito, perché l'indirizzo effettivo della variabile non è ancora noto; il file oggetto contiene una tabella (i "riposizionamenti") che informa il linker della posizione di ciascun foro che deve essere riempito nella fase di collegamento.

Il problema, naturalmente, è che non hai il lusso di un linker quando fai un codice shell. Devi produrre una sequenza di byte che è già pronta, senza buchi da riempire, e in grado di funzionare a qualunque indirizzo verrà caricato, indirizzo che non è necessariamente noto in anticipo (grazie, in parte, a ASLR ). Devi scrivere codice indipendente dalla posizione . I compilatori offrono spesso un flag da riga di comando (es. -fPIC con gcc ) ma questo non è "vero" PIC: quel tipo di codice ha ancora dei buchi da riempire da un linker, ma questa volta dinamico linker; è PIC solo in virtù del fatto che i buchi sono stati raggruppati in una tabella speciale (il GOT, ovvero "Tabella degli scostamenti globali"), rendendo l'attività più facile per il linker dinamico.

Sfortunatamente, i compilatori C e i compilatori C ++ lo fanno ancora, generando codice senza buchi. Ad esempio, un tipico compilatore C produrrà il codice con chiamate nascoste a memcpy() (la funzione di libreria standard) quando si tratta di valori di struct . C ++ lo rende peggio a causa di tutto l'armamentario delle funzionalità C ++, che sono supportate da funzioni nascoste e variabili statiche (ad esempio l'oggetto vtables).

Il kernel di Linux risolve questi problemi usando MMU . Il codice di caricamento all'avvio (che è un pezzo di assemblaggio scritto a mano) prende il controllo della MMU per assicurarsi che le pagine RAM in cui è presente il codice del kernel vengano visualizzate in un indirizzo fisso e noto nello spazio degli indirizzi: il kernel, quando compilato, è collegato a quell'assunzione esplicita; i buchi sono pieni di indirizzi che sono corretti solo se il kernel si trova effettivamente nella posizione esatta nello spazio degli indirizzi. Giocare con la MMU è un privilegio del kernel, non puoi farlo dal codice userland, figuriamoci da un codice shell.

Potresti immaginare di rendere il tuo codice shell un pezzo di assemblaggio che è, in effetti, un linker dinamico, in grado di caricare un pezzo di codice binario prodotto da un compilatore C o C ++, applicandolo in modo dinamico, cioè riempiendo i "buchi" con l'indirizzo di caricamento effettivo. Questo è ciò che accade negli exploit forniti con metasploit : l'exploit stesso installa quel pezzo di codice elegante, che può quindi installare "payloads" che sono DLL generica, scritta in lingue di livello superiore, fino a un completo server VNC .

Comunque lo metti, però, a un certo punto dovrai eseguire qualche codice PIC nell'assemblaggio manuale. Scrivere il tuo caricatore / linker è un ottimo esercizio pedagogico e può essere utilizzato per sfruttare un exploit iniziale a livelli di controllo arbitrari, ma al suo interno, sarà ancora necessario un assemblaggio manuale.

    
risposta data 15.05.2013 - 13:21
fonte
19

È perfettamente valido scrivere codice shell in qualsiasi lingua che venga compilata in base alle istruzioni del codice macchina. A condizione che per il suo funzionamento non siano necessarie librerie esterne non collegate dal programma della vittima.

Tuttavia, non è quasi mai il caso che il codice compilato direttamente (anche da solo C) sia uno shellcode valido e iniettabile. Il motivo più comune sarebbe il requisito per rimuovere NULL byte in un'iniezione buffer-stringa.

Anche il codice compilato sarà gonfio, eseguendo varie attività di tipo "contabilità" come prologhi di funzioni, impilaggio di stack frame e pulldown ecc. Tutto ciò lo rende una scelta sbagliata per lo shellcode, che in genere deve essere molto piccolo.

Quindi, dovrai scrivere il tuo programma, compilarlo, e poi sintonizzarlo a livello di codice macchina . Questo è in realtà un approccio abbastanza comune a molte persone che potrebbero trovare un assemblaggio da zero un po 'scoraggiante.

Tuttavia, nella mia esperienza, finirai per avere a che fare con lo shellcode finale così tanto che avrai familiarità con ogni byte che contiene prima che tu abbia finito, e probabilmente sarebbe stato più veloce usare l'assembly direttamente, direttamente da l'inizio.

Naturalmente, se si sta tentando di eseguire un'attività complessa e di grandi dimensioni, la scrittura in un linguaggio di livello superiore può essere allettante. In realtà, tuttavia, l'approccio migliore è di solito creare un semplice shellcode stager , che viene iniettato dall'exploit. Lo stager quindi scarica il payload reale e lo esegue. Ciò consente al vero carico utile di essere un eseguibile complesso e autonomo che collega a piacimento le proprie librerie.

Ricorda, naturalmente, da dove viene il nome; un codice shell è generalmente destinato a svolgere l'attività di uno stager, con lo stager archetipico che è execve("/bin/sh", null, null).

Come sottolinea TildalWave, anche durante la compilazione da C ++, è possibile avere un controllo a grana fine sul codice macchina di output. Il modo più diretto è quello di includere istruzioni di assemblaggio inline all'interno del sorgente C ++, per esempio;

__asm__ ("movl %eax, %ebx\n\t"
         "movb %ah, (%ecx)");

Puoi anche modificare le direttive del compilatore, e quando compili il codice da modificare dovresti disabilitare sempre l'ottimizzazione del compilatore . Poche cose sono più confuse di quanto facciano i compilatori di cose strane per ridurre il numero di istruzioni.

    
risposta data 15.05.2013 - 12:12
fonte

Leggi altre domande sui tag