Bagaimana JPEG kerentanan kematian beroperasi?

94

Saya telah membaca tentang eksploitasi lama terhadap GDI + di Windows XP dan Windows Server 2003 yang disebut JPEG kematian untuk proyek yang saya kerjakan.

Eksploitasi dijelaskan dengan baik di tautan berikut: http://www.infosecwriters.com/text_resources/pdf/JPEG.pdf

Pada dasarnya, file JPEG berisi bagian yang disebut COM yang berisi kolom komentar (mungkin kosong), dan nilai dua byte yang berisi ukuran COM. Jika tidak ada komentar, ukurannya 2. Pembaca (GDI +) membaca ukuran, mengurangi dua, dan mengalokasikan buffer dengan ukuran yang sesuai untuk menyalin komentar di heap. Serangan itu melibatkan penempatan nilai 0di lapangan. GDI + mengurangi 2, yang mengarah ke nilai -2 (0xFFFe)yang akan dikonversi ke bilangan bulat unsigned 0XFFFFFFFEoleh memcpy.

Kode sampel:

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

Perhatikan bahwa malloc(0)di baris ketiga harus mengembalikan pointer ke memori yang tidak terisi di heap. Bagaimana menulis 0XFFFFFFFEbyte ( 4GB!!!!) mungkin tidak merusak program? Apakah ini menulis di luar area heap dan ke dalam ruang program dan OS lain? Lalu apa yang terjadi?

Seperti yang saya pahami memcpy, itu hanya menyalin nkarakter dari tujuan ke sumber. Dalam kasus ini, sumber harus ada di tumpukan, tujuan di heap, dan nadalah 4GB.

Rafa
sumber
malloc akan mengalokasikan memori dari heap. saya pikir eksploitasi dilakukan sebelum memcpy dan setelah memori dialokasikan
iedoc
hanya sebagai catatan tambahan: bukan memcpy yang mempromosikan nilai menjadi integer unsigned (4 byte), melainkan pengurangan.
rev
1
Memperbarui jawaban saya sebelumnya dengan contoh langsung. The mallocukuran ed hanya 2 byte bukan 0xFFFFFFFE. Ukuran yang sangat besar ini hanya digunakan untuk ukuran salinan, bukan untuk ukuran alokasi.
Neitsa

Jawaban:

96

Kerentanan ini jelas merupakan luapan tumpukan .

Bagaimana menulis 0XFFFFFFFE byte (4 GB !!!!) mungkin tidak merusak program?

Mungkin bisa, tetapi pada beberapa kesempatan Anda punya waktu untuk mengeksploitasi sebelum crash terjadi (terkadang, Anda bisa mengembalikan program ke eksekusi normalnya dan menghindari crash).

Saat memcpy () dimulai, salinan akan menimpa beberapa blok heap lain atau beberapa bagian dari struktur manajemen heap (mis. Daftar bebas, daftar sibuk, dll.).

Pada titik tertentu salinan akan menemukan halaman yang tidak dialokasikan dan memicu AV (Access Violation) saat menulis. GDI + kemudian akan mencoba mengalokasikan blok baru di heap (lihat ntdll! RtlAllocateHeap ) ... tetapi struktur heap sekarang semuanya kacau.

Pada titik itu, dengan menyusun gambar JPEG Anda secara hati-hati, Anda dapat menimpa struktur manajemen heap dengan data yang terkontrol. Ketika sistem mencoba mengalokasikan blok baru, sistem mungkin akan memutuskan tautan blok (gratis) dari daftar gratis.

Blok dikelola dengan (terutama) flink (Tautan maju; blok berikutnya dalam daftar) dan berkedip (Tautan mundur; blok sebelumnya dalam daftar) penunjuk. Jika Anda mengontrol kedipan dan kedip, Anda mungkin memiliki kemungkinan WRITE4 (tulis kondisi Apa / Di mana) di mana Anda mengontrol apa yang dapat Anda tulis dan di mana Anda dapat menulis.

Pada titik itu Anda dapat menimpa pointer fungsi (pointer SEH [Structured Exception Handlers] adalah target pilihan pada waktu itu di tahun 2004) dan mendapatkan eksekusi kode.

Lihat entri blog Korupsi Tumpukan: Studi Kasus .

Catatan: meskipun saya menulis tentang eksploitasi menggunakan freelist, penyerang mungkin memilih jalur lain menggunakan metadata heap lain ("metadata heap" adalah struktur yang digunakan oleh sistem untuk mengelola heap; flink dan blink adalah bagian dari metadata heap), tetapi eksploitasi unlink mungkin adalah yang "termudah". Pencarian google untuk "eksploitasi heap" akan menghasilkan banyak penelitian tentang ini.

Apakah ini menulis di luar area heap dan ke dalam ruang program dan OS lain?

Tidak pernah. OS modern didasarkan pada konsep ruang alamat virtual sehingga setiap proses memiliki ruang alamat virtualnya sendiri yang memungkinkan pengalamatan hingga 4 gigabyte memori pada sistem 32-bit (dalam praktiknya Anda hanya mendapatkan setengahnya di lahan pengguna, sisanya untuk kernel).

Singkatnya, sebuah proses tidak dapat mengakses memori dari proses lain (kecuali jika ia meminta kernel untuk itu melalui beberapa layanan / API, tetapi kernel akan memeriksa apakah pemanggil berhak melakukannya).


Saya memutuskan untuk menguji kerentanan ini akhir minggu ini, jadi kami bisa mendapatkan ide bagus tentang apa yang sedang terjadi daripada spekulasi murni. Kerentanannya sekarang sudah 10 tahun, jadi saya pikir tidak masalah untuk menulis tentang itu, meskipun saya belum menjelaskan bagian eksploitasi dalam jawaban ini.

Perencanaan

Tugas tersulit adalah menemukan Windows XP dengan hanya SP1, seperti pada tahun 2004 :)

Kemudian, saya mengunduh gambar JPEG yang hanya terdiri dari satu piksel, seperti yang ditunjukkan di bawah ini (dipotong agar singkat):

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

Gambar JPEG terdiri dari penanda biner (yang menghasilkan segmen). Pada gambar di atas, FF D8adalah marker SOI (Start Of Image), sedangkan FF E0, misalnya, adalah marker aplikasi.

Parameter pertama dalam segmen penanda (kecuali beberapa penanda seperti SOI) adalah parameter panjang dua byte yang mengkodekan jumlah byte dalam segmen penanda, termasuk parameter panjang dan tidak termasuk penanda dua byte.

Saya hanya menambahkan penanda COM (0x FFFE) tepat setelah SOI, karena penanda tidak memiliki urutan yang ketat.

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

Panjang segmen COM diatur 00 00untuk memicu kerentanan. Saya juga menyuntikkan byte 0xFFFC tepat setelah penanda COM dengan pola berulang, angka 4 byte dalam hex, yang akan berguna saat "mengeksploitasi" kerentanan.

Debugging

Mengklik ganda gambar tersebut akan segera memicu bug di shell Windows (alias "explorer.exe"), di suatu tempat di gdiplus.dll, dalam fungsi bernama GpJpegDecoder::read_jpeg_marker().

Fungsi ini dipanggil untuk setiap penanda dalam gambar, cukup: membaca ukuran segmen penanda, mengalokasikan buffer yang panjangnya adalah ukuran segmen dan menyalin konten segmen ke buffer yang baru dialokasikan ini.

Di sini awal fungsinya:

.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 menunjuk ke ukuran segmen dan edimerupakan jumlah byte yang tersisa pada gambar.

Kode kemudian melanjutkan untuk membaca ukuran segmen, dimulai dengan byte paling signifikan (panjangnya adalah nilai 16-bit):

.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

Dan byte paling signifikan:

.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

Setelah ini selesai, ukuran segmen digunakan untuk mengalokasikan buffer, mengikuti perhitungan ini:

alokasi_ukuran = ukuran_segmentasi + 2

Ini dilakukan dengan kode di bawah ini:

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

Dalam kasus kami, karena ukuran segmen adalah 0, ukuran yang dialokasikan untuk buffer adalah 2 byte .

Kerentanan tepat setelah alokasi:

.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

Kode tersebut hanya mengurangi ukuran_segmen (panjang segmen adalah nilai 2 byte) dari seluruh ukuran segmen (0 dalam kasus kami) dan berakhir dengan kekurangan bilangan bulat: 0-2 = 0xFFFFFFFE

Kode kemudian memeriksa apakah ada byte yang tersisa untuk diurai dalam gambar (yang benar), dan kemudian melompat ke salinan:

.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

Potongan di atas menunjukkan bahwa ukuran salinan adalah potongan 0xFFFFFFFE 32-bit. Buffer sumber dikontrol (konten gambar) dan tujuannya adalah buffer di heap.

Tulis kondisi

Salinan akan memicu pengecualian pelanggaran akses (AV) ketika mencapai akhir halaman memori (ini bisa dari penunjuk sumber atau penunjuk tujuan). Saat AV dipicu, heap sudah berada dalam status rentan karena salinan telah menimpa semua blok heap berikut hingga halaman yang tidak dipetakan ditemukan.

Apa yang membuat bug ini dapat dieksploitasi adalah bahwa 3 SEH (Structured Exception Handler; ini adalah percobaan / kecuali pada level rendah) menangkap pengecualian pada bagian kode ini. Lebih tepatnya, SEH pertama akan melepas tumpukan sehingga kembali mengurai marker JPEG lain, sehingga sepenuhnya melewatkan marker yang memicu pengecualian.

Tanpa SEH kode hanya akan membuat crash seluruh program. Jadi kode melewatkan segmen COM dan mem-parsing segmen lain. Jadi kita kembali ke GpJpegDecoder::read_jpeg_marker()segmen baru dan ketika kode mengalokasikan buffer baru:

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

Sistem akan memutuskan tautan blok dari daftar gratis. Kebetulan struktur metadata ditimpa oleh konten gambar; jadi kami mengontrol pemutusan tautan dengan metadata terkontrol. Kode di bawah ini di suatu tempat di sistem (ntdll) di manajer heap:

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

Sekarang kita bisa menulis apa yang kita mau, dimana kita mau ...

Neitsa
sumber
3

Karena saya tidak tahu kode dari GDI, yang di bawah ini hanyalah spekulasi.

Nah, satu hal yang muncul dalam pikiran adalah salah satu perilaku yang saya perhatikan pada beberapa OS (saya tidak tahu apakah Windows XP memiliki ini) adalah ketika mengalokasikan dengan yang baru / malloc, Anda benar-benar dapat mengalokasikan lebih dari RAM Anda, asalkan Anda tidak menulis ke memori itu.

Ini sebenarnya adalah perilaku kernel linux.

Dari www.kernel.org:

Halaman dalam ruang proses alamat linier tidak selalu tersimpan dalam memori. Misalnya, alokasi yang dibuat atas nama suatu proses tidak langsung dipenuhi karena ruang hanya dicadangkan dalam vm_area_struct.

Untuk masuk ke memori penduduk, kesalahan halaman harus dipicu.

Pada dasarnya Anda perlu membuat memori kotor sebelum benar-benar dialokasikan pada sistem:

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

Terkadang itu tidak benar-benar membuat alokasi nyata dalam RAM (program Anda masih tidak akan menggunakan 4 GB). Saya tahu saya telah melihat perilaku ini di Linux, tetapi saya tidak dapat menirunya sekarang pada instalasi Windows 7 saya.

Mulai dari perilaku ini skenario berikut ini mungkin terjadi.

Untuk membuat memori itu ada di RAM, Anda harus membuatnya kotor (pada dasarnya memset atau menulis ke dalamnya):

  memset(comment, 0, size);

Namun kerentanan mengeksploitasi buffer overflow, bukan kegagalan alokasi.

Dengan kata lain, jika saya memiliki ini:

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

Ini akan menyebabkan penulisan setelah buffer, karena tidak ada yang namanya segmen memori berkelanjutan 4 GB.

Anda tidak memasukkan apapun ke dalam p untuk membuat seluruh 4 GB memori kotor, dan saya tidak tahu apakah memcpymembuat memori kotor sekaligus, atau hanya halaman demi halaman (saya pikir itu halaman demi halaman).

Akhirnya itu akan berakhir menimpa bingkai tumpukan (Stack Buffer Overflow).

Kerentanan lain yang lebih mungkin adalah jika gambar disimpan dalam memori sebagai array byte (baca seluruh file ke dalam buffer), dan ukuran komentar digunakan hanya untuk melewati informasi non-vital.

Sebagai contoh

     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

Seperti yang Anda sebutkan, jika GDI tidak mengalokasikan ukuran tersebut, program tidak akan pernah macet.

MichaelCMS
sumber
4
Itu bisa jadi dengan sistem 64-bit, di mana 4GB bukanlah masalah besar (berbicara tentang ruang tambahan). Tetapi dalam sistem 32-bit, (tampaknya juga rentan) Anda tidak dapat memesan 4GB ruang alamat, karena hanya itu yang ada! Jadi malloc(-1U)pasti akan gagal, kembali NULLdan memcpy()akan hancur.
rodrigo
9
Saya tidak berpikir baris ini benar: "Akhirnya akan berakhir dengan menulis ke alamat proses lain." Biasanya satu proses tidak dapat mengakses memori yang lain. Lihat Manfaat MMU .
chue x
Manfaat @MMU ya, Anda benar. Saya dimaksudkan untuk mengatakan bahwa itu akan melewati batas tumpukan normal dan mulai menimpa bingkai tumpukan. Saya akan mengedit jawaban saya, terima kasih telah menunjukkannya.
MichaelCMS