JPEG of Death güvenlik açığı nasıl çalışır?


94

Windows XP ve Windows Server 2003'te GDI + 'ya karşı üzerinde çalıştığım bir proje için ölüm JPEG olarak adlandırılan daha eski bir istismar hakkında okuyorum .

İstismar aşağıdaki bağlantıda iyi açıklanmıştır: http://www.infosecwriters.com/text_resources/pdf/JPEG.pdf

Temel olarak, bir JPEG dosyası, (muhtemelen boş) bir açıklama alanı ve COM boyutunu içeren iki baytlık bir değer içeren COM adlı bir bölüm içerir. Yorum yoksa, boyut 2'dir. Okuyucu (GDI +) boyutu okur, ikisini çıkarır ve yığındaki yorumları kopyalamak için uygun boyutta bir arabellek ayırır. Saldırı, 0alana bir değer yerleştirmeyi içerir . GDI + çıkarır 2bir değere yol açan, bir -2 (0xFFFe)olan işaretsiz bir tamsayı dönüştürülmüş olur 0XFFFFFFFEgöre memcpy.

Basit kod:

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

malloc(0)Üçüncü satırda, öbek üzerindeki ayrılmamış belleğe bir işaretçi döndürmesi gerektiğini gözlemleyin . 0XFFFFFFFEBayt ( 4GB!!!!) yazmak programı nasıl çökertmez? Bu, yığın alanının ötesine ve diğer programların ve işletim sisteminin alanına yazıyor mu? O zaman ne olacak?

Anladığım kadarıyla memcpy, nkarakterleri hedeften kaynağa kopyalıyor . Bu durumda, kaynak, yığında öbek hedef olmalı ve nbir 4GB.


malloc, yığıntan bellek ayıracaktır. bence istismar
memcpy'den

Sadece bir yan not olarak: o oluyor değil daha ziyade işaretsiz bir tamsayı değeri (4 bayt), ancak çıkarma tanıttığı memcpy.
devir

1
Önceki cevabımı canlı bir örnekle güncelledim. mallocEd boyutu sadece 2 bayt yerine olup 0xFFFFFFFE. Bu muazzam boyut, tahsis boyutu için değil, yalnızca kopya boyutu için kullanılır.
Neitsa

Yanıtlar:


96

Bu güvenlik açığı kesinlikle bir yığın taşmasıydı .

0XFFFFFFFE bayt (4 GB !!!!) yazmak programı nasıl çökertmez?

Muhtemelen olacaktır, ancak bazı durumlarda çökme meydana gelmeden önce istismar etmek için zamanınız olur (bazen programı normal çalışmasına geri döndürebilir ve çökmeyi önleyebilirsiniz).

Memcpy () başladığında, kopya ya diğer yığın bloklarının ya da yığın yönetimi yapısının bazı bölümlerinin (örneğin boş liste, meşgul listesi, vb.) Üzerine yazacaktır.

Bir noktada kopya, tahsis edilmemiş bir sayfayla karşılaşır ve yazma sırasında bir AV'yi (Erişim İhlali) tetikler. GDI + daha sonra yığın içinde yeni bir blok tahsis etmeye çalışacaktır (bkz . Ntdll! RtlAllocateHeap ) ... ama yığın yapılarının hepsi artık karışıktır .

Bu noktada, JPEG görüntünüzü dikkatlice oluşturarak, kontrollü verilerle yığın yönetimi yapılarının üzerine yazabilirsiniz. Sistem yeni bloğu tahsis etmeye çalıştığında, muhtemelen boş listeden bir (ücretsiz) bloğun bağlantısını kaldıracaktır.

Blok (özellikle) bir flink (İleri bağlantı; listedeki sonraki blok) ve yanıp sönen (Geri bağlantı; listedeki önceki blok) işaretçilerle yönetilir. Hem flink'i hem de yanıp sönmeyi kontrol ediyorsanız, ne yazabileceğinizi ve nerede yazabileceğinizi kontrol ettiğiniz olası bir WRITE4'e (Ne / Nerede koşulunu yazın) sahip olabilirsiniz.

Bu noktada, bir işlev işaretçisinin üzerine yazabilir ( SEH [Yapılandırılmış İstisna İşleyicileri] işaretçileri, 2004'te o zamanlar tercih edilen bir hedefti) ve kod yürütme elde edebilirsiniz.

Heap Corruption: A Case Study blog gönderisine bakın .

Not: Serbest listeyi kullanarak istismar hakkında yazmış olsam da, bir saldırgan diğer yığın meta verilerini kullanarak başka bir yol seçebilir ("yığın meta verileri", sistem tarafından yığını yönetmek için kullanılan yapılardır; flink ve yanıp sönme, yığın meta verilerinin parçasıdır), ancak bağlantı kaldırma sömürü muhtemelen "en kolay" olandır. Google'da "yığın istismarı" araması bununla ilgili çok sayıda çalışma döndürür.

Bu, yığın alanının ötesine ve diğer programların ve işletim sisteminin alanına yazıyor mu?

Asla. Modern işletim sistemi, sanal adres alanı kavramına dayanmaktadır, bu nedenle her işlemin, 32 bitlik bir sistemde 4 gigabayta kadar belleği adreslemeye olanak tanıyan kendi sanal adres alanı vardır (pratikte bunun yalnızca yarısını kullanıcı alanında elde edersiniz, gerisi çekirdek içindir).

Kısacası, bir işlem başka bir işlemin belleğine erişemez (bunun dışında bir hizmet / API aracılığıyla çekirdeği istemesi dışında, ancak çekirdek, arayanın bunu yapma hakkına sahip olup olmadığını kontrol edecektir).


Bu güvenlik açığını bu hafta sonunda test etmeye karar verdim, böylece saf spekülasyondan ziyade neler olup bittiğine dair iyi bir fikir edinebilirdik. Güvenlik açığı artık 10 yaşında, bu yüzden bu cevapta sömürü kısmını açıklamamama rağmen bunun hakkında yazmanın uygun olduğunu düşündüm.

Planlama

En zor görev, 2004'te olduğu gibi yalnızca SP1 içeren bir Windows XP bulmaktı :)

Ardından, aşağıda gösterildiği gibi yalnızca tek bir pikselden oluşan bir JPEG görüntüsü indirdim (kısalık için kesin):

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
[...]

Bir JPEG resmi, (segmentlere sokan) ikili işaretlerden oluşur. Yukarıdaki resimde, FF D8SOI ( Görüntünün Başlangıcı) işaretçisi, FF E0örneğin, bir uygulama işaretleyicisidir.

Bir markör segmentindeki ilk parametre (SOI gibi bazı markörler hariç), uzunluk parametresi dahil ve iki baytlık markör hariç olmak üzere markör segmentindeki bayt sayısını kodlayan iki baytlık bir uzunluk parametresidir.

SOI'nin FFFEhemen arkasına bir COM işaretçisi (0x ) ekledim , çünkü işaretçilerin kesin sırası yoktur.

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
[...]

COM segmentinin uzunluğu 00 00, güvenlik açığını tetikleyecek şekilde ayarlanmıştır . Ayrıca, güvenlik açığından "yararlanılırken" kullanışlı olacak şekilde, yinelenen bir model olan, onaltılık olarak 4 baytlık bir sayı olan COM işaretinin hemen arkasına 0xFFFC bayt enjekte ettim.

Hata ayıklama

Görüntüye çift tıklamak, Windows kabuğundaki (diğer adıyla "explorer.exe"), gdiplus.dlladında bir işlevdeki hatayı hemen tetikleyecektir GpJpegDecoder::read_jpeg_marker().

Bu fonksiyon, resimdeki her bir işaretçi için çağrılır, basitçe: işaretleyici segment boyutunu okur, uzunluğu segment boyutu olan bir tampon tahsis eder ve segmentin içeriğini bu yeni tahsis edilmiş tampon belleğe kopyalar.

İşte fonksiyonun başlangıcı:

.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

eaxkayıt, segment boyutunu gösterir ve edigörüntüde kalan bayt sayısıdır.

Kod daha sonra en önemli bayttan başlayarak segment boyutunu okumaya devam eder (uzunluk 16 bitlik bir değerdir):

.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

Ve en önemsiz bayt:

.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

Bu yapıldıktan sonra, bu hesaplamayı takiben segment boyutu bir tampon ayırmak için kullanılır:

tahsis_boyutu = segment_boyutu + 2

Bu, aşağıdaki kodla yapılır:

.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)

Bizim durumumuzda, segment boyutu 0 olduğundan, tampon için ayrılan boyut 2 bayttır .

Güvenlik açığı, ayırmadan hemen sonra:

.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, segment_size boyutunu (segment uzunluğu 2 baytlık bir değerdir) tüm segment boyutundan (bizim durumumuzda 0) çıkarır ve bir tamsayı yetersizliği ile son bulur: 0 - 2 = 0xFFFFFFFE

Kod daha sonra görüntüde ayrıştırılacak bayt olup olmadığını kontrol eder (bu doğrudur) ve ardından kopyaya atlar:

.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

Yukarıdaki kod parçası, kopya boyutunun 0xFFFFFFFE 32 bitlik parçalar olduğunu gösterir. Kaynak arabellek kontrol edilir (resmin içeriği) ve hedef yığın üzerinde bir arabellektir.

Yazma koşulu

Kopyalama, bellek sayfasının sonuna ulaştığında bir erişim ihlali (AV) istisnasını tetikleyecektir (bu, kaynak işaretçisinden veya hedef işaretçisinden olabilir). AV tetiklendiğinde, eşlenmemiş bir sayfayla karşılaşılıncaya kadar kopya zaten tüm sonraki yığın bloklarının üzerine yazdığı için yığın zaten savunmasız bir durumdadır.

Bu hatayı istismar edilebilir kılan şey, 3 SEH'nin (Yapılandırılmış İstisna İşleyicisi; bu, düşük seviyede denemek / hariç) kodun bu bölümünde istisnaları yakalıyor olmasıdır. Daha doğrusu, 1. SEH yığını çözer, böylece başka bir JPEG işaretleyicisini ayrıştırmak için geri döner, böylece istisnayı tetikleyen işaretleyiciyi tamamen atlar.

SEH olmasaydı kod tüm programı çökertebilirdi. Dolayısıyla kod, COM segmentini atlar ve başka bir segmenti ayrıştırır. Dolayısıyla GpJpegDecoder::read_jpeg_marker(), yeni bir segmente geri dönüyoruz ve kod yeni bir tampon ayırdığında:

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

Sistem, ücretsiz listeden bir bloğun bağlantısını kaldıracaktır. Görüntünün içeriği meta veri yapılarının üzerine yazılır; bu yüzden bağlantıyı kesmeyi kontrollü meta verilerle kontrol ediyoruz. Yığın yöneticisindeki sistemde (ntdll) bir yerde aşağıdaki kod:

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!!!

Artık istediğimizi, istediğimiz yere yazabiliriz ...


3

GDI'deki kodu bilmediğim için, aşağıda sadece spekülasyon var.

Pekala, aklıma gelen bir şey, bazı işletim sistemlerinde fark ettiğim davranışlardan biri (Windows XP'de buna sahip olup olmadığını bilmiyorum), yeni / ile ayırma yaparken malloc, aslında RAM'inizden daha fazlasını ayırabilirsiniz. o anıya yazmazsın.

Bu aslında linux çekirdeğinin bir davranışıdır.

Www.kernel.org adresinden:

İşlem doğrusal adres alanındaki sayfaların bellekte yerleşik olması gerekmez. Örneğin, alan vm_area_struct içinde rezerve edildiğinden, bir süreç adına yapılan tahsisler hemen karşılanmaz.

Yerleşik belleğe girmek için bir sayfa hatası tetiklenmelidir.

Temel olarak, sisteme gerçekten tahsis edilmeden önce belleği kirletmeniz gerekir:

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

Bazen RAM'de gerçek bir ayırma yapmaz (programınız yine de 4 GB kullanmaz). Bu davranışı bir Linux'ta gördüğümü biliyorum, ancak şimdi Windows 7 kurulumumda kopyalayamıyorum.

Bu davranıştan başlayarak aşağıdaki senaryo mümkündür.

Bu hafızanın RAM'de mevcut olmasını sağlamak için, onu kirletmeniz gerekir (temelde memset veya başka bir şey ona yazın):

  memset(comment, 0, size);

Ancak güvenlik açığı, bir ayırma hatasını değil, arabellek taşmasını kullanır.

Başka bir deyişle, buna sahip olsaydım:

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

Bu, arabellekten sonra yazmaya yol açar, çünkü 4 GB sürekli bellek bölümü diye bir şey yoktur.

4 GB belleğin tamamını kirletmek için p'ye hiçbir şey koymadınız ve belleği memcpytek seferde mi yoksa sadece sayfa sayfa mı kirlettiğini bilmiyorum (sayfa sayfa olduğunu düşünüyorum).

Sonunda, yığın çerçevesinin (Stack Buffer Overflow) üzerine yazılır.

Bir başka olası güvenlik açığı, resmin bir bayt dizisi olarak bellekte tutulması (tüm dosyayı arabelleğe okuma) ve yorumların boyutunun yalnızca hayati olmayan bilgileri atlamak için kullanılmasıydı.

Örneğin

     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

Bahsettiğiniz gibi, GDI bu boyutu ayırmadıysa, program asla çökmeyecektir.


4
Bu, 4GB'nin önemli olmadığı 64 bitlik bir sistemle olabilir (adres alanı hakkında konuşursak). Ancak 32 bitlik bir sistemde (onlar da savunmasız görünüyorlar) 4GB adres alanı ayıramazsınız, çünkü mevcut olan bu kadar! Yani malloc(-1U)kesinlikle başarısız olacak, geri dönecek NULLve memcpy()çökecek.
rodrigo

9
Bu satırın doğru olduğunu sanmıyorum: "Sonunda başka bir işlem adresine yazılacak." Normalde bir işlem diğerinin belleğine erişemez. MMU Avantajlarına bakın .
chue x

@MMU Faydaları evet, haklısınız. Bunun normal yığın sınırlarını aşacağını ve yığın çerçevesinin üzerine yazmaya başlayacağını söylemem gerekiyordu. Cevabımı düzenleyeceğim, işaret ettiğiniz için teşekkürler.
MichaelCMS
Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.