Tochę o usuwaniu plików

Jakiś czas temu zainteresowałem się kwestią sposobów usuwania plików, a raczej możliwościami wywołania takiego procesu w „trudnych przypadkach”. Co mam na myśli mówiąc „trudne przypadki” :

1. Zabicie procesu malware’u i próba usunięcia jego pliku wykonywalnego jest utrudniona z tego względu, że innym proces wciąż odnawia proces naszych zainteresowań.

2. Aplikacja/malware posiada otwarty handler do pliku z takimi uprawnieniami (brak FILE_SHARE_DELETE) , które uniemożliwiają jego usunięcie.

3.Usuniecie pliku nie jest możliwe ze względów na hooki , czy to na poziomie r3 czy r0, które chronią wybrane pliki malware’u.

Poniżej przedstawię parę sposobów na radzenie sobie w takich „trudnych sytuacjach” spowodowanych przez nie do końca poprawnie działającą aplikacje lub malware.

1.

Zabicie procesu malware’u i próba usunięcia jego pliku wykonywalnego jest utrudniona z tego względu, że innym proces wciąż odnawia proces naszych zainteresowań.

Porzućmy kwestie jaką przedstawiam w tytule tego punktu i wyobraźmy sobie prostszy scenariusz o identycznym charakterze.
Załóżmy teoretyczną sytuację, że istnieje malware, który składa się dwóch modułów na potrzeby przykładu nazwane następująco:
matka.exe – „dobrze ukryty” plik w systemie, który uruchamia się przy każdym uruchomieniu komputer’a następnie tworzy zdalny wątek w jednym z procesów systemowych i stamtąd wykonuje swoje zadania. Jego jedynym zadaniem jest uruchamianie modułu zle_dziecko.exe oraz monitorowanie jego stanu. W przypadku kiedy, proces zle_dziecko.exe zostanie zabity ukryty gdzieś w czeluściach systemu wątek matki tworzy jego nowy proces.

zle_dziecko.exe – moduł posiadający najważniejsza funkcjonalność keyloggingu,itp uruchamiany i utrzymywany przy życiu przez moduł matka.exe.

Dodatkowo scenariusz postępowania zakłada, że naszym priorytetem jest w pierwszej kolejności pozbycie się złośliwego procesu zle_dziecko.exe ponieważ co każdą minutę wysyła on cenne skradzione dane z naszego systemu na drop host.
Niestety pojawiają się drobne problemy, ponieważ po zabiciu procesu zle_dziecko.exe jest on wciąż odnawiany, przez co „nie możliwe” staje się usunięcie jego pliku wykonywalnego ze względu na „blokady” ze strony systemu.

[+]Sposób na obejście tego przypadku:
Analizując driver filesystemu (w moim przypadku źródłem zainteresowań był NTFS[ntfs.sys]) dochodzimy do wniosków, że tak naprawdę dwa pola w FILE_OBJECT’e pliku wykonywalnego, który posiada aktywny proces mogą uniemożliwiać usunięcie powiązanego z nim pliku.

kd>dt _FILE_OBJECT [ecx]
ntdll!_FILE_OBJECT
   +0x000 Type             : 5
   +0x002 Size             : 112
   +0x004 DeviceObject     : 0x88fd1900 _DEVICE_OBJECT
   +0x008 Vpb              : 0x88fa5780 _VPB
   +0x00c FsContext        : 0xe12f33d0 
   +0x010 FsContext2       : 0xe189f870 
   +0x014 SectionObjectPointer : 0x88c4d964 _SECTION_OBJECT_POINTERS
   +0x018 PrivateCacheMap  : (null) 
   +0x01c FinalStatus      : 0
   +0x020 RelatedFileObject : (null) 
   +0x024 LockOperation    : 0 ''
   +0x025 DeletePending    : 0 ''
   +0x026 ReadAccess       : 0x1 ''
   +0x027 WriteAccess      : 0 ''
   +0x028 DeleteAccess     : 0 ''
   +0x029 SharedRead       : 0x1 ''
   +0x02a SharedWrite      : 0x1 ''
   +0x02b SharedDelete     : 0x1 ''
   +0x02c Flags            : 0x840042
   +0x030 FileName         : _UNICODE_STRING "\zle_dziecko.exe"
   +0x038 CurrentByteOffset : _LARGE_INTEGER 0x0
   +0x040 Waiters          : 0
   +0x044 Busy             : 0
   +0x048 LastLock         : (null) 
   +0x04c Lock             : _KEVENT
   +0x05c Event            : _KEVENT
   +0x06c CompletionContext : (null) 

„File object dla pliku zle_dziecko.exe posiadający aktywny proces”

Czytając specyfikacje FILE_OBJECT na msdn’e:
DeleteAccess
A read-only member (nie był bym tego taki pewny :P). If TRUE, the file associated with the file object has been opened for delete access. If FALSE, the file has been opened without delete access. This information is used when checking and/or setting the share access of the file.
Źródło: http://msdn.microsoft.com/en-us/library/ff545834(VS.85).aspx

I bardzo ważna informacja znajdująca się na dole strony:

During the processing of an IRP_MJ_CREATE request, a file system driver calls the IoSetShareAccess routine (if the client is the first to open the file) or the IoCheckShareAccess routine (for subsequent clients that want to share the file). IoSetShareAccess and IoCheckShareAccess update the ReadAccess, WriteAccess, and DeleteAccess members to indicate the access rights that are granted to the client if the client has exclusive access to the file.
Źródło: http://msdn.microsoft.com/en-us/library/ff545834(VS.85).aspx

Ważną wskazówką tutaj dla nas jest to, iż sprawdzenie flagi DeleteAccess odbywa się podczas przetwarzania zapytania IRP_MJ_CREATE przez driver( już teraz dopowiem, że zapytanie to jest wysyłane również podczas usuwania pliku, a procedura obsługująca je w NTFS’e nazywa sie NtfsFsdCreate) w funkcji IoCheckShareAccess i tam należy badać jej wpływ na dalszy przebieg operacji usuwania.
Oczywiści z badań wynikło, że ustawienie tej flagi na TRUE pozwala przejść check w IoCheckShareAccess pozytywnie.

SectionObjectPointer :_SECTION_OBJECT_POINTERS
w tej strukturze interesuje nas tak naprawdę jeden wskaźnik

ImageSectionObject : PVOID
Opaque pointer to an image section object (that is, a CONTROL_AREA structure) that is used to track state information for an executable file stream. Memory manager sets this member whenever an executable image section is created for the stream. A NULL value indicates that the executable image is currently not in memory; this value, however, can change at any time.
Źródło: http://msdn.microsoft.com/en-us/library/ff563687(VS.85).aspx

W skrócie, pointer ten ma wartość NULL wtedy, gdy plik wykonywalny nie jest załadowany do pamięci(nie posiada aktywnego procesu). W innym przypadku pointer ten jest ustawiony, a próba skasowania pliku wykonywalnego kończy się kodem błędu STATUS_SHARING_VIOLATION.

[+]Implementacja: [źródło inspiracji gmer.sys]
Żeby móc manipulować na file object’e musimy zejść w czeluści piekielne naszego OS’u , czyli ring0.
Oczywiście dokonamy tego pisząc driver, w którym przykładowa funkcja realizująca nasze zadanie będzie wyglądała tak :

NTSTATUS ForceDelete(wchar_t *path)
{

	HANDLE fileHandle;
	NTSTATUS result;
	IO_STATUS_BLOCK ioBlock;
	DEVICE_OBJECT *device_object;
	void* object = NULL;
	OBJECT_ATTRIBUTES fileObjectAttr;
	wchar_t deviceName[14];
	UNICODE_STRING uDeviceName;
	UNICODE_STRING uPath;

	//switch context to UserMode
	EPROCESS *eproc = IoGetCurrentProcess();
	KeAttachProcess(eproc);
	
	//e.g "\??\C:\"
	memset(deviceName,0,sizeof(deviceName));
	wcsncpy(deviceName,path,7);

	RtlInitUnicodeString(&uDeviceName,deviceName); //initialize volume name 	
	RtlInitUnicodeString(&uPath,path); //initialize path
	
	/* get volume handle */
	InitializeObjectAttributes(&fileObjectAttr,
								uDeviceName,
								OBJ_CASE_INSENSITIVE,
								NULL,
								NULL);

	result = ZwOpenFile(&fileHandle,
						SYNCHRONIZE,
						&fileObjectAttr,
						&ioBlock,
						FILE_SHARE_READ,
						FILE_SYNCHRONOUS_IO_NONALERT|FILE_DIRECTORY_FILE);

	if(result != STATUS_SUCCESS)
	{
		DbgPrint("Some problems with open file ;[");
		goto _end;
	}

    if ( !ObReferenceObjectByHandle(fileHandle, 0, 0, 0, &object, 0) )
    {

      device_object = IoGetBaseFileSystemDeviceObject(object); //get the lowest-level file system volume device object associated with a file
      ObfDereferenceObject(object);
    }
	
    ZwClose(fileHandle);	
		
	InitializeObjectAttributes(&fileObjectAttr,
							   &uPath,
							   OBJ_CASE_INSENSITIVE,
							   NULL,
							   NULL);

	result = IoCreateFileSpecifyDeviceObjectHint(
			   &ileHandle,
			   SYNCHRONIZE | FILE_WRITE_ATTRIBUTES | FILE_READ_ATTRIBUTES | FILE_READ_DATA,
			   &fileObjectAttr,
			   &ioBlock,
			   0,
			   0,
			   FILE_SHARE_READ | FILE_SHARE_WRITE |FILE_SHARE_DELETE,
			   FILE_OPEN,
			   FILE_NON_DIRECTORY_FILE | FILE_SYNCHRONOUS_IO_NONALERT,
			   0,
			   0,
			   CreateFileTypeNone,
			   0,
			   IO_IGNORE_SHARE_ACCESS_CHECK,
			   device_object);
	if(result != STATUS_SUCCESS)
	{
		DbgPrint("error in IoCreateFileSpecifyDeviceObjectHint");
		goto _end;
	}

	result = ObReferenceObjectByHandle(fileHandle, 0, 0, 0, &object, 0);

	if(result != STATUS_SUCCESS)
	{
		DbgPrint("error in ObReferenceObjectByHandle");
		ZwClose(fileHandle);
		goto _end;
	}
	/* set up proper FILE_OBJECT members  to allow file delete */
	((FILE_OBJECT*)object)->SectionObjectPointer->ImageSectionObject = 0; //.exe file is not in memory
	((FILE_OBJECT*)object)->DeleteAccess = 1; //we have access to delete file (.exe file doesn't have active process)
	 
	 /*Try of file deletion*/
	 result = ZwDeleteFile(&fileObjectAttr);
	
	if(result != STATUS_SUCCESS)
	{
		DbgPrint("\nerror in ZwDeleteFile");
	}
	ObDereferenceObject(object);
	ZwClose(fileHandle);
	 
_end:
	//return to r0
	KeDetachProcess();
	return result;
}

Myślę, że małe wyjaśnienia przydadzą się w następujących linijkach:

device_object = IoGetBaseFileSystemDeviceObject(object); //get the lowest-level file system volume device object associated with a file

pobieramy najniżej położony na stosie device_object związany z naszym plikiem, po to by później w:

IoCreateFileSpecifyDeviceObjectHint(
(...)
device_object);

DeviceObject [in, optional]

A pointer to the device object to which the create request is to be sent. The device object must be a filter or file system device object in the file system driver stack for the volume on which the file or directory resides. This parameter is optional and can be NULL. If this parameter is NULL, the request will be sent to the device object at the top of the driver stack.
Żródło: http://msdn.microsoft.com/en-us/library/ff548289(VS.85).aspx
gdzie nasz device object przedstawia się następująco:

//88f8b020 == device_object
kd>; !devobj 88f8b020
Device object (88f8b020) is for:
  \FileSystem\Ntfs DriverObject 88f733d0
Current Irp 00000000 RefCount 0 Type 00000008 Flags 00000000
DevExt 88f8b0d8 DevObjExt 88f8b880 
ExtensionFlags (0000000000)  
AttachedDevice (Upper) 88fc4b30 \FileSystem\sr
Device queue is not busy.

kd< !devstack 88f8b020
  !DevObj   !DrvObj            !DevExt   ObjectName
  88fc4b30  \FileSystem\sr     88fc4be8  
>88f8b020  \FileSystem\Ntfs   88f8b0d8

Myślę, że cały sens użycia tego api wyjaśnia jego opis:

The IoCreateFileSpecifyDeviceObjectHint routine is used by file system filter drivers to send a create request only to the filters below a specified device object and to the file system.

Co dzięki temu zyskujemy? Większą pewność, że nasze zapytanie nie zostanie po drodze zmodyfikowane przez sterownik rootkit’a podpiętego pod stos urządzeń powiązanych z file systemem.

2.

Wiele aplikacji/malware posiada otwarte handler’y do pliku z takimi uprawnieniami (brak FILE_SHARE_DELETE) , które uniemożliwiają jego usunięcie.

W tym przypadku nasz trick z ustawieniem odpowiednich flag dla FILE_OBJECT’u niestety nie zda egzaminu, ze względu na to, że file system w swoich wewnętrznych strukturach utrzymuje np.ReferenceCount dla takiego pliku i nie pozwoli na jego usunięcie, jeśli nie jest ono równe 0. Modyfikacja wewnętrznych struktur NTFS’a nie jest kwestią trywialną imo ze względu na sama kontrukcje tych struktu:
Root FCB jest tworzone dla kazdego plik tylko w jednej instacji -> FCB zawiera pointer na LCB , który łączy FCB z SCB, gdzie może występować wiele SCB posiadających wiele dzieci typu FCB,a do tego wszystkiego dochodzi jeszcze CCB tworzone w zależności od wymaganych praw na otwieramy pliku :D. Dorzućmy jeszcze do tego wszystkiego to, że struktury te nie są udokumentowane i myślę, że już wystarczająco zachęciłem was do ich ręcznej modyfikacji ;).

My podejdziemy do tego tematu w prostszy sposób, i do tego jakże logiczny, bo przecież jak pewna aplikacja ma jakiś otwarty handler do pliku i przez to tego pliku usunąć się nie da ,to co należało by zrobic? 😀 Oczywiście zamknąć jej ten uchwyt! :D. I tak też uczynimy.

[+]Sposób na obejście tego przypadku:
Dla tego przypadku zaproponuje dwa rozwiązania, jedno na poziomie ring 3 oraz kolejne na poziomie ring0.
Zajmijmy się w pierwszej kolejności r3.

[+]Implementacja
W potrzebie tworzenia aplikacji, która zrealizuje dla nas założone zadanie przychodzi nam api:

__kernel_entry NTSTATUS
NTAPI 
NtQuerySystemInformation (
    IN SYSTEM_INFORMATION_CLASS SystemInformationClass,
    OUT PVOID SystemInformation,
    IN ULONG SystemInformationLength,
    OUT PULONG ReturnLength OPTIONAL
    );

oraz jego nieudokumentowana klasą informacji wraz z strukturami:

#define SystemHandleInformation 0x10

typedef struct _SYSTEM_HANDLE_TABLE_ENTRY_INFO {
    USHORT UniqueProcessId;
    USHORT CreatorBackTraceIndex;
    UCHAR ObjectTypeIndex;
    UCHAR HandleAttributes;
    USHORT HandleValue;
    PVOID Object;
    ULONG GrantedAccess;
} SYSTEM_HANDLE_TABLE_ENTRY_INFO, *PSYSTEM_HANDLE_TABLE_ENTRY_INFO;

typedef struct _SYSTEM_HANDLE_INFORMATION {
    ULONG NumberOfHandles;
    SYSTEM_HANDLE_TABLE_ENTRY_INFO Handles[ 1 ];
} SYSTEM_HANDLE_INFORMATION, *PSYSTEM_HANDLE_INFORMATION;

Korzystając z jej dobrodziejstwa otrzymamy tablice zawierającą wszystkie procesy oraz uchwyty należące do tych procesów wraz z typami obiektów, z jakimi te handler’y są powiązane.
Te wszystkie informacje pozwolą nam na zlokalizowanie otwartych uchwytów do naszego pliku w każdym procesie istniejącym w systemie, dzięki czemu po przez drobne sztuczki uzyskamy możliwość ich zamknięcia, a następnie usunięcia pliku.

//funkcja główna
void fileToDelete(char *filePath)
{
	closeAllHandles(filePath);

	//after closing all handler to the file ,try to delete it
	DeleteFileA(filePath);

}

Rzućmy okien na funkcje closeAllHandles:

void closeAllHandles(char* filePath)
{
	SYSTEM_HANDLE_INFORMATION *shiTable;
	char foundPath[MAX_PATH];
	
	shiTable = enumerateHandles();
	for(int i = 0; i < shiTable->NumberOfHandles;i++)
	{		
		if( isFile( shiTable->Handles[i].ObjectTypeIndex) )
		{
			HANDLE fileHandle = getDuplicatedHandle(shiTable->Handles[i].UniqueProcessId,
															    shiTable->Handles[i].HandleValue,
																false);
				if(!fileHandle)
					continue;//probably a bad handle
					
			if(!GetFileNameFromHandle(fileHandle,foundPath))//check whether we have been able to receive filePath
					continue;
			if( !strcmp(filePath,foundPath) )
			{
				//handle to interesting file has been found,,,let's close it
				getDuplicatedHandle(shiTable->Handles[i].UniqueProcessId,
									shiTable->Handles[i].HandleValue,
									true);
			}
		}
	}
	
	VirtualFree(shiTable,0,MEM_RELEASE);		
}

 

postępując w kolejności:

SYSTEM_HANDLE_INFORMATION * enumerateHandles()
{			
	SYSTEM_HANDLE_INFORMATION shi = {0};
	SYSTEM_HANDLE_INFORMATION *shiTable;	
	unsigned long shiTableSize;

	int status  = NtQuerySystemInformation((SYSTEM_INFORMATION_CLASS)SystemHandleInformation,
						     &shi,
							 sizeof(SYSTEM_HANDLE_INFORMATION),
							 &shiTableSize);
	//TODO: check status							 
	shiTable = (SYSTEM_HANDLE_INFORMATION*)VirtualAlloc(0,shiTableSize,MEM_RESERVE|MEM_COMMIT,PAGE_READWRITE);
	status = NtQuerySystemInformation((SYSTEM_INFORMATION_CLASS)SystemHandleInformation,
									  shiTable,
									  shiTableSize,
									  0);
	
	return shiTable;
}

Myślę, że tutaj wszystko jest jasne, a funkcja getDuplicatedHandle będzie bardziej interesująca. Ahh
jeszcze mała funkcja isFile:

bool isFile(unsigned char type)
{
	return type == 0x1c; //for XP
} 

Sprawdzamy tutaj czy element opisany przez SYSTEM_HANDLE_TABLE_ENTRY_INFO jest plikiem. Jeśli tak, to przechodzimy dalej:

HANDLE getDuplicatedHandle(unsigned long procID,unsigned long handleValue,bool closeSourceHandle)
{
	HANDLE rFile = (HANDLE)handleValue;
	HANDLE rProc;
	HANDLE duplicatedHandle;
	rProc = OpenProcess(PROCESS_DUP_HANDLE,0,procID);
	unsigned long option = closeSourceHandle?(DUPLICATE_CLOSE_SOURCE | DUPLICATE_SAME_ACCESS):DUPLICATE_SAME_ACCESS;
	
	DuplicateHandle(rProc,rFile,GetCurrentProcess(),&duplicatedHandle,0,0,option);			
	CloseHandle(rProc);	
	if(closeSourceHandle)
	{
		CloseHandle(duplicatedHandle);
		return 0;
	}

	return duplicatedHandle;
}

Do funkcji trafiają trzy parametry:

unsigned long procID      // ID procesu do którego należy handler
unsigned long handleValue // wartość handler’a
bool closeSourceHandle    // flaga, na podstawie której będzie wybierana opcja dla api DuplicateHandle

W pierwszej kolejności uzyskujemy handler do procesu, który być może, trzyma otwarty handler do interesującego nas pliku. Istotną kwestią tutaj jest flag DesiredAccess ustawiona na PROCESS_DUP_HANDLE, ponieważ zawiązana ona jest z mechanizmem, który wykorzystamy za chwilę.
Api DuplicateHandle pozwala nam na stworzenie kopi uchwytu pochodzącego z naszego procesu lub też (co jest najciekawsze) z procesu zdalnego. Bez tej możliwości wartość handler’a uzyskanego przez NtQuerySystemInformation była by dla nas bezużyteczna, ponieważ nie moglibyśmy skorzystać z niej w obrębie naszego procesu. Myślę, że co niektórym z was nasuwa się kolejna idea, a mianowicie możliwość wykorzystania zdalnych wątków. Fakt, w ten sposób również można podejść do tego tematu, jednak mi bardziej do gustu przypadło to rozwiązanie.

Opcja DuplicateHandle, która najbardziej nas tutaj interesuje to:

DUPLICATE_CLOSE_SOURCE

dzięki, której jak się domyślacie stworzymy kopie uchwytu przy jednoczesnym zamknięciu uchwyty źródłowego!;]. Oczywiście nie chcemy zamykać wszystkich uchwytów każdego procesu, dlatego też, przed sprawdzeniem, z jakim plikiem powiązany jest dany handler ustawiamy tylko opcje:

DUPLICATE_SAME_ACCESS

Po uzyskaniu klonu uchwytu wypadałoby, w jakiś sposób teraz ustalić, z jakim plikiem powiązany jest ten handler. Z pomocą przychodzi tutaj funkcja
GetFileNameFromHandle
źródło: http://msdn.microsoft.com/en-us/library/aa366789(VS.85).aspx
, a tak naprawdę api związane z mapowaniem pliku, dokładnie mówiąc:

GetMappedFileName

Ja delikatnie zmodyfikowałem tą funkcje na swoje potrzeby i teraz prezentuje się ona następująco

BOOL GetFileNameFromHandle(HANDLE hFile,char* filePath) 
{
  BOOL bSuccess = FALSE;
  char pszFilename[MAX_PATH+1];
  HANDLE hFileMap;

  // Create a file mapping object.
  hFileMap = CreateFileMapping(hFile, 
                    NULL, 
                    PAGE_READONLY,
                    0, 
                    1,
                    NULL);

  if (hFileMap) 
  {
    // Create a file mapping to get the file name.
    void* pMem = MapViewOfFile(hFileMap, FILE_MAP_READ, 0, 0, 1);

    if (pMem) 
    {
      if (GetMappedFileNameA (GetCurrentProcess(), 
                             pMem, 
                             pszFilename,
                             MAX_PATH)) 
      {

        // Translate path with device name to drive letters.
        char szTemp[1024];
        szTemp[0] = '0';

        if (GetLogicalDriveStringsA(BUFSIZE-1, szTemp)) 
        {
          char szName[MAX_PATH];
          char szDrive[3] = " :";
          BOOL bFound = FALSE;
          char* p = szTemp;

          do 
          {
            // Copy the drive letter to the template string
            *szDrive = *p;

            // Look up each device name
            if (QueryDosDeviceA(szDrive, szName, MAX_PATH))
            {
              UINT uNameLen = strlen(szName);

              if (uNameLen < MAX_PATH) 
              {
                bFound = strnicmp(pszFilename, szName, uNameLen) == 0;

                if (bFound && *(pszFilename + uNameLen) == '\\') 
                {					           
					strcpy(filePath,szDrive);
					strcpy(filePath+strlen(szDrive),pszFilename + strlen(szName));
                }
              }
            }

            // Go to the next NULL character.
            while (*p++);
          } while (!bFound && *p); // end of string
        }
      }
      bSuccess = TRUE;
      UnmapViewOfFile(pMem);
    } 

    CloseHandle(hFileMap);
  }
  CloseHandle(hFile);

  return(bSuccess);
}
&#91;/cpp&#93;
Istotną zmianą jest tutaj usunięcie check’u polegającego na sprawdzaniu wielkości pliku po przez <strong>GetFileSize</strong>. Niestety ale dla pewnych handler’ow wskazujących na takie twory jak:
<strong>78: File  (---)   \Device\NamedPipe\net\NtControlPipe15</strong>
<strong>GetFileSize zawisa.</strong>
Kiedy uda nam się zdobyć ścieżkę do pliku kwestia jest już prosta. Wykonujemy check czy dana ścieżka jest identyczna do tej podanej przez nas 

Jeśli tak, to wywołujemy wcześniej omawianą funkcję <strong>duplicateHandle</strong>, lecz tym razem z parametrem <strong>closeSourceHandle ustawionym na true</strong>


<font color="green">[+]Analogiczne rozwiązanie lecz w świecie ring0[inspiracja gmer.sys].</font>
Początek prezentuje sie identycznie jak wersja dla r3.

void r0_fileToDelete(wchar_t *filePath)
{	
	r0_closeAllHandles(filePath);
	/*
		after closing all handles to the file ,try to delete it
		ofc,,,before it convert DosPath to NtPath (just add \??\ before filePath)
	*/
	//ZwDeleteFile();
}

Przyjrzyjmy się obecnej wersji closeAllHandles:

void r0_closeAllHandles(wchar_t* filePath)
{
	MY_SYSTEM_HANDLE_INFORMATION *shiTable;
	EPROCESS *eprocess;
	unsigned long i;
	FILE_OBJECT *file;
	OBJECT_NAME_INFORMATION *objectNameInformation = 0;
	unsigned long filePathLength = wcslen(filePath);

	shiTable = enumerateHandles();
	for(i = 0; i < shiTable->NumberOfHandles;i++)
	{		
		if( isFile( shiTable->Handles[i].ObjectTypeIndex) )
		{					
			file = (FILE_OBJECT*)shiTable->Handles[i].Object;			
			if(!file || file->FileName.Length == 0)
				continue;
			
			getFullPathName(file,&objectNameInformation);

			if((objectNameInformation->Name.Length/2 ) != filePathLength)
				continue;
					
			if( !_wcsnicmp(filePath,(wchar_t*)objectNameInformation->Name.Buffer,filePathLength) )
			{
				PsLookupProcessByProcessId((HANDLE)shiTable->Handles[i].UniqueProcessId,&eprocess);
				KeAttachProcess(eprocess);//switch context to process one
				ZwClose((HANDLE)shiTable->Handles[i].HandleValue);
				KeDetachProcess();				
			}
		}
	}
	
	ExFreePoolWithTag(shiTable,0xdeadbeef);
}

Tak jak poprzednio używamy enumerateHandles do zdobycia informacji o handler’ach, jedyna zmianą tutaj jest sposób alokacji:

MY_SYSTEM_HANDLE_INFORMATION * enumerateHandles()
{			
	MY_SYSTEM_HANDLE_INFORMATION shi = {0};
	MY_SYSTEM_HANDLE_INFORMATION *shiTable;	
	unsigned long shiTableSize;

	int status  = ZwQuerySystemInformation((SYSTEM_INFORMATION_CLASS)SystemHandleInformation,
						     &shi,
							 sizeof(MY_SYSTEM_HANDLE_INFORMATION),
							 &shiTableSize);
	//TODO: check status		
	shiTable = (MY_SYSTEM_HANDLE_INFORMATION*)ExAllocatePoolWithTag(NonPagedPool,shiTableSize,0xdeadbeef);
	status = ZwQuerySystemInformation((SYSTEM_INFORMATION_CLASS)SystemHandleInformation,
									  shiTable,
									  shiTableSize,
									  0);
	
	return shiTable;
}

Poruszając się w głab funkcji r0_closeAllHandles napotykamy linijkę gdzie:

file = (FILE_OBJECT*)shiTable->Handles[i].Object;

Możemy skorzystać z tej dogodności, jaką daje nam obecność w r0, a mianowicie do wyciagnięcia interesujących nas informacji o ścieżce do pliku przy pomocy FILE_OBJECT’u dostarczonego w _SYSTEM_HANDLE_INFORMATION przez ZwQuerySystemInformation. Niestety sytuacja na wstępie nie jest taka kolorowa jak mogła by się wydawać, chodzi tutaj o nie dogodność związana z formatem sciężki do pliku z jaki powiązany jest FILE_OBJECT:

kd> dt _FILE_OBJECT 88e280d0
ntdll!_FILE_OBJECT
   +0x000 Type             : 5
   +0x002 Size             : 112
   +0x004 DeviceObject     : 0x88f76900 _DEVICE_OBJECT
   +0x008 Vpb              : 0x88fa5780 _VPB
   +0x00c FsContext        : 0xe154c0d0 
   +0x010 FsContext2       : 0xe154c228 
   +0x014 SectionObjectPointer : 0x88ee87ac _SECTION_OBJECT_POINTERS
   +0x018 PrivateCacheMap  : (null) 
   +0x01c FinalStatus      : 0
   +0x020 RelatedFileObject : (null) 
   +0x024 LockOperation    : 0 ''
   +0x025 DeletePending    : 0 ''
   +0x026 ReadAccess       : 0x1 ''
   +0x027 WriteAccess      : 0x1 ''
   +0x028 DeleteAccess     : 0 ''
   +0x029 SharedRead       : 0 ''
   +0x02a SharedWrite      : 0 ''
   +0x02b SharedDelete     : 0 ''
   +0x02c Flags            : 0x40008
   +0x030 FileName         : _UNICODE_STRING "\WINDOWS\system32\config\default.LOG"
   +0x038 CurrentByteOffset : _LARGE_INTEGER 0x0
   +0x040 Waiters          : 0
   +0x044 Busy             : 0
   +0x048 LastLock         : (null) 
   +0x04c Lock             : _KEVENT
   +0x05c Event            : _KEVENT
   +0x06c CompletionContext : (null)

Tak jak widać ścieżka do pliku nie zawiera informacji o partycji, na której plik się znajduje. Jak uzyskać literę partycji? Jest na to parę sposobów.
Najwygodniejszym wydaje się użycie IoQueryFileDosDeviceName. getFullPathName w wersji dla r0 prezentuje się następująco:

NTSTATUS getFullPathName(FILE_OBJECT* fileObject,OBJECT_NAME_INFORMATION **objectNameInformation)
{
	 
	 return IoQueryFileDosDeviceName(fileObject,
							  objectNameInformation
							  );
	//remeber about _OBJECT_NAME_INFORMATION deallocation!!!					      
}

Funkcja ta zwróci nam wypełniona strukturę, która tak naprawdę zawiera jedno pole UNICODE_STRING, zawierające pełną ścieżkę do pliku. Po porównaniu ścieżek do plików i znalezieniu tej interesującej nas przystępujemy tak jak poprzednio do zamknięcia uchwytu. I tu kolejna zmiana:

				PsLookupProcessByProcessId((HANDLE)shiTable->Handles[i].UniqueProcessId,&eprocess);
				KeAttachProcess(eprocess);//switch context
				ZwClose((HANDLE)shiTable->Handles[i].HandleValue);
				KeDetachProcess();

Tak jak odrazu widać nie posługujemy się DuplicateHandle jak poprzednio, lecz korzystamy z możliwości zmiany kontekstu(KeAttachProcess) na kontekst procesu, w którym dany handler istnieje i zamykamy go w trywialny sposób korzystając z ZwCloseHandle. Po zamknięciu wszystkich otwartych handler’ów do naszego pliku jesteśmy gotowi na jego usunięcie przy pomocy ZwDeleteFile.

3.

Usuniecie pliku nie jest możliwe ze względów na hooki , czy to na poziomie r3 czy r0, które chronią wybrane pliki malware’u.

Niestety czasami bywa tak, że chcemy usunąć jakiś plik, lecz nie jest to możliwe, a przeglądając listę otwartych handlerów wszystkich procesów nie odnajdujemy tam interesującego nas pliku o_0. So WTF?!.
Problemem mogą tu być hooki zakładane przez malware na różne api, których zdaniem jest zwracania kodu błędu w przypadku kiedy użytkownik zapragnie usunąć plik znajdujący się na liście plików chronionych przez malware.

Zanim przejdziemy do konkretnego przypadku i moim zdaniem jednego z lepszych rozwiązań w tym przypadku, prześledźmy z grubsza drogę, jaką musi przebyć żądanie usunięcia pliku z pod r3.

Ring3
------------
DeleteFile
|
V
NtOpenFile
|
V
KiFastSystemCall
|
V
Ring0
---------
NtDeleteFile
|
V
ObOpenObjectByName
|
V
ObpCaptureObjectCreateInformation
|
V
ObpLookupObjectName
|
V
ObpLookupDirectoryEntry
|
V
IopParseDevice
|
V
IofCallDriver
|
V 
IRP_MJ_CREATE in filter drivers
|
V
File system driver (e.g NtfsFsdCreate)

Tak jak widać ścieżka od DeleteFile do NtfsFsdCreate jest dość długa i na każdej z jej etapów mogą pojawić się hooki uniemożliwiające usunięcie wybranego przez malware pliku. Sprawdzanie wszystkich tych api w r3 i r0 pod kątem istnienia hooku na nich jest dość czasochłonne i czasem nie trywialne. Było by wspaniale gdybyśmy mogli w pewien sposób oszukać hooki malware’u sprawdzające ścieżkę do pliku.
Czy jest to możliwe? Okazuje się, że od samego DeleteFile do NtfsFsdCreate ścieżka wskazująca na plik do usunięcia nie musi wskazywać na plik dokładnie ten, który chcemy usunąć!!!
Dopiero, w NtfsFsdCreate(a dokładnie w NtfsCommonCreate) ścieżka ta ma znaczenie i jest rozwiązywana na konkretne wewnętrzne struktury NTFS’owe.
Co nam to daje?
Wspaniałą możliwość ominięcia masy istniejących hook’ow na api zaprezentowanych powyżej!!!.

[+]Sposób na obejście tego przypadku:
Wystarczy uzyskać bezpośredni dostęp do urządzenia file systemu, a następnie, podmienić w obiekcie sterownika handler(założyć hook…jak kto woli) obsługujący IRP_MJ_CREATE (w naszym przypadku jest to NtfsFsdCreate) na własny. Nasz handler będzie tylko i wyłącznie odpowiedzialny za podmianę ścieżki do pliku, którą wcześniej ustalimy, jako coś ala „turn on trigger”(np. \ice_psuje.exe). Ścieżkę tą, podmienimy na taką by wskazywała ona na plik malware’u, który rzeczywiście jest naszym celem.

[+]Implementacja (inspiracja gmer.sys)

void hook_NtfsFsdCreate(wchar_t* filePath)
{
	HANDLE fileHandle;
	NTSTATUS result;
	IO_STATUS_BLOCK ioBlock;
	DEVICE_OBJECT *device_object;
	void* object = NULL;
	OBJECT_ATTRIBUTES objectAttr;
	UNICODE_STRING uDeviceName;
	wchar_t deviceName[14];

	//switch context to UserMode
	EPROCESS *eproc = IoGetCurrentProcess();
	KeAttachProcess(eproc);

	//initialize file to delete variable
	g_fileToDelete = filePath;
	g_fileToDelete += 6; //take from \??\C:\zlo only \zlo

	//e.g "\??\C:\"
	memset(deviceName,0,sizeof(deviceName));
	wcsncpy(deviceName,filePath,7);
	wcsncpy(g_tmpFile,filePath,7);

	RtlInitUnicodeString(&uDeviceName,deviceName);
	InitializeObjectAttributes(&objectAttr,
								&uDeviceName,
								OBJ_CASE_INSENSITIVE,
								NULL,
								NULL);

	result = ZwOpenFile(&fileHandle,
						SYNCHRONIZE,
						&objectAttr,
						&ioBlock,
						FILE_SHARE_READ,
						FILE_SYNCHRONOUS_IO_NONALERT|FILE_DIRECTORY_FILE);

	if(result != STATUS_SUCCESS)
	{
		DbgPrint("Some problem with open file ;[");
		goto _end;
	}

    if ( !ObReferenceObjectByHandle(fileHandle, 0, 0, 0, &object, 0) )
    {
      device_object = IoGetBaseFileSystemDeviceObject(object);
      ObfDereferenceObject(object);
    }

	hook_it(device_object);
	
_end:
	KeDetachProcess();}

Myślę, że ta część kodu jest już wam dobrze znana. Nowością może tu być linia gdzie pojawia się zmienna globalna:

	g_fileToDelete = filePath;
	g_fileToDelete += 6; //take from \??\C:\zlo only \zlo

Ustawiamy zmienna globalną g_fileToDelete, którą później będziemy wykorzystywać w hooku, na ścieżkę do pliku, który rzeczywiście chcemy skasować. W lini:

	wcsncpy(g_tmpFile,filePath,7);

gdzie, zmienna g_tmpFile prezentuje się domyślnie tak:

wchar_t *g_tmpFile = L"\\??\\C:\\ice_psuje.exe";

dokonujemy podmiany domyślnej wartości woluminu na tą, na której znajduje się plik naszych zainteresowań.

void hook_it(DEVICE_OBJECT *device_object)
{	
	NTSTATUS result;
	OBJECT_ATTRIBUTES fileObj;
	UNICODE_STRING uTmpFile;
	HANDLE fileHandle;
	IO_STATUS_BLOCK ioStatus;	
	FILE_BASIC_INFORMATION fileBasicInfo;

	//initialize variables related with fake file
	RtlInitUnicodeString(&uTmpFile,g_tmpFile);

	InitializeObjectAttributes(&fileObj,
								&uTmpFile,
								OBJ_CASE_INSENSITIVE,
								NULL,
								NULL);
	
	//save original MJ functions	
	create  = device_object->DriverObject->MajorFunction[0];

	result = ZwCreateFile(&fileHandle,
						  4,
						  &fileObj,
						  &ioStatus,
						  0,
						  0x80,
						  2,
						  3,
						  0x20,
						  0,
						  0);	
	if(result != STATUS_SUCCESS)
		return;
	
	ZwClose(fileHandle);
	
	//install hooks
	device_object->DriverObject->MajorFunction[0]    = HookedNtfsFsdCreate;  

	ZwDeleteFile(&fileObj);//launche our hooks
	
	//restore original MJ functions
	device_object->DriverObject->MajorFunction[0]    = create;
	
}

W kolejności:

//save original MJ functions	
	create  = device_object->DriverObject->MajorFunction[0];

Zachowujemy oryginalny handler IRP_MJ_CREATE, następnie tworzymy plik, który będzie trigerem. Instalujemy nasz hook:

	//install hooks
	device_object->DriverObject->MajorFunction[0]    = HookedNtfsFsdCreate;  

Chcąc skorzystać z dobroci naszego hook’a, wywołujemy ZwDeleteFile na \??\X:\ice_psuje.exe.

//global
wchar_t *g_tmpFileName = L"\\ice_psuje.exe";

//--------
//(...)
NTSTATUS NTAPI
HookedNtfsFsdCreate(PDEVICE_OBJECT DeviceObject,
	   PIRP Irp)
{	
	unsigned long proper_file = 0; //whether we get Irp with interesting for us file_object ?
	UNICODE_STRING bfile; //backup file
	UNICODE_STRING uMalwareFile;
	NTSTATUS rez;
	PFILE_OBJECT pFileObject;
	
	RtlInitUnicodeString(&uMalwareFile,g_fileToDelete);
	pFileObject = Irp->Tail.Overlay.CurrentStackLocation->FileObject;
	if(!pFileObject)
		goto _end;

	if((pFileObject->FileName.Length / 2) != wcslen(g_tmpFileName))
		goto _end;
			
	if(!_wcsnicmp(g_tmpFileName,
				  (wchar_t*)pFileObject->FileName.Buffer,
				  (pFileObject->FileName.Length / 2)))
	{
			
		    bfile = pFileObject->FileName;//backup current string
			pFileObject->FileName = uMalwareFile; //hook string value 
			proper_file = 1;			
	}

_end:

	rez = Call_NtfsFsdCreate(DeviceObject,Irp);
	if(pFileObject)
	{
		if(proper_file)
		{
			pFileObject->FileName = bfile;
		}
	}
	return rez;
}

Wydaje mi się, że po wcześniejszych opisach sprawa jest jasna. Jeżeli do NtfsFsdCreate trafia FILE_OBJECT, którego FileName to „\ice_psuje.exe” następuje podmiana tej ścieżki na ścieżkę podana jako parametr hook_NtfsFsdCreate i zachowaną w:

	//initialize file to delete variable
	g_fileToDelete = filePath;
	g_fileToDelete += 6; //take from \??\C:\zlo only \zlo

Uff w końcu dobrnęliśmy do końca tego skromnego postu ;). Ma nadzieje, że ktoś dotarł do tego miejsca, znalazł coś ciekawego dla siebie i zostawi jakiś feedback ;).
Enjoy

Ten wpis został opublikowany w kategorii Analiza, Aplikacja, RE i oznaczony tagami , , , , , , , , , , . Dodaj zakładkę do bezpośredniego odnośnika.

5 odpowiedzi na „Tochę o usuwaniu plików

  1. Grzonu pisze:

    No wpis bardzo ciekawy 😉
    Ja zastanawiałem się ostatnio nad jeszcze jedna kwestia czy dałoby sie to samo osiagnac np. przez bezposrednia modyfikacje MFT, no ale nigdy nie ma czasu.

    • Icewall pisze:

      Ciesze sie, ze sie podobalo;).
      Co do Twoich rozwazan. To rowniez przez mysl przeszlo mi taki podejscie lecz jest ono dosc mocno inwazyjne i w pewnych okolicznosciach moze sporo namieszac;).
      Mozna tutaj pokwapic sie o usuniecie/uszkodzenie rekordu w MFT, lub np. wykorzystaniu api do defragmentacji i bezposrednim nadpisaniu klastrow danego pliku.
      Inside Windows NT Disk Defragmenting.

  2. Pingback: Tweets that mention Icewall's blog » Tochę o usuwaniu plików -- Topsy.com

  3. Borys Łącki pisze:

    Rewelacyjnie szczegółowy i wyczerpujący opis. Oby tak dalej i wcześniej :]

  4. Icewall pisze: