Lỗ hổng JPEG of Death hoạt động như thế nào?


94

Tôi đã đọc về một cách khai thác cũ hơn chống lại GDI + trên Windows XP và Windows Server 2003 được gọi là JPEG của cái chết cho một dự án mà tôi đang thực hiện.

Việc khai thác được giải thích rõ ràng trong liên kết sau: http://www.infosecwriters.com/text_resources/pdf/JPEG.pdf

Về cơ bản, tệp JPEG chứa một phần được gọi là COM chứa trường nhận xét (có thể trống) và giá trị hai byte chứa kích thước của COM. Nếu không có nhận xét nào, kích thước là 2. Trình đọc (GDI +) đọc kích thước, trừ hai và phân bổ một bộ đệm có kích thước thích hợp để sao chép các nhận xét trong đống. Cuộc tấn công liên quan đến việc đặt một giá trị 0trong trường. GDI + trừ đi 2, dẫn đến một giá trị -2 (0xFFFe)được chuyển đổi thành số nguyên không dấu 0XFFFFFFFEbởi memcpy.

Mã mẫu:

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

Quan sát rằng malloc(0)trên dòng thứ ba sẽ trả về một con trỏ đến bộ nhớ chưa được phân bổ trên heap. Làm thế nào để viết 0XFFFFFFFEbyte ( 4GB!!!!) có thể không làm hỏng chương trình? Điều này có ghi ra ngoài vùng heap và vào không gian của các chương trình khác và hệ điều hành không? Điều gì xảy ra sau đó?

Theo tôi hiểu memcpy, nó chỉ đơn giản là sao chép các nký tự từ đích đến nguồn. Trong trường hợp này, nguồn phải nằm trên ngăn xếp, đích trên đống, và n4GB.


malloc sẽ cấp phát bộ nhớ từ heap. tôi nghĩ rằng vấn đề khai thác đã được thực hiện trước khi memcpy và sau khi bộ nhớ được phân bổ
iedoc

chỉ là một lưu ý phụ: đó không phải là memcpy thứ thúc đẩy giá trị thành một số nguyên không dấu (4 byte), mà là phép trừ.
rev

1
Cập nhật câu trả lời trước của tôi với một ví dụ trực tiếp. Các mallockích thước ed là chỉ có 2 byte hơn 0xFFFFFFFE. Kích thước khổng lồ này chỉ được sử dụng cho kích thước bản sao, không cho kích thước phân bổ.
Neitsa

Câu trả lời:


96

Lỗ hổng này chắc chắn là một sự cố tràn đống .

Làm cách nào để ghi 0XFFFFFFFE byte (4 GB !!!!) có thể không làm hỏng chương trình?

Nó có thể sẽ xảy ra, nhưng trong một số trường hợp, bạn có thời gian để khai thác trước khi sự cố xảy ra (đôi khi, bạn có thể đưa chương trình trở lại hoạt động bình thường và tránh sự cố).

Khi memcpy () khởi động, bản sao sẽ ghi đè lên một số khối heap khác hoặc một số phần của cấu trúc quản lý heap (ví dụ: danh sách rảnh, danh sách bận, v.v.).

Tại một số điểm, bản sao sẽ gặp phải một trang không được phân bổ và kích hoạt AV (Vi phạm Truy cập) khi ghi. GDI + sau đó sẽ cố gắng phân bổ một khối mới trong heap (xem ntdll! RtlAllocateHeap ) ... nhưng các cấu trúc heap giờ đã lộn xộn.

Tại thời điểm đó, bằng cách tạo ảnh JPEG cẩn thận, bạn có thể ghi đè cấu trúc quản lý đống bằng dữ liệu được kiểm soát. Khi hệ thống cố gắng phân bổ khối mới, nó có thể sẽ hủy liên kết một khối (miễn phí) khỏi danh sách miễn phí.

Khối được quản lý bằng (đặc biệt là) con trỏ nháy (Liên kết chuyển tiếp; khối tiếp theo trong danh sách) và nháy (Liên kết lùi; khối trước đó trong danh sách). Nếu bạn kiểm soát cả nhấp nháy và nhấp nháy, bạn có thể có một WRITE4 khả thi (viết điều kiện What / Where), nơi bạn kiểm soát những gì bạn có thể viết và nơi bạn có thể viết.

Tại thời điểm đó, bạn có thể ghi đè một con trỏ hàm (con trỏ SEH [Structured Exception Handlers] là mục tiêu được lựa chọn vào thời điểm đó vào năm 2004) và thực thi mã.

Xem bài đăng trên blog Heap Corrupt: A Case Study .

Lưu ý: mặc dù tôi đã viết về việc khai thác bằng cách sử dụng tác giả tự do, kẻ tấn công có thể chọn một con đường khác bằng cách sử dụng siêu dữ liệu heap khác ("siêu dữ liệu heap" là cấu trúc được hệ thống sử dụng để quản lý heap; flink và flash là một phần của siêu dữ liệu heap), nhưng khai thác hủy liên kết có lẽ là "dễ nhất". Một tìm kiếm trên google cho "khai thác đống" sẽ trả lại nhiều nghiên cứu về điều này.

Điều này có ghi ra ngoài vùng heap và vào không gian của các chương trình khác và hệ điều hành không?

Không bao giờ. Hệ điều hành hiện đại dựa trên khái niệm không gian địa chỉ ảo nên mỗi tiến trình trên đều có không gian địa chỉ ảo riêng cho phép giải quyết tối đa 4 gigabyte bộ nhớ trên hệ thống 32 bit (trong thực tế, bạn chỉ có một nửa trong số đó trong vùng đất của người dùng, phần còn lại dành cho nhân).

Nói tóm lại, một tiến trình không thể truy cập bộ nhớ của một tiến trình khác (ngoại trừ nếu nó yêu cầu hạt nhân cho nó thông qua một số dịch vụ / API, nhưng hạt nhân sẽ kiểm tra xem người gọi có quyền làm như vậy hay không).


Tôi đã quyết định kiểm tra lỗ hổng này vào cuối tuần này, để chúng ta có thể biết rõ về những gì đang diễn ra hơn là suy đoán thuần túy. Lỗ hổng bảo mật hiện đã 10 năm tuổi, vì vậy tôi nghĩ rằng có thể viết về nó, mặc dù tôi chưa giải thích phần khai thác trong câu trả lời này.

Lập kế hoạch

Nhiệm vụ khó khăn nhất là tìm một Windows XP chỉ có SP1, như vào năm 2004 :)

Sau đó, tôi tải xuống một hình ảnh JPEG chỉ bao gồm một pixel duy nhất, như được hiển thị bên dưới (cắt cho ngắn gọn):

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

Ảnh JPEG bao gồm các điểm đánh dấu nhị phân (tạo ra các phân đoạn). Trong hình trên, FF D8là điểm đánh dấu SOI (Start Of Image), trong khi FF E0, ví dụ, là một điểm đánh dấu ứng dụng.

Tham số đầu tiên trong phân đoạn điểm đánh dấu (ngoại trừ một số điểm đánh dấu như SOI) là một tham số độ dài hai byte mã hóa số byte trong phân đoạn điểm đánh dấu, bao gồm tham số độ dài và loại trừ điểm đánh dấu hai byte.

Tôi chỉ cần thêm một điểm đánh dấu COM (0x FFFE) ngay sau SOI, vì các điểm đánh dấu không có thứ tự nghiêm ngặt.

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ài của đoạn COM được đặt 00 00để kích hoạt lỗ hổng bảo mật. Tôi cũng đã chèn các byte 0xFFFC ngay sau điểm đánh dấu COM với một mẫu lặp lại, số 4 byte trong hệ lục phân, điều này sẽ trở nên hữu ích khi "khai thác" lỗ hổng.

Gỡ lỗi

Nhấp đúp vào hình ảnh sẽ ngay lập tức kích hoạt lỗi trong Windows shell (còn gọi là "explorer.exe"), ở đâu đó gdiplus.dll, trong một hàm có tên GpJpegDecoder::read_jpeg_marker().

Hàm này được gọi cho mỗi điểm đánh dấu trong hình, nó chỉ đơn giản: đọc kích thước đoạn mã đánh dấu, cấp phát một bộ đệm có độ dài là kích thước đoạn và sao chép nội dung của đoạn vào bộ đệm mới được cấp phát này.

Đây là phần bắt đầu của hàm:

.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

eaxđăng ký trỏ đến kích thước phân đoạn và edilà số byte còn lại trong hình ảnh.

Sau đó, mã sẽ tiến hành đọc kích thước phân đoạn, bắt đầu bằng byte quan trọng nhất (độ dài là giá trị 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

Và byte ít quan trọng nhất:

.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

Khi điều này được thực hiện, kích thước phân đoạn được sử dụng để cấp phát bộ đệm, theo phép tính sau:

phân bổ_size = phân_khoảng + 2

Điều này được thực hiện bởi đoạn mã dưới đây:

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

Trong trường hợp của chúng tôi, vì kích thước phân đoạn là 0, kích thước được phân bổ cho bộ đệm là 2 byte .

Lỗ hổng bảo mật nằm ngay sau khi phân bổ:

.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

Mã chỉ cần trừ kích thước segment_size (chiều dài phân khúc là giá trị 2 byte) từ kích thước toàn bộ phân khúc (trong trường hợp của chúng tôi là 0) và kết thúc bằng một dòng số nguyên: 0 - 2 = 0xFFFFFFFE

Sau đó, mã kiểm tra xem có còn byte nào để phân tích cú pháp trong hình ảnh hay không (đúng), và sau đó chuyển đến bản sao:

.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

Đoạn mã trên cho thấy kích thước bản sao là 0xFFFFFFFE khối 32-bit. Bộ đệm nguồn được kiểm soát (nội dung của hình ảnh) và đích là bộ đệm trên heap.

Viết điều kiện

Bản sao sẽ kích hoạt ngoại lệ vi phạm quyền truy cập (AV) khi nó đến cuối trang bộ nhớ (điều này có thể là từ con trỏ nguồn hoặc con trỏ đích). Khi AV được kích hoạt, heap đã ở trạng thái dễ bị tấn công vì bản sao đã ghi đè lên tất cả các khối heap tiếp theo cho đến khi gặp phải trang không được ánh xạ.

Điều khiến lỗi này có thể khai thác được là 3 SEH (Trình xử lý ngoại lệ có cấu trúc; đây là thử / ngoại trừ ở cấp thấp) đang bắt các ngoại lệ trên phần này của mã. Chính xác hơn, SEH thứ nhất sẽ giải phóng ngăn xếp để nó quay trở lại phân tích cú pháp đánh dấu JPEG khác, do đó hoàn toàn bỏ qua điểm đánh dấu đã kích hoạt ngoại lệ.

Nếu không có SEH, mã sẽ bị hỏng toàn bộ chương trình. Vì vậy, mã bỏ qua phân đoạn COM và phân tích một phân đoạn khác. Vì vậy, chúng tôi quay lại GpJpegDecoder::read_jpeg_marker()với một phân đoạn mới và khi mã phân bổ bộ đệm mới:

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

Hệ thống sẽ hủy liên kết một khối khỏi danh sách miễn phí. Nó xảy ra rằng cấu trúc siêu dữ liệu đã bị ghi đè bởi nội dung của hình ảnh; vì vậy chúng tôi kiểm soát việc hủy liên kết bằng siêu dữ liệu được kiểm soát. Đoạn mã dưới đây ở một nơi nào đó trong hệ thống (ntdll) trong trình quản lý đống:

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

Bây giờ chúng tôi có thể viết những gì chúng tôi muốn, nơi chúng tôi muốn ...


3

Vì tôi không biết mã từ GDI, những gì bên dưới chỉ là suy đoán.

Chà, một điều xuất hiện trong tâm trí là một hành vi mà tôi đã nhận thấy trên một số hệ điều hành (tôi không biết Windows XP có điều này không) là khi phân bổ với new / malloc, bạn thực sự có thể phân bổ nhiều hơn RAM của mình, miễn là bạn không ghi vào bộ nhớ đó.

Đây thực sự là một hành vi của Kernel linux.

Từ www.kernel.org:

Các trang trong không gian địa chỉ tuyến tính của tiến trình không nhất thiết phải nằm trong bộ nhớ. Ví dụ: phân bổ được thực hiện thay mặt cho một quy trình sẽ không được thỏa mãn ngay lập tức vì không gian chỉ được dành riêng trong vm_area_struct.

Để đi vào bộ nhớ thường trú, một lỗi trang phải được kích hoạt.

Về cơ bản, bạn cần làm bẩn bộ nhớ trước khi nó thực sự được cấp phát trên hệ thống:

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

Đôi khi nó sẽ không thực sự phân bổ RAM thực sự (chương trình của bạn sẽ vẫn không sử dụng 4 GB). Tôi biết mình đã thấy hành vi này trên Linux, nhưng hiện tại tôi không thể sao chép nó trên bản cài đặt Windows 7 của mình.

Bắt đầu từ hành vi này, kịch bản sau có thể xảy ra.

Để làm cho bộ nhớ đó tồn tại trong RAM, bạn cần làm cho nó bẩn (về cơ bản là memset hoặc một số ghi vào nó):

  memset(comment, 0, size);

Tuy nhiên, lỗ hổng khai thác lỗi tràn bộ đệm, không phải là lỗi cấp phát.

Nói cách khác, nếu tôi muốn có điều này:

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

Điều này sẽ dẫn đến ghi sau khi bộ đệm, vì không có thứ gì gọi là phân đoạn bộ nhớ liên tục 4 GB.

Bạn đã không đặt bất cứ thứ gì vào p để làm bẩn toàn bộ 4 GB bộ nhớ và tôi không biết liệu có memcpylàm bẩn bộ nhớ cùng một lúc hay chỉ từng trang một (tôi nghĩ là từng trang).

Cuối cùng nó sẽ kết thúc việc ghi đè lên khung ngăn xếp (Tràn bộ đệm ngăn xếp).

Một lỗ hổng khác có thể xảy ra là nếu hình ảnh được lưu trong bộ nhớ dưới dạng một mảng byte (đọc toàn bộ tệp vào bộ đệm) và các nhận xét về kích thước được sử dụng chỉ để bỏ qua thông tin không quan trọng.

Ví dụ

     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

Như bạn đã đề cập, nếu GDI không phân bổ kích thước đó, chương trình sẽ không bao giờ bị lỗi.


4
Đó có thể là với hệ thống 64 bit, trong đó 4GB không phải là vấn đề lớn (nói về không gian bổ sung). Nhưng trong hệ thống 32-bit, (chúng cũng có vẻ dễ bị tấn công) bạn không thể dự trữ 4GB không gian địa chỉ, bởi vì đó là tất cả! Vì vậy, a malloc(-1U)chắc chắn sẽ thất bại, quay trở lại NULLmemcpy()sẽ sụp đổ.
rodrigo

9
Tôi không nghĩ dòng này đúng: "Cuối cùng thì nó sẽ được ghi vào một địa chỉ quy trình khác." Thông thường một tiến trình không thể truy cập bộ nhớ của tiến trình khác. Xem Lợi ích của MMU .
chue x

@MMU Lợi ích có, bạn đúng. Tôi muốn nói rằng điều đó sẽ vượt qua ranh giới heap thông thường và bắt đầu ghi đè khung ngăn xếp. Tôi sẽ chỉnh sửa câu trả lời của mình, cảm ơn vì đã chỉ ra nó.
MichaelCMS
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.