Siedząc w pokoju hotelowym na PyCONie naszło mnie żeby sprawdzić co się dzieje jeśli wyczyści się procesowi całą pamięć (lub proces sam sobie ją wyczyści). "Wyczyści" w znaczeniu uwolni co się da (VirtualFree oraz UnmapViewOfFile), a resztę w miarę możliwości wyzeruje. Eksperyment miał za zadanie sprawdzić przy okazji jak zareaguje na to system, oraz inne aplikacje.

Podejście które ostatecznie zastosowałem zakłada że proces sam sobie uwalnia pamięć. Sam proces jest podzielony na dwie części. Pierwsza napisana w C++ korzysta sobie wygodnie z WinAPI i uwalnia co może, oraz wrzuca drugą część do Thread Environment Block i ją wywołuje. Natomiast druga część napisana jest w assemblerze (dialekt Netwide Assembler aka NASM) i korzysta z syscalli do odmapowania reszty pamięci. Dokładny algorytm wygląda następująco:

1. Proces wrzuca kod "drugiej części" do TEB
2. Potem robi VirtualProtect z parametrem PAGE_EXECUTE_READWRITE na całej przestrzeni użytkownika (00000000-7FFFFFFF w moim przypadku, nie korzystałem z /3G, a aplikacja była 32 bitowa).
3. Następnie robi VirtualFree z parametrem MEM_RELEASE na całej pamięci oprócz stosu głównego wątku.
4. I skacze do TEB do drugiej częsci
5. Druga część korzystając z syscalla NtUnmapViewOfSection odmapowuje resztę pamięci.
6. I wpada w nieskończoną pętlę - tak żeby można się podłączyć stosownym monitorem/debuggerem i zobaczyć co zostało.

Pojawia się kilka komplikacji. Pierwsza z nich - jak uzyskać liniowy adres TEB?

Jak wiadomo TEB znajduje się pod wirtualnym adresem 0 w segmencie FS, natomiast w C++ bez wstawek assemblera możemy operować jedynie na adresie liniowym. Z pomocą przychodzi krótka funkcja w assemblerze która robi push fs; pop eax, oraz funkcja z WinAPI GetThreadSelectorEntry - funkcja ta zwraca wpis dotyczący danego segmentu z LDT/GDT. Kod pobierający adres TEB wygląda następująco:

WORD GetFS(void) { __asm__(".ascii \"\\x0F\\xA0\\x58\""); }
 [...]
 LDT_ENTRY ldt;
 HANDLE thdl = GetCurrentThread();
 GetThreadSelectorEntry(thdl, GetFS(), &ldt);

 DWORD Off = ldt.BaseLow | (ldt.HighWord.Bytes.BaseMid << 16) | (ldt.HighWord.Bytes.BaseHi << 24);
 BYTE *Teb = (BYTE*)Off;


Dodam że na potrzeby TEB zaalokowana jest jedna mała (0x1000 bajtów) strona pamięci, natomiast nie cała ta pamięć jest wykorzystana przez strukturę TEB, więc zostaje trochę miejsca żeby umieścić tam kod. Empirycznie doszedłem do wniosku że TEB+800h jest całkiem niezłym miejscem na kod drugiej części.

Drugą komplikacją było ustalenie pozycji stosu.

Ze stosem jest taki problem, że na początku zaalokowana na jego potrzeby jest niewielka ilość pamięci, natomiast dużo większa ilość pamięci jest zarezerwowana na przyszłość (wraz z kolejnymi "push'ami" stos się rozrasta - pamięć jest doalokowywana), przez co jeżeli VirtualFree trafi nawet w obszar jeszcze "nieaktywny", to i tak cały stos zostanie uwolniony - a tego chciałbym uniknąć. Trzeba więc znaleźć całkowity rozmiar stosu. Można to zrobić na kilka sposobów:
- pobrać z TEB adres "końca" stosu (najwyższy adres), a następnie idąc od tego adresu w stronę niższych adresów, "dotykać" bajtów na nich (tak żeby wywołać alokację), i sprawdzić kiedy zostanie podniesiony exception - wtedy przestać i zapisać ostatni adres który był OK lub pobrać z TEB adres "początku" stosu (który się zmienia w zależności od faktycznej zaalokowanej wielkości stosu)
- można pobrać z TEB adres "końca" stosu, a następnie z nagłówka PE głównego exeka pobrać pole OptionalHeader.SizeOfStackReserve, wykonać proste odejmowanie, i już - problem tu jest tylko taki, że jest to pole nieobowiązkowe, i na dobrą sprawę jest tylko sugestią dla loadera PE, który może zdecydować się je zignorować
- można pobrać z TEB adres "końca" stosu, i następnie korzystając z VirtualQuery pobierać informacje o skrawkach pamięci, i cofać się strona po stronie aż do napotkania jakieś strony ze stanem MEM_FREE (stos będzie miał MEM_COMMIT lub MEM_RESERVED)
W moim przypadku skorzystałem z tej ostatniej metody, chociaż prawdopodobnie pierwsza jest kapkę lepsza (jestem otwarty na nowe lepsze pomysły w kwestii ustalania wielkości stosu ;>). Kod ją realizujący wygląda następująco:

 DWORD ThreadStackTop    = *(DWORD*)&Teb[8]; // Lower side
 DWORD ThreadStackBottom = *(DWORD*)&Teb[4]; // Higher side

 MEMORY_BASIC_INFORMATION MemInfo;
 for(;;)
 {
   VirtualQuery((LPCVOID)(ThreadStackTop), &MemInfo, sizeof(MemInfo));
   if(MemInfo.State == MEM_FREE)
     break;

   ThreadStackTop -= 0x1000;
   printf("Seeking stack size: %.8x: %.8x (%s)\r", ThreadStackTop, MemInfo.RegionSize,
       MemInfo.State == MEM_COMMIT ? "MEM_COMMIT" :
       MemInfo.State == MEM_FREE ? "MEM_FREE" : "MEM_RESERVE"

       );
 }
 putchar('\n');


Samo kopiowanie kodu (z przyzwyczajenia nazywanego przeze mnie shellcode'em) do TEB oraz wywołania VirtualProtect, VirtualFree i w końcu drugiej częsci wyglądają następująco:

 // Copy shellcode
 size_t CodeSize = 0;
 uint8_t* Code = FileGetContent("shellcode", &CodeSize);
 printf("Shellcode: %.8x bytes\n", CodeSize);

 puts("Copying shellcode...");
 memcpy(&Teb[0x800], Code, CodeSize);

 // Remove memory protection
 puts("Removing memory protection...");
 DWORD Addr = 0;
 DWORD OldPrivs;
 while(Addr < 0x80000000)
 {    
   VirtualProtect((LPVOID)Addr, 1, PAGE_EXECUTE_READWRITE, &OldPrivs);
   Addr += 0x1000;
 }

 // Free memory excluding the stack
 puts("Freeing memory...");
 Addr = 0;
 while(Addr < 0x80000000)
 {
   if(!(Addr >= ThreadStackTop && Addr <= ThreadStackBottom))
     VirtualFree((LPVOID)Addr, 0, MEM_RELEASE);
   Addr += 0x1000;
 }

 // Call the second part
 ((void(*)())&Teb[0x800])();


Od razu chciałbym zaznaczyć że VirtualFree tak na prawdę niedużo zwalnia, ponieważ większa część pamięci jest zamapowana (MapViewOfFile & co.), tak że lwią część roboty i tak wykonuje shellcode. Sam shellcode wywołuje w pętli UnmapViewOfFile (korzystając bezpośrednio z syscalla NtUnmapViewOfSection - jego numer na sztywno wklepałem pod Windowsa XP), a następnie wpada w nieskończoną pętlę. Kod shellcode'u wygląda następująco:

[bits 32]
[org 0deadbabeh]

;
; Macros
;
%macro UnmapViewOfFile 1
 push %1 ; Address
 push -1 ; Current process
 call NtUnmapViewOfSection
%endmacro

;
; Code
;
start:
 ;
 ; Unmap memory from 0 to 0x7FFFFFFF
 ;
 xor ebx, ebx
 __unmap_loop_start:

   ; Call
   UnmapViewOfFile   ebx

   ; Iterate
   add ebx, 1000h
   test ebx, ebx
   jns __unmap_loop_start ; Do not call unmap over 0x80000000

 ;
 ; Do something - now the process contains only:
 ; - TEB (unfreeable)
 ; - PEB (unfreeable)
 ; - Main stack thread
 ; - Kernel memory area (unfreeable)
 ;
 jmp short $

;
; Syscall operations
;
%define SYSCALL_NtUnmapViewOfSection 10Bh ; XP SP0/SP1/SP2/SP3

KiFastSystemCall:
 mov edx, esp
 sysenter
 ret

NtUnmapViewOfSection:
 mov eax, SYSCALL_NtUnmapViewOfSection
 call KiFastSystemCall
 retn 8


I wszystko by było dobrze, tylko że to w pewnym momencie się wyłoży ;>  

Cały problem związany jest z mechanizmem sysenter i sysexit. Mianowicie pod Windowsem XP sysexit nie wraca za sysenter, tylko pod pewien zapisany pod adresem 7ffe0304 adresik, którym zazwyczaj jest funkcja ntdll.KiFastSystemCallRet. Czyli chcąc korzystać z interfejsu sysenter/sysexit, nie można zwolnić pamięci obrazu NTDLL.dll. Co zrobić w takiej sytuacji? Mi do głowy przyszło kilka rozwiązań:
1. Wyzerować całą pamięć ntdll.dll, tylko to nieszczęsne C3 aka ret w KiFastSystemCallRet zostawić - OK działające rozwiązanie, ale wiszący w pamięci ntdll.dll "psuje krajobraz" ;D
2. W momencie gdy sysexit wróci do KiFastSystemCallRet, które przestało istnieć, podnoszony jest exception. Można by więc użyć SEH do wyłapania exceptiona, i korzystając ze struktury CONTEXT przestawić EIP w bardziej dogodne miejsce. Teoretycznie wszystko OK, praktycznie całość psuje fakt że kernel mode po otrzymaniu exceptiona nie przechodzi bezpośrednio do funkcji obsługującej wyjątek której adres jest w SEH, tylko "zleca" to zadanie funkcji ntdll.KiUserExceptionDispatcher. Czyli nic z tego.
3. Można na chwilę zapomnieć o nowoczesnym "sysenter", i skorzystać ze "staromodnego" int 0x2E, po którego obsłużeniu kernel-mode grzecznie wraca za int 0x2E. Bingo.

Poprawiona wersja shellcode'u korzystająca z ostatniego rozwiązania różni się tylko w jednym miejscu:

;
; Syscall operations
;
%define SYSCALL_NtUnmapViewOfSection 10Bh ; XP SP0/SP1/SP2/SP3

KiSlowSystemCall:
 lea edx, [esp+8]
 int 0x2E
 ret

NtUnmapViewOfSection:
 mov eax, SYSCALL_NtUnmapViewOfSection
 call KiSlowSystemCall
 retn 8




Sytuacja po tym jest całkiem niezła. Mianowicie w pamięci został tylko stos, TEB, PEB oraz nieruszalny read-only fragment pamięci należący do kernela i znajdujący się pod adresem 7ffe0000 (tam gdzie "wiszą" liczniku czasu i ścieżka do katalogu z Windowsem).

Czy można tutaj coś jeszcze zdziałać? Tak, można usunąć stos, a esp przerzucić do TEB lub PEB. Natomiast tego już nie robiłem, ponieważ w TEB i tak się ciasno zrobiło, a prawie pusty stos w niczym nie przeszkadza.

OK, programik więc działa. Natomiast jak się system zachowuje w obecności takiego tworu? Otóż póki co (za dużo testów jeszcze nie robiłem) zupełnie normalnie.
Jedynym wyjątkiem jest OllyDbg w momencie attachowania debuggera do działjącego procesu któru już pozwalniał pamięć - podczas attachowania OllyDbg tworzy na chwilę wątek w kontekscie procesu, i wywołuje kilka funkcji WinAPI - otóż nic z tego, tych funkcji nie ma już w pamięci, więc wątek Olliego rzuca wyjątkiem i pozostaje w kontekscie procesu. Stwarza to pewien wektore umożliwiający detekcję debuggera - wystarczy że proces będzie sprawdzał ile ma wątków lub czy żadna inna pamięć (TEB i stos drugiego wątku), i detekcja gotowa (chociaż przyznaje bez bicia że zbyt wygodna ta metoda nie jest).

OK, chyba tyle. Na koniec jeszcze download i kilka linków ;>

annihilate_memory.zip (7kb, źródełko + binarki)

Warto rzucić okiem:
System call optimization with the SYSENTER instruction
A catalog of NTDLL kernel mode to user mode callbacks, part 2: KiUserExceptionDispatcher

Comments:

2008-11-26 06:20:45 = user
{
Wspaniałe umiejętności RE!
}
2009-05-21 18:01:21 = noob_student_IT
{
Świetny tekst. Świetny poziom merytoryczny. Będę ciężej ćwiczyć, bo też tak chce umieć... ;)
}
2012-01-24 17:02:36 = fir
{
fenomenal
}

Add a comment:

Nick:
URL (optional):
Math captcha: 4 ∗ 6 + 8 =