Niecałe trzy lata temu opublikowałem post pod tytułem "Wolność dla wszystkiego, czyli kompletne anihilowanie pamięci procesu", który był opisem próby uwolnienia wszystkich regionów pamięci należących do danej Windowsowej aplikacji, z poziomu tejże aplikacji. Wczoraj dostałem maila od raphael<AT>gfreedom<DOT>org z opisem (udanej) próby wykonania podobnego eksperymentu pod systemem opartym o jądro Linux. Dostałem również zgodę na opublikowanie jego maila w formie postu gościnnego, co niniejszym czynie.

Zanim przejdę do rzeczy, krótka uwaga skierowana do co bardziej początkujących czytelników mojego bloga: zwróćcie uwagę jak autor maila poradził sobie z napotkanymi problemami - często nie trzeba znać najbardziej optymalnej metody na osiągnięcie czegoś, wystarczy trochę pomyśleć i pokombinować :)

OK, without further ado:

-- Post gościnny by raphael<AT>gfreedom<DOT>org --


Gdy czytałem posta na Twoim blogu "Wolność dla wszystkiego, czyli kompletne anihilowanie pamięci procesu" (http://gynvael.coldwind.pl/?id=91) zapamiętałem tylko kilka rzeczy: zostaje mały fragment pamięci i działa to na Windowsie. Oczywiście pomyślałem o Linuksie. Ale nie wiedziałem jak to zorganizować. Potrzebne mi były funkcje jak VirtualFree().
Potem przeczytałem "Simplified Assembly Loader" (http://gynvael.coldwind.pl/?id=387), i w kodzie źródłowym było wywołanie mmap().
Myśląc dalej, jeżeli jest mmap(), to musi być munmap(). Jest. Pojawiła się wtedy koncepcja. Brakowało mi tylko informacji co konkretnie mam zwolnić. (przypominam, że zapomniałem co wcześniej przeczytałem; zrobiłeś VirtualProtect() a następnie VirtualFree() na całej pamięci. Swoją drogą nie wiem czy to będzie działać na Linuksie przez mprotect() oraz munmap() ).

I przypomniałem sobie o katalogu /proc. W pliku /proc/<PID>/maps są wszystkie mapy pamięci z adresami. Nie zastanawiając się więcej zacząłem pisać.
Zasada działania programu:
* Alokujemy 4KB pod adresem 0x08000000.
* Zapisujemy fragment kodu pod adres 0x08000800 (przypadkowo wybrałem taki sam adres)
* Czytamy plik maps, początek oraz koniec mapy (pomijając nasz obszar)
* Zapisujemy informacje, początek i długość, pod adresy 0x08000000 oraz 0x08000100
* Zapisujemy liczbę wpisów pod adres 0x080007fc
* jmp lub call pod 0x08000800
* Kod wykonuje munmap() dla każdego zestawu.
* Potem pause()
* I exit() dla pewności.

Wygląda to tak:
Kod asm:
bits 32
org 0x08000800                   ;pod ten adres zostanie załadowany kod
begin:
mov esi, [0x080007fc]            ;ładujemy ilość wpisów
loop:
and esi, esi                     ;sprawdzamy czy jakieś zostały
jz inf                           ;jeżeli nie opuszczamy pętle
dec esi
mov eax, 91                      ;munmap()
mov ebx, [0x08000000+esi*4]
mov ecx, [0x08000100+esi*4]
int 0x80
jmp loop
inf: mov eax, 29                 ;pause()
int 0x80
mov eax, 1                       ;exit()
int 0x80

Kompilujemy: nasm -f bin -o code.bin code.asm

I wstawiamy jako string do poniższego kodu.
Miałem tutaj zonka, hexdump odwraca bajty. np 0x12, 0x34, 0x56, 0x78 wyświetla jak 0x34, 0x12, 0x78, 0x56. Napisałem małe narzędzie które utworzy cały string, 15 min pracy.
Nie znalazłem disasemblera dla czystych plików binarnych, napisałem własny z wykorzystaniem libdisasm, 30 min pracy.
Kod w C:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

#include <sys/mman.h>

unsigned char* code=
{
"\x8b\x35\xfc\x07\x00\x08\x21\xf6\x74\x18\x4e\xb8\x5b\x00\x00\x00\x8b\x1c\xb5\x00\x00"
"\x00\x08\x8b\x0c\xb5\x00\x01\x00\x08\xcd\x80\xeb\xe4\xb8\x1d\x00\x00\x00\xcd\x80\xb8"
"\x01\x00\x00\x00\xcd\x80"
};

unsigned int code_length=48;

int main()
{
  int pid;
  void* pointer;
  FILE* file;
  char name[24];
  char buf[128];
  int i=0;
  unsigned int begin,end,length;
  unsigned long* start_table=(unsigned long*)0x08000000;
  unsigned long* length_table=(unsigned long*)0x08000100;
  unsigned int total=0;
  int* num;
  pid=getpid();
  printf("PID: %d\n",pid);
  pointer=(void*)0x08000000;
  printf("Alokacja jednej ramki pod adresem %p\n",pointer);
  pointer=mmap(pointer,0x1000,PROT_READ|PROT_WRITE|PROT_EXEC,MAP_FIXED|MAP_ANONYMOUS|MAP_PRIVATE,0,0);
  if(pointer!=(void*)0x08000000)
  {
      perror("mmap() : ");
      exit(2);
  }
  printf("Alokacja udana: %p\n",pointer);
  //tutaj kopiujemy kod do ramki
  printf("Zapisywanie kodu pod adresem 0x08000800\n");
  memcpy((void*)0x08000800,code,code_length);


  printf("Wyznaczanie pliku nazwy pliku... ");
  sprintf(name,"/proc/%d/maps",pid);
  printf("%s\n",name);
  printf("Otwieranie pliku... ");
  file=fopen(name,"r");
  if(file==NULL)
  {
      printf("Nie można otworzyć pliku %s\n",name);
      exit(2);
  }
  printf("OK\n");

  printf("Odczyt map pamięci...\n");
  while(!feof(file))
  {
      if(fgets(buf,128,file)==NULL) break;
      sscanf(buf,"%x-%x",&begin,&end);
      length=end-begin;
      total+=length;
      printf("Segment begin=0x%08x, end=0x%08x, length=0x%08x  %u\n",begin,end,length,length);
      if(begin!=0x08000000)
      {
          start_table[i]=begin;
          length_table[i]=length;
          i++;
      }
  }
  fclose(file);
  printf("Wszystkich segmentów:            %d\n",i+1);
  printf("Całkowity rozmiar:               0x%08x , %dB\n",total,total);
  printf("Rozmiar segmentów do usunięcia:  0x%08x , %dB\n",total-4096,total-4096);
  printf("Naciśnij enter aby kontynuować...");
  munlockall();
  num=(int*) 0x080007fc;
  *num=i;
  getchar();
  __asm__ __volatile__ ("jmp 0x08000800\n");
  return 0;
}


I w pamięci pozostaje tylko 4KB pamięci.

raphael:/proc/17369 $ cat maps
08000000-08001000 rwxp 00000000 00:00 0
bfee2000-bfee2000 rw-p 00000000 00:00 0
raphael:/proc/17369 $

Ten drugi wpis ma rozmiar 0, i powstaje podczas jednego z munmap(), chyba tego dla stosu.
Ogólnie to bezpośrednio nic pożytecznego nie zrobiłem, ale nauczyłem się masę rzeczy.
Bawiąc się dalej z mmap(), dowiedziałem się, że nie możemy zaalokować pamięci pod adresem 0x00000000. (Dokładniej to tylko root może). A jest to spowodowane tym, że odwołanie do NULL, powinno powodować błąd.
Testowałem malloc() (Tak, proces może mieć tylko 3GB pamięci), ale po zaalokowaniu tej pamięci, nie jest ona zabierana z puli systemu. Tzn przed uruchomieniem programu jest powiedzmy 10% używanej pamięci RAM, tyle samo jest po zaalokowaniu pamięci. Dopiero jakaś operacja na pamięci (read write) zabiera, chyba stronę, z puli systemu. Nie wiem, dlaczego Linuks tak działa, ale jestem w trakcie szukania.

-- The end --



I tyle ;)

Comments:

2011-08-31 12:40:47 = zjaadc
{
Być może trochę głupie pytanie - ale w jakim celu tworzony był własny disasembler? Kod asm obsługujący wywoałania systemowe w pętli został napisany ręcznie, skompilowany, a następnie wklejony w postaci szesnastkowej jako string do kodu w C. Nie bardzo widać jaką rolę odgrywał w tym całym eksperymencie własny disasembler.
}
2011-08-31 20:30:24 = vnd
{
Nie nazwałbym tego disassemblerem, to swego rodzaju shellcode. Jest on umieszczany w osobnym buforze, ponieważ jest później kopiowany i uruchamiany spod zmmapowanego adresu, a to po to, aby usunąć z pamięci sekcję .text gdzie domyślnie lokowane są wszystkie inne funkcje.

Anyway, zadałbym inne pytanie. Czy jest jakiś inny powód używania C, poza tym, żeby zagwarantować sobie obiekty do usunięcia? ;) Innymi słowy, czy takich samych rezultatow z punktu widzenia celu eksperymentu nie da `_start: jmp _start` skompilowane pod nasmem?
}
2011-09-01 07:48:18 = zjaadc
{
No właśnie - zależy co jest rozumiane pod pod pojęciem disassebler. Sądząc po tym, że autor użył libdisasm to pewnie chodzi o właściwe rozumienie - narzędzie, które na wejściu dostaje kod binarny a na wyjściu daje zrozumiałe dla człowieka tekstowe mnemoniki instrukcji wraz z operandami. Shellcode - tak, ten "string" jest shellcodem. Ale nadal nie widać w jakim celu tworzony i używany był ten dissasembler, który z resztą nigdzie tu nie jest pokazany.

Jeśli chodzi o powód używania C, to jak dla mnie:
1. Sparsowanie liczby i rozmiarów regionów pamięci z procfs
2. Stworzenie "środowiska" dla wykonania shellcode'u, który niejako "z zewnątrz" (w sensie, że z innego, nowego, anonimowego regionu pamięci) wyczyści całą dotychczasową pamięć procesu
}
2011-09-01 07:59:30 = antekone
{
objdump -D -b binary -mi386 -Maddr32,data32 code.bin
}
2011-09-01 09:02:52 = lukasz1235
{
Jest jeszcze ndisasm
}
2011-09-01 12:49:59 = vnd
{
zjaadc, nie chodzi tu o disassembler w znaczeniu przekładu kodu maszynowego na mnemoniki. Swoją drogą libdisasm w ogóle nie jest tutaj konieczne. Problem stanowiło wyświetlenie pliku binarnego w postaci czytelnej i możliwej do umieszczenia w kodzie źródłowym. Można to osiągnąć przez hexdump z parametrem -C, czy też pisząć prosty skrypt.

Co do C, gdyby program nie byłby utworzony przez wysokopoziomowy kompilator tj. gcc, to nie byłoby potrzeby usuwania jakichkolwiek regionów pamięci ;) nazwałem to shellcodem, bo sposób użycia najbardziej przypominał działanie tego typu kodów, jednak równie dobrze ten sam kod mógłby być umieszczony w sekcji .text i jego uruchominie nie wymagałoby żadnych dodatkowych przygotowań.
}
2011-09-01 15:22:48 = lRem
{
> Tzn przed uruchomieniem programu jest powiedzmy 10% używanej pamięci RAM, tyle samo jest po zaalokowaniu pamięci. Dopiero jakaś operacja na pamięci (read write) zabiera, chyba stronę, z puli systemu. Nie wiem, dlaczego Linuks tak działa, ale jestem w trakcie szukania.

*Lazy allocation* jest jednym z podstawowych ficzerów zarządzania pamięcią pod Linuksem. Zdaje się można to było wyłączyć gdzieś w /proc/ (może z jakimś patchem, vserver?).
}
2011-09-01 15:44:28 = raphael
{
Jeżeli zależy wam na wyniku eksperymentu to jest
KOD:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/mman.h>
/*
bits 32
org 0x08000000
mov eax, 91
mov ebx, 0x08001000
mov ecx, 0xb7ffefff
int 0x80
mov eax, 29
int 0x80
mov eax, 1
int 0x80
*/
unsigned char* code=
{
"\xb8\x5b\x00\x00\x00\xbb\x00\x10\x00\x08\xb9\xff\xef\xff\xb7\xcd"
"\x80\xb8\x1d\x00\x00\x00\xcd\x80\xb8\x01\x00\x00\x00\xcd\x80"
};
unsigned int code_length=31;
int main()
{
unsigned char* pointer=(unsigned char*)0x08000000;
pointer=mmap(pointer,0x1000,PROT_READ|PROT_WRITE|PROT_EXEC,MAP_FIXED|MAP_ANONYMOUS|MAP_PRIVATE,0,0);
memcpy(pointer,code,code_length);
__asm__ __volatile__ ("jmp 0x08000000");
return 0;
}
WYNIK:
raphael:/proc/8993 $ cat maps
08000000-08001000 rwxp 00000000 00:00 0
raphael:/proc/8993 $

Dla mnie ważny był sam proces poznawania zarządzania pamięcią. To jest efektem ubocznym.

Po co disassembler? Program powodował segfault, sprawdzam dmesg, rejestr IP, i muszę mieć każdą instrukcję w hex osobno, aby sprawdzić która powoduje segfault. IP był w środku instrukcji, więc podejrzenia padły na hexdump.
Na stronie libdisasm jest example 20 LOC, wystarczyło przepisać, dodać kolumnę z adresami, i kolumnę z hex.

@vnd: System utworzy stos, może inne obszary pamięci, sprawdziłem:
KOD:
1 section .text
2 global _start
3 _start:
4 jmp _start
WYNIK:
raphael:/proc/9440 $ cat maps
08048000-08049000 r-xp 00000000 08:07 10462292 /home/raphael/nop
b78eb000-b78ec000 r-xp 00000000 00:00 0 [vdso]
bff1f000-bff40000 rwxp 00000000 00:00 0 [stack]
raphael:/proc/9440 $

@lukasz1235: o ndisasm przypomniał mi również Gyn
@antekone: a jednak objdump to potrafi, a ja nie czytałem manulala, korzystałem z pomocy w programie. I nie pomyślałem o tym, że trzeba podać więcej info na temat kodu, objdump jest przecież wieloplatformowy.

@vnd: Można jeszcze pobawić się z tworzeniem plików ELF od zera, tam można modyfikować Program Header Table, czyli informację dla kernela, jak utworzyć mapy pamięci dla procesu.

Nie napisałem jeszcze o kilku rzeczach które wpadły mi do głowy. Narazie przychodzi mi tylko jedna, W systemie z ALSR, możemy poznać adres dna stosu podczas działania programu. Ten wpis jest oczywiście w pliku maps.
}

Add a comment:

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