CreateProcess
Hum, un post un peu forcé, histoire de poser quelque chose sur ce blog. J’avoue que sur ce coup je suis un peu décu de mon taff. Bref, un échec ne peut qu’être enrichissant … Je me suis lancé dans l’idée de coder ma propre fonction CreateProcess afin de réaliser une attaque précise, malheureusement j’ai rencontré 2 gros problèmes..
D’abord pour ce mettre dans l’ambiance, on va s’écouter du Burzum, ca aide à décrasser le cerveau.
La création d’un processus n’est pas si compliqué que ca. Tout d’abord il nous faut un joli binaire, on commence gentiment en l’ouvrant avec CreateFile puis on lui associe un objet section avec l’API CreateFileMapping avec l’attribut SEC_IMAGE. Cet attribut permet de dire à l’API quelle représente une image exécutable afin que l’API nous fabrique une structure SECTION_IMAGE_INFORMATION dessus, nous verrons son utilité par la suite. Dans le code suivant j’utilise directement les API natives, j’avais envie de me prendre la tête
InitializeObjectAttributes(&ObjectAttributes, &usPath, OBJ_CASE_INSENSITIVE, NULL, NULL); //ouvre le fichier binaire Status=ZwOpenFile(&hFile, FILE_EXECUTE|SYNCHRONIZE, &ObjectAttributes, &IoBlock, FILE_SHARE_READ, FILE_SYNCHRONOUS_IO_NONALERT); if(!NT_SUCCESS(Status)) { printf("Error with ZwOpenFile : 0x%x\n", Status); goto end; } InitializeObjectAttributes(&ObjectAttributes, NULL, 0, NULL, NULL); //cree l'objet section avec l'attribut SEC_IMAGE specifique au executable images. Status=ZwCreateSection(&hSection, SECTION_ALL_ACCESS, &ObjectAttributes, 0, PAGE_EXECUTE, SEC_IMAGE, hFile); if(!NT_SUCCESS(Status)) { printf("Error with ZwCreateSection : 0x%x\n", Status); goto end; }
Maintenant qu’on a notre handle sur la section on peut crée l’objet EPROCESS himsefl avec l’API ZwCreateProcess.
NTSTATUS NtCreateProcess( __out PHANDLE ProcessHandle, __in ACCESS_MASK DesiredAccess, __in_opt POBJECT_ATTRIBUTES ObjectAttributes, __in HANDLE ParentProcess, __in BOOLEAN InheritObjectTable, __in_opt HANDLE SectionHandle, __in_opt HANDLE DebugPort, __in_opt HANDLE ExceptionPort ) Routine Description: This routine creates and initializes a process object. Arguments: ProcessHandle - Returns the handle for the new process. DesiredAccess - Supplies the desired access modes to the new process. ObjectAttributes - Supplies the object attributes of the new process. ParentProcess - Supplies a handle to the process' parent process. If this parameter is not specified, then the process has no parent and is created using the system address space. Flags - Process creation flags SectionHandle - Supplies a handle to a section object to be used to create the process' address space. If this parameter is not specified, then the address space is simply a clone of the parent process' address space. DebugPort - Supplies a handle to a port object that will be used as the process' debug port. ExceptionPort - Supplies a handle to a port object that will be used as the process' exception port.
Remarquez le commentaire au niveau du SectionHandle, si jamais celui-ci est a NULL, on fabrique un fork du process courant, ce qui dans certains cas peut s’avérer utile.
Quand on appel ZwCreateProcess, le kernel se charge de mettre en place la structure EPROCESS associé avec lui en le créant avec ObCreateObject. Son espace mémoire est crée avec MmCreateProcessAddressSpace et init avec MmInitializeProcessAddressSpace.
La suite consiste à initialiser le primary du futur process. Pour cela on doit lui allouer une mémoire qui lui servira de stack. La taille de stack est retrouvé à l’aide du PE header du binaire, ce qui est cool c’est que la fonction ZwQuerySection peut nous renvoyer une structure SECTION_IMAGE_INFORMATION qui contient les valeurs suivantes :
typedef struct _SECTION_IMAGE_INFORMATION { PVOID TransferAddress; ULONG ZeroBits; SIZE_T MaximumStackSize; SIZE_T CommittedStackSize; ULONG SubSystemType; union { struct { USHORT SubSystemMinorVersion; USHORT SubSystemMajorVersion; }; ULONG SubSystemVersion; }; ULONG GpValue; USHORT ImageCharacteristics; USHORT DllCharacteristics; USHORT Machine; BOOLEAN ImageContainsCode; BOOLEAN Spare1; ULONG LoaderFlags; ULONG Reserved[ 2 ]; } SECTION_IMAGE_INFORMATION, *PSECTION_IMAGE_INFORMATION;
2 champs nous intéresse, CommittedStackSize et MaximumStackSize. Le premier correspond à la taille de stack à allouer obligatoirement, le second à la taille max. Sachant que le thread n’aura pas forcément besoin de toute sa stack, on va éviter le gaspillage en réservant la mémoire sur une taille de MaximumStackSize tout en utilisant physiquement que CommitedStackSize de mémoire.
Plus tard, pour crée le thread avec ZwCreateThread on va avoir besoin de lui fournir une structure INITIAL_TEB :
typedef struct _INITIAL_TEB { struct { PVOID OldStackBase; PVOID OldStackLimit; } OldInitialTeb; PVOID StackBase; PVOID StackLimit; PVOID StackAllocationBase; } INITIAL_TEB, *PINITIAL_TEB;
Comme nous connaissons les besoins de la main stack, nous allons remplir cette structure. Première étape, allouer en MEM_RESERVE MaximumStackSize de mémoire avec ZwAllocateVirtualMemory dans l’espace mémoire fraichement crée. On récupère l’adresse dans le champ StackAllocationBase. Ensuite, on fabrique la pile selon le schéma suivant :
StackAllocationBase | | | StackLimit | | | | | | StackBase | +-----------------+ L'espace representé par StackAllocationBase est en MEM_RESERVE. Celui entre StackLimit et StackBase est en MEM_COMMIT|MEM_RESERVE.
Tout l’espace de StackAllocationBase est en MEM_RESERVE, celui de l’adresse la plus haute de StackAllocationBase (Stackbase) jusqu’a Stackbase-ImageInfo.CommittedStackSize est en MEM_COMMIT|MEM_RESERVE.
Cela veut dire que physiquement on aura réservé dans la ram que CommittedStackSize de mémoire, alors que les VAD (Virtual Address Descriptors) du process décriront l’espace représenter par StackAllocationBase comme utilisé.
//recup la structure ImageInfo Status=ZwQuerySection(hSection, SectionImageInformation, &ImageInfo, sizeof(ImageInfo), NULL); if(!NT_SUCCESS(Status)) { printf("Error with ZwQuerySection : 0x%x\n", Status); goto end; } printf("ImageInfo.MaximumStackSize : 0x%x\n", ImageInfo.MaximumStackSize); printf("ImageInfo.CommittedStackSize : 0x%x\n", ImageInfo.CommittedStackSize); RtlZeroMemory(&InitialTeb, sizeof(InitialTeb)); //Alloue ImageInfo.MaximumStackSize et stoque l'adresse dans InitialTeb.StackAllocationBase //les pages de sont que MEM_RESERVE Status=ZwAllocateVirtualMemory(hProcess, &InitialTeb.StackAllocationBase, 0, &ImageInfo.MaximumStackSize, MEM_RESERVE, PAGE_READWRITE); if(!NT_SUCCESS(Status)) { printf("Error with ZwAllocateVirtualMemory (1) : 0x%x\n", Status); goto end; } InitialTeb.StackBase=(PVOID)((ULONG)InitialTeb.StackAllocationBase+ImageInfo.MaximumStackSize); InitialTeb.StackLimit=(PVOID)((ULONG)InitialTeb.StackBase-ImageInfo.CommittedStackSize); n=(ULONG)(ImageInfo.CommittedStackSize); p=(PVOID)InitialTeb.StackLimit; //Commit les pages entre StackBase et StackLimit qui seront utilisées pour la stack Status=ZwAllocateVirtualMemory(hProcess, (PVOID*)&p, 0, &n, MEM_COMMIT, PAGE_READWRITE); if(!NT_SUCCESS(Status)) { printf("Error with ZwAllocateVirtualMemory (2): 0x%x\n", Status); goto end; }
Après tout ca, on peut tranquillement créer notre thread avec ZwCreateThread de proto :
NTSTATUS NtCreateThread( __out PHANDLE ThreadHandle, __in ACCESS_MASK DesiredAccess, __in_opt POBJECT_ATTRIBUTES ObjectAttributes, __in HANDLE ProcessHandle, __out PCLIENT_ID ClientId, __in PCONTEXT ThreadContext, __in PINITIAL_TEB InitialTeb, __in BOOLEAN CreateSuspended ) /*++ Routine Description: This routine creates and initializes a thread object. Arguments: ThreadHandle - Returns the handle for the new thread. DesiredAccess - Supplies the desired access modes to the new thread. ObjectAttributes - Supplies the object attributes of the new thread. ProcessHandle - Supplies a handle to the process that the thread is being created within. ClientId - Returns the CLIENT_ID of the new thread. ThreadContext - Supplies a pointer to a context frame that represents the initial user-mode context for a user-mode thread. The absence of this parameter indicates that a system thread is being created. InitialTeb - Supplies the contents of certain fields for the new threads TEB. This parameter is only examined if both a trap and exception frame were specified. CreateSuspended - Supplies a value that controls whether or not a user-mode thread is created in a suspended state. --*/
Hop le code très simple qui réalise cela :
RtlZeroMemory(&Context, sizeof(CONTEXT)); //Definit le context de depart du thread Context.SegGs=0; Context.SegFs=0x3B; //segment fs r3 Context.SegEs=0x23; //segment de data r3 Context.SegDs=0x23; Context.SegSs=0x23; Context.SegCs=0x1B; //segment de code r3 Context.EFlags=0x200; // force interrupts on, clear all else. Context.Esp=(ULONG)InitialTeb.StackBase-4; //TransferAddress est renvoye dans la structure ImageInfo, c'est l'entrypoint de notre programme Context.Eip=(ULONG)ImageInfo.TransferAddress; printf("ImageInfo.TransferAddress (EntryPoint) : 0x%x\n", ImageInfo.TransferAddress); Status=ZwCreateThread(&hThread, //ThreadHandle THREAD_ALL_ACCESS, //DesiredAccess NULL, //ObjectAttributes hProcess, //ProcessHandle &ClientId, //ClientId, &Context, //ThreadContext &InitialTeb, //InitialTeb TRUE); //CreateSuspended if(!NT_SUCCESS(Status)) { printf("Error with ZwCreateThread : 0x%x\n", Status); goto end; }
Remarquez que le thread est crée en suspended. On doit faire encore une petite opération avec de la lancer.
Entre temps nous somme censés initialiser le champ ProcessParameters du PEB du futur process à l’aide de la structure RTL_USER_PROCESS_PARAMETERS et de l’API RtlCreateProcessParameters :
typedef struct _RTL_USER_PROCESS_PARAMETERS { ULONG MaximumLength; ULONG Length; ULONG Flags; ULONG DebugFlags; HANDLE ConsoleHandle; ULONG ConsoleFlags; HANDLE StandardInput; HANDLE StandardOutput; HANDLE StandardError; CURDIR CurrentDirectory; // ProcessParameters UNICODE_STRING DllPath; // ProcessParameters UNICODE_STRING ImagePathName; // ProcessParameters UNICODE_STRING CommandLine; // ProcessParameters PVOID Environment; // NtAllocateVirtualMemory ULONG StartingX; ULONG StartingY; ULONG CountX; ULONG CountY; ULONG CountCharsX; ULONG CountCharsY; ULONG FillAttribute; ULONG WindowFlags; ULONG ShowWindowFlags; UNICODE_STRING WindowTitle; // ProcessParameters UNICODE_STRING DesktopInfo; // ProcessParameters UNICODE_STRING ShellInfo; // ProcessParameters UNICODE_STRING RuntimeData; // ProcessParameters RTL_DRIVE_LETTER_CURDIR CurrentDirectores[ RTL_MAX_DRIVE_LETTERS ]; } RTL_USER_PROCESS_PARAMETERS, *PRTL_USER_PROCESS_PARAMETERS; extern "C" NTSYSAPI NTSTATUS NTAPI RtlCreateProcessParameters( PRTL_USER_PROCESS_PARAMETERS *ProcessParameters, PUNICODE_STRING ImagePathName, PUNICODE_STRING DllPath, PUNICODE_STRING CurrentDirectory, PUNICODE_STRING CommandLine, PVOID Environment, PUNICODE_STRING WindowTitle, PUNICODE_STRING DesktopInfo, PUNICODE_STRING ShellInfo, PUNICODE_STRING RuntimeData );
On retrouve un peu la même structure que celle de PROCESS_INFORMATION qu’on fournit en entrée à CreateProcess. Pour init le PEB on utilise le code suivant :
VOID CreateProcessParameters(HANDLE hProcess, PPEB Peb, PUNICODE_STRING ImageFile) { ULONG n; PVOID p; NTSTATUS Status; PRTL_USER_PROCESS_PARAMETERS pp; Status=RtlCreateProcessParameters(&pp, ImageFile, 0, 0, 0, 0, 0, 0, 0, 0); if(!NT_SUCCESS(Status)) { printf("Error with RtlCreateProcessParameters : 0x%x\n", Status); return; } pp->Environment=CopyEnvironment(hProcess); n=pp->MaximumLength; p=0; //alloue l'espace pour la structure RTL_USER_PROCESS_PARAMETERS Status=ZwAllocateVirtualMemory(hProcess, &p, 0, &n, MEM_COMMIT, PAGE_READWRITE); if(!NT_SUCCESS(Status)) { printf("Error with ZwAllocateVirtualMemory : 0x%x\n", Status); return; } //copie la structure dans l'espace memoire du process Status=ZwWriteVirtualMemory(hProcess, p, pp, pp->MaximumLength, 0); if(!NT_SUCCESS(Status)) { printf("Error with ZwWriteVirtualMemory : 0x%x\n", Status); return; } //met a jour le champ du PEB Status=ZwWriteVirtualMemory(hProcess, (PCHAR)Peb + 0x10, &p, sizeof(p), 0); if(!NT_SUCCESS(Status)) { printf("Error with ZwWriteVirtualMemory : 0x%x\n", Status); return; } Status=RtlDestroyProcessParameters(pp); }
Enfin il reste la partie la plus mystérieuse, informé le subsystem à travers le LPC \Windows\ApiPort du process csrss.exe. Je vous avouerais que c’est la partie la moins documentée, qui, de ce fait, pose le plus de problème. D’après ce que j’ai pu récupérer, on aurait un code dans ce style là :
typedef struct _CSR_CAPTURE_HEADER { ULONG Length; PCSR_CAPTURE_HEADER RelatedCaptureBuffer; ULONG CountMessagePointers; PCHAR FreeSpace; ULONG_PTR MessagePointerOffsets[1]; // Offsets within CSR_API_MSG of pointers } CSR_CAPTURE_HEADER, *PCSR_CAPTURE_HEADER; typedef ULONG CSR_API_NUMBER; typedef struct _PBASE_CREATEPROCESS_MSG { HANDLE hProcess; HANDLE hThread; DWORD dwProcessId; DWORD dwThreadId; DWORD CreationFlags; CLIENT_ID DebuggerClientId; DWORD VdmBinaryType; }BASE_CREATEPROCESS_MSG ,*PBASE_CREATEPROCESS_MSG; typedef struct _BASE_API_MSG { PORT_MESSAGE h; PCSR_CAPTURE_HEADER CaptureBuffer; CSR_API_NUMBER ApiNumber; ULONG ReturnValue; ULONG Reserved; union { BASE_CREATEPROCESS_MSG CreateProcess; } u; } BASE_API_MSG, *PBASE_API_MSG; VOID InformCsrss(HANDLE hProcess, HANDLE hThread, ULONG pid, ULONG tid) { NTSTATUS Status; BASE_API_MSG m; PBASE_CREATEPROCESS_MSG a=(PBASE_CREATEPROCESS_MSG)&m.u.CreateProcess; RtlZeroMemory(&m, sizeof(m)); a->hProcess=hProcess; a->hThread=hThread; a->dwProcessId=pid; a->dwThreadId=tid; Status=CsrClientCallServer((PVOID)&m, 0, 0x10000, sizeof(*a)); if(!NT_SUCCESS(Status)) { printf("Error with CsrClientCallServer : 0x%x\n", Status); return; } }
Hop quand ca c’est fait, il n’y a plus qu’a lancé le thread avec ResumeThread et c’est partit. Normalement …
Le code que je fournis ne marche pas à chaque fois, j’ai un peu de mal à déterminer d’ou provient le bug et je serais content si quelqu’un pouvais me donner une piste voir même la solution.
En fait, au départ, si j’ai commencé ce code, c’était dans l’optique de crée un process à partir de rien sur le disque. Je voulais uniquement crée la section depuis la mémoire d’un process « lanceur » qui aurait servit de base pour ZwCreateProcess, apparemment dès qu’on utilise l’attribut SEC_IMAGE avec ZwCreateSection, il faut obligatoirement un handle sur un fichier exécutable. De ce fait, je n’ai pas réussit à implémenter ma petite idée, tant pis
Vous trouverez le code+binaire ici :
http://ivanlef0u.fr/repo/CreateProcess.rar
Sinon, pour jouer, quelques liens sur le petit monstre du moment, je veux bien sur parler du rooktit infectant le MBR Pour ceux qui s’ennuient vous pouvez même récupérer le binaire chez OffensiveComputing.
http://www2.gmer.net/mbr/
http://www.symantec.com/enterprise/security_response/weblog/2008/01/from_bootroot_to_trojanmebroot.html
http://www.microsoft.com/security/portal/Entry.aspx?name=VirTool:WinNT/Sinowal.A
Refs :
Windows Intenal, Chapter 6, Flow of CreateProcess
http://book.itzero.com/read/microsoft/0507/Microsoft.Press.Microsoft.Windows.Internals.Fourth.Edition.Dec.2004.internal.Fixed.eBook-DDU_html/0735619174/ch06lev1sec2.html
Kernel-mode backdoors for Windows NT
http://www.phrack.org/issues.html?issue=62&id=6&mode=txt
What Goes On Inside Windows 2000: Solving the Mysteries of the Loader
http://msdn.microsoft.com/msdnmag/issues/02/03/Loader/
Windows 2000 Native Api Reference
Example 6.2: Creating a Win32 Process
A catalog of NTDLL kernel mode to user mode callbacks, part 6: LdrInitializeThunk
http://www.nynaeve.net/?p=205
15 comments janvier 19th, 2008